😆 分代垃圾算法有点像垃圾分类的感觉
堆内存回收过程详解
我们分析了如何判断对象是否可回收,还有 3 种基础的垃圾回收算法,以及年轻代、老年代的内存区域划分与原因。接下来我们就一步一步来分析堆内存的回收流程。
内存初始状态
假设在第一垃圾回收之前,内存中的状态如图所示,Eden 区有 2 个存活对象,4 个垃圾对象,内存的可用区域已经所剩无几,Survivor 区因为还没有进行任何 MinorGC 所以是空的,有 1 个大对象直接分配到了老年代。
第 1 次执行 MinorGC 后状态
当新的对象分配到 Eden 区,发现内存空间不够,于是触发第一次 MinorGC,垃圾回收器首先将 Edne 区中的两个存活对象复制到 S0 区,然后在清空 Eden 区的空间,如下图:
程序运行一段时间后状态
经过第 1 次 MinorGC 程序再运行一段时间后,堆内存状态如下:Eden 区又产生了大量的对象,并且大部分对象都可回收状态,这也符合对象“朝生夕灭”的特征,S0 区中也有 1 个对象可以回收,S1 与老年代没有变化,在这种状态下,如果新对象分配再次触发 MinorGC 会发生什么呢?
执行第 2 次 MinorGC 后状态
新对象分配 Eden 区空间不足,又触发了第二次 MinorGC,第二次 MinorGC 与第一次 GC 时在 Eden 区的操作是一样的:将 Eden 区存活的对象复制到 S1 区,然后在清空整个 Eden 区,同时也将 S0 区存活的对象复制到 S1 区并将对象的年龄加 1,再清空 S0 区,GC 后的状态如下图所示:
执行第 2 次 MinorGC 后,程序运行一段时间后状态
经过第二 MinorGC 后程序又运行了一段时间,Eden 区中有生成了很多对象,S1 区也有一个对象可回收。
第 15 次 MinorGC 后内存状态
在接下来的每次 MinorGC 时,都是第二次一样,从 Eden 区和 survivor 非空白区移动存活对象到 survivor 区中空白区域,并清空这两个区域内存空间,存活对象每此从 survivor 两个区域移动一次,对象年龄加 1,下图表示经过了 15 次 MinorGC 后的堆内存状态。
对于年轻代区域的内存收集,使用的是标记-复制算法,只是为了减少复制算法空白区域的内存浪费,并不是将内存一份为二,而是巧妙的将内存分为三个区域,预留的空白区域只占整个年轻代区域的 1/10。
对象如何进入老年代
以上是年轻代的分配与回收问题,那对象如何进入老年代呢?个人认为对象进入老年代,可以分为 2 种类型 6 种情况。
直接分配
对象创建时直接分配到老年代具体分为 3 种情况:
- 超过虚拟机 PretenureSizeThreshold 参数设置大小的对象,该参数的默认值是 0,也就是说任何大小的对象都会先分配到 Eden 区。
- 超过 Eden 大小的对象
- 如果新生代分配失败,一个大数组或者大字符串
从年轻代晋升
从年轻代空间晋升到老年代也可分为 3 种情况:
- 新生代分配担保,在执行 MinorGC 时要将 Eden 区存活的对象复制到 Survivor 区,但是 Survivor 区默认空间是只有新生代的 2/10,实际使用的只有 1/10,当 Survivor 区内存不够所有存活对象分配时,就需要将 Survivor 无法容纳的对象分配到老年代去,这种机制就叫分配担保。
- 对象年龄超过虚拟机 MaxTenuringThreshold 的设置值,最大为 15,
- Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半(TargetSurvivorRatio),年龄大于或等于该年龄的对象直接进入老年代。
老年代回收算法-FullGC
当有对象要进入老年代,而老年代空间又不足时就会触发 FullGC,当然,反过来说触发 FullGC 的条件不仅仅只是老年代空间不足,FullGC 使用的算法是上面说的标记-整理算法。
总结
- 判断对象是否可以回收是垃圾回收的基础与前提,通过可达性分析从 GCRoots 开始进行”顺藤摸瓜”找到不可达对象(可回收)
- 对象生命周期的特征”朝生夕灭”与”越战越强”是垃圾回收算法的理论基础
- 基础的垃圾回收算法有 3 种分别是 标记-清除算法、标记-复制算法、标记整理算法,都有各自的适应场合与优缺点
- 分代垃圾算法根据对象生命周期的特征,将其划分到不同的区域,从而使用最适合的垃圾算法来进行优化
- 在 JDK8 默认的配置下使用 新生代,老年代的垃圾回收策略,新生代区域使用标记-复制算法,老年代区域使用标记-整理算法