HotSpot中的“世代/分代”(Generations)概念 for Java 10

Java的优势之一是它对开发者屏蔽了复杂的内存分配和垃圾回收,但当垃圾回收成为瓶颈时,了解它的内部实现机制是有必要的。

当运行时没有任何指针指向一个对象时,那这个对象就是个“垃圾”。最直截了当的垃圾收集算法遍历所有“可达”对象,剩下所有的对象都视为“垃圾”😀。这种算法的时间消耗跟“存活”的对象数量成正比,对于持有大量“存活”对象的大型程序禁止这么干。

采用分代收集的虚拟机整合了不同的垃圾回收算法。不成熟的垃圾收集器检测堆中每个存活的对象,而分代收集则观测根据程序运行经验以最少的工作完成垃圾回收。最重要的观测属性是“weak generational hypothesis”(弱世代假设),这种状态的大多数对象都存活很短的时间。

下图中的蓝色区域是对象常规的寿命分布。X轴是以字节为单位的对象寿命,Y轴的字节数量是相应生存期的对象的总字节数。左边的断崖表示对象在创建之后很快被回收(即对象已“死”)。例如Iterator,通常指存活一次迭代。

对象寿命

一些对象存活时间长,因此延伸到了分布图的右侧。例如单例对象,存活周期从初始化开始知道进程退出才结束。介于两个极端之间的情况是,对象存活于一些中间计算,如右侧的初始峰值。很多应用看起来不同,但对象寿命根这张图却惊人地相似。最有效的收集一般都聚焦在那些“英年早逝”的对象上。

为了针对这种情况进行优化,内存被分代管理(存储不同年龄对象的内存池)。垃圾收集发生在每个世代分区被填满的时候。绝大多数对象被分配到年轻对象专用的对象池(即新生代),大多数对象都在新生代就“死”去。当新生代填满的时候,将触发minor collection(次代收集/新生代收集),只回收新生代。其它分区的垃圾不会被回收。假设“弱世代假设”成立,新生代中的大部分对象都是垃圾、可以被回收,那么minor collection可以被优化。这种收集方式的花费跟存活对象数量成正比,被“死亡”对象填满的新生代可以被迅速回收。通常情况下,一些从minor collection中存活下来的新生代中的对象将移动到“永久代”(tenured generation)。最终永久代也会被填满且必须被回收,触发major collection。会对整个对进行回收。由于有大量对象参与,因此major collection比minor collections耗时。

serial (串行)垃圾收集器为小数据量的应用设计,默认参数对小型应用最有效。parallel(并行)收集器或throughput(吞吐量优先)收集器用于具有大型数据的应用程序。其参数的设置为server端程序提供更高的性能。这些选择大多数情况都很有效,但也不是100%凑效。

Serial收集器中默认分代情况

在初始阶段,Java HotSpot虚拟机在地址空间中保留整个Java堆,但除非必须,不给它分配任何物理内存。从逻辑上将,覆盖整个Java堆的地址空间分为年轻代和老年代。为对象存储保留的完整地址空间可分为年轻代和老年代。

年轻代由Eden区和两个Survivor区组成,大部分对象都分配带Eden区。其中一个Survivor区总是空的,服务于垃圾收集时Eden区和另一个Survivor区存活对象的目标空间,垃圾收集结束后,Eden区和源Survivor区将被清空。下一次垃圾收集时,两个Survivor区的功能将做交换。就像上次垃圾收集一样,其中一个Survivor区用作source,被对象填充,另一个Survivor区用作destination。

对象一直在两个Survivor区之间来回拷贝,直到拷贝次数达到一定值或者一个Survivor没有足够的空间,这些对象将被拷贝到Old区。这个过程叫做“老化”。

性能考量

垃圾收集的主要主要衡量指标是吞吐量和延迟。

吞吐量 = 垃圾收集时间 / (总时间-垃圾收集时间)

延迟,是程序的响应能力。垃圾收集的停顿影响响应能力。