当前位置: 首页 > Web前端 > HTML

《堆内存占用持续高,ygc回收效果差》排查实践

时间:2023-03-28 13:33:09 HTML

作者:京东零售 王江波说明:部分素材来源于网络,数据分析均为真实数据。一、问题背景两套自建工具,在运行一段时间后,均因内存占用高、younggc频繁、效果不佳而触发告警。曾多次尝试解决,但由于种种原因耽搁了,最近下定决心要处理这个问题。2、问题描述Q:堆内存1018M,使用达到950M左右触发younggc。ygc后内存占用630M,并没有发生fullgc。系统名称Linux?OS架构amd64?CPU数量2?JRE版本1.8.0_191?JVM启动时间2023-02-1817:14:10.873?启动路径/export/App?FullGCPSMarkSweep?YoungGCPSScavenge?进程ID115135?物理内存大小251.4GB(269888389120Byte)?交换区大小0.0GB(0Byte)?虚拟内存大小12.5GB(13368197120Byte)?应用程序路径/export/App/lib/tp-center-web.jar!/BOOT-INF/lib/JVM启动参数-javaagent:/export/xxx/lib/pfinder-profiler-agent-1.0.8-20210322032622-6f12bda2.jar-Ddeploy.app.name=xxx-Ddeploy.app.id=xxx-Ddeploy.instance.id=0-Ddeploy.instance.name=server1-DJDOS_DATACENTER=HT-Dloader.path=./conf-Dspring.profiles.active=pre-Xms1024m-Xmx1024m-Xmn384m-XX:MetaspaceSize=64m-XX:CMSInitiatingOccupancyFraction=70-XX:+UseCMSInitiatingOccupancyOnly-XX:ParallelGCThreads=2-XX:CICompilerCount=2-XX:MaxDirectMemorySize=128m-Duser.timezone=亚洲/上海-Dcom.sun.management.jmxremote-Dcom.sun.management.jmxremote.port=xxx-Dcom.sun.management.jmxremote.authenticate=false-Dcom.sun.management.jmxremote.ssl=false-Djava.rmi.server.hostname=xxx四、问题分析1、younggc是什么时候?R:ygc会在新生代的eden区满后触发,使用复制算法回收新生代的垃圾。2、为什么younggc后堆内存占用率还是很高?R:新生代太小,大对象直接进入老年代。在编码时,尽量减少使用大对象。3、fullgc的时机?R:在HotSpotVM中,除了CMS,其他能收集老年代的GC会同时收集整个GC堆,包括新生代1)在fgc发生前检查,老年代是否有连续可用的内存空间是<新生代的所有ygcs提升到老年代的总对象的平均大小。这时候先触发oldgc,再执行ygc2)执行ygc后,需要将一批对象放入老年代。此时老年代没有足够的空间存放对象,所以必须触发一次ogc3)老年代内存使用超过阈值时,也会触发ogc4。当元空间不足时,也会触发一次。4.内存占用高是什么原因?R:堆转储分析,gc日志五、收集器吞吐量优先级(ParallelScavenge):新生代收集器,关注吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间))控制。?SerialOld:SerialOld是Serial收集器的旧版本。同样是单线程收集器,使用标记-排序算法:由于旧的SerialOld收集器对服务器应用性能的“拖累”,使用ParallelScavenge收集器可能无法在整体上最大化吞吐量。由于单线程老年代收集无法充分利用服务器多处理器的并行处理能力,在老年代内存空间大、硬件规格比较先进的运行环境下,这种组合的总吞吐量可能甚至高于ParNew。CMS的组合非常出色。?ParallelOld:老版本的ParallelScavenge收集器,支持多线程并发收集,基于标记排序算法实现。这个收集器直到JDK6才提供,在此之前,新一代的ParallelScavenge收集器一直处于比较尴尬的状态,因为如果新生代选择Para??llelScavenge收集器,那么老年代就只能选择SerialOld了(PSMarkSweep)收集器UseParallelGC:JDK9之前虚拟机运行Server模式的默认值,使用ParallelScavenge+SerialOld(PSMarkSweep)的收集器组合进行内存回收。UseParallelOldGC:开启此开关后,使用ParallelScavenge+ParallelOld的收集器组合进行内存回收。?ParNew收集器类似于Parallel收集器:该收集器是许多运行在服务器模式下的虚拟机的首选。除了Serial收集器,只有它可以与CMS收集器一起使用。ConcurrentMarkSweep(CMS)collector:它是一个回收器,其目标是获得最短的恢复停顿时间。回收器是基于“标记-清除”算法实现的。注意回收时产生的停顿时间,是停顿时间最短的收集器。非常适合注重用户体验的应用。它是Hotspot虚拟机的第一个真正的并发收集器。垃圾回收线程和用户线程几乎可以同时运行,这还是第一次。六、优化策略1、添加gc日志-XX:+PrintGCDetails//创建详细的gc日志-XX:+PrintGCTimeStamps-Xloggc:/export/Logs/com/gc.log//gc日志文件名-XX:+UseGCLogFileRotation//限制gc日志保存的数据量-XX:GCLogFileSize=10M//日志文件大小-XX:NumberOfGCLogFiles=10//日志文件个数-XX:+HeapDumpAfterFullGC//保存fullgc前的dump文件-XX:HeapDumpPath=/export/Logs/com/fgcdump.log//dump文件保存路径-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=directory//oom生成dump文件//===================================附加参数如下===============================================//可以使用-XX:DisableExplicitGC选项禁用显示调用Fullgc(cmsgc下的system.gc我们使用-XX:+ExplicitGCInvokesConcurrent做一个稍微高效的GC(效果比FullGC更好))-XX:+UnlockExperimentalVMOptions//解锁实验参数,允许使用实验参数rs。JVM中有些参数不能通过-XX直接复制,需要先解锁。比如有些参数要用到的时候可能不生效,需要设置这个参数用于解锁-XX:+UseParNewGC//真正的并发收集器,配合CMS使用(ParNew收集器是多线程的Serial收集器版本,使用该参数后,新生代会进行并行回收,老年代仍使用Serial回收。新生代S区仍使用复制算法)-XX:+UseConcMarkSweepGC//执行加上应用线程,虚拟机停顿时间小于StopTheWorld。更少的停顿和更低的吞吐量。它采用mark-and-sweep算法,运行过程为初始标记-并发标记-再标记-并发清除四个步骤。是老年代的收集算法,新生代使用ParNew收集算法。默认关闭CMS收集器的缺点是对服务器CPU资源比较敏感,并发标记时会降低吞吐量。它使用的mark-and-clear算法也会产生大量的空间碎片。空间碎片的存在会增加FullGC的频率。虽然老年代还有足够的内存,但是因为内存空间不连续,不得不进行FullGC-XX:CMSInitiatingOccupancyFraction=75//允许最大占用率-XX:+UseCMSInitiatingOccupancyOnly//只使用集合回收threshold,如果不指定,JVM只会在第一次使用设置的值,之后会自动调整-XX:+ExplicitGCInvokesConcurrent//参数ExplicitGCInvokesConcurrent与CMS配合使用。开启后,System.gc()仍然会触发FullGC,但不是完全的stop-the-worldFullGC,而是并发的CMSGC-XX:+ParallelRefProcEnabled//可以用来处理Referencesinparallel加快处理,缩短耗时-一般CMS80%的GC时间都在remark阶段//切记不要乱加以下参数-XX:+UseCMSCompactAtFullCollection//FullGC后,进行一次排序过程。排序过程是排他性的,会造成较长的停顿时间。仅在使用CMS收集器时生效。-XX:CMSFullGCsBeforeCompaction//默认为0,表示每次FullGC都会对老年代进行碎片整理和压缩。建议保持默认2.gc日志指标S0:新生代中Survivor空间0的已用空间百分比S1:新生代中Survivor空间1的已用空间百分比E:已用空间百分比新生代O:老年代已用空间百分比M:元空间已用空间百分比CCS:压缩空间利用率百分比YGC:从应用开始至今,杨GC发生次数YGCT:时间YangGC从应用开始到现在采取的[单位:秒]FGC:从应用开始到现在FullGC发生的次数FGCT:从应用开始到现在,FullGCGCT使用的时间:从应用开始到现在垃圾回收使用的总时间[单位:秒]3.最大线程数系统可以创建的最大线程数=(系统可用内存机器本身-他JVM分配的ap内存——JVM元素数据区)/线程堆栈大小4.内存分析4.1常用命令4.1.1jstack//Java堆栈跟踪工具,用于打印出给定java进程ID的Java堆栈信息、核心文件和远程调试服务。jstack命令用于生成虚拟机当前时刻的线程快照。线程快照是当前虚拟机中每个线程正在执行的方法栈的集合。使用它们的主要目的是定位导致线程长期停顿的原因,例如线程间的死锁和死循环,请求外部资源导致长时间等待等问题。当线程暂停时,使用jstack查看线程的调用栈,就可以知道没有响应的线程在干什么,或者等待什么资源。如果java程序崩溃生成了core文件,jstack可以用来获取core文件的java栈和native栈信息,也可以附加到正在运行的java程序,查看java栈和native栈信息当前运行的java程序。如果是hung状态,jstack很有用Application:jstack[option]//打印一个进程的堆栈信息?线程状态?monitormonitorlockitem1:实际案例1jstack死锁问题分析死锁指的是两个或多个线程在执行过程中,因争夺资源而导致的相互等待的现象。如果没有外力,是无法进行jps的:在终端输入jsp查看当前运行的java程序88965684JUnitStarter17836Jps19804Launcher文件分析:【该文件没有死锁,仅用于分析,发生死锁时,线程1持有A等B,线程2持有B等A】“consumer_monitor_report_tp_pre_1_1_3_1677132947525”#7851守护进程prio=5os_prio=0tid=0x00007f8d2001b000nid=0x31c8c等待条件[0x00007f8ceeeef000]java.lang.Thread.State:TIMED_WAITING(cisparking)停车<0x000000000000fad6df60>(java.util.concurrent.countdownlatch$sync)...锁定已锁定的synchronizers:-none//none//contiger_monitor_monoritor_report_tp_pre_1_1_1_1_1_1_1_1_1_1_1_1_167713294752525252525252525252525252525252525209.:实战案例2jstack分析cp过高的问题。通过top命令查看各个进程的cpu使用情况。默认情况下,它是按cpu使用率从高到低排序的。通过top-Hppid查看进程下各个线程的cpu使用情况。定位cpu使用率高的线程后,使用jstackpid命令查看当前java进程的堆栈状态nid=0x31c8b二进制转换,在文件中,每个线程都有一个nid,查看状态4.1.2jstat//java虚拟机统计工具,利用JVM内置指令在命令行实时监控Java应用的资源和性能远程监控:jstat-gcutilport@ipS0S1EOMCCSYGCYGCTFGCFGCTGCT0.0081.7524.5794.4395.3692.41564982.69620.64983.3460.0081.7526.1594.4395.3692.41564982.69620.64983.3460.0081.7527.1994.4395.3692.41564982.69620.64983.3460.0081.7528.8194.4395.3692.41564982.69620.64983.3460.0081.7529.9194.4395.3692.41564982.69620.64983.346说明:GCT=YGCT+FGCT4.1.3jmap-histo//查看堆内存中的对象个数,大小统计直方图,如果带活的,只统计活的对象包括监控Heap大小和垃圾回收状态4.1.4jmqp-heap//查看进程堆内存使用情况,包括使用的GC算法、堆配置参数以及使用线程本地对象分配的每一代中的堆内存使用情况。具有2个线程的并行GC堆配置:MinHeapFreeRatio=0MaxHeapFreeRatio=100MaxHeapSize=1073741824(1024.0MB)NewSize=402653184(384.0MB)MaxNewSize=402653184(384.0MB)OldSize=671088640Survivatio2Rvivatio2(640.0MB)8METASPACESIZE=67108864(64.0MB)compressedClassSpaceSize=1073741824(1024.0MB)maxMetaspaceSize=17592186044415MBG1HEAPRIGIONIZEIZE=0(0.0MB)953MB)free=11486640(10.954513549804688MB)97.07879638671875%usedFromSpace:capacity=4718592(4.5MB)used=3489328(3.3276824951171875MB)free=1229264(1.1723175048828125MB)73.94849989149306%usedToSpace:capacity=4718592(4.5MB)used=0(0.0MB)免费=4718592(4.5MB)0.0%使用的PS旧生成容量=671088640(640.0MB)使用=633681304(604.32558444116211MB)Free=37407336(37407336).5jmqp-dump//生成堆快照(瞬时)使用jvisualvm分析内存转储文件:点击File->Load->FileType,选择“Heap”VisualVM的OQL语言是查询HeapDump,类似于SQL查询语言,其基本语法如下:select[from[instanceof][where]]OQL由3部分组成:selectsub子句、from子句和where子句select子句指定要在查询结果中显示的内容。from子句指定查询的范围,可以指定类名,如java.lang.String,char[],[Ljava.io.File;(文件数组)。where子句用于指定查询条件。4.2添加日志后,看调优方案7.调优方案一:增加新生代1.分析gc日志元空间分配64M(不够自动调整),新生代改为480m25.939:[FullGC(元数据GC阈值)[PSYoungGen:4133K->0K(461312K)][ParOldGen:34773K->36327K(557056K)]38907K->36327K(1018368K),[元空间:62967K->62967K(1105920K)5se),00.72sys=0.02,real=0.36secs]25.939FullGC(MetadataGCThreshold)GC(AllocationFailure)GC(GCLockerInitiatedGC)PSYoungGen:4133K->0K(461312K)ParOldGen:34773K->36327K(557056K)38907->36327K(1018368K)Metaspace:62967K->62967K(1105920K)0.3564880secsuser=0.72sys=0.02real=0.36secs容器启动时出现一次FGC(metaspace不足导致)openJDK的一个bug,younggeneration没有足够的空间,只是一直没有修复(https://bugs.openjdk.org/brow...),所以不要太在意默认的总容量0.9*新生代数据变大是因为新生代垃圾进来和GC前堆内存的已用容量→GC堆内存容量(总堆内存容量=0.9*新生代+老年代)元空间不回收整个GC耗时CPUworksinusermodetimeCPUworksinkernelmodetimeGCtotaltime8.调优方案二:增加metaspace+CMS&PNEW以下方案暂时没有问题。9.临时解决方案看效果:批量处理,减小对象大小也可能是一个不错的解决方案,有待验证。-Xms1024m-Xmx1024m-Xmn480m-Xss512k-XX:MetaspaceSize=128m-XX:ParallelGCThreads=2-XX:CICompilerCount=2-XX:MaxDirectMemorySize=256m-XX:+UnlockExperimentalVMOptions-XX:+UseParNewGC-XX:+WeepMark-XCMSInitiatingOccup占用=75-XX:+UseCMSInitiatingOccupancyOnly-XX:+ExplicitGCInvokesConcurrent-XX:+ParallelRefProcEnabled-XX:+CMSParallelRemarkEnabled-XX:+PrintGCDetails-XX:+PrintGCTimeStamps-Xloggc:/Xgc:FileGC.Log/xxx=100M-XX:+HeapDumpAfterFullGC内容-XX:HeapDumpPath=/export/Logs/xxx/fgcdump.log部署稳定后jvm监控图如下: