当前位置: 首页 > 后端技术 > Java

老大难的GC原理及调优,这下全说清楚了

时间:2023-04-01 15:51:25 Java

GC这个由来已久的难题,其原理和调优在这里得到了充分的说明。阅读时间约30分钟,主要内容如下:GC基本原理、相关调优目标、GC事件分类、JVM内存分配策略、GC日志分析等CMS原理及调优G1原理及调优GC问题Troubleshooting及解决思路GC基本原理1GC调优目标大多数情况下,GC调优是针对Java程序进行的,主要关注两个目标:响应速度和吞吐量。响应性是指程序或系统对请求的响应速度。它的响应速度如何。比如用户订单查询响应时间,一个对响应速度要求高的系统,大的停顿时间是不能接受的。调优的重点是在短时间内快速响应。吞吐量(Throughput)关注应用系统在特定时间段内的最大工作量,例如批处理系统每小时可以完成的任务数,在吞吐量方面进行了优化。在系统中,较长的GC停顿时间也是可以接受的,因为高吞吐量的应用更关心如何尽快完成整个任务,而不管GC导致的应用停顿时间对快速响应用户的影响requestsinGCtuning系统响应速度,GC处理线程的CPU使用率影响系统吞吐量2GC分代收集算法现代垃圾收集器基本使用分代收集算法,其主要思想是将Java堆内存在逻辑上分为两部分:新生代和老年代针对不同生命周期和大小的对象采用不同的垃圾回收策略。年轻代(YoungGeneration)也称为年轻代。大多数对象都是在新生代创建的,很多对象的生命周期很短。短的。每次新生代垃圾回收(也称YoungGC、MinorGC、YGC)后,只有少量对象存活,所以使用复制算法,只需少量的复制操作成本即可完成。新生代分为三个区域:一个Eden区域,两个Survivor区域(S0,S1,也称为FromSurvivor,ToSurvivor),大部分对象都在Eden区域生成。当Eden区满了,存活的对象会被复制到两个Survivor区(其中之一)。当Survivor区已满时,在该区存活且不满足晋升到老年代条件的对象将被复制到另一个Survivor区。对象每被复制一次,年龄加1,达到提升年龄阈值后,转移到老年代。新生代经过N次垃圾回收后仍然存活的对象,将被放入老年代。该地区的受试者存活率很高。老年代的垃圾回收通常采用“mark-sort”算法3GC事件分类根据垃圾回收的不同区域,垃圾回收通常分为YoungGC,OldGC,FullGC,MixedGC(1)YoungGC新生代内存垃圾回收事件称为YoungGC(也称为MinorGC)。当JVM无法为新生代中的新对象分配内存空间时,总会触发YoungGC,比如Eden区已满。分配新对象的频率越高,YoungGC的频率就越高。YoungGC每次都会导致世界停止,暂停所有应用程序线程。停顿时间相比老年代GC引起的停顿几乎可以忽略不计Excluding(2)OldGC,FullGC,MixedGCOOldGC,只清理老年代空间的GC事件,只有CMS的并发收集是这个modeFullGC,清理整个堆的GC事件,包括新生代,老年代,元空间等MixedGC,清理整个新生代和部分老年代GC,只有G1有这种模式4GC日志分析GC日志是一个非常重要的工具,它准确的记录了每次GC的执行时间和执行结果,通过分析GC日志可以调优堆设置和GC设置,或者改善应用程序的对象分配模式。启用的JVM启动参数如下:-verbose:gc-XX:+PrintGCDetails-XX:+PrintGCDateStamps-XX:+PrintGCTimeStampsYoungGC和FullGClogs含义如下:免费GC日志图形分析工具推荐下面两个:GCViewer,下载jar包直接运行gceasy,web工具,在线上传GC日志使用5内存分配策略Java提供了自动内存管理,可以归结为解决对象的内存分配和回收问题。内存回收前面已经介绍过了。下面是一些最常见的内存分配策略。对象首先分配在Eden区。大多数情况下,对象是在第一代伊甸园中分配的。当Eden区没有足够的空间分配时,虚拟机会发起一次YoungGC大对象,直接进入老年代中分配内存),大于参数设置的阈值的对象直接分配在oldgeneration,可以避免直接在Eden和两个Survivor中进行对象的大内存拷贝。长期存活的对象会进入老年代。如果不被回收,它的年龄将增加1。大于年龄阈值参数(-XX:MaxTenuringThreshold,默认15)的对象将被提升到老年代,以保证空间分配。在进行YoungGC之前,JVM需要评估:老年代能否容纳YoungGC后提升到老年代的新生代中存活的对象,从而判断是否需要触发GC回收老年代预留空间,根据空间分配保证策略计算:continueSize:老年代最大可用连续空间。如果YoungGC成功(YoungGC之后,提升对象可以放到老年代),说明保证成功,不需要进行FullGC来提升性能;如果失败,会出现“promotionfailed”的错误,表示保证失败,需要FullGC动态判断年龄。分代对象的年龄可能未达到阈值(由MaxTenuringThreshold参数指定)就被提升为老年代。如果在YoungGC之后,新生代的存活对象达到相同年龄并且所有对象大小的总和大于任意一个Survivor空间(S0或S1总空间)的一半,此时S0或S1区域将无法容纳存活的新生代对象,年龄大于等于这个年龄的对象可以直接进入老年代,无需等待MaxTenuringThreshold中要求的年龄。另外,如果YoungGC后S0或S1区域不足以容纳:不满足晋升到老年代条件的新生代存活对象会导致这些存活对象直接进入老年代。需要尽量避免CMS原则和调优。以一系列称为“GCRoots”的对象为起点(常见的GCRoots包括系统类加载器、栈中对象、活动线程等),根据对象引用关系,从GCRoots开始向下查找,行进的路径称为参考链。当一个对象在没有任何引用链的情况下连接到GCRoot,就证明这个对象已经不存在了。StopTheWorld:分析GC过程中的对象引用关系,以保证分析结果准确来说,需要停止所有Java执行线程,保证引用关系不再动态变化。这个暂停事件叫做StopTheWorld(STW)Safepoint:代码执行过程中的一些特殊位置,当线程执行到这些位置时,说明虚拟机当前状态是安全的。如果需要GC,线程可以在这个位置挂起HotSpot。采用主动中断方式,让执行线程在运行期间轮询是否需要挂起该标志。必要时中断中断2CMS简介CMS(ConcurrentMarkandSweepConcurrent-Mark-Sweep)是一种基于并发、使用标记-清除算法的垃圾回收算法。它只对老年代进行垃圾回收。CMS收集器工作时,尽量让GC线程和用户线程并发执行,减少STW时间。使用以下命令行参数启用CMS垃圾收集器:-XX:+UseConcMarkSweepGC复制代码值得补充的是,下面的介绍大家看到的CMSGC是指老年代的GC,而FullGC是指整个堆的GC事件,包括年轻代、老年代、元空间等,两者是不同的。3新一代垃圾收集器可与CMS结合使用新一代垃圾收集器包括Serial收集器和ParNew收集器。这两个收集器都使用标记复制算法,都触发STW事件停止所有应用线程。不同的是Serial是单线程执行,而ParNew是多线程执行。4老年代垃圾收集CMSGC的目标是获得最少的停顿时间,尽可能减少STW时间。可分为7个阶段。Stage1:初始标记(InitialMark)这个阶段的目标是标记所有老年代存活的对象,包括GCRoot的直接引用,以及新生代存活对象引用的对象,触发第一个STW事件。此进程支持多线程(JDK7之前为单线程,JDK8之后为并行,可通过参数CMSParallelInitialMarkEnabled进行调整)Phase2:并发标记(ConcurrentMark)该阶段GC线程与应用线程并发执行,遍历Phase1中最初标记的幸存对象,然后继续递归地将这些对象标记为可用到达对象阶段3:并发预清理(ConcurrentPreclean)在这个阶段,GC线程和应用程序线程也是并发执行的,因为阶段2是与应用程序线程并发执行,一些引用关系可能已经改变。通过CardMarking,将老年代空间在逻辑上预先划分成大小相等的区域(Cards)。如果引用关系发生变化,JVM会将变化的区域标记为“脏卡”,然后在这个阶段,会发现这些脏区,刷新引用关系,清除“脏区”标记。阶段4:ConcurrentAbortablePreclean此阶段不会停止应用程序线程。尽可能多地在最后的remark阶段之前做一些工作,以减少应用暂停时间。本阶段连续循环处理:在老年代标记可达对象,在DirtyCard区扫描处理对象,循环终止条件为:1循环次数达到2循环执行时间阈值达到3新生代内存使用率达到阈值Stage5:FinalRemark这是GC事件中的第二个(也是最后一个)STW阶段,目标是完成所有存活对象的Marker。本阶段执行:1遍历新生代对象,备注2根据GCRoots,备注3遍历老年代的DirtyCard,备注阶段6:并发扫描(ConcurrentSweep)该阶段与应用程序并发执行,并发不需要STWPause,根据标记结果清除垃圾对象Phase7:并发重置(ConcurrentReset)该阶段与应用并发执行,重置CMS算法相关的内部数据,为下一次GC循环做准备5CMS常见问题最终标记阶段停滞CMS的GC暂停时间大约有80%发生在最终标记阶段(FinalRemark)。如果这个阶段停顿时间过长,常见的原因是新生代对老年代有无效引用。可以取消前一阶段的并发在预清理阶段,如果在执行阈值内没有完成循环,来不及触发YoungGC,通过添加参数:-XX清理这些无效引用:+CMSScavengeBeforeRemark。在执行final操作前触发YoungGC,从而减少新生代对老年代的无效引用,减少finalmark阶段的停顿,但是如果YoungGC在上一阶段已经触发过(并发可取消预清洗)),还会重复TriggerYoungGCconcurrentmodefailure(并发模式失败)&promotionfailed(提升失败)问题。并发模式失败:CMS在进行回收时,新生代发生垃圾回收,同时老年代没有足够的空间容纳被提升的对象。CMS垃圾收集将退化为单线程FullGC。所有的应用线程都会被挂起,所有老年代的无效对象都会被回收。提升失败:当新生代发生垃圾回收时,老年代有足够空间容纳提升对象,但由于空闲空间碎片化,提升失败,此时会触发带压缩的单线程FullGC并发模式.失败和提升失败会造成长时间的停顿。常见的解决方法是:降低触发CMSGC的门槛,即参数-XX:CMSInitiatingOccupancyFraction的值,让CMSGC尽早执行,保证有足够的空间增加CMSGC的数量CMS线程,即参数-XX:ConcGCThreads,增加老年代的空间,让对象尽可能在新生代回收,避免老年代出现内存碎片的问题。通常CMS的GC过程是基于marksClear算法,没有压缩动作,导致需要压缩的内存碎片越来越多。以下几种常见场景会触发内存碎片压缩:新生代YoungGC发生新生代提升失败(promotionfailed),程序主动执行System.gc()参数CMSFullGCsBeforeCompaction的值可以用来设置多少次FullGC触发压缩。默认值为0,表示每次进入FullGC,都会触发compaction。带有压缩动作的算法就是上面提到的单线程SerialOld算法,停顿时间(STW)需要很长的时间,需要尽可能减少压缩时间。G1原理与调优1G1简介针对多核处理器和大容量内存的机器,G1的主要设计目标是实现可预测和可配置的STW停顿时间。2G1堆空间划分Region为了实现停顿时间低的大内存空间回收,会划分成多个大小相等的Region。每个小堆区可能是Eden区、Survivor区或Old区,但只能同时属于某一代。逻辑上,所有的Eden区和Survivor区组合起来形成newgeneration,所有的Old区组合起来。就是老年代,新生代和老年代各自的内存区域由G1自动控制,巨型对象不断变化。当对象大小超过region的一半时,就认为是巨型对象(HumongousObject),直接分配给老年代的巨型对象。对象区域(Humongousregions),这些巨大的区域是一组连续的区域。每个Region中最多有一个巨型对象,一个巨型对象可以占据多个Region。将堆内存划分Region的意义在于每次GC不会处理整个堆空间,而是每次只处理Region的一部分,实现大容量内存的GC计算每个Region的回收值,包括回收需要的时间和可回收的空间,在有限的时间内尽可能回收这也是G1名字的由来:garbage-first3G1的工作模式是针对新生代和老年代的。G1提供两种GC模式,YoungGC和MixedGC,两种都会导致StopTheWorldYoungGC。当新生代空间不足时,G1触发YoungGC回收新生代空间。YoungGC主要是对Eden区进行GC。当Eden空间耗尽时触发,基于分代回收的概念和复制算法,每次YoungGC会选择新生代的所有区域,计算Eden区和Survivor区所需的空间nextYoungGC,动态调整新生代占用的region数量,控制YoungGC的开销。MixedGC当老年代空间达到阈值时,会触发MixedGC,新生代中的所有region都会被选中,根据全局并发标记阶段的统计,得到几个回收收益高的老年代region(如下面所描述的)。在用户指定的开销目标范围内,尽可能选择高产老年代Region进行GC,通过选择哪些老年代Region以及选择多少Region来控制MixedGC开销4.全局并发标记全局并发marking主要是针对MixedGC计算回收收益高的Region区域具体分为五个阶段Stage1:初始标记(InitialMark)暂停所有应用线程(STW),同时标记从GCRoot(native)直接可达的对象stackobjects,Globalobject,JNIobject),当满足触发条件时,G1不会立即发起并发标记循环,而是等待下一次新生代收集,并利用新生代收集的STW时间段完成初始标记。这个方法称为Piggybackingstage2:RootRegionScan在初始标记暂停后,新生代收集完成并将对象复制到Survivor,应用线程变为活动状态;此时,为了保证标记算法的正确性,对于所有新复制到Survivor分区的对象,需要找出哪些对象引用了老年代的对象,并将这些对象标记为Root;这个过程称为根区域扫描。Survivor分区也称为根区域(RootRegion);根分区扫描必须在下一次新生代垃圾回收开始前完成(下一次并发标记过程可能会被几次新生代垃圾回收打断),因为每第二次GC都会产生新的存活对象集合阶段3:并发标记标记线程与应用程序线程并行执行,标记堆中每个区域的存活对象信息。这一步可能会被新的YoungGC打断。所有标记任务必须在堆满之前完成扫描。如果并发标记的时间比较长,有可能在并发标记过程中要经过几次新生代收集。阶段4:标记(Remark)类似于CMS,暂停所有应用线程(STW),完成标记过程,短暂停止应用线程,标记并发标记阶段发生变化的对象,以及所有未标记的存活对象,并完成生存数据计算阶段5:清理(Cleanup)为即将到来的转移阶段做准备,该阶段还为接下来的标记进行所有必要的排序和计算工作:排序和更新每个Region各自的RSet(记忆集,HashMap结构,记录哪些老年代对象指向这个Region,key指向这个Rregion的对象的引用,值为指向Region的具体Card区域,通过RSet可以确定Region中对象的存活信息,避免了全堆扫描)没有的Region的统计计算包含存活对象高收益(基于释放空间和暂停目标)的老年代分区收集5G1调优注意点FullGC问题G1正常处理流程中没有FullGC,只会出现当无法处理(或主动触发)垃圾收集时。G1的FullGC是单线程执行串行oldgc会导致STW很长,这是调优的重点。有必要尽可能避免FullGC。常见的原因有:程序主动执行System.gc()全局并发标记(并发模式失败)时,老年代空间被填满;MixedGC时,老年代空间被填满(提升失败)。YoungGC时,Survivor空间和年老代没有足够空间容纳存活对象。与CMS类似,常见的解决方法是:增加-XX:ConcGCThreads=n选项,增加并发标记线程数Quantity,或者STW时的并行线程数:-XX:ParallelGCThreads=n减少-XX:InitiatingHeapOccupancyPercent启动提前标记周期,增加预留内存-XX:G1ReservePercent=n,默认值为10,表示使用10%的堆内存来预留内存,当Survivor区没有足够的空间容纳新的promoted对象,它会尝试使用预留的内存巨型对象来分配一个巨型对象。区域内每个Region都包含一个巨型对象,剩余空间不再使用,造成空间碎片化。当G1没有合适的空间分配巨型对象时,G1会启动串行FullGC来释放空间。可以通过增加-XX:G1HeapRegionSize来增加Region的大小,这样相当一部分巨型对象就不再是巨型对象了。而是使用普通的分配方式,不设置Young区的大小。原因是尽量满足目标停顿时间,逻辑Young区会动态调整。如果设置了大小,它将覆盖并禁用暂停时间的控制。平均响应时间设置以应用程序的平均响应时间为参考来设置MaxGCPauseMillis。JVM会尽量满足这个条件,可能90%的请求或者更多的响应时间都在这个范围内,但并不代表所有的请求都能得到满足。如果平均响应时间设置的太小,会导致频繁的GC调优方式和思路。如何分析系统JVMGC运行状态并合理优化?GC优化的核心思想是尽可能在年轻代分配和回收对象,避免过多的对象进入老年代,造成老年代垃圾回收频繁,同时提供足够的为系统减少新生代垃圾回收次数的内存。系统分析和优化也是围绕这个思路进行的1分析系统的运行状态每秒系统请求数,每次请求创建多少个对象,占用多少内存,YoungGC触发的频率,对象进入老年代的速率、老年代占用的内存、FullGC的触发频率、FullGC触发的原因、??长时间FullGC的原因。常用的工具如下:jstatjvm自带的命令行工具,可以用来统计内存分配率、GC次数、GC耗时。常用命令格式为jstat-gc<统计间隔时间><统计次数>复制代码输出的返回值含义如下:例如:jstat-gc32683100010,统计进程pid=32683,每秒计数一次,计数10次jmapjvm自带命令行工具,可以用来了解系统运行时的对象分布情况。常用命令格式如下//命令行输出类名、类数、类内存大小,//根据类内存大小降序排列jmap-histo//生成堆内存转储快照,导出dump.hrpof当前目录下的二进制文件,//可以使用eclipse的MAT图形化工具进行分析jmap-dump:live,format=b,file=dump.hprofCopycodejinfocommandFormatjinfoCopythe查看运行Java应用扩展参数的代码,包括Java系统属性和JVM命令行参数其他GC工具监控告警系统:Zabbix、Prometheus、Open-Falconjdk自动实时内存监控工具:VisualVM堆外内存监控:JavaVisualVM安装BufferPools插件,googleperf工具、JavaNMT(NativeMemoryTracking)工具GC日志分析:GCViewer、gceasyGC参数检查与优化:xxfox.perfma.com/2GC优化案例数据分析平台系统频繁FullGC平台主要对APP中用户行为进行定时分析统计和支持报表导出。使用CMSGC算法,数据分析师发现系统页面在使用过程中经常卡顿。通过jstat命令发现,每次YoungGC后,系统中大约有10%的存活对象进入老年代。原来是Survivor区空间设置太小了。每次YoungGC后,存活的对象都不能放在Survivor区,提前进入老年代。通过增加Survivor区的大小,Survivor区可以容纳YoungGC后存活的对象。Survivor区的对象经历了很多次YoungGC达到年龄阈值才进入老年代。调整后,每次YoungGC后进入老年代的存活对象只有几百Kb稳定运行,FullGC的频率大大降低。业务连接网关OOM网关主要是消费Kafka数据,进行数据处理后计算并转发到另一个Kafka队列,系统运行几个小时就出现OOM,重启系统几个小时就出现OOM。通过jmap导出堆内存,通过eclipseMAT工具分析后发现原因:某业务Kafka主题数据的主题数据异步打印。业务数据量大,内存中堆积大量对象等待打印,导致OOM账户权限管理系统频繁且长期的FullGC。该服务经常不可用。通过Zabbix的监控平台监控,发现系统频繁发生长时间的FullGC,触发时老年代的堆内存通常都未满。原来业务代码中调用了System.gc()来总结GC问题。可以说没有捷径可走。解决在线性能问题并不容易。除了掌握本文介绍的原理和工具,我们还需要不断积累经验,真正做到性能最优。