Java-JVM-GC

原文链接: https://blog.csdn.net/renfufei/article/details/49230943

Java-JVM-GC

转载声明

本文大量内容系转载自以下文章,有删改,并参考其他文档资料加入了一些内容:

  • 快速解读GC日志
    作者:铁锚
    出处:CSDN

  • 什么时候触发MinorGC?什么时候触发FullGC?
    作者:summerZBH123
    出处:CSDN

  • Minor GC、Major GC和Full GC之间的区别
    作者:javacodegeeks
    出处:程序猿DD

  • Linux下Tomcat开启查看GC信息
    作者:阿龙
    出处:cnblogs

  • 从实际案例聊聊Java应用的GC优化
    作者:录录
    出处:美团技术团队

  • JVM原理讲解和调优
    作者:小小水滴

  • HotSpot VM GC收集器的易混淆的名称问题
    作者:anranran
    出处:51CTO

  • Java对象内存分配策略
    作者:用户5673393671

  • JVM 垃圾回收GC Roots Tracing
    作者:mine_song

  • [资料] 名词链接帖 [占位ing]
    作者:RednaxelaFX

  • 可能是最全面的G1学习笔记
    作者:javaadu

  • java垃圾回收以及jvm参数调优概述
    作者: 大鹏展翅

摘要

本文是 Plumbr 发行的 Java垃圾收集指南 的部分内容。文中将介绍GC日志的输出格式, 以及如何解读GC日志, 从中提取有用的信息。

1 堆内存划分

堆内存划分为 Eden、Survivor 和 Tenured/Old 空间
Java-JVM-GC_第1张图片
Java-JVM-GC_第2张图片
Java-JVM-GC_第3张图片

1.1 新生代(Young Generation)

  • 朝生夕灭-复制算法
    大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以快速完成回收。
  • 新生代分区
    一个Eden区,两个Survivor区(一般而言),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区中的一个。当一个Survivor区满时,此区的存活且不满足“晋升老年代”条件的对象将被复制到另外一个Survivor区。
  • 新生代年龄
    新生代对象每经历一次Minor GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在Serial和ParNew GC两种回收器中,“晋升年龄阈值”通过参数MaxTenuringThreshold设定,默认值为15。
  • MinorGC
    新生代GC称为MinorGC

1.2 老年代(Old Generation)

  • 来源
    在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代。
  • 老不死
    该区域中对象存活率高。
  • 回收算法
    老年代的垃圾回收(又称Major GC)通常使用“标记-清理”(CMS)或“标记-整理(压缩)”算法。
  • FullGC
    包括新生代和老年代的垃圾回收称为Full GC(HotSpot VM里,除了CMS MajorGC之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新生代)。

1.3. 永久代(Perm Generation)

  • 概念
    主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。
  • 问题
    绝大部分 Java 程序员应该都见过 java.lang.OutOfMemoryError:PermGenspace这个异常。这里的 PermGen space其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有“PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。

1.4 Metaspace(元空间)

1.4.1 永久代->元空间

由于永久代实现方法区有不少问题,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。

1.4.2 改变原因

  1. 字符串存在永久代中,容易出现性能问题和内存溢出。
  2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
  3. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  4. Oracle 可能会将HotSpot 与 JRockit 合二为一。

1.4.3 元空间的特点

  • 充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致。
  • 每个类加载器有专门的存储空间。如果GC发现某个类加载器不再存活了,会把该类加载器相关的空间整个回收掉,而不会单独回收某个类。
  • 只进行线性分配,元空间里的对象的位置是固定的
  • 省掉了GC扫描及压缩的时间

1.4.4 元空间的内存分配模型

  • 绝大多数的类元数据的空间都从本地内存中分配
  • 用来描述类元数据的类也被删除了
  • 分元数据分配了多个虚拟内存空间
  • 给每个类加载器分配一个内存块的列表。块的大小取决于类加载器的类型; sun/反射/代理对应的类加载器的块会小一些
  • 归还内存块,释放内存块列表
  • 一旦元空间的数据被清空了,虚拟内存的空间会被回收掉
  • 减少碎片的策略

2 GC重要概念

2.1 GC Roots

Java-JVM-GC_第4张图片

2.1.1 GC Roots搜索 - GC Roots Tracing

GC Roots搜索(即Gc Roots Tracing)算法的基本思路就是通过一系列名为GC Roots的引用作为起始点,从这些节点开始向下搜索,通过引用关系遍历对象图。搜索所走过的路径称为引用链(Reference Chain),可达的对象就判定为活着的,不可被回收。而其余对象就是GC Roots不可达对象(即一个对象没有任何引用链相连),则证明此对象是不可用的,可以被回收。

比如上图中左侧的ABCD皆从GC Roots可达,不能回收;右侧的Object IFGH皆从GC Roots不可达,所以可以回收。

2.1.2 GC roots的选择

目前HotSpot主要使用两类作为GC roots:

  1. 全局性引用
    • 方法区中的常量引用的对象
    • 方法区中的类的静态属性引用的对象
  2. 线程执行上下文(包括阻塞和正在运行的线程)
    • JVM栈帧中的本地(局部)变量表中引用的对象。即当前所有正在被调用的方法的引用类型的参数/局部变量/临时值
  3. JNI
    • 本地方法栈中JNI(即Native方法)引用的对象
    • JNI 全局
  4. 其他

2.1.3 找到GC Roots 的方法

  1. 扫描一遍线程栈找出所有引用
  2. 扫描一遍老年代对应的内存页中的脏状态页,找出所有引用作为GC Roots。
  3. 把这些GC Roots指向的对象以字节方式复制到新区域A
  4. 复制完成后后扫描区域A,再次得到他们的引用的集合,
  5. 再次重复第三步复制,直到某次扫描找不到新的引用为止。

更多可查看Garbage Collection Roots

2.1.4 注意事项

  • 枚举根节点会STW,因为一致性视图的要求,不能出现枚举根节点分析过程中对象引用关系还在不断变化的情况。
  • HotSpot使用一组成为OopMap的数据结构来存储引用,帮助快速准确地完成GC Roots枚举。

2.1.5 OopMap

因为可作为GC Roots的节点主要是在全局性引用(如常量或静态属性)与执行上下文(如栈帧中的本地变量表)中,范围大数据量大,不可能挨个检查引用。又由于HotSpot采用准确式GC(即知道某数据是什么类型,比如内存中有一个32位整数123456,JVM可以分辨出到底是数值还是reference类型指向123456的内存地址),所以当系统STW时,不需要全局查找,而只需要使用OopMap来查找。

具体来说,类加载完成时,HotSpot VM就会将对象内什么偏移量是什么类型的数据计算完毕,在JIT编译过程中也会在特定位置记录栈和寄存器中哪些位置是引用。这样,HotSpot就能知道哪些位置数据是引用了。在GC时,可以直接使用。

有了OopMap,HotSpot就能快速准确完成GC Roots枚举了。

2.2 SafePoint

可参考Java-JVM-安全点SafePoint

2.2.1 什么是SafePoint

为每条指令生成OopMap开销太大,所以只是在特定位置记录信息,这些位置被称为安全点(SafePoint),具体是程序执行时并非在所有地方都能停止并开始GC,而是程序在达到sSafePoint的时候才能暂停并开始GC。

2.2.2 SafePoint产生位置

SafePoint的选定不能太少而让GC等待太久,也不能过密以使得运行时过多GC。一句话,安全点选定标准是以是否具有让程序长时间执行的特征。具体来说,普通指令流执行速度很快,不能长时间运行,所以一般在方法调用、循环跳转、异常跳转等指令选择产生SafePoint。

2.2.3 SafePoint如何实现

GC时让所有线程(不包括JNI调用线程)都到最近的SafePoint并停止的主流方法是主动式中断:
当GC需要中断线程时,不直接对线程操作,而是设一个标志位,每个线程都主动去轮询该标志,发现中断标志就本线程中断并挂起。该轮询标志的地方和安全点就是重合的。

2.3 SafeRegion

当线程没有分配CPU时间(Sleep或Blocking状态时),无法响应JVM中断请求从而到无法到安全点挂起。此时不可能一直等待线程重新分配CPU时间继续执行。此时就轮到SafeRegion发威了。

SafeRegion指在一段代码片段中引用关系不会改变,那么在该区域中任何地方开始GC都是安全的。

线程在执行到SafeRegion代码时,会标识自己进入SafeRegion,GC时就认为该线程可以开始GC。

线程在离开SafeRegion的时候需要判断GC Roots枚举或整个GC过程是否完成,如果已经完成就继续执行程序, 否则就必须等待直到收到可以安全离开SafeRegion的信号为止。

2.4 finalize

即使在可达性分析算法中的不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“死刑缓刑”阶段。而要真正宣告一个对象死亡,至少要经历两次标记过程。

  • 第一次标记
    如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,并且进行一次筛选。筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过(任何对象的finalize()方法都只会被调用一次!),虚拟机将这两种情况都视为“没有必要执行”。(即意味着直接回收该对象)

  • F-Queue
    如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由JVM自动建立的、低优先级的Finalizer线程去执行它。**这里所谓的“执行”是指JVM会触发F-Queue队列中的对象调用finalize方法,但并不承诺会等待该方法运行结束。**原因是如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个GC系统崩溃。

  • 第二次标记
    finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可。譬如把自己(this关键字)赋值给某个类变量(方法区的类变量属于GC Roots)或者对象的成员变量(栈区的局部变量表中引用对象属于GC Roots),那在第二次标记时它将被移除出“即将回收”的集合;

    如果对象这时候还没有逃脱,那基本上它就真的被回收了。

  • 一般不要尝试用finalize做重要的工作
    如前所述,finalize方法只能保证触发不能保证被执行完,所以不要尝试用finalize做重要的工作。

    之所以要使用finalize(),是存在着垃圾回收器不能处理的特殊情况。假定你的对象(并非使用new方法)获得了一块“特殊”的内存区域,由于垃圾回收器只知道那些显示地经由new分配的内存空间,所以它不知道该如何释放这块“特殊”的内存区域,那么这个时候Java允许在类中定义一个由finalize()方法。

    特殊的区域例如:

    • 由于在分配内存的时候可能采用了类似C语言的做法,而非JAVA的通常new做法。这种情况主要发生在native method中,比如native method调用了C/C++方法malloc()函数系列来分配存储空间,但是除非调用free()函数,否则这些内存空间将不会得到释放,那么这个时候就可能造成内存泄漏。但是由于free()方法是在C/C++中的函数,所以finalize()中可以用本地方法来调用它。以释放这些“特殊”的内存空间。
    • 又或者打开的文件资源,这些资源不属于垃圾回收器的回收范围。

2.5 引用与GC关系

  • 强引用
    存在则不会被GC

  • 软引用
    内存溢出前会再次对软引用对象进行GC,如果还是内存不够则会抛出OOM。

    该引用过多会导致老年代中存活对象过多,FullGC时间长

  • 弱引用
    每次GC都会干掉弱引用对象。一般可用来做缓存,如WeakHashMap,

  • 虚引用
    随时可能被干掉,能通过引用队列获取到通知

2.6 Java对象内存分配策略

2.6.1 概述

  • 对象在堆上内存分配,主要是新生代Eden区。
  • 如果启动了本地线程分配缓冲,则将线程优先在TLAB上分配。
  • 少数情况下也可能会直接分配在老年代中
  • 分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。
  • 本文中的内存分配策略指的是Serial / Serial Old收集器下(ParNew / Serial Old收集器组合的规则也基本一致)的内存分配和回收的策略。

2.6.2 对象优先在Eden分配

  • 概述
    多数情况下,对象优先在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

  • 代码示例:

private static final int _1MB = 1024 * 1024;  
/**  
 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 
  */  
public static void testAllocation() {  
     byte[] allocation1, allocation2, allocation3, allocation4;  
     allocation1 = new byte[2 * _1MB];  
     allocation2 = new byte[2 * _1MB];  
     allocation3 = new byte[2 * _1MB];  
     allocation4 = new byte[4 * _1MB];  // 出现一次Minor GC  
 } 
  • 运行结果GC日志:
[GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
Heap  
 def new generation   total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000)  
  eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)  
  from space 1024K,  14% used [0x032d0000, 0x032f5370, 0x033d0000)  
  to   space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)  
 tenured generation   total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000)  
   the space 10240K,  60% used [0x033d0000, 0x039d0030, 0x039d0200, 0x03dd0000)  
 compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)  
   the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)  
No shared spaces configured.  
  • 配置说明
    代码的testAllocation()方法中,尝试分配3个2MB大小和1个4MB大小的byte数组对象。在运行时通过-Xms20M-Xmx20M-Xmn10M这3个参数限制了Java堆大小为20MB,年轻代10MB,老年代10MB。-XX:SurvivorRatio=8决定了新生代中Eden区与两个Survivor区的空间比例是8:1:1,从输出的结果也可以清晰地看到eden space 8192Kfrom space 1024Kto space 1024K的信息,新生代总可用空间为def new generation total 9216K(即Eden区+1个Survivor区的总容量)。

  • 执行结果说明
    执行testAllocation()中分配allocation4对象的语句时会因为给allocation4分配内存时Eden区剩余内存不足而发生一次Minor GC,这次GC的结果是新生代6651KB变为148KB,而总内存占用量则几乎没有减少(因为allocation1、allocation2、allocation3三个对象都是存活的,虚拟机几乎没有找到可回收的对象)。

    GC发现已有的3个2MB对象全部无法放入1MB大小的Survivor,所以只好通过分配担保机制提前晋升到老年代。

    本次GC结束后,4MB的allocation4对象可顺利分配在GC后的Eden中,因此程序执行完的结果是Eden占用4MB,Survivor基本空闲,老年代被分配担保机制晋升的3个2MB对象占用6MB。

2.6.3 大对象直接进入老年代

  • 概述
    所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。

    大对象对虚拟机的内存分配来说就是一个坏消息(比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发GC以获取足够的连续空间来安置大对象。

    虚拟机提供了一个-XX:PretenureSizeThreshold参数,大于该阈值的对象直接在老年代分配,做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

  • 代码示例:

private static final int _1MB = 1024 * 1024;   
/**  
 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 
 * -XX:PretenureSizeThreshold=3145728 
 */  
public static void testPretenureSizeThreshold() {  
  byte[] allocation;  
  allocation = new byte[4 * _1MB];  //直接分配在老年代中  
} 
  • 运行结果GC日志:
1.Heap  
2.def new generation   total 9216K, used 671K [0x029d0000, 0x033d0000, 0x033d0000)  
3.eden space 8192K,   8% used [0x029d0000, 0x02a77e98, 0x031d0000)  
4.from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)  
5.to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
6.tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)  
7.the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)  
8.compacting perm gen  total 12288K, used 2107K [0x03dd0000, 0x049d0000, 0x07dd0000)  
9.the space 12288K,  17% used [0x03dd0000, 0x03fdefd0, 0x03fdf000, 0x049d0000)  
10.No shared spaces configured. 
  • 执行结果说明
    执行代码中的testPretenureSizeThreshold()方法后,Eden空间几乎没有被使用,而老年代的10MB空间被使用了40%,也就是4MB的allocation对象直接就分配在老年代中,这是因为PretenureSizeThreshold=3MB,超过的对象都会直接在老年代进行分配。

    注意:PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数,Parallel Scavenge收集器一般并不需要设置。但并不是大对象直接进入老年代分配策略对其就是无效的,在Parallel Scavenge中自有它的实现,大约等于Eden区域一半的对象会被认成大对象。感兴趣的可以点击这里

    如果遇到必须使用此参数的场合,可以考虑ParNew加CMS的收集器组合。

2.6.4 长期存活的对象将进入老年代

  • 概述
    为了在内存回收时能识别哪些对象应放在新生代,哪些对象应放在老年代中。虚拟机给每个对象定义了一个对象年龄(Age)计数器,对象年龄的判定规则如下:

    1. 如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。
    2. 对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。
    3. 对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
  • 代码示例:

    private static final int _1MB = 1024 * 1024;  
    /**  
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 
     * -XX:+PrintTenuringDistribution  
     */  
    @SuppressWarnings("unused")  
    public static void testTenuringThreshold() {  
      byte[] allocation1, allocation2, allocation3;  
      allocation1 = new byte[_1MB / 4];    
       // 什么时候进入老年代取决于XX:MaxTenuringThreshold设置  
      allocation2 = new byte[4 * _1MB];  
      allocation3 = new byte[4 * _1MB];  
      allocation3 = null;  
      allocation3 = new byte[4 * _1MB];  
    } 
    
  • MaxTenuringThreshold=1参数来运行的结果:

    1.[GC [DefNew  
    2.Desired Survivor size 524288 bytes, new threshold 1 (max 1)  
    3.- age   1:     414664 bytes,     414664 total  
    4.: 4859K->404K(9216K), 0.0065012 secs] 4859K->4500K(19456K), 0.0065283 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]   
    5.[GC [DefNew  
    6.Desired Survivor size 524288 bytes, new threshold 1 (max 1)  
    7.: 4500K->0K(9216K), 0.0009253 secs] 8596K->4500K(19456K), 0.0009458 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
    8.Heap  
    9.def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)  
    10.eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)  
    11.from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)  
    12.to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    13.to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    14.to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    15.to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    16.tenured generation   total 10240K, used 4500K [0x033d0000, 0x03dd0000, 0x03dd0000)  
    17.the space 10240K,  43% used [0x033d0000, 0x03835348, 0x03835400, 0x03dd0000)  
    18.compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)  
    19.the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)  
    20.No shared spaces configured. 
    
  • 以MaxTenuringThreshold=15参数来运行的结果:

    1.[GC [DefNew  
    2.Desired Survivor size 524288 bytes, new threshold 15 (max 15)  
    3.- age   1:     414664 bytes,     414664 total  
    4.: 4859K->404K(9216K), 0.0049637 secs] 4859K->4500K(19456K), 0.0049932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
    5.[GC [DefNew  
    6.Desired Survivor size 524288 bytes, new threshold 15 (max 15)  
    7.- age   2:     414520 bytes,     414520 total  
    8.: 4500K->404K(9216K), 0.0008091 secs] 8596K->4500K(19456K), 0.0008305 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
    9.Heap  
    10.def new generation   total 9216K, used 4582K [0x029d0000, 0x033d0000, 0x033d0000)  
    11.eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)  
    12.from space 1024K,  39% used [0x031d0000, 0x03235338, 0x032d0000)  
    13.to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    14.tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)  
    15.the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)  
    16.compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)  
    17.the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)  
    18.No shared spaces configured.  
    
  • 运行结果分析:
    分别以两种设置来执行代码中的testTenuringThreshold()方法,allocation1对象需要256KB内存,Survivor空间可以容纳。

    MaxTenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年代,新生代已使用的内存GC后非常干净地变成0KB。

    MaxTenuringThreshold=15时,第二次GC发生后,allocation1对象则还留在新生代Survivor空间,这时新生代仍然有404KB被占用。

2.6.5 动态对象年龄判定

  • 概述
    为了能更好地适应不同程序的内存状况,JVM并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,所以引入了动态对象年龄判定的概念:

    当在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半时,年龄大于或等于该年龄的对象就可以直接进入老年代,无须再等到MaxTenuringThreshold中要求的年龄。

  • 代码示例

    private static final int _1MB = 1024 * 1024;  
    /**  
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 
     * -XX:+PrintTenuringDistribution  
     */  
    @SuppressWarnings("unused")  
    public static void testTenuringThreshold2() { 
      byte[] allocation1, allocation2, allocation3, allocation4;  
      allocation1 = new byte[_1MB / 4];   
        // allocation1+allocation2大于survivo空间一半  
      allocation2 = new byte[_1MB / 4];    
      allocation3 = new byte[4 * _1MB];  
      allocation4 = new byte[4 * _1MB];  
      allocation4 = null;  
      allocation4 = new byte[4 * _1MB];  
    } 
    
  • 运行结果:

    1.[GC [DefNew  
    2.Desired Survivor size 524288 bytes, new threshold 1 (max 15)  
    3.- age   1:     676824 bytes,     676824 total  
    4.: 5115K->660K(9216K), 0.0050136 secs] 5115K->4756K(19456K), 0.0050443 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]   
    5.[GC [DefNew  
    6.Desired Survivor size 524288 bytes, new threshold 15 (max 15)  
    7.: 4756K->0K(9216K), 0.0010571 secs] 8852K->4756K(19456K), 0.0011009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
    8.Heap  
    9.def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)  
    10.eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)  
    11.from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)  
    12.to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    13.tenured generation   total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000)  
    14.the space 10240K,  46% used [0x033d0000, 0x038753e8, 0x03875400, 0x03dd0000)  
    15.compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)  
    16.the space 12288K,  17% used [0x03dd0000, 0x03fe09a0, 0x03fe0a00, 0x049d0000)  
    17.No shared spaces configured. 
    
  • 运行结果说明
    执行代码中的testTenuringThreshold2()方法,并设置-XX:MaxTenuringThreshold=15,会发现运行结果中Survivor的空间占用仍然为0%,而老年代比预期增加了6%,也就是说,allocation1、allocation2对象都直接进入了老年代,而没有等到15岁的临界年龄。因为这两个同Age的对象加起来已经到达了512KB达到了其中一个Survivor区大小一半,触发了动态对象年龄判定。

    我们只要注释掉其中一个对象new操作,就会发现另外一个就不会晋升到老年代中去了。

2.6.6 HandlePromotionFailure空间分配担保

新生代Survivor不够时需要依赖老年代HandlePromotion,如果MinorGC时,目的地Survivor无足够空间存放GC后还存活的对象,则需要通过HandlePromotion机制直接晋升到老年代。

具体来说,在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于或HandlePromotionFailure设置不允许,那这时也要改为进行一次Full GC。

下面解释一下“冒险”是冒了什么风险,前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure),那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。

2.7 名词解释

2.7.1 STOP THE WORLD(STW)

当负责GC的线程进行tracing 标记(tracks object references),或者移动标记对象等操作的时候,GC线程必须确保应用程序线程(Application threads)没有正在使用这些对象。当GC线程移动一个对象的时候,往往会导致对象在内存(堆)中的地址变化,这就会导致应用程序线程再也没有办法访问到这些对象;为了防止这种现象的发生,在GC 过程中, 应用程序必须暂停执行,称为STW(The pauses when all application threads are stopped are called stop-the-world pauses.)。

2.7.2 Remembered Set

Remembered Set是一种抽象概念,而card table可以是remembered set的一种实现方式。

Remembered Set是在实现部分垃圾收集(partial GC)时用于记录从非收集部分指向收集部分的指针的集合的抽象数据结构(比如老年代指向)。

partial GC的两种情况:

  • 分代式GC-卡表
    当分两代时,通常把这两代叫做young gen和old gen;通常能单独收集的只是young gen。此时remembered set记录的就是从old gen指向young gen的跨代指针,即卡表(card table)。

    具体来说YoungGC时只会对年轻代对象做存活标记,只收集年轻代对象,不会管老年代对象。只不过会将老年代引用了年轻代的引用作为YoungGC的GC Roots的一部分。同时,经过统计信息显示,老年代持有新生代对象引用的情况不足1%,根据这一特性JVM引入了卡表来实现这一目的,即YoungGC用卡表记录了老年代对新生代引用,避免了去扫描老年代。
    卡表使用字节数组来实现card的记录,每个card对应该数组里的一个bit或一个byte,例如:

    struct CardTable {  
      byte table[MAX_CARDTABLE_SIZE];  
    };  
    
  • Region式GC
    Regional collector也是一种部分垃圾收集的实现方式,此时remembered set就要记录跨region的指针。

2.8 GC器的职责

  • 分配内存
  • 保证被引用对象保留在内存中
  • 回收那些在执行代码时无法从引用访问的对象使用的内存

2.9 GC器的理想情况

  • 不能错误回收存活对象
  • 垃圾不能在几次GC后依然存活没回收
  • 高效运行,避免应用程序长时间STW
  • GC器分配内存和垃圾回收的可伸缩性
    需要在堆大小、执行时长、GC频率之间做出权衡。

2.10 GC器主要指标

  • 吞吐量
    用于用户程序执行时间占总时间(用户程序执行时间+GC时间)百分比
  • GC开销
    用于GC执行时间占总时间百分比
  • 暂停时间
    用户程序再GC发生时的暂停执行时长
  • GC频率
  • Promptness
    对象变为垃圾后到该内存区域被回收并变为可用的时长

3 GC算法

3.1 概述

3.1 引用计数(Reference Counting)

  • 概念
    比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。
  • 缺点
    此算法最致命的是无法处理循环引用的问题。

3.2 复制(Copying)

Java-JVM-GC_第5张图片

  • 概念
    此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。

    采用复制算法进行垃圾回收时:

    1. 从GC Roots开始进行遍历年轻代对象图,
    2. 每当第一次发现一个正在使用中的对象就立刻复制到另外一个Survivor区域中或晋升到老年代,
    3. 并在旧的对象位置的Mark Word域上设置forwarding pointer指向新地址,
    4. 如果下一次另外的对象也引用该已复制的对象,直接通过mark word域的forwarding pointer来更新引用对象的地址即可
    5. 最后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

    复制算法没有mark和copy阶段,而就是一个合并起来的阶段称为scavenge(或copy)。

    copy算法会在遇到一个未处理的引用的时候看这个引用指向的对象是否已经被copy了,如果没有则copy之并且修正引用,如果已经copy了则只修正引用。注意无论如何都是要修正引用的,所以复制后对象依然可以找到引用的对象(已经是复制后的那一份)。

  • 优点

    • 此算法每次只处理正在使用中的对象(即只关心活对象——每碰到一个未处理的活对象就把它copy走,没有标记阶段),因此复制成本比较小。具体来说,只需要遍历一趟存活对象而非整个堆,就完成了对象拷贝和指针修正,数据局部性非常好。

    • 直接可覆盖使用旧有内存进行分配
      在整个copy流程完成后,所有活对象都不在原来的空间了,原来的空间就可以直接被用来再次分配新对象——死对象自然就被覆盖了。

    • 同时复制过去以后还能对原空间进行相应的内存整理,不过出现"碎片"问题。

    • 复制算法的开销只跟活对象的多少(live data set)有关系,而跟它所管理的堆空间的大小没关系。

      例如20GB的年轻代,应用程序在非常快速的向里面分配新对象,但是这些新对象的绝大部分(例如> 99%)都在下一次YoungGC时已死的话,那其实活对象也没多少(< 204MB),并行处理它们的话或许100-200ms这种范围的时间之内就够了。

      而如果场景是一个没多大的young gen(例如2GB),但新生对象几乎全都能活过下一次young gen GC的话,活对象占堆空间的比例很高时copying GC的效率就会很差…此时young gen GC要消耗的时间会远大于前一个场景的。

  • 缺点
    需要两倍内存空间,不适用如老年代存在大量长期存活对象的情况。

  • 应用
    新生代采用了此算法,因为新生代大多对象的特点是朝生夕灭,一般MinorGC就要回收大量新生代对象,存活对象少,剩余对象直接从from survivoreden区copy到to survivor就好了,适用于快速的复制算法,相当于copy算法以较小的空间换较快的时间。

    具体来说,DefNew ParNew PSScavenge都用了此算法。

  • 关于为什么复制算法快于标记-整理算法的讨论
    关于JVM垃圾搜集算法(标记-整理算法与复制算法)的效率
    GC复制算法和标记-压缩算法的疑问

3.3 标记-清除(Mark-Sweep)

Java-JVM-GC_第6张图片

  • 概念
    此算法执行分两阶段:
    1. 从引用根节点(GC Roots)开始标记所有被引用的对象
    2. 遍历整个堆,把未标记的对象清除
  • 优点
    • 无需复制、整理移动对象,GC效率高
  • 缺点
    • 此算法需要暂停整个应用,标记和清除效率低下
    • 会产生内存碎片,不利于大对象分配,因为分配大对象时因为内存碎片导致没有可供分配的连续内存空间,从而使得GC提前。
    • 只能就地释放的空间分配给新对象,通常效率低于压缩后的空间分配,因为就地分配还需要去搜索堆内的足够大的连续空间来容纳新对象。
  • 应用
    CMS采用了标记清除。

3.4 标记-整理(Mark-Compact)

Java-JVM-GC_第7张图片

  • 算法实现
    此算法结合了"标记-清除"和"复制"两个算法的优点,经典的实现算法是LISP2,分为四个阶段:

    1. mark
      从根节点开始标记所有被引用的存活对象
    2. compute new address
      遍历全堆,计算存活对象应该移动到的目标内存地址
    3. remap(fixup pointer)
      遍历全堆,修正这些存活对象的内存地址指针
    4. relocate(copy)
      遍历全堆,把存活对象"压缩"复制到堆的其中一边按顺序排放,并清除边界外的未标记对象。

    注意mark阶段只遍历活对象,后面三步都要遍历全堆(可以通过bitmap marking来降低遍历全堆的开销。后来有一些更复杂的算法可以把后三步合成到一步来实现,但这样也至少有分离的mark和compact两步,总体效率仍然低于copying GC。

  • 关于Compact
    在GC器确定内存中的哪些对象是活的以及哪些是垃圾后,就可以压缩内存,即将所有活动对象移动到一起,最后完全回收剩余的内存。

    压缩完成后,很方便地就能快速分配内存给新对象了,因为可以利用简单的指针就能跟踪可用于对象分配的下一个空闲位置。

  • 优点
    此算法避免了"标记-清除"的碎片问题可创造空闲的连续大空间,同时也避免了"复制"算法的两份空间导致空间利用率低的问题。

  • 缺点
    但因需要多次遍历堆中的对象关系图、标记、压缩等操作所以速度肯定没有复制算法块,故而新生代还是继续采用复制算法。

  • Compact和Coping区别:

    • 狭义地说,Compact是原地 Compact,而Coping是拷贝到别的内存空间去。
    • 广义地说,Copy的目的确实也是Compact

3.5 增量收集(Incremental Collecting)

  • 实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。

    增量收集适用于:当需要由并发收集器提供的低暂停时间的应用程序,在具有少量CPU(例如1或2核)的机器上运行时,此功能非常有用。

  • CMS能采用增量收集。该模式可通过周期性地停止并发收集阶段来处理应用程序,以此来减少很长并发阶段带来的的停止时间的影响。具体来说,CMS的工作被分为若干小ChunkTime,这些时间被分配在每次YoungGC之间调度。该特性适用于应用程序需要低暂停时间的CMS,且运行在少量CPU的机器上(比如1或2核)时。

3.6 分代收集(Generational Collecting)

可以参考java的gc为什么要分代?
Java-JVM-GC_第8张图片

3.6.1 关于垃圾部分收集

分代GC是一种部分收集(partial collection)的做法。在执行部分收集时,从GC堆的非收集部分指向收集部分的引用,也必须作为GC Roots的一部分。

具体到分两代的分代式GC来说,如果第0代叫做年轻代,第1代叫做老年代。那么如果有minor GC只收集年轻代里的垃圾,则年轻代属于“收集部分”,而老年代属于“非收集部分”。此时,从老年代指向年轻代的引用就必须作为minor GC的GC Roots的一部分。

具体到HotSpot VM里的分两代式GC来说,除了老年代到年轻代的引用之外,有些带有弱引用语义的结构,例如记录所有当前被加载的类的sun.jvm.hotspot.memory.SystemDictionary、记录字符串常量引用的sun.jvm.hotspot.memory.StringTable等,在minor GC时必须要作为Strong GC Roots。而他们在收集整堆的Full GC时则不会被看作Strong GC Roots换句话说,young GC比full GC的GC roots还要更大一些

3.6.2 分代收集是什么

分代收集是基于对对象生命周期分析后得出的垃圾回收算法:
把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法进行回收

  • 新生代因为对象特点是朝生夕灭,所以在minorGC中选择了只复制少量对象的复制算法(时间和存活对象数成正比,在活对象少时很快),且使用老年代作为HandlePromotion(分配担保)。

    注意,YoungGC时只会对年轻代对象做存活标记,只收集年轻代对象,不会管老年代对象。只不过会将老年代引用了年轻代的引用作为YoungGC的GC Roots的一部分。同时,经过统计信息显示,老年代持有新生代对象引用的情况不足1%,根据这一特性JVM引入了卡表(card table)来实现这一目的。YoungGC会用到卡表,记录了老年代对新生代引用,避免了去扫描老年代。

    具体来说,卡表的具体策略是将老年代的空间分成大小为512B的若干张卡片(card),JVM采用卡表维护了每一个块的状态。卡表本身是单字节数组,数组中的每个元素对应着一张卡。当发生老年代引用新生代时,虚拟机将该卡对应的卡表元素设置为适当的值。如下图所示,卡表3因为3号卡内的对象引用了新生代对象,所以该卡被标记为脏状态(卡表还有另外的作用,标识并发标记阶段哪些块被修改过),之后Minor GC时通过扫描卡表就可以很快的识别哪些卡中存在老年代指向新生代的引用。这样虚拟机通过空间换时间的方式,避免了全堆扫描:
    Java-JVM-GC_第9张图片

  • 老年代对象特点是存活时间长,而且没有其他空间来HandlePromotion,所以使用标记->整理/清理(特别是标记清理,因为该算法不用移动对象)。

    注意,HotSpot VM里只有CMS有老年代GC即Major GC。

3.6.3 分代收集怎么区分年轻代/年老代对象

通常年轻代和年老代都是有地址范围的,当YoungGC从GC Roots开始搜索的时候,通过地址指针来匹配地址范围便可得知处于哪个分代中。

具体来说:

  • HotSpot VM旧的GC算法都是要求整个GC堆在连续的地址空间上,只有一个分界线,一侧是年轻代另一侧是老年代,所以检查分代的开销非常低;
  • G1则需要遍历一个年轻代Region列表来获得年轻代的地址范围。因为G1的分代都不是连续的了,所以检查分代的开销也变高了,而G1的post write barrier里分代检查是在异步处理的部分做的,而不是同步做的。

3.6.4 分代收集的好处

  • 缩短STW时间
    对传统的、基本的GC实现来说,由于它们在GC的整个工作过程中都要STW,如果能想办法缩短GC一次工作的时间长度就是件重要的事情。如果说收集整个GC堆耗时太长,那不如只收集其中的一部分?于是就有好几种不同的划分(partition)GC堆的方式来实现部分收集,而分代式GC就是这其中的一个思路。

    这个思路所基于的基本假设大家都很熟悉了:weak generational hypothesis——大部分对象的生命期很短(die young),而没有die young的对象则很可能会存活很长时间(live long)。这是对过往的很多应用行为分析之后得出的一个假设。

    基于这个假设,如果让新创建的对象都在年轻代里创建,然后频繁收集年轻代,则大部分垃圾都能在minor GC中被收集掉。由于年轻代的大小配置通常只占整个GC堆的较小部分(年轻代和老年代的比例默认为1:3),而且较高的对象死亡率(或者说较低的对象存活率)让它非常适合使用复制算法来收集,这样就不但能降低单次GC的时间长度,还可以提高GC的工作效率。

  • 提高并发式GC所能应付的内存分配速率
    对于一个并发GC来说,能够尽快回收出越多空间,就能够应付越高的应用内存分配速率,从而更好地保持GC以完美的并发模式工作。

    因为并发GC根本上要跟应用玩追赶游戏:应用一边在分配,GC一边在收集,如果GC收集的速度能跟得上应用分配的速度,那就一切都很完美;一旦GC开始跟不上了,垃圾就会渐渐堆积起来,最终到可用空间彻底耗尽的时候,应用的分配请求就只能暂停来等GC追赶上来。

3.7 小结

3.7.1 GC器与GC算法

Java-JVM-GC_第10张图片

3.7.2 三种GC算法对比

标记清除 标记压缩/整理 复制
速度
时间开销 mark存活对象阶段与存活对象数成正比,遍历清除不存活对象阶段与整堆大小成正比 mark存活对象阶段与存活对象数成正比,遍历存活对象进行整理阶段与存活对象数成正比 遍历复制存活对象,与存活对象数成正比
空间开销 少,但有内存碎片 少,无内存碎片 两倍空间,无内存碎片
移动对象

3.7.3 GC各阶段耗时对比

  • compaction >= copying > marking > sweeping
  • marking + sweeping > copying

原因如下:

  • compaction一般分为:
    • 计算一次对象的目标地址
    • 修正指针
    • 移动对象
  • copying则可以把这几件事情合为一体来做,所以可以快一些。

4 GC器

4.1 GC器重要概念

4.1.1 GC历史

4.1.1.1 GC体系

Java-JVM-GC_第11张图片

  • 分代GC框架体系有若干GC器:Serial (就是DefNew), ParNewCMSSerialOld(MSC)可以任意搭配。
  • ParallelScavenge体系里的PSScavenge(即上图中的Parallel Scavenge),只能和其同一体系的Parallel Old或分代体系的MSC搭配(ParallelScavenge和Serial Old的连线,被称为PSMarkSweep,是从分代式GC框架里抽出来的Serial Old收集器,加了一层包装而已)。
  • G1不属于上述两个体系,单独开发,可同时垃圾收集新生代、老年代。
4.1.1.2 DefNewGeneration和ParNewGeneration

原本HotSpotVM里并没有并行GC,当时只有NewGeneration,且新生代,老年代都使用串行回收收集。

后来准备加入新生代并行GC,就把NewGeneration改名为DefNewGeneration,然后把新加的并行版叫做ParNewGeneration,他俩都在Hotspot VM”分代式GC框架“内:

  • DefNew为新生代串行GC
  • ParNew为新生代并行GC(注意是GC线程并行,而用户线程必须暂停等待;CMS部分阶段才是GC线程和用户线程并发执行)。
4.1.1.3 ParallelScavenge

最开始,新生代GC只有属于分代GC框架体系的DefNewGeneration和ParNewGeneration,但有个人自已硬写了个新的并行GC,测试后效果还不错。于是也被放入VM的GC中,即ParallelScavenge-一个注重吞吐量优先的并行GC。

这个时候就出现了两个新生代的并行GC收集器:ParNewGeneration和ParallelScavenge。

4.1.1.4 Scavenge的含义-复制

Scavenge或者叫scavenging GC,其实就是copying GC(即复制算法GC)的另一种叫法而已

HotSpot VM里的GC都是在minor GC收集器里用scavenging的,DefNew、ParNew和ParallelScavenge都是,只不过DefNew是串行的copying GC,而后两者是并行的copying GC。 由此名字就可以知道,ParallelScavenge的初衷就是把scavenge给并行化。换句话说就是把minor GC并行化。

至于full GC则不是当初关注的重点。

4.1.1.5 PSScavenge和PSMarkSweep
  • PSScavenge基于复制算法
    ParallelScavenge因为和其他几个GC不在一个框架内,最初的ParallelScavenge体系对老年代的回收拿的是VM的“分代式框架“里在 Serial Old收集器的代码,改了接口,负责full GC,并命名为:PSMarkSweep(“ParallelScavenge的MarkSweep”),仍然是老年代串行收集。这里的ParallelScavenge已经不是Parallel Scavenge(并行新生代收集器),而是一套GC框架体系。
  • PSMarkSweep基于标记-整理算法
    为了名称与VM“分代式框架”里的收集器好区别,在这套体系时,新生代收集器叫:PSScavenge,老年代收集器叫:PSMarkSweep。(PS看成是ParallelScavenge缩写,作为前缀)。
4.1.1.6 PSCompact

后来,因为未知的原因,老年代GC的并行化没有在VM分代式GC框架中完成,而选择了在ParallelScavenge框架中完成。其成果就是使用了LISP2算法的并行版的full GC收集器,名为PSCompact(“ParallelScavenge-MarkCompact”),收集整个GC堆,就是上图中右下方的Parallel Old

PSCompact基于标记-整理算法。

4.1.1.7 JConsole观察

在JConsole查看时:

  • PSCompact、PSMarkSweep都显示为PSMarkSweep
  • DefNew显示为Copy
  • Serial Old(MSC)显示为MarkSweepCompact

4.1.2 GC分类

GC有很多分类,如:

  • 按照并行(ParNew、Parallel Scavenage、Parallel Old、CMS并发标记阶段、g1)、并发(CMS并发标记和并发清理阶段、g2)和串行(Serial、Serial Old/PS Mark-Sweep)
  • 按照性能指标比如吞吐量(Parallel Scavenge、Parallel Old)、低停顿(CMS)、增量式(G1)。
  • 按照算法
    • 复制(Coping)
      • Serial Young(DefNew)
      • ParNew
      • Parallel Young
    • 标记-整理(Compact)
      • Serial Old
      • PS MarkSweep
      • Parallel Old(PSscavenge)
      • FullGC for CMS
      • FullGC for G1
    • 标记-清理(Sweep)
      • CMS Old
    • 疏散(Evacuate)
      • G1 GC
        把存活对象从一个Region移动到另一个Region。

4.1.3 GC中的并行和并发

  • 并行
    指多个GC线程并行执行,但用户线程处于等待状态。
  • 并发
    指GC线程和用户线程并发执行,但不一定是并行的,可能是在极短时间内交替执行。用户线程和GC线程运行于不同CPU。

4.1.4 配置

  • -XX:+UseSerialGC
    Serial + Serial Old组合
  • -XX:+UseParNewGC
    新生代使用ParNew并行收集器,老年代使用SerialOld串行回收收集器
  • -XX:+UseParallelGC
    PSScavenge+ PSMarkSweep(SerialOld)老年代串行的组合。
  • -XX:+UseParallelOldGC
    PSScavenge+PSCompact(ParallelOld)老年代并行的组合。
  • -XX:+UseConcMarkSweepGC
    新生代使用ParNew并行收集器,老年代使用CMS并发收集,以及在CMS失败(CMS老年代预留空间不够)时使用Serial Old(基于标记整理,解决碎片问题)进行FullGC,此时开销很大。
    --XX:+UseG1GC
    使用-XX:+UseG1GC(G1收集器)。Young GC + mixed GC(新生代,再加上部分老年代),以及在G1 GC算法赶不上新对象分配时使用 Full GC for G1 GC算法,此时开销很大。

4.2 Serial(串行收集器)

4.2.1 基本概念

  • 简介
    最早的GC器,串行方式进行GC,其他所有线程暂停等待。

  • 打开方式
    使用-XX:+UseSerialGC打开。

  • GC范围
    分为年轻代和老年代。

    • 年轻代Serial基于复制算法,可以和老年代CMS/SeriralOld配合使用
    • 老年代Serial基于标记整理算法,可以和年轻代Serial/ParNew/Parallel Scavenge配合使用
  • 特点

    • 使用单线程处理所有垃圾回收工作,因为无需多线程交互和CPU切换,所以效率比较高。

    但是,也无法使用多处理器的优势,所以此收集器适合单处理器机器。当然,SerialGC也可以用在小数据量(100M左右)情况下的多处理器机器上。

    • SerialGC时,必须STW,暂停其他所有工作线程直到GC结束。
    • 在一般情况下,SerialGC的停顿时间可以控制在几十毫秒甚至一百多毫秒以内,还是可以接受的。

4.2.2 运行流程

Java-JVM-GC_第12张图片

4.2.3 实现原理

  • Serial 新生代 GC采用单线程复制算法,需要STW
    Java-JVM-GC_第13张图片
    Java-JVM-GC_第14张图片
  • Serial 老年代 GC采用单线程标记-整理算法,需要STW
    Java-JVM-GC_第15张图片

4.2.4 使用场景

  • SeriaNew(DefNew)
    Client模式下默认新生代收集器(内存100MB左右新生代时,STW时间可控制在100毫秒以内),且对STW没有极短的时长要求的,可在调试时使用,或是单CPU场景。

    在当代硬件条件下,串行收集器可以有效地管理许多非常重要的应用程序,其中64MB堆和相对较短的最坏情况暂停时间还不到半秒。

  • SerialOld

    • 主要给Client模式下的虚拟机使用。
    • 在Serve模式下有两个用途:
      • 在JDK 1.5之前的版本中与Parallel Scavenge收集器搭配使用;
      • 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用SerialOld进行Full GC。

4.3 ParNew(并行收集器)

4.3.1 基本概念

  • 简介
    ParNew收集器其实是Serial新生代收集器的多线程版本,他们都属于JVM分代收集体系。
    ParNew与新生代SerialGC不同的地方就是在GC过程中使用多线程,而其他所有行为包括控制参数、复制收集算法、STW、对象分配规则和回收策略等都一样。
  • 打开方式
    使用-XX:+UseParallelGC打开
  • 搭配
    年轻代ParNew基于复制算法,可以和基于标记-整理的SerioOld或基于标记-清除的CMS结合使用。
  • 对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间,是Serial的多想城版本。一般在多线程多处理器机器上使用。jdk1.6后可对年老代进行并行收集。如果年老代不使用并发收集的话,是使用单线程进行垃圾回收,因此会制约扩展能力。
  • 特点
    • ParNew在单线程情况下由于线程交互的开销并没有Serial收集器的效果好。不过,随着CPU个数的增加,它对于GC时系统资源的有效利用还是很有好处的。
    • 它默认开启的收集线程数与CPU的数量相同。
    • 可以使用-XX:ParallelGCThreads参数来限制ParNew GC时的线程数。

4.3.2 运行流程

Java-JVM-GC_第16张图片

4.3.3 实现原理

年轻代ParNew基于复制算法,需要STW,可以和基于标记-整理的SerioOld或基于标记-清除的CMS结合使用。

4.3.4 配置

此收集器可以进行如下配置:

  • 使用-XX:ParallelGCThreads=n限制并行垃圾回收的线程数。此值可以设置与机器处理器数量相等。

4.3.5 使用场景

  • 多CPU场景
  • 没STW时间严格限制,因为老年代不频繁但还是可能发生长时间的GC
  • 需要和CMS搭配时作为默认新生代搜集器
  • 适合批处理环境,如计费、计算工资单、科学计算等

4.4 Parallel Scavenge(并行、吞吐量优先)

4.4.1 基本概念

  • 简介
    Parallel Scavenge收集器和ParNew类似,是一个新生代并行多线程收集器,使用复制算法。

    不过和ParNew不同的是,Parallel Scavenge收集器的关注点在于达到一个可控制的吞吐量。

    吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+运行GC时间)。如果虚拟机一共运行100分钟,垃圾收集运行了1分钟,那么吞吐量就是99%。

    停顿时间越短就越适合与用户交互的程序,良好的响应速度能提升用户体验;而高吞吐量则可以高效的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多用户交互的任务。

  • 开启方法
    分为年轻代(-XX:+UseParallelGC)和老年代PS Compact(-XX:+UseParallelOldGC)。

  • 特点
    侧重于吞吐量的控制,提升用户线程的CPU使用率。

4.4.2 运行流程

下图为PSScavenge+ParallelOld组合的GC过程:
Java-JVM-GC_第17张图片

4.4.3 实现原理

4.4.3.1 GC搭配
  • 年轻代Parallel Scavenge基于并行的复制算法,需要STW,负责执行minor GC(只收集young gen)。可以和老年代SeriralOld(PS MarkSweep)/Parallel Scavenge Old(此时PS Scavenge 收集新生代,而Parallel Old 收集整堆)搭配使用。
    • 与SerialOld那条线,老年代基于串行的LISP2的标记-压缩算法,需要STW,负责执行full GC(收集整个GC堆,包括young gen、old gen、perm gen)。为了名称与VM“分代式框架”里的收集器好区别,在这套体系中,新生代收集器叫:PSScavenge,老年代收集器叫:PSMarkSweep。(PS看成是ParallelScavenge缩写,作为前缀)。
  • 老年代Parallel Old
    年轻代PS Scavenge与Paralle lOld那条线,即老年代PS Compact基于标记整理算法,可以和年轻代Parallel Scavenge配合使用,是个并行的全堆收集器
4.4.3.2 PSScavenge和SerialOld(PSMarkSweep)
  1. PSScavenge和SerialOld(PSMarkSweep)搭配使用时,默认是在要触发full GC前先执行一次young GC。
  2. 并且两次GC之间能让应用程序稍微运行一小下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量)。
  3. 然后紧接着去用PS MarkSweep执行一次真正的full GC收集全堆。所以有可能观察到这个跟随的full GC时,Eden区没什么变化,Survivor区反而被清空,老年代上升的情况。
  4. 控制这个行为参数是-XX:+ScavengeBeforeFullGC
4.4.3.4 PSScavenge和ParNew对比

PSScavenge和ParNew都是并行收集年轻代,目的和性能其实都差不多。最明显的区别有下面几点:

  • 遍历对象图方式不同
    PSScavenge以前是广度优先顺序来遍历对象图的,JDK6的时候改为默认用深度优先顺序遍历,并留有一个UseDepthFirstScavengeOrder参数来选择是用深度还是广度优先。但在JDK6u18之后这个参数被去掉,PSScavenge变为只用深度优先遍历。ParNew则是一直都只用广度优先顺序来遍历
  • 只有PSScavenge能用UseAdaptiveSizePolicy
    PSScavenge完整实现了adaptive size policy,而ParNew及分代式GC框架内的其它GC都没有实现完(倒不是不能实现,就是麻烦+没人力资源去做)。所以千万别在用ParNew+CMS的组合下用UseAdaptiveSizePolicy,请只在使用UseParallelGC或UseParallelOldGC的时候用它
  • PSScavenge老年代使用并行的基于标记-整理的Parallel Old算法
  • PSScavenge不能和CMS搭配
    由于在“分代式GC框架”内,ParNew可以跟CMS搭配使用,而ParallelScavenge不能。当时ParNew GC被从Exact VM移植到HotSpot VM的最大原因就是为了跟CMS搭配使用。
  • NUMA优化的实现
    在ParallelScavenge成为主要的吞吐量 GC之后,它还实现了针对NUMA(Non Uniform Memory Access Architecture)技术,可以使众多服务器像单一系统那样运转,同时保留小系统便于编程和管理的优点)的优化;而ParNew一直没有得到NUMA优化的实现。
  • 可参考JVM full GC的奇怪现象,求解惑?

4.4.4 配置

  • 最大垃圾回收暂停时间
    指定垃圾回收时的最长暂停时间,通过-XX:MaxGCPauseMillis=指定,单位为毫秒。如果指定了此值的话,堆大小和垃圾回收相关参数会进行调整以尽量达到指定值。

    注意,设定此值可能会减少应用的吞吐量,因为GC时间的缩短是以牺牲吞吐量和新生代空间为代价的,短的GC时间会导致更加频繁的垃圾收集行为,从而导致整体吞吐量的降低。比如系统会自动把新生代调小,GC更小的新生代肯定快于更大的,所以GC会更频繁,比如原来每10秒一次GC+每次STW100毫秒,现在变为5秒一次GC+每次STW70毫秒,注定GC会更加频繁,整体吞吐量下降。

  • 吞吐量
    吞吐量为垃圾回收时间与非垃圾回收时间的比值,通过-XX:GCTimeRatio=N来设定,公式为1/(1+N)。例如,-XX:GCTimeRatio=19时,表示5%的时间用于GC。默认情况为99,即1%的时间用于GC。

  • GC自适应策略
    参数-XX:UseAdaptiveSizePolicy,这是一个开关参数,当打开后,不需手工指定新生代的大小(-Xmn)、Eden和Survivor的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节了。JVM会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最适合的停顿时间或最大的吞吐量,这叫GC自适应的调节策略。这也是Parallel Scavenge和ParNew的一个重要区别。

4.4.5 使用场景

  • 吞吐量优先场景
  • 多核,但CPU资源敏感场景
  • 在后台运算而不需要太多交互的任务(响应时间要求低)

4.5 CMS-并发标记清除收集器-停顿时间优先

Java-JVM-GC_第18张图片

4.5.1 基本概念

  • 简介
    CMS(Concurrent Mark Sweep)收集器是一种以获取最短GC停顿时间为目标的收集器。在重视响应速度和用户体验的应用中,CMS应用很多。

  • 打开方式
    使用-XX:+UseConcMarkSweepGC打开

  • 并发收集思想
    并发GC根本上要跟应用玩追赶游戏:应用一边在分配,GC一边在收集。如果GC收集的速度能跟得上应用执行分配新对象的速度,那就一切都很完美;一旦GC开始跟不上了,垃圾就会渐渐堆积起来,最终到可用空间彻底耗尽的时候,应用的分配请求就只能暂停等GC追赶上来。所以,对于一个并发GC来说,能够尽快回收出越多空间,就能够应付越高的应用内存分配速率,从而更好地保持GC以完美的并发模式工作。

    总结来说,CMS的设计聚焦在获取最短的时延,为此它“不遗余力”地做了很多工作,包括尽量让应用程序和GC线程并发、增加可中断的并发预清理阶段、引入卡表等,虽然这些操作牺牲了一定吞吐量但获得了更短的回收停顿时间。

  • 目标和特点
    可保证大部分用户代码和GC并发进行,GC只STW很少的时间,以获取最短回收停顿时间为目标。CMS适合对响应时间要求比较高的中、大规模应用。CMS缺点如下:

    • CMS收集器对CPU资源非常敏感。CMS默认启动的回收线程数是(CPU数量+3)/4,当CPU个数大于4时,垃圾收集线程使用不少于25%的CPU资源,当CPU个数不足时,CMS对用户程序的影响很大;
    • CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC;
    • CMS使用标记-清除算法,会产生内存碎片;

4.5.2 运行流程

Java-JVM-GC_第19张图片

  1. 初始标记(CMS initial mark)- 需要STW
    初始标记只标记GC Roots能直接关联到的对象,速度很快。

  2. 并发标记(CMS concurrent mark)- 时间较长
    并发标记阶段就是进行从GC Roots开始沿着引用链搜索存活对象的过程。此时用户线程和GC线程并发地继续运行,又可能产生垃圾。此过程中如果遇到old->young会立刻停止这一路径的tracing动作。

  3. 重新标记(CMS remark)- 需要STW
    重新标记阶段是为了修正并发标记期间因用户程序继续运行而导致的GC标记产生变动(变动是指修改了对象与引用的关系)的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

    CMS的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个年轻代作为GC Roots进行扫描。

  4. 并发清除(CMS concurrent sweep)- 时间较长
    并发清除阶段会清除没有被标记存活的对象区域(无论没有对象还是有死亡对象都进行回收)。此时用户线程和GC线程并发地继续运行,又可能产生垃圾,这些垃圾需要等到下一次GC处理。

4.5.3 实现原理

  • 实现算法
    CMS基于标记-清除算法实现。可参考并发垃圾收集器(CMS)为什么没有采用标记-整理算法来实现?

    1. CMS追求尽量缩短GC停顿时间。
    2. CMS支持增量式GC,内部使用了write barrier(写内存屏障)来保证用户代码逻辑和GC代码逻辑的对象视图保持一致。所以如果采用整理算法,则整理内存时移动对象的动作就必须停止用户代码逻辑从而导致STW且时间较长,直到整理阶段的对象移动和指针修正完毕才能继续运行。
    3. 而且老年代对象生命周期长、GC回收率不高,也就是说每次老年代GC要移动大量存活对象,不划算。

    而使用标记清除算法不用移动大量存活的老年代对象,也不用修正指针,而且标记和清除两个阶段都可以和用户代码逻辑并行执行,效率高,所以CMS老年代GC采用了标记清除算法。

CMS基于并行的标记-清除算法,主要减少年老代的STW时间,可在应用不停止的情况下使用独立的GC线程跟踪可达对象。CMS会定时去检查老年代的使用量,当超过触发比例就会启动一次CMS Major GC,对老年代做并发收集.

在每个年老代垃圾回收周期中,在收集初期CMS会对整个应用进行简短的暂停,在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。

CMS使用多处理器换来短暂的停顿时间。在一个N个处理器的系统上,并发收集部分使用K/N个可用处理器进行回收,一般情况下1<=K<=N/4。在只有一个处理器的主机上使用并发收集器,设置为incremental mode模式也可获得较短的停顿时间。

  • CMS原则
    宁愿放过已死亡的对象不回收(下次GC再回首回收),也不能错误回收活着的对象。

    假设有对象b如下:

    class B {
        A a;
       ..
    }
    

    其中Field a在并发标记时是指向对象a1,但在并发标记完成前Field a转而指向a2。

    我们原则是必须要保证a2不会被回收(而a1可以被错误保留,等待下次gc再回收)。这就需要重标记阶段来标记a2存活。简单地说,就是在并发标记阶段,记下所有改动的引用到Remember Set log(RSLog。最后在重标记阶段再trace一遍从变动过的引用开始的局部object graph。

  • 并发标记阶段变动对象处理

    1. 假设并发标记产生的可到达对象关系集RCon
    2. 在CMS的并发标记阶段采用write barrier+Remember Set log(RSLog)记录对象变动情况,CMS具体是用insert barrier监听新增引用关系忽略删除引用(等待下一次处理)
    3. 重标记阶段将RCon和RSLog进行整合得到最终对象情况RFinal进行GC
  • 关于空闲区域指定-Free List
    连续空闲内存可用指针来指向,但因CMS采用标记-清除算法会产生不连续的区域,所以使用了若干称之为Free List的结构来进行标注,每个free list都指向一个内存Region。分配对象时,必须根据所需的内存大小来搜索适合的list,使用其对应的Region来存放新对象。这种分配代价较昂贵。

  • 关于浮动垃圾
    由于在应用运行的同时进行GC,虽然在重标记阶段后可保证所有存活对象都被正确标记出来,但有些对象可能在标注阶段已经死掉应该被回收,对于CMS来说本次GC不会处理而是留到下一次GC处理,这样就被称为浮动垃圾(Floating Garbage)。

    所以,CMS一般需要20%的预留空间用于这些浮动垃圾。

4.5.4 Full GC

  • Concurrent Mode Failure
    并发收集器在应用运行时进行并发收集,所以需要保证堆在GC的这段时间有足够的空间供程序使用。否则,GC还未完成,堆空间先满了。这种情况下将会发生Concurrent Mode Failure(并发模式失败),此时会发生Stop the world,回退到使用serialOldGC的mark-compact算法做full GC来进行GC。

    通过设置-XX:CMSInitiatingOccupancyFraction=指定还有多少剩余堆百分比时开始执行并发收集,使用CMS时必须非常小心的调优,尽量推迟由碎片化引致的full GC的发生。一旦发生full GC,暂停时间可能又会很长,这样原本为低延迟而选择CMS的优势就没了。

  • 内存碎片
    CMS Full GC除了上述Concurrent Mode Failure,还可能由于老年代搜集后的内存碎片导致对象无法分配,也会触发Full GC来整理内存。这种情况的表现是堆内存还有较多剩余空间,但却发生了Full GC!

4.5.5 CMS对比其他GC器

  • CMS对比SerialOld
    Java-JVM-GC_第20张图片
    • 可以看到,SerialOld在整个GC阶段是串行且一直STW;
    • 而CMS:
      1. 在初始标记阶段是串行且STW,但时间短
      2. 在并发标记阶段是GC串行执行且和用户线程并行执行,不需要STW
      3. 在重新标记阶段是GC并行(因为相对初始标记阶段,本阶段事情更多),需要STW,但时间短
      4. 在并发清理阶段是GC串行执行且和用户线程并行执行,不需要STW
  • CMS对比并行收集器
    • CMS减少了老年代收集暂停(有时候特别明显),代价是年轻代暂停稍多一些,吞吐量下降了一些,堆大小要求较大

4.5.6 CMS FAQ

  1. 为什么初始标记不能也做成并发的?
    可以做成并发的,就是实现起来麻烦一些而已。Android Runtime里的CMS实现在初始标记的时候采用了checkpointing做法,就不是完全stop-the-world而是一个个线程分别错开一点时间来暂停。这样系统在扫描一个线程的栈的时候其它线程还可以跑,比stop-the-world的影响小。

  2. 如果在并发清除阶段产生了新的对象,这个对象没有被标记为可达,并且此时已经错过了重新标记阶段,如何保证这个对象在并发清除阶段不被回收?(GC是标记活的,收回未标记的内存区域)
    这是并发GC的一个重要的设计点。CMS的做法是在并发标记过程中创建出来的新对象都是在young gen里的,并发标记并不管它们的生死。而且过程中还可以有若干次ParNew GC(young GC)收集young gen;

    等到concurrent marking结束,会做一个STW的重新标记,此时会完整扫描一次young gen,把里面的对象当作root set的一部分,这样新创建出来的对象就会被扫描上。中间还有concurrent precleaning / abortable concurrent precleaning

4.5.7 小结

  • 优点
    • 控制GC停顿时间
    • 老年代并发GC
  • 缺点
    • 内存碎片
    • 采用FreeList而不是指针,分配对象代价较昂贵
    • 需要比其他收集器更大的堆,因为允许应用程序在并发标记阶段继续运行,可以继续分配内存,所以老年代可能继续增长。
    • 无法处理浮动垃圾,需要下次GC处理
    • CMS收集器对CPU资源非常敏感
      CMS默认启动的回收线程数是(CPU数量+3)/4,当CPU个数大于4时,垃圾收集线程使用不少于25%的CPU资源,当CPU个数不足时,CMS对用户程序的影响很大;
    • Concurrent Mode Failure时会发生Full GC
      所以需要合理调整-XX:CMSInitiatingOccupancyFraction=指定还有多少剩余堆百分比时开始执行并发收集

4.5.8 使用场景

  • 总的来说,如果您的应用程序需要更短的GC暂停,并且在应用程序运行时能够与GC共享处理器资源(由于它
    并发性,CMS收集器在收集周期中占用CPU周期),请使用CMS收集器。

    通常,拥有大量长寿对象的应用,或者最低拥有1-2核的机器可开始尝试CMS。

  • 重视响应速度和用户体验的应用,如网站

  • B/S系统

4.6 G1

可参考Garbage First介绍

4.6.1 目标

从设计目标看G1完全是为了大型服务端应用而准备的,目标是用在多核、大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保持较高的吞吐量:

  • 支持很大的堆
  • 高吞吐量
    • 支持多CPU和垃圾回收线程
    • 在主线程暂停的情况下,使用并行收集
    • 在主线程运行的情况下,使用并发收集
  • 实时目标:可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收

当然G1要达到实时性的要求,相对传统的分代回收算法,在性能上会有一些损失。适合追求停顿时间的应用。

4.6.2 基本概念

4.6.2.1 分区(Region)
  • Region
    G1采取了不同的策略来解决并行、串行和CMS收集器的碎片、暂停时间不可控制等问题——G1将整个Java堆分成相同大小的分区(Region),如下图所示:
    Java-JVM-GC_第21张图片

  • 分代
    每个Region都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。

    年轻代、幸存区、老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。而他们在物理上不需要连续,则带来了额外的好处——为了达到对回收时间的可预计性,G1在扫描了Region以后,对其中的活跃对象的大小进行排序,每次根据允许的收集时间来优先回收价值最大的Region,以便快速回收空间(要复制的活跃对象少了,所以这种方式被称为Garbage First(G1)的垃圾回收算法,即:垃圾优先的回收。

    而新生代其实并不是适用于这种算法的,依然是在新生代满了的时候,对整个新生代进行回收——整个新生代中的对象,要么被回收、要么晋升,至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小。

    在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

  • 内存整理
    G1还是一种带压缩(整理)的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩。

4.6.2.2 收集集合(Collection Set)

一组可被回收的Region的集合,称为Collection Set,简称CSet。在CSet中存活的数据会在GC过程中被移动到另一个可用Region,CSet中的Region可以来自Eden空间、survivor空间、或者老年代。

CSet会占用不到整个堆空间的1%大小。

4.6.2.3 已记忆集合(Remembered Set)

已记忆集合即Remembered Set,简称为RSet,G1引入了RSet来记录其他Old Region引用Eden区Region对象的情况,属于points-into结构(谁引用了我的对象)。作为对比,在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。

而G1中因为分区数量众多,有时候不需要扫描全Region,仅关心部分Region及该Region对象被其他Region的引用情况,所以G1 RSet是point-in思想。具体来说,由于每次GC时,所有年轻代Region都会被扫描,所以只需要记录老年代到新生代之间的引用即可。

RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可。

如下图所示,Region1和Region3中的对象都引用了Region2中的对象,因此在Region2的RSet中记录了这两个引用。

Java-JVM-GC_第22张图片
Java-JVM-GC_第23张图片

  • 摘一段R大的解释
    G1 GC则是在points-out的card table之上再加了一层结构来构成points-into RSet:每个region会记录下到底哪些别的Region有指向自己的指针,而这些指针分别在哪些card的范围内。

    这个RSet其实是一个HashTable,key是别的Region的起始地址,value是一个集合,里面的元素是card table的index。举例来说,如果region A的RSet里有一项的key是region B,value里有index为1234的card,它的意思就是region B的一个1234 card里有引用指向region A。

    所以对region A来说:

    • 该RSet记录的是points-into的关系;
    • card table仍然记录了points-out的关系。
4.6.2.4 Snapshot-At-The-Beginning(SATB)

考虑如下情况,其中黑色代表根对象或已扫描该对象及子对象,灰色代表对象本身被扫描但还没扫描完该对象中的子对象,白色代表未被扫描对象及扫描完成后依然不可达对象:
Java-JVM-GC_第24张图片
这个时候执行A.c=C;B.c=null,则对象图变为:
Java-JVM-GC_第25张图片
这时候垃圾收集器再标记扫描的时候就会下图成这样:
Java-JVM-GC_第26张图片
我们会发现随着B被扫描完后本来或者的C却被认为是垃圾需要被回收了,显然错误。解决方式有两种:

  • 在插入的时候记录对象
    在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象(未被扫描对象)的引用被赋值到一个黑对象(已扫描本身及子对象的对象)的字段里,那就把这个白对象变成灰色的,表明需要扫描其子对象。即CMS是在插入的时候记录下来。

    而CMS会忽略那些本来或者但在并发标记阶段删除(死亡)的对象,留到下一次GC处理。

  • 在删除的时候记录对象
    在G1中,使用的是STABsnapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:

    1. 在开始标记的时候生成一个快照图标记存活对象
    2. 在并发标记的时候所有被改变的对象入队(在write barrier里拦截后把所有旧的引用所指向的对象都变成非白的)
    3. 可能存在游离的垃圾,将在下次被收集

SATB是维持并发GC的正确性的一个手段,G1 GC的并发理论基础就是SATB,SATB是由Taiichi Yuasa为增量式标记清除垃圾收集器设计的一个标记算法。Yuasa的SATAB的标记优化主要针对标记-清除垃圾收集器的并发标记阶段。按照R大的说法:CMS的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;G1的SATB设计在remark阶段则只需要扫描剩下的satb_mark_queue。

SATB算法创建了一个对象图,它是堆的一个逻辑“快照”。标记数据结构包括了两个位图:previous marking bitmapnext marking bitmap。previous marking bitmap保存了最近一次完成的标记信息,而并发标记周期会创建并更新next marking bitmap。随着时间的推移,previous marking bitmap会越来越过时,最终在并发标记周期结束的时候,next marking bitmap会将previous marking bitmap覆盖掉。

4.6.3 运行流程

4.6.3.1 概述

G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。分代式G1模式下有两种选定CollectionSet的子模式,分别对应young GC与mixed GC:

  • Young GC
    选定所有young gen里的region。通过控制young gen的region个数来控制young GC的开销。

  • Mixed GC
    选定所有young gen里的region,外加根据global concurrent marking统计得出收集收益高的若干old gen region。在用户指定的开销目标范围内尽可能选择收益高的old gen region。

    可以看到young gen region总是在CSet内。因此分代式G1不维护从young gen region出发的引用涉及的RSet更新。

    还需要注意的是,Mixed GC的initial marking阶段一般都会在young GC里面就顺带做了,因为工作有重叠。(除了System.gc()+ -XX:+ExplicitGCInvokesConcurrent的情况)

    所以假想的G1 GC时间线如下
    Java-JVM-GC_第27张图片

  • Full GC
    如果mixed GC无法跟上mutator(指会修改引用的应用程序)分配的速度(针对老年代分区的回收速度比较慢、对象过快得从新生代晋升到老年代、有很多大对象直接在老年代分配),导致没有足够的空region来完成mixed GC,那么就会使用serial old GC( mark-compact)来对整堆收集一次(Full GC)。

4.6.3.2 新生代GC
  • 触发条件
    Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。
  • 收集范围
    G1 Young GC会对整个新生代进行回收
  • STW情况
    G1 Young GC期间,整个应用STW
  • 并行情况
    新生代GC是由多GC线程并发执行的
  • 数据移动
    • Eden空间的数据移动到Survivor空间中;此时如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
    • Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。
    • 最终Eden空间的数据为空,GC停止工作,应用线程继续执行。
    • 回收前,其中箭头为数据移动方向:
      Java-JVM-GC_第28张图片
    • 回收后,可以看到Eden和Survivor区部分移动到新的Survivor区,部分晋升到Old区:
      Java-JVM-GC_第29张图片
      Young GC 运行流程如下:
  1. 根扫描
    扫描作为GC Roots的静态和栈中对象
  2. 更新RSet
    处理dirty card队列更新RSet
  3. 处理RSet
    检测从年轻代指向老年代的对象
  4. 对象拷贝
    拷贝存活的对象到survivor/old区域
  5. 处理引用队列
    软引用,弱引用,虚引用处理
4.6.3.3 Mixed GC

Java-JVM-GC_第30张图片
Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。

它的GC步骤分可以相对独立的执行的两步:

  1. 全局并发标记(global concurrent marking)
  2. 拷贝存活对象(evacuation)
4.6.3.3.1 全局并发标记(global concurrent marking)

具体步骤如下:

  1. 初始标记(Initial Marking)- 需要STW
    初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并将它们的字段压入扫描栈(marking stack)中等到后续扫描,且修改TAMSNext Top at Mark Start)的值,让下一并发标记阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要STW,但耗时很短。具体如下:

    • G1对于每个Region都保存了两个标识用的bitmap,一个为previous marking bitmap,一个为next marking bitmap,bitmap中包含了一个bit的地址信息来指向对象的起始点。G1使用外部的bitmap来记录mark信息,而不使用对象头的mark word里的mark bit。
    • 开始Initial Marking之前,首先并发的清空next marking bitmap,然后停止所有应用线程,并扫描标识出每个region中root可直接访问到的对象,将region中top的值放入TAMSNext Top at Mark Start)中,之后恢复所有应用线程。
    • 触发这个步骤执行的条件为:
      • G1定义了一个JVM Heap大小的百分比的阀值,称为h,另外还有一个H,H的值为(1-h)Heap Size,目前这个h的值是固定的,后续G1也许会将其改为动态的,根据jvm的运行情况来动态的调整,在分代方式下,G1还定义了一个u以及soft limit,soft limit的值为H-uHeap Size,当Heap中使用的内存超过了soft limit值时,就会在一次clean up执行完毕后在应用允许的GC暂停时间范围内尽快的执行此步骤;
      • 在pure方式下,G1将marking与clean up组成一个环,以便clean up能充分的使用marking的信息,当clean up开始回收时,首先回收能够带来最多内存空间的regions,当经过多次的clean up,回收到没多少空间的regions时,G1重新初始化一个新的marking与clean up构成的环。
  2. 并发标记(Concurrent Marking)- 时间较长
    按照之前Initial Marking扫描到的对象图(在marking stack中)进行遍历,以识别这些对象的下层对象的活跃状态,对于在此期间应用线程并发修改的对象的依赖关系则记录到remembered set logs(RSLog)中,而新创建的对象则放入比top值更高的地址区间中,这些新创建的对象默认状态即为活跃的,同时修改top值。

    并发标记阶段耗时较长,可与用户程序并发执行,也可以被 STW的年轻代GC中断

  3. 最终标记暂停(Final Marking Pause)- 需要STW
    最终标记阶段是为了修正在并发标记期间因用户程序继续运行而导致标记产生变动的那一部分标记(变化记录在线程RSLog里),G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。当应用线程的RSLog未满时,是不会放入filled RS buffers中的,此时,这些RSLog中记录的card的修改就会被更新了。具体来说,最终标记阶段需要把RSLog的数据合并到Remembered Set中。

    同时这个阶段也进行弱引用处理(reference processing)。

    注意最终标记阶段暂停与CMS的remark阶段有一个本质上的区别,那就是G1最终标记暂停只需要扫描SATB buffer,而CMS的remark需要重新扫描mod-union table里的dirty card外加整个根集合,而此时整个young gen(不管对象死活)都会被当作根集合的一部分,因而CMS remark有可能会非常慢。

    最终标记阶段需要停顿线程,多GC线程并行执行。

  4. 存活对象计算及清除-筛选回收(Live Data Counting and Cleanup)- 需要STW
    该阶段首先对各个Region的回收价值和成本进行排序,然后根据用户所期望的GC停顿时间来制定回收计划并。但需要注意的是,该阶段不会在堆上清理实际对象,而是在marking bitmap里统计每个region被标记为活的对象有多少。

    这个阶段如果发现完全没有活对象的region,就会将其整体回收到可分配region列表中。

筛选阶段其实也可以做到与应用程序并发执行,但未这么做的原因是:

  • 因为只回收一部分Region,时间可控
  • 而且停顿用户线程专注于GC将大幅提高收集效率
4.6.3.3.2 拷贝存活对象(evacuation)

在全局并发标记完成后,G1现在可以知道哪些老的分区可回收垃圾最多。在某个时刻开始Mixed GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。混合式垃圾收集如下图:
Java-JVM-GC_第31张图片
在完成evacuation后,Eden和Survivor区部分移动到新的Survivor区,部分晋升到Old区。

注意:Eden和混合式GC也是采用的复制的清理策略,当GC完成后,会重新释放空间。

evacuation后如下图:
Java-JVM-GC_第32张图片
值得注意的是,在G1中,并不是说Final Marking Pause执行完了,就肯定执行Cleanup这步的,由于这步需要STW,G1为了能够达到准实时的要求,需要根据用户指定的最大的GC造成的暂停时间来合理的规划什么时候执行Cleanup,另外还有几种情况也是会触发这个步骤的执行的:

  • G1采用的是复制方法来进行收集,必须保证每次的to space的空间都是够的,因此G1采取的策略是当已经使用的内存空间达到了H时,就执行Cleanup这个步骤;
  • 对于full-youngpartially-young的分代模式的G1而言,则还有情况会触发Cleanup的执行:
    • full-young模式下,G1根据应用可接受的暂停时间和回收young regions需要消耗的时间来估算出一个yound regions的数量值,当JVM中分配对象的young regions的数量达到此值时,Cleanup就会执行;
    • partially-young模式下,则会尽量频繁的在应用可接受的暂停时间范围内执行Cleanup,并最大限度的去执行non-young regions的Cleanup。

具体来说,Evacuation阶段依赖每个Region的Remember Set来选定若干Region构成CollectionSet,采用并行复制算法把一部分region里的活对象拷贝到空region里去,然后回收原本的region的空间。整个过程完全暂停。

4.6.4 实现原理

Java-JVM-GC_第33张图片

4.6.4.1 G1特点就是博采众家之长,力求到达一种完美
  • 吸取了增量收集优点,把整个堆划分为众多相等大小的区域(region),内存的回收和划分都以region为单位。
  • G1通过每次GC只选择收集很少量Region来控制移动对象带来的暂停时间,这样整体的Full GC可以被最大程度的避免,既能实现低延迟也不会受碎片化的影响。
  • 同时,他也吸取了CMS的特点,把这个垃圾回收过程分为几个阶段,分散GC过程;
  • 而且,G1也认同分代垃圾回收的思想,认为不同对象的生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。
  • 虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
4.6.4.2 G1高效原因

这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

4.6.4.3 G1中避免全堆扫描的方法

类似其他GC器老年代和年轻代之间的引用,G1中每个Region也有对应的Remmbered Set用来记录跨Region引用。具体来说,JVM发现程序对Reference类型的数据进行写操作时,会产生一个Write Barrier来暂时中断写操作,然后检查该引用是否处于不同的Region之间。如果跨Region就通过Card Table将引用信息记录到被引用对象所属Region的Remembered Set中。

随后,在GC发生时将Remembered Set加入到GC Roots,可避免全堆扫描,而只扫描GC Roots和相关的Region即可。

4.6.4.4 巨型对象的管理
  • 巨型对象概念
    在G1中,如果一个对象的大小超过Region大小的一半,该对象就被定义为巨型对象(Humongous Object)。

    巨型对象直接分配到老年代Region(注:有的文章说是有专门的巨型对象Region),如果一个对象的大小超过一个Region的大小,那么会直接在老年代分配两个连续的Region来存放该巨型对象。

    巨型对象Region一定是连续的,分配之后也不会被移动——没啥益处。

由于巨型对象的存在,G1的堆中的分区就分成了三种类型:新生代分区、老年代分区和巨型分区,如下图所示:
Java-JVM-GC_第34张图片

  • 内存碎片
    如果一个巨型对象跨越两个Region,开始的那个Region被称为“开始巨型”,后面的分区被称为“连续巨型”,这样最后一个分区的一部分空间是被浪费掉的。那么如果有很多巨型对象都刚好比Region大小多一点,就会造成很多的空间的浪费,从而导致堆的碎片化。如果你发现有很多由于巨型对象分配引起的连续的并发周期,并且堆已经碎片化(明明空间够,但是触发了FULL GC),可以考虑调整-XX:G1HeapRegionSize参数,减少或消除巨型对象的分配。
  • 关于巨型对象的回收
    • 在JDK8u40之前,巨型对象的回收只能在并发收集周期的清除阶段或FULL GC过程中过程中被回收
    • 在JDK8u40(包括这个版本)之后,一旦没有任何其他对象引用巨型对象,那么巨型对象也可以在年轻代收集中被回收。

4.6.5 G1执行过程中的异常情况

在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。整个应用处于假死状态,不能处理任何请求,我们的程序当然不希望看到这些。那么发生Full GC的情况有哪些呢?

4.6.5.1 并发模式失败-并发标记周期开始后的FULL GC
  • GC日志
    G1启动了标记周期,但是在并发标记完成之前,就发生了Full GC,日志通常如下所示:

    51.408: [GC concurrent-mark-start]
    65.473: [Full GC 4095M->1395M(4096M), 6.1963770 secs]
     [Times: user=7.87 sys=0.00, real=6.20 secs]
    71.669: [GC concurrent-mark-abort]
    
  • GC分析
    GC concurrent-mark-start开始之后就发生了FULL GC,这说明在G1启动标记周期之后、Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。

    原因可能是针对老年代分区的回收速度比较慢 / 对象过快得从新生代晋升到老年代 / 有很多大对象直接在老年代分配。

  • 解决方案

    • 调大整个堆的大小
    • 更快得触发并发回收周期
    • 让更多的回收线程参与到垃圾收集的动作中
4.6.5.2 混合收集模式中的FULL GC
  • GC分析
    在GC日志中观察到,在一次混合收集之后跟着一条FULL GC,这意味着Mixed GC的速度太慢,在老年代释放出足够多的分区之前,应用程序就来请求比当前剩余可分配空间大的内存。
  • 解决方案
    • 增加每次Mixed GC收集掉的老年代Region个数;
    • 增加并发标记的线程数;
    • 提高混合收集发生的频率。
4.6.5.3 Evacuation失败或晋升失败
  • GC日志
    在新生代垃圾收集快结束时,找不到可用的Survivor Region接收存活下来的对象,可以在日志中看到(to-space exhausted)或者(to-space overflow),常见如下的日志:
    60.238: [GC pause (young) (to-space overflow), 0.41546900 secs]
    
  • GC分析
    这意味着整个堆的碎片化已经非常严重了,G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。
  • 解决方案
    • 增加整个堆的大小
      增加-XX:G1ReservePercent的值以及增加总的堆大小,为“目标空间”增加预留内存量
    • 通过减少-XX:InitiatingHeapOccupancyPercent提前启动标记周期
    • 通过增加-XX:ConcGCThreads选项的值来增加并发标记线程的数目
4.6.5.4 巨型对象分配失败
  • GC分析
    如果在GC日志中看到莫名其妙的FULL GC日志,又对应不到上述讲过的几种情况,那么就可以怀疑是巨型对象分配导致的。当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。

    这里我们可以考虑使用jmap命令进行堆dump,然后通过MAT对堆转储文件进行分析。

  • 解决方案
    这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。

4.6.6 小结

G1(Garbage first)收集器是最先进的收集器之一,在JDK9中提升了性能,自动决定若干重要优化参数,被当做了默认GC器。G1是面向服务端的垃圾收集器。与其他收集器相比,G1收集器有如下优点:

  • 并行与并发
    G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短STW停顿的时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

  • 分代收集
    可以不使用其他收集器配合就可管理整个Java堆。使用G1收集器时,Java堆的内存布局与其他收集器有很大的差别,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

  • 内存整理-无碎片
    与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

  • 可预测的停顿
    G1除了降低停顿外,还能建立可预测的停顿时间模型。降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。这几乎是实时JavaGC器特征。

    G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行所有Region的垃圾收集。G1跟踪各个Region里垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表。每次优先收集价值最大的那个Region。这样就保证了在有限的时间内尽可能提高效率。

4.6.7 使用场景

  • 类似CMS允许GC线程和应用线程并行执行,即需要额外的CPU资源
  • 需要更易预测的GC暂停时间
  • 不需要实现很高的吞吐量,因为G1 GC对性能有一定影响

4.7 ZGC

一文读懂Java 11的ZGC为何如此高效

4.8 小结

  • 串行处理器:

    • 适用情况:数据量比较小(100M左右);单处理器下并且对响应时间无要求的应用。
    • 缺点:只能用于小型应用
  • 并行处理器:

    • 适用情况:“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算。
    • 缺点:应用响应时间可能较长
  • 并发处理器:

    • 适用情况:“对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器、电信交换、集成开发环境。
  • 在JDK7中ParNew/CMS对比G1
    以下对比中,G1的参数为-XX:G1RSetUpdatingPauseTimePercent=20,红色为ParNew/CMS,蓝色为G1。

    • CPU使用率
      Java-JVM-GC_第35张图片
    • 应用程序在GC期间吞吐量
      Java-JVM-GC_第36张图片

5 GC 配置

可参考JVM服务端常用启动配置(JDK8)

5.1 堆大小设置

在通过一张图来了解如何通过参数来控制各区域的内存大小:
Java-JVM-GC_第37张图片

-Xms:设置堆的最小空间大小。

-Xmx:设置堆的最大空间大小。

-Xmn:设置年轻代大小

-XX:NewSize设置新生代最小空间大小。

-XX:MaxNewSize设置新生代最大空间大小。

-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为14,年轻代占整个堆的1/5

-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6

-XX:PermSize设置永久代最小空间大小。

-XX:MaxPermSize设置永久代最大空间大小。

-Xss设置每个线程的堆栈大小

-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。

-XX:ParallelGCThreads=20:配置并行收集器的线程数,:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。

-XX:MaxTenuringThreshold=0:设置新生代对象最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

注意:没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制:

老年代空间大小=堆空间大小-年轻代大空间大小

典型JVM参数配置参考:

-Xmx3550m
-Xms3550m
-Xmn2g
-Xss128k
-XX:ParallelGCThreads=20
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC

其中:
-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。

-Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小+年老代大小+持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,官方推荐配置为整个堆的3/8。

-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大 小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000 左右。

5.2 回收器选择

5.2.1 DefNew-串行收集

  • -XX:+UseSerialGC
    新生代,老年代都使用串行回收收集器

5.2.2 ParNew-并行收集器

  • -XX:+UseParNewGC
    新生代使用ParNew并行收集器,老年代使用SerialOld串行回收收集器
  • -XX:+UseConcMarkSweepGC
    新生代使用ParNew并行收集器,老年代使用CMS并发收集
  • 并行收集器设置
    • -XX:ParallelGCThreads=n
      设置并行收集器收集时使用的CPU数。
    • -XX:MaxGCPauseMillis=n
      设置并行收集最大暂停时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
    • -XX:GCTimeRatio=n
      设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
    • -XX:+UseAdaptiveSizePolicy
      并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时打开。

5.2.3 PSYoungGen-Parallel Scavenge吞吐量优先

  • -XX:+UseParallelOldGC
    新生代,老年代都使用并行回收收集器。其中PS Scavenge 收集新生代,而Parallel Old 收集整堆。

  • -XX:+UseParallelGC -XX:ParallelGCThreads=20
    UseParallelGC:新生代使用并行回收收集器,老年代使用串行收集器;

    ParallelGCThreads:配置新生代并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。

  • -XX:MaxGCPauseMillis=100
    设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。

  • -XX:+UseAdaptiveSizePolicy
    设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

5.2.4 CMS-响应时间优先的并发收集器

  • -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
    UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。

    UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

  • -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
    UseCMSCompactAtFullCollection:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生"碎片",使得运行效率降低。此配置可打开CMS对年老代的压缩。可能会影响性能,但是可以消除碎片.

    CMSFullGCsBeforeCompaction:此值设置运行多少次CMS GC以后对老年代内存空间进行压缩、整理。

  • -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。

  • -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

  • -XX:+CMSClassUnloadingEnabled:可使permgen加入cms collection cycle中来

  • -XX:+UseCMSInitiatingOccupancyOnly:控制CMS cycle是不是纯粹由oldgen(或permgen if CMSClassUnloadingEnabled=TRUE)的使用比例来触发;

  • -XX:CMSInitiatingOccupancyFraction=70和-XX:CMSInitiatingPermOccupancyFraction
    可以精细在oldgen和permgen分别控制触发CMS cycle的比例。

    注意,在永久代移除后,CMS相关参数中,CMSInitiatingPermOccupancyFraction被去掉了,其他三个参数继续有用:UseCMSInitiatingOccupancyOnly、CMSClassUnloadingEnabled和CMSInitiatingOccupancyFraction。

5.2.5 G1(garbage-first heap)

参数名 含义 默认值
-XX:+UseG1GC 使用G1收集器 JDK1.8中还需要显式指定
-XX:MaxGCPauseMillis=n 设置一个期望的最大GC暂停时间,这是一个柔性的目标,JVM会尽力去达到这个目标 200
-XX:InitiatingHeapOccupancyPercent=n 当整个堆的空间使用百分比超过这个值时,就会触发一次并发收集周期,记住是整个堆 45
-XX:NewRatio=n 新生代和老年代的比例 2
-XX:SurvivorRatio=n Eden空间和Survivor空间的比例 8
-XX:MaxTenuringThreshold=n 对象在新生代中经历的最多的新生代收集,或者说最大的岁数 G1中是15
-XX:ParallelGCThreads=n 设置垃圾收集器的并行阶段的垃圾收集线程数 不同的平台有不同的值
-XX:ConcGCThreads=n 设置垃圾收集器并发执行GC的线程数 n一般是ParallelGCThreads的四分之一
-XX:G1ReservePercent=n 设置作为空闲空间的预留内存百分比,以降低目标空间溢出(疏散失败)的风险。默认值是 10%。增加或减少这个值,请确保对总的 Java 堆调整相同的量 10
-XX:G1HeapRegionSize=n 分区的大小 堆内存大小的1/2000,单位是MB,值是2的幂,范围是1MB到32MB之间
-XX:G1HeapWastePercent=n 设置您愿意浪费的堆百分比。如果可回收百分比小于堆废物百分比,JavaHotSpotVM不会启动混合垃圾回收周期(注意,这个参数可以用于调整混合收集的频率)。 JDK1.8是5
-XX:G1MixedGCCountTarget=8 设置并发周期后需要执行多少次混合收集,如果混合收集中STW的时间过长,可以考虑增大这个参数。(注意:这个可以用来调整每次混合收集中回收掉老年代分区的多少,即调节混合收集的停顿时间) 8
-XX:G1MixedGCLiveThresholdPercent=n 一个分区是否会被放入mix GC的CSet的阈值。对于一个分区来说,它的存活对象率如果超过这个比例,则改分区不会被列入mixed gc的CSet中 JDK1.6和1.7是65,JDK1.8是85

5.3 GC日志选项

5.3.1 -XX:+PrintGC

输出格式:

[GC 118250K->113543K(130112K), 0.0094143 secs]
[Full GC 121376K->10414K(130112K), 0.0650971 secs]

5.3.2 -XX:+PrintGCDetails

打印GC中的详细信息,包括使用什么搜集器

输出格式:

[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]

[GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

5.3.3 -XX:+PrintGCTimeStamps -XX:+PrintGC

PrintGCTimeStamps可与上面两个混合使用,打印GC发生的时间,自JVM启动以后开始统计,单位为秒。

还有个-XX:+PrintGCDateStamps,可记录GC发生的时间信息。

输出格式:

11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

5.3.4 -XX:+PrintGCApplicationConcurrentTime

打印每次GC期间,用户程序继续并发执行时间。可与前面提到的参数混合使用

输出格式:

Application time: 0.5291524 seconds

5.3.5 -XX:+PrintGCApplicationStoppedTime

打印GC期间用户程序暂停的时间。可与上面混合使用

输出格式:

Total time for which application threads were stopped: 0.0468229 seconds

5.3.6 -XX:PrintHeapAtGC

打印GC前后的详细堆栈信息

输出格式:

34.702: [GC {Heap before gc invocations=7:

def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)

  eden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)

  from space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000)

  to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000)

  tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)

  the space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)

  compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)

  the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)

  ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)

  rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)

  34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:

  def new generation total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)

  eden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)

  from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)

  to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000)

  tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)

  the space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)

  compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)

  the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)

  ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)

  rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)

  }

  , 0.0757599 secs]

5.3.7 -Xloggc:filename

与上面几个配合使用,把相关日志信息记录到文件以便分析。

6 GC日志解读

6.1 GC配置

我们通过 -XX:+UseSerialGC 选项指定使用串行垃圾收集器, 并使用下面的启动参数让 JVM 打印出详细的GC日志:

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps

这样配置以后,发生GC时输出的日志就类似于下面这种格式(为了显示方便,已手工折行):

2015-05-26T14:45:37.987-0200: 151.126: 
  [GC (Allocation Failure) 151.126:
    [DefNew: 629119K->69888K(629120K), 0.0584157 secs]
    1619346K->1273247K(2027264K), 0.0585007 secs] 
  [Times: user=0.06 sys=0.00, real=0.06 secs]

2015-05-26T14:45:59.690-0200: 172.829: 
  [GC (Allocation Failure) 172.829: 
    [DefNew: 629120K->629120K(629120K), 0.0000372 secs]
    172.829: [Tenured: 1203359K->755802K(1398144K), 0.1855567 secs]
    1832479K->755802K(2027264K),
    [Metaspace: 6741K->6741K(1056768K)], 0.1856954 secs]
  [Times: user=0.18 sys=0.00, real=0.18 secs]

6.2 解读

6.2中的GC日志暴露了JVM中的一些信息。事实上,这个日志片段中发生了 2 次垃圾回收事件(Garbage Collection events)。其中一次清理的是年轻代(Young generation), 而第二次处理的是整个堆内存。下面我们来看,如何解读第一次GC事件,发生在年轻代中的小型GC(Minor GC):
Java-JVM-GC_第38张图片
Java-JVM-GC_第39张图片
通过上面的分析, 我们可以计算出在垃圾收集期间, JVM 中的内存使用情况。在垃圾收集之前, 堆内存总的使用了 1.54G (1,619,346K)。其中, 年轻代使用了 614M(629,119k)。可以算出老年代使用的内存为: 967M(990,227K)。

下一组数据( -> 右边)中蕴含了更重要的结论, 年轻代的内存使用在垃圾回收后下降了 546M(559,231k), 但总的堆内存使用(total heap usage)只减少了 337M(346,099k). 通过这一点,我们可以计算出, 有 208M(213,132K) 的年轻代对象被提升到老年代(Old)中。

这个GC事件可以用下面的示意图来表示, 上方表示GC之前的内存使用情况, 下方表示结束后的内存使用情况:
Java-JVM-GC_第40张图片

6.3 user,sys,real

  • user
    用户空间(Linux系统内核以外)在GC期间消耗的CPU时间

  • sys
    Linux系统内核在GC期间消耗的CPU时间。

  • real
    GC期间消耗的总CPU时间。

    需要注意的是,几乎所有GC事件中real时间< user时间 + sys时间。原因是GC线程并发执行,比如user+sys=2000ms,有5个GC线程并行,则real时间略高于2000ms/5=400ms,原因是CPU竞争或是IO负载较高(可参考brilliant article from LinkedIn engineers)。

7 GC调优

首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。

对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数,过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量。特别要关注Full GC,因为它会对整个堆进行整理

7.1 新生代大小选择

  • 响应时间优先的应用???
    尽可能将年轻代设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。

  • 吞吐量优先的应用
    尽可能将年轻代设大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

  • 新生代设置过小

    • MinorGC次数非常频繁,增大系统消耗;
    • 导致大对象直接进入旧生代,占据了旧生代剩余空间,诱发Full GC
  • 新生代设置过大

    • 直接导致旧生代过小(堆总大小一定),从而诱发较多Full GC;
    • 新生代GC耗时大幅度增加
    • 一般说来新生代占整个堆1/3比较合适
  • Survivor设置过小
    导致对象从eden直接到达老年代,降低了在新生代的存活时间,导致更多的FullGC

  • Survivor设置过大
    导致eden过小,增加了MinorGC频率

  • MaxTenuringThreshold调大晋升年龄
    通过将-XX:MaxTenuringThreshold=n设置为一个较大值,尽量让对象在新生代Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,最终增加对象在年轻代即被回收的概率,从而避免升到老年代造成FullGC。

7.2 老年代大小选择

  1. 响应时间优先的应用
    年老代使用并发收集器,所以其大小需要慎重设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
    • 并发垃圾收集信息
    • 持久代并发收集次数
    • 传统GC信息
    • 花在年轻代和年老代回收上的时间比例

减少年轻代和年老代花费的时间,一般会提高应用的效率

  1. 吞吐量优先的应用
    一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

7.3 较小堆引起的碎片问题

参见这里

7.4 FullGC过多

可参考关于full gc频繁的分析及解决

一般FullGC有如下原因和解决方案:

  • 老年代空间不足
    调优时尽量让对象在新生代GC时被回收,让对象在新生代多存活一段时间和不要创建过大的对象及数组,从而避免直接在旧生代创建对象 。

  • Concurrent Mode Failure
    参见这里。需要设置-XX:CMSInitiatingOccupancyFraction=指定还有多少剩余堆百分比时开始执行并发收集,避免CMS 老年代GC时因为又有对象进入老年代,而空间不够导致的FullGC。

    还可参考这里开启CMS GC对老年代压缩。

  • 老年代内存碎片
    由于采用CMS造成老年代大量内存碎片,无法提供连续空间存入大对象导致触发Full GC。关于内存碎片可参考这里

  • Pemanet Generation空间不足
    增大Perm Gen空间,避免太多静态对象 。移除永久代换为元空间后没有这个。

  • 统计得到的GC后晋升到老年代的平均大小大于老年代剩余空间
    控制好新生代和老年代大小的比例

  • System.gc()被显示调用
    垃圾回收不要手动触发,尽量依靠JVM自身的机制

7.5 G1调优

7.5.1 G1调优原则

  1. 不要自己显式设置新生代的大小(避免使用-Xmn或-XX:NewRatio参数),如果显式设置新生代的大小,会导致目标时间这个参数失效。

  2. 关于设置最大暂停时间
    由于G1收集器自身已经有一套预测和调整机制了,因此我们首先的选择是相信它,即调整-XX:MaxGCPauseMillis=N参数,这也符合G1的目的——让GC调优尽量简单

    首先,我们能容忍的最大暂停时间是有一个限度的,我们需要在这个限度范围内设置。但是应该设置的值是多少呢?我们需要在吞吐量跟MaxGCPauseMillis之间做一个平衡:

    • 如果MaxGCPauseMillis设置的过小,就意味着会调小新生代的大小,那么GC就会频繁,吞吐量就会下降。同时,还会导致Mixed GC周期中回收的老年代分区减少,从而增加FULL GC的风险。
    • 如果MaxGCPauseMillis设置的过大,应用程序暂停时间就会变长。G1的默认暂停时间是200毫秒,我们可以从这里入手,调整合适的时间。

7.5.2 关键调优参数

  • -XX:G1HeapRegionSize=n
    设置的G1区域的大小。值是2的幂,范围是1MB到32MB之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。

  • -XX:ParallelGCThreads=n
    设置垃圾收集器的STW并行阶段的垃圾收集线程数。将n的值设置为逻辑处理器的数量,最多为8。

    如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的 5/8 左右。这适用于大多数情况,除非是较大的 SPARC 系统,其中n的值可以是逻辑处理器数的 5/16 左右。

  • -XX:ConcGCThreads=n
    设置并行标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads) 的1/4左右。

  • -XX:InitiatingHeapOccupancyPercent=45
    设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。

7.5.3 G1 Full GC解决

见G1 Full GC

7.5.4 G1最佳实践

  1. 关键参数项
    • -XX:+UseG1GC,告诉JVM使用G1垃圾收集器
    • -XX:MaxGCPauseMillis=200,设置GC暂停时间的目标最大值,这是个柔性的目标,JVM会尽力达到这个目标
    • -XX:InitiatingHeapOccupancyPercent=45,如果整个堆的使用率超过这个值,G1会触发一次并发周期。记住这里针对的是整个堆空间的比例,而不是某个分代的比例。
  2. 最佳实践
    1. 不要设置年轻代的大小
      通过-Xmn显式设置年轻代的大小,会干扰G1收集器的默认行为:
      • G1不能再以设定的暂停时间为目标,换句话说,如果设置了年轻代的大小,就无法实现自适应的调整来达到指定的暂停时间这个目标
      • G1不能按需扩大或缩小年轻代的大小
    2. 响应时间度量
      不要根据平均响应时间(ART)来设置-XX:MaxGCPauseMillis=n这个参数,应该设置希望90%的GC都可以达到的暂停时间。这意味着90%的用户请求不会超过这个响应时间,记住,这个值是一个目标,但是G1并不保证100%的GC暂停时间都可以达到这个目标

8 GC分类和触发条件

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

  • Partial GC:并不收集整个GC堆的模式
    • Young GC:只收集young gen的GC
    • Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
    • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
  • Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

8.1 MinorGC(Young GC)

8.1.1 概念

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,只收集young gen。

当发生MinorGC事件的时候,有一些有趣的地方需要注意:

  1. 当 JVM 无法为一个新的对象在年轻代分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
  2. MinorGC标记复制
    内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。
  3. 执行 Minor GC 操作时,不会影响到永久代。
    从永久代到年轻代的引用被当成 GC roots,而从年轻代到永久代的引用在标记阶段被直接忽略掉。
  4. 质疑常规的认知,所有的 Mino GC 都会触发stop-the-world,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,MinorGC 执行时暂停的时间将会长很多。

所以 Minor GC 的情况就相当清楚了——每次 MinorGC 会清理年轻代的内存。

8.1.2 触发条件

  • 当young gen中的eden区分配满的时候触发MinorGC。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。

    至于为什么不是eden+from survivor满才触发minor gc,是因为HotSpot的GC设计是只在eden里直接分配新对象的,survivor space只是个young跟old之间的缓冲区,用来让中等存活期的对象不要过早晋升到old gen。

  • PSScavenge和SerialOld(PSMarkSweep)搭配使用时,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一小下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量)。控制这个行为参数是-XX:+ScavengeBeforeFullGC

8.1.3 MinorGC与老年代关系

虚拟机在进行MinorGC之前,会先判断老年代最大的可用连续空间是否大于新生代的所有对象总空间

  1. 如果大于的话,直接执行MinorGC
  2. 如果小于,判断是否开启HandlerPromotionFailure,没有开启直接FullGC
  3. 如果开启了HanlerPromotionFailure, JVM会判断老年代的最大连续内存空间是否大于历次晋升的大小,如果小于直接执行FullGC
  4. 如果大于的话,执行MinorGC

8.2 MajorGC(OldGC)

8.2.1 概念

Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC。

如果是指代OldGC,则是清理老年代,只有CMS的concurrent collection是这个模式,其他的MajorGC都会触发full gc。

8.2.2 触发条件

CMS GC主要是定时去检查old gen的使用量,CMS GC的initial marking的触发条件是当使用量超过了触发比例就会启动一次CMS GC(因为是并发收集,会产生浮动垃圾,需要预留空间),对old gen做并发收集。

8.3 FullGC

8.3.1 概念

清理整个堆空间—包括年轻代和老年代。

**注意,HotSpot VM的GC里,除了CMS的concurrent collection能只收集old gen以外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC。

  • FullGC的例外情况
    但PSScavenge和SerialOld(PSMarkSweep)搭配使用时,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一小下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量),然后紧接着去用PS MarkSweep执行一次真正的full GC收集全堆。所以有可能观察到这个跟随的full GC时,Eden区没什么变化,Survivor区反而被清空,老年代上升的情况。

  • 两种算法

    • 分代收集算法Full GC
      当老年代(屡次young gc后依然存活的对象晋升老年代的,以及巨型对象直接到老年代还命长的,再加上一直赖在老年代寿命还长的一起把老年代占满了)或永久区满了,会开始在所有分代做FullGC。通常先收集年轻代。分代收集过程中使用该代对应设计的GC算法,因为是定制化设计的通常最快,即年轻代就用年轻代算法如复制,老年代和永久代采用老年代算法如标记整理或标记清除。如果发生Compact,则每代分开做。
    • 老年代算法全堆Full GC
      当年轻代GC后把符合条件的存活对象放入老年代时,如果老年代空间不够存放可能晋升上来的年轻代对象,则除了CMS (CMS只能收集老年代,年轻代优先使用ParNew/DefNew,Concurrent Mode Fail时会发生STW,此时回退到使用SerialOldGC的标记-整理算法来做FullGC)以外的GC器都会直接在整个堆内各代运行老年代收集算法(如标记-整理或标记-清除)

8.3.2 触发条件

  • 老年代空间不足
    如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发FullGC。为了避免这种情况,最好就是不要创建太大的对象。

  • 持久代空间不足(如果还有的话)
    如果有持久代空间的话,系统当中需要加载的类/调用的方法很多,同时持久代当中没有足够的空间,就触发一次FullGC

  • YoungGC出现promotion failure
    promotion failure发生在Young GC, 如果Survivor区当中存活对象的年龄达到了设定值,会就将Survivor区当中的对象拷贝到老年代,如果老年代的空间不足,就会发生promotion failure, 接下去就会发生Full GC.

  • 统计YoungGC发生时晋升到老年代的平均总大小大于老年代的空闲空间
    在发生前YoungGC时会判断,是否安全,即当前老年代空间可以容纳YoungGC晋升的对象的平均大小。如果不安全,就不会执行YoungGC,转而执行Full GC。

  • CMS出现concurrent mode failure,会使用Full GC

  • 显示调用System.gc

8.3.3 执行流程

  1. marking
    从GC Roots出发,扫描一遍整个Java Heap(有时可以加上元空间),找到所有存活对象
  2. Compute new address
    我们已知所有的存活对象,所以能够准确计算出它们在第四步Compact时的新地址,将其存在对象头的mark word
  3. Adjust new address
    更新所有存活对象上存的引用指向第二步计算好的新地址
  4. compaction-Copy to new address
    复制存活对象到第二步计算出的新地址;

这类算法有Serial Old GC、PS MarkSweep GC、Parallel Old GC、Full GC for CMS算法和Full GC for G1 GC算法。应该能想到步骤1 2 3 4 都可以并行处理。

需要注意的是,FullGC是整体收集的,无所谓先收集old还是young。marking阶段是整体一起做的,然后compaction是old gen先来再处理young gen。

也就是说,Full GC时,就不在分 “young gen使用young gen自己的收集器(一般是copy算法);old gen使用old gen的收集器(一般是mark-sweep-compact算法)”,而是,整个heap以及perm gen,所有内存,全部的统一使用 old gen的收集器(一般是mark-sweep-compact算法) 一站式搞定。

8.4 Mixed GC

8.4.1 概念

  • 收集整个young gen以及部分old gen的GC。
  • 只有G1有这个模式

8.4.2 触发条件

G1 GC的initial marking的触发条件是Heap使用比率超过某值,跟CMS类似;

9 GC应用

9.1 Tomcat程序日志配置

打开$TOMCAT_HOME/bin/catalina.sh

  • 程序日志和gc日志混合:
    export JAVA_OPTS="-Xms4G -Xmx6G -XX:PermSize=256m 
    -XX:MaxPermSize=512m -Xss1024k -XX:+DisableExplicitGC 
    -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled 
    -XX:+UseCMSCompactAtFullCollection 
    -XX:+UseFastAccessorMethods 
    -XX:+UseCMSInitiatingOccupancyOnly 
    -XX:CMSInitiatingOccupancyFraction=80 
    -XX:+PrintGC 
    -XX:+PrintGCTimeStamps"
    
    export CATALINA_OUT=/var/log/java/tomcat-xxx-8000/tomcat/catalina.out
    
  • gc日志单独:
    export JAVA_OPTS="-Xms4G -Xmx6G -XX:PermSize=256m 
    -XX:MaxPermSize=512m -Xss1024k -XX:+DisableExplicitGC 
    -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled 
    -XX:+UseCMSCompactAtFullCollection 
    -XX:+UseFastAccessorMethods 
    -XX:+UseCMSInitiatingOccupancyOnly 
    -XX:CMSInitiatingOccupancyFraction=80 
    -XX:+PrintGC -XX:+PrintGCTimeStamps 
    -Xloggc:var/log/java/tomcat-xxx-8000/tomcat/tomcat_gc.log"
    

10 GC分析工具

可参考

  • Java-JVM-监控诊断工具
  • GCViewer
  • Jstat

11 GC FAQ

  1. write barrier在GC中的作用?如何理解G1 GC中write barrier的作用?

    写屏障是一种内存管理机制,用在这样的场景:
    当代码尝试修改一个对象的引用时,在前面放上写屏障就意味着将这个对象放在了写屏障后面。

    write barrier在GC中的作用有点复杂,我们这里以trace GC算法为例讲下:

    trace GC有些算法是并发的,例如CMS和G1,即用户线程和垃圾收集线程可以同时运行,即mutator(用户应用程序)一边跑,collector一边收集。这里有一个限制是:黑色的对象不应该指向任何白色的对象。如果mutator视图让一个黑色的对象指向一个白色的对象,这个限制就会被打破,然后GC就会失败。针对这个问题有两种解决思路:

    • 通过添加read barriers阻止mutator看到白色的对象;

    • 通过write barrier阻止mutator修改一个黑色的对象,让它指向一个白色的对象。
      write barrier的解决方法就是将黑色的对象放到写write barrier后面。如果真得发生了white-on-black这种写需求,一般也有多种修正方法:增量得将白色的对象变灰,将黑色的对象重新置灰等等。我理解,增量的变灰就是CMS和G1里并发标记的过程,将黑色的对象重新变灰就是利用卡表或SATB的缓冲区将黑色的对象重新置灰的过程,当然会在重新标记中将所有灰色的对象处理掉。关于G1中write barrier的作用,可以参考R大的这个帖子里提到的:
      然后G1在mutator一侧需要使用write barrier来实现:

      • SATB snapshot的完整性
      • 跨region的引用记录到RSet里。

      这两个动作都使用了logging barrier,其处理有一部分由collector一侧并发执行。 `

  2. G1里在并发标记的时候,如果有对象的引用修改,要将旧的值写到一个缓冲区中,这个动作前后会有一个write barrier,这段可否细说下?

    这块涉及到SATB标记算法的原理,SATB是指start at the beginning,即在并发收集周期的第一个阶段(初始标记)是STW的,会给所有的分区做个快照,后面的扫描都是按照这个快照进行;在并发标记周期的第二个阶段,并发标记,这是收集线程和应用线程同时进行的,这时候应用线程就可能修改了某些引用的值,导致上面那个快照不是完整的,因此G1就想了个办法,我把在这个期间对对象引用的修改都记录动作都记录下来,有点像mysql的操作日志。

  3. GC算法中的三色标记算法怎么理解?

    trace GC将对象分为三类:白色(垃圾收集器未探测到的对象)、灰色(活着的对象,但是子对象依然没有被垃圾收集器扫描过)、黑色(活着的对象,并且已经被垃圾收集器扫描过)。

    垃圾收集器的工作过程,就是通过灰色对象的指针扫描它指向的白色对象,如果找到一个白色对象,就将它设置为灰色,如果某个灰色对象的可达对象已经全部找完,就将它设置为黑色对象。当在当前集合中找不到灰色的对象时,就说明该集合的回收动作完成,然后所有白色的对象的都会被回收。

好书推荐

  • The Garbage Collection Handbook
    英文原版
  • 垃圾回收算法手册:自动内存管理的艺术
    中文翻译版
  • 从表到里学习JVM实现

好文推荐

  • Java-JVM知识总结
  • Java-JVM-监控诊断工具
  • GC性能优化
  • 应用GC长时间停顿分析
  • JVM参数设置和分析一览
  • java的gc为什么要分代? - RednaxelaFX 的回答 - 知乎
  • Major GC和Full GC的区别是什么?触发条件呢?- RednaxelaFX 的回答 - 知乎
  • 主流的垃圾回收机制都有哪些? - RednaxelaFX 的回答 - 知乎
  • 关于CMS、G1垃圾回收器的重新标记、最终标记疑惑? - RednaxelaFX 的回答 - 知乎
  • 火车算法在目前在哪些JVM中有用到,G1吗? - RednaxelaFX 的回答 - 知乎
  • JVM GC遍历一次新生代所有对象是否可达需要多久?- RednaxelaFX 的回答 - 知乎
  • 有关 Copying GC 的疑问?- RednaxelaFX 的回答 - 知乎
  • 关于HotSpot VM的Serial GC中的minor GC的“简单”讲解- RednaxelaFX
  • Memory Management in the Java HotSpotTM Virtual Machine

参考文档

  • REAL TIME IS GREATER THAN USER AND SYS TIME
  • JVM有关垃圾回收机制的配置
  • minor gc 会发生stop the world 现象吗?
  • 关于JVM垃圾搜集算法(标记-整理算法与复制算法)的效率?
  • 并发垃圾收集器(CMS)为什么没有采用标记-整理算法来实现?
  • https://hllvm-group.iteye.com/group/topic/28594
  • java的gc为什么要分代? - RednaxelaFX 的回答 - 知乎
  • 谁能简单概括下cms和旧的gc算法的区别? - RednaxelaFX 的回答 - 知乎
  • 有关 Copying GC 的疑问?
  • Memory Management in the Java HotSpotTM Virtual Machine
  • 关于CMS、G1垃圾回收器的重新标记、最终标记疑惑? - RednaxelaFX 的回答 - 知乎
  • java垃圾收集器
  • 深入理解JVM(5) : Java垃圾收集器

你可能感兴趣的:(jvm)