【JVM】元空间内存溢出导致频繁FullGC,CPU飙高

【JVM】元空间内存溢出导致频繁FullGC,CPU飙高
【一】问题描述页面卡住查看服务器资源CPU飙高top -H看到线程多是GC Thread而内存占用并不高没有触发OOM。服务重启后CPU降到14%页面恢复正常。【二】排查过程【1】查看服务器1查看进程ps-aux--sort-%cpu|head-102查看进程内线程# 查看进程内线程按CPU排序top-Hpjava\_pidtop-Hp418243jstack定位# 高CPU线程转16进制后续jstack定位printf%x\\n线程TIDprintf%x\\n418644JDK8 无动态日志直接看启动参数是否配了-Xloggc拉取 gc.log 分析把日志拉到本地分析5服务重启以后CPU降低了很多从70%多降到18%内存基本没有变化说明之前的页面卡顿主要就是因为CPU飙高导致的gc日志没有那么多Full GC了线程也回归正常【2】分析gc日志1日志案例2026-07-16T27:21.2670800:4747.908:[Full GC(Metadata GC Threshold)[PSYoungGen: 7595K-32K(2795520K)][ParOldGen: 282745K-275182K(8388096K)][Metaspace: 233597K-233597K(1282048K)],0.0134125secs][Times:user0.04sys0.00,real0.01secs]1第一部分时间戳2026-07-16T27:21.2670800: 4747.908:2026-07-16T27:21.2670800本地时区时间精确到毫秒用来定位什么时候发生 GC匹配业务日志看当时在跑什么接口 / 定时任务。4747.908JVM 启动后经过的秒数程序运行第 4747.9 秒触发这次 Full GC。2第二部分GC 类型 GC 触发原因最核心判断故障根源[Full GC (Metadata GC Threshold)GC 类型YGC / GC年轻代回收Minor GC只清理 EdenS0/S1STW 停顿短Full GC整堆回收新生代 老年代 元空间STW 停顿长会抢占大量 CPU页面卡死元凶括号内触发 Full GC 的根因线上排查第一优先级日志出现两种成对出现的原因Metadata GC Threshold元空间内存达到扩容阈值JVM 主动触发 Full GC尝试卸载无用 Class 释放元空间出现这个 程序在持续加载新类、元空间不停扩容。Last ditch collection兜底 Full GC上一次 Metadata GC 回收不到内存JVM 强制再做一轮完整全堆回收属于补救回收连续成对出现代表元空间回收完全失效类卸载失败。其他常见触发原因补充方便你以后对照Allocation Failure新生代内存不够分配新对象触发 YGCErgonomics老年代空间不足自动触发 Full GCSystem.gc()代码手动调用 System.gc () 强制全堆回收Metadata GC Overhead Limit Exceeded元空间占用过高OOM 前兆。3第三部分分代内存数据三段格式统一回收前 - 回收后 (总容量)[PSYoungGen: 7595K-32K(2795520K)] [ParOldGen: 282745K-275182K(8388096K)] [Metaspace: 233597K-233597K(1282048K)]PSYoungGen 新生代Parallel Scavenge 收集器格式回收前占用 - 回收后占用 (新生代总容量)7595KGC 执行前新生代已使用内存32KGC 结束后新生代剩余内存差值 本次 GC 回收掉的短期对象你这里几乎清空说明新生代对象都能正常回收新生代无瓶颈(2795520K)新生代总大小≈2.7G容量充足。ParOldGen 老年代Parallel Old 收集器格式同上回收前 - 回收后 (老年代总容量)282745K-275182K每次 Full GC 能释放一小部分老年代内存但无法彻底清空(8388096K)老年代总容量≈8G堆内存空间很大排除堆内存不足 OOM。Metaspace 元空间本次故障核心字段233597K-233597K(1282048K)233597KGC 前元空间占用 233MB-233597K回收后大小完全不变—— 关键故障信号没有任何类被卸载存在类元数据泄漏(1282048K)元空间最大上限≈1.22G当前只用到 233M还没到上限但不停触发 GC。4第四部分单次 GC 耗时, 0.0134125 secs本次 Full GC 完整耗时单位秒你这里单次仅 10ms单次停顿很短。卡顿不是单次 GC 慢而是每秒多次连续 Full GCCPU 被 GC 线程永久抢占。5第五部分Times CPU 耗时判断 CPU 消耗[Times: user0.04 sys0.00, real0.01 secs]real0.01实际业务停顿时间STW 时长用户感知卡顿的核心user0.04GC 线程在用户态 CPU 总耗时user real 代表 GC 多线程并行跑大量 CPU 核心被 GC 任务占满对应你 top -H 看到一堆 GC task 线程sys内核态 CPU 耗时一般很小。2GC 日志关注点1GC 类型 触发原因定位根本故障1只要大量出现Full GC (Metadata GC Threshold)故障 元空间持续加载新 Class且类无法卸载 → 类泄漏2大量Full GC (Ergonomics)老年代满了要么堆太小要么业务内存泄漏3大量GC (Allocation Failure)新生代太小瞬时创建大量短对象4频繁成对出现Metadata GC Last ditch元空间回收完全失效线上紧急处理。2Metaspace 回收前后内存变化你这个日志的核心疑点判断标准正常情况GC 后 Metaspace 数值下降旧类被卸载异常GC 前后数值完全不变你的日志就是 233597K→233597K 类泄漏配套观察元空间占用持续缓慢上涨不会回落。3分代内存回收效果区分堆 / 元空间问题PSYoungGen每次 GC 后能打回很低的值 → 新生代正常排除瞬时大对象ParOldGenGC 后内存大幅下降无堆泄漏只是流量高峰内存吃紧GC 后只降一点点长期缓慢上涨堆对象内存泄漏Metaspace不下降 类元数据泄漏你当前场景。4GC 频率判断卡顿来源看时间戳间隔每隔几秒就一次 Full GC持续循环 → GC 持续抢占 CPU业务线程得不到调度网页卡住哪怕单次 STW 只有 10ms一分钟几十次全堆回收整体 CPU 全部耗尽。5GC 耗时 Times 指标user 远大于 realGC 多线程并行吃满 CPU对应top -H一堆 GC task 线程高占用real 数值大100ms单次停顿长用户直接感觉到页面超时、接口抖动。3日志分析结论GC 类型 原因全是 Full GC成对触发 Metadata 扩容 兜底回收Metaspace 字段回收前后无变化类完全卸载不掉元数据泄漏堆分代新生代、老年代容量充足每次回收都能释放堆内存堆无泄漏耗时单次 Full GC 仅 10ms但频率极高GC 线程长期占满 CPU结论页面卡死根因不是堆内存是动态类频繁生成 类卸载失败高频触发 Full GC 吃光 CPU。4为什么频繁触发 Metadata GC Threshold元空间存放类、注解、反射、动态代理、动态生成 Class 的元数据触发条件二选一程序持续动态生成大量 Class高频反射、动态代理MyBatis、Feign、AOP脚本引擎Groovy/QLExpress 动态编译脚本热加载框架、动态 SQL 解析、CGLIB 频繁创建代理类元空间自动扩容机制JDK8 Metaspace 无固定初始大小动态扩容每次扩容前会触发 Full GC 尝试释放无用 Class5为什么 Full GC 后 Metaspace 内存完全不下降233597K-233597K说明没有任何类被卸载存在类元数据泄漏自定义类加载器未销毁框架自定义 ClassLoader 加载 Class 后加载器长期存活Class 无法卸载ThreadLocal / 静态集合持有 Class 对象、反射 Method/Field 引用第三方中间件 / 数据同步框架DataX、Canal、Doris 客户端频繁创建动态类且不释放循环内重复创建代理类、动态脚本旧类引用无法被 GC Roots 回收6堆内存佐证不是堆内存溢出新生代 2.7G、老年代 8G每次 Full GC 老年代内存可回落堆内存资源充足没有Allocation Failure分配失败 YGC、没有Full GC (Ergonomics)堆内存不足的日志排除大对象、查询分页、堆内存过小问题故障 100% 锁定元空间类泄漏【3】分析Metaspace元空间内存1Metaspace元空间的相关配置MetaspaceSize初始元空间大小避免频繁扩容触发 GCMaxMetaspaceSize限制元空间最大占用防止无限膨胀/home/damp/server/jdk/jre/bin/java-Xms8G-Xmx8G-XX:MetaspaceSize256m-XX:MaxMetaspaceSize256m-XX:SurvivorRatio8-XX:PrintGCDetails-XX:PrintGCDateStamps-Xloggc:/home/damp/server/logs/damp-ind/gc/gc-20260716163643.log-XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath/home/damp/server/logs/damp-ind/heap.dump-Dserver.port20119-cp/home/damp/server/services/damp-ind/conf/:/home/damp/server/driver/*:/home/damp/server/ext/*:/home/damp/server/services/damp-ind/lib/* com.hzyatop.damp.Application --spring.config.additional-location/home/damp/server/conf/bootstrap.yml上述的配置元空间的大小配置是256m但是不能简单的调大内存因为GC日志显示的是FullGC也没有清除掉元空间的内存所以导致不断的触发FullGC所以需要找出是什么类导致的2开启类卸载日志定位元空间泄漏来源-XX:TraceClassLoading-XX:TraceClassUnloading重启后观察日志确认 Class 只加载、无卸载日志实锤类泄漏3长期代码 / 配置修复脚本 / 动态代理优化复用脚本编译实例、缓存代理类避免循环重复生成 Class自定义 ClassLoader任务执行完成后销毁加载器主动释放资源ThreadLocal 清理所有 ThreadLocal 使用后调用 remove ()避免持有类引用JVM 参数优化配套Parallel GC 适配你的 PS 收集器-XX:UseParallelGC-XX:ParallelGCThreads4# 限制GC线程防止吃光服务器CPU-XX:ClassUnloadingWithConcurrentMark【4】拉取堆内存快照分析1堆内存快照内存溢出 / 内存泄漏jmap dump 只会冻结执行命令那一刻JVM 存活的所有对象、ClassLoader、Class 对象不会记录过去 1 小时内什么时候加载类、加载了多少、中途卸载了哪些、元空间内存曲线变化。# 先查看进程ps-aux--sort-%cpu|head-10# 堆dump生产建议低峰执行会STW# ./jmap -dump:formatb,fileheap.hprof 73110jmap-dump:formatb,fileheap.hprofpid# 或者生成bin文件# jmap -dump:formatb,fileheap.bin 73110jmap-dump:formatb,fileheap.binpid# 查看堆概况快速判断新生代/老年代占用jmap-heappid# 查看对象统计快速定位大对象jmap-histopidhisto.txt上述命令配置好jdk的环境变量后可以直接执行了来到目录下能看到拿到的堆内存快照2解决jmap提示bash: jmap: 未找到命令要使用jmap必须保证虚拟机中已经安装了java-1.8.0-openjdk-devel-debug或java-1.7.0-openjdk-devel-debug或java-1.6.0-openjdk-devel-debug。还有就是jdk没有配置环境变量。验证是否有安装#方法 1which java 先找到 java 命令软链最快直接得到你当前启动脚本里使用的 JRE 路径。#注意这是java命令文件真实 JDK 根目录往上退 2 层/home/damp/server/jdk/whichjava#方法 2ls -l 追踪软链接系统自带 java 会多层软链ls-l$(whichjava)#方法 3java -XshowSettings:properties -version 精准获取 java.home推荐#JDK 根目录去掉末尾/jre即可/home/damp/server/jdkjava-XshowSettings:properties-version21|grepjava.home#方法 4从运行中的 Java 进程获取对应你这个服务# 替换为你的java进程PIDcat/proc/进程PID/environ|tr\0\n|grepJAVA_HOME#方法 5查看系统环境变量 JAVA\_HOMEecho$JAVA_HOME或者直接使用进程里有jdk的路径ps-aux--sort-%cpu|head-10在 /home/damp/server/jdk/jdk1.8.0_371/bin 目录下找到jmap工具1方案一永久配置环境变量所有终端生效vim/etc/profile# 文件末尾添加exportJAVA_HOME/home/damp/server/jdk/jdk1.8.0_371exportPATH$PATH:$JAVA_HOME/bin# 配置生效source/etc/profile# 验证echo$JAVA\_HOMEwhichjmap jmap-version2方案二不想改全局编辑用户目录.bashrcvim~/.bashrc# 添加同样两行JAVA_HOME配置source~/.bashrc3方案三全路径格式# jmap 查看堆概况/home/damp/server/jdk/bin/jmap-heap你的java进程PID# 导出dump/home/damp/server/jdk/bin/jmap-dump:formatb,fileheap.hprof 你的PID# jstack 线程栈/home/damp/server/jdk/bin/jstack 你的PID# jstat 监控GC/home/damp/server/jdk/bin/jstat-gcPID10004方案四命令加上././jmap-dump:formatb,fileheap.hprof73110jmap不加 ./系统会从PATH 环境变量里定义的目录列表依次搜索名为jmap的可执行文件找到第一个就执行。如果已经把 JDK 的bin目录加入 PATH配置过 JAVA_HOME直接能执行如果你只把 jre/bin 放进 PATH、没加 jdk/bin你当前场景PATH 里找不到 jmap会报bash: jmap: 未找到命令。./jmap带 ./.代表当前终端所在目录./jmap强制让系统只在当前目录找jmap文件不会去 PATH 里搜索。只有你cd进入到 jdk/bin 目录后这条命令才生效3线程栈确认业务线程阻塞# 连续3次jstack间隔2秒对比线程变化jstackpidstack1.txtsleep2jstackpidstack2.txtsleep2jstackpidstack3.txt4使用JDK的VisualVM分析堆快照JDK为我们提供了监测工具VisualVM我们直接在 终端或者cmd 中输入 jvisualvm 命令就可以进入文件-装入heap.bin查看右侧 类 进行查看具体 实例数排序正下方可以搜索关键yatop 就可以定位到最靠前的哪个类有问题heap.hprof5堆快照分析Eclipse MAT方案1、然后使用linux里面的jdk1.8命令 jhat heap.bin -- 启动服务 http://172.17.0.59:7000 查看Show instance countsforall classes(excluding platform)或者 Show heap histogram 搜索关键字yatop 就可以定位到最靠前的哪个类有问题 方案2、然后使用windows里面的jdk命令 JDK为我们提供了监测工具VisualVM我们直接在 终端或者cmd 中输入 jvisualvm 命令就可以进入 打开C:\Program Files\Java\jdk1.8.0_341\bin\jvisualvm.exe文件-装入heap.bin 查看右侧 类 进行查看具体 实例数排序正下方可以搜索关键yatop 就可以定位到最靠前的哪个类有问题 方案3、使用软件eclipse mat File - Open Heap Dump - 打开heap.bin文件 在右侧default_report_org…… 的title 中间会出现 Problem Suspect123等 逐一点击下方 蓝色关键字 See stacktrace 就可以看到哪里代码 哪行有问题 ctrl F 搜索打开 heap.hprof执行Leak Suspects Report看嫌疑泄漏对象查看 Dominator Tree占用堆最多的对象查看 Histogram对象数量 占用大小查看引用链 GC Roots找到是谁持有对象无法回收静态变量、ThreadLocal、线程池【5】快速分析处理出现JVM的空间问题快速保存堆快照进行对象分析# 先查看进程ps-aux--sort-%cpu|head-10# 堆dump生产建议低峰执行会STW# ./jmap -dump:formatb,fileheap.hprof 73110jmap-dump:formatb,fileheap.hprofpid# 查看堆概况快速判断新生代/老年代占用jmap-heappid# 查看对象统计快速定位大对象jmap-histopidhisto.txt【三】JVM参数【1】案例分析/home/damp/server/jdk/jre/bin/java-Xms8G-Xmx8G-XX:MetaspaceSize256m-XX:MaxMetaspaceSize256m-XX:SurvivorRatio8-XX:PrintGCDetails-XX:PrintGCDateStamps-Xloggc:/home/damp/server/logs/damp-ind/gc/gc-20260716163643.log-XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath/home/damp/server/logs/damp-ind/heap.dump-Dserver.port20119-cp/home/damp/server/services/damp-ind/conf/:/home/damp/server/driver/*:/home/damp/server/ext/*:/home/damp/server/services/damp-ind/lib/* com.hzyatop.damp.Application --spring.config.additional-location/home/damp/server/conf/bootstrap.yml1堆内存-Xms8G -Xmx8G2元空间参数-XX:MetaspaceSize256m -XX:MaxMetaspaceSize256mJDK8 机制元空间使用达到MetaspaceSize阈值时强制触发 Full GC尝试卸载类上述参数把初始阈值和上限锁死 256M只要元空间占用接近 256M会无限循环Metadata GC Threshold Full GC上限设置过小业务存在动态类生成动态代理、反射、脚本、DataX/Canal 动态类256M 极易打满阈值持续 Full GC。3新生代比例-XX:SurvivorRatio8Parallel ScavengePS 收集器默认 SurvivorRatio8 本身没问题但没有手动指定新生代总大小-Xmn新生代大小会由 JVM 自适应 ergonomics 动态调整8G 堆下新生代容量波动不可控同时未设置晋升阈值-XX:MaxTenuringThreshold短期对象容易过早晋升到老年代加重老年代压力。4GC 日志参数-XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:/xxx/gc.log日志信息不全缺少关键排查日志无 GC 耗时、应用吞吐量打印无类加载 / 卸载日志无法定位元空间类泄漏日志不切割长期运行单个 gc.log 文件巨大无法归档回溯缺少安全点日志STW 卡顿排查必备5OOM Dump 参数-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/home/damp/server/logs/damp-ind/heap.dump固定文件名 heap.dump多次 OOM 会直接覆盖快照丢失历史现场仅 OOM 才 dump当前高频 Full GC 属于元空间泄漏不会触发 OOM抓不到故障快照dump 时无超时控制大堆 8G dump 会长时间 STW 卡死服务。6GC 收集器隐藏最大短板你日志是 PSParOld 并行 GC整条启动命令没有显式指定 GC 收集器JDK8 默认使用 Parallel GCPS/ParOldParallel GC 是吞吐量收集器Full GC 是全 STW一旦频繁 Full GC整个应用完全停顿、CPU 被 GC 线程占满网页卡死完全不适合 Web 在线业务。【2】优化方案1元空间参数# 初始阈值512M达到512M才会触发扩容GC上限放大1.5G预留缓冲-XX:MetaspaceSize512m-XX:MaxMetaspaceSize1536m# 开启并发类卸载提升元空间回收能力-XX:ClassUnloadingWithConcurrentMark# 打印类加载/卸载日志定位动态类泄漏源头-XX:TraceClassLoading-XX:TraceClassUnloadingMetaspaceSize 是扩容触发阈值不要和 MaxMetaspaceSize 相等留出缓冲区间避免一靠近上限就疯狂 Full GC上限 1.5G 满足动态代理、脚本、同步组件的类加载需求TraceClassLoading/Unloading 可以观察是否只加载类、不卸载精准定位类泄漏代码。2GC 收集器替换当前默认 Parallel GC 全 STW Full GCWeb 在线业务推荐G1GCFull GC 大幅缩短停顿并发标记减少 STW追加参数替换收集器# 使用G1垃圾收集器-XX:UseG1GC# 目标最大停顿100ms适配网页接口低延迟需求-XX:MaxGCPauseMillis100# G1区域块大小-XX:G1HeapRegionSize4m# 并发标记线程数容器环境限制CPU占用-XX:ConcGCThreads2# 混合GC阈值提前回收老年代-XX:G1MixedGCLiveThresholdPercent70# 避免元空间打满强制Full GC-XX:AlwaysPreTouch3堆与新生代精细化调优8G 堆配套旧参数仅-XX:SurvivorRatio8补充新生代固定大小、晋升阈值-Xms8G-Xmx8G# 固定新生代2G8G堆合理分配避免自适应波动-Xmn2g# 原SurvivorRatio保留Eden:S0:S18:1:1-XX:SurvivorRatio8# 对象最大晋升年龄短期对象多经过几次YGC再进老年代减少老年代占用-XX:MaxTenuringThreshold10# 新生代内存预分配减少运行时内存页分配卡顿-XX:AlwaysPreTouch4限制 GC 线程解决 top -H GC 线程吃光 CPUParallel GC / G1 通用防止 GC 线程抢占全部 CPU# Parallel GC并行GC工作线程STW阶段容器4核机器限制4以内-XX:ParallelGCThreads45GC 日志增强 滚动切割旧日志单文件无切割补充完整日志参数# 原有基础上新增-XX:PrintGCApplicationStoppedTime-XX:PrintGCApplicationConcurrentTime-XX:PrintSafepointStatistics-XX:PrintSafepointStatisticsCount100# JDK8日志滚动切割按大小拆分防止单文件几十G-XX:UseGCLogFileRotation-XX:NumberOfGCLogFiles10-XX:GCLogFileSize100M作用打印安全点停顿、应用运行 / 停顿时间日志自动切割归档便于回溯 GC 异常时段。6OOM Dump 参数优化旧配置固定文件名会覆盖修改带时间戳 dump增加 dump 超时# OOM dump带时间戳不覆盖历史快照-XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath/home/damp/server/logs/damp-ind/heap-%t.hprof# dump最长等待10秒防止8G堆dump长时间STW-XX:HeapDumpTimeout10# 元空间溢出同样输出dump-XX:DumpOnClassLoadFailure7堆外内存限制IO、数据库、同步组件防堆外泄漏# 限制最大堆外直接内存2G-XX:MaxDirectMemorySize2G8高并发 Web 通用优化参数# 关闭偏向锁高并发多线程竞争场景减少锁开销-XX:-UseBiasedLocking# 加大代码缓存动态代理、反射大量生成字节码-XX:ReservedCodeCacheSize512m# 禁用System.gc()手动Full GC防止第三方框架乱调用-XX:DisableExplicitGC注意如果框架依赖System.gc()做资源释放去掉-XX:DisableExplicitGC。【3】整合优化后完整启动 JVM 参数/home/damp/server/jdk/jre/bin/java\# 堆内存固定8G新生代2G-Xms8G-Xmx8G-Xmn2g\# 新生代比例晋升阈值-XX:SurvivorRatio8-XX:MaxTenuringThreshold10\# 元空间修复解决高频Metadata Full GC核心-XX:MetaspaceSize512m-XX:MaxMetaspaceSize1536m\-XX:ClassUnloadingWithConcurrentMark\-XX:TraceClassLoading-XX:TraceClassUnloading\# G1收集器替换默认Parallel GC降低STW-XX:UseG1GC-XX:MaxGCPauseMillis100\-XX:G1HeapRegionSize4m-XX:ConcGCThreads2\-XX:G1MixedGCLiveThresholdPercent70\# 限制GC线程防止吃光CPU-XX:ParallelGCThreads4\# 内存预分配-XX:AlwaysPreTouch\# 堆外内存限制-XX:MaxDirectMemorySize2G\# GC完整日志滚动切割-XX:PrintGCDetails-XX:PrintGCDateStamps\-XX:PrintGCApplicationStoppedTime-XX:PrintGCApplicationConcurrentTime\-XX:PrintSafepointStatistics-XX:PrintSafepointStatisticsCount100\-XX:UseGCLogFileRotation-XX:NumberOfGCLogFiles10-XX:GCLogFileSize100M\-Xloggc:/home/damp/server/logs/damp-ind/gc/gc-20260716163643.log\# OOM快照优化不覆盖-XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath/home/damp/server/logs/damp-ind/heap-%t.hprof\-XX:HeapDumpTimeout10-XX:DumpOnClassLoadFailure\# 高并发优化-XX:-UseBiasedLocking-XX:ReservedCodeCacheSize512m\-XX:DisableExplicitGC\# 原有业务参数不变-Dserver.port20119\-cp/home/damp/server/services/damp-ind/conf/:/home/damp/server/driver/*:/home/damp/server/ext/*:/home/damp/server/services/damp-ind/lib/*\com.hzyatop.damp.Application\--spring.config.additional-location/home/damp/server/conf/bootstrap.yml