从垃圾回收GCDetails看JVM GC原理
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从垃圾回收GCDetails看JVM GC原理相关的知识,希望对你有一定的参考价值。
参考技术A1.启动参数
2.回收原理
为了更好的理解GCDetails信息回忆下新生代回收的算法(图出自网友),此处不会对回收算法进行详细的讲解,也不会介绍ParallelGC的XX:MaxGCPauseMilli、XX:GCTimeRatio等参数。
3.GCDetails信息
4.分析
Parallel Scavenge是JDK8默认的新生代垃圾回收算法,是一种以吞吐量为目标的垃圾回收器,基于标记复制算法,内存分布为堆内存中划分了2块区域,一块为新生代,一块为老年代,默认配置新生代占堆内存1/3,老年代占堆内存2/3,新生代分为Eden,Survivor_To,Survivor_From,默认分配比例为8:1:1,Survivor区负责存储垃圾回收未能回收的对象和晋升到老年代的操作,从上面的GCDetails可以分析一下垃圾回收的大概原理。
以第一行为例,131584K为新生代回收前占用内存大小,21503K为新生代回收后占用内存大小,可以得知这一次GC回收了约110M,垃圾回收器使用Parallel Scavenge,131584K为整个堆内存回收前内存占用大小,42320K为整个堆内存这次GC后内存占用的大小,可以得知这一次GC回收了约89M内存,垃圾回收器使用Parallel Old,花费了大约8.3毫秒,那新生代回收内存与整个堆回收的内存大小差就是新生代晋升到老年代的内存大小,可以得知为21M。
以第二次GC为例,可以看到他触发了Full GC,原因很简单,就是老年代的空间不足,在这次GC中,新生代回收了16269K内存空间,OK代表新生代全部内存都已经回收,老年代回收了316390K-236940K内存空间,大约80M,整个堆内存回收了333077K-271355K内存空间,大约61M,可以得知新生代晋升80-61内存空间,约19M。
在过往的生产经验中只要JVM或者程序没有相关的告警,其实很少去调整启动参数,通过GC日志的学习与分析,我们发现堆内存的大小与系统的吞吐量还是有直接的关系,大家可以尝试使用不同的-Xms、-Xmx1参数来测试系统的吞吐量,我们会发现如果内存过小,会频繁的发生年轻代GC甚至会频发的一直发生Full GC或内存泄漏,其实内存离谱的大也会影响性能,老年代Full GC时STW(stop the word)的时间会比较久,所以一个合适的启动参数是在性能测试中必不可少的一环。
5.垃圾回收器选择参数
在新生代都是Parallel Scavenge回收器的时候,本来想测试一下配合不同的老年代算法来看一下性能,准备使用XX:+UseParallelGC、XX:+UseParallelOldGC,但是发现老年代回收算法一直都是ParOldGen,困惑了一段时间后通过搜索资料发现在JDK 7u4以后的7和JDK8,新生代算法为Parallel Scavenge,老年代默认都为Parallel Old。
6.一点疑问
上面的GC日志中,其中有两个括号,如第一行的(153088K)和(502784K)分别代表新生代分配的内存大小与堆内存分配的大小,但是我们发现这个大小并不是固定的,在七次乃至后面GC的时候括号内的大小发生了改变,这是为什么呢,其实JDK8 Parallel Scavenge默认开启了-XX:+ UseAdaptiveSizePolicy 参数,这个参数会根据吞吐量与垃圾回收的时间动态的调整各内存区域的大小,虽然有默认新生代的分配比例SurvivorRatio=8,但是也不会固定大小,如果在启动参数显示的写SurvivorRatio=8,则分配的内存不会动态调整,大家可以动手实验一下。
在JD8以前新生代垃圾回收除了Parallel Scavenge还有Serial和ParNew大家可以通过以下的指令进行测试,都基于标记复制算法,GC日志在新生代部分都是相同的分析方法。
1.启动参数
2.回收原理
CMS作为一款老年代垃圾回收期,以减少垃圾回收的停顿时间为目标,采用标记清除算法,只能与新生代垃圾回收ParNew和Serial搭配使用,JDK9后只支持与ParNew配合使用,同样本部分不做详细的原理的说明,主要是分析GCDetails日志来加深理解,这部分有两个比较重要的概念,一个是三色标记法与读写屏障,一个是卡表结构,没有了解的可以参考 这篇文章 ,感觉写的比较好理解。
3.GCDetails日志
4.分析
GC (Allocation Failure)是使用ParNew作为新生代的垃圾回收算法,此部分的内存情况与之前分析Parallel相似,所以这部分主要分析CMS老年代的垃圾回收原理。CMS垃圾回收日志其实与垃圾回收的阶段强相关,主要也是通过看日志来反向加强垃圾回收的流程与原理。
1)初始标记-CMS Initial Mark
CMS为了减少停顿时间大家可以记两段标记阶段都STW,剩下的阶段都并行,日志中时间0.328行可以得知一些信息,初始标记阶段老年代使用内存大小为203782K,整个老年代分配大小为349568K,整个堆内存使用内存为221541K,整个堆内存分配的大小为506816K,标记的时间也很短,从这里也能大概算出新生代分配的内存大小约为506816K-349568K=157M左右,与第一行的新生代内存分配大小一致。
2)并发标记-CMS-concurrent-mark,并发标记主要是根据初始标记来进行对象的可达性,日志中时间0.330行得知并行标记花费了0.01s;预清理-CMS-concurrent-preclean,预清理主要解决在并发标记的过程中业务线程对对象引用关系的修改(包括跨代引用和引用改变),主要是扫描标记为“脏块”的卡表,花费了0.01s;
3)可终止预清理-CMS-concurrent-abortable-preclean,该步骤不是一定执行,如果现在新生代内存小于CMSScheduleRemarkEdenSizeThreshold则不执行,如果大于该值继续标记(一直重复)的时候会有退出条件如循环次数、时间阈值、Eden区内存使用率等,在循环退出之前会进行一次YGC减轻后面最终标记的压力(因为老年代存在指向年轻代的指针),通过日志我们可以看出在0.331的可终止预清理阶段进行了三次YGC,从内存的情况上看 有大量对象从新生代晋升到老年代。
小疑问:在该部分我们观察时间指标是0.003/0.136 secs,0.003s是CPU执行的时间,0.136s是可终止预清理的总耗时,他们之间存在一个差值,这部分我其实也没有一个明确的答案,我第一感觉可能是在等待一次新生代GC。
3)最终标记-CMS Final Remark
从日志中时间0.468可以分析最终标记的结果,17759K新生代使用内存的大小,Rescan (parallel)为最后的标记SWT的时间,weak refs processing为清理弱引用的时间,class unloading为卸载未使用类的时间,scrub symbol table为清理符号表,我理解就是清理符号引用,scrub string table为清理字符表,我理解就是清理字面量,最终CMS的最终标记后老年代使用内存为334051K,堆内存使用为351810K。
4)并发清理-CMS-concurrent-sweep,很好理解就是并发清理;并发重置-CMS-concurrent-reset,重新调整CMS的内存结构,以便下次垃圾回收。
5.调优
在CMS中不是并发的垃圾回收一定成功,可能会发生失败,这样就会降级为Serial Old垃圾回收算法,为了避免这个可以考虑CMSInitiatingOccupancyFraction参数,默认92%
JVM
GC:
概念:垃圾收集(Gabage Collection)
作用:自动检测对象是否超过作用域从而自动回收内存。
优点:编写程序时不需要再考虑内存管理
原理:垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中
已经死亡或者长时间没有使用的对象进行清除和回收。
回收机制:复制垃圾回收、标记垃圾回收、增量垃圾回收
垃圾回收器(GC)的基本原理:
1、对象创建开始,GC就对对象的地址,大小以及使用情况监控
2、GC通过有向图的方式记录和管理堆中的所有对象,确定哪些对象是“可达的”,哪些对象是“不可达的”;
3、当GC确定一个对像“不可达时”,GC就有责任回收内存空间。
4、程序员可以手动执行System.gc(),通过GC运行,但是java语言规范并不保证GC一定会执行。
java中内存泄漏?
概念:
场景:
栗子:
判断内存泄漏:检查java中内存泄漏,一定要让程序各种分支情况都完整执行到程序结束,然后看某个对象是否被使用,不被使用,才能判定这个对象属于内存泄漏。
类加载器本质:除了bootstrap之外,其他的类加载器本身也是java类,它们的父类是ClassLoader。
以上是关于从垃圾回收GCDetails看JVM GC原理的主要内容,如果未能解决你的问题,请参考以下文章
精华推荐 | JVM深层系列「GC底层调优系列」一文带你彻底加强夯实底层原理之GC垃圾回收技术的分析指南(GC原理透析)