概述
本文介绍GC根底原理和实践,GC调优办法思路和办法,基于Hotspot jdk1.8,学习之后将理解如何对生产零碎呈现的GC问题进行排查解决
浏览时长约30分钟,内容次要如下:
GC根底原理,波及调优指标,GC事件分类、JVM内存调配策略、GC日志剖析等
CMS原理及调优
G1原理及调优
GC问题排查和解决思路
GC根底原理
1 GC调优指标
大多数状况下对 Java 程序进行GC调优, 次要关注两个指标:响应速度、吞吐量
响应速度(Responsiveness)
响应速度指程序或系统对一个申请的响应有多迅速。比方,用户订单查问响应工夫,对响应速度要求很高的零碎,较大的进展工夫是不可承受的。调优的重点是在短的工夫内疾速响应
吞吐量(Throughput)
吞吐量关注在一个特定时间段内利用零碎的最大工作量,例如每小时批处理零碎能实现的工作数量,在吞吐量方面优化的零碎,较长的GC进展工夫也是能够承受的,因为高吞吐量利用更关怀的是如何尽可能快地实现整个工作,不思考疾速响应用户申请
GC调优中,GC导致的利用暂停工夫影响零碎响应速度,GC解决线程的CPU使用率影响零碎吞吐量
2 GC分代收集算法
古代的垃圾收集器根本都是采纳分代收集算法,其次要思维:
将Java的堆内存逻辑上分成两块:新生代、老年代,针对不同存活周期、不同大小的对象采取不同的垃圾回收策略
新生代(Young Generation)
新生代又叫年老代,大多数对象在新生代中被创立,很多对象的生命周期很短。每次新生代的垃圾回收(又称Young GC、Minor GC、YGC)后只有大量对象存活,所以应用复制算法,只需大量的复制操作老本就能够实现回收
新生代内又分三个区:一个Eden区,两个Survivor区(S0、S1,又称From Survivor、To Survivor),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足降职到老年代条件的对象将被复制到另外一个Survivor区。对象每经验一次复制,年龄加1,达到降职年龄阈值后,转移到老年代
老年代(Old Generation)
在新生代中经验了N次垃圾回收后依然存活的对象,就会被放到老年代,该区域中对象存活率高。老年代的垃圾回收通常应用“标记-整顿”算法
3 GC事件分类
依据垃圾收集回收的区域不同,垃圾收集次要通常分为Young GC、Old GC、Full GC、Mixed GC
(1) Young GC
新生代内存的垃圾收集事件称为Young GC(又称Minor GC),当JVM无奈为新对象调配在新生代内存空间时总会触发 Young GC,比方 Eden 区占满时。新对象调配频率越高, Young GC 的频率就越高
Young GC 每次都会引起全线进展(Stop-The-World),暂停所有的利用线程,进展工夫绝对老年代GC的造成的进展,简直能够忽略不计
(2) Old GC 、Full GC、Mixed GC
Old GC,只清理老年代空间的GC事件,只有CMS的并发收集是这个模式
Full GC,清理整个堆的GC事件,包含新生代、老年代、元空间等
Mixed GC,清理整个新生代以及局部老年代的GC,只有G1有这个模式
4 GC日志剖析
GC日志是一个很重要的工具,它精确记录了每一次的GC的执行工夫和执行后果,通过剖析GC日志能够调优堆设置和GC设置,或者改良应用程序的对象分配模式,开启的JVM启动参数如下:
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps
复制代码常见的Young GC、Full GC日志含意如下:
收费的GC日志图形剖析工具举荐上面2个:
GCViewer,下载jar包间接运行
gceasy,web工具,上传GC日志在线应用
5 内存调配策略
Java提供的主动内存治理,能够归结为解决了对象的内存调配和回收的问题,后面曾经介绍了内存回收,上面介绍几条最广泛的内存调配策略
对象优先在Eden区调配
大多数状况下,对象在先新生代Eden区中调配。当Eden区没有足够空间进行调配时,虚拟机将发动一次Young GC
大对象间接进入老年代
JVM提供了一个对象大小阈值参数(-XX:PretenureSizeThreshold,默认值为0,代表不论多大都是先在Eden中分配内存),大于参数设置的阈值值的对象间接在老年代调配,这样能够防止对象在Eden及两个Survivor间接产生大内存复制
长期存活的对象将进入老年代
对象每经验一次垃圾回收,且没被回收掉,它的年龄就减少1,大于年龄阈值参数(-XX:MaxTenuringThreshold,默认15)的对象,将降职到老年代中
空间调配担保
当进行Young GC之前,JVM须要预估:老年代是否可能包容Young GC后新生代降职到老年代的存活对象,以确定是否须要提前触发GC回收老年代空间,基于空间调配担保策略来计算:
continueSize:老年代最大可用间断空间
Young GC之后如果胜利(Young GC后降职对象能放入老年代),则代表担保胜利,不必再进行Full GC,进步性能;如果失败,则会呈现“promotion failed”谬误,代表担保失败,须要进行Full GC
动静年龄断定
新生代对象的年龄可能没达到阈值(MaxTenuringThreshold参数指定)就降职老年代,如果Young GC之后,新生代存活对象达到雷同年龄所有对象大小的总和大于任一Survivor空间(S0 或 S1总空间)的一半,此时S0或者S1区行将包容不了存活的新生代对象,年龄大于或等于该年龄的对象就能够间接进入老年代,毋庸等到MaxTenuringThreshold中要求的年龄
另外,如果Young GC后S0或S1区不足以包容:未达到降职老年代条件的新生代存活对象,会导致这些存活对象间接进入老年代,须要尽量避免
CMS原理及调优
1 名词解释
可达性剖析算法:用于判断对象是否存活,根本思维是通过一系列称为“GC Root”的对象作为终点(常见的GC Root有零碎类加载器、栈中的对象、处于激活状态的线程等),基于对象援用关系,从GC Roots开始向下搜寻,所走过的门路称为援用链,当一个对象到GC Root没有任何援用链相连,证实对象不再存活
Stop The World:GC过程中剖析对象援用关系,为了保障剖析后果的准确性,须要通过进展所有Java执行线程,保障援用关系不再动态变化,该进展事件称为Stop The World(STW)
Safepoint:代码执行过程中的一些非凡地位,当线程执行到这些地位的时候,阐明虚拟机以后的状态是平安的,如果有须要GC,线程能够在这个地位暂停。HotSpot采纳被动中断的形式,让执行线程在运行期轮询是否须要暂停的标记,若须要则中断挂起
2 CMS简介
CMS(Concurrent Mark and Sweep 并发-标记-革除),是一款基于并发、应用标记革除算法的垃圾回收算法,只针对老年代进行垃圾回收。CMS收集器工作时,尽可能让GC线程和用户线程并发执行,以达到升高STW工夫的目标
通过以下命令行参数,启用CMS垃圾收集器:
-XX:+UseConcMarkSweepGC
复制代码值得补充的是,上面介绍到的CMS GC是指老年代的GC,而Full GC指的是整个堆的GC事件,包含新生代、老年代、元空间等,两者有所辨别
3 新生代垃圾回收
能与CMS搭配应用的新生代垃圾收集器有Serial收集器和ParNew收集器。这2个收集器都采纳标记复制算法,都会触发STW事件,进行所有的利用线程。不同之处在于,Serial是单线程执行,ParNew是多线程执行
4 老年代垃圾回收
CMS GC以获取最小进展工夫为目标,尽可能减少STW工夫,能够分为7个阶段
阶段 1: 初始标记(Initial Mark)
此阶段的指标是标记老年代中所有存活的对象, 包含 GC Root 的间接援用, 以及由新生代中存活对象所援用的对象,触发第一次STW事件
这个过程是反对多线程的(JDK7之前单线程,JDK8之后并行,可通过参数CMSParallelInitialMarkEnabled调整)
阶段 2: 并发标记(Concurrent Mark)
此阶段GC线程和利用线程并发执行,遍历阶段1初始标记进去的存活对象,而后持续递归标记这些对象可达的对象
阶段 3: 并发预清理(Concurrent Preclean)
此阶段GC线程和利用线程也是并发执行,因为阶段2是与利用线程并发执行,可能有些援用关系曾经产生扭转。
通过卡片标记(Card Marking),提前把老年代空间逻辑划分为相等大小的区域(Card),如果援用关系产生扭转,JVM会将产生扭转的区域标记位“脏区”(Dirty Card),而后在本阶段,这些脏区会被找进去,刷新援用关系,革除“脏区”标记
阶段 4: 并发可勾销的预清理(Concurrent Abortable Preclean)
此阶段也不进行利用线程. 本阶段尝试在 STW 的 最终标记阶段(Final Remark)之前尽可能地多做一些工作,以缩小利用暂停工夫
在该阶段一直循环解决:标记老年代的可达对象、扫描解决Dirty Card区域中的对象,循环的终止条件有:
1 达到循环次数
2 达到循环执行工夫阈值
3 新生代内存使用率达到阈值
阶段 5: 最终标记(Final Remark)
这是GC事件中第二次(也是最初一次)STW阶段,指标是实现老年代中所有存活对象的标记。在此阶段执行:
1 遍历新生代对象,从新标记
2 依据GC Roots,从新标记
3 遍历老年代的Dirty Card,从新标记
阶段 6: 并发革除(Concurrent Sweep)
此阶段与应用程序并发执行,不须要STW进展,依据标记后果革除垃圾对象
阶段 7: 并发重置(Concurrent Reset)
此阶段与应用程序并发执行,重置CMS算法相干的外部数据, 为下一次GC循环做筹备
5 CMS常见问题
最终标记阶段进展工夫过长问题
CMS的GC进展工夫约80%都在最终标记阶段(Final Remark),若该阶段进展工夫过长,常见起因是新生代对老年代的有效援用,在上一阶段的并发可勾销预清理阶段中,执行阈值工夫内未实现循环,来不及触发Young GC,清理这些有效援用
通过增加参数:-XX:+CMSScavengeBeforeRemark。在执行最终操作之前先触发Young GC,从而缩小新生代对老年代的有效援用,升高最终标记阶段的进展,但如果在上个阶段(并发可勾销的预清理)已触发Young GC,也会反复触发Young GC
并发模式失败(concurrent mode failure) & 降职失败(promotion failed)问题
并发模式失败:当CMS在执行回收时,新生代产生垃圾回收,同时老年代又没有足够的空间包容降职的对象时,CMS 垃圾回收就会进化成单线程的Full GC。所有的利用线程都会被暂停,老年代中所有的有效对象都被回收
降职失败:当新生代产生垃圾回收,老年代有足够的空间能够包容降职的对象,然而因为闲暇空间的碎片化,导致降职失败,此时会触发单线程且带压缩动作的Full GC
并发模式失败和降职失败都会导致长时间的进展,常见解决思路如下:
升高触发CMS GC的阈值,即参数-XX:CMSInitiatingOccupancyFraction的值,让CMS GC尽早执行,以保障有足够的空间
减少CMS线程数,即参数-XX:ConcGCThreads,
增大老年代空间
让对象尽量在新生代回收,防止进入老年代
内存碎片问题
通常CMS的GC过程基于标记革除算法,不带压缩动作,导致越来越多的内存碎片须要压缩,常见以下场景会触发内存碎片压缩:
新生代Young GC呈现新生代降职担保失败(promotion failed)
程序被动执行System.gc()
可通过参数CMSFullGCsBeforeCompaction的值,设置多少次Full GC触发一次压缩,默认值为0,代表每次进入Full GC都会触发压缩,带压缩动作的算法为下面提到的单线程Serial Old算法,暂停工夫(STW)工夫十分长,须要尽可能减少压缩工夫
G1原理及调优
1 G1简介
G1(Garbage-First)是一款面向服务器的垃圾收集器,反对新生代和老年代空间的垃圾收集,次要针对装备多核处理器及大容量内存的机器,G1最次要的设计指标是: 实现可预期及可配置的STW进展工夫
2 G1堆空间划分
Region
为实现大内存空间的低进展工夫的回收,将划分为多个大小相等的Region。每个小堆区都可能是 Eden区,Survivor区或者Old区,然而在同一时刻只能属于某个代
在逻辑上, 所有的Eden区和Survivor区合起来就是新生代,所有的Old区合起来就是老年代,且新生代和老年代各自的内存Region区域由G1自动控制,一直变动
巨型对象
当对象大小超过Region的一半,则认为是巨型对象(Humongous Object),间接被调配到老年代的巨型对象区(Humongous regions),这些巨型区域是一个间断的区域集,每一个Region中最多有一个巨型对象,巨型对象能够占多个Region
G1把堆内存划分成一个个Region的意义在于:
每次GC不用都去解决整个堆空间,而是每次只解决一部分Region,实现大容量内存的GC
通过计算每个Region的回收价值,包含回收所需工夫、可回收空间,在无限工夫内尽可能回收更多的内存,把垃圾回收造成的进展工夫管制在预期配置的工夫范畴内,这也是G1名称的由来: garbage-first
3 G1工作模式
针对新生代和老年代,G1提供2种GC模式,Young GC和Mixed GC,两种会导致Stop The World
Young GC
当新生代的空间有余时,G1触发Young GC回收新生代空间
Young GC次要是对Eden区进行GC,它在Eden空间耗尽时触发,基于分代回收思维和复制算法,每次Young GC都会选定所有新生代的Region,同时计算下次Young GC所需的Eden区和Survivor区的空间,动静调整新生代所占Region个数来管制Young GC开销
Mixed GC
当老年代空间达到阈值会触发Mixed GC,选定所有新生代里的Region,依据全局并发标记阶段(上面介绍到)统计得出收集收益高的若干老年代 Region。在用户指定的开销指标范畴内,尽可能抉择收益高的老年代Region进行GC,通过抉择哪些老年代Region和抉择多少Region来管制Mixed GC开销
4 全局并发标记
全局并发标记次要是为Mixed GC计算找出回收收益较高的Region区域,具体分为5个阶段
阶段 1: 初始标记(Initial Mark)
暂停所有利用线程(STW),并发地进行标记从 GC Root 开始间接可达的对象(原生栈对象、全局对象、JNI 对象),当达到触发条件时,G1 并不会立刻发动并发标记周期,而是期待下一次新生代收集,利用新生代收集的 STW 时间段,实现初始标记,这种形式称为借道(Piggybacking)
阶段 2: 根区域扫描(Root Region Scan)
在初始标记暂停完结后,新生代收集也实现的对象复制到 Survivor 的工作,利用线程开始沉闷起来;
此时为了保障标记算法的正确性,所有新复制到 Survivor 分区的对象,须要找出哪些对象存在对老年代对象的援用,把这些对象标记成根(Root);
这个过程称为根分区扫描(Root Region Scanning),同时扫描的 Suvivor 分区也被称为根分区(Root Region);
根分区扫描必须在下一次新生代垃圾收集启动前实现(接下来并发标记的过程中,可能会被若干次新生代垃圾收集打断),因为每次 GC 会产生新的存活对象汇合
阶段 3: 并发标记(Concurrent Marking)
标记线程与应用程序线程并行执行,标记各个堆中Region的存活对象信息,这个步骤可能被新的 Young GC 打断
所有的标记工作必须在堆满前就实现扫描,如果并发标记耗时很长,那么有可能在并发标记过程中,又经验了几次新生代收集
阶段 4: 再次标记(Remark)
和CMS相似暂停所有利用线程(STW),以实现标记过程短暂地进行利用线程, 标记在并发标记阶段发生变化的对象,和所有未被标记的存活对象,同时实现存活数据计算
阶段 5: 清理(Cleanup)
为行将到来的转移阶段做筹备, 此阶段也为下一次标记执行所有必须的整顿计算工作:
整顿更新每个Region各自的RSet(remember set,HashMap构造,记录有哪些老年代对象指向本Region,key为指向本Region的对象的援用,value为指向本Region的具体Card区域,通过RSet能够确定Region中对象存活信息,防止全堆扫描)
回收不蕴含存活对象的Region
统计计算回收收益高(基于开释空间和暂停指标)的老年代分区汇合
5 G1调优留神点
Full GC问题
G1的失常解决流程中没有Full GC,只有在垃圾回收解决不过去(或者被动触发)时才会呈现, G1的Full GC就是单线程执行的Serial old gc,会导致十分长的STW,是调优的重点,须要尽量避免Full GC,常见起因如下:
程序被动执行System.gc()
全局并发标记期间老年代空间被填满(并发模式失败)
Mixed GC期间老年代空间被填满(降职失败)
Young GC时Survivor空间和老年代没有足够空间包容存活对象
相似CMS,常见的解决是:
增大-XX:ConcGCThreads=n 选项减少并发标记线程的数量,或者STW期间并行线程的数量:-XX:ParallelGCThreads=n
减小-XX:InitiatingHeapOccupancyPercent 提前启动标记周期
增大预留内存 -XX:G1ReservePercent=n ,默认值是10,代表应用10%的堆内存为预留内存,当Survivor区域没有足够空间包容新降职对象时会尝试应用预留内存
巨型对象调配
巨型对象区中的每个Region中蕴含一个巨型对象,残余空间不再利用,导致空间碎片化,当G1没有适合空间调配巨型对象时,G1会启动串行Full GC来开释空间。能够通过减少 -XX:G1HeapRegionSize来增大Region大小,这样一来,相当一部分的巨型对象就不再是巨型对象了,而是采纳一般的调配形式
不要设置Young区的大小
起因是为了尽量满足指标进展工夫,逻辑上的Young区会进行动静调整。如果设置了大小,则会笼罩掉并且会禁用掉对进展工夫的管制
均匀响应工夫设置
应用利用的均匀响应工夫作为参考来设置MaxGCPauseMillis,JVM会尽量去满足该条件,可能是90%的申请或者更多的响应工夫在这之内, 然而并不代表是所有的申请都能满足,均匀响应工夫设置过小会导致频繁GC
调优办法与思路
如何剖析零碎JVM GC运行状况及正当优化?
GC优化的外围思路在于:尽可能让对象在新生代中调配和回收,尽量避免过多对象进入老年代,导致对老年代频繁进行垃圾回收,同时给零碎足够的内存缩小新生代垃圾回收次数,进行系统分析和优化也是围绕着这个思路开展
1 剖析零碎的运行状况
零碎每秒申请数、每个申请创立多少对象,占用多少内存
Young GC触发频率、对象进入老年代的速率
老年代占用内存、Full GC触发频率、Full GC触发的起因、长时间Full GC的起因
常用工具如下:
jstat
jvm自带命令行工具,可用于统计内存调配速率、GC次数,GC耗时,常用命令格局
jstat -gc <pid> <统计间隔时间> <统计次数>
复制代码输入返回值代表含意如下:
例如: jstat -gc 32683 1000 10 ,统计pid=32683的过程,每秒统计1次,统计10次
jmap
jvm自带命令行工具,可用于理解零碎运行时的对象散布,常用命令格局如下
// 命令行输入类名、类数量数量,类占用内存大小,
// 依照类占用内存大小降序排列
jmap -histo <pid>
// 生成堆内存转储快照,在当前目录下导出dump.hrpof的二进制文件,
// 能够用eclipse的MAT图形化工具剖析
jmap -dump:live,format=b,file=dump.hprof <pid>
复制代码
jinfo
命令格局
jinfo <pid>
复制代码用来查看正在运行的 Java 应用程序的扩大参数,包含Java System属性和JVM命令行参数
其余GC工具
监控告警零碎:Zabbix、Prometheus、Open-Falcon
jdk主动实时内存监控工具:VisualVM
堆外内存监控: Java VisualVM装置Buffer Pools 插件、google perf工具、Java NMT(Native Memory Tracking)工具
GC日志剖析:GCViewer、gceasy
GC参数检查和优化:xxfox.perfma.com/
2 GC优化案例
数据分析平台零碎频繁Full GC
平台次要对用户在APP中行为进行定时剖析统计,并反对报表导出,应用CMS GC算法。数据分析师在应用中发现零碎页面关上常常卡顿,通过jstat命令发现零碎每次Young GC后大概有10%的存活对象进入老年代。
原来是因为Survivor区空间设置过小,每次Young GC后存活对象在Survivor区域放不下,提前进入老年代,通过调大Survivor区,使得Survivor区能够包容Young GC后存活对象,对象在Survivor区经验屡次Young GC达到年龄阈值才进入老年代,调整之后每次Young GC后进入老年代的存活对象稳固运行时仅几百Kb,Full GC频率大大降低
业务对接网关OOM
网关次要生产Kafka数据,进行数据处理计算而后转发到另外的Kafka队列,零碎运行几个小时候呈现OOM,重启零碎几个小时之后又OOM,通过jmap导出堆内存,在eclipse MAT工具剖析才找出起因:代码中将某个业务Kafka的topic数据进行日志异步打印,该业务数据量较大,大量对象沉积在内存中期待被打印,导致OOM
账号权限管理系统频繁长时间Full GC
零碎对外提供各种账号鉴权服务,应用时发现零碎常常服务不可用,通过Zabbix的监控平台监控发现零碎频繁产生长时间Full GC,且触发时老年代的堆内存通常并没有占满,发现原来是业务代码中调用了System.gc()
总结
GC问题能够说没有捷径,排查线上的性能问题自身就并不简略,除了将本文介绍到的原理和工具死记硬背,还须要咱们一直去积攒教训,真正做到性能最优