乐趣区

关于内存:堆内存持续占用高-且-ygc回收效果不佳-排查处理实践

作者:京东批发 王江波

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

一、问题背景

自建的两套工具,运行一段时间后均呈现 内存占用高触发报警,频繁 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.939 Full 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 secs user=0.72 sys=0.02 real=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 监控图表如下:



退出移动版