在前面介绍"外貌平平无奇"的标记清除算法时, 引入了内存碎片的问题;后来通过标记整理算法解决了内存碎片问题, 但是又引入了效率不高的问题;
接着又通过复制算法解决了效率不高的问题, 但是又引入了内存利用率不高的问题;
那么有没有一种方法可以解决所有问题呢? 这个问题有点难. 我们先对比一下标记整理算法和复制算法:
通过对比, 不难发现, 标记整理算法相当于是用时间换空间, 复制算法相当于是用空间换时间, 这个世界是公平的, 一个算法无法让时间和空间全部达到最优, 那么我们只能根据自己的实际场景来二选一了吗? 本着"小孩子才做选择题,成年人当然是全都要"的精神, 我们有了分代算法.
分代算法的核心思想是同时发挥"标记整理算法"和"复制算法"的优点, 让他们分别去处理自己最适合处理的场景:分代算法为每个可回收的堆内存对象记录年龄(每个对象在被创建时初始年龄为0, 之后每经历一次GC, 如果未被回收掉就把它的年龄加1).
对于年龄小于某个固定值(JAVA中默认是15岁, 不是现实中成年人的18岁哈)的对象被认为是属于年轻代(Young). 对于年轻代内的对象使用复制算法进行内存回收, 这里有个假设是年轻代的对象一般非常不稳定, 每次GC都能回收掉其中的决大部分, 根据经验这个假设在现实生活中基本是成立的.
对于年龄高过这个固定值的对象被认为是属于老年代(Old). 对于老年代的对象使用标记整理算法, 这里也有个假设是老年代的对象一般比较稳定, 内存空间紧张, 这个假设也基本是成立的.
分代算法为年轻代和老年代分别配置了独立的内存区域, 当年轻代内存空间满时就触发年轻代GC(也叫Minor GC), 当老年代内存空间满时就触发老年代GC(也叫FullGC). 正常情况下, 应该是每次年轻代GC发生后, 会有部分对象进入老年代, 年轻代GC发生若干次后, 老年代空间被占满了, 开始进行老年代GC.
以上基本就是分代算法的核心内容, 但是还有一点需要补充, 就是实际经验中发现, 年轻代的对象使用复制算法时, 对象回收比例非常高(有人说98%左右), 主要的原因在于绝大部分对象都是朝生夕死, 而非朝生夕死的对象, 在到达15岁时被挪到了老年代中. 基于这个事实, 是否可以对复制算法有些优化呢? 如果每次复制算法都只能活下不到10%的对象, 那么是不是平时留下10%的空间就足够了, 如下图:
但是这种做法有个问题, 就是每次回收完后, 存活下来的对象会全部进入到10%的空间中, 这里需要把这些对象重新移动到90%的空间中, 再开始. 为此还有一个优化手段, 就是预留两个10%的区域, 如下图:
让R0和R1这两个区域轮流成为原本的预留区域, 别一个区域和80%的稳定可用区域加起来就是之前的90%可用区域, 这样达到跟刚才的方法相同的利用率, 还减少一次从预留区域向非预留区域的移动过程.
最终使用分代算法的内存的分配情况如下:
在这个图中:首先是把内存分隔成了两个大区域, Young区和Old区, 目的是为了使用不同的回收算法
Yound区为了节省复制算法的内存代价又分成了Eden区, S0, S1这三个区. 其中S是Survivor的简写, 这个划分规则与上面说介绍的StableAvailable, R0, R1是一致的, 只是说法上的不同. Eden和Survivor这套说法是官方的标准说法, 非常形象生动, 但是个人觉得不太益于了解它的原理本质, 为此自己引入了StableAvailable和Reserved的说法.
还有1点需要补充一下:有些内存对象一旦被创建就无法再被回收, 有些文档中会说成永久代, 我觉得这块跟垃圾回收关系不大, 还容易引起混淆, 所以没有介绍.
至此, 你已经掌握了JavaGC的内功心法, 后面会开始介绍基于这套心法延伸出来的各种奇技淫巧, 包括parNew, parallelScavenge, concurrentMarkSweep, g1, zgc. 为此, 本人决定闭关一段时间, 潜心修炼, 敬请期待更新.