垃圾收集算法
分代收集理论
3个假说
- 弱分代假说:绝大多数对象都是朝生夕灭的。(设计了年轻代)
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。(设计了老年代)
- 跨代引用假说:跨代引用相对于同代引用仅占极少数。(实际Java应用中,可能会存在年轻代的对象跨代引用了老年代的对象)(设计了
记忆集
,在新生代上建立全局的记忆集,把老年代划分为若干个小块,标识出老年代的哪一块内存会存在跨代引用,此后在Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描)
分代实现
HotSpot虚拟机中,根据对象存活周期的不同,将内存划分为几块。一般是将Java堆分为新生代和老年代,比例为
2:1
;新生代又细分为Eden
区、From Survivor
区和To Survivor
区,比例为8:1:1
。不同的代采用不同的回收算法:
- 新生代:复制算法
- 老年代:标记-清除算法,或者标记-整理算法
定义一些关于GC的名词
- 部分收集(Partial GC): 不是完整收集Java堆的收集。
- 新生代收集(Minor GC/Young GC):只是新生代的收集。
- 老年代收集(Major GC/Old GC):只是老年代的收集。目前只有CMS收集器会有单独收集老年代的行为。
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
垃圾收集算法
知道了如何判定无用的对象、废弃常量和无用的类后,接下来就是将这些无用的对象给回收掉。 如下是常用的3种垃圾收集算法。
标记-清除
该算法分为标记和清除两个阶段。
- 标记:遍历所有的 GC Roots,然后将所有 GC Roots 可达的对象标记为存活的对象。
- 清除:遍历堆中所有的对象,将没有标记的对象清除掉。同时清除对象上的标记,以便下一次垃圾回收。
这种方法有2个不足:
- 效率问题:标记和清除2个过程效率都不高
- 空间问题:回收后内存会有大量碎片;碎片太多可能导致以后需要分配大对象时,无法找到足够连续的内存不得不提前触发另一次垃圾回收动作。
复制(新生代)
为了解决效率问题,复制算法出现了。它将可用内存按容量划分为大小相同的两块,每次只使用其中的一块。当这一块内存用完,需要进行垃圾收集时,就将存活的对象复制到另一块上,然后清除掉这一块内存。
这种算法有优有劣:
- 优势:不会产生碎片。
- 劣势:内存缩小为原来的一般,浪费空间。
为了解决空间利用率问题,可以将内存分为3块:Eden、From Survivor、To Survivor,比例是8:1:1,每次使用Eden和其中一块Survivor。回收时,将Eden和Survivor中存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才使用的Survivor空间。这样只有10%的内存被浪费。
但是无法保证每次回收都只有不多于10%的对象存活,当Survivor空间不足时,需要依赖其他内存(老年代)进行分配担保。
分配担保是指如果另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时(超过10%),这些对象将直接通过分配担保机制进入老年代。
标记-整理(老年代)
该算法分为标记和整理两个阶段。
- 标记:遍历所有的 GC Roots,然后将所有 GC Roots 可达的对象标记为存活的对象。(和标记-清除的标记阶段一样)
- 整理:移动所有存活的对象,按着内存地址次序依次排列,然后将末端地址以后的全部内存回收。
这是一种老年代的收集算法,老年代的对象存活时间比较长。
参考资料
- 周志明 * 《深入理解Java虚拟机》
- Java虚拟机底层原理知识总结 * https://doocs.github.io/jvm/