一、对象的死亡判定
JVM垃圾回收针对哪些区域?回收的是什么?
答:
1. 主要针对Java堆。
2. 回收“死亡”的对象,即没有引用的对象。
那么如何判断对象是否“死亡”呢?
两种方法,引用计数法和可达性分析算法。
引用计数法
原理:给每个对象添加一个引用计数器。
- 每当有一个地方引用它,引用计数器+1;
- 每当一个引用失效,引用计数器-1;
- 当引用计数器为0时,表示该对象无引用,即可回收。
缺点:无法解决对象之间循环引用的问题。
GCobject A = new GCobject();
GCobject B = new GCobject();
A.instance = B;
B.instance = A;
...
A = null;
B = null;
可以看到,A、B对象都为null,已经不可能再访问,但由于A、B都有字段引用着对方,引用计数器不为0,因此不会被回收。
可达性分析算法
原理:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链”,当一个对象到 GC Roots没有任何引用链相连时, 则说明此对象是不可用的。
如图,Object5、Object6、Object7这三个对象就是不可用的。
“GC Roots”对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(Native方法)引用的对象;
然而,在可达性分析算法中不可达的对象,也并不是“非死不可”。一个对象真正死亡,至少要经历两次标记过程:
第一次标记: 对象无GC Roots的引用链,筛选是否执行finalize()方法:
- 对象没有覆盖finalize()或finalize()被JVM调用过,则不需要执行;
- 若执行finalize(),则将对象放入F-Queue队列中,会由JVM自动建立的线程执行该方法;
第二次标记:对F-Queue队列中的对象进行小规模的标记:
- 对象在finalize()中拯救自己(重新与引用链相连),则不会回收;
- 对象没有在finalize()中逃脱;
注:任何一个对象的finalize()方法只能被调用一次。
二、垃圾收集算法
主要有4种垃圾收集算法。
Java堆从GC的角度可以细分为: 新生代(Eden区、From Survivor区和To Survivor区)和老年代。
标记-清除算法(老年代)
它是最基础的收集算法,算法分为标记和清除两个阶段:
1. 首先标记出所有可回收的对象
2. 统一回收被标记对象
不足:
1. 效率问题,标记和清除的效率不高;
2. 空间问题,标记清除后容易产生不连续的空间碎片(不利于大对象的内存分配)
复制算法(新生代)
算法的核心是将可用内存按容量划分为大小相等的两块,每次只用其中一块,当这一块的内存用完,就将还存活的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉。(如图)
优点:无内存碎片;
不足:可用内存减小;
针对以上不足,就有了现代复制算法:
不需完全按照1∶1的比例划分新生代空间,将新生代划分为一块较大的Eden区和两块较小的Survivor区(from和to)(HotSpot默认Eden和Survivor的大小为8∶1),每次只用Eden和其中一块Survivor(from)。
- 当发生MinorGC时, 将Eden和Survivor(from)中还存活着的对象一次性地拷贝到另外一块Survivor(to)上,最后清理掉Eden和刚才用过的Survivor(from)空间。
- 当Survivor(to)空间不够用(不足以保存尚存活的对象)时, 需要依赖老年代进行空间分配担保机制,这部分内存直接进入老年代。
注:现代的商业虚拟机都是采用这种收集算法回收新生代。
标记-整理算法(老年代)
标记-清除算法会产生内存碎片问题,而复制算法需要有额外的内存担保空间,于是针对老年代的特点,又有了标记整理算法。
标记整理算法:标记过程与标记-清除算法相同, 但后续步骤不再对可回收对象直接清理, 而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。 (如图)
分代收集算法
当前主流JVM垃圾收集都采用”分代收集”(Generational Collection)算法。 这种算法会根据对象存活周期的不同将内存划分为几块, 如JVM中的新生代、老年代、永久代。 这样就可以根据各年代特点分别采用最适当的GC算法:
- 新生代:每次垃圾收集都能发现大批对象已死, 只有少量存活。因此选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
- 老年代:因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记—清理”或“标记—整理”算法来进行回收,不必进行内存复制,且直接腾出空闲内存。