当一个对象无法从其正在运行的进程的任意对象引用到它时,我们将其认为是垃圾,Virtual Machine将会重用这款内存。
从理论上说最简单的垃圾收集器算法就是每次运行时遍历整个堆,存活下来的对象我们就认为时垃圾,但是可想而知这种方式性能差,花费的时间和对象数量成正比。那我们该怎么解决这个问题呢?
下面引出我们的主题, 分代收集器(Generational Collection)。
以下为一个对象存活的经典布局(Typical Distribution for Lifetimes of Objects):
蓝色区域表示对象生命周期的典型分布。X 轴显示“已分配的字节为单位的对象”生存时间。Y 轴上表示在对应的阶段存活对象总的字节数。可以从上图看出,大部分对象基本在一轮迭代后就死亡了。当然也有些对象确实存活时间较长,因此分布向右延伸。例如,有一些对象从初始化开始一直存活到Virtual Machine退出。介于这两个极端之间的是一些处于计算期间存活的对象。当然有些应用程序有非常不同的外观分布,但令人惊讶的是,大量的应用程序具有这种一般形状。通过大部分对象“早逝”这一特点,高效的收集成为可能。
在上述基础情况上, 我们提出两个假说。
这两个分代假说共同奠定了多款垃圾收集器的一致的设计原则。通过将 Java堆划分出不同的区域, 依据对象年龄(年龄即对象熬过垃圾收集过程的次数,该变量存放在对象头的Mark Word中占有4bit用于标记年龄)分配到不同的区域之中存储。 显而易见,如果把同种类型的对象集中起来一起管理将会大大节约空间和时间。这就是在我们现在常看到了新生代(Young Generation) 和老年代(Old Generation)这两个区域,其实就是建立在上述的理论基础上的。新生代(Young Generation)顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。 读到这里大家会有个疑问:如何老年代对象有指向新生代的怎么办呢?毕竟实际项目中的对象经常出现跨代引用。那该怎么解决这个问题呢?
当然最简单的方式就是我再遍历另外一个区域就好了啊(这里为什么需要遍历和可达性分析算法有关),这种方式自然可以,但是性能太差了。因此我们添加了第三条假说,这其实是可根据前两条假说逻辑推理得出的隐含推论: 存在互相引用关系的两个对象应该倾向于同时生存或者消亡的。 举个例子,如果某个新生代对象存在跨代引用, 由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了。
通过其上述理论,该情况时很少的,没必要遍历整个老年代。每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构即“记忆集”(Remembered Set),这个结构把老年代划分成若干小块, 标识出老年代的哪一块内存会存在跨代引用。 此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。 虽然这种方法会增加一些运行时的开销,因为需要在对象改变引用关系时维护记录数据的正确性,但比起收集时扫描整个老年代来说好太多了。
在上述理论背景下,我们应该知道了为什么有了不同的回收分类 “Minor GC”、“Major GC”、“Full GC”,以及依据不同区域存活对象的特征相匹配的垃圾收集算法 “标记-复制算法”、“标记-清除算法”、“标记-整理算法”等算法。因为我们针对上述理论基础,划分不同区域之后我们其不同特性对不同区域进行特殊处理。
总结:每个事务的出现都有其背后的原因。当然现在阶段大家都应该知道分代设计也存在很多缺陷的,因此也就有了Garbage-First(G1)、Z Garbage Collector(ZGC)等新型垃圾收集器的出现。