两种垃圾回收策略
引用计数法
通过四种引用判断实例是否需要被回收。
强引用:典型如
Object o = new Object()
,只要存在,垃圾收集器就永远不会回收掉被引用的对象。软引用:有用,但非必要的对象。只被软引用关联的对象,在系统将要发生内存溢出时,会把这些对象列入回收范围进行二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
弱引用:非必要对象,强度比软引用更弱,只有弱引用关联的对象只能生存到下一次垃圾回收,然后会被直接回收掉。
虚引用:幽灵引用/幻影引用,被虚引用的对象只是会在回收时收到一个系统通知。
四种引用的代码实现如下:
// 强引用
String s = “强引用”;
// 软引用
SoftReference<String> soft = new SoftReference<>(“软引用”);
// 弱引用
WeakReference<String> weak = new WeakReference<>("弱引用");
//虚引用
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<String> strs = new PhantomReference<>("虚引用 ", queue);
可达性算法
从GC Roots开始向下搜索,走过的路程称为引用链。当一个对象到GC Roots没有任何引用链相连,即不可达。
固定可作为GC Roots的对象包括:
虚拟机栈、本地方法栈的本地变量表中引用的对象
方法区中类静态属性引用的对象、字符串常量池里的引用
Java虚拟机内部的引用,如基本数据类型对应的Class对象、常驻的异常对象例如NullPointException、OutOfMemoryError等,还有系统类加载器
被同步锁synchronized关键字持有的对象
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
不可达不代表一定要死,可以重写Object#finalize
方法进行自救。但自救行为仅仅只能被JVM触发一次。这个方法是一个不推荐的方法。
例如JDK1.8的ThreadPoolExecutor中实现了Object#finalize
调用shutdown,执行完当前任务就不再接收新的任务。如果在线程轮询任务期间,被判定为不可达的,就会进行垃圾回收,从而执行finalize方法,引发线程不能再执行任务的bug。
对于这种场景,jdk9以后已经取消了finalize的使用,改为try final,重新调用建立连接的方法。
方法区中是常量、类信息,虽然号称永久代或元数据,但是其实也有回收行为,只不过触发非常严格。例如类的回收要求他一个实例都没有,且类加载器已经被回收,且该类对象的对象没有任何引用,也不能反射调用。
Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0
垃圾收集算法
垃圾收集的基石 - 分代收集理论
分代收集理论有两个假说:弱分代假说 (绝大多数对象都是朝生夕灭的);强分代假说(熬过越多次垃圾收集的对象就越难以消亡)
基于这种理论,普遍会把内存分区,一部分是存放朝生夕灭的对象,另一部分是存放难以消亡的对象。在JAVA虚拟机中即新生代(Young Generation)和老年代(Old Generation),同样对应也有了Minor GC、Major GC、Full GC分别对应新生代、老年代、整改JAVA堆和方法区的GC。(还有Mixed GC混合收集)
对象是会跨代引用的,例如新生代被老年代引用,老年代不消亡,新生代可能也不会消亡。因此会使新生代向老年代晋升。同时,为了减少因为跨代而对老年代的扫描,往往采用在新生代上建立一个全局的数据结构(记忆集),标志出老年代的哪一块内存会存在跨代引用。当发生Minor GC时,只有包含了跨代引用的老年代才会被加入到GC Root中扫描。
几种常见的垃圾回收算法
步骤:
先找到GC roots根来遍历将非垃圾对象进行标记。
将垃圾进行清除,就是图中的情况。
注意:jvm并不是真正的把垃圾对象进行了遍历,把内部的数据都删除了,不是这样的,而是把垃圾对象的首地址和尾地址进行了保存,等到再次分配内存时,直接去地址列表中分配,所以清除的效率高。
优点:清除速度快,效率高。
缺点:
会产生大量的内存碎片(就是很多不连续的内存空间),如果放入一个大的数组的时候,没有连续的内存放置大的数组,就会出现内存溢出,但是所有的内存碎片加起来可以放置一个大的对象,所有说内存使用率低,造成内存不连续
执行效率不稳定,对象越多,需要标记清除的动作就越多,效率低
标记-复制算法
将内存划分为两块,每次只使用一块,当这一块用完了,就把存活对象全都复制到另一块,然后回收用完的一整块。
简单,但需要两倍的内存空间,空间浪费较多。
后来JDK1.8的Eden、Survivor区就是结合这个思想,即把新生代分为一块较大的Eden空间何两块较小的Survivor空间,每次分配内存使用一块Eden和其中一块Survivor。发生垃圾收集后,将Eden和Survivor中仍存活的对象一次性复制到另外的Survivor上,然后直接清理Eden和之前那个Survivor空间。默认Eden和Survivor大小是8比1
标记-整理算法
首先标记可回收对象,再将存活对象都向一端移动,然后清理掉边界以外的内存。移动本身就是一个危险的负重操作,需要全部暂停用户应用程序才能进行。相比标记清除算法,移动会使垃圾回收吞吐量更划算,不移动会使内存分配合访问更划算且更少或不会停顿。
因此关注吞吐量的Parallel Scavenge收集器是基于标记整理算法的,而关注延迟的CMS收集器则是基于标记清除算法的。
垃圾回收算法实现细节
以下是几种优化垃圾回收算法的细节手段:
OopMap:通过GC Root标记垃圾非常耗时,且会STW停顿,即使是CMS、G1、ZGC等收集器也逃不开在这个步骤停顿。为了尽量减少停顿时长,HotSpot虚拟机通过一个kv映射存储某个内存区域中对于的指针引用,方便快速完成标记,这个就是叫OopMap
安全点:程序运行时,OopMap肯定就会变化,且每个指令都存储在OopMap的话,开销肯定很大。因此因此增加了一个安全点的概念,在这个点上,才会为线程记录OopMap,线程可以停下来执行垃圾回收。安全点的选取不能太少以至于垃圾回收器长时间无法回收,也不能太多导致程序卡壳。选取标准是“能让程序长时间执行”
一般来说,方法调用、循环跳转、异常跳转才倾向于生成安全点
主动式中断:当系统要进行垃圾回收时,通过给线程设置一个标志位让它不断轮询,一旦发现条件成立,线程就会运行到最近的安全点主动挂起
安全区域:如果线程处于sleep、或wait状态,就没办法运行到安全点挂起了。因此引入了安全区域的概念,即在这个代码片段内,引用不发生变化。一旦线程进入安全区域,会标识自己进入安全区域。如果这期间发生垃圾回收,虚拟机就不管这些线程了。如果线程要离开安全区域,会先看虚拟机是否已经完成了GC Root,如果完成了,就正常运行,否则要等待虚拟机的信号才能离开安全区域
记忆集和卡表:为了解决跨代垃圾回收的性能压力。在新生代中建立了记忆集(Remember Set)的数据结构,用于存储非收集区域指向手机区域的指针集合。记忆集有以下几个记录粒度:
子长精度:每个记录精确到一个机器字长
对象精度:每个记录精确到一个对象
卡精度:每个记录精确到一块内存区域。该种精度就是卡表,即记忆集的一种实现
写屏障:写屏障类似于AOP,它的切入点是引用类型字段赋值的时候,即赋值的时候默认更新卡表。它是为了解决卡表的更新问题,即赋值后卡表就过期了的问题。写屏障包括写前屏障和写后屏障。这里的写屏障要与后面的内存屏障区分
并发的可达性分析:GC Root分析可以理解为从根节点开始,逐渐把所有的节点从“未分析”变成“已分析”的过程。如果程序是停止的,这种变化就不会有问题,如果程序还没停止,即收集器和用户线程并发,这种变化就可能出问题。假设有三类节点:未扫描,已扫描,半扫描(节点已扫描过,但至少还有一个未扫描的引用关联它)主要是以下场景:
赋值器插入了一条或多条从已扫描对象到未扫描对象的引用:通过增量更新解决。即有新插入,先记录一下,等并发扫描结束,再把记录对应的节点扫一遍
赋值器删除了全部从半扫描到未扫描对象的直接或间接引用:通过原始快照解决。即当要删除半扫描对象的引用关系时,就先把这些要删除的记录下来,并发扫描结束后,以这些记录涉及的半扫描对象为Root,再扫一次
经典垃圾收集器
经典垃圾收集器举例
其中可以看出,根据代际差异,垃圾收集器有:
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
跨代收集器:G1
其中,跨代收集器G1是兼顾新生代和老年代的,不需要与其他收集器搭配,而其他分代收集器则是需要新老年代搭配使用,例如经典的CMS+ParNew组合
Serial收集器+Serial Old收集器
单线程的新老年代收集器组合,基于整理-标记算法
垃圾收集时必须STOP THE WORLD,导致恶劣的体验
但是它的好处在于开销最小,对于低性能系统,可能只能使用这种低消耗的收集器
ParNew收集器
是Serial收集器的多线程并行版本,收集新生代,往往与CMS搭配使用,通过参数-XX: +/-UseParNewGC
控制启用
从JDK9开始,-XX: +/-UseParNewGC
参数不再可用,且ParNew只能搭配CMS,也不再被官方支持
ParNew的并发收集能力并不一定比Serial强,尤其是在一些伪双核机器上,甚至还要承担额外的线程转换开销
参数-XX:ParallelGCThreads
可用用于限制垃圾收集线程数
Parallel Scavenge收集器+Parallel Old收集器
基于标记-复制算法的收集器,GC时发生stop-the-world,使用多个GC线程。 吞吐量优先收集器
吞吐量优先的概念是,吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间, 而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,因此在注重吞吐量或者处理器资源较为稀缺的场合, 都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合
CMS收集器
JDK8以前非常推荐的一个老年代收集器,也是非常经典常用的收集器
它的收集过程主要分为:
初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些, 但也远比并发标记阶段的时间短
并发清除:清理删除掉标记阶段判断的已经死亡的对象, 由于不需要移动存活对象, 所以这个阶段也是可以与用户线程同时并发的
四个步骤,如图所示:
其中,初始标记、重新标记两个步骤需要Stop The World
CMS的问题在于:
CMS 收集器对处理器资源非常敏感。事实上,面向并发设计的程序都对处理器资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低总吞吐量。
由于CMS收集器无法处理“浮动垃圾”, 有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生,空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间, 但就是无法找到足够大的连续空间来分配当前对象, 而不得不提前触发一次Full GC的情况。
CMS的默认处理器数量是(核心数+3)/4,即核数越多,CMS占用性能越少,但核数不足4个时,性能压力就很大
Garbage First - G1收集器
G1的分区逻辑
jdk8及以后推荐使用G1收集器代替CMS
虽然G1也是基于分代理论的,但是G1的目标不再是局限于收集新生代/老年代,而是将内存整体划分为大小相等独立的区域(Region),每个区域根据需要,扮演Eden空间、Survivor空间,每个区域的大小可以通过参数-XX:G1HeapRegionSize
决定,取值范围为1~32MB,且必须为2的N次方
除了这两个空间以外,G1还针对大对象提供了Humongous区域。大对象指的是超过区域容量一半的对象。如果大对象甚至超过了整个区域大小,则会被存放在N个连续的Humongous区域中。Humongous区域也被视为老年代区域。
G1收集器的原理
区域Region是G1回收器单次回收的最小回收单元。每个Region都有其回收的“价值”,即回收所获得的空间与回收所需时间的比值
G1维护一个优先级列表,记录每个Region的回收价值,再根据用户主动设置的收集停顿时间(-XX:MaxGCPauseMillis
参数设置,默认200ms),优先回收那些回收收益最大的Region
以下是几个G1的实现细节:
双向卡表:G1也记忆集减少GC Root扫描的耗时,但由于Region的划分,G1通过双向卡表实现记忆集,双向指的是记录我指向谁+谁指向我。因此G1比其他传统垃圾收集对内存的占用更高,约需要Java堆容量的10%~20%的额外内存。这一点在实际开发由CMS切换到G1的过程也可以看出来,堆内存占用是上升的
原始快照(SATB):在并发标记阶段,CMS通过增量更新算法解决并发问题,G1则是使用了原始快照的方法。G1为每个Region设置两个TAMS指针,在并发回收过程中,新对象必须在这两个指针指向地址之上。这时G1认为这些新分配对象是存活的,不需要回收的。但这时可能产生的问题就是,收集太慢,分配太快,就有可能导致回收赶不上分配,产生FullGC,不得不Stop The World
衰减均值模型:用户可以通过
-XX:MaxGCPauseMillis
参数设置停顿时间,但是G1不可能严格按照这个时间回收。实际回收时,G1惠济路每个Region的回收耗时,计算平均耗时和成本,使得每次回收平均耗时最接近用户设置值。因此样板越新,越能够在接近用户设置时间的基础上实现最高收益
G1的具体收集流程如下:
初始标记:仅标记下GC Root直接关联的对象,并且修改TAMS指针值,从而在下个并发标记阶段可以正常分配新对象。这个阶段需要Stop The World,但耗时很短,而且是借用Minor GC同步完成的
并发标记:从GC Root做堆对象的完整的可达性分析,耗时较长,分析完后还要处理下原始快照记录下的并发时有引用变动的对象。这个阶段是与用户线程并行的
最终标记:处理并发标记阶段遗留的少量SATB,需要Stop The World,耗时短
筛选回收:更新Region的统计数据,对各个Region回收价值和成本进行排序,根据用户设置的停顿时间更新回收计划,然后把存活对象复制到空Region,此时需要Stop The World,由多条线程并发完成
看到筛选回收阶段复制存活对象的过程,可知G1是基于标记-整理算法设计的
G1调优
因为G1是考验基于用户期望指定暂停时长的,这是G1的优势所在。但是暂停时长不能随意设置,太低可能导致每次垃圾回收不全,进而使垃圾逐渐积累,最终导致FullGC。
因此G1调优的目标就是调整垃圾回收停顿时长和回收次数的平衡,因为:
回收太频繁,更多的新生代就有可能熬过多次回收,晋升到老年代,导致以后也很难回收掉
回收暂停时长太短,回收不彻底,也可能导致新生代熬过多次回收,还会增加FullGC的可能性
通过调整-XX:G1NewSizePercent
参数可以起到调整Eden区、Survivor区比例的效果,当新生代比例变大,触发回收的时长就更慢一些,再结合-XX:MaxGCPauseMillis
可以让每次回收更彻底
一个典型案例如下:
在这种离线峰谷流量,峰值时业务快进快出的场景,反而不适合高频回收,而是低频,较长回收,使新生代避免向老年代晋升,同时避免FullGC出现
G1抽样线程
G1常说是空间换时间,抽样线程就是它用空间换时间的方案。在线程中看到G1RefineThread相关的就是G1的抽样线程。抽样线程有两个作用:
处理新生代分区的抽样,并且在满足响应时间的这个指标下,更新YHR的数目
管理RSet,即引用关系,G1把所有的引用关系都放到一个队列中(DCQ),G1是分region的,DCQ就不止有一个,都存到一个set中即DCQS
为了处理这些引用关系,G1把线程分成了GC线程、refine线程,并且给了三个阈值划分了4个区:
白区:[0, Green),在该区中只有GC线程处理DCQ
绿区:[Green, Yellow),在该区中会依据queue set数值大小启动不同数量的Refine线程处理DCQ
黄区:[Yellow, Red),在该区中所有refine线程启动处理DCQ
红区:[Red, ),在该区中业务线程也参与进来处理DCQ
下面这三个阈值可以通过参数设置,也可以不设置,默认值为0,但G1会根据回收时间动态调整,即refine时间超过预期,变为1.1倍,反之变为0.9倍。
-XX:G1ConcRefinementGreenZone
-XX:G1ConcRefinementYellowZone
-XX:G1ConcRefinementRedZone
在绿区中,refine线程的数量可以通过参数指定同时也具有动态调整能力,一般是CPU核数的5/8,这些线程都是处于冻结状态的,被逐渐唤醒:refine#0是被业务线程唤醒的,其他refine#n线程是被refine#n-1线程唤醒的,唤醒的条件是queue set数量达到了一个阈值,这个阈值也是动态调整的
抽样线程相关日志输出参数可以参考如下链接:
G1的常规日志分析
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:xxx/log/gc-xxx.log
当使用如上三个参数时,G1可以输出一些常规日志
1 2024-06-06T14:25:15.607+0800: 1358.779: [GC pause (G1 Evacuation Pause) (young), 0.1006389 secs]
2 [Parallel Time: 45.6 ms, GC Workers: 38]
3 [GC Worker Start (ms): Min: 4053175.2, Avg: 4053184.8, Max: 4053215.7, Diff: 40.5]
4 [Ext Root Scanning (ms): Min: 0.0, Avg: 1.2, Max: 8.6, Diff: 8.6, Sum: 47.1]
5 [Update RS (ms): Min: 0.0, Avg: 8.4, Max: 41.3, Diff: 41.3, Sum: 317.3]
6 [Processed Buffers: Min: 0, Avg: 13.3, Max: 39, Diff: 39, Sum: 505]
7 [Scan RS (ms): Min: 0.0, Avg: 0.2, Max: 0.4, Diff: 0.4, Sum: 7.7]
8 [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.3]
9 [Object Copy (ms): Min: 0.0, Avg: 20.6, Max: 32.6, Diff: 32.5, Sum: 783.9]
10 [Termination (ms): Min: 0.0, Avg: 4.5, Max: 5.2, Diff: 5.2, Sum: 171.5]
11 [GC Worker Other (ms): Min: 0.0, Avg: 0.2, Max: 0.6, Diff: 0.6, Sum: 5.9]
12 [GC Worker Total (ms): Min: 4.1, Avg: 35.1, Max: 44.6, Diff: 40.5, Sum: 1333.8]
13 [GC Worker End (ms): Min: 4053219.7, Avg: 4053219.9, Max: 4053220.4, Diff: 0.8]
14 [Code Root Fixup: 0.3 ms]
15 [Code Root Purge: 0.0 ms]
16 [Clear CT: 1.5 ms]
17 [Other: 53.2 ms]
18 [Choose CSet: 0.0 ms]
19 [Ref Proc: 39.2 ms]
20 [Ref Enq: 8.6 ms]
21 [Redirty Cards: 1.3 ms]
22 [Humongous Reclaim: 0.0 ms]
23 [Free CSet: 2.0 ms]
24 [Eden: 4708.0M(4708.0M)->0.0B(4724.0M) Survivors: 204.0M->188.0M Heap: 5528.0M(8192.0M)->804.9M(8192.0M)]
25 [Times: user=1.37 sys=0.02, real=0.10 secs]
1行是输出时间、停顿类型,执行了youngGC,花了0.1s
疏散停顿(G1 Evacuation Pause)是将活着的对象从一个区域(young or young + old)拷贝到另一个区域的阶段
GCLocker Initiated GC临界区GC:如果线程执行在 JNI 临界区时,刚好需要进行 GC,此时 GC Locker 将会阻止 GC 的发生,同时阻止其他线程进入 JNI 临界区,直到最后一个线程退出临界区时触发一次 GC
2行是GC中的Stop The World时间+GC线程数
3-13行是GC中各个步骤的耗时,其中:
Ext Root Scanning:扫描root集合花费时间
Update RS:更新RSet花费时间
Processed Buffers:表示在Update RS过程中处理多少日志缓冲区
Scan RS:扫描每个新生代分区的RSet花费时间
17行Others是一些其他耗时,其中:
Ref Proc:寻找处理引用的耗时,跟Update RS时间一般相关
Ref Enq:遍历所有的引用,将不能回收的放入pending列表
Humongous Register:巨对象相关
24行是回收结果,即Eden区满触发了young gc,全部回收掉,survivors区也回收了部分,堆内存总量发生了变化
25行是耗时,其中user是所有线程总CPU耗时,sys是内核态线程的CPU耗时,real是真实耗时
G1的refine日志分析
-XX:+UnlockDiagnosticVMOptions
-XX:+G1TraceConcRefinement
-XX:+G1SummarizeRSetStats
-XX:G1SummarizeRSetStatsPeriod=2
补充这四个参数可以输出每次gc的refine日志,其中第三个参数代表几次refine输出一次日志
Current rem set statistics
Total per region rem sets sizes = 90437K. Max = 1875K.
3113K ( 3.4%) by 163 Young regions
138K ( 0.2%) by 13 Humonguous regions
7902K ( 8.7%) by 740 Free regions
79282K ( 87.7%) by 723 Old regions
比较核心的日志是可以看到DCQ的内存占比,分别是年轻代的DCQ占比、巨对象的占比、老年代的占比等
选择合适的垃圾收集器和调优经验
如果缺少调优经验,可以选择解决方案式的Vega收集器和ZingVM,使用C4收集器
如果预算不足,但期望使用新版本,可用使用ZGC收集器(G1的低延迟版)
如果无法考虑试验阶段的收集器,或必须在windows操作系统下,可用使用Shenandoah收集器
如果JDK版本较落后,对于4~6G以下的堆内存,CMS一般较好,更大的堆内存适合用G1
看一些使用和调优的案例:
CMS+ParNew使用和调优
老版本的回收器都是单年代的,大部分老java版本系统会选择CMS+ParNew:
ParNew:新生代多线程并行收集器,相比serial单线程,效率高,回收算法是一致的。默认情况下ParNew给自己的线程数量就与CPU核数一致,也可以通过-XX:ParallelGCThreads设置,但超过CPU核数会导致上下文频繁切换,有开销效率
CMS:老年代垃圾回收,标记清理算法,会产生碎片,而且老年代是堆内存主要回收的对象,因此调优的重点一般在CMS上
该组合的一些场景如下:
CMS回收期间,进入老年代对象已经大于可用内存空间
如果出现这种场景,会发生Concurrent Mode Failure,系统强制使用Serial Old代替CMS,强制进行stop the world,重新进行长时间的GC Roots追 踪,标记出来全部垃圾对象,不允许新的对象产生 然后一次性把垃圾对象都回收掉,完事儿了再恢复系统线程。
调优方案:
使用-XX:CMSInitiatingOccupancyFaction
参数,设置老年代占用多少比例的时候触发CMS垃圾回收,老年代占用了92%空间了,就自动进行CMS垃圾回收,预留8%的空间给并发回收期间,系统程序把一些新对象放入老年代中
CMS产生的内存碎片问题
CMS采用标记-清理算法,会产生内存碎片,导致没有连续的内存空间给新的老年代用,就会强制触发FullGC,高频率的FullGC会导致stop the world更频繁。
调优方案:
使用-XX:+UseCMSCompactAtFullCollection
参数可以设置FullGC后是否再次Stop the World进行碎片整理,默认就打开,会把存活对象挪到一起,空出来大片 连续内存空间,避免内存碎片
使用-XX:CMSFullGCsBeforeCompaction
参数,这个意思是执行多少次Full GC之后再执行一次内存碎片整理的工作,默认是0,意思就是每次Full GC之后都会进行一次内存整理
4c8g服务的模板参数
-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:PermSize=256M
-XX:MaxPermSize=256M -XX:+UseParNewGC
-XX: + UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92
-XX: + UseCMSCompactAtFullCollection -
XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSParallellnitialMarkEnabled -XX:+CMSScavengeBeforeRemark
G1回收器使用和调优
-XX:+UseG1GC 使用G1垃圾回收器
-XX:MaxGCPauseMillis:暂停时间,默认值200ms
新生代和老年代比例
如果G1达不到要求的MaxGCPauseMillis,就会相应调整,对应YoungGC,会减少Eden区个数,即缩小新生代空间,对于MixedGC,会减少每次选择的Cset,但是会增加MixedGC次数,对吞吐量产生影响。用以下参数调整新生代和老年代比例:
-XX:G1NewSizePercent -XX:G1MaxNewSizePercent
调整新生代占比,大的Eden空间也会减少Young GC的次数,但是同样老生代就会减少,可能会引发FullGC
并行线程数
-XX:ParallelGCThreads
SWT阶段的并行线程数,一般不用指定
-XX:ConcGCThreads
非SWT阶段的并行线程数,以8核CPU为例,指定为2,则只有6个业务线程能同时进行,调太大也会影响业务吞吐量
Mixed GC频率
-XX:G1HeapWastePercent
触发Mixed GC的堆垃圾占比,默认值5%
可以适当降低MixedGC频率
G1的通用配置模板
-XX:MaxPermSize=512m-Djava.net.preferIPv4Stack=true -Dfile.encoding=UTF-8
-Dsun.jnu.encoding=UTF-8 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC
-XX:+PrintGCDateStamps
-Xloggc:/opt/wildfly/standalone/log/verbose.gc -XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/wildfly/standalone/log/ -Djboss.modules.system.pkgs=org.jboss.byteman
-Djava.awt.headless=true -Dcom.ibm.mq.cfg.TCP.Connect_Timeout=20 -XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=20M -Xms4096m -Xmx4096m -XX:+UseG1GC
G1的调优实际案例
案例1:峰谷流量调优
附录一、常用垃圾收集器参数
JDK8常用参数
-XX:+PrintGC:查看GC基本信息
-XX:+PrintGCDetails:查看GC详细信息
-XX:+PrintHeapAtGC:查看GC前后堆、方法区可用容量变化
-XX:+PrintGCApplicationConcurrentTime
-XX:+PrintGCApplicationStoppedTime:查看GC过程中用户现场并发时间及停顿时间
-XX:+PrintAdaptiveSizePolicy:查看收集器自动设置堆空间各分代区域大小、收集目标等
-XX:+PrintTenuringDistribution:查看熬过手机后剩余对象的年龄分布信息
-XX:+PrintGCTimestamps:输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDatestamps:输出GC的时间戳(以日期的形式
通用配置
-Xms 初始堆大小,默认物理内存的1/64 -Xms512M
-Xmx 最大堆大小,默认物理内存的1/4 -Xms2G
-Xmn 新生代内存大小,官方推荐为整个堆的3/8 -Xmn512M
-XX:NewRatio=n 设置新生代和年老代的比值。如: 3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 -XX:NewRatio=3
-XX:SurvivorRatio=n 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如: 8,表示Eden:Survivor=8:1:1,一个Survivor区占整个年轻代的1/10 -XX:SurvivorRatio=8
-Xss 线程堆栈大小,jdk1.5及之后默认1M,之前默认256k -Xss512k
-XX:PermSize=n 永久代初始值,默认为物理内存的1/64 -XX:PermSize=128M
-XX:MaxPermSize=n 永久代最大值,默认为物理内存的1/4 -XX:MaxPermSize=256M
-verbose:class 在控制台打印类加载信息
-verbose:gc 在控制台打印垃圾回收日志
-XX:+PrintGC 打印GC日志,内容简单
-XX:+PrintGCDetails 打印GC日志,内容详细
-XX:+PrintGCDateStamps 在GC日志中添加时间戳
-Xloggc:filename 指定gc日志路径 -Xloggc:/data/jvm/gc.log
-XX:+DisableExplicitGC 关闭System.gc()
-XX:+UseBiasedLocking 自旋锁机制的性能改善
-XX:PretenureSizeThreshold 对象超过多大是直接在旧生代分配,默认值 0 ,单位字节
-XX:TLABWasteTargetPercent TLAB 占eden区的百分比 默认值 1%
-XX:+CollectGen0First fullGC 时是否先 youngGC 默认值 false
-XX:+PrintHeapAtGC 打印 GC 前后的详细堆栈信息
-XX:ParallelGCThreads=n 设置并行收集器时使用的CPU数。此值最好配置与处理器数目相等,同样适用于CMS -XX:ParallelGCThreads=4
年轻代配置
-XX:+UseSerialGC 年轻代设置串行收集器Serial
-XX:+UseParallelGC 年轻代设置并行收集器Parallel Scavenge
-XX:UseParNewGC 启用ParNew收集器
-XX:MaxTenuringThreshold 几次 youngGC 后会被分到老年代,默认是15次
-XX:MaxGCPauseMillis=n 年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值
G1配置
-XX:+UseG1GC 使用 G1 (Garbage First) 垃圾收集器
-XX:InitiatingHeapOccupancyPercent 老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能 就要触发MixedGC了
-XX:MaxGCPauseMillis 目标暂停时间(默认200ms) 也就是垃圾回收的时候允许停顿的时间
-XX:G1MixedGCCountTarget 在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一 会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长
-XX:G1HeapWastePercent (默认5%) 在混合回收时,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立 即停止混合回收,意味着本次混合回收就结束了
-XX:ConcGCThreads=n 并发垃圾收集器使用的线程数量. 默认值随JVM运行的平台不同而不同
附录二、垃圾收集器日志解析
开启 GC 日志分析 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps
G1-youngGC
[GC pause (G1 Evacuation Pause) (young), 0.0026083 secs]
[Parallel Time: 1.2 ms, GC Workers: 10]
[GC Worker Start (ms): Min: 308.9, Avg: 308.9, Max: 309.2, Diff: 0.3]
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.3, Max: 0.6, Diff: 0.6, Sum: 2.6]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
[Object Copy (ms): Min: 0.5, Avg: 0.8, Max: 1.0, Diff: 0.5, Sum: 8.2]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 11.8, Max: 18, Diff: 17, Sum: 118]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.0, Sum: 0.3]
[GC Worker Total (ms): Min: 0.9, Avg: 1.1, Max: 1.2, Diff: 0.3, Sum: 11.2]
[GC Worker End (ms): Min: 310.1, Avg: 310.1, Max: 310.1, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 1.3 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 1.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 6144.0K(6144.0K)->0.0B(5120.0K) Survivors: 0.0B->1024.0K Heap: 6144.0K(10.0M)->1625.5K(10.0M)]
[Times: user=0.16 sys=0.00, real=0.00 secs]
23:46:36.458 [main] INFO com.jzj.jvmtest.jvmready.G1Test - ======== 1次添加3M对象
23:46:36.575 [main] INFO com.jzj.jvmtest.jvmready.G1Test - ======== 2次添加3M对象
===================================================================================
===================== 下面是 并发标记 周期 initial-mark ===========================
===================================================================================
日志解读如下:
GC pause (G1 Evacuation Pause) (young), 0.0026083 secs
表示G1转移暂停,YoungGC, 持续时间 0.0026083 secs,系统暂定 0.0026083 secs
Parallel Time: 1.2 ms, GC Workers: 10
并行开始收集任务,一共由10个GC线程,从新生代垃圾收集到最后一个任务结束一共耗时1.2ms,该过程STW
GC Worker Start (ms): Min: 308.9, Avg: 308.9, Max: 309.2, Diff: 0.3
GC开始工作, min指的是第一个垃圾收集线程开始工作时,JVM启动后经过的时间, max指的时最后一个垃圾收集器开始工作时,JVM启动后经过的时间,diff指的是min和max的差值,avg表示平均值,理想情况下,我们希望多个线程是同时开始的,即最好的情况是diff=0
Ext Root Scanning (ms): Min: 0.0, Avg: 0.3, Max: 0.6, Diff: 0.6, Sum: 2.6
根扫描的耗时时间,每一个GC线程在处理根扫描(全局变量,系统数据字典,线程栈等),所花费的时间,min 最小时间,max最大时间,Sum表示所有线程花费的总时间,尝试找到所有root集合中的节点指向当前的收集集合(CSet)
Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0
更新记忆集Rset的耗时时间,每个分区都有自己的Rset,用过来记录其他分区指向当前分区的指针,如果Rset有更新,新的引用的card卡表会被标记为dirty,放入日志缓冲区,Update RS 表示允许垃圾处理GC线程处理本次垃圾收集前,没有处理好的日志缓冲区,这样可以证当前分区的RSet是最新的
Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
日志缓冲区处理时间,就是Update RS上一步的过程中处理了多少个日志缓冲区
Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
扫描每个新生代分区的RSet耗时,找出有多少指向当前分区的引用来自CSet
Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
Code Root 表示代码中的局部变量的Root节点,code root指经过JIT编译后的代码里,引用了heap中的对象,引用关系保存在RSet中,该过程就是扫描这部分Root节点所花费的时间,为了减少扫描时间,这个阶段只扫描CSet中region的Reset来查看是否有被code root引用的关系。
Object Copy (ms): Min: 0.5, Avg: 0.8, Max: 1.0, Diff: 0.5, Sum: 8.2
疏散暂停时间,所有Cset的Region分区,全都要被疏散转移,需要拷贝CSet集合里面所有分区存活对象到新分区Survivor/Old区。所有存活的对象会被复制到thread-local GC allocated buffers(GCLABS)中,GCLABS在目标region中进行分配。
Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
当一个垃圾收集线程完成任务时,它就会进入一个临界区,并尝试帮助其他垃圾线程完成任务(steal outstanding tasks)偷其他线程的任务。min表示该垃圾收集线程什么时候尝试terminatie,max表示该垃圾收集回收线程什么时候真正terminated终止
Termination Attempts: Min: 1, Avg: 11.8, Max: 18, Diff: 17, Sum: 118]
如果一个垃圾收集线程成功偷取了其他线程的任务,那么它会再次偷取更多的任务,每次重新terminate的时候,这个数值就会增加。
GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.0, Sum: 0.3]
GC线程花费在其他工作上(上面都没计算在内的其他)的时间。
GC Worker Total (ms): Min: 0.9, Avg: 1.1, Max: 1.2, Diff: 0.3, Sum: 11.2]
汇总展示每个垃圾收集线程的最小、最大、平均、差值和总共时间。
GC Worker End (ms): Min: 310.1, Avg: 310.1, Max: 310.1, Diff: 0.0]
汇总线程结束时间。min表示最早结束的垃圾收集线程结束时该JVM启动后的时间;max表示最晚结束的垃圾收集线程结束时该JVM启动后的时间。理想情况下,最好是同一时间结束。
[Code Root Fixup: 0.0 ms]
用来将code root修正到正确的回收之后的对象位置所花费的时间。串行执行。
[Code Root Purge: 0.0 ms]
清理code root的数据结构。串行执行。
[Clear CT: 0.1 ms]
清理 Card Table的耗时, 串行执行。
[Other: 1.3 ms] 其他事项耗时。串行执行。
[Choose CSet: 0.0 ms] 选择要进行回收的分区放入CSet,选择回收分区的时
[Ref Proc: 1.1 ms] 处理Java中的各种引用如Soft/Weak 弱引用,软引用时间
[Ref Enq: 0.0 ms] 遍历所有的引用,将不能回收的放入pending队列ReferenceQueues,处理入队时间
[Redirty Cards: 0.1 ms] 在回收过程中被修改的Card将会被重置为dirty。
[Humongous Register: 0.0 ms] 巨型对象可以在新生代收集的时候被回收。
[Humongous Reclaim: 0.0 ms] 确保巨型对象可以被回收,释放该巨型对象所占的分区,重置分区类型,并将分区还到free-list列表,并且更新空闲空间大小。
[Free CSet: 0.0 ms] 释放CSet中的分区到空闲列表free-list。
Eden: 6144.0K(6144.0K)->0.0B(5120.0K) Survivors: 0.0B->1024.0K Heap: 6144.0K(10.0M)->1625.5K(10.0M)
Eden区GC回收前使用6144K(回收前总大小6144K)->回收后大小使用0.0B(Eden区回收后总大小5120K)
suivivors区GC回收前大小0.0B->回收后大小1024K
整堆HEAP回收前使用6144K(整堆大小10M)->回收后整堆使用1625.5K(总堆大小10M)
[Times: user=0.16 sys=0.00, real=0.00 secs]
user是用户态总共占用cpu时间(多线程并行执行算总和),sys是系统内核的总共占用cpu时间,real是wall clock time
G1-并发标记日志
[GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0013339 secs]
[Parallel Time: 1.0 ms, GC Workers: 10]
[GC Worker Start (ms): Min: 453.9, Avg: 454.0, Max: 454.0, Diff: 0.1]
[Ext Root Scanning (ms): Min: 0.2, Avg: 0.3, Max: 0.3, Diff: 0.1, Sum: 2.8]
[Update RS (ms): Min: 0.0, Avg: 0.1, Max: 0.5, Diff: 0.5, Sum: 0.9]
[Processed Buffers: Min: 0, Avg: 1.1, Max: 2, Diff: 2, Sum: 11]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
[Object Copy (ms): Min: 0.1, Avg: 0.4, Max: 0.5, Diff: 0.5, Sum: 4.2]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.4]
[Termination Attempts: Min: 1, Avg: 13.5, Max: 21, Diff: 20, Sum: 135]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 0.8, Avg: 0.8, Max: 0.9, Diff: 0.1, Sum: 8.4]
[GC Worker End (ms): Min: 454.8, Avg: 454.8, Max: 454.8, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.3 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.2 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 1024.0K(5120.0K)->0.0B(3072.0K) Survivors: 1024.0K->1024.0K Heap: 4584.0K(10.0M)->3766.6K(10.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[GC concurrent-root-region-scan-start]
[GC concurrent-root-region-scan-end, 0.0004062 secs]
[GC concurrent-mark-start]
[GC concurrent-mark-end, 0.0020466 secs]
[GC remark [Finalize Marking, 0.0001629 secs] [GC ref-proc, 0.0000789 secs] [Unloading, 0.0006677 secs], 0.0010284 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[GC cleanup 4790K->4790K(10M), 0.0005226 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0018078 secs]
GC暂停,暂停原因是大对象分配,在全局并发标记阶段
[Parallel Time: 1.2 ms, GC Workers: 10]
并行收集任务,此过程STW,耗时1.2ms,GC垃圾回收的线程数是10
[GC Worker Start (ms): Min: 445.7, Avg: 445.7, Max: 445.7, Diff: 0.1]
GC开始工作, min指的是第一个垃圾收集线程开始工作时,JVM启动后经过的时间, max指的时最后一个垃圾收集器开始工作时,JVM启动后经过的时间,diff指的是min和max的差值,avg表示平均值,理想情况下,我们希望多个线程是同时开始的,即最好的情况是diff=0
[Ext Root Scanning (ms): Min: 0.3, Avg: 0.3, Max: 0.4, Diff: 0.1, Sum: 3.1]
扫描的耗时时间,每一个GC线程在处理根扫描(全局变量,系统数据字典,线程栈等),所花费的时间
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.4]
更新记忆集Rset的耗时时间,通过更新记忆集Rset,保证当前分区的RSet是最新的
[Eden: 1024.0K(5120.0K)->0.0B(1024.0K) Survivors: 1024.0K->1024.0K Heap: 5596.7K(10.0M)->5843.5K(10.0M)]
最终结果Eden区GC回收前使用1024K(回收前总大小5120K)->回收后大小使用0.0B(Eden区回收后总大小1024K)
suivivors区GC回收前大小1024K->回收后大小1024K
整堆HEAP回收前使用5596.7K(整堆大小10M)->回收后整堆使用5843.5K(总堆大小10M)
[GC concurrent-root-region-scan-start]
initial mark结束后,就开始了跟扫描 root-region-scan, 当初始标记结束后,新生代收集也完成了对象的复制工作,存活的对象都进入了Survivor区,这部分进入Survivor的对象全部都会被标记为根,同时刚刚扫描的Surivivor区也被标记为根分区RootRegion,只有完成了该阶段,才能STW,进行下一次Ygc
[GC concurrent-root-region-scan-end, 0.0004062 secs]
根分区扫描结束
[GC concurrent-mark-start], 0.0008744 secs]
并发标记阶段开始,开始标记整个堆的存活对象,该阶段是可以与用户线程一起执行的,并发标记的线程数可以由参数-XX:ConcGCThreads可配置,默认线程数是GC线程工作的1/4
[GC concurrent-mark-end, 0.0020466 secs]
并发标记结束
[GC remark [Finalize Marking, 0.0001629 secs]
GC remark 重新标记阶段,该阶段STW, 会标记并发阶段变化的对象,并计算整个堆的垃圾情况,清空剩余的SATB缓冲区,此阶段所有存活的对象都会被标记
Finalize Marking 最终标记阶段
[GC ref-proc, 0.0000789 secs]
软引用,弱引用等引用处理
[Unloading, 0.0006677 secs], 0.0010284 secs]
卸载类信息
[GC cleanup 4790K->4790K(10M), 0.0005226 secs]
清理阶段,此阶段没有对象存活的老年代和巨型对象占用的分区都会被清理和释放出来,然后按照排序,选择最优,性价比最高的分区进行清理
参考文章
https://juejin.cn/post/7220782242255519803#heading-5
评论区