1、jdk自带线程池为什么先入队列,后创建非核心线程?
因为队列增加一个元素的成本更低。
2、内部类引用外部类方法的局部变量,为什么必须使用final修饰?
因为外部类方法的变量运行完毕就会回收,但加载的内部类可能仍然存在。
3、方法的签名包括方法名和参数列表,为什么不包括返回值?
因为调用端可以不接收返回值,此种情况就无法判断具体是哪个方法。
4、Spring的线程池工具ThreadPoolTaskExecutor,也是基于JDK的ThreadPoolExecutor实现,它有什么好处、陷阱?
好处:参数、单例可配置,队列、拒绝策略有默认。缺点:错过了解底层的机会,直接使用ThreadPoolExecutor也不难。
5、有哪些令人眼花缭乱的锁?
自旋、阻塞、重入、读写、乐观、悲观、偏向、轻量级、重量级,共享锁、排他锁、行锁、表锁、间隙锁、意向锁。
6、状态模式与策略模式有什么区别?
结构上看,没有区别。
目的上看,
状态模式由一组状态驱动,用于处理状态的变化;
策略模式由一组算法驱动,用于处理算法的变化。
具体来说,
策略模式中,算法是否变化完全由客户端决定,而且一般一次只能选择一种算法,不存在中途变化的情况;
状态模式中,状态本身存在线性的生命周期,是否变化由具体状态内部决定,这种变化对客户端是透明的。
7、HashMap.put(key, value)的执行过程。
计算key的哈希值。
计算哈希值在数组中的位置。
若该key已存在,则替换掉旧值。
否则,在该位置添加新的节点。
最后检查是否需要扩容。
8、HashMap何时扩容?怎么扩容?数据迁移?
何时扩容?
put的最有一步,若size>=threshold(capcity*loadFactor),则进行扩容。
怎么扩容?
创建一个新数组,容量=旧数组容量*2。
将旧数组中的数据,迁移到新数组。
将HashMap的底层数组指向新数组。
重新计算threshold。
数据迁移?
遍历旧数组。
针对每个数组成员,从头开始遍历链表。
针对每个链表节点,重新计算哈希值,以及该哈希值在新数组中的位置。
将当前节点的next,指向新数组中该位置的节点。
将新数组中该位置,指向当前节点。
以上过程造成了两种后果:
若重新哈希后没有冲突,直接存放为数组元素;
若重新哈希后仍有冲突,新链表的顺序与原来相反。
9、HashMap.get(key)多线程环境下,CPU飙到100%。
根本原因,在HashMap扩容,数据迁移的时候,出现了环状链表结构。
相关代码:
void transfer(Entry[] newTable)
{
Entry[] src = table;
int newCapacity = newTable.length;
// 下面这段代码的意思是:
// 从OldTable里摘一个元素出来,然后放到NewTable中
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next; // 线程一在这里挂起
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
形成过程:
为了便于分析问题,假定链表长度为2,头结点为node1,尾节点为node2。
第一阶段->线程1–>第1次循环–>上半场。
创建数组a,遍历旧链表,第1个语句,之后当前线程挂起;
此时情况,持有两个引用,e指向头结点(node1),next指向尾节点(node2),即e.next。
第二阶段->线程2。
创建数组b,遍历旧链表,完毕之后,新链表的顺序与原来相反。
此时情况,node1变成了尾节点,node2变成了头结点。
b[i]=node2,node2.next=node1,node1.next=null。
第三阶段–>线程1–>第1次循环–>下半场。
初始情况,e指向尾节点(node1),next指向头结点(node2)。
第2个语句,计算尾节点e(node1)的数组下标i。
第3个语句,尾节点e(node1)的next(e.next),指向a[i](此时为null)。
第4个语句,a[i]指向尾节点e(node1)。
第5个语句,e指向头结点next(node2)。
此时情况,a[i]指向尾节点(node1),e指向头结点(node2),next也指向头结点(node2)。
a[i]=node1,node1.next=null,b[i]=node2,node2.next=node1。
第四阶段–>线程1–>第2次循环。
第1个语句,next指向尾节点(node1)。
第2个语句,计算头结点e(node2)的数组下标i。
第3个语句,头结点e(node2)的next(e.next),指向a[i](此时为node1),相当于node2.next=node1。(没有任何效果)
第4个语句,a[i]指向头结点e(node2)。
第5个语句,e指向尾节点next(node1)。
此时情况,a[i]指向头节点(node2),e指向尾结点(node1)。
a[i]=node2,node2.next=node1,node1.next=null,b[i]=node2。
第五阶段–>线程1–>第3次循环。
第1个语句,next指向null。
第2个语句,计算尾结点e(node1)的数组下标i。
第3个语句,尾结点e(node1)的next(e.next),指向a[i](此时为node2),即node1.next=node2。
总结。
第四阶段,node2.next=node1。
第五阶段,node1.next=node2。
由此环路形成。
10、HashSet.toString()多线程环境下,出现内存溢出。
两个原因,
HashSet基于HashMap实现,多线程环境下,出现了环状链表结构。
HashSet.toString拼装StringBuilder时,遍历上述结构,陷入死循环。
11、两阶段提交和三阶段提交。
两阶段提交:
第一阶段,投票+预提交(各就各位+预备);
第二阶段,提交(走你)。
三阶段提交:
第一阶段,投票(各就各位);
第二阶段,预提交(预备);
第三阶段,提交(走你)。
12、JDK6、JDK7、JDK8,常量池、方法区(永久代)、元空间。
JDK6,常量池位于方法区,又叫永久代,配置参数-XX:PermSize、-XX:MaxPermSize。
JDK7,常量池位于堆内存。
JDK8,彻底去掉了方法区,取而代之的是元空间,配置参数-XX:MetaspaceSize、-XX:MaxMetaspaceSize。
13、TLAB是什么?它位于堆内存,还是栈内存?
14、对象可以直接分配在栈内存吗?
15、JVM线上问题分类排查。
CPU问题。
死循环:查看系统负载,排查方法:top/top H/top -Hp pid。
线程阻塞:查看线程栈,排查方法:jstack pid。
频繁GC:查看GC情况,排查方法:jstat -gcutil pid。
内存类问题。
堆内存:OutOfMemoryError:java heap space,排查方法:集合、缓存、多线程、大对象、jmap –heap、jmap –histo、jmap -dump:format=b,file=xxx.hprof。
栈内存:
StackOverflowError,排查方法:栈深度过大、递归死循环等;
OutOfMemoryError:unable to create new native thread,排查方法:线程数量过多、物理内存不足、超过系统限制。
方法区:OutOfMemoryError:PermGen space,排查方法:XX:MaxPermSize、class/jar过多、重复加载、动态代理。
直接内存:OutOfMemoryError,排查方法:Java Native Interface、java.nio.DirectByteBuffer等。
IO类问题。
磁盘IO。
网络IO。
16、栈内存什么情况下StackOverflowError,什么情况下OutOfMemoryError?
单个线程的栈帧太大,抛出StackOverflowError。
创建的线程数量过多,抛出OutOfMemoryError。
17、强引用、软应用、弱引用、虚引用。
强引用,诸如Object obj = new Object(),永远不会垃圾回收。
软引用,诸如SoftReference reference = new SoftReference(obj),在内存溢出之前,进行二次回收。
弱引用,诸如WeakReference reference = new WeakReference(obj),下一次垃圾回收时进行回收。
虚引用,诸如PhantomReference reference = new PhantomReference(obj),对垃圾回收无任何影响,也无法通过虚引用取得对象,它存在的唯一目的,就是对象被回收时,收到一个系统通知。
18、判定对象是否应该被回收,引用计数器 OR 可达性分析?
答案是可达性分析,因为引用计数器无法解决循环引用的问题。
可达性分析是指,从对象到GC Roots没有任何引用链相连接。
GC Roots包含,虚拟机栈和本地方法栈中引用的对象,方法区中静态成员变量和常量引用的对象。
19、对象被判定为不可达后,是否非死不可?
答案是NO。
标记对象是否有必要执行finalize方法(对象覆盖了finalize方法,且未执行过,表示有必要执行)
若无必要执行,则进行回收。
创建的线程数量过多,抛出OutOfMemoryError。
若有必要执行,则登记F-Queue队列,启动Finalizer线程,执行finalize方法。
若finalize方法中把对象复活,则对象不进行回收,否则进行回收。
以上复活的机会只有一次,下一次finalize方法不再执行
20、CPU负载飙高甚至达到100%,如何排查?
第一步,找到CPU负载最高的进程pid:top/top -c
第二步,找到CPU负载最高的线程pid:top -Hp pid。
第三步,线程pid转换为十六进制:printf ‘%x\n’ pid。
第四步,找到Java堆栈信息:jstack 进程pid | grep 线程pid十六进制 -C10 –color。
第五步,定位到线程信息和问题代码。
21、JVM参数-XX:+DisableExplicitGC、-XX:+ExplicitGCInvokesConcurrent的含义。
-XX:+DisableExplicitGC,禁用System.gc()显式调用GC。
-XX:+ExplicitGCInvokesConcurrent,启用并行的FullGC。
常与堆外直接内存配合使用。(?)
22、什么是堆外内存?
狭义的堆外内存,通过java.nio.DirectByteBuffer分配的内存,通过JVM参数-XX:MaxDirectMemorySize指定大小。
若未指定,则默认=新生代+老生代-survivor=-Xmx-survivor。
广义的堆外内存,堆之外的所有内存,包含程序计数器、虚拟机栈、本地方法栈、直接内存。
注意:对于HotSpot,Java6/Java7的方法区又叫永久代,与堆内存一起分配和管理,因此属于堆内存的一部分。
对于频繁操作内存,只需临时存储的场景,建议使用堆外内存,并且做成缓冲池,便于重复利用。
23、使用堆外内存,为什么显式调用GC?
DirectByteBuffer对象创建的时候,关联PhantomReference用于对象跟踪,创建sun.misc.Cleaner用于对象回收。(Unsafe的free接口。)
GC过程中,若发现对象只被PhantomReference引用,则该引用登记到java.lang.ref.Reference.pending队列。
GC完毕后,通知ReferenceHandler守护线程进行后置处理,若pending队列为空,则线程阻塞,否则进行遍历。
若Reference类型为Cleaner,则调用Cleaner.clean(),否则登记ReferenceQueue队列,可以用于对象的复活。(类似于finalizer)
24、JVM监控小工具jstat。
jstat -gc pid——S0C、S1C、S0U、S1U、EC、EU、OC、OU、PC、PU、YGC、YGCT、FGC、FGCT、GCT
jstat -gcutil pid——S0、S1、E、O、P、YGC、YGCT、FGC、FGCT、GCT。
jstat -gccapacity pid。
jstat -gccause pid。
25、-XX:+DisableExplicitGC,关闭System.gc(),亦即禁止手动触发STW的FGC。
26、-XX:+ExplicitGCInvokesConcurrent,YGC+部分OGC,性能比Full GC要好,STW时间变短,后台回收。
27、