文章

JVM GC二三事儿

JVM GC二三事儿

前言

深入JVM GC机制前,先简单了解下JVM运行时的内存结构。下图是JDK 8运行时的内存结构,主要分为3大块,

Desktop View JVM 运行时内存结构

Tips

  • 第一大块,包括栈内存、程序计数器、本地方法栈,是线程独享的。
    • 本地方法栈是JNI的,C语言的本地native方法。
  • 右边是堆内存和本地内存,都是线程所共享的。
    • 创建对象所分配的内存空间,都在堆内存里;
    • 使用DirectByteBuffer创建直接内存在本地内存里。

常见的分代GC方式

  • MinorGC,也叫YGC(即年轻代GC);
  • MixedGC,回收Young新生代、部分Old老年代,在堆占用超过InitiatingHeapOccupancyPercent规定的阈值时,启动并发标记,并发标记完成后,YGC变成MixedGC,回收部分老年代;
  • FullGC是回收整个堆,包括Young新生代、Old老年代,PermGen永久代(在JDK 1.8及以后,永久代换成了metaspace元空间)等,它是回收所有部分的模式。

回到GC机制

GC机制是围绕堆内存开展的,堆外内存的回收还是因为在堆内存里边的那一点点冰山的山顶被回收之后,再来回收堆外内存里边所关联的那一部分。

堆外内存有两部分,

  • 一部分是在堆里边的Java对象(DirectByteBuffer对象),这个对象很小;
  • 更多的字节是在本地内存里,用C语言的malloc分配的。

DirectByteBuffer有两部分,底下直接内存部分的回收和堆内的DirectByteBuffer对象是相关联的,所以大部分Java开发者一般不用关心堆外内存的回收,只需要关心堆内冰山的山顶那部分,在水面上的那部分。

堆内存的垃圾回收机制

堆内存分为两大块,有不同的垃圾回收器。简单地理解,一大块叫年轻代,一大块叫老年代。

Desktop View 堆内存结构(传统默认的,非G1)

新生代和老年代的比例,默认是1:2,新生代占1/3的堆空间,-XX:NewRatio参数可以自定义指定这个值。

MinorGC,指的是新生代的垃圾回收,清除Edenfrom,转到to中。之后fromto转换,继续清除Eden和新的from,转到to。清除一次后存活超过年龄的,转到老年代;to到了阈值后,部分对象转到老年代。

  • 晋升老年代参数:-XX:MaxTenuringThreshold,为什么最大是15?是因为HotSpot会在对象头里的标记字段记录年龄,只分配了4位,所以最多只能记录到15
  • 如果单个Survivor区已经被占用了50%,那么较高复制次数的对象也会被晋升至老年代。对应JVM参数-XX:TargetSurvivorRatio。 在年轻代中经历了N次垃圾回收后,仍然存活的对象,就会被放到老年代中,可以认为老年代中存放的都是一些生命周期较长的对象。

看到网上有个很形象的比喻,把Java的垃圾回收机制,类比厨师做黄焖鸡,先翻炒,然后小火焖。
怎么理解这句话呢?

  • 年轻代是炒锅,老年代是焖锅;
  • 年轻代又分为Eden(鸡下蛋的地儿)、还有两个炒锅(fromto)。翻炒就是MinorGC。在年轻代里的edenfromto区域不断翻炒,翻个7-8遍,最多15遍,可能这个时间很短,2分钟就翻完了;
  • 翻完了之后,把它放在大锅(老年代)里边放水焖,在老年代焖的时间就比较长,焖个半个小时,或者一个小时,甚至半天,美味的黄焖鸡就算做好了。

聚焦细节

  • MinorGC的过程:炒锅翻炒8
  • 升级老年代:小火焖锅20分钟

MinorGC的过程是怎么做的?

在炒锅里边翻炒的过程,每翻炒一次,就是一次MinorGC

把炒锅分为三部分,分别是Edenfromto,默认比例是8:1:1Eden可以理解为鸡窝(鸡下蛋的地儿),在Java里边用new创建对象都是在Eden里边分配内存,如果Eden满了(鸡窝满了,没空间了),这个时候就把炒锅翻炒一下(触发一次MinorGC)。

初始阶段,新创建的对象被分配到Eden区,survivor的两块空间都为空。

如果Eden空间占满了(MinorGC的一个简单的前提),会触发MinorGCMinorScavengeGC后,仍然存活的对象会被复制到S0survivor 0)中去。 这样Eden就被清空,可以继续分配给新的对象。

Desktop View 从Eden区出生了

看图说话,经过扫描与标记,存活的对象被复制到空的S0区域,不存活的对象会被回收。

MinorGC很简单,首先把炒锅里边的对象做一个标记,对Eden里边所有对象做标记,标记的方式就是看对象有没有被引用到,引用到的(强引用)标记为蓝色,没引用(弱引用、虚引用)到的标记为黄色。标记出来,怎么做垃圾回收?通过复制,把被引用到的对象(幸存者)复制到S这个小区域(S0区域,S0S1无所谓,反正两块大小一样)。复制完之后,Eden区可以被直接清空,清空完的Eden区就可以放入新的对象了。这是第一次做MinorGC的流程,垃圾回收的第一轮翻炒就是这样的。

第一次MinorGC,此时S0S1区对换,所有存活的对象被移动到S0区,S1区和Eden区对象被清空。

Desktop View 第1次MinorGC,从Eden区长大,还活着的就转到S0,等待下一锅翻炒

第二次翻炒,就有点不一样了。 这一次MinorGC的流程,大致如下,S0Eden中存活的对象被复制到S1中,并且S0Eden被清空。

Eden区不大,过一会又满了,接下来就是下一次翻炒,这次翻炒的情况就变了,Eden有对象,S0也有对象了,这次把EdenS0中的对象都标记,把EdenS0区的存活对象都复制到S1区,复制完之后,把EdenS0中的对象全部清空,这时Eden空出来了,可以放入新的对象了。这是第二轮翻炒

Desktop View 第2次MinorGC,从Eden区长大的和S0长大的,活着的都就转到S1,等待下一锅翻炒

在这一次的MinorGC中,Eden区和之前一样,存活的对象被复制到Survivor区,未存活的对象被回收。 不过这一次不同的是,Eden区中存活的对象被复制到了S1区,而S0中未存活的对象被回收,存活的对象被移动到了S1区,S0被清空,这里之前在S0中存活的对象在移动到S1中后,年龄要加1,所以此时S1中存活了不同年龄的对象。

第二轮翻炒完后,过段时间Eden又满了,满了之后又得翻炒,这次把EdenS1区的对象都标记了,标记完后再做复制,EdenS1区存活的对象都复制到S0区,复制完后,把EdenS1区全部清空。这是第三轮翻炒

Desktop View 第3次MinorGC,从Eden区长大的和S1长大的,活着的都就转到S0,等待下一锅翻炒

需要注意的一点:幸存的对象每做一次复制,年龄要加1,为什么叫EdenEden是鸡下蛋的地儿,0岁。

此处省略后面几次翻炒过程,可参照前3次翻炒的方式脑补下…

升级Promotion

经过几次MinorGC过程后,当年轻代中存活的对象年龄达到一个值时,就会被从年轻代升级Promotion到老年代,从炒锅升级到焖锅。这个值就是由前面说的MaxTenuringThreshold参数设置的,不设置默认是8

Desktop View 年轻代准备升级

随着一次次的MinorGC,会有对象在达到年龄后被送到老年代,如下图,

Desktop View 升级到老年代

在年轻代里边进行分配,反复翻炒,翻炒到7-8遍后,升级到老年代(焖锅),截止到当前,鸡到了焖锅里边。

总结

  • 在同一时刻,只有Eden和一个Survivor区同时被操作;
  • 当每次对象从Eden复制到Survivor区,或者从Survivor区中的一个复制到另一个,
    • 有一个计数器会自动增加值;
    • 默认情况下,如果复制发生超过8次,JVM会停止复制,并把他们移到老年代中去。

老年代的清理

焖锅里边怎么清理?方法和炒锅不一样。

炒锅里边是不断的标记-复制,因为炒锅比较特殊,它有3块,其中一块一直空在那里,焖锅是完整的一只锅,焖锅里边不是用的标记-复制,而是用的标记-整理(压缩)的方法。

标记-复制算法,在上面阐述MinorGC细节时已经见到了。年轻代的Eden区和Survivor区,Eden区的分配是连续的,而且总有一个Survivor区是空的,经过一次GC和复制,Eden区和一个Survivor区中的存活对象被复制到另一个Survivor区,之后他们里边的对象都被清空,在下一次GC时,两个Survivor区再交换角色,重复上述过程。

而在老年代,存活对象时间较长,标记-复制算法效率不高,一般用的标记-整理算法,存活对象标记,清除未存活对象,并将对象往一端移动,内存是连续的。详见标记-整理算法

JVM三大GC算法

不管是在年轻代,还是在老年代,都分为两个阶段。第一阶段都是标记,第二阶段因为区域、垃圾回收器不同,机制也不一样。在老年代里,复制不好使,复制需要一块空地,老年代没有留这么一块空地,因为要存活的对象比较多,所以用的是整理算法。整理算法上面还有其他提效的方法,比如直接清除。

垃圾回收器的工作流程,大体如下,

  1. 标记出哪些对象是存活的,哪些是垃圾(可回收);
  2. 进行回收(清除/复制/整理),如果有移动过对象(复制/整理),还需要更新引用。

GC常用的垃圾回收算法有三种,

  • 标记-清除
  • 标记-复制
  • 标记-整理

标记-复制算法

Mark-Copy 
回收前回收后
Desktop ViewDesktop View

假设绿色标记存活的,黄色标记回收的,做完标记,在回收阶段做复制工作,把绿色存活的对象都复制到空地里边去,复制完后把原来的内存统一做清理。这有两个比较耗时的地方,它要进行对象的复制工作,内存的复制都是比较费时间的,还有一个特点就是,要有一块差不多的空地在那里等着,如果全部是绿色,就得全都复制一遍,如果没有这块所谓的空地,就得需要额外空间进行担保,比较耗内存空间。既浪费了时间,也耗费了空间。

如果绿色的比较少,占用空间小,这种标记-复制算法还是可以的,比如在年轻代里边。

复制回收算法,在对象存活率较高时,就要进行较多的复制操作,效率将会变低。

更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

标记-清除算法

Mark-Sweep是一种非常常见的垃圾回收算法,简单地说,先找出所有不可达的对象,并将它们放入空闲列表Free,该算法被J McCarthy等人在1960年提出,并应用于Lisp语言。

Mark-Sweep算法分为两个阶段——标记和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。

Mark-Sweep
Desktop View

标记阶段和之前的没什么区别,区别在第二阶段,这里直接把可回收对象清理掉,跟标记-复制算法比,既不耗时间(不需要做整体的对象移动),也不耗空间。

从图中可以发现,该算法最大的问题是,内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。

这些算法都有在垃圾回收器在用,待会可以看下,有哪些垃圾回收器在用标记-清除算法,它们是怎么解决内存碎片化问题的。

标记-整理算法

Mark-Compact
Desktop View

在回收时,首先做下标记,黄色部分是存活的对象,灰色是可以回收的对象,标记完了之后,就开始做压缩,怎么压缩呢?简单来说,就是从前面挨个儿往前移,把所有存活的对象做整理,后边的地方就空出来了。这时,就能存放从年轻代升级过来要焖的黄焖鸡了。

标记过程和标记-清除算法的一样,但是后续步骤不是直接对可回收对象进行清理。而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

和清理算法比,有个移动对象的时间,有大量的内存复制,这个很耗时间。在没有优化的前提下,这种内存移动必须暂停应用程序,因为对象的引用地址变了,在这个过程中还在修改,就麻烦了。这种算法不耗空间,只耗时间。

在老年代每次回首时,都有大量存活对象,移动存活对象并更新所有引用对象时,必须全程暂停用户引用程序,这种停顿就是Stop The World(对JVM的应用层进行停顿)。

三色标记法

Go语言的垃圾回收机制,使用的就是这种算法。

三色标记法,实际上是一个迭代和遍历的过程,迭代的起点是在GC的根部开始的,有一个集合GC Root Set记录了根部的对象,比如类里的静态变量就是根部的对象,从根部对象开始,不断地遍历堆里边的对象。

要找出存活对象,根据可达性分析,从GC Roots开始进行遍历访问,可达的则为存活对象。

我们把遍历对象图过程中遇到的对象,按是否访问过这个条件标记成以下三种颜色,

  • 白色:尚未遍历过;
  • 黑色:本对象已遍历过,而且本对象引用到的其他对象也全部遍历过了;
  • 灰色:本对象已遍历过,但是本对象引用到的其他对象尚未全部遍历完。全部遍历后,会转换为黑色。

在刚开始的时候,所有的对象都没有遍历过,所有的对象通通都是白色。
灰色代表的是一种中间状态,处理问题的时候不是非黑即白,灰色代表的就是人情世故里的灰度。

三色标记迭代过程

有一个很重要的前提,遍历是从根部开始的,根部当然就是要留下来的,根部所有的对象都在白色集合中,

假设现在有白、灰、黑三个集合(表示当前对象的颜色),其遍历过程为,

初始时,所有对象都在白色集合中;

Desktop View

GC Roots直接引用到的对象挪到灰色集合中; 从灰色集合中获取对象,

  • 将本对象引用到的其他对象全部挪到灰色集合中;
  • 将本对象挪到黑色集合中。

Desktop View

AD从白色集合,挪到灰色集合中,接下来开始迭代,从灰色集合获取到对象,开始一个一个处理处于中间状态的对象,先将AD的引用移到灰色集合,

假设D引用到E,先把E移到灰色集合里边,把D移动到黑色集合里边。等到AD处理完了之后(即把所引用的对象都挪到灰色集合中后),把AD移到黑色集合里边,表示AD的中间状态已经结束,并且AD是存活的对象。

Desktop View

接下来不断地迭代,

Desktop View

Desktop View

重复以上步骤,直至灰色集合为空时结束。 结束后,仍在白色集合的对象即为GC Roots不可达,可以进行回收。

注:如果标记结束后对象仍为白色,意味着已经找不到该对象在哪了,不可能会再被重新引用。

A: 当Stop The WorldSTW)时,对象间的引用是不会发生变化的,可以轻松完成标记。而当需要支持并发标记时,即标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。想想这种问题怎么解决?
Q: G1有使用屏障机制来解决多标、漏标问题,感兴趣可以去找相关资料学习,这里不多阐述。

比较常见的JVM8垃圾回收器

对主流JDK使用到的JVM垃圾回收器用的算法,做下简单总结。

新生代垃圾回收器

新生代里边用的垃圾回收器里边用的算法,一般是标记-复制算法。

  • Serial GC是新生代单线程版本,单线程版本效率都不高,使用复制算法,可以说是最基本、发展历史最悠久的回收器。Serial-New算法在进行垃圾回收时,必须暂停其他所有工作线程,直到它回收完成。
  • Java程序现在都变成了多线程版本,所以出现了Parallel GC,并发的标记-复制,新生代并行回收器,简称为ParNewParNew回收器-复制算法),就是Searial GC回收器的多线程版本。
  • 经过不断发展,出现了一种新的框架——Parallel Scavenge(并行回收-复制算法),也是新生代并行回收器,这个已经不是用的分代式GC算法框架(代码维度)了,它是以目标导向的,目标是达到一个可控制的吞吐量。
    • 用户代码执行时间越高,效率就越好,吞吐量越高。吞吐量可以用参数来配置,待会再说。Parallel Scavenge是一个目标导向的回收器,可以配置吞吐量来控制。目标是追求高吞吐量,高效利用CPU
    • 吞吐量(Throughput),是CPU用于运行用户代码的时间,与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾回收时间)。
    • 停顿时间越短,就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可用高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

老年代垃圾回收器

先看前面3大回收算法,哪些适合老年代。

  • 标记-复制算法肯定不适合,老年代的内存占比很大,占了堆内存的2/3,如果要预留这么大一块空间等着,假如内存9G,老年代占6G,那么还得有一个预留空间6G空闲着;
  • 标记-清除算法,适合老年代,但要解决内存碎片;
  • 标记整理算法,适合老年代,但要提升移动效率。

数一数老年代里边的垃圾回收器,

  • 首先是Serial OldSerial回收器的老年代版本,用的是标记-整理算法,但也是单线程版本,串行模式,虽然效率低,但也是有使用场景的,比如在Client模式的虚拟机里边使用,Client模式主要用于客户端的桌面程序。
  • Parallel Old,并行模式,Serial Old的多线程版本,即Parallel Scavenge回收器的老年代版本,用的标记-整理算法。这是两种传统的GC
  • CMSConcurrent Mark Sweep),标记-清除算法,既不耗时间,也不耗空间。

G1Garbage First Garbage Collector),一种更先进的算法,也是基于标记-整理算法,分布式的垃圾回收,分而治之,把内存分成了很多新生代和很多老生代,先不展开,待会再说。G1没有传统意义上的新生代、老生代。

这些垃圾回收器都会导致STW,串行停顿时间是最长的,并行短点,G1停顿最短。

垃圾回收器组合选型

下面主要关注几个问题,

  • 哪些是串行,哪些是并行?
  • 哪些效率高,哪些效率低?
  • 谁和谁可以合作,谁和谁不可以合作?

年轻代算法都是基于复制算法,准确的说,是标记-复制算法。因为第一步是要先标记可达对象,然后把可达对象复制到一块空区域,再把原来的区域清空。区别在于,

  • Serial是串行的,Serial工作过程中用户线程都是停掉的。
  • ParNewParallel Scavenge是并行的。所谓并行是指多个线程同时做垃圾回收的事情,但是仍然是要停下用户线程的工作的。
  • Parallel ScavengeParNew的一个优势在于Parallel Scavenge可以设置自适应调节EdenSurvivor区的比例、晋升老年代的比例。

老年代算法中,

  • Serial Old老年代算法采用的是标记整理算法,Paralled Old老年代算法采用的也是标记整理算法,不同点只是一个是完全串行的。
  • Paralled Old垃圾回收的时候有多个线程来跑,但是不可以跟用户线程一起跑。但是不管年轻代、老年代,以及目前市面上所有算法都不能避免STWStop The World,停止用户线程)。
  • CMSConcurrent Mark Sweep的缩写,就是并发标记清除算法,它与其他两种老年代算法不同,它是只标记清除,不整理,标是减少STW。
    • CMS不能配合Paralled Scavenge使用,只能用ParNew。为啥呢?Parallel Scavenge没有使用原本HotSpot其它GC通用的那个GC框架,所以不能跟使用了那个框架的CMS搭配使用.
    • ParallelScavengePS)的Young Collector就如名字所示,是并行的拷贝式回收器。因为它不兼容原本的分代式GC框架,为了凸显出它是不同的,所以它的Young Collector带上了PS前缀,全名变成PS Scavenge。对应地,它的Old Collector的名字也带上了PS前缀,叫做PS MarkSweep

G1是分布式的标记-整理算法。

通常来讲,STW引用线程的停顿时间:Serial Old > Paralled Old > CMS > G1。但是CMS有个致命的弱点,CMS必须要在老代码堆内存用尽之前完成垃圾回收,否则会触发担保机制,退化成Serial Old来垃圾回收,这时会造成较大的STW停顿。所以JDK 1.8默认的垃圾回收器是Paralled Scavenge+Paralled Old方式。年轻代用Parallel Scavenge,老年代用Parallel Old。在实际项目工程中,CMS+ParNew也是小型低配、老年代不频繁GC的场景下比较常用的一种组合。

Desktop View 可以搭配的GC组合

CMS回收器

CMS是一种以获取最短回收停顿时间为目标的回收器;
CMS基于并发标记清理实现,在标记清理时,不会导致用户线程无法定位引用对象;
CMS仅作用于老年代回收。

CMS的步骤如下:

  1. 初始标记(CMS initial mark):独占CPUSTW,仅标记GCroots能直接关联的对象,速度比较快;
  2. 并发标记(CMS concurrent mark)可以和用户线程并发执行,通过GCroots Tracing标记所有可达对象;
  3. 重新标记(CMS remark):独占CPUSTW,对并发标记阶段用户线程运行产生的垃圾对象进行标记修正,以及更新逃逸对象;
  4. 并发清理(CMS concurrent sweep):可以和用户线程并发执行,清理在重复标记中被标记为可回收的对象。

优点

  • 支持并发回收;
  • 低停顿,因为CMS可以控制将耗时的两个STW操作,保持与用户线程恰当的时机并发执行,并且能保证在短时间执行完成,这样就达到了近似并发的目的。

缺点

  • CMS回收器对CPU资源非常敏感,在并发阶段,虽然不会导致用户线程停顿,但是会因为占用了一部分CPU资源,如果在CPU资源不足的情况下,应用会有明显的卡顿。
  • 无法处理浮动垃圾:在执行并发清理步骤时,用户线程也会同时产生一部分可回收对象,但是这部分可回收对象只能在下次执行清理时才会被回收。如果在清理过程中,预留给用户线程的内存不足,就会出现Concurrent Mode Failure,一旦出现此错误时,便会切换到Serial Old回收方式。
  • CMS清理后,会产生大量的内存碎片,当有不足以提供整块连续的空间给新对象晋升为老年代对象时,又会触发FullGC
  • 1.9及以后将其废除。

CMS可以启用一个标记-整理的动作,可以配置,但是这样一来,就回到了解放前,退回到Parallel Old

CMS有个致命的弱点,使用时必须特别注意。CMS必须要在老代码堆内存用尽之前完成垃圾回收,否则会触发担保机制,退化成Serial Old来垃圾回收,这时会造成较大的STW停顿。 所以JDK 1.8默认的垃圾回收器是Parallel Scavenge+Parallel Old方式。

使用场景

它关注的是垃圾回收最短的停顿时间(低停顿),在老年代并不频繁GC的场景下,是比较适用的。

G1回收器

G1 GC200410月论文Garbage-First Garbage Collection发表,2012年在JDK 7u4(即JDK 7 Update 4)版本中正式推出并得到完全支持的Garbage first回收器(G1),旨在更好地支持大于4GB的堆。
准确点说,最初作为体验版在更早的JDK 6u14中面世,但直到JDK 7u4才被正式发布,JDK 8中基本成熟。
G1JDK 9中成为默认的垃圾回收器,逐步取代了CMS回收器,2020年在JDK 14删除CMS
G1回收器的内存结构完全区别于CMS,弱化了CMS原有的分代模型(分代可以是不连续的空间)。
G1的原理是分治法,将堆分成若干个等大的小区域,能实现一些复杂精细的功能。
G1将堆内存划分成一个个Region1MB-32MB,默认2048个分区),这么做的目的是在进行回收时不必在全堆范围内进行,不会像传统垃圾回收器一样等到空间满了才回收,而是在空间还没满时就进行了回收。
应用程序内存,我们往往把堆内存会配置32G,比如ESJava程序往往会配置8G-16G。大内存场景下,做一次堆内存老年代的复制、移动、压缩,耗费的时间挺长的,能用要命来形容。

G1的原理

G1原理很简单,就是做分治法,在小的区域上,用多线程做垃圾回收(标记-整理),可以理解为分布式的标记整理,不需要在全堆范围STW,导致JVM停车的时间缩短了,效率也就大大提升了。

Desktop View

每个RegionG1中扮演了不同的角色,比如Eden(新生区)、Survivor(幸存区)或者Old(老年代)。除了传统的老年代、新生代,G1还划分出了Humongous区域,用来存放巨大对象(humongous objectH-obj)。

G1 GCYoung GenerationOld Generation组成。G1Java堆空间分割成了若干个Region,即年轻代/老年代是一系列Region的集合,这就意味着,在分配空间时不需要一个连续的内存区间,即不需要在JVM启动时决定哪些Region属于老年代,哪些属于年轻代。因为随着时间推移,年轻代Region被回收后,又会变为可用状态(后面会说到的Unused RegionAvailable Region)了。

G1年轻代回收器是并行STW回收器,和其他HotSpot GC一样,当一个年轻代GC发生时,整个年轻代被回收。

G1的老年代回收器有所不同,它在老年代不需要整个老年代回收,只有一部分Region被调用。

G1 GC的年轻代由Eden RegionSurvivor Region组成。当一个JVM分配Eden Region失败后就触发一个年轻代回收,这意味着Eden区间满了。然后GC开始释放空间,第一个年轻代回收器会移动所有的存储对象从Eden RegionSurvivor Region,这就是Copy to Survivor过程。

对于每个区域使用的垃圾回收算法,实际上G1没有什么创新,年轻代还是并行拷贝,老年代主要采用并发标记配合增量压缩。算法方面也比较成熟了。

G1主要特点在于,达到可控的停顿时间,用户可以指定回收操作在多长时间内完成,即G1提供了接近实时的回收特性。

G1是响应时间优先的GC算法

G1里,用户可以通过MaxGCPauseMillis设定整个GC过程的期望停顿时间,默认是200msG1会努力在该时间内完成一次GCG1根据停顿预测模型,基于历史数据来预测本次回收需要选择的堆分区数量,从而尽量满足用户设定的期望停顿时间目标。

G1的衰减预测模型

  1. 计算衰减平均值(Decaying Average)‌,赋予近期数据更高的权重,使预测更贴近当前运行状况。 计算公式如下,

    1
    
     _davg = (1.0 - α) * 当前值 + α * 上次_davg
    
  2. 计算出衰减平均值之后可以得到衰减方差,衰减标准差(Decaying Standard Deviation, dsd)即为方差的平方根,用于衡量数据离散程度。

    1
    2
    3
    
     n = 1时,_dvariance = 当前值
    
     n > 1时,_dvariance = (1.0 - α) * (当前值 - _davg)² + α * 上次_dvariance
    
  3. G1最终就是根据衰减方差来实现的,使用以下公式综合均值与标准差进行保守预测。

    1
    2
    3
    4
    
     预测时间 = MAX2(davg + sigma * dsd, davg * confidence_factor)
     其中:
     sigma由G1ConfidencePercent控制(默认50,对应0.5),表示置信度;
     confidence_factor在样本不足时(<5次)大于1,以补偿不确定性。
    

    Desktop View

G1在选择回收哪些Region时,能基于历史数据的‌趋势与稳定性‌,动态调整工作负载,尽量在 -XX:MaxGCPauseMillis(默认200ms)设定的时间内完成回收。
停顿预测模型主要是针对老年代的,但如果设置的时间太小了,YGC时新生代回收也会受到影响。最大停顿时间,设置过小,导致GC时间内能处理的东西减少,从而JVM自动缩小了Eden区的堆大小,设置过大,则导致在YGC的时候,S区放不下。

G1的创新步骤(MixGC如下,

  1. 初始标记(Initial Marking):标记一下GC Roots能直接关联到的对象,伴随一次普通的YGC发生,并修改NTAMSNext Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,此阶段是STW操作。
  2. 根区间扫描,标记所有幸存者区间的对象引用,扫描Survivor到老年代的引用,该阶段必须在下一次YGC发生前结束。
  3. 并发标记(Concurrent Marking):是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行,该阶段可以被YGC中断。
  4. 最终标记(Final Marking):是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,此阶段是STW操作,使用snapshot-at-the-beginningSATB)算法。
  5. 筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,回收没有存活对象的Region并加入可用Region队列。这个阶段也可以做到与用户程序一起并发执行,但因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高回收效率。

G1的对象管理流程

如下,从左到右,

Desktop View新对象分配在新生代Eden区Desktop View启动YGC:活的对象被集中分到一个区域Desktop View继续分配,Eden区再次填满时,
E区和S区中活的对象分配规则,如下,
1. 如果某对象移动次数大于规定值,将其放到老年代(O区);
2. 其他的都被分配到一个新的S区。

G1的三种GC

G1的三种GC分别是YGCMixedGCFullGC,大致的触发顺序,如下,

  • Eden区不够了,触发YGC
  • 新生代中的对象不断地晋升(满15次)老年代,当老年代达到阈值,则先触发MixedGC,将新生代和部分老年代进行垃圾回收;
  • 如果回收后,老年代对象仍不够用,则会触发完全回收(FullGC);
  • 多次FullGC失败后,抛出OOM
  • Desktop View

YGC的步骤如下,

  1. GC roots出发标记存活对象;
  2. 复制存活对象到S区,该过程最耗时。通过对CMS的执行过程理解,首先是初始标记、并发标记、预处理、重新标记、并发清理、重置线程,而G1在标记为存活对象后,就立刻移动该对象到S区。
  3. 释放垃圾集合,回收Region。该工作反而比较快,类似硬盘格式化。当区块被回收后,就会变成自由分区(或者叫空白分区),再有G1根据情况决定,这个区块下一步分变为Eden区还是别的区块。
  4. 动态调整新生代区域Region的数量。自动判断增加还是减少Region区的数量,如果需要更大的新生代就增加。如果YGC能力弱,回收时间太长,那就要减少数量。G1执行YGC时,新生代的数量是可以动态变化的,一般什么都不设置的情况下,Eden区占比例在5%~60%之间(调整的原因,是根据每次执行垃圾回收后,根据当前的情况判断增加Region数量,每增加一个Region数量,Eden区就会增加一个,如果没有必要,Eden数量太多,导致回收时间太长,这时JVM就会减少Eden区数量)。
  5. 判断是否需要开启并发标记,如果开启了,并发标记是为下一步执行MixedGC做准备的。G1不断地YGC后,当某一次YGC判断老年代空间可能不太够用,则为下一次可能执行的MixedGC做准备,提前开启并发标记,提高回收效率。

YGC里并行执行的任务

  • 对于YGC,复制对象和标记是同时进行的,不是所有标记完才开始复制,而是找到一个对象就复制;
  • 更新RSet,处理跨区引用问题;
  • 并行执行的任务,一般是重要的,且耗时短的。

YGC里串行执行的任务

  • 释放分区,类似格式化硬盘,工作比较简单也比较快,涉及到一些全局状态表、全局集合的调整,所以采用串行的方式比较好;
  • 尝试扩展内存;
  • 调整新生代分区的数目;
  • 尝试启动并发标记,如果启动成功就要进入MixedGC了。

YGCMixedGC

MixedGCG1垃圾回收器特有的回收模式,在YGC之后,已分配内存超过内存总容量的45%会触发,可由参数-XX:InitiatingHeapOccupancyPercent控制,默认45%。实际要执行MixedGC还有一个条件-XX:G1HeapWastePercent,就是可回收的空间占总空间的比例大于5%,才会启动MixedGC。如果低于5%,JVM认为启动MixedGC的意义不大,所以不启动MixedGC。考虑到停顿时间,MixedGC会多次执行,-XX:G1MixedGCCOuntTarget参数,默认是8,表示MixedGCCSet分配回收,最多分为8次。

MixedGC的步骤如下,

  1. 初始标记,标记出所有有GC Root等直接引用的对象,会暂停用户程序;
  2. 并发标记,标记出上一步中标记的所有引用对象,执行时间略长,用户程序也会同时执行,不会STW
  3. 再标记,标记出上一个阶段没有被标记的对象,会STW,执行速度非常快;为什么需要再标记?还要STW?因为并发标记阶段耗时非常长,有些对象是死的,被标记成了活动,有些对象是活的被标记成了死的;
  4. 存活对象计数,统计出每个Region存活对象的数量;为什么要统计呢?下一步回收时,只会从老年代回收一部分区域,因此先统计每个区域存活数量、垃圾对象以及占比高低,才能判断该怎么选择,才能满足用户设定的停顿时间并保证收益最大。这里就是前面讲到的,怎么统计每个Region的垃圾集合、以及Region回收排序。
  5. 垃圾回收,选择回收价值高的区域,把存活对象复制到新分区,然后回收掉老区域。

MixedGC的并发标记从哪里开始?YGC的处理结果是否可以用一下?

  1. 新的S区对象
  2. 老年代GCRoot直接引用的对象
  3. 解决跨区引用的老年代RSet

MixedGC前面为什么会有一次YGC呢?

前面讲了MixedGC的触发条件是,在YGC后,已分配的内存占总内存的45%,触发MixedGC;并发标记,标记的什么呢,主要标记的是老年代的对象。

YGCMixedGC的前奏,YGC完成,就表示MixedGC已经完成了初始标记阶段,YGC已经帮MixedGC干完了初始化的工作。也就说MixedGC之前一定先进行一次YGC

为什么MixedGC会多次进行?

主要考虑停顿时间

  • 根据其他条件计算出CSet里有400Region满足回收的条件,但根据停顿时间一次只能50个,怎么办?
  • 分成多次,一次完成50个,8次搞定,-XX:G1MixedGCCountTarget,默认是8MixedGCCSet分配回收,最多分成8次。

Desktop View

MixedGCFullGC

  • FullGCG1中并不是一个独立的GC模式,而是当MixedGC无法跟上程序分配内存的速度,导致老年代填满无法继续进行MixedGC时,才会触发。此时,G1会使用Serial Old GC来回收整个堆内存。
  • MixedGCFullGCG1中是分别处理的。MixedGCG1的常规回收模式,它通过并发标记和选择性回收老年代Region来实现低延迟的垃圾回收。而FullGC则是G1在极端情况下的兜底机制,用于处理无法通过MixedGC解决的内存分配问题。两者不会同时进行,而是根据不同的触发条件和回收策略分别执行。

FullGC的触发条件

  • YGCMixedGC都不够(无法分配对象)触发FullGC,永久区满了也会触发FullGC
  • FullGC执行时间非常长,大概2-3秒,FullGC可能进行两次,如果执行一次仍然不够,会再次执行,第二次回收软引用,如果还不够用,对象仍然无法分配,系统基本上就要OOM了,所以FullGC是非常危险的,当出现FullGC,我们就应该进行检查,防止更大的风险发生。

FullGC怎么复制对象?

  • YGCMixedGC中,复制对象时,都可以先转移到一个空间的Region中,走的是标记复制算法;而FullGC是标记压缩,时间更慢,代价更高。

FullGC的步骤如下,

  1. 标记活跃对象,FullGC时,进入标记阶段,标记出所有的存活对象,这个过程和YGCMixedGC类似;
  2. 计算引用对象的地址,逐个遍历每个Region,每个Region从头开始,找到存活对象指向到接下来被回收的新位置;
  3. 更新引用对象的地址,上一步已经计算好每个对象新的地址了,那么此时就需要遍历所有存活对象,将对象间的引用也指向到新的位置上;
  4. 复制对象;
  5. 复制后的处理,对回收工作进行收尾,比如调整堆分区大小等。

G1的特点

  • 并行与并发‌:G1充分发挥多核性能,使用多CPU来缩短STW的时间。
  • 分代回收‌:G1能够自己管理不同分代内已创建对象和新对象的回收。
  • 空间整合‌:G1从整体上来看是基于标记-整理算法实现,从局部(相关的两块Region)上来看是基于复制算法实现,这两种算法都不会产生内存空间碎片。
  • 可预测的停顿‌:它可以自定义停顿时间模型,可以指定一段时间内消耗在垃圾回收上的时间不大于预期设定值。
  • 优势:每次垃圾回收时间短,系统吞吐量高。

使用场景

  • G1 GC切分堆内存为多个区间(Region),从而避免很多GC操作在整个Java堆或者整个年轻代进行。 - G1 GC是基于RegionGC,适用于大内存机器。即使内存很大,Region扫描,性能还是很高的。服务端多核CPUJVM内存占用较大的应用(至少大于4G
  • G1适用于想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
      我们可以从G1的机制和高并发场景的特性来理解这句话,
        
      一、G1如何实现可控、可预期的GC停顿周期
      1. ‌基于Region的灵活回收‌
      - G1将Java堆划分为多个独立的Region,回收时可以精准选择回收价值高、回收成本低的Region,而不是像传统GC那样全量回收整个年轻代或老年代。它会根据用户设定的停顿时间目标,动态调整回收的Region数量,比如用户指定GC停顿时间不超过200ms,G1就会只回收能在这个时间内完成的Region,从而把GC停顿控制在预期范围内。
      2. 停顿时间可自定义‌
      - G1支持通过参数(如-XX:MaxGCPauseMillis)直接指定最大GC停顿时间,虚拟机会在运行过程中根据堆内存的使用情况、对象存活比例等数据,自动调整回收策略,确保GC消耗的时间不会超过设定值,让停顿时间可预测、可控制。
    
      二、如何防止高并发下应用雪崩现象
      1. 避免长停顿拖垮系统‌
      - 在高并发场景下,用户请求量巨大,系统需要持续处理请求。如果GC停顿时长不可控,出现长时间的Stop-The-World,会导致大量请求堆积,无法及时处理,进而引发雪崩。G1的可控停顿机制,能保证GC停顿时间在可接受的范围内,不会让系统长时间无响应,避免请求堆积到超出系统承载能力。
      2. 并发回收降低影响‌
      - G1的并发标记、并发回收等阶段可以和用户程序同时运行,不会完全阻塞业务线程。在高并发时,即使进行GC,业务请求也能继续被处理,最大程度降低GC对系统性能的影响,防止因GC导致系统负载骤增、响应超时,最终引发雪崩。
    
      G1是多线程的回收方式,可以发挥多核优势,缩短JVM停顿的时间。
    

分代回收

整体上来看,它是标记-整理算法;局部来说,是基于复制算法,找到一个空白区来完成复制,因为有很多空白区。

JVM停顿的时间可以预测,有相关的参数可以调整。和Scavenge有点像,比如吞吐量配置也是可以影响STW时间的。

JDK 8的三种垃圾回收器配置

JDK 8默认的GC回收器

A: JDK 8默认使用的垃圾回收器是什么?

java命令查看一下,

1
2
3
4
5
$ java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=132500800 -XX:MaxHeapSize=2120012800 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_431"
Java(TM) SE Runtime Environment (build 1.8.0_431-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.431-b10, mixed mode)

UseParallelGC,即 Parallel Scavenge+ParOldGen

  • PSYoungGenPS就是Parallel Scavenge的缩写,
  • ParOldGen,就是Parallel Old

进一步,查看GC快照,

JDK 8的快照,如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
$ java -XX:+PrintGCDetails -version
java version "1.8.0_431"
Java(TM) SE Runtime Environment (build 1.8.0_431-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.431-b10, mixed mode)
Heap
 PSYoungGen      total 38400K, used 2670K [0x00000000d5e00000, 0x00000000d8880000, 0x0000000100000000)
                 eden space 33280K, 8% used [0x00000000d5e00000,0x00000000d609bb30,0x00000000d7e80000)
                 from space 5120K, 0% used [0x00000000d8380000,0x00000000d8380000,0x00000000d8880000)
                 to   space 5120K, 0% used [0x00000000d7e80000,0x00000000d7e80000,0x00000000d8380000)
 ParOldGen       total 87552K, used 0K [0x0000000081a00000, 0x0000000086f80000, 0x00000000d5e00000)
                 object space 87552K, 0% used [0x0000000081a00000,0x0000000081a00000,0x0000000086f80000)
 Metaspace       used 2445K, capacity 4480K, committed 4480K, reserved 1056768K
                 class space    used 267K, capacity 384K, committed 384K, reserved 1048576K

也可以和JDK 7的快照对比下,看有什么不同,

1
2
3
4
5
6
7
8
9
10
11
12
13
$ java -XX:+PrintGCDetails -version
java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)
Heap
 PSYoungGen      total 36864K, used 1904K [0x00000000d7200000, 0x00000000d9b00000, 0x0000000100000000)
                 eden space 31744K, 6% used [0x00000000d7200000,0x00000000d73dc3e0,0x00000000d9100000)
                 from space 5120K, 0% used [0x00000000d9600000,0x00000000d9600000,0x00000000d9b00000)
                 to   space 5120K, 0% used [0x00000000d9100000,0x00000000d9100000,0x00000000d9600000)
 ParOldGen       total 83456K, used 0K [0x0000000085600000, 0x000000008a780000, 0x00000000d7200000)
                 object space 83456K, 0% used [0x0000000085600000,0x0000000085600000,0x000000008a780000)
 PSPermGen       total 21504K, used 2171K [0x0000000080400000, 0x0000000081900000, 0x0000000085600000)
                 object space 21504K, 10% used [0x0000000080400000,0x000000008061ec20,0x0000000081900000)

UseParallelGC回收器是在JDK1.6中才开始提供的,在此之前Parallel Scavenge一直处于尴尬的状态。 JDK1.6之前,默认是Parallel Scavenge+Serial OldJDK1.6以及之后,默认是Parallel Scavenge + Parallel Old

原因是,如果新生代选择了Parallel Scavenge回收器,老年代除了Serial Old别无选择,由于老年代Serial Old性能上的拖累,使用了Parallel Scavenge回收器也未必能够在整体应用上获得吞吐量的最大化效果,直到Parallel Old回收器出现后,吞吐量优先回收器终于有了名副其实的应用组合。

Parallel OldParallel Scavenge回收器的老年版本,使用多线程标记-整理算法; Parallel Scavenge回收器的关注点与其他回收器不同。Parallel Scavenge回收器的目标则是达到一个可控制的吞吐量(Throughput)。 高吞吐量,即减少垃圾回收时间,让用户代码获得更长的运行时间;主要适合在后台计算而不需要太多交互的任务。

Parallel Scavenge + Parallel Old 使用场景
适用于一些需要长期运行,且对吞吐量有一定要求的后台程序。

怎么控制吞吐量?

Scavenge提供了几个可配置的参数,先简单看下,细节只有在实际用的时候才会深入研究,也不会经常研究,研究完会形成一个模板,后面再用的时候就是微调下模板就可以用了。

重要的参数有三个,其中两个参数用于精确控制吞吐量,分别是

  • -XX:MaxGCPauseMillis
  • -XX:GCTimeRatio
  • -XX:UseAdaptiveSizePolicy

-XX:MaxGCPauseMillis参数,控制最大垃圾回收停顿时间 MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,回收器将尽力保证内存回收花费的时间不超过设定值。

-XX:GCTimeRatio参数,直接设置GCTime的比例 此参数的值表示运行用户代码时间是GC运行时间的n倍。 表示希望在GC花费不超过应用程序执行时间的1/(1+n),n为大于0小于100的整数。 举个官方的例子,参数设置为19,那么GC最大花费时间的比率=1/(1+19)=5%,程序每运行100分钟,允许GC停顿共5分钟,其吞吐量=1-GC最大花费时间比率=95% 默认情况下,VM设置此值为99,运行用户代码时间是GC停顿时间的99倍,即GC最大花费时间比率为1% GCTimeRatio参数的值应当是一个大于0小于100的整数,默认值为99,就是允许最大1%(即1 / (1+99))的垃圾回收时间。

-XX:+UseAdaptiveSizePolicy是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、EdenSurvivor区的比例(-XX:SurvivorRatio)、晋升老年代表对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况回收性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。

JDK8使用CMS回收器参考配置

G1出来之前,CMS绝对是OLTP系统的标配,即使G1出来几年了,生产环境很多的JVM实例还是采用ParNew+CMS的组合。 注意,JDK 8的默认垃圾回收器是Parallel Scavenge + Parallel Old,不采用CMS是因为CMS不稳定可能会退化成Serial Old

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-server                                         //服务端模式
-Xmx2g                                          //JVM最大允许分配的堆内存,按需分配
-Xms2g                                          //JVM初始分配的堆内存,一般和Xmx配置成一样以避免每次gc后JVM重新分配内存。
-Xm256m                                         //年轻代内存大小,整个JVM堆内存=年轻代 + 年老代 + 持久代
-Xx:PermSize=128m                               //持久代内存大小
-Xss256k                                        //设置每个线程的堆栈大小
-XX+DisableExplicitGC                           //忽略手动调用GC,System.gc()的调用就会变成一个空调用,完全不触发GC
-XX+UseConcMarkSweepGC                          //并发标记清除(CMS)回收器
-XX:+CMSParallelRemarkEnabled                   //降低标记停顿
-XX+UseCMSCompactAtFullCollection               //在FullGC的时候对老位的压缩
-XX+LargePageSizeInBytes=128m                   //内存的大小
-XX+UseFastAccessorMethdgs                      //原始类型的快速优化
-XX+UseCMSInitiatingOccupancyOnly               //使用手动定义初始化定义开始CMS回收
-XX:CMSInitiatingOccupancyFraction=70           //使用cms作为垃圾回收使用70%后开始CMS回收

-Xmn(年轻代大小)和-Xmx(堆内存最大值)之比大概是1:9,这种配置方式旨在优化年轻代的内存分配,使得新生代足够大以容纳短期存活的对象,同时又不至于让老年代过小而频繁触发FullGC。如果把新生代内存设置得太大,会导致YGC时间较长。

不过,这个比例并非适用于所有系统。对于不同的应用场景,例如游戏服务器等需要处理大量长连接的场景,年轻代可能需要设置得更大,与堆大小的比例可能会接近1:3。因此,在实际调优过程中,应根据应用的特点和GC日志分析结果来调整年轻代的大小。

一个好的Web系统应该是每次HTTP请求申请内存,都能够在YGC回收掉,FullGC永不发生,当然这是最理想的情况。

理论上来说,如果我们内存充足,比如有8-16G,用CMS要比用Scavenge要好点,Scavenge有硬伤,用的是标记-整理算法,整理伴随着停顿,停顿还是比较大的。

CMS在大内存场景下,虽然会产生碎片,但是清除的效率高,而且内存比较足,在G1出来前,在大内存场景下,还是推荐使用CMS

需要对老年代的碎片做压缩和整理,-XX:UseCMSCompactAtFullCollection,在FullGC时对老年代进行压缩,这是必要的。

G1出来后,对大内存,通通都是用G1回收器。

JDK 8 使用G1回收器参考配置

使用G1的核心配置

  • -XX:+UseG1GC //使用G1回收器
  • -XX:MaxGCPauseMillis=200 //用户设定的最大GC停顿时间,默认是200ms

JDK 8 update 20刚刚推出的另一个漂亮的优化是,G1回收器字符串重复数据删除。

由于字符串(及其内部char[]数组)占用了我们的大部分堆空间,因此进行了新的优化,使G1回收器可以识别在整个堆中重复多次的字符串,并更正它们以指向同一内部字符[]数组,以避免同一字符串的多个副本无效地驻留在堆中。 如果你的应用使用的是G1回收器,并且JDK的版本大于JDK 8 update 20,那么可以尝试开启-XX:+UseStringDeduplication,如果你的应用中存在大量长时间存活的对象,那结果一定还不错。 但需要注意的是,-XX:+UseStringDeduplication需要额外CPU做字符串去重,低配(比如2C)下不划算,CPU资源比较重要。

G1生产环境的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-Xms32G                             // 最小堆
-Xmx320G                            // 最大堆
-Xss10M                             // 栈空间
-XX:MetaspaceSize=2G                // Metaspace扩容时触发FullGC的初始化阈值
-XX:+UseStringDeduplication         // JVM在做GC的同时会做重复字符串消除
-XX:+PrintStringDeduplicationStatistics
-XX:+UseG1GC                        // 使用G1 GC
-XX:+UnlockExperimentalVMOptions    // 允许使用experimental的参数
-XX:G1HeapWastePercent=5            // Sets the percentage of heap that you are willing to waste.
-XX:G1MixedGCLiveThresholdPercent=85        // Sets the occupancy threshold for an old region to be included in a mixed garbage collection cycle. (experimental)
-XX:G1HeapRegionSize=32M            // Sets the size of a G1 region.
-XX:MaxGCPauseMillis=10000          // Sets a target value for desired maximum pause time.
-verbose:gc                         // alias for -XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-Xloggc:path_to_my_gc.log           // GC log 文件存放位置
-Dcom.sun.management.jmxremote.port=1234        // 远程调试端口
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
-Dlog4j.configuration=file://$LOG4J_FILE        // log4j文件存放位置

关于STW卡顿
JDK1.3到现在,从Serial回收器 –> Parallel回收器 –> CMS –> G1,用户线程停顿时间不断缩短,但仍然无法完全消除。

几个有用的Tips

CMS GC中,YGC与FullGC的发生频率和时机

这个问题和直接内存的回收也有关系,MinorGC实际是很频繁的,比如MinorGC只要大于10秒就不算频繁,算是正常。

一般需要进行GC的优化参考指标,如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化;如果GC时间超过1~3秒,或者频繁GC,则必须优化。

如果满足下面的指标,则一般不需要进行GC优化,

  • MinorGC执行时间不到50ms,不需要优化;反之,超过50ms需要优化;
  • MinorGC执行不频繁,约10秒一次,不需要优化;反之,小于10s需要优化;
  • FullGC执行时间不到1s;反之,超过1s需要优化;
  • FullGC执行频率不算频繁,不小于10分钟1次。反之,如果小于10分钟需要优化;

也就是说,最长150s,也就是3分钟之后,如果DirectByteBuffer没有被回收,就会移动到old

A: 什么时候YGC,什么时候FullGC

YGCEden空间不足,创建对象时就会执行YGCFullGC的时候会先触发MinorGC

FullGCOld老年代空间不足,或者显式调用方法System.gc()YGC时可能触发FullGCdump live的内存信息时(jmap -dump:live,导出快照),都会执行FullGC

特殊情况:

  1. 如果创建一个大对象,Eden区当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发FullGC。为了避免这种情况,最好就是不要创建太大的对象。
  2. 发生MinorGC之前,判断老年代可用空间是否大于当此GC时新生代所有对象容量,或者老年代可用空间是否大于平均晋升大小,如果满足,则执行MinorGC,否则FullGC

YGC时可能触发FullGC,就是特殊情况里边的第2种。

Minor GC的结果是,有一些对象要升级,如果所有的对象都升级,都升到老年代去,老年代可用空间如果大于当次GC时新生代所有对象容量,那么这次MinorGC是没有风险的。

如果满足不了上面的条件,还有一个保底的条件,老年代可用空间大于平均晋升大小(历次MinorGC的平均大小),就认为这次升级的大小也会是平均大小左右,所以这次晋升风险是有,但是不大,可以勉强尝试MinorGC

如果条件二都达不到,毫无疑问,晋升的风险太大了,干脆不做MinorGC,直接FullGC

垃圾回收线程自动扫描,不是每次都等到创建对象时才做MinorGC

什么时候发生MinorGC(YGC)?

  • Eden区满了,或者新创建的对象大小 > Eden所剩空间;
  • CMS设置了CMSScavengeBeforeRemark参数,这样在CMSRemark之前会先做一次MinorGC来清理新生代,加速之后的Remark的速度。这样整体的STW时间反而短;
  • FullGC的时候会先触发MinorGC

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

  1. 如果大于的话,直接执行MinorGC
  2. 如果小于,判断是否开启HandlerPromotionFailure,没有开启直接FullGC
  3. 发生MinorGC之前,判断老年代可用空间是否大于平均晋升大小,或者老年代可用空间是否大于当此GC时新生代所有对象容量,如果满足,则执行MinorGC,否则FullGC

什么时候会发生FullGC

从年轻代空间(包括EdenSurvivor区)回收内存被称为MinorGC,对老年代GC称为Major GC,而FullGC是对整个堆来说的,在最近几个版本的JDK里默认包括了对永生代(即方法区)的回收(JDK8中无永生代了),出现FullGC的时候经常伴随至少一次的MinorGC,但非绝对的。MajorGC的速度一般会比MinorGC10倍以上。

下面看看有哪种情况触发JVM进行FullGC,以及应对策略。

  1. System.gc()方法的调用
    • 此方法的调用是建议JVM进行FullGC,虽然只是建议而非一定,但很多情况下它会触发FullGC,从而增加FullGC的频率,也即增加了间歇性停顿的次数。
    • 强烈建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过-XX:+DisableExplicitGC来禁止RMI调用System.gc。 在使用堆外内存的场景,此项配置要非常小心,不建议使用。
  2. 老年代空间不足
    • 老年代空间只有在新生代对象转入及创建大对象、大数组时才会出现不足的现象,当执行FullGC后空间仍然不足,则抛出如下错误:java.lang.OutOfMemoryError: Java heap space
    • 为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在MinorGC阶段被回收,让对象在新生代多存活一段时间,以及不要创建过大的对象及数组。
  3. 堆中分配很大的对象
    • 所谓大对象,是指需要大量连续内存空间的Java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行FullGC
    • 为了解决这个问题,CMS垃圾回收器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection开关参数,用于在享受完FullGC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但卡顿时间不得不变长了。
    • JVM设计者们还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的FullGC后,跟着来一次带压缩的。
  4. 永生区空间不足
    • JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息,常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行FullGC。如果经过FullGC仍然回收不了,那么JVM会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space
    • 为避免Perm Gen占满造成FullGC现象,可采用的方法为增大Perm Gen空间,或转为使用CMS GC
  5. CMS GC时出现promotion failedconcurrent mode failure
    • 对于采用CMS进行老年代GC时,尤其要注意GC日志中有promotion failedconcurrent mode failure两种状况,当这两种状况出现时可能会触发FullGC
  6. MinorGC引发FullGC
    • 发生MinorGC之前,判断老年代可用空间是否大于平均晋升大小,或者老年代可用空间是否大于当此GC时新生代所有对象容量,如果满足,则执行,否则FullGC

G1 GC的CSet选择过程

G1 GCCSetCollection Set)是每次垃圾回收操作中需要处理的一组Region集合。CSet的选择过程是G1实现低延迟和高吞吐量的关键机制之一,包括MixedGC中新生代和老年代分区的选择,以及-XX:G1MixedGCLiveThresholdPercent参数的影响。

CSet代表每次GC暂停时回收的一系列目标分区。在任意一次回收暂停中,CSet的所有分区都会被释放,内部的存活对象会被转移到分配的空闲分区中。无论是YGC还是MixedGC,其工作原理都是一致的。

CSet = YGC中的CSet + MixedGC中的CSet

  • YGC时,CSet包含所有新生代Region,即EdenSurvivor区域。这个过程是STW的,目的是回收年轻代中的垃圾对象,将存活对象复制到Survivor区域或晋升到老年代区域。
  • MixedGC中,CSet包含所有新生代RegionEden+Survivor)以及部分选中的老年代RegionMixedGCG1的核心机制之一,它在并发标记完成后开始,旨在回收部分老年代Region,以达到控制GC暂停时间的目标。

MixedGC中,G1会根据一定的策略选择老年代Region加入CSet。这个过程包括以下几个关键步骤,

  1. 选择新生代Region
    • MixedGCCSet首先包含所有新生代Region,即EdenSurvivor区域。这部分Region在每次MixedGC中都会被回收,以清理年轻代中的垃圾对象。
  2. 选择老年代Region
    • 在选择老年代Region时,G1会根据回收价值进行排序。回收价值通常基于Region中垃圾对象的比例和回收所需的时间。G1会优先选择垃圾比例高、回收收益大的Region加入CSet
  3. 回收价值的计算
    • G1通过计算每个Region的回收价值来决定是否将其加入CSet。回收价值的计算通常考虑以下因素:
    • 垃圾比例‌:Region中垃圾对象所占的比例越高,回收价值越高。
    • 回收时间‌:回收一个Region所需的时间越短,回收价值越高。
    • 回收收益‌:回收一个Region所能释放的内存空间大小。
    • G1会维护一个优先级列表,按照回收价值从高到低排序,以便在每次回收时优先选择价值最高的Region

-XX:G1MixedGCLiveThresholdPercent参数用于设置候选老年代分区的CSet准入条件。默认值为85%,意味着只有当老年代Region中存活对象的比例低于85%时,该Region才会被选入CSet进行回收。

  1. 参数的作用
    • 这个参数的主要作用是拦截那些回收开销巨大的对象。如果一个Region中存活对象的比例过高(例如超过85%),那么回收这个Region的收益就会很低,因为需要复制大量的存活对象(前面讲了,一边标记一边复制,最耗时的是复制移动对象),而回收的空间相对较少。因此,G1会跳过这些Region,以避免不必要的开销。
  2. 参数的设置
    • 通过调整-XX:G1MixedGCLiveThresholdPercent参数,可以控制MixedGC中回收老年代Region的策略。例如,如果设置为90%,则只有当Region中存活对象比例低于90%时,才会被选入CSet。这会使得G1在回收时更加保守,减少对高存活率Region的回收,从而可能增加GC的频率,但减少每次GC的耗时。

为了在有限的时间内完成垃圾回收,G1会根据以下参数和策略来优化CSet的选择,

  1. CSet大小的限制
    • G1通过参数-XX:G1OldCSetRegionThresholdPercent(默认10%)来限制每次MixedGC中可以包含的老年代Region数量。这个参数定义了CSet中老年代Region占整个堆的比例上限。
  2. 回收次数的限制
    • G1通过参数-XX:G1MixedGCCountTarget(默认8)来限制在一次全局并发标记周期中MixedGC的最大次数。这个参数有助于控制GC的频率,避免过多的GC暂停影响应用程序的性能。
  3. 堆废物百分比
    • -XX:G1HeapWastePercent参数用于设置堆废物百分比,当回收达到这个参数时,不再启动新的MixedGC。这有助于避免在堆中浪费过多空间。

G1CSet选择过程是一个动态且智能的过程,旨在最大化垃圾回收的效率并控制GC暂停时间。在MixedGC中,CSet包含所有新生代Region和部分选中的老年代Region,其中老年代Region的选择基于回收价值和参数-XX:G1MixedGCLiveThresholdPercent的限制。通过这些机制,G1能够在保证应用程序性能的同时,实现高效的垃圾回收。

换种说法,G1怎么选择垃圾集合?怎么判断区域回收的代价?怎么看哪些是回收价值高的?如何统计回收时间?

G1 GC在选择回收区域Region时,采用了一种基于垃圾对象比例排序的算法,以实现高效且可预测的垃圾回收。这个过程涉及多个关键步骤,包括判断回收代价、识别高价值区域以及统计回收时间。

  1. ‌基于垃圾对象比例排序的Region选择算法‌
    • G1将堆内存划分为多个固定大小的Region,每个Region可以扮演不同的角色(EdenSurvivorOldHumongous)。在垃圾回收过程中,G1会根据每个Region中垃圾对象所占的比例进行排序,优先选择垃圾比例高的Region进行回收。这种策略被称为垃圾优先(Garbage-First)。
    • 具体来说,G1在并发标记阶段(Concurrent Marking)会计算每个Region的垃圾回收价值,即回收该Region所能释放的空间大小与回收所需时间的比值。这个比值越高,说明该Region的回收价值越高。G1会维护一个优先级列表,按照回收价值从高到低排序,以便在每次回收时优先选择价值最高的Region‌。
  2. ‌如何判断区域回收的代价‌

    判断一个Region的回收代价主要基于以下几个因素,

    • 回收时间‌:G1会记录每次回收时每个Region的耗时数据。例如,如果配置的-XX:MaxGCPauseMillis200ms,而每个Region的回收耗时为40ms,那么在一次回收中,G1最多能处理4Region
    • 回收空间大小‌:回收的Region中包含的垃圾对象数量和大小决定了能释放的内存空间。G1会计算每个Region的垃圾回收价值,即回收空间大小与回收时间的比值。
    • Region的活跃对象比例‌:活跃对象比例越高,说明该Region中存活对象多,回收的收益相对较低。因此,G1会优先选择活跃对象比例低的Region
  3. ‌如何识别回收价值高的区域‌

    G1通过以下方式识别回收价值高的区域,

    • 垃圾比例‌:G1会统计每个Region中垃圾对象所占的比例。垃圾比例高的Region通常具有更高的回收价值。
    • 回收收益计算‌:G1会计算每个Region的回收收益,即回收所获得的空间大小与回收所需时间的经验值。这个收益值越高,说明该Region的回收价值越高。
    • 优先级列表‌:G1维护一个优先级列表,根据回收收益对Region进行排序。在每次回收时,G1会优先选择列表中收益最高的Region
  4. ‌如何统计回收时间‌?

    G1在垃圾回收过程中会详细记录每次回收的耗时数据,以帮助优化下一次回收的策略。

    具体统计方式如下,

    • 性能记录与优化‌:在执行YGC过程中,G1会记录每次回收时每个Eden区和Survivor区的详细耗时数据。这些数据为下次回收提供了宝贵的参考,帮助G1更精确地计算出在给定的最大暂停时间内可以回收的Region数量。
    • GC日志分析‌:通过分析GC日志,可以获取每次回收的具体耗时信息。例如,日志中会显示[GC pause(G1Evacuation Pause)(young), 0.0063650secs],表示一次YGC的暂停时间为0.0063650秒。此外,日志还会记录各个阶段的耗时,如[Object Copy(ms):Min:2.2,Avg:2.4,Max:2.5,Diff:0.4,Sum:9.5],表示对象复制阶段的耗时。
  5. ‌选择Region的策略‌

    G1根据回收能力选择Region的策略如下,

    • 设定最大回收区域数‌:G1会根据配置的最大暂停时间(-XX:MaxGCPauseMillis)和每个Region的回收时间,计算出在一次回收中最多可以处理多少个Region
    • 优先选择高价值Region:根据垃圾对象占对象的比例排序,如果本次能清理3Region,就选择回收价值最高的123Region;如果只能清理2个,就清理12Region;如果只能清理1Region,就只清理1Region。这些选出来的Region会被放入回收集(CSet)中进行回收。

通过这种方式,G1能够在保证暂停时间可控的前提下,最大化垃圾回收的效率,从而实现低延迟和高吞吐量的平衡‌。

总结
‌G1CSet选择过程通过多种优化策略,在保证低停顿时间的前提下最大化回收效率‌。这些策略动态调整回收范围和节奏,决定哪些Region会被回收,确保系统吞吐与响应速度的平衡。

  1. 基于回收价值的优先级排序(Garbage-First核心)
    • G1不会全堆扫描,而是根据每个Region的‌垃圾密度‌(即回收后可释放空间的比例)进行排序,优先回收性价比最高的Region。这使得每次GC都能在有限时间内清理出尽可能多的内存空间。
  2. 控制老年代Region回收比例:-XX:G1OldCSetRegionThresholdPercent
    • 该参数限制每次MixedGC中可加入CSet的老年代Region数量,默认为堆大小的10%。通过控制单次回收规模,避免因处理过多老年代Region导致STW时间过长,从而保障应用响应性。
  3. 限制MixedGC执行次数:-XX:G1MixedGCCountTarget
    • 在一次并发标记周期后,G1会分批执行多轮MixedGC来逐步清理老年代。此参数设定最大轮数(默认8次),防止GC持续时间过长影响业务。例如,若设为5,则最多执行5MixedGC,即使老年代仍有可回收空间。
  4. 过滤高存活率Region-XX:G1MixedGCLiveThresholdPercent
    • 默认值为85%,表示只有存活对象占比低于85%的老年代Region才可能被选入CSet。因为高存活率Region复制开销大、回收收益低,跳过它们能显著减少GC耗时,提升整体效率。
  5. 动态调整新生代大小以匹配暂停目标
    • G1会根据-XX:MaxGCPauseMillis设定的停顿目标,动态调整EdenSurvivor区的Region数量。若需缩短暂停时间,G1会减少新生代大小,从而降低YGCMixedGC的负担,但可能增加GC频率。
  6. 堆废物容忍机制:-XX:G1HeapWastePercent
    • 允许堆中存在一定比例的无用空间(默认5%),当回收收益低于此阈值时,G1将停止MixedGC。这避免了为清理极少量垃圾而付出高昂时间成本,提升整体运行效率。

GCroots有哪些?

  • 方法区(元空间)所关联的类、对象、常量,方法区里的东西一般是常驻内存的;
  • 正在执行的线程关联的类、对象(虚拟机栈中的局部变量引用),是不会被标记清除的;
  • 跨代引用,老年代引用了新生代对象,这些新生代对象也是不会被清除的;
  • 这些对象之所以能作为GC Roots,是因为它们代表了程序运行时必须存活的根源引用。只要一个对象能通过引用链连接到任意一个GC Root,就不会被垃圾回收器回收。

GC Roots主要包括四类对象:虚拟机栈中的局部变量引用、方法区中的静态变量引用、方法区中的常量引用、本地方法栈中的JNI引用。

  1. 虚拟机栈中引用的对象‌
    • 指当前线程执行方法时,栈帧中的局部变量表所引用的对象。例如方法内创建的对象实例User user = new User(),只要方法未执行完毕,该对象就一直被根引用。
  2. 方法区中类的静态属性引用的对象‌
    • 被static修饰的类变量所引用的对象。由于类加载后长期存在,其引用的对象也被视为根,如public static List<String> cache = new ArrayList<>()中的cache实例。
  3. 方法区中常量池引用的对象‌
    • 包括字符串常量池中的引用(如hello)、类或方法的符号引用等。这些常量在类加载时被解析并注册,其指向的对象也被视为GC Root
  4. 本地方法栈中JNI引用的对象‌
    • 在调用Native方法(C/C++)时,通过JNIJava Native Interface)创建的全局引用(Global Ref)所指向的Java对象。局部引用(Local Ref)不在此列,因其生命周期随JNI方法结束而终止。

注意:循环引用(如A引用BB引用A)若没有与任何GC Root相连,仍会被判定为不可达,从而被回收。这正是可达性分析算法优于引用计数法的关键所在。

查看GC频率

  • jps 默认输出包括pid和主类名或jar文件名
  • jstat -gc pid 垃圾回收统计
  • jstat -gccause pid 进行GC原因分析,上一次GC的原因,并且持续监控,3秒输出一次
1
2
3
$ jps
4567 xxx.jar
5589 Jps

实时监控Java进程ID4567JVM GC状态,采样间隔为3000毫秒(即3秒)。

  • gc:指定监控垃圾回收相关的统计信息,显示堆内存各区域的使用情况及垃圾回收次数和耗时。
  • gccause:指定监控垃圾回收的详细信息,包括最后一次GC的原因。这个选项显示的信息比-gc更详细,可以查看触发GC的具体原因。
1
2
3
4
$ jstat -gc 4567 3000
S0C     S1C     S0U S1U     EC      EU      OC      OU      MC      MU      CCSC   CCSU   YGC YGCT  FGC FGCT  GCT
22528.0 15360.0 0.0 14922.8 87040.0 59405.7 29184.0 22575.0 67072.0 63993.0 8704.0 7884.4 21  0.997 4   1.523 2.521
22528.0 15360.0 0.0 14922.8 87040.0 59503.6 29184.0 22575.0 67072.0 63993.0 8704.0 7884.4 21  0.997 4   1.523 2.521
列名含义
S0C第一个幸存区(Survivor 0)的容量大小,单位KB
S1C第二个幸存区(Survivor 1)的容量大小,单位KB
S0U第一个幸存区(Survivor 0)的已使用大小,单位KB
S1U第二个幸存区(Survivor 1)的已使用大小,单位KB
ECEden区的容量大小,单位KB
EUEden区的已使用大小,单位KB
OCOld区容量大小,单位KB
OUOld区已使用大小,单位KB
MC方法区(Metaspace区)容量大小,单位KB
MU方法区(Metaspace区)已使用大小,单位KB
CCSC压缩类空间容量大小,单位KB
CCSU压缩类空间已使用大小,单位KB
YGC年轻代垃圾回收次数
YGCT年轻代垃圾回收消耗时间,单位:秒
FGCFullGC次数
FGCTFullGC消耗时间,单位:秒
GCT所有垃圾回收消耗总时间,单位:秒
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
1. 年轻代(Young Generation)‌
- Eden区(EC)‌:87040.0KB(约 85 MB)
- Eden区使用量(EU)‌:59405.7KB(约58MB)
- Survivor 0区(S0C)‌:22528.0KB(约22MB)
- Survivor 0区使用量(S0U)‌:0.0KB
- Survivor 1区(S1C)‌:15360.0KB(约15MB)
- Survivor 1区使用量(S1U)‌:14922.8KB(约14.5MB)
- 年轻代总容量‌:EC + S0C + S1C = 87040.0 + 22528.0 + 15360.0 = 124928.0KB(约122MB)
- 年轻代已使用量‌:EU + S0U + S1U = 59405.7 + 0.0 + 14922.8 = 74328.5KB(约72.6MB)
分析:
- Eden区使用率约为68.3%(59405.7 / 87040.0),表明Eden区已接近满载,可能即将触发YGC。
- Survivor 0区未使用,Survivor 1区使用率约为97.1%(14922.8 / 15360.0),表明大部分对象在上一次GC后都进入了Survivor 1区。年轻代整体使用率约为59.5%(74328.5 / 124928.0),说明年轻代内存使用尚可。

2. 老年代(Old Generation)‌
- 老年代容量(OC)‌:29184.0KB(约 28.5MB)
- 老年代已使用量(OU)‌:22575.0KB(约 22.1MB)
- 老年代使用率‌:约77.4%(22575.0 / 29184.0)
分析:
老年代使用率较高,已使用77.4%,说明老年代内存压力较大。需要关注是否存在对象过早进入老年代或老年代内存不足的问题。

3. 元空间(Metaspace)‌
- 元空间容量(MC)‌:67072.0KB(约 65.5MB)
- 元空间已使用量(MU)‌:63993.0KB(约 62.5MB)
- 元空间使用率‌:约95.4%(63993.0 / 67072.0)
分析:
元空间使用率非常高,接近95.4%,需要警惕元空间内存不足的风险。如果类加载频繁或存在类加载器泄漏,可能导致元空间溢出。

4. 压缩类空间(Compressed Class Space)‌
- 压缩类空间容量(CCSC)‌:8704.0KB(约 8.5MB)
- 压缩类空间已使用量(CCSU)‌:7884.4KB(约 7.7MB)
- 压缩类空间使用率‌:约 90.6%(7884.4 / 8704.0)
分析:
压缩类空间使用率也较高,接近90.6%,但通常不会像元空间那样引发严重问题。

5. 垃圾回收情况分析
- 年轻代 GC次数(YGC)‌:21次
- 年轻代 GC总耗时(YGCT)‌:0.997秒
- FullGC次数(FGC)‌:4次
- FullGC总耗时(FGCT)‌:1.523秒
- GC总耗时(GCT)‌:2.521秒
分析:
- 年轻代GC频繁,共21次,总耗时0.997秒,平均每次耗时约47.5毫秒。这表明应用中对象生命周期较短,或者Eden区设置过小,导致频繁触发MinorGC。
- FullGC次数为4次,总耗时1.523秒,说明老年代内存压力较大,可能触发了FullGC。需要进一步分析FullGC的原因。
- GC 总耗时占程序运行时间的比例约为2.521 / (21 * 3) ≈ 3.9%,表明GC对程序性能有一定影响,但尚在可接受范围内。

综合结论
- 当前应用内存使用情况较为紧张,特别是老年代和元空间的使用率较高。
- 需要关注Eden区的使用率,防止频繁触发YGC。
- 需要监控FullGC的频率和耗时,避免因老年代内存不足或内存泄漏导致性能问题。
- 元空间使用率接近上限,需警惕元空间溢出风险,考虑适当增大-XX:MaxMetaspaceSize参数。

Tips

  • 监控GC行为‌:通过观察YGCFGC的变化,可以判断垃圾回收的频率是否过高或FullGC是否频繁发生。
  • ‌内存使用情况‌:通过EUEden使用量)和OUOld使用量)的数值,可以判断内存分配和回收的趋势。如果EU快要接近EC,说明Eden区即将满,可能即将触发YGC
  • ‌性能瓶颈识别‌:如果YGCTFGCT持续增加,表明垃圾回收耗时较长,可能影响应用性能。
  • ‌持续监控‌:设置3000毫秒的间隔,可以持续观察JVM的运行状态,及时发现内存使用异常或GC频率突增等问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ jstat -gccause 4567 3000
S0    S1     E      O      M      CCS    YGC YGCT    FGC  FGCT     GCT   LGCC                GCC
0.00  97.15  59.81  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  59.81  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  59.81  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  60.05  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  60.05  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  60.05  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  60.05  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  60.19  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  60.30  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  60.38  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  60.42  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  60.42  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  60.42  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  60.52  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  60.54  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  60.54  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC
0.00  97.15  60.64  77.35  95.41  90.58  21  0.997   4    1.523    2.521 Allocation Failure  No GC

含义分析

  • S0, S1, E, O, M, CCS:分别表示 Survivor 0区、Survivor 1区、Eden区、Old区、Metaspace区和压缩类空间的已使用空间百分比。这些百分比反映了各个内存区域的使用情况。
  • YGC, YGCT:表示从应用程序启动到采样时发生的YGC次数及其总耗时。频繁的YGC可能表示Eden区太小或对象分配速率过高。
  • FGC, FGCT‌:表示从应用程序启动到采样时发生的FullGC次数及其总耗时。频繁或耗时长的FullGC可能表示老年代不足或存在内存泄漏。
  • GCT‌:表示从应用程序启动到采样时用于垃圾回收的总时间。
  • LGCC, GCC:表示最后一次GC的原因和导致上次GC发生的原因。例如Allocation Failure表明本次引起GC的原因,是因为在年轻代中没有足够的空间能存储新的数据了,表示由于内存分配失败导致的GCNo GC表示没有发生GC
列名含义
S0Survivor 0区已使用空间的百分比
S1Survivor 1区已使用空间的百分比
EEden区已使用空间的百分比
OOld区已使用空间的百分比
MMetaspace区已使用空间的百分比
CCS压缩类空间已使用空间的百分比
YGC从应用程序启动到采样时发生YGC的次数
YGCT从应用程序启动到采样时YGC所用的时间(单位秒)
FGC从应用程序启动到采样时发生FullGC的次数
FGCT从应用程序启动到采样时FullGC所用的时间(单位秒)
GCT从应用程序启动到采样时用于垃圾回收的总时间(单位秒)
LGCC最后一次GC的原因
GCC导致上次GC发生的原因

Tips

  • 监控GC原因‌:通过-gccause选项,可以查看每次GC是由什么原因触发的,例如是YGC还是FullGC,以及具体的触发原因。
  • 深入分析GC行为‌:结合GC原因,可以更深入地分析GC行为是否正常,是否存在频繁的FullGC等问题。
  • 性能瓶颈识别‌:如果发现某些特定原因频繁触发GC,可以针对性地优化代码或调整JVM参数。
  • 持续监控‌:设置3000毫秒的间隔,可以持续观察JVM的运行状态,及时发现GC相关的问题。

接下来看下YGCFullGC的频率,下面有个GC优化的参考指标,我们从这个参考指标不是看怎么做GC优化,而是看在合理情况下的GC频率。

频率需要看进程的启动的时间来判断,除以它的次数

  • YGC / YGCT = 单次YGC时间
  • FGC / FGCT = 单次FGC时间

Java诊断工具合集

Java诊断工具可以帮助你监控、分析和排查Java应用程序的问题。这里不做详细参数介绍,仅列举出来做抛砖引玉之效。

工具详细描述
jpsjps,(Java Virtual Machine Process Status Tool)是JDK自带的一个轻量级命令行工具。可列出当前系统中运行的Java进程信息,但只能列出由当前用户启动的Java进程(root用户可查看所有)。
jstack可生成Java进程的线程堆栈信息,分析线程状态、定位死锁、查找线程阻塞等问题。
jmap可生成Java进程的内存映射信息或堆转储快照(heap dump),查看堆内存的使用情况、分析内存泄漏等。
jstat监控Java虚拟机的统计信息,如垃圾回收、内存使用等,实时监控JVM的运行状态,分析性能瓶颈。
jinfo可查看或修改正在运行的Java进程的JVM参数和系统属性,比如查看JVM启动参数、动态修改部分JVM参数。
jhat可分析堆转储文件(heap dump),比如分析堆内存中的对象、查找内存泄漏。
jconsoleJava监视与管理控制台,提供图形化界面来监控JVM,用于查看内存、线程、类加载等信息。
VisualVM功能强大的多合一工具,用于监控、分析和调试Java应用程序,提供图形界面,比如支持内存分析、线程分析、CPU分析等。使用方式简单,启动VisualVM并连接到目标Java进程。
MATMemory Analyzer Tool,是一款功能强大的JVM堆内存分析工具,主要用于诊断内存泄漏、分析内存溢出(OOM)问题以及优化Java应用的内存使用‌。通过分析Java进程生成的‌Heap Dump(堆转储)文件,帮助开发者深入理解JVM在某一时刻的内存状态。
Arthas阿里巴巴开源的Java诊断工具,支持动态追踪、方法级监控、类加载分析等。在不重启应用的情况下进行诊断,适用于线上问题排查。

这些工具各有侧重,通常会结合使用以全面诊断Java应用程序的问题。例如,jps用于识别进程,jstack用于分析线程,jmap用于分析内存,jstat用于监控JVM统计信息,而jconsoleVisualVM则提供图形化界面以辅助分析。

参考资料

Java Language and Virtual Machine Specifications

The Java® Virtual Machine Specification, Java SE 8 Edition

End🌈🌈🌈

本文由作者按照 CC BY 4.0 进行授权

© ManShouyuan. 保留部分权利。

本站总访问量 本站访客数人次

🚩🚩🚩🚩🚩🚩