• 本文仅代表Hotspot VM,参考自 
  • Hotspot使用分代回收策略
    • 当一个对象不能被当前运行程序中的任何指针指向时,会被认为是垃圾。更具体的说,Hotspot使用Tracing算法来判断,所有根引用不可达的对象就是垃圾。
      • 根引用指:Class对象、当前线程和活动的线程块、monitor(所有执行了wait()/notify()或synchronized锁住的对象)、本地变量、Finalizer(重写了finally的对象会有)、JVM栈的帧(包含局部变量和操作数栈)
    • (上图指的是JDK8除Parallel和G1算法之外的分代内存描述,JDK8开始移除了永生代的概念)
    • Minor collection:收集young generation(=Eden+S1+S2)空间的垃圾
      • 一般发生在JVM无法为新对象分配内存时(新对象总是在Eden分配内存),例如Eden满时。
      • young generation中“活的”对象越少,minor collection越快
      • 一些对象会在多次minor collection后,移动到tenured generation
    • Major collection:当tenured generation充满时触发,清理整个heap的内存(也称Full GC)
  • 不同的垃圾回收算法
    • HotSpot VM of JDK8提供了三种(也可以说四种)回收算法
    • Serial collector:单线程回收,适用于单核CPU或小内存应用。
      • JVM Client模式默认垃圾回收器
      • 对应-XX:+UseSerialGC
    • Parallel collector(也叫Throughput collector):允许Minor GC并行。其中包含的Parallel compaction允许Major GC并行。
      • JVM Server模式默认垃圾回收器
      • 对应-XX:+UseParallelGC
      • Parallel compaction默认开启,使用 -XX:-UseParallelOldGC可以关闭
    • Mostly concurrent collectors:旨在最大化降低GC暂停时间(尤其是老生代),包括两种
      • Concurrent Mark Sweep (CMS) Collector
        • 对应-XX:+UseConcMarkSweepGC
      • Garbage-First(G1) Garbage Collector
        • 对应-XX:+UseG1GC
    • 如何选择垃圾回收算法?
      • 除非应用有严格的暂停时间限制,建议首先让VM自己选择,如果需要的话调整heap大小。如果此时仍不能满足性能要求,尝试采用下面的方式:
      • 如果应用内存小于100M,使用Serial collector
      • 如果应用在一个处理器核心上运行,并且没有暂停时间限制,建议让VM自己选择或者使用Serial collector
      • 如果注重峰值时的程序性能,并且没有暂停时间限制(可以忍受1s或以上的暂停),建议让VM自己选择或者使用Parallel collector
      • 如果注重程序响应时间(即不能忍受暂停时间),使用CMS或G1
  • 内存的分配
    • 为了提高速度,使用了指针碰撞(bump-the-pointer)技术:跟踪上一次分配内存的对象的末尾地址,当新对象分配内存时仅需判断剩余空间是否足够,足够则按顺序继续分配,否则执行Minor GC。
    • 为了提高多线程分配速度,使用了TLABs(Thread-Local Allocation Buffers,线程本地分配缓冲)技术:每个线程在Eden区都有一小块私有空间,分配内存时可以实现无锁分配。(除非TLABs区满)
  • Serial collector
    • 新生代
      • 当Eden满时触发GC,此时会把Eden中活着的对象和Survivor1区(每个时刻总会有一个Survivor区为空,这里假设此时Survivor1区在用,Survivor2区为空)活着的对象迁移到Survivor2(如果放不下,多余的对象会放入Tenured区)。下一次触发GC时就会把Eden和Survivor2区活着的对象迁移到Survivor1。以此类推。
    • 老生代
      • 使用“标记-清除-整理”算法:
        • 1. 标记老年代存活的对象
        • 2. 清理死亡的对象
        • 3. 整理内存空间,使存活的对象连续存放
  • Parallel collector
    • 新生代
      • 算法与上面相同,不过允许并行(具体细节待补充),可以充分利用多核优势
    • 老生代(Parallel compaction)
      • 执行三个阶段:标记-汇总-压缩(mark – summary – compaction)
        • 在标记阶段,运行中代码的直接引用对象(根对象)会根据gc线程数拆分,使标记阶段并行执行。
        • 在汇总阶段,由于老生代中存活的对象往往集中在左侧,左侧的一些小碎片是不值得去压缩的。因此首先会找到值得压缩的临界点:其左侧的对象维持不动,右侧才会移除垃圾对象。
          • 注意这个阶段目前(JDK5)是串行执行的,因为此阶段相比其他两个阶段耗时较短
        • 在压缩阶段,各线程并行的拷贝压缩,形成左密右空的内存布局。
    • 细节:并行GC算法导致Major GC(Full GC)的情况
      • 1. 要(从新生代)晋升的对象大小大于老生代剩余空间时
      • 2. 老生代的剩余空间小于平均晋升对象大小时
  • CMS collector
    • 新生代
      • 同Parallel collector
    • 老生代
      • 大部分老生代的CMS GC是和程序代码并发(concurrent)执行的
      • 主要步骤
        • 1. initial-mark,暂停JVM,找到所有的root根的引用,标记在一个bitmap中(单线程执行)
        • 2. concurrent-mark,恢复程序运行,并发的标记所有根引用的可达对象(还有处理young-old之间的引用等细节)
        • 3. remark,由于上一步程序又执行了一段时间,需要再次暂停JVM,并更新这段时间修改过的对象状态(实现关键词:incremental update write barrier(多线程执行)
        • 4. concurrent-sweep,恢复程序运行,直接释放掉垃圾对象
      • 注意,CMS算法释放垃圾对象之后不做内存压缩,因此需要建立一个可用内存列表用来分配内存。这也使得分配内存(即从新生代晋升)的性能下降。
      • CMS并不是当老生代变满时进行GC,而是根据统计数据(历史GC耗时,老生代何时充满)自己决定。同时如果老生代的使用率超过initiating occupancy(默认值68%)时,也会触发GC。
      • CMS还提供了增量模式(Incremental Mode)的选项,开启后在CMS的并发阶段(上图中的Concurrent Mark和Concurrent Sweep)会周期性的暂停,目的是减少对程序线程的干扰。通常用于单核或少核CPU。
  • Garbage-First(G1) Collector
    • 由于Hotspot缺乏相关文档,此处对G1的简单阐述针对算法本身
    • 内存空间分配
      • 整个heap被分成了若干个等大连续(指虚拟地址)的内存块(region)。这些区域被映射为逻辑上的 Eden, Survivor, tenured(old) , Humongous regions(用于存储大对象)和一些暂未使用的空间。
    • 收集步骤(来自RednaxelaFX,见 http://hllvm.group.iteye.com/group/topic/44381
      • 1. 全局并发标记(global concurrent marking) 
        • 初始标记(initial marking):暂停阶段。找到所有的root根的引用,压入扫描栈中等到后续扫描。
        • 并发标记(concurrent marking):并发阶段。递归扫描扫描栈的引用,标记所有可达对象。
        • 最终标记(final marking/remarking):暂停阶段。处理上个阶段的增量更新,实现关键词SATB write barrier(Snapshot-At-The-Beginning),此处的算法优于CMS。
        • 清理(cleanup):暂停阶段。在marking bitmap里统计每个region被标记为活的对象有多少。
      • 2.  拷贝存活对象(evacuation) 
        • 全暂停阶段。自由选择任意多个region来独立收集构成收集集合(collection set,简称CSet)。选定CSet后,采用并行copying(类似Parallel Young GC)算法把CSet里每个region里的活对象拷贝到新的region里。
        • CSet的选定:
          • Young GC:选定所有young gen里的region。通过控制young gen的region个数来控制young GC的开销。 
          • Mixed GC:选定所有young gen里的region,外加根据global concurrent marking统计得出收集收益高的若干old gen region。在用户指定的开销目标范围内尽可能选择收益高的old gen region。
      • 分代式G1的正常工作流程就是在young GC与mixed GC之间视情况切换。如果old gen填满无法继续进行mixed GC,会切换到G1之外的serial old GC来收集整个heap。
    • 其降低延迟的关键,就是不会evacuate所有有活对象的region(虽然标记使全局的),通过只选择收益高的少量region来evacuate,这种暂停的开销就可以(在一定范围内)可控
  • 补充
    • GC是如何暂停所有线程的?
      • 每个被JIT编译过后的方法会在一些特定的位置(例如循环回跳处、方法临返回前 / 调用方法时、可能抛异常的位置)记录下OopMap(Hotspot用于标记一块数据是不是指针的数据结构),记录了执行到该方法的某条指令时,栈上和寄存器里哪些位置是引用。这些位置叫做“安全点”(safepoint)。因此,JIT编译后的代码只能在safepoint处进入GC。 
        • 注意:解释执行的方法(即非热点代码)可以在任意字节码边界上进入GC,此时的OopMap由解释器自动生成
      • 中断方式通常采用主动式中断(Voluntary Suspension):每个线程执行到安全点时会插入一条汇编指令,检测一块内存地址是否可读,如果不可读,线程就会主动暂停。
        • 注意:如果一个线程正在睡眠/阻塞/执行native代码,主动式中断不会生效。因此一个线程在进入上述状态时,JVM会标记这个线程进入安全区域(Safe Region),使得主动式中断不去关心这个线程。在线程退出上述状态时,需要检查GC标记。
      • 关于OopMap参见 http://rednaxelafx.iteye.com/blog/1044951
      • 关于Safepoint参见 http://zhihu.com/question/34341582/answer/58444959  http://zhihu.com/question/29268019/answer/43762165 (此处要给R大再次鼓掌)