腾讯CSIG面试题目总结
1.MySQL高可用如何做?链接
- 主从同步:网络波动等一些客观原因,导致半同步复制发生超时而切换为异步复制,那么这时便不能保证数据的一致性。所以尽可能的保证半同步复制,便可提高数据的一致性。
- binlog文件服务器:搭建两条半同步复制通道,其中连接文件服务器的半同步通道正常情况下不启用,当主从的半同步复制发生网络问题后,启动与文件服务器的半同步复制通道。当主从半同步复制恢复后,关闭与文件服务器的半同步复制通道。
- 一主多从或者多主多从的集群。MHA Manager会定时探测集群中的master节点,当master出现故障时,它可以自动将最新数据的slave提升为新的master,然后将所有其他的slave重新指向新的master,整个故障转移过程对应用程序完全透明。MHA Node运行在每台MySQL服务器上,主要作用是切换时处理二进制日志,确保切换尽量少丢数据。
PS:什么是脑裂?由于某些原因,导致两台keepalived高可用服务器在指定时间内,无法检测到对方的心跳,各自取得资源及服务的所有权,而此时的两台高可用服务器又都还活着。 - zookeeper+proxy:Zookeeper使用分布式算法保证集群数据的一致性,使用zookeeper可以有效的保证proxy的高可用性,可以较好的避免网络分区现象的产生。
- MySQL cluster:是官方集群的部署方案,通过使用NDB存储引擎实时备份冗余数据,实现数据库的高可用性和数据一致性。
2.高级的单例模式 链接
- 双重检查懒汉式 (可用,推荐)
这种写法用了两个if判断,也就是Double-Check,并且同步的不是方法,而是代码块,效率较高,是对第三种写法的改进。为什么要做两次判断呢?这是为了线程安全考虑,还是那个场景,对象还没实例化,两个线程A和B同时访问静态方法并同时运行到第一个if判断语句,这时线程A先进入同步代码块中实例化对象,结束之后线程B也进入同步代码块,如果没有第二个if判断语句,那么线程B也同样会执行实例化对象的操作了
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
注意上面的volatile关键字(点击查看链接):
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
- 静态内部类 (可用,推荐)
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
这是很多开发者推荐的一种写法,这种静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成对象的实例化。
同时,因为类的静态属性只会在第一次加载类的时候初始化,也就保证了SingletonInstance中的对象只会被实例化一次,并且这个过程也是线程安全的。有点类似JPA下Pageable的Sort类,只能通过单例模式调用Sort方法,无法通过private的构造方法获得.
3.Redis的持久化 详细链接
-简介
Redis的所有数据都是保存在内存中,然后不定期的通过异步方式保存到磁盘上(这称为“半持久化模式”);也可以把每一次数据变化都写入到一个append only file(aof)里面(这称为“全持久化模式”)。
二者区别:
- AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
- RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
- 二者优缺点比较
RDB存在哪些优势呢?
1). 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。
2). 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。
3). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。
4). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。
RDB又存在哪些劣势呢?
1). 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
AOF的优势有哪些呢?
1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。
2). 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。
3). 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。
4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
AOF的劣势有哪些呢?
1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。
如何选择
二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。
4.Redis为什么是单线程?redis面试题
单线程效率比多线程效率高,因为多线程会频繁上下文切换,会导致额外的开销。
5.redis集群详解
- 主从模式:一个master可以拥有多个slave,但是一个slave只能对应一个master.master挂了以后,不影响slave的读,但redis不再提供写服务,master重启后redis将重新对外提供写服务,master挂了以后,不会在slave节点中重新选一个master
- Sentinel模式:哨兵 sentinel以每秒钟一次的频率向它所知的master,slave以及其他sentinel实例发送一个 PING 命令 ,当master挂了以后,sentinel会在slave中选择一个做为master
- Cluster模式 :cluster模式的出现就是为了解决单机Redis容量有限的问题
6.Nginx负载均衡的方式有哪些?
nginx负载均衡的三种方式主要是轮询模式、weight权重模式、ip_hash。
当一台服务器的单位时间内的访问量越大时,服务器压力就越大,大到超过自身承受能力时,服务器就会崩溃。为了避免服务器崩溃,让用户有更好的体验,我们通过负载均衡的方式来分担服务器压力。
- 轮询模式(默认)
每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除,可以设置备份服务器,当服务挂了,可以自动启用备份服务器,备份服务器平时是不会收到请求。
适合服务器配置相当,无状态且短平快的服务使用。也适用于图片服务器集群和纯静态页面服务器集群。 - weight权重模式
这种方式比较灵活,当后端服务器性能存在差异的时候,通过配置权重,可以让服务器的性能得到充分发挥,有效利用资源。weight和访问比率成正比,用于后端服务器性能不均的情况。权重越高,在被访问的概率越大 - ip_hash
上述weight权重模式方式存在一个问题,在负载均衡系统中,假如用户在某台服务器上登录了,那么该用户第二次请求的时候,因为我们是负载均衡系统,每次请求都会重新定位到服务器集群中的某一个,那么已经登录某一个服务器的用户再重新定位到另一个服务器,其登录信息将会丢失,这样显然是不妥的。
可以采用ip_hash指令解决这个问题,如果客户已经访问了某个服务器,当用户再次访问时,会将该请求通过哈希算法,自动定位到该服务器。
每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session不能跨服务器的问题。但HASH会导致大量请求同时访问某一台服务器,其他服务器摸鱼.
7.XSS及CSRF攻击防御
XSS (Cross Site Scripting),即跨站脚本攻击,是一种常见于 Web 应用中的计算机安全漏洞。恶意攻击者往 Web 页面里嵌入恶意的客户端脚本,当用户浏览此网页时,脚本就会在用户的浏览器上执行,进而达到攻击者的目的。比如获取用户的 Cookie、导航到恶意网站、携带木马等。借助安全圈里面非常有名的一句话:所有的输入都是有害的。
CSRF(Cross-site request forgery,跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF;
CSRF攻击攻击原理及过程如下:
1. 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
2.在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
2. 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
3. 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
4. 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。
8.HashMap底层实现原理
HashMap中的put()和get()的实现原理:
- map.put(k,v)实现原理
(1)首先将k,v封装到Node对象当中(节点)。
(2)然后它的底层会调用K的hashCode()方法得出hash值。
(3)通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。没有相同K的,那么这个新的节点将被添加到链表的末尾(头插法)。如其中有一个相同K,那么这个节点的value将会被覆盖。
附录:HashMap线程不安全原因
- 多线程时候resize扩容,会导致形成循环链表,get时候会死循环
- 线程A 和线程B 共同对同一个HashMap进行PUT操作,发生哈希碰撞,此时HashMap按照平时的做法是形成一个链表(若超过八个节点则是红黑树),现在我们插入的下标为null(Table[i]==null)则进行正常的插入,此时线程A进行到了这一步正准备插入,这时候线程A堵塞,线程B获得运行时间,进行同样操作,也是Table[i]==null , 此时它直接运行完整个PUT方法,成功将元素插入. 随后线程A获得运行时间接上上面的判断继续运行,进行了Table[i]==null的插入(此时其实应该是Table[i]!=null的操作,因为前面线程B已经插入了一个元素了),这样就会直接把原来线程B插入的数据直接覆盖了,如此一来就造成了线程不安全问题.
- 就是大部分并发都存在的问题吧,临界区资源没有上锁会导致脏读、丢失更新什么的。
就两个线程同时put同一个key,那有一个线程的value就丢失更新了。或者某个线程同时get了某个key,这个key又被其他线程修改了,脏读了。
-
map.get(k)实现原理
(1)先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
(2)通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有的比较结果都为false,即找不到,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。 -
为何随机增删、查询效率都很高的原因是?
原因: 增删是在链表上完成的,而查询只需扫描部分,则效率高。
HashMap集合的key,会先后调用两个方法,hashCode and equals方法,这这两个方法都需要重写。 -
为什么放在hashMap集合key部分的元素需要重写equals方法?
因为equals方法默认比较的是两个对象的内存地址 -
HashMap红黑树原理分析
相比 jdk1.7 的 HashMap 而言,jdk1.8最重要的就是引入了红黑树的设计,当hash表的单一链表长度超过 8 个的时候,链表结构就会转为红黑树结构。
为什么要这样设计呢?好处就是避免在最极端的情况下链表变得很长很长,在查询的时候,效率会非常慢。
红黑树查询:其访问性能近似于折半查找,时间复杂度 O(logn);
链表查询:这种情况下,需要遍历全部元素才行,时间复杂度 O(n);
简单的说,红黑树是一种近似平衡的二叉查找树,其主要的优点就是“平衡“,即左右子树高度几乎一致,以此来防止树退化为链表,通过这种方式来保障查找的时间复杂度为 log(n)。
附录:什么是红黑树
关于红黑树的内容,网上给出的内容非常多,主要有以下几个特性:
1、每个节点要么是红色,要么是黑色,但根节点永远是黑色的;
2、每个红色节点的两个子节点一定都是黑色;
3、红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色);
4、从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;
5、所有的叶节点都是是黑色的(注意这里说叶子节点其实是上图中的 NIL 节点);
在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件 3 或条件 4,需要通过调整使得查找树重新满足红黑树的条件。
9.刁钻问题:为什么HashMap负载因子(loadFactor)为0.75?
为什么是0.75? 这个我没找到一个肯定完整的答案,这个问题比较偏,问到的几率很小。但经过探讨,你可以回答
- 取 0.5和1 的缺点是什么, 同时说一下上面第一段引用的JDK API,就说API中也没有说为什么取0.75.
- 对方如果是个杠精,你可以深入理解一下stackoverflow中的这个计算,然后告诉他。 如果对方考你高数……我也没办法了,
- 一个bucket空和非空的概率为0.5,通过牛顿二项式等数学计算,得到这个loadfactor的值为
log(2)
,约等于0.693. 同回答者所说,可能小于0.75 大于等于log(2)的factor都能提供更好的性能
英文解释 :
Actually, from my calculations, the “perfect” load factor is closer to log 2 (~ 0.7). Although any load factor less than this will yield better performance. I think that .75 was probably pulled out of a hat.
Proof:
Chaining can be avoided and branch prediction exploited by predicting if a bucket is empty or not. A bucket is probably empty if the probability of it being empty exceeds .5.
Let s represent the size and n the number of keys added. Using the binomial theorem, the probability of a bucket being empty is:
P(0) = C(n, 0) * (1/s)^0 * (1 - 1/s)^(n - 0)
Thus, a bucket is probably empty if there are less than
log(2)/log(s/(s - 1)) keys
As s reaches infinity and if the number of keys added is such that P(0) = .5, then n/s approaches log(2) rapidly:
lim (log(2)/log(s/(s - 1)))/s as s -> infinity = log(2) ~ 0.693...
10. ConcurrentHashMap原理
下面我们来谈一下为什么要使用ConcurrentHashMap。
在并发编程中使用HashMap可能导致程序死循环。而使用线程安全的HashTable效率又非常低下,基于以上两个原因,便有了ConcurrentHashMap的登场机会
1)线程不安全的HashMap
在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。
2)效率低下的HashTable
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。
3)ConcurrentHashMap的锁分段技术可有效提升并发访问率
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
jdk8放弃了分段锁而采用了Node锁,降低了锁的粒度,提高了性能,并使用CAS操作来确保Node的一些操作的原子性,取代了锁。
但是,ConcurrentHashMap的一些操作使用了synchronized锁,而不是ReentrantLock,虽然说jdk8中对synchronized进行了性能优化,但是我觉得使用ReentrantLock锁能更多的提高性能。
ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。
Synchronized是悲观锁,在jdk1.8之后,加入了偏向锁,轻量级锁(自旋锁),性能得到了极大优化。
上述两种锁都是可重入锁,在锁的细粒度和灵活度方面,很明显ReenTrantLock优于Synchronized
11.Redis有哪些数据结构
字符串(String):
与其它编程语言或其它键值存储提供的字符串非常相似,键(key)------值(value) (字符串格式),字符串拥有一些操作命令,如:get set del 还有一些比如自增或自减操作等等。redis是使用C语言开发,但C中并没有字符串类型,只能使用指针或符数组的形式表示一个字符串,所以redis设计了一种简单动态字符串(SDS[Simple Dynamic String])作为底实现:
定义SDS对象,此对象中包含三个属性:
- len buf中已经占有的长度(表示此字符串的实际长度)
- free buf中未使用的缓冲区长度
- buf[] 实际保存字符串数据的地方
所以取字符串的长度的时间复杂度为O(1),另,buf[]中依然采用了C语言的以\0结尾可以直接使用C语言的部分标准C字符串库函数。
空间分配原则:当len小于IMB(1024*1024)时增加字符串分配空间大小为原来的2倍,当len大于等于1M时每次分配 额外多分配1M的空间。
由此可以得出以下特性:
- redis为字符分配空间的次数是小于等于字符串的长度N,而原C语言中的分配原则必为N。降低了分配次数提高了追加速度,代价就是多占用一些内存空间,且这些空间不会自动释放。
- 二进制安全的
- 高效的计算字符串长度(时间复杂度为O(1))
- 高效的追加字符串操作。
列表(List):一个列表结构可以有序地存储多个字符串
集合(Set):edis的集合和列表都可以存储多个字符串,它们之间的不同在于,列表可以存储多个相同的字符串,而集合则通过使用散列表(hashtable)来保证自已存储的每个字符串都是各不相同的(这些散列表只有键,但没有与键相关联的值),redis中的集合是无序的。还可能存在另一种集合,那就是intset,它是用于存储整数的有序集合,里面存放同一类型的整数。共有三种整数:int16_t、int32_t、int64_t。查找的时间复杂度为O(logN),但是插入的时候,有可能会涉及到升级(比如:原来是int16_t的集合,当插入int32_t的整数的时候就会为每个元素升级为int32_t)这时候会对内存重新分配,所以此时的时间复杂度就是O(N)级别的了。注意:intset只支持升级不支持降级操作。
有序集合(zset): 有序集合和散列一样,都用于存储键值对:有序集合的键被称为成员(member),每个成员都是各不相同的。有序集合的值则被称为分值(score),分值必须为浮点数。有序集合是redis里面唯一一个既可以根据成员访问元素(这一点和散列一样),又可以根据分值以及分值的排列顺序访问元素的结构。
哈希(hash): redis的散列可以存储多个键 值 对之间的映射,散列存储的值既可以是字符串又可以是数字值,并且用户同样可以对散列存储的数字值执行自增操作或者自减操作。散列可以看作是一个文档或关系数据库里的一行。
12.Redis如何处理同时过来的多个连接?如何处理多个客户端同时get?
- Redis基于Reactor模式开发了自己的网络事件处理器:文件事件处理器。
文件事件处理器采用了I/O多路复用程序,可以同时监听多个套接字,然后根据当前套接字的不同任务关联不同的事件处理器。
文件事件处理器由四个部分构成:套接字、I/O多路复用程序、文件事件分派器、事件处理器 - 多个客户端同时get会怎么样?
并发产生多个I/O事件的情况,I/O多路复用程序会把产生事件的套接字都放入到一个队列里,然后通过这个队列,以有序的、同步、每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕),I/O多路复用程序才会向文件事件分派器传送下一个套接字也就是说,多个客户端同时get,只有一个客户端能正常执行,其他的客户端请求被阻塞。
13.有没有有顺序的Map实现类,如果有,他们怎么保证有序。
顺序的Map实现类:LinkedHashMap,TreeMap
- LinkedHashMap是基于元素进入集合的顺序或者被访问的先后顺序排序。
- TreeMap则是基于元素的固有顺序(由Comparator或者Comarable确定)。
14.抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口吗,类可以实现多个接口吗?
抽象类和接口的区别有:
- 抽象类可以有自己的实现方法,接口在jdk1.8之后才可以有自己的实现方法(用default修饰)。
- 抽象类的抽象方法必须有继承的子类实现,如果子类不实现,则子类也需要定义为抽象的;接口的抽象方法必须由实现类来实现,如果实现类不能实现接口中所有方法,则将实现类定位为抽象类。
- 抽象方法必须是pulic/protected,接口中的变量隐式指定为public static final变量,抽象方法被隐式指定为public abstract。
- 抽象类中可以存在普通属性、方法、静态属性和方法。如果一个类中有一个抽象方法,那么当前类肯定是抽象类。
- 子类只能继承一个父类,接口可以继承多个接口,类似于:Interface1 Extends Interface2, Interface3, Interface4……
- 类也可以实现多个接口。
主要注意的是,抽象方法不能用synchronized修饰。
从设计角度来看抽象类和接口:
- 抽象类是is a,是实例必须要有的,比如Door必须有开和关。而接口就是has a,可以有也可以没有,比如Door可以有报警器,但不是必须的,是可拓展的行为。
- 抽象类强调的是同类事务的抽象,接口强调的是同类方法的抽象。
- 抽象类是从子类中发现了公共的东西,泛化出父类,然后子类继承父类;接口是根本不知道子类的存在,方法如何实现还不确认,预先定义。
- 若行为跨越不同类的对象,可使用接口;对于一些相似的类对象,用继承抽象类。
15.反射的原理,反射创建类实例的三种方式是什么?
反射机制:Java反射机制是在运行状态中,对于任意一个类,如果知道一个类的名称,都能够知道这个类的所有属性和方法;对于任意一个对象,如果知道一个实例对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
反射获取Class对象有三种方式:使用Class.forName(“类路径名称”)静态方法。
- 使用类的.class方法。
- 使用实例对象的getClass()方法。
根据Class获取实例对象有两种方式: - 直接使用字节码文件获取对应实例,Object o=clazz.newInstance();
- 对带参数的构造函数的类,先获取到其构造对象,再通过该构造方法类获取实例,如下。
/ /获取构造函数类的对象
Constroctor constroctor = clazz.getConstructor(String.class,Integer.class);
// 使用构造器对象的newInstance方法初始化对象
Object obj = constroctor.newInstance("龙哥", 29);
16.讲讲你理解的nio,他和bio的区别是什么,谈谈reactor模型。详解
- BIO(Blocking I/O):同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个线程不做任何事情会造成不必要的线程开销,当然可以通过线程池来改善。
- NIO (New I/O):同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。是基于事件驱动思想完成的。
- AIO (Asynchronous I/O):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
- reactor模型:反应器模式(事件驱动模式):当一个主体发生改变时,所有的属性都得到通知,类似于观察者模式。
解释一下同步与异步:
-
同步IO,是一种用户空间与内核空间的调用发起方式。同步IO是指用户空间线程是主动发起IO请求的一方,内核空间是被动接受方。
-
异步IO则反过来,是指内核kernel是主动发起IO请求的一方,用户线程是被动接受方。
再解释一下阻塞和非阻塞:
-
阻塞是指用户空间(调用线程)一直在等待,而且别的事情什么都不做;
-
非阻塞是指用户空间(调用线程)拿到状态就返回,IO操作可以干就干,不可以干,就去干的事情。
I/O多路复用是指内核一旦发现进程中指定的一个或者多个IO条件准备读取,它就通知该进程。也可以理解为,使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。
具体reactor可以看这篇文章: Reactor模式详解
在Java中,Selector这个类是select/epoll/poll的外包类。
17. JVM内存结构
- 程序计数器。当前线程执行的字节码的行号指示器,是线程私有的。也是唯一一个不会发生内存溢出的区域。
- Java虚拟机栈。也是线程私有的,描述的是Java方法执行的内存模型,线程请求的栈深度大于虚拟机所允许的最大深度,则将抛出StackOverflowError异常。
- 本地方法栈。与虚拟机栈相似,区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
- Java堆。是Java虚拟机中管理的内存中最大的一块,所有线程共享区域,唯一目的就是存放对象实例。所有的对象实例以及数组都要在堆上分配内存。Java堆也是垃圾回收器管理的主要区域,也被称为gc堆,收集器基本都采用分代收集算法,Java堆中还可以细分为:新生代和老年代。
- 方法区。所有线程共享区域,用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。很多人也愿意称之为“永久代”。
- 运行时常量池。是方法区的一部分,用于存放编译器生成的各种字面量和符号引用。
- 直接内存。并不是虚拟机运行时数据区的一部分。例如NIO,它可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样避免了在Java堆和Native堆中来回复制数据,提高了性能。
JVM中要对堆进行分代,分代的理由是优化GC性能,很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
HotSpot JVM把新生代分为了三部分:1个Eden区和2个Survivor区(分别叫from Survivor和to Survivor)。默认比例为8:1。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
18.JVM内存为什么要分成新生代、老年代和持久代。新生代中为什么要分Eden和Survivor
堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。给堆内存分代是为了提高对象内存分配和垃圾回收的效率。
- 堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,时间代价巨大,会严重影响GC效率。
- 有了内存分代,新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中。静态属性、类信息等存放在永久代中。新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC。老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收。永久代中回收效果太差,一般不进行垃圾回收。还可以根据不同年代的特点采用合适的垃圾收集算法。分代收集大大提升了收集效率。
HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
19.JVM如何一次完整的GC流程。对象如何晋升到老年代。几种主要的JVM参数。
GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
总结来说 :
1. 亚当区存活的对象被复制到To Survivor,From Survivor区存活的对象看年龄决 > 15 去老年代 , 不够15的去To Survivor
2. 清空亚当区和From Survivor区 , 此时新生代存活的对象都在To Survivor。
3. To Survivor与From Survivor交换角色,总之每次GC后,To Survivor都是空的.
4. 如果GC时, To Survivor没有足够的空间存放新生代收集的存活对象,就需要老年代分配担保
附言 : 此处用到的算法是
复制算法:将可用内存按容量分为两块(Eden和Survivor空间),每次只使用一块,当这一块内存用完后,就将还活着的对象复制到另外一块上面,然后再把已使用过内存空间一次清理掉。
对象晋升老年代有三种可能:
- 当对象达到成年,经历过15次GC(默认是15,可配置),对象就晋升到老年代了。
- 大的对象会直接在老年代创建。
- 新生代的Survivor空间内存不足时,对象可能直接晋升到老年代。
jvm参数:
- -Xms:初始堆大小
- -Xmx:堆最大内存
- -Xss:栈内存
- -XX:PermSize 初始永久代内存
- -XX:MaxPermSize 最大永久代内存
在默认情况下,JVM初始分配的堆内存大小是物理内存的1/64,最大分配的堆内存大小是物理内存的1/4。
默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。
因此服务器一般设置-Xms、-Xmx相等,来避免每次GC后调整堆的大小。
20.当出现了内存溢出,怎么排错
- 首先控制台查看错误日志。
- 然后使用jdk自带的VisualVM来查看系统的堆栈日志(也可以用jmap查看堆转储快照)。
- 定位出内存溢出的空间:堆,栈还是永久代(jdk8后没有永久代的溢出了)。
- 如果是堆内存溢出,看是否创建了超大的对象。
- 如果是栈内存溢出,看是否创建了超大的对象,或者产生了死循环,或者递归调用。
21.如何查看线程栈信息
StackTraceElement[] elements = (new Throwable()).getStackTrace();
StringBuffer buf = new StringBuffer();
for(int i=0; i<elements.length; i++) {
buf.append("\n"
+ elements[i].getClassName()//打印线程当前执行的详细类名
+ "."
+ elements[i].getMethodName()//打印线程当前方法名
+ "("
+ elements[i].getFileName()//打印线程当前执行类的文件名
+ ":"
+ elements[i].getLineNumber()//打印线程当前执行的行数
+ ")");
}
22.创建对象的几种方式
- 用new关键字创建。
User user = new User();
-
调用对象的clone方法。
-
利用反射,调用Class类的或者是Constructor类的newInstance()方法。
User user = User.class.newInstance();
或者是
Constructor<User> constructor =User.class.getConstructor();
User user= constructor.newInstance();
- 用反序列化,调用ObjectInputStream类的readObject()方法。
23.是不是所有的对象和数组都会在堆内存分配空间?
那么你可以告诉他:不一定,随着JIT编译器的发展,在编译期间,如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配。但是这也并不是绝对的。