前言

几乎所有的对象都在堆(Heap)分配,我们只有在程序运行期时才知道要创建哪些对象,这部分内存的分配和回收都是动态的。垃圾收集器所关注的正是这部分内存。学习GC之前务必清楚地知道JVM运行时内存,可以先阅读文章 Java虚拟机——内存区域

可回收对象

垃圾收集器回收的都是可回收对象,可回收对象一般有两种方法来判断:

  • 引用计数器法:给每个对象设置一个引用计数器,对象被引用一次,计数器加一;引用失效时,计数器减一。任一时刻,引用计数器为0的对象就是可回收对象。但是引用计数器很难解决循环引用的问题,a,b两个对象如果相互引用,而没有被其他任何对象引用,理论上来说这两个对象均为可回收对象,但是因为他们的引用计数器均不为0,导致无法被回收。
  • 可达性分析:基本思路是从一系列名为“GC Roots”的对象出发开始向下搜索,搜索走过的路径称为引用链,当一个对象到“GC Roots”没有路径相连(不可达)时,该对象就是可回收对象。

GC Roots对象包括以下:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,比如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
  • 本地方法栈中引用的对象
  • 方法区中静态属性引用的对象,比如Java类的引用类型静态变量
  • 方法区中常量引用的对象,比如字符串常量池里的引用
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(如NullPointerException、OutOfMemoryError)等,还有系统类加载器
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

垃圾收集算法

标记-清除算法

标记-清除算法(Mark-Sweep)分为“标记”和“清除”两个阶段,首先标记出需要回收的对象,在标记完成后统一回收所有标记的对象。算法优点是简单,缺点有两个:效率低下容易产生内存碎片

算法可视化过程大致如下:
标记清除算法
可以看到,整个过程需要扫描两次。

复制算法

复制算法(Copying)将可用内存分为大小相等的两块,每次只使用其中的一块。当其中的一块内存用完了,就将还存活的对象复制到另一块,然后再把可回收的对象都回收。这样每次都是对一半的区域进行垃圾清理,分配对象时不用考虑内存碎片的问题,只需要移动堆顶指针,按照顺序分配内存即可。

优点是实现简单运行高效不会产生内存碎片。缺点是内存利用率太低,每次只使用一半的内存。一般年轻代会用此算法,因为年轻代中约有98%的对象都是“朝生夕死”的,所以也不需要按照1:1来划分两块区域。HotSpot虚拟机年轻代是分为Eden区和两个Survivor区,大小比例是8 : 1(是Eden区和其中一个Survivor区的比例,两个Survivor一样大)。每次只使用Eden区和其中一个Survivor区,回收时,将Eden区和Survivor区的存活对象复制到另一个Survivor区,然后清理掉Eden区和使用过的Survivor区。

算法可视化过程大致如下:
复制算法

标记-整理算法

标记整理算法(Mark-Compact)或者叫标记-压缩算法,算法分为“标记”和“整理”两个阶段,“标记”阶段和“标记-清理”算法一样。但是后续步骤不是直接清理标记对象,而是把存活对象向一端移动,然后直接清理端边界以外的内存。

算法可视化过程大致如下:
标记整理算法
可以看到标记-整理算法也不会产生内存碎片

现在低版本商用虚拟机采用的都是分代收集算法,根据对象的存活周期把堆(Heap)分为年轻代老年代永久代(JDK1.8之后改成Matespace)。每次收集,年轻代会回收绝大部分对象,所以采用复制算法,只需要消耗少量对象的复制成本。老年代中对象存活周期较长,所以采用标记-清除算法或者标记-整理算法。

垃圾收集器

垃圾收算法是理论指导,垃圾收集器则是实际应用。HotSpot虚拟机垃圾收集器大致有以下几款(高版本还有ZGCShenandoah):
垃圾收集器
连线表示两款垃圾收集器可以搭配使用,所处区域表示垃圾收集器工作的区域。G1横跨新生代和老年代是因为G1逻辑上分区,物理上不分区。

Serial收集器

Serial是最基本,发展历史最悠久的收集器。这个收集器是一个单线程的收集器,在进行垃圾收集时,必须暂停所有工程的线程,直到它收集结束,也就是“Stop The World”。优点是简单、高效,单线程不会有线程切换的开销。

-XX:+UseSerialGC参数指定使用该收集器。工作过程如下(Serial和Serial Old搭配使用):
Serial/Serial Old收集器

ParNew收集器

ParNew是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余的行为均与Serial收集器一致。

参数-XX:+UseParNewGC指定使用该收集器,工作过程如下(ParNew和Serial Old搭配使用):
ParNew-Serial Old

Parallel Scavenge收集器

Parallel Scavenge是工作于新生代垃圾收集器,使用复制算法。目的是达到一个可控制的吞吐量,是吞吐量优先的垃圾收集器。

参数-XX:+UseParallelGC指定使用该收集器,吞吐量 = 运行用户代码的时间 / (运行用户代码的时间 + 垃圾收集时间)Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大停顿时间的-XX:MaxGCPauseMillis,以及直接设置吞吐量的参数-XX:GCTimeRatio

除此之外,还提供了参数-XX:+UseAdaptivePolicy实现GC自适应调节策略,即不需要手动设置年轻代大小、Eden区与Survivor区比例、晋升老年代年龄等参数,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量。其工作过程如下(Parallel Scavenge和Parallel Old搭配使用):
Parallel Scavenge-Parallel Old

Serial Old收集器

Serial Old是Serial收集器的老年代版本,是一个单线程收集器,使用“标记-整理”算法,参数-XX:+UseSerialOldGC指定使用该收集器。

Parallel Old收集器

Parallel OldParallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,参数-XX:+UseParallelOldGC指定使用该收集器。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一款以获取最短回收时间为目的的垃圾收集器,基于“标记-清除”算法,参数-XX:+UseConcMarkSweepGC指定使用该收集器,运行过程分为四步:初始标记、并发标记、重新标记、并发清除。下文会详细介绍CMS。

G1收集器

G1(Garbage First)收集器与其他垃圾收集器相比,G1收集器具有并发与并行、分代收集、空间整合、可预测停顿等特点,参数-XX:+UseG1GC指定使用该收集器。下文会详细介绍G1。

CMS收集器

CMS(Concurrent Mark Sweep)垃圾收集器是一款以获取最短停顿时间为目的的垃圾收集器,从名字上可以看出基于“标记-清除”算法实现。整个过程分为四个步骤:
CMS-gc

  • 初始标记(CMS initial Mark):该过程仅仅标记“Gc Roots”直接关联的对象,速度很快
  • 并发标记(CMS concurrent Mark):该过程就是从Gc Roots开始向下搜索的过程
  • 重新标记(CMS remark):该过程是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动那一部分对象的标记记录,消耗时间比初始标记长,但是远小于并发标记
  • 并发清除(CMS concurrent sweep)

CMS收集器的主要优点是并发收集低停顿;主要缺点有三个:

  • 对CPU资源敏感。CMS收集器默认启动的回收线程数是(CPU数量 + 3)/ 4,当CPU数量大于4时,并发回收时垃圾线程不少于25%的CPU资源,但是当CPU数量小于4个时,CMS对用户程序的影响就会变得很大。
  • CMS收集器无法处理浮动垃圾,可能会出现Concurrent Mode Failure失败,而导致另一次Full GC的产生。在并发清除阶段,由于工作线程还在运行,所以必定会产生新的垃圾。但是新的垃圾是出现在标记之后,所以只有下一次gc的时候才能清除,这部分垃圾就是浮动垃圾
    正是由于浮动垃圾的存在,所以CMS不能等到老年代几乎快满了才进行垃圾回收,而是预留一些空间。CMS收集器在老年代使用了大约68%(参数-XX:CMSInitiatingOccupancyFraction指定)后就会启动。要是CMS运行期间预留的空间无法满足程序需要,就会出现Concurrent Mode Failure失败,这时虚拟机将启用后备方案,临时使用Serial Old收集器来重新进行老年代垃圾回收,这样停顿时间就很长了。
  • 容易产生内存碎片:因为CMS基于“标记-清除”算法。内存碎片过多的话,容易给大对象的分配带来很大的麻烦,往往老年代还有大量的空间,但是无法找到连续的空间来分配大对象,所以不得不提前触发一次Full GC
    为了解决这个问题,虚拟机提供了一个参数-XX:+UseCMSCompactAtFullCollection,用于CMS由于内存碎片要进行Full GC时,开启内存碎片的合并整理过程。内存碎片的问题解决了,但是停顿时间变长了。虚拟机还提供了另一个参数-XX:CMSFullGCsBeforeCompaction,这个参数表示执行多少次不压缩的Full GC后,接着执行一次压缩的Full GC(默认值为0,表示每次Full GC时都压缩)。

G1收集器

特点

G1收集器,全称Garbage-First Garbage Collector。在JDK1.7中正式推出,在JDK1.9已经被设置成默认收集器。G1应用在多处理器大容量内存环境中,实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。另外,它还具有以下特性:

  • 像CMS收集器一样, 能与应用程序线程并发执行
  • 整理空闲空间更快。
  • 需要更多的时间来预测GC停顿时间。
  • 不希望牺牲大量的吞吐性能。
  • 不需要更大的Java Heap。

优点

G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:

  • G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
  • G1的STW更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间-XX:MaxGCPauseMillis)。

内存结构

G1收集器并不是像上面几种收集器一样将内存空间划分为新生代、老年代和永久代(或者Metaspace),而是将堆划分为若干个区域Region(它在逻辑上是分代的,但是物理上不分代)。
G1收集器
(图片来源:https://tech.meituan.com/2016/09/23/g1.html)

这些Region被分成了Eden区(E)、Survivor区(S)、老年代(O)和Humongous区(H)。前三个概念和之前的分代含义一样。而Humongous表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象。

H-obj有如下几个特征:

  • H-obj直接分配到了老年代,防止了反复拷贝移动。
  • H-obj在global concurrent marking阶段的cleanup和full GC阶段回收。
  • 在分配H-obj之前先检查是否超过-XX:InitiatingHeapOccupancyPercent-XX:G1ReservePercent,如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures 和 full GC。

默认情况下,Region大小在1M-32M之间,是2的指数。参数-XX:G1HeapRegionSize可以修改

为了减少连续H-objs分配对GC的影响,需要把大对象变为普通的对象,建议增大Region size。

GC

这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。

从每个Region的角度来看,G1使用的是复制算法,但是从整个堆来看,G1使用的是整理(压缩)算法。

G1提供了两种GC模式:Young GCMixed GC。这里并不是说G1不会产生Full GC,G1也会产生Full GC

Young GC和Mixed GC都是STW模式。

  • Young GC:选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。
  • Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region。Mixed GC是既回收年轻代,又回收部分老年代

Mixed GC不是full GC,它只能回收部分老年代的Region,如果Mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。

global concurrent marking的执行过程类似CMS,但是不同的是,在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为四个步骤:

  • 初始标记(initial mark,STW)。它标记了从GC Root开始直接可达的对象。
  • 并发标记(Concurrent Marking)。这个阶段从GC Root开始对heap中的对象标记,标记线程与应用程序线程并行执行,并且收集各个Region的存活对象信息。
  • 最终标记(Remark,STW)。标记那些在并发标记阶段发生变化的对象,将被回收。
  • 清除垃圾(Cleanup)。清除空Region(没有存活对象的),加入到free list。

第一阶段initial mark是共用了Young GC的暂停,这是因为他们可以复用root scan操作,所以可以说global concurrent marking是伴随Young GC而发生的。第四阶段Cleanup只是回收了没有存活对象的Region,所以它并不需要STW。

Mixed GC发生的时机,由一些参数控制着的,另外也控制着哪些老年代Region会被选入CSet。 CSet是指Collection Set一组可以被回收的分区集合,在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可能来自Eden区、Survivor区或者老年代。CSet占用不会超过整个堆的1%大小

  • G1HeapWastePercent:在global concurrent marking结束之后,可以知道老年代regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。
  • G1MixedGCLiveThresholdPercent:老年代region中的存活对象的占比,只有在此参数之下,才会被选入CSet。
  • G1MixedGCCountTarget:一次global concurrent marking之后,最多执行Mixed GC的次数。
  • G1OldCSetRegionThresholdPercent:一次Mixed GC中能被选入CSet的最多老年代region数量。

完整的GC过程

新创建的对象在Eden区,当Eden区空间不足时,触发Minor GC(年轻代GC),会把Eden区和已使用的Survivor区中的存活对象复制到另一个Survivor区,然后清空Eden区和之前使用的Survivor区。

  • 当对象熬过15次Minor GC-XX:MaxTenuringThreshold参数指定该值,不同的垃圾收集器默认值不一样)后依然存活,会转移到老年代
  • 如果Survivor区放不下该对象,会直接放入老年代
  • 新生成的大对象-XX:+PretenureSizeThreshold指定阈值,超过该值就是大对象,该参数只对Serial和ParNew两款收集器有效)直接进入老年代
  • 如果在Survivor区相同年龄所有对象大小总和大于Survivor区的一半,则大于等于该年龄的对象进入老年代而无需等待-XX:MaxTenuringThreshold指定的参数。

Full GC是指老年代GC,但是老年代GC往往伴随着年轻代GC的发生,Full GC触发条件:

  • 老年代空间不足
  • 永久代空间不足(JDK7及以下)
  • CMS收集器出现promotion failed或者concurrent mode failure。前者是在Minor GC过程中,Survivor空间不足,对象进入老年代,而老年代空间也不足时会触发Full GC;后者是CMS收集器收集时,老年代预留空间不足(并发清除阶段,产生浮动垃圾),会触发Full GC。
  • Minor GC进入到老年代的平均大小大于老年代剩余空间
  • 程序调用System.gc(),提醒收集器进行Full GC,不过仅仅是提醒,具体的执行还是由JVM决定。

常用参数

通用部分

  • -XX:+PrintGC:打印GC日志
  • -XX:+PrintGCDetails:打印GC详细日志
  • -Xss每个线程的虚拟机栈大小,一般256K
  • -Xms:堆的初始值
  • -Xmx:堆的最大值,一般设置和Xms一致,因为heap扩容时,会发生内存抖动,影响程序稳定性
  • -XX:NewSize:年轻代初始空间大小。
  • -XX:MaxNewSize:年轻代最大空间大小。
  • -Xmn:年轻代大小
  • -XX:SurvivorRatio:年轻代中Eden和Survivor的比值,默认8:1(Eden占8/10,两个Survivor各占1/10)
  • -XX:NewRatio:老年代和年轻代内存大小比例,默认2:1
  • -XX:MaxTenuringThreshold:年轻代对象进入老年代,熬过Minor GC次数的阈值
  • -XX:+DisableExplictGC :禁用System.gc()
  • -XX:+PrintGCTimeStamps:JVM启动过后到GC所经历的时间
  • -XX:+PrintGCDateStamps:GC发生的时间,yyyy-MM-dd'T'HH:mm:ss.SSSZ +0800
  • -XX:+PrintFlagsFinal:打印JVM参数,例如java -XX:+PrintFlagsFinal -version |grep G1

Paralegal

  • -XX:+ParallelGCThreads :并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同
  • -XX:+UseAdaptiveSizePolicy :GC自适应调节策略,自动选择各区大小比例
  • GCTimeRatio:设置GC时间占用程序运行时间的百分比
  • -XX:MaxGCPauseMillis:停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代

CMS

  • -XX:+UseConcMarkSweepGC:老年代使用CMS收集器
  • -XX:ParallelCMSThreads:CMS线程数量
  • -XX:CMSInitiatingOccupancyFraction:使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小
  • -XX:+UseCMSCompactAtFullCollection:在Full GC时进行压缩,清理内存碎片
  • -XX:CMSFullGCsBeforeCompaction :多少次Full GC之后,进行压缩

G1

  • -XX:+UseG1GC:使用G1收集器
  • -XX:MaxGCPauseMillis:建议值,G1会尝试调整Young区的块数来达到这个值
  • -XX:GCPauseIntervalMillis:GC的间隔时间
  • -XX:+G1HeapRegionSize:Region分区大小。 随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长
  • -XX:G1NewSizePercent:新生代最小比例,默认为5%,不需要修改,由G1自动调整
  • -XX:G1MaxNewSizePercent:新生代最大比例,默认为60%,不需要修改,由G1自动调整
  • -XX:GCTimeRatio:GC时间建议比例,G1会根据这个值调整堆空间
  • -XX:ConcGCThreads:并发标记的线程数
  • -XX:InitiatingHeapOccupancyPercent:触发标记周期的堆占用率阈值。默认占用率是整个堆的45%

参考

  • 《深入理解Java虚拟机》
  • https://tech.meituan.com/2016/09/23/g1.html
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐