串行(Serial) vs 并行(Parallel)
串行, 比较简单, 事情一件一件按顺序做, 单线程执行GC的所有工作.
并行, 多个线程一起负责GC的工作, 通常是每个线程分担一段不重叠的部分, 这样的话可以减少线程间的干扰和同步开销.
并发(Concurrent) vs Stop The World
并发, 是指应用的代码在运行时, GC的工作线程同时也在工作, 比如CMS收集器就属于这种. 不过并不是所有的GC阶段都可以和应用代码并发, 只是其中部分过程(比如标记阶段). 通过GC安全点的介绍我们知道为了heap的一致和确定性, 不可避免的要阻塞应用线程, 并发的收集可以使打断造成的不良影响降低.
整理(Compacting) vs 不整理(Non-compacting)
熟悉C式内存的管理可能都接触过内存碎片(fragmentation)这个概念, 在分配之前, 未使用的内存是连续的一段, 随着malloc的分配和释放, 逐渐就有了很多空洞, 空洞前后都是使用中的内存, 夹在其中的是被分配使用完后又释放了的一块内存. 不连续的空洞多了之后, 会造成分配的过程变得复杂, 比如大块拆小块, 释放的时候相邻小块合并成大块等. 除此之外, 还可能造成一种后果: 比如存在100个1k的空洞, 但是却无法分配一块连续的2k的空间这种情况.
Compacting型的算法, 就是在GC的时候, 会把或者的对象向一端移动, 使对象紧凑的挨在一起, 消除内存碎片带来的问题. 除了避免了碎片问题外, 对象的分配也变得简单多了, 只需要维护一个top和end指针即可, 当需要分配的对象大小加上top不超过end时, 直接返回当前的top值, 然后将top往end端移动对象大小的字节偏移即可. 即使在并发情况下, 也只需要CAS(compare and swap)指令就能完成. 不过凡事有利有弊, 因为要移动对象, 对象的位置发生了改变, 必须同步修改引用对象的那些位置, 为了维持一致性, 通常需要在GC安全点的环境下移动对象, 当要整理的heap很大时, 将耗费较长的时间.
Non-compacting型的算法就是另一种, 不整理的, 空闲区域一般通过链表进行管理. 相比于Compacting的方式, 需要维护额外的链表结构, 并且分配也变得复杂, 同时也可能引发内存碎片问题. 另一方面, 由于不移动对象, 节省了内存copy的时间, 同时对象的释放可以和应用的代码一起执行, 这是它的一个优势.
分代收集的概念是基于弱代假设(weak generational hypothesis)的, 读起来和翻译起来很拗口, 我们可以不关注名词本身, 只需要关注名词后面的含义:
- 大部分分配的对象都不会被引用很久, 他们分配后用了一段时间就死了. 比如方法内部分配的局部变量, 出了函数就死了.
- 年长的对象只引用少部分最近分配的对象.
弱代假设是根据大部分程序的运行时行为统计的出来的, 根据上面的假设, hotspot大部分收集器都设计成分代收集, 把heap分成不同的区域, 举Serial GC 和Parallel GC为例, 默认参数下, 年轻代和老年代的比例为1:2, 即3G的heap下, 年轻代1G, 老年代2G. 因为年轻代的对象生生死死比较快, 收集的频率高, 为了避免GC需要花费过长时间, 所以设置的比老年代要小. 而老年代的收集(Full GC)只在万不得已的情况下才会触发, 一旦触发, 花费的时间也相对较长.
需要留意的是, 分代的情况下, 大部分的时候只要执行年轻代的GC就能满足需要了, 年轻代的GC通常耗时很短(几十毫秒到一两百毫秒), 甚至可以做到不触发老年代的GC. 其次, 不同的代可以采用不同的算法, 串行/并行, 整理/不整理等. 实际使用时需要根据应用的特点进行选择, 之所以存在这么多类型的算法, 说明了一点, 目前还不存在能满足所有需求的一个算法.