10、垃圾回收器

十、垃圾回收器

GC分类与性能指标

分类

  • 线程数分,可以分为串行垃圾回收器和并行垃圾回收器。

串行回收指的是同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。

  • 在诸如单CPU处理或者较小的引用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的Client模式下的JVM中。
  • 在并发能力比较强的CPU上,并行回收器产生的停顿时间要短语串行回收器。

和串行回收器相反,并行手机可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收与串行回收一样,采用独占式,使用了“Stop-the-world”机制。

  • 工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器

    • 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
    • 独占式垃圾回收器一旦运行,就停止应用程序中的所有用户线程,知道垃圾回收过程完全结束
  • 碎片处理方式分,可以分为压缩式垃圾回收器和非压缩式垃圾回收器

    • 压缩式垃圾回收器会在回收后完成后,对存活对象进行压缩整理,消除回收后的碎片。
    • 非压缩式的垃圾回收器不进行这部操作
  • 工作的内存区间分,又可以分为年轻代垃圾回收器和老年代垃圾回收器。

GC性能指标

  • 吞吐量:运行用户代码的时间占总运行时间的比例
    • 总运行时间:程序的运行时间+内存回收的时间
  • 垃圾收集开销:吞吐量的补数,垃圾收集所用的时间与总运行时间的比例。
  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间
  • 收集频率:相对于应用程序的执行,收集操作发生的频率
  • 内存占用:Java堆区所占的内存大小
  • 快速:一个对象从诞生到被回收所经历的时间。

红色三者共同构成一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。

这三项里,暂停的时间重要性日益凸显。随着硬件的发展,内存占用多些越来越能容忍,硬件性能的提升也有助于降低收集器运行时对应用程序的影响,即吞吐量提高了。而内存的扩大,对延迟反而带来负面效果,

简单来说:主要抓住两点:

  • 吞吐量

  • 暂停时间

吞吐量

吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

比如:虚拟机总共运行了100分钟,其中垃圾收集划掉1分钟,那么吞吐量就是99%

这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的。

吞吐量优先,意味着单位时间内,STW的时间最短:0.2+0.2=0.4

暂停时间
  • “暂停时间”是指一个时间段内应用程序线程暂停,让GC线程执行的状态
    • 例如,GC期间100毫秒的暂停时间意味着在这100ms期间内没有应用程序线程是活动的
  • 暂停时间优先,意味着竟可能让单次STW的时间最短:0.1+0.1+0.1+0.1+0.1=0.5
吞吐量VS暂停时间

高吞吐量较好因为这会让应用程序的最终与虎感觉只有应用程序线程在做“生产性”工作。自觉上,吞吐量越高程序运行越快。

地暂停时间(低延迟)较好因为从最终用户的角度来看不管是GC还是其他原因导致一个应用被挂起始终是不好的。这取决于应用程序的类型,有时候绳子短暂的200毫秒暂停都可能打断中断用户体验。因此,具有低的较大暂停时间是非常重要的,特别是对于一个交互式应用程序。

不信的是“高吞吐量”和“低暂停时间”是一对相互竞争的目标(矛盾)。

  • 如果选择以吞吐量优先,那么必然需要降低内存会收到的执行频率,但是这样会导致GC需要更长的暂停时间来执行内存回收。
  • 相反的,入股选择以低延迟优先为原则,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收,但这又引起了年轻代内存的缩减和导致程序吞吐量的下降。

在设计(或使用)GC算法时,必须确定一个目标:一个GC算法只能针对两个目标之一(即专注于较大吞吐量或最小暂停时间),或尝试找到一个二者的折中。

现在的标准:在最大吞吐量优先情况下,降低停顿时间M

常见的垃圾收集器

7款经典的垃圾收集器

  • 串行回收器:Serial、Serial Old
  • 并行回收器:ParNew、Parallel Scavenge、Parallel Old
  • 并发回收器:CMS、G1

除了上述的GC收集器,JDK11引入了Epsilon垃圾回收器,同时引入ZGC:可升缩的低延迟垃圾回收器(Experimental);JDK12引入了Shenandoah GC(红帽公司开发的);JDK13增强了ZGC,JDK14删除了CMS垃圾回收器,扩展ZGC在macOS和Windows上的应用。

https://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

七款垃圾收集器与垃圾分代之间的关系

  • 新生代收集器:Serial、ParNew、Parallel Scavenge;
  • 老年代收集器:Serial Old、Parallel Old、CMS;
  • 整堆收集器:G1;

垃圾收集器的组合关系


1、两个收集器间有连线,表明他们可以搭配使用:

Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;

2、其中Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备方案,

3、(红色虚线)由于维护和兼容性测试的成本,在JDK4 8时将Serial+CMS、ParNew+Serial Old这两个组合声明为废(JEP 713),并在JDK 9中完全取消了这些组合的支持(JEP214),即:移除

4、(绿色虚线)JDK 14中,启用Parallel Scavenge和SerialOld GC组合(JEP366)

5、(青色虚线)JDK 14中:删除了CMS垃圾回收器(JEP 363)

垃圾收集器 类型 作用域 使用算法 特点 适用场景
Serial 串行回收 新生代 复制算法 响应速度优先 适用于单核 CPU环境下的 Client模式
Serial Old 串行回收 老年代 标记-压缩算法 响应速度优先 适用于单核 CPU环境下的 Client模式
ParNew 并行回收 新生代 复制算法 响应速度优先 多核 CPU环境中 Server模式下与 CMS配合使用
Parallel Scavenge 并行回收 新生代 复制算法 吞吐量优先 适用于后台运算, 而交互少的场景
Parallel Old 并行回收 老年代 标记-压缩算法 吞吐量优先 适用于后台运算, 而交互少的场景
CMS(Concurrent Mark-Sweep) 并发回收 老年代 标记-清除算法 响应速度优先 适用于B/S业务, 也就是交互多的场景
G1(Garbage-First) 并发,并行回收(此收集器后期优化后并行方式同时存在) 新生代& 老年代(整堆收集器) 复制算法& 标记-压缩算法 响应速度优先 面向服务端的应用

我们选择的只是对具体应用最合适的收集器

查看默认的垃圾收集器

  • -XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)
  • 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID

Serial回收器——串行回收

Serial收集器是最基本、历史最悠久的垃圾收集器,JDK1.3之前回收新生代唯一的选择。Serial收集器作为HotSpot中Client模式下的默认新生代垃圾收集器。Serial收集器采用复制算法、串行回收和“Stop-the-world”机制的方式执行内存回收。除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。Serial Old收集器同样采用了串行回收和“Stop-the-world”机制,只不过内存回收算法使用的是标记-压缩算法

  • Serial Old是运行在Client模式下默认的老年代的垃圾回收器
  • Serial Old在Server模式下只要有两个用途:1、与新生代的Parallel Scavenge配合使用。2、作为老年代CMS收集器的后备垃圾收集方案。

这个收集器时一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它指挥使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它手机结束(Stop The World)。

Serial的优势:简单高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

  • 运行在Client模式下的虚拟机是个不错的选择。

在用户的桌面环境场景中,可用内存一般不大(几十MB至一两百MB),可以在较短时间内完成垃圾收集(几十ms至一百多ms),只要不频繁发生,使用串行回收器是可以接受的。

在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。

  • 等价于新生代用Serial GC,且老年代使用Serial Old GC

ParNew回收器——并行回收

如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本。

  • Par是Parallel的缩写,New:只能处理的是新生代

ParNew收集器除了采用并行回收的方式执行内存回收外, 两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、“Stop-the-world”机制。ParNew是很多JVM运行在Server模式下的默认垃圾收集器。

对于新生代,回收次数频繁,使用并行方式高效;对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)。

由于ParNew收集器时基于并行回收,那么是否可以断定ParNew收集器的回收效率在任何场景下都会比Serial收集器更高效?

  • ParNew收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量。
  • 但是在单个CPU的环境下,ParNew收集器不比Serial收集器更高。虽然Serial收集器时基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销

除了Serial外,目前只有ParNew GC能与CMS收集器配合工作。

在程序中,可以通过选项-XX:+UseParNewGC手动指定使用ParNew收集器执行内存回收任务。表示年轻代使用并行收集器,不影响老年代。

-XX:ParallelGCThreads限制线程数量,默认开启和CPU数量相同的线程数。

  • 对于新生代,回收次数频繁,使用并行方式高效
  • 对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)

Parallel回收器:吞吐量优先

HotSpot的年轻代中除了拥有ParNew收集器时基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法、并行回收和“Stop-the-world”机制。

那么Parallel收集器的出现是否多此一举呢?

  • 和ParNew收集器不同,Parallel Scavenge收集器的目标则是达到一个可控制吞吐量(Throughput),也被称为吞吐量优先的垃圾收集器。
  • 自适应调节策略也是Parallel Scavenge与ParNew一个重要区别

高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。

Parallel 收集器在JDK1.6时提供了用于执行老年代收集的Parallel Old收集器,用来代替老年代的Serial Old收集器。

Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和“Stop-the-world”机制。

  • 在程序吞吐量优先的应用场景中,Parallel收集器和Parallel Old收集器的组合,在Server模式下的内存回收性能狠不错
  • 在Java8中,默认是Parallel Scavenge垃圾收集器

Parallel的相关参数设置

-XX:+UseParallelGC:手动指定年轻代使用Parallel并行收集器执行内存回收任务

-XX:+UseParallelOldGC:手动指定老年代使用并行回收收集器

  • 分别适用于新生代和老年代。默认JDK8是开启的
  • 上面两个参数,默认开启一个,另一个也会被开启(互相激活)

-XX:ParallelGCThreads:设置年轻代并行收集器的线程数。一般情况下,最好与CPU数量相等,以避免过多的下层数量音响垃圾收集性能。

  • 在默认情况下,当CPU数量小于8个,ParallelGCThreads的值等于CPU数量。
  • 当CPU数量大于8个,ParallelGCThreads的值等于3+[(5*Cpu_Count)/8]。

-XX:MaxGCPauseMillis:设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒

  • 为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整Java堆大小或者其他一些参数
  • 对于用户来讲,停顿时间越短越好。但是在服务端,我们注重高并发,整体的吞吐量。所以服务端适合Parallel进行控制
  • 该参数使用需谨慎

-XX:GCTimeRatio:垃圾收集时间占总时间的比例(=1/N+1)。用于衡量吞吐量的大小

  • 取值范围(0,100)。默认值99,也就是垃圾回收时间不超过1%。
  • 与前一个-XX:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Ratio参数就容易超过设定的比例。

-XX:+UseAdaptiveSizePolicy:设置Parallel Scavenge收集器具有自适应调节策略

  • 这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡。
  • 在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机直接完成调优工作。

CMS回收器:低延迟

JDK1.5时期,HotSpot退出了一款在强交互应用中几乎可以认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器时HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。

CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。

  • 目前很大一部分Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度,系统系统停顿时间最短,已给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

CMS的垃圾收集器算法采用标记-清除算法,也会“Stop-the-world”。

不信的是,CMS作为老年代的收集器,无法与JDK 1.4h中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK1.5中使用CMS来手机老年代的时候,新生代只能选择ParaNew或者Serial收集器中的一个。在G1出现之前,CMS使用还是非常广泛的。知道今天,仍然有很多系统使用CMS GC。

CMS整个过程比之前的收集器复杂,分为4个主要阶段,即初始标记、并发标记、重新标记和并发清除阶段。

  • 初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-world”机制出现短暂的暂停,这个阶段主要任务仅仅只是标记处GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快

  • 并发标记(Concurrent-Mark)阶段:从GC Roots的直接从关联对象开始遍历整个对象图的过程。这个过程耗时较长但是不需要停顿用户线程,可以与用户线程一起并发运行。

  • 重新标记(Remark)阶段:由于在并发标记阶段,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,单页远比并发标记阶段的时间短。

  • 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发执行的。

CMS特点与弊端

CMS尽管采用的是并发回收(非独占式),但是在其初始化和再次标记这两个阶段中任然需要执行“Stop-the-world”机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要“Stop-the-world”,只是尽可能的缩短暂停时间。

由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。

另外,由于垃圾换收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了在进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

CMS采用的是标记-清除算法,不可避免地将会产生一些内存碎片。那么CMS在为新生代对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。

Mark Sweep会造成内存碎片,为什么不把算法完成Mark Compact呢?

因为在并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提它的运行的资源不受影响,Mark Compact更适合“Stop-the-world”这种场景下使用。

CMS优点
  • 并发收集
  • 低延迟
CMS弊端
  • 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。
  • CMS收集器堆CPU资源非常敏感。在并发阶段,虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
  • CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾阶段,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。

CMS相关参数

  • -XX:+UseConcMarkSweepGC 手动指定使用CMS收集器执行内存回收任务
    • 开启该参数后会自动将-XX:+UseParNewGC打开,即:ParNew(Young区用)+CMS(Old区用)+Serial Old的组合
  • -XX:CMSLnitiatingOccupanyFraction 设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收
    • JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收。JDK6及以上版本默认值为92%
    • 如果内存增长缓慢,就可以设置一个大的值,大的阈值可以邮箱降低CMS的触发频率,减少老年代回收逇次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数。
  • -XX:+UseCMSCompactATFullCollection 用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,锁带来的问题就是停顿时间变得更长了。
  • -XX:CMSFullGCsBeforeCompaction 设置在执行多少次Full GC后对内存空间进行压缩整理
  • -XX:ParallelCMSThreads 设置CMS的线程数量
    • CMS默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数。当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。
  • -XX:ConcGCThreads:并发的GC线程数
  • -XX:UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少内存碎片)
  • -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都压缩一次
  • -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:+CMSInitiatingoCCupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后序则会自动调整。
  • -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor GC,目的在于减少老年代对年轻代的引用,降低CMSGC的标记阶段时的开销,一般CMS的GC耗时80%都在标记阶段。
  • -XX:+CMSParallelInitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
  • -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW。
  • -XX:+CMSInitiatingoCCupancyFraction: 回收阈值

小结

HotSpot有这么多垃圾回收器,那么如果有人问,Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个GC有什么不同呢?

  • 如果想要最小化地使用内存和并行开销,用Serial GC;
  • 如果想要最大化应用程序的吞吐量,用Parallel GC;
  • 如果想要最小化GC的中断或停顿时间,用CMS GC。

G1回收器:区域分代式

前面已经有了几个强大的GC,为什么还要发布Garbage First(G1)GC?

原因就是应用程序应对的业务越来越庞大、复杂,用户越来越多,没有GC不能保证应用程序正常进行,而经常造成STW的GC有跟不上实际需求,所以才会不断地尝试优化GC。G1是在Java7 update 4之后引入的一个新的垃圾回收器,是当今收集器技术发展的前沿成果之一。

与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量,官方给G1设定的目标是延迟可控的情况下获得尽可能高的吞吐量,所以才当起“全功能收集器”的重任与期望。

G1主要是面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能的特征

概述

G1是一个并行的回收器,它把堆内存分割为很多不相关的区域(Region)(物理上是不连续的)。使用不同的Region来表示Eden区、幸存者0区,幸存者1区,老年代等。G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。由于这种方式的侧重点在于回收垃圾最大的区间(Region),所以G1又有一个名字:垃圾优先(Garbage First)。

G1是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以及高概率满足GC停顿时间的同时,还兼顾高吞吐量的性能特征。JDK1.7版本正式启用,移除了Experimental的标识,是JDK9以后默认垃圾收集器,取代了CMS回收器以及Parallel+Parallel Old组合。被Oracle官方称为“全功能垃圾收集器”。与此同时,Cms已经在JDK 9中被标记为废弃(deprecated)。在JDK8中还不是默认的垃圾回收器,需要使用-XX:+UseG1GC来启用。

G1回收器的优势与不足

G1的优势

与其他GC收集器相比,G1使用了全新的分区算法,特点如下:

  • 并行与并发

    • 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW。
    • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用的情况。
  • 分代收集

    • 从分带上来看,G依然属于分代型垃圾收集器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,他不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量
    • 将堆空间分为若干个区域(Region),这些区域汇总包含了逻辑上的年轻代和老年代。
    • 和之前的各类回收期不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代。
  • 空间整合

    • CMS:“标记-清除”算法、内存碎片、若干次GC后进行一次碎片整理
    • G1将内存划分为一个个的Region。内存回收是以Region作为基本单位的。Region之间是复制算法,但整体上实际可以看做是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
  • 可预测的停顿模型(即:软实时soft real-time)

    是G相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的实施时间不得超过N毫秒

    • 由于分区,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
    • G1跟踪各个Region里面的垃圾堆积的价值大小(回收获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的搜集效率。
    • 相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
G1的不足

相较于CMS,G1还不具备全方位、压倒性优势。比如用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是运行时的额外执行负载(Overload)都要比CMS要高。

从经验上来说,在小内存应用上CMS的表现概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。

G1的相关参数设置

  • -XX:+UseG1GC 手动指定使用G1收集器执行内存回收任务
  • -XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围是1MB-32MB之间,目标是根据最小的Java堆大小分出约2048个区域。默认堆内存的1/2000。
  • -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms。
  • -XX:ParallelGCThread 设置STW时GC线程数的值。最多设置为8。
  • -XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右
  • -XX:InitiatingHeapOccupancyPercent 老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发mixedGC了
  • -XX:G1NewSizePercent 设置新生代初始占比 默认整堆5%,值配置整数,默认就是百分比
  • -XX:G1MaxNewSizePerent 设置新生代最大占比,最多不会超过60%
  • -XX:TargetSurvivorRatio Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)综合草果了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
  • -XX:MaxTenuringThreshold 最大年龄阈值(默认15)
  • -XX:G1MixedGCLiveThresholdPercent 默认85%,region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的意义不大。
  • -XX:G1MixedGcCountTarget 在一次回收过程汇总指定几次筛选回收(默认8次),在最后一个筛选回收阶段回收一会儿,然后暂停,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。
  • -XX:G1HeapWastePercent gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region的垃圾对象全部清理掉,这样的话回收过程就会不断空出来行的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。

G1的常见操作步骤

G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:

1、开启G1垃圾收集器

2、设置堆的最大内存

3、设置最大的停顿时间

G1中提供三种垃圾回收模式:YoungGC、Mixed GC和Full GC,在不同的条件下触发。

G1的使用场景

1、面向服务端应用,针对具有大内存、多处理器的机器(在普通大小的堆里表现并不惊喜)

2、最主要的应用场景是需要低GC延迟,并具有大堆的应用程序提供解决方案。

​ 如:在堆大小约6GB或更大时,可预测的停顿时间可以低于0.5秒;(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次GC停顿时间不会过长)

3、用来替换掉JDK1.5中的CMS收集器。

​ 下面的情况,使用G1可能比CMS好;

​ 1️⃣ 超过50%的Java堆被活动数据占用

​ 2️⃣对象分配频率或年代提升频率变化很大;

​ 3️⃣GC停顿时间过长(长于0.5至1秒)

4、HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用程序承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速回收过程

5、50%以上的堆被存活对象占用

6、对象分配和晋升的速度变化非常大

7、垃圾回收时间特别长,超过1秒

8、停顿时间是500ms以内

分区Region:化整为零

使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region快,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB-32MB之间,且为2的N次幂。可以通过-XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会改变。

虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。通过Region的动态分配实现逻辑上的连续。

一个Region有可能属于Eden、Survivor或者Old/Tenured内存区域。但是一个Region只可能属于一个角色。E表示region属于Eden内存区域,S表示属于Survivor内存区域,O表示Old内存区域。空白表示未使用的内存空间。

G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H快。主要用于存储大对象,如果超过Region大小的50%,就放到H。

设置H的原因:

对于堆中的大对象,默认直接会分配到老年代,但是如果他是一个短期存在的大对象,就会堆垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。G1的大多数行为都把H区作为老年代的一部分来看待。

G1回收器垃圾回收过程

主要有下面三个环节:

  • 年轻代GC(Young GC)
  • 老年代并发标记过程(Concurrent Marking)
  • 混合回收(Mixed GC)
  • (如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。)

    应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年代区间,也有可能是两个区间都会涉及到。

当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。

标记完成后,马上开始混合回收。对于一个混合回收期,G1 GC从老年代区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收期不需要整个老年代被回收,一次只需要扫描/回收一部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。

举个例子:一个Web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31个小时整个堆的使用率会达到45%,会开始老年代并发标记过程,标记完成后开始4到5次的混合回收。

大致分为以下步骤:

  • 初始标记(initial mark,STW):暂停所有的其他线程,并记录下GC Roots直接能引用的对象,速度很快
  • 并发标记(Concurrent Marking):同CMS的并发标记
  • 最终标记(Remark,STW):同CMS的重新标记
  • 筛选回收(cleanup STW): 筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿STW时间(可用JVM参数-XX:MaxGCPauseMillis指定)来制定回收计划,比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只会回收一部分Region,时间是用户控制的,而且停顿用户线程将大幅提高收集效率。不干事老年代或是年轻代,回收算法主要用的是复制算法,将一个region的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。(注意:CMS回收阶段是跟用户线程一起并发执行的,G1因为内部实现太复杂,展示没实现并发回收,不过到了ZGC,Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本)

    G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First的由来),比如一个 Region花200ms能回收10M垃圾,另外一个Region话50ms能回收20m垃圾,在回收的时间有限的情况下,G1会优先选择后面这个Region回收。这种使用Region划分内存空间以及优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能的提高收集效率。
Remembered Set

一个对象可能被不同区域引用的问题。一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?在其他的分代收集器,也存在这样的问题(G1更突出)。回收新生代也不等不同时扫描老年代?这样的话降低Minor GC的效率;

解决办法:

无论G1还是其他分代收集器,JVM都使用Remembered Set来避免全局扫描:

每个Region都有一个对应的Remombered Set,每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作,然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象)。如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Reion对应的Remembered Set中。当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set,就可以保证不进行全局扫描,也不会有遗漏。

G1垃圾回收细节

一、年轻代GC

JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程。年轻代垃圾回收指挥回收Eden区和Survivor区。YGC时,首先G1停止引用程序的执行(Stop-The-World),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。

然后开始如下回收过程:

  • 第一阶段:扫描根

    根指的是static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同Rset记录的外部引用作为扫描存活对象的入口

  • 第二阶段:更新RSet

    dirty card queue中的card,更新RSet。此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用

  • 第三阶段:处理RSet

    识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活对象。

  • 第四阶段:复制对象

    二段,对象树被遍历,Eden区内存段总存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阈值会被复制到Old区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。

  • 第五阶段:处理引用

    处理Soft、Weak、Phantom、Final、JNI Weak等引用。最终Eden空间数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。

Dirty Card Queue:对于应用程序的引用赋值语句Object.field=object,JVM会在之前和之后执行特殊的操作以在dirty card queue中入队一个保存了对象引用信息的card。在年轻代回收的时候,G1会在Dirty Card Queue中所有的card进行处理,以更新RSet,保证RSet实时准确的反映引用关系。

那为什么不在引用赋值语句处直接更新RSet呢? 这是为了性能的需要,RSet的处理需要线程同步,开销会很大,使用队列性能会好很多。

二、并发标记过程

  • 初始标记:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC
  • 根区域扫描(Root Region Scanning):G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这个过程必须在Young GC之前完成。
  • 并发标记(Concurrent Marking):在整个堆中进行并发标记(和应用程序并发执行),此过程可能被Young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
  • 再次标记(Remark):由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1采用了比CMS更快的初始快照算法:snapshot-at-the-beginning(SATB)。
  • 独占清理(cleanup,STW):计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下一阶段做铺垫。是STW的
    • 这个阶段并不会实际上做垃圾的收集
  • 并发清理阶段:识别并清理完全空闲的区域。

三、混合回收

当越来越多的对象晋升到老年代Old Region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region。这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些Old Region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是Mixed GC并不是Full GC。

  • 并发标记结束后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。默认情况下,这些老年代的内存分段会分8次(可以通过-XX:G1MixedGCCountTarget设置)被回收
  • 混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程参考年轻代回收过程。
  • 由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,约会被先回收。并且有一个阈值会决定内存分段是否被回收,-XX:G1MixedGCLiveHtresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。
  • 混合回收并不一定要进行8次。有一个阈值:-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。

四、Full GC

G1的初衷就是要避免Full GC的出现。如果上述方式不能正常工作,G1会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。

要避免Full GC的发生,一旦发生需要进行调整。什么时候会发生Full GC呢?比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到Full GC,这种情况可以通过增大内存解决。

导致G1Full GC的原因可能有两个:

  • Evacuation的时候没有足够的to-space来存放晋升的对象;
  • 并发处理过程完成之前空间耗尽。

G1回收器的优化建议

  • 年轻代大小
    • 避免使用-Xmn或-XX:NewRatio 等相关选项显式设置年轻代大小
    • 固定年轻代的大小会覆盖暂停时间目标
  • 暂停时间目标不要太过严苛
    • G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间
    • 评估G1 GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示愿意承担更多的垃圾回收开销,而这些会直接影响到吞吐量。

假设参数- XX. MaxGCPause Mills设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的60%了,此时才触发年轻代gc。那么存活下来的对象可能就会很多,此时就会导致Suⅳvo区域放不下那么多的对象,就会进入老年代中。或者是你年轻代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中。所以这里核心还是在于调 -XX:MaxGCPauseMills个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc后的存活对象有多少避免存活对象太多快速进入老年代,频繁触发mixed gc

ZGC收集器

官方文档]

ZGC是一款JDK 11中新加入的具有实验性质的低延迟垃圾收集器,ZGC可以说源自于Azul System公司开发的C4(Concurrent Continuously Compacting Collector)收集器。

ZGC目标

如下图,主要由4个:

  • 支持TB量级的堆。生产环境上硬盘也就TB级别,这应该可以满足未来十年内,所有Java应用的需求了
  • 最大GC停顿时间不超过10ms。目前一般线上环境运行良好的JAVA应用 Minor gc停顿时间在10ms左右, Major GC一般都需要100ms以上(G1可以调节停顿时间,但是如果调的过低的话,反而会适得其反),之所以能做到这一点是因为它的停顿时间主要跟Rωot扫描有关,而Roo数量和堆大小是没有任何关系的。
  • 奠定未来GC特性的基础
  • 最糟糕的情况下吞吐量会降低15%,这都不是事,停顿时间足够优秀。至于吞吐量,通过扩容可以解决

另外,Oracle官方提到了它最大的优点:它的停顿时间不会随着堆的增大而增长,也就是说,几十G的堆停顿时间是10ms一下,几百G甚至TB级别的堆的停顿时间也是10ms一下。

不分代(暂时)

单代,即ZGC(没有分代),我们知道以前的垃圾回收器之所以分代,是因为原语【大部分对象朝生夕死】的假设,事实上大部分系统的对象分配行为也确实符合这个假设。

那为什么ZGC就不分代呢?因为分代实现起来很麻烦,作者就先实现出一个比较简单可用的单代版本,后续优化。

ZGC内存布局

ZGC收集器是一款基于 Region内存布局的,暂时不设分代的,使用了读屏障、颜色指针等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。

ZGC的 Region可以具有如下图所示的大、中、小三类容量:

  • 小型 Region( Small Region):容量固定为2MB,用于放置小于256KB的小对象。

  • 中型 Region( Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。

  • 大型 Region( Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型 Region中只会存放一个大对象,这也预示着星然名字叫作“大型 Region”,但它的实际容量完全有可能小于中型 Region,最小容量可低至4MB。大型Region在zGC的实现中是不会被重分配(重分配是zGC的一种处理动作,用于复制对象的收集器阶段,稍后会介绍到)的,因为复制一个大对象
    的代价非常高昂。

NUMA-aware

NUMA对应的有UMA,UMA即 Uniform Memory Access Architecture,NUMA就是 Non Uniform Memory Access Architecture。UMA表示内存只有一块,所有CPU都去访问这一块内存,那么就会存在竞争问题(争夺内存总线访问权),有竞争就会有锁,有锁效率就会受到影响,而且CPU核心数越多,竟争就越激烈。NUMA的话毎个CPU对应有一块内存,且这块内存在主板上离这个CPU是最近的,每个CPU优先访问这块内存,那效率自然就提高了

服务器的NUMA架构在中大型系统上一直非常盛行,也是高性能的解决方案,尤其在系统延迟方面表现都很优秀。ZGC是能自动感知NMA架构并充分利用NUMA架构特性的。

ZGC运作过程

大致分为4个阶段:

  • 并发标记(Concurrent Mark): 与G1一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记 Mark Start和最终标i记( Mark End)也会出现短暂的停顿,与G1不同的是,zGC的标记是在指针上而不是在对象上进行的,标记阶段会更新颜色指针(见下面详解)中的 Marked0、Marked1标志位。

  • 并发预备重分配( Concurrent Prepare for Relocate):这个阶段需要根据特定的査询条件统计得岀本次收集过程要清理哪些 Regioη,将这些 Region组成重分配集( Relocation Set)。ZGC每次回收都会扫描所有的 Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。

  • 并发重分配( Concurrent relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个 Region维护—个转发表( Forward Table),记录从旧对象到新对象的转向关系。zGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障(读屏障(见下面详解)所截获,然后立即根据 Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,zGC将这种行为称为指针的“自愈”(Sef- Healing)能力。

    ZGC的颜色指针因为“自愈”(Self- Healing)能力,所以只有第一次访问旧对象会变慢,一旦重分配集中某个 Region的存活对象都复制完毕后,
    这个 Region就可以立即释放用于新对象的分配,但是转发表还得留着不能释放掉,因为可能还有访问在使用这个转发表。
    
  • 并发重映射( Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈" 功能,所以这个重映射操作并不是很迫切。zGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉了。

颜色指针

Colored Pointers,即颜色指针,如下图所示,zGC的核心设计之一。以前的垃圾回收器的GC信息都保存在对象头中,而zGC的GC信息保存在指针。

每个对象有一个64位指针,这64位被分为:

  • 18位:预留给以后使用;
  • 1位: Finalizable标识,此位与并发引用处理有关,它表示这个对象只能通过 finalize才能访问
  • 1位: Remapped标识,设置此位的值后,对象未指向 relocation se坤中( relocation set表示需要GC的 Region集合);
  • 1位: Marked1标识;
  • 1位: Marked0标识,和上面的 Marked1都是标记对象用于辅助GC
  • 42位:对象的地址(所以它可以支持2^42=4内存

为什么有2个mark标记?

每一个GC周期开始时,会交换使用的标记位,使上次GC周期中修正的已标记状态失效,所有引用都变成未标记。

GC周期1:使用mark0,则周期结束所有引用mak标记都会成为01。

GC周期2:使用mark1,则期待的mak标记10,所有引用都能被重新标记。

通过对配置zGC后对象指针分析我们可知,对象指针必须是64位,那么ZGC就无法支持32位操作系统,同样的也就无法支持压缩指针了(CompressedOops,压缩指针也是32位)。

颜色指针的三大优势:

1、一旦某个 Region的存活对象被移走之后,这个 Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该 Regioη的引用都被修正后才能清理,这使得理论上只要还有一个空闲 Region,ZGC就能完成收集

2、颜色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,zGC只使用了读屏障。

3、颜色指针具备强大的扩展性,它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提髙性能。

读屏障

之前的GC都是采用 Write barrier,这次ZGC采用了完全不同的方案读屏障,这个是ZGC—个非常重要的特性。

在标记和移动对象的阶段,每次「从堆里对象的引用类型中读取一个指针」的时候,都需要加上一个 Load Barriers

那么我们该如何理解它呢?看下面的代码,第一行代码我们尝试读取堆中的—个对象引用 obj. fieldA并赋给引用o( fieldA也是一个对象时才会加上读屏障)。如果这时候对象在GC时被移动了,接下来JM就会加上一个读屏障,这个屏障会把读出的指针更新到对象的新地址上,并且把堆里的这个指针修正"到原本的字段里。这样就算GC把对象移动了,读屏障也会发现并修正指针,于是应用代码就永远都会持有更新后的有效指针,而且不需要STW

那么,JVM是如何判断对象被移动过呢?就是利用上面提到的颜色指针,如果指针是 Bad Colσ,那么程序还不能往下执行,需要「 slow path」,修正指针;如果指针是 Good color,那么正常往下执行即可

“这个动作是不是非常像JDK并发中用到的CAS自旋?读取的值发现已经失效了,需要重新读取。而ZGC这里是之前持有的指针由于GC后失效了,需要通过读屏障修正指针。”

后面3行代码都不需要加读屏障: Object p=σ这行代码并没有从堆中读取数据;o. dosomething()也没有从堆中读取数据;obj. fieldB不是对象引用,而是原子类型。

正是因为 Load Barriers的存在,所以会导致配置zGC的应用的吞吐量会变低。官方的测试数据是需要多出额外4%的开销

那么,判断对象是 Bad color还是 Good Color的依据是什么呢?就是根据上一段提到的 Colored Pointers的4个颜色位。当加上读屏障时,根据对象指针中这4位的信息,就能知道当前对象是 Bad/Good Color了。

PS:既然低42位指针可以支持4T内存,那么能否通过预约更多位给对象地址来达到支持更大内存的目的呢?答案肯定是不可以。因为目前主板地址总线最宽只有48bt,4位是颜色位,就只剩44位了,所以受限于目前的硬件,zGC最大只能支持16T的内存,JDK13就把最大支持堆内存从4T扩大到了16TB

ZGC存在的问题

ZGC最大的问题是浮动垃圾。ZGC的停顿时间是在10ms以下,但是ZGC的执行时间还是远远大于这个时间的。假如ZGC全过程需要执行10分钟,在这期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,所以只能在下次GC的时候进行回收,这些只能等到下次GC才能回收的对象就是浮动垃圾。

ZGC没有分代概念,每次都需要进行全堆扫描,导致一些“生夕死”的对象没能及时的被回收

解决方案

目前唯一的办法是增大堆的容量,使得程序得到更多的喘息时间,但是这个也是一个治标不治本的方案。如果需要从根本上解决这个问题,还是需要引入分代收集,让新生对象都在一个专门的区域中创建,然后专门针对这个区域进行更频繁、更快的收集。

ZGC参数设置

启用ZGC比较简单,设置JVM参数即可:-XX:+UnlockExperimentalVMOptions 「-XX:+UseZGC」。调优也并不难,因为ZGC调优参数并不多,远不像CMS那么复杂。它和G1一样,可以调优的参数都比较少,大部分工作JVM能很好的自动完成。下图所示是ZGC可以调优的参数:

ZGC触发时机

有4中触发机制:

  • 定时触发,默认为不使用,可通过 ZCollectionInterval参数配置
  • 预热触发,最多三次,在堆內存达到10%、20%、30%时触发,主要时统计GC时间,为其他GC机制使用。
  • ·分配速率,基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽之前触发GC(耗尽时间 - 一次GC最大持续时间 - 一次GC检测周期时间)
  • 主动触发,(默认开启,可通过 ZProactive参数配置)距上次GC堆内存增长10%,或超过5分钟时,对比距上次GC的间隔时间跟(49次GC的最大持续时间),超过则触发

7种经典垃圾回收器总结

截止JDK1.8,一共有7款不同的垃圾收集器。每一款的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器。

怎么选择垃圾收集器?

1、优先调整堆的大小让JVM自适应完成。

2、如果内存小于100M,使用串行收集器

3、如果是单核、单机程序、并且没有停顿时间要求,串行收集器。

4、如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择。

5、如果是多CPU、追求低停顿,需要快速响应(比如延迟不能超过1秒,如互联网应用),使用功能并发收集器

​ 官方推荐G1,性能高,线程互联网的项目,基本都是使用G1。

GC日志分析

通过阅读GC日志,可以了解Java虚拟机内存分配与回收策略。

内存分配与垃圾回收的参数列表

-XX:+PrintGC 输出GC日志。类似:-verbose:gc
-XX:+PrintGCDetails 输出GC 的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如2016-05-04T21:32:23.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc: ../logs/gc.log 日志文件的输出路径

打开日志(JDK1.8):

-XX:+PrintGC

这个参数只会显示总的GC堆的变化,如下:

[GC (Allocation Failure)  46534K->44046K(93184K), 0.0113419 secs]
[Full GC (Ergonomics)  44046K->43850K(131072K), 0.0106248 secs]

参数解析:

GC、Full GC:GC的类型,GC只在新生代上进行,Full GC包括永久代、新生代、老年代
Allocation Failure:GC发生的原因
46534K->44046K:堆在GC前的大小和GC后的大小
93184K:现在的堆大小
0.0113419 secs:GC持续时间。

-XX:+PrintGCDetails

[GC (Allocation Failure) [PSYoungGen: 15353K->2508K(17920K)] 15353K->14118K(58880K), 0.0043880 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 17808K->2536K(17920K)] 29418K->29176K(58880K), 0.0075165 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2536K->0K(17920K)] [ParOldGen: 26640K->29141K(40960K)] 29176K->29141K(58880K), [Metaspace: 3232K->3232K(1056768K)], 0.0094275 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 15315K->3500K(17920K)] [ParOldGen: 29141K->40544K(40960K)] 44457K->44044K(58880K), [Metaspace: 3232K->3232K(1056768K)], 0.0077402 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 17920K, used 10353K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
  eden space 15360K, 67% used [0x00000000fec00000,0x00000000ff61c4f8,0x00000000ffb00000)
  from space 2560K, 0% used [0x00000000ffd80000,0x00000000ffd80000,0x0000000100000000)
  to   space 2560K, 0% used [0x00000000ffb00000,0x00000000ffb00000,0x00000000ffd80000)
 ParOldGen       total 40960K, used 40544K [0x00000000fc400000, 0x00000000fec00000, 0x00000000fec00000)
  object space 40960K, 98% used [0x00000000fc400000,0x00000000feb980b0,0x00000000fec00000)
 Metaspace       used 3239K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

参数解析:

PSYoungGen:使用了Parallel Scavenge并行垃圾收集器的新生代GC前后大小的变化
ParOldGen:使用你了parallel Old并行垃圾收集器的老年代GC前后大小的变化
Metaspace:元数据区GC前后大小的变化,JDK1.8引入了元数据区以替代永久代
xxx secs:指GC花费的时间
Times: user: 指的是垃圾收集器花费的所有CPU时间,sys: 花费在等待系统调用或系统事件的时间。real: GC从开始到结束的时间,包括其他进程占用时间片的实际时间。

日志补充说明

  • "[GC"和"[Full GC"说明了这次垃圾收集的停顿类型,如果有“Full”则说明GC发生了“Stop-the-world”

  • 使用Serial收集器在新生代的名字是Default New Generation,因此显示的是“[DefNew”

  • 使用ParNew收集器在新生代的名字会变成“[ParNew”,意思是“Parallel New Generation”

  • 使用Parallel Scavenge 收集器在新生代的名字是"[PsYoungGen]"

  • 老年代的收集和新生代倒立一样,名字也是收集器决定的

  • 使用G1收集器的话,会显示“garbage-first heap”

  • Allocation Failure:表明本次引起GC的原因是因为年轻代中没有足够的空间能够存储新的数据了

  • user代表用户状态回收耗时,sys内核态回收耗时,real时间耗时。由于多核的原因,时间总和可能会超过real时间

堆空间数据解读

JVM参数:

-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRaion=8 -XX:+UseSerialGC

public class GCLogTest2 {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        testAllocation();
    }

    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];
    }
}
[GC (Allocation Failure) [DefNew: 8138K->642K(9216K), 0.0048980 secs] 8138K->6786K(19456K), 0.0049552 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
  def new generation   total 9216K, used 4820K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  from space 1024K,  62% used [0x00000000ff500000, 0x00000000ff5a0968, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
 Metaspace       used 3236K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

日志分析工具

如果想要把GC日志存到文件的话,可以使用下面的参数

-Xloggc:./logs/gc.log

常见的日志分析工具:GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat等。

posted @ 2021-10-26 20:36  程序员清风  阅读(474)  评论(0编辑  收藏  举报