[Java JVM] Hotspot GC研究- 探寻GC代码前的最后准备, GC的维度和分类

关于GC的几个维度

  1. 吞吐量(Throughput), 简而言之就是花在GC之外的时间(干 ‘正事’的时间)和程序整体运行时间的比值. 举写代码为例, 一位连续编码7天, 需要花1天时间来休息的程序员, 相比于编码1天就要休息1天的程序员来说, 显然前者的吞吐量更大.
  2. 停顿时间(Pause Time), GC执行的时候有可能需要打断应用执行, 比如GC安全点, 对应用来说就是被冻住了, 不能响应. 所以停顿时间越小越好. 这个指标和吞吐量没有直接的关系. 比如10天时间内, A工作8天需要休息2天, 如此往复. 而B每工作一天就要休息一天, 总体来看, B在10天内只工作了5天. 一般情况下我们可能认为A的好, 因为产量高, 但是在一些特殊场合, 比如不允许出现2天的空闲状态的时候, B却能满足要求, 所以没有场景的话很难评价哪个更好. 放到实际情况中, 比如做大数据批处理的应用, 可能关注的是吞吐量, 中间停顿长一点没关系, 只要整体时间范围内, 比如一天, 处理尽量多的数据即可. 而对于后台服务来说, 停顿时间可能显得更为重要, 比如用户的http请求如果被GC卡住得不到响应的话, 体验是很糟糕的.
  3. 收集频率(Frequency of collection). 一段时间内GC的次数, 收集的时候不可避免地要打断程序执行, 一般情况下, 对应用程序的打扰越少越好.
  4. Footprint(不好翻译, 可以理解为开销). 主要指GC过程需要的资源, 比如扫描过程中辅助型的内存等. 其中有的资源是GC时分配的, 比如mark stack用来遍历对象图; 有的资源是heap初始化的时候就要分配好的, 比如marking card, 用来标记某块内存区域发生了对象赋值.
  5. 及时性(Promptness). 指对象成为垃圾之后, 到其被回收之前这段时间. 注意, 死亡的对象延迟回收是允许的. 死亡对象的存在不会影响heap的完整和正确性. 换一个角度来说, 这也好理解, GC只在特定的时间才会触发, 期间程序运行, 很多对象产生, 很多对象消亡. 这些消亡的对象, 只有等待GC时候才会被回收. 另一方面, 有些整理型的算法会移动对象, 为了兼顾效率, 可能会有意的留一些死亡对象保持在原来位置, 以此来避免过多的对象移动. 这些对象只有在特定的时间或者非常时刻, 才会被回收掉.

GC的分类

串行(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)的, 读起来和翻译起来很拗口, 我们可以不关注名词本身, 只需要关注名词后面的含义:

  1. 大部分分配的对象都不会被引用很久, 他们分配后用了一段时间就死了. 比如方法内部分配的局部变量, 出了函数就死了.
  2. 年长的对象只引用少部分最近分配的对象.

弱代假设是根据大部分程序的运行时行为统计的出来的, 根据上面的假设, hotspot大部分收集器都设计成分代收集, 把heap分成不同的区域, 举Serial GC 和Parallel GC为例, 默认参数下, 年轻代和老年代的比例为1:2, 即3G的heap下, 年轻代1G, 老年代2G. 因为年轻代的对象生生死死比较快, 收集的频率高, 为了避免GC需要花费过长时间, 所以设置的比老年代要小. 而老年代的收集(Full GC)只在万不得已的情况下才会触发, 一旦触发, 花费的时间也相对较长.

需要留意的是, 分代的情况下, 大部分的时候只要执行年轻代的GC就能满足需要了, 年轻代的GC通常耗时很短(几十毫秒到一两百毫秒), 甚至可以做到不触发老年代的GC. 其次, 不同的代可以采用不同的算法, 串行/并行, 整理/不整理等. 实际使用时需要根据应用的特点进行选择, 之所以存在这么多类型的算法, 说明了一点, 目前还不存在能满足所有需求的一个算法.

你可能感兴趣的:(Hotspot学习)