1. 垃圾定位算法
1.1 引用计数算法
主流jvm都不使用,因为无法检测引用闭环,导致垃圾清理不全。
1.2 根可达算法
判断一个对象是否为垃圾,依据该对象是否可以通过引用链链接到GC root。GC root包括:
- 系统类加载器或者启动类加载器加载的Class对象。自定义的类加载器加载的Class对象不一定是GC root,取决于自定义类加载器本身是否根可达
- 激活状态的线程对象
- 虚拟机栈(栈桢中的本地变量表)中的引用的对象
- 本地方法栈中JNI(native方法)引用的对象
- 方法区中的类静态属性引用的对象
2. 垃圾回收算法
垃圾回收算法包括:标记清理、复制、标记整理、分代收集。以下是一个形象的例子帮助理解这四种算法。
经常说GC,也就是垃圾回收,那么就得强调下垃圾回收发生的地方是一个在逻辑划分上被称为堆和方法区的地方。
当然,更多的是堆,所以猪仓库这里想模拟的是堆这个空间(不熟悉JMM的同学可以找资料复习下JMM中的五大区域)。
再次强调。
GC发生在JMM中的堆和方法区!
GC发生在JMM中的堆和方法区!
GC发生在JMM中的堆和方法区!
这里,猪仓库模拟的是堆空间,堆空间存放的是对象,一只猪就是java中的一个对象。
所以,想象一下,在一个仓库里,每只猪都在一个笼子里,整个仓库都是猪…
猪仓库会存在什么问题呢?仓库会不断的进货,会不断有新的猪进来,如果库存又没有减少,这个仓库的物理空间是固定的,即使你留有一定的备用仓库,那也不可能是无限空间,也就是说弹性也是一个有限制的弹性(堆空间也有一个初始化的空间值和最大阈值的限制,只有哆啦A梦的口袋是万能的)。仓库空间用完了,怎么办呢?直接就干不下去了嘛,这个时候你肯定首先是会想办法腾出一些空间来。你发现有些猪已经挂掉了,死的猪已经没用了(死的猪相当于堆中的”死”对象),可以把它们清理掉,腾出一些空间给新进来的猪。这个“清理死猪,腾出空间”给新来的猪的操作就是jvm中的GC操作。很多人会忽略触发GC的时机,也就是你什么时候会去做”清理死猪,腾出空间”这件事情?jvm中采用的是类似懒触发机制,因为做这件事情耗费时间和资源(也许你清理死猪的时候,新来的猪就得等待清理完才能进去了,类似GC所说的stop the world event),所以,一般情况下你不是发现有猪死了,就去做这件事情,而是发现空间已经不够用了,没办法了,才会去做这件事情。
那么如何做”清理死猪,腾出空间”这件事情呢?2.1 标记-清理
你发现可以分两步走。
第一步先将仓库里的所有猪检查一遍,死猪就在猪上画个红叉。
第二步就是将画有红叉的猪清理掉,腾出其原来占的地方。
注意这里并没有挪动还活着的猪,所以想象一下”清理死猪,腾出空间”后,整个仓库会存在一些零散的空位。
这个方法看起来还挺简单,但是不足的地方就是会存在”零散的空位”(也就是内存碎片,不连续的空间)。为什么”零散的空位”是不足呢?因为此时如果来了一头体积比之前清理掉的猪还大的猪,那些腾出位置没有一个能放的下它,这时怎么办呢,只能干不下去了,瘫痪了(jvm此时就只能抛OOM异常了)。
猪仓库空间满了,触发清理死猪,腾出空间操作,也就是触发GC
第一步,检查一遍猪仓库,标记死猪
第二步,将有标记的死猪“清理”
标记-清理算法的不足,存在内存碎片,来了一头特别大的猪,发现空位放不下它2.2 复制法
听说过生物克隆技术吗?你发现“标记-清除算法”存在”零散空位”的不足,然后”复制法”可以避免这个问题(实际中,我们当然不会为了空间的问题就搞生物克隆,现实中的生物克隆成本还是很高)。于是,你把仓库隔离成两个一样大小的空间,每次新来的猪只放在其中的一个隔离间,当这个隔离间空间不足的时候,你就开始”清理死猪,腾出空间”。你怎么做呢?你一边检查这个隔离间的猪,发现猪还活着,你就一边”克隆”这只猪到另外的那个隔离间。为什么用”一边…一边…”这种表述呢,因为这里并没有对猪进行单独的”画红叉”(标记)操作了,时间主要耗在”克隆”上。这种方法解决了”内存碎片”的问题,但是又有什么不足呢?
有两个明显的不足:
一个就是存活率高的情况下,”清理死猪,腾出空间“的时候,”克隆”操作要进行多次,这很有点耗时,当然前提是存活率高的情况下;
另一个就是空间利用率不高,每次都一个隔离间是空的。
复制算法,将原有空间划分为两个
存放猪的隔间满了,触发清理死猪,腾出空间操作,也就是触发GC
一边检查活猪,一边将其“克隆”到另外一个空到隔间,注意是“克隆”,不是移动
“克隆”完毕,清理掉原来存放猪的那个隔间
新来的猪存到现在存放猪的那个隔间
复制算法的不足,每次都有一个空的隔间导致空间利用率不高2.3 标记整理算法
你发现上面两种方法都有不足的地方,于是结合两者优缺点,决定分两步走。
第一步,和”标记-清理”的一样,将仓库所有猪检查一遍,死猪画上红叉。
第二步,除了清理打上红叉的死猪,还得将挪动所有活猪到仓库的一边。
这样子,”零散空位”的问题解决了,也不会存在空间利用率不高的问题。不足的应该就是”移动活猪”需要耗点时间。
第一步,同“标记-清理算法”,检查一遍仓库,标记死猪
第二步,清理带标记的死猪
第二步除了清理死猪,还得移动活猪到一端,解决“内存碎片”,注意,这里是移动不是“克隆”,也就是“整理”2.4 分代收集法
但是,哪里有十全十美的方法呢。适合自己的才是最好的!
仍然将仓库划分为不同的隔间,当然不一定要等比例了,找到一个最合适的比例就好,逻辑上分为年轻代的隔间,另一个为老年代的隔间。
你发现猪的寿命有长短,有些寿命很长,有些寿命很短。一开始新来的猪都放在年轻代的隔间,年轻代的隔间空间不足的时候,这时候就得”清理死猪,腾出空间”。
你总结规律,发现年轻代隔间的猪存活率很低,在这个前提下,你决定在年轻代隔间里采用”复制法”,因为存活率低,”克隆”次数并不会很多。所以,你在年轻代隔间里又划分了隔间(年轻代划分为了eden,survivor0,survivor1)。
在jvm中,把发生在年轻代的GC细分为了young GC或者minor GC。
那么老年代隔间做什么呢?有一天,你发现young GC后已经无法再年轻代腾出空间了,那么你就得把一些存活
很久的猪“克隆”到老年代隔间中。
总有一天,老年代隔间的空间也会不足,这时的GC就被细分为full GC,这是一个针对整个仓库的”清理死猪,腾出空间”操作。
你发现,老年代隔间存在“内存碎片”并不是大问题,于是采用”标记-清理算法”。
所以,你在年轻代和老年代中采取了所谓的”分代收集算法”。3. JVM内存分代模型
层级关系如下:
GC发生区域
|- 新生代
|- 伊甸区
|- 存活区S0
|-存活区S1
|- 老年代
|- 方法区4. GC种类
根据发生区域的不同,GC可以细分为:Minor GC,Major GC,Full GC,Mixed GC4.1 Minor GC
在年轻代Young space
(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC,Minor GC只会清理年轻代.4.2 Major GC
Major GC清理老年代(old GC),但是通常也可以指和Full GC是等价,因为收集老年代的时候往往也会伴随着升级年轻代,收集整个Java堆。所以有人问的时候需问清楚它指的是full GC还是old GC。4.3 Full GC
full gc是对新生代、老年代、永久代(jdk8后改为元空间)统一的回收。4.4 Mixed GC
收集整个young gen以及部分old gen的GC。只有G1有这个模式5. 垃圾收集器
5.1 新生代垃圾收集器
5.1.1 Serial
最基本、发展最久的收集器,采用复制法,在jdk3以前是gc收集器的唯一选择,Serial是单线程收集器,Serial收集器只能使用一条线程进行收集工作,在收集的时候必须得停掉其它线程,等待收集工作完成其它线程才可以继续工作。
虽然Serial看起来很坑,需停掉别的线程以完成自己的gc工作,但是也不是完全没用的,比如说Serial在运行在Client模式下优于其它收集器[简单高效,不过一般都是用Server模式,64bit的jvm甚至没Client模式]
优点:对于Client模式下的jvm来说是个好的选择。适用于单核CPU【现在基本都是多核了】
缺点:收集时要暂停其它线程,有点浪费资源,多核下显得。5.1.2 Parallel Scavenge
采用复制算法的收集器,和ParNew一样支持多线程。但是该收集器重点关心的是吞吐量【吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间) 如果代码运行100min垃圾收集1min,则为99%】对于用户界面,适合使用, GC停顿时间短,不然因为卡顿导致交互界面卡顿将很影响用户体验。对于后台高吞吐量可以高效率的利用cpu尽快完成程序运算任务,适合后台运算。5.1.3 ParNew收集器
可以认为是Serial的升级版,因为它支持多线程[GC线程],而且收集算法、Stop The World、回收策略和Serial一样,就是可以有多个GC线程并发运行,它是HotSpot第一个真正意义实现并发的收集器。默认开启线程数和当前cpu数量相同【几核就是几个,超线程cpu的话就不清楚了 - -】,如果cpu核数很多不想用那么多,可以通过-XX:ParallelGCThreads来控制垃圾收集线程的数量。
优点:
1.支持多线程,多核CPU下可以充分的利用CPU资源
2.运行在Server模式下新生代首选的收集器【重点是因为新生代的这几个收集器只有它和Serial可以配合CMS收集器一起使用】
缺点:
在单核下表现不会比Serial好,由于在单核能利用多核的优势,在线程收集过程中可能会出现频繁上下文切换,导致额外的开销。
ParNew 和 Parallel Scavenge的不同之处在于,ParNew做了一些增强,比如:ParNew拥有一些同步机制,使其可以在CMS收集器的并发时期运行。5.2 老年代垃圾收集器
5.2.1 Serial Old
和新生代的Serial一样为单线程,Serial的老年代版本,不过它采用”标记-整理算法”,这个模式主要是给Client模式下的JVM使用。
如果是Server模式有两大用途
1.jdk5前和Parallel Scavenge搭配使用,jdk5前也只有这个老年代收集器可以和它搭配。
2.作为CMS收集器的后备。5.2.2 Parallel Old
支持多线程,Parallel Scavenge的老年版本,jdk6开始出现, 采用”标记-整理算法”【老年代的收集器大都采用此算法】
在jdk6以前,新生代的Parallel Scavenge只能和Serial Old配合使用【根据图,没有这个的话只剩Serial Old,而Parallel Scavenge又不能和CMS配合使用】,而且Serial Old为单线程Server模式下会拖后腿【多核cpu下无法充分利用】,这种结合并不能让应用的吞吐量最大化。
Parallel Old的出现结合Parallel Scavenge,真正的形成“吞吐量优先”的收集器组合。5.2.3 CMS
CMS收集器(Concurrent Mark Sweep)是以一种获取最短回收停顿时间为目标的收集器。【重视响应,可以带来好的用户体验,被sun称为并发低停顿收集器】
启用CMS:-XX:+UseConcMarkSweepGC
正如其名,CMS采用的是”标记-清除”(Mark Sweep)算法,而且是支持并发(Concurrent)的
它的运作分为以下5个阶段:
阶段 | 描述 |
---|---|
初始标记(stop the world) | 标记老年代的可达对象(包括根可达和从新生代对象出发可达)。只标记直接可达的对象,因此停顿时间较短,与minor gc的停顿时间相当 |
并发标记 | 从第一阶段已经标记的对象出发,并发扫描老年代剩余对象,标记所有根可达的对象。业务线程并发运行。 |
重新标记(stop the world) | 寻找并发标记阶段遗漏的对象,因为并发标记阶段业务线程也在运行,可能产生新的对象或者丢弃某个旧对象。 |
并发清除 | 回收所有被标记为不可达的对象。每清理一个死亡对象,该对象占用的空间被添加到可用空间列表中,此时可能发生死亡对象占用空间合并。记住,存活对象不会被移动,因此会产生碎片 |
重置 | 清除临时数据结构,准备下一次并发收集 |
新生代收集器和老生代收集器可以自由组合搭配,允许的组合如下图所示:
5.3 G1垃圾收集器
G1(garbage first:尽可能多收垃圾)是一个更适合服务端使用的垃圾收集器,尤其适用于多处理器大内存的机器。它能够实现可预期的停顿时间,同时获得高吞吐量。
G1作为CMS的替代品,主要有以下两个优势:
- 支持碎片整理
- 实现可预期的停顿时间
什么情况下我们应该使用G1垃圾收集器呢?
- 堆内存很大且对GC停顿时间有限制的应用:堆内存6G左右或大于6G、小于0.5s的稳定且可预测的停顿时间
当前使用CMS或者ParallelOld的应用,如果存在以下问题,应该切换到G1收集器
- GC时间太长或者太频繁
- 新对象分配内存区域的频率或者升级到老年代的频率变化非常大
- 垃圾收集或内存压缩耗时太长(长于0.5到1s)
5.3.1 G1收集器的堆内存划分
G1将jvm堆内存划分为若干个大小相同的区域【region】。这些区域在逻辑上被标记为伊甸区、存活区和老年代。jvm可以设置每个region的大小(1-32m,大小得看堆内存大小,必须是2的幂),它会根据当前的堆内存分配合理的region大小。
-XX:G1HeapRegionSize=n 设置g1 region大小,不设置的话自己会根据堆大小算,目标是根据最小堆内存划分2048个区域,区域的大小在1M ~ 32M之间(等大)。
-XX:MaxGCPauseMillis=200 最大停顿时间 默认200毫秒
还有另外两种类型的分区:Humongous,储存大小超过标准区域大小50%的大对象;Available,未分配的区域(空区域)5.3.2 工作原理
进行垃圾收集时,G1和CMS的行为很相似。G1并发地全局扫描整个堆空间,标记对象的存活情况。标记阶段完成后,G1获知哪些区域的垃圾最多,便首先收集这些区域的垃圾,尽快腾出更多的可用空间。G1提供了一个停顿时间预测模型,根据用户指定的停顿时间来决定进行垃圾收集的区域的数量,但是这个预测模型不是100%精准的,只能保证在合理范围内。
被选中进行收集的区域,G1先清理其中标记为死亡的对象,然后将多个区域中存活的对象复制到一个区域,减少碎片。这个过程是多线程并发进行的,以减少停顿,增加吞吐量。因此,每一次垃圾回收,G1都会减少内存碎片。
而CMS不会进行内存压缩、 ParallelOld会压缩整个堆空间,导致长停顿。
相比于CMS和ParallelOld,使用G1的进程占用内存会更大一点,主要因为G1需要使用一些额外的数据结构来进行统计工作,比如 Remembered Sets 和 Collection Sets。
Remembered Sets,每个区域都有一个Rset,记录该区域的对象引用链,使得收集器可以独立、并行地收集某个区域。整体占用空间低于5%。
Collection Sets,记录本次GC需要回收的区域集合。所有CSset中存活的对象被移动或复制。CSet中包含的区域可能是伊甸区、存活区或者老年代。整体占用空间低于1%
关于G1收集器详见https://www.yuque.com/jiujiedebaomihua/bqif7y/dbeyg86. GC日志
基础参数
如果你要在生产环境中使用G1 GC,下面这些跟日志相关的参数是必备的,有了这些参数,你才能排查基本的垃圾回收问题。
使用-XX:GCLogFileSize
设置合适的GC日志文件大小,使用-XX:NumberOfGCLogFiles
设置要保留的GC日志文件个数,使用-Xloggc:/path/to/gc.log
设置GC日志文件的位置,通过上面三个参数保留应用在运行过程中的GC日志信息,我建议最少保留一个星期的GC日志,这样应用的运行时信息足够多的,方便排查问题。新生代收集
和其他垃圾收集器一样,G1也使用-XX:PrintGCDetails
打印出详细的垃圾收集日志,下面这张图是新生代收集的标准流程,我在这里将它分成了6个步骤:
四个关键信息
- 新生代垃圾收集发生的时间——2016-12-12T10:40:18.811-0500,通过设置
-XX:+PrintGCDateStamps
参数可以打印出这个时间; - JVM启动后的相对时间——25.959
- 这次收集的类型——新生代收集,只回收Eden分区
- 这次收集花费的时间——0.0305171s,即30ms
- 新生代垃圾收集发生的时间——2016-12-12T10:40:18.811-0500,通过设置
- 列出了新生代收集中并行收集的详细过程
- Parallel Time:并行收集任务在运行过程中引发的STW(Stop The World)时间,从新生代垃圾收集开始到最后一个任务结束,共花费26.6ms
- GC Workers:有4个线程负责垃圾收集,通过参数
-XX:ParallelGCThreads
设置,这个参数的值的设置,跟CPU有关,如果物理CPU支持的线程个数小于8,则最多设置为8;如果物理CPU支持的线程个数大于8,则默认值为number * 5/8 - GC Worker Start:第一个垃圾收集线程开始工作时JVM启动后经过的时间(min);最后一个垃圾收集线程开始工作时JVM启动后经过的时间(max);diff表示min和max之间的差值。理想情况下,你希望他们几乎是同时开始,即diff趋近于0。
- Ext Root Scanning:扫描root集合(线程栈、JNI、全局变量、系统表等等)花费的时间,扫描root集合是垃圾收集的起点,尝试找到是否有root集合中的节点指向当前的收集集合(CSet)
- Update RS(Remembered Set or RSet):每个分区都有自己的RSet,用来记录其他分区指向当前分区的指针,如果RSet有更新,G1中会有一个post-write barrier管理跨分区的引用——新的被引用的card会被标记为dirty,并放入一个日志缓冲区,如果这个日志缓冲区满了会被加入到一个全局的缓冲区,在JVM运行的过程中还有线程在并发处理这个全局日志缓冲区的dirty card。Update RS表示允许垃圾收集线程处理本次垃圾收集开始前没有处理好的日志缓冲区,这可以确保当前分区的RSet是最新的。
- Processed Buffers,这表示在Update RS这个过程中处理多少个日志缓冲区。
- Scan RS:扫描每个新生代分区的RSet,找出有多少指向当前分区的引用来自CSet。
- Code Root Scanning:扫描代码中的root节点(局部变量)花费的时间
- Object Copy:在疏散暂停期间,所有在CSet中的分区必须被转移疏散,Object Copy就负责将当前分区中存活的对象拷贝到新的分区。
- Termination:当一个垃圾收集线程完成任务时,它就会进入一个临界区,并尝试帮助其他垃圾线程完成任务(steal outstanding tasks),min表示该垃圾收集线程什么时候尝试terminatie,max表示该垃圾收集回收线程什么时候真正terminated。
- Termination Attempts:如果一个垃圾收集线程成功盗取了其他线程的任务,那么它会再次盗取更多的任务或再次尝试terminate,每次重新terminate的时候,这个数值就会增加。
- GC Worker Other:垃圾收集线程在完成其他任务的时间
- GC Worker Total:展示每个垃圾收集线程的最小、最大、平均、差值和总共时间。
- GC Worker End:min表示最早结束的垃圾收集线程结束时该JVM启动后的时间;max表示最晚结束的垃圾收集线程结束时该JVM启动后的时间。理想情况下,你希望它们快速结束,并且最好是同一时间结束。
- 列出了新生代GC中的一些任务:
- Code Root Fixup :释放用于管理并行垃圾收集活动的数据结构,应该接近于0,该步骤是线性执行的;
- Code Root Purge:清理更多的数据结构,应该很快,耗时接近于0,也是线性执行。
- Clear CT:清理card table
- 包含一些扩展功能
- Choose CSet:选择要进行回收的分区放入CSet(G1选择的标准是垃圾最多的分区优先,也就是存活对象率最低的分区优先)
- Ref Proc:处理Java中的各种引用——soft、weak、final、phantom、JNI等等。
- Ref Enq:遍历所有的引用,将不能回收的放入pending列表
- Redirty Card:在回收过程中被修改的card将会被重置为dirty
- Humongous Register:JDK8u60提供了一个特性,巨型对象可以在新生代收集的时候被回收——通过
G1ReclaimDeadHumongousObjectsAtYoungGC
设置,默认为true。 - Humongous Reclaim:做下列任务的时间:确保巨型对象可以被回收、释放该巨型对象所占的分区,重置分区类型,并将分区还到free列表,并且更新空闲空间大小。
- Free CSet:将要释放的分区还回到free列表。
- 展示了不同代的大小变化,以及堆大小的自适应调整。
- Eden:1097.0M(1097.0M)->0.0B(967.0M):(1)当前新生代收集触发的原因是Eden空间满了,分配了1097M,使用了1097M;(2)所有的Eden分区都被疏散处理了,在新生代结束后Eden分区的使用大小成为了0.0B;(3)Eden分区的大小缩小为967.0M
- Survivors:13.0M->139.0M:由于年轻代分区的回收处理,survivor的空间从13.0M涨到139.0M;
- Heap:1694.4M(2048.0M)->736.3M(2048.0M):(1)在本次垃圾收集活动开始的时候,堆空间整体使用量是1694.4M,堆空间的最大值是2048M;(2)在本次垃圾收集结束后,堆空间的使用量是763.4M,最大值保持不变。
第6点展示了本次新生代垃圾收集的时间
标志着并发垃圾收集阶段的开始:
- GC pause(G1 Evacuation Pause)(young)(initial-mark):为了充分利用STW的机会来trace所有可达(存活)的对象,initial-mark阶段是作为新生代垃圾收集中的一部分存在的(搭便车)。initial-mark设置了两个TAMS(top-at-mark-start)变量,用来区分存活的对象和在并发标记阶段新分配的对象。在TAMS之前的所有对象,在当前周期内都会被视作存活的。
- 表示第并发标记阶段做的第一个事情:根分区扫描
- GC concurrent-root-region-scan-start:根分区扫描开始,根分区扫描主要扫描的是新的survivor分区,找到这些分区内的对象指向当前分区的引用,如果发现有引用,则做个记录;
- GC concurrent-root-region-scan-end:根分区扫描结束,耗时0.0030613s
- 表示并发标记阶段
- GC Concurrent-mark-start:并发标记阶段开始。(1)并发标记阶段的线程是跟应用线程一起运行的,不会STW,所以称为并发;并发标记阶段的垃圾收集线程,默认值是Parallel Thread个数的25%,这个值也可以用参数
-XX:ConcGCThreads
设置;(2)trace整个堆,并使用位图标记所有存活的对象,因为在top TAMS之前的对象是隐式存活的,所以这里只需要标记出那些在top TAMS之后、阈值之前的;(3)记录在并发标记阶段的变更,G1这里使用了SATB算法,该算法要求在垃圾收集开始的时候给堆做一个快照,在垃圾收集过程中这个快照是不变的,但实际上肯定有些对象的引用会发生变化,这时候G1使用了pre-write barrier记录这种变更,并将这个记录存放在一个SATB缓冲区中,如果该缓冲区满了就会将它加入到一个全局的缓冲区,同时G1有一个线程在并行得处理这个全局缓冲区;(4)在并发标记过程中,会记录每个分区的存活对象占整个分区的大小的比率; - GC Concurrent-mark-end:并发标记阶段结束,耗时0.3055438s
- GC Concurrent-mark-start:并发标记阶段开始。(1)并发标记阶段的线程是跟应用线程一起运行的,不会STW,所以称为并发;并发标记阶段的垃圾收集线程,默认值是Parallel Thread个数的25%,这个值也可以用参数
- 重新标记阶段,会Stop the World
- Finalize Marking:Finalizer列表里的Finalizer对象处理,耗时0.0014099s;
- GC ref-proc:引用(soft、weak、final、phantom、JNI等等)处理,耗时0.0000480s;
- Unloading:类卸载,耗时0.0025840s;
- 除了前面这几个事情,这个阶段最关键的结果是:绘制出当前并发周期中整个堆的最后面貌,剩余的SATB缓冲区会在这里被处理,所有存活的对象都会被标记;
- 清理阶段,也会Stop the World
- 计算出最后存活的对象:标记出initial-mark阶段后分配的对象;标记出至少有一个存活对象的分区;
- 为下一个并发标记阶段做准备,previous和next位图会被清理;
- 没有存活对象的老年代分区和巨型对象分区会被释放和清理;
- 处理没有任何存活对象的分区的RSet;
- 所有的老年代分区会按照自己的存活率(存活对象占整个分区大小的比例)进行排序,为后面的CSet选择过程做准备;
并发清理阶段
- GC concurrent-cleanup-start:并发清理阶段启动。完成第5步剩余的清理工作;将完全清理好的分区加入到二级free列表,等待最终还会到总体的free列表;
- GC concurrent-cleanup-end:并发清理阶段结束,耗时0.0012954s
混合收集
在并发收集阶段结束后,你会看到混合收集阶段的日志,如下图所示,该日志的大部分跟之前讨论的新生代收集相同,只有第1部分不一样:GC pause(G1 Evacuation Pause)(mixed),0.0129474s,这一行表示这是一个混合垃圾收集周期;在混合垃圾收集处理的CSet不仅包括新生代的分区,还包括老年代分区——也就是并发标记阶段标记出来的那些老年代分区。
Full GC
如果堆内存空间不足以分配新的对象,或者是Metasapce空间使用率达到了设定的阈值,那么就会触发Full GC——你在使用G1的时候应该尽量避免这种情况发生,因为G1的Full Gc是单线程、会Stop The World,代价非常高。Full GC的日志如下图所示,从中你可以看出三类信息
Full GC的原因,这个图里是Allocation Failure,还有一个常见的原因是Metadata GC Threshold;
- Full GC发生的频率,每隔几天发生一次Full GC还可以接受,但是每隔1小时发生一次Full GC则不可接受;
- Full GC的耗时,这张图里的Full GC耗时150ms(PS:按照我的经验,实际运行中如果发生Full GC,耗时会比这个多很多)
基础配置参数中,我这里还想介绍两个:-XX:+PrintGCApplicationStoppedTime
和-XX:+PrintGCApplicationConcurrentTime
,这两个参数也可以为你提供有用的信息,如下图所示:
- 记录了应用线程在安全点被暂停的总时间(也就是STW的总时间)
- 记录了让所有应用线程进入安全点所花费的总时间