作者:京东批发 王江波

阐明:局部素材来源于网络,数据分析全为实在数据。

一、 问题背景

自建的两套工具,运行一段时间后均呈现 内存占用高触发报警,频繁young gc且成果不佳。已经尝试屡次解决,因各种起因耽误,最近下定决心解决此问题。

二、 问题形容

Q:堆内存1018M,应用达到950M左右触发一次young gc,ygc之后内存占用630M,未产生full gc

三、 容器配置

已解决要害信息

•主机名 xxx

•实例ID xxx

•ip

•操作系统名称Linux

•操作系统体系结构amd64

•CPU个数2

•JRE版本1.8.0_191

•JVM启动工夫2023-02-18 17:14:10.873

•启动门路/export/App

•Full GCPS MarkSweep

•Young GCPS Scavenge

•过程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=Asia/Shanghai -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、 young gc的机会?

R:ygc在新生代的eden区满了之后就会触发,采纳复制算法回收新生代的垃圾。

2、 为何young gc后堆内存使用率依然很高?

R:新生代过小,大对象间接进入老年代,编码时要尽量减少大对象的应用。

3、 full gc 的机会?

R:HotSpot VM里,除了CMS之外,其它能收集老年代的GC都会同时收集整个GC堆,包含新生代

1) 产生fgc之前进行查看,如果 老年代可用的间断内存空间 < 新生代历次ygc后升入老年代的对象总和的均匀大小,此时先触发old gc,而后执行ygc

2) 执行ygc之后有一批对象须要放入老年代,此时老年代没有足够空间寄存对象,必须触发一次ogc

3) 老年代内存使用率超过阈值,也要触发ogc

4) 元空间有余时也会触发一次

4、 什么起因导致内存占用高?

R:heap dump剖析、gc日志

五、 回收器

•吞吐量优先(Parallel Scavenge):新生代收集器,侧重于吞吐量(吞吐量=运行用户代码工夫/(运行用户代码工夫+运行垃圾收集工夫))的管制。

•Serial Old:Serial Old是Serial收集器的老年代版本, 它同样是一个单线程收集器, 应用标记-整顿算法:因为老年代 Serial Old 收集器在服务端利用性能上的“连累”, 应用Parallel Scavenge 收集器也未必能在整体上取得吞吐量最大化的成果。因为单线程的老年代收集中无奈充分利用服务器多处理器的并行处理能力, 在老年代内存空间很大而且硬件规格比拟高级的运行环境中, 这种组合的总吞吐量甚至不肯定比ParNew加CMS的组合来得优良。

•Parallel Old :是 Parallel Scavenge 收集器的老年代版本, 反对多线程并发收集, 基于标记-整顿算法实现. 这个收集器是直到 JDK 6 时才开始提供的, 在此之前, 新生代的 Parallel Scavenge收集器始终处于相当难堪的状态, 起因是如果新生代抉择了 Parallel Scavenge 收集器, 老年代除了 Serial Old(PS MarkSweep)收集器以外别无选择

UseParallelGC: JDK9 之前虚拟机运行在 Server 模式下的默认值, 应用 ParallelScavenge + Serial Old(PS MarkSweep)的收集器组合进行内存回收。

UseParallelOldGC: 关上此开关后, 应用 Parallel Scavenge + Parallel Old 的收集器组合进行内存回收。

•ParNew收集器与Parallel收集器相似:此收集器是许多运行在Server模式下的虚拟机的首选,除了Serial收集器外,只有它能与CMS收集器配合工作

•并发标记革除(CMS)回收器:是一种以获取最短回收进展工夫为指标的回收器,该回收器是基于“标记-革除”算法实现的。重视回收时产生的进展工夫,是进展工夫最短的收集器。非常适合重视用户体验的利用上,是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=目录 // oom时生成dump文件

// ================================= 附加参数如下 =============================================

// 能够应用-XX:DisableExplicitGC选项禁用显示调用Full gc(system.gc在cms gc下咱们通过-XX:+ExplicitGCInvokesConcurrent来做一次略微高效点的GC(成果比Full GC要好些))

-XX:+UnlockExperimentalVMOptions // 解锁试验参数,容许应用实验性参数,JVM中有些参数不能通过-XX间接复制须要先解锁,比方要应用某些参数的时候,可能不会失效,须要设置这个参数来解锁 -XX:+UseParNewGC // 真正的并发收集器,与CMS搭配应用(ParNew收集器是Serial收集器的多线程版本,应用这个参数后会在新生代进行并行回收,老年代仍旧应用串行回收。新生代S区任然应用复制算法) -XX:+UseConcMarkSweepGC // 和应用程序线程一起执行,绝对于Stop The World来说虚拟机进展工夫较少。进展缩小,吞吐量会升高。它应用的是 标记革除算法,运作过程为四个步骤,别离是 初始标记—并发标识—从新标记—并发革除。它是老年代的收集算法,新生代应用ParNew收集算法。默认敞开

CMS收集器的毛病是对服务器CPU资源较为敏感,在并发标记时会升高吞吐量。它应用的标记革除算法也会产生大量空间碎片,空间碎片的存在会加大Full GC的频率,尽管老年代还有足够的内存,然而因为内存空间不间断,不得不进行Full GC -XX:CMSInitiatingOccupancyFraction=75 // 容许最大占用占用率 -XX:+UseCMSInitiatingOccupancyOnly // 只用设定的回收阈值,如果不指定,JVM仅在第一次应用设定值,后续则主动调整 -XX:+ExplicitGCInvokesConcurrent // ExplicitGCInvokesConcurrent这个参数是配合CMS应用的,开启后System.gc()还是会触发Full GC,不过并不是一个齐全的stop-the-world的Full GC,而是并发的CMS GC -XX:+ParallelRefProcEnabled // 能够用来并行处理 Reference,以放慢处理速度,缩短耗时 -XX:+CMSParallelRemarkEnabled // 在CMS GC前启动一次ygc,目标在于缩小old gen对ygc gen的援用,升高remark时的开销-----个别CMS的GC耗时 80%都在remark阶段

// 切记以下参数不要乱加

-XX:+UseCMSCompactAtFullCollection // Full GC后,进行一次整顿,整顿过程是独占的,会引起进展工夫变长。仅在应用CMS收集器时失效。

-XX:CMSFullGCsBeforeCompaction // 默认为 0,就是每次FullGC都对老年代进行碎片整顿压缩,倡议放弃默认

2、 gc日志指标

S0: 新生代中Survivor space 0区已应用空间的百分比

S1: 新生代中Survivor space 1区已应用空间的百分比 E: 新生代已应用空间的百分比 O: 老年代已应用空间的百分比 M: 元空间已应用空间的百分比

CCS: 压缩类空间利用率百分比 YGC: 从应用程序启动到以后,产生Yang GC 的次数

YGCT: 从应用程序启动到以后,Yang GC所用的工夫【单位秒】 FGC: 从应用程序启动到以后,产生Full GC的次数 FGCT: 从应用程序启动到以后,Full GC所用的工夫 GCT: 从应用程序启动到以后,用于垃圾回收的总工夫【单位秒】

3、 最大线程数

零碎可创立最大线程数 = (机器自身可用内存 - JVM调配的堆内存 - JVM元数据区)/ 线程栈大小

4、 内存剖析

4.1 常用命令

4.1.1 jstack

// Java堆栈跟踪工具,它用于打印出给定的java过程ID、core file、近程调试服务的Java堆栈信息.

jstack命令用于生成虚拟机以后时刻的线程快照线程快照是以后虚拟机内每一条线程正在执行的办法堆栈的汇合,应用其次要目标是定位线程呈现长时间进展的起因,如线程间死锁、死循环、申请内部资源导致长时间期待等问题线程呈现进展的时候通过jstack来查看线程的调用堆栈,就能够晓得没有响应的线程到底在做什么事,或者期待什么资源若java程序解体生成core文件,jstack可用来取得core文件的java stack和native stack的信息

还可从属到正在运行的java程序上,看到以后运行的java程序的java stack和native stack信息,若出现hung状态,jstack很有用

利用:jstack [option] <pid> // 打印某个过程的堆栈信息

•线程状态

•Monitor监督锁

item1:实战案例1 jstack剖析死锁问题

死锁指的是两个或两个以上的线程在执行过程中,因抢夺资源而造成的一种相互期待的景象,若无外力作用,将无奈持续进行

jps:终端中输出jsp查看以后运行的java程序

8896

5684 JUnitStarter

17836 Jps

19804 Launcher

文件剖析:【本文件未死锁,仅用于剖析,死锁时会呈现 线程1持有A等B,线程2持有B等A】

"consumer_monitor_report_tp_pre_1_1_3_1677132947525" #7851 daemon prio=5 os_prio=0 tid=0x00007f8d2001b000 nid=0x31c8c waiting on condition [0x00007f8ceeeef000] java.lang.Thread.State: TIMED_WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000000fad6df60> (a java.util.concurrent.CountDownLatch$Sync) ... Locked ownable synchronizers: - None

// consumer_monitor_report_tp_pre_1_1_3_1677132947525 的线程处于TIMED_WAITING状态,持有 - None 锁(无锁),期待 0x00000000fad6df60 的锁

item2:实战案例2 jstack剖析cp过高问题

•通过 top 命令查看各个过程cpu应用状况,默认按cpu使用率高到低排序

•通过 top -Hp pid 查看该过程下,各个线程的cpu应用状况

定位到cpu占用率高的线程之后,应用 jstack pid 命令查看以后java过程的堆栈状态

•nid=0x31c8b 进制转换,文件中,每个线程都有一个nid,查看状态

4.1.2 jstat

// java虚拟机统计信息工具,利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控

近程监控:jstat -gcutil port@ip

S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 81.75 24.57 94.43 95.36 92.41 5649 82.696 2 0.649 83.346 0.00 81.75 26.15 94.43 95.36 92.41 5649 82.696 2 0.649 83.346 0.00 81.75 27.19 94.43 95.36 92.41 5649 82.696 2 0.649 83.346 0.00 81.75 28.81 94.43 95.36 92.41 5649 82.696 2 0.649 83.346 0.00 81.75 29.91 94.43 95.36 92.41 5649 82.696 2 0.649 83.346

阐明:GCT = YGCT + FGCT

4.1.3 jmap-histo

// 查看堆内存中的对象数目、大小统计直方图,如果带上live则只统计活对象

包含对 Heap size 和 垃圾回收情况的监控

4.1.4 jmqp-heap

// 查看过程堆内存应用状况,包含应用的GC算法、堆配置参数和各代中堆内存应用状况

using thread-local object allocation. Parallel GC with 2 thread(s)

Heap Configuration: MinHeapFreeRatio = 0 MaxHeapFreeRatio = 100 MaxHeapSize = 1073741824 (1024.0MB) NewSize = 402653184 (384.0MB) MaxNewSize = 402653184 (384.0MB) OldSize = 671088640 (640.0MB) NewRatio = 2 SurvivorRatio = 8 MetaspaceSize = 67108864 (64.0MB) CompressedClassSpaceSize = 1073741824 (1024.0MB) MaxMetaspaceSize = 17592186044415 MB G1HeapRegionSize = 0 (0.0MB)

Heap Usage: PS Young Generation Eden Space: capacity = 393216000 (375.0MB) used = 381729360 (364.0454864501953MB) free = 11486640 (10.954513549804688MB) 97.07879638671875% used From Space: capacity = 4718592 (4.5MB) used = 3489328 (3.3276824951171875MB) free = 1229264 (1.1723175048828125MB) 73.94849989149306% used To Space: capacity = 4718592 (4.5MB) used = 0 (0.0MB) free = 4718592 (4.5MB) 0.0% used PS Old Generation capacity = 671088640 (640.0MB) used = 633681304 (604.3255844116211MB) free = 37407336 (35.674415588378906MB) 94.4258725643158% used

49351 interned Strings occupying 4857128 bytes.

4.1.5 jmqp-dump

// 生成堆快照(刹时

利用jvisualvm剖析内存dump文件:点击文件->装入->文件类型选“堆”

Visual VM的OQL语言是对HeapDump进行查问,相似于SQL的查询语言,它的根本语法如下:

select <JavaScript expression to select> [ from [instanceof] <class name> <identifier> [ where <JavaScript boolean expression to filter> ] ]

OQL由3个局部组成:select子句、from子句和where子句。select子句指定查问后果要显示的内容。from子句指定查问范畴,可指定类名,如java.lang.String、char[]、[Ljava.io.File;(File数组)。where子句用于指定查问条件。

4.2 加日志后

见调优计划

七、 调优计划1:增大新生代

1、 解析gc日志

元空间调配64M(有余主动调整了),新生代改为480m

•25.939: [Full GC (Metadata GC Threshold) [PSYoungGen: 4133K->0K(461312K)] [ParOldGen: 34773K->36327K(557056K)] 38907K->36327K(1018368K), [Metaspace: 62967K->62967K(1105920K)], 0.3564880 secs] [Times: user=0.72 sys=0.02, real=0.36 secs]

25.939Full GC (Metadata GC Threshold)GC (Allocation Failure)GC (GCLocker Initiated GC)PSYoungGen: 4133K->0K(461312K)ParOldGen: 34773K->36327K(557056K)38907K->36327K(1018368K)Metaspace: 62967K->62967K(1105920K)0.3564880 secsuser=0.72sys=0.02real=0.36 secs
容器已启动工夫产生一次 fgc(元空间不够用导致)年老代无足够空间openJDK的一个bug,只是始终没被修复(https://bugs.openjdk.org/brow...),不必过于关注默认总容量为 0.9*新生代数据变大因为新生代的垃圾进来了GC前堆内存已应用容量→GC堆内存容量(堆内存总容量 = 0.9*新生代 + 老年代)元空间未回收整个GC破费工夫CPU工作在用户态工夫CPU工作在内核态工夫GC总工夫

八、 调优计划2:增大元空间 + CMS & PNEW

以下计划临时没啥问题

九、 长期计划以观后效

阐明:去批量解决,升高对象大小,兴许也是一个不错的计划,待验证。

-Xms1024m -Xmx1024m -Xmn480m -Xss512k -XX:MetaspaceSize=128m -XX:ParallelGCThreads=2 -XX:CICompilerCount=2 -XX:MaxDirectMemorySize=256m -XX:+UnlockExperimentalVMOptions -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExplicitGCInvokesConcurrent -XX:+ParallelRefProcEnabled -XX:+CMSParallelRemarkEnabled -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/export/Logs/xxx/gc.log -XX:GCLogFileSize=100M -XX:+HeapDumpAfterFullGC -XX:HeapDumpPath=/export/Logs/xxx/fgcdump.log

部署稳固后jvm监控图表如下: