当Java虚拟机(JVM)提示“内存满”时,最直接的结论是:不要盲目加大堆内存,先通过工具定位是内存泄漏还是内存压力,再针对代码或参数做精准治理。这篇文章用排障视角拆解JVM内存打满的完整流程,涵盖堆内存、非堆内存和操作系统层面的排查实操。
从“满”到“崩”:先搞清楚JVM到底哪里满了
很多人听到“Java虚拟机满了”,第一反应是看java.lang.OutOfMemoryError异常,但错误信息后半段往往藏着真正线索,行业共识认为,Java进程占用的内存远不止堆内存,而“满了”发生在不同区域,处理方案天差地别。
JVM内存按用途分为三类:
- 堆内存(Heap):存放对象实例,最常见的是
java.lang.OutOfMemoryError: Java heap space。 - 元空间(Metaspace):存放类的元数据,大量动态生成代理类、反射调用频繁时容易撑爆。
- 线程栈(Stack):每个线程占一块原生内存(通常512KB~1MB),线程数过多时栈空间叠加起来越积越多。
提示Java heap space,代表堆内存耗尽;提示Metaspace,说明加载的类太多;提示unable to create new native thread,意味着操作系统层面的线程数触顶。
还有一个隐蔽场景:直接内存(Direct Memory) 由NIO的ByteBuffer使用,它不受-Xmx控制,很多Java后端服务用Netty做RPC通信,直接内存被Netty申请多了,整体进程内存就会居高不下,这种情况的排查思路和堆内存完全不同。
想要区分堆和非堆使用情况,第一步就用JVM自带的jcmd工具,在命令行执行:
jcmd <pid> VM.native_memory summary
这个命令会返回内存分类占用表,能直观看到Java Heap、Metaspace、Thread和Internal四个区块的内存变化,注意:这个命令需要JVM启动参数里开启-XX:NativeMemoryTracking=summary,生产环境一般默认开着。
java虚拟机内存溢出排查方法:从现象到根因的9个实操步骤
排查思路不能靠猜,要按“观察指标定位线程快照对比代码反推”的顺序推进,下面是实际生产环境最常用的排查步骤。
第1步:保存现场,提取堆转储文件
进程即将挂掉之前的堆快照最有价值,在OutOfMemoryError没有发生时,通过jmap人为触发堆转储:
jmap -dump:live,format=b,file=/data/logs/heap_dump.hprof <pid>
live参数会先触发Full GC再做快照,能过滤掉大部分垃圾对象,文件体积更小,如果JVM已经卡死,执行jcmd <pid> GC.heap_dump往往更稳定。
经验教训:堆转储文件可能很大(2~4GB),务必保证磁盘有足够空间。 优先转储到本地磁盘而非NFS挂载盘,因为大型文件写入网络盘有时会因I/O阻塞拖垮整个系统。
第2步:看GC日志,判断是泄漏还是压力
启动JVM时加上-XX:+PrintGCDetails和-XX:+PrintGCDateStamps是生产环境的基础配置,但2026年的主流做法是使用Unified Logging参数:

-Xlog:gc:/data/logs/gc.log:time,uptime,level,tags
拿到GC日志后,重点看Full GC的规律:
- Full GC频繁但回收后内存降到低位:说明对象生命周期短,分配速率太快,属于内存压力大(配置不足)。
- Full GC间隔越来越短,每次回收后剩余量依然缓慢上升:说明存在对象一直被GC Roots引用,无法回收,属于内存泄漏。
- FULL GC后老年代占用率一直超过95%但程序没报错:可能是系统缓存设计不合理,比如本地缓存全放在HashMap里。
第3步:用jstat观察内存动态趋势
jstat是JDK自带的轻量级监控命令,适合日常巡检:
jstat -gcutil <pid> 5000 10
这个命令每5秒输出一次,共输出10次,重点看从E(Eden,年轻代伊甸园区)、O(Old,老年代)到M(Metaspace)的使用情况,如果O从20%一路涨到80%只有几分钟,基本可以确定存在短时间内的对象滞留。
第4步:分析堆转储,找到大对象
堆转储文件提取后,用自带的jhat命令(JDK9后被废弃)或者Eclipse MAT、VisualVM分析。分析对象引用链是精准定位的关键,比如MAT的Leak Suspects报告会直接给出“0xXXXXXXXXXX”这样的对象地址和线程栈,顺着栈进去就能看到具体业务代码。
最常发现的泄漏对象是数据库连接、HTTP客户端连接池、未关闭的IO流。一个经验法则是:每轮Full GC后对比两次堆快照,能稳定增长的类基本就是泄漏源头。
第5步:用Arthas在线诊断线程状态
生产环境打不了断点,Arthas的thread命令可以查看线程状态,thread -n 3能直接定位CPU占用最高的三个线程,拿到它们的堆栈,执行:
thread -n 3
看到大量WAITING或BLOCKED状态的线程堆栈,配合GC日志中的Allocation Failure(内存分配失败)现象,就能推断出是锁竞争太严重导致对象堆积,还是频繁创建大数组。
第6步:检查Metaspace对应到代码
Metaspace打满的症状是启动一段时间后抛出java.lang.OutOfMemoryError: Metaspace,重启就好了,过一阵子又复发,排查思路走代码层:
- CGLIB、Javassist动态生成class,是否在循环里重复生成?
- 用
jstat -gcutil观察M列,如果持续上涨且永不复位,说明有类加载器泄漏。
第7步:对比操作系统层与JVM层内存
有些时候JVM堆没满,但整个进程内存占用非常高,使用top -Hp <pid>查看进程内线程资源,或ps -o rss, vsz -p <pid>看常驻内存。JVM的堆外内存占用如果超过堆的30%,需要检查Direct Buffer和线程总数。
第8步:检查连接池和框架缓存配置
大量的数据库连接和HTTP连接池默认参数在低并发场景下没问题,并发上来后,连接池的超时时间过长,会导致大量线程阻塞,进而产生大量等待中的对象。很多情况下“java虚拟机满了”是假象,真实问题是线程池队列堆满。

第9步:压测复现与代码侧修护
线上不好动手时,把堆转储和GC日志带回测试环境,用JMH或自研压测脚本模拟同类请求,观察负载超过临界值时能否复现“老年代迅速增长”现象,找到代码问题后,重点检查是否有静态集合持有局部变量、是否ThreadLocal用完后没有remove。
java jvm内存使用率高的原因:三个高频业务场景的排查案例
理论讲完了,用三个经典场景展示具体怎么联动排查。
每日定时任务触发后内存飙升
某后台服务每整点执行批量报表导出,执行几分钟后老年代内存使用率冲到85%以上,查看GC日志发现,FULL GC频率从30分钟一次暴增到3分钟一次。
排查过程:
jmap -dump:live导出堆快照。- MAT打开后显示
SavedReportResult对象占用了70%的堆空间。 - 顺着引用链看到,该对象在校验报表模板时被加入一个静态
List,清理逻辑只在任务成功时触发,而校验失败时会提前return,跳过清理代码。 - 修复方案是使用
try-finally确保清理,同时用WeakReference持有历史对象。
双十一促销期间热点商品缓存导致堆满
活动期间热点商品被大量查询,Redis缓存穿透后全部打到本地ConcurrentHashMap上,数据量不大但对象数量极多,每个JSON字符串转成对象数组后又拆成对象集合,导致Young GC每秒触发接近10次。
修复方案:
- 接入分布式缓存中间件,本地缓存只放阈值内的热点数据。
- 使用Caffeine的
maximumSize和expireAfterAccess组合控制本地缓存容量上限。 - 核心对象使用规范的数据类(如
record),替代Map<String, Object>结构,减少HashMap自身的wrapper对象数量。
部署在4GB内存服务器上的小应用
小服务堆设置-Xms2g -Xmx2g -XX:MaxMetaspaceSize=512m,还省出几百MB给堆外和线程,某次升级后JDK版本从8升到17,应用启动半天就被Linux内核OOM Killer杀掉。
原因分析:JDK17的JIT编译器(C2)和GC线程占用的原生内存比8代有提升,加上-XX:MaxMetaspaceSize只限制了类元数据,而链路的堆外内存未限制,修复方式是按服务器规格重新估算内存分配,启动参数改为-Xms1536m -Xmx1536m -XX:MaxDirectMemorySize=256m -XX:+UseG1GC,同时用容器限制(cgroup)约束整体进程内存。
服务内存参数调整原则:在“够用”和“浪费”之间取平衡
内存参数的设置没有固定公式,但行业共识需要遵守三条基本原则:
- 堆大小与对象生命周期必须匹配:短生命周期对象多,年轻代尽量大;长周期缓存多,老年代相应增加,没有万能模板,但可以通过GC日志动态修正。
- 堆内堆外统一规划:不建议把所有物理内存都塞给堆,预留内存给线程、直接内存、JIT和JNI,按经验比例,堆上限最好不超过物理内存的一半。
- 直接内存必须显式开启参数

:使用Netty等NIO框架时,
-XX:MaxDirectMemorySize不能省,不设置时,默认等于堆大小,容易造成堆外内存失控。
G1垃圾回收器在JDK11后成为主流,启动参数推荐这样调整:
java -Xms4g -Xmx4g -XX:MaxGCPauseMillis=200 -XX:+UseG1GC -XX:MaxDirectMemorySize=1g -jar app.jar
不要试图把MaxGCPauseMillis设到20ms,JVM会为此牺牲吞吐量,实际STW时间也不会精确控制在设定值,对于追求吞吐量的批处理服务,可以换用Parallel GC。
预防Java虚拟机内存溢出的落地制度
日常开发和上线流程里,增加几项固定动作能提前发现隐患。
上线前:
- 压测环境填满“基础数据”后,连续跑24小时观察GC曲线。
- 启动参数统一挂在配置中心,由架构组审核,避免每个团队各自为政。
- 集成Java Flight Recorder(JFR)持续录制低开销的监控事件,上线后保留近一周数据。
运行期:
- 配置
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs,等OOM时自动落盘并触发告警。 - 用Prometheus+Grafana监控
jvm_memory_used_bytes、jvm_gc_pause_seconds和process_cpu_utilization三个指标,超过阈值自动通知值班群。 - 每周对
jstat -gcutil历史数据做一次趋势分析,发现老年代占用率周环比上升超过10%时,主动排查。
代码Review阶段:
- Checkstyle或SpotBugs增加“未关闭资源”“静态集合持有对象”“ThreadLocal未清理”三个检查规则。
- 定时任务和批量处理代码强制要求使用块级别的局部变量,避免对象逃逸到外层作用域。
- 团队规范明确:任何对象缓存必须设置容量上限和过期策略,禁止直接持有List或Map长期存活。
常见问题解答
哪些JVM参数能解决java虚拟机满了的问题?
解决思路取决于内存的分配情况。-Xmx控制最大堆内存,-XX:MaxMetaspaceSize控制元空间上限,-XX:MaxDirectMemorySize控制堆外直接内存,绝大多数场景下,先调整这三个参数到合理阈值,再检查代码中的对象生命周期,若调整完仍然定期出现OOM,说明存在线程或IO资源泄漏,修改参数无法根治。
内存泄漏和内存压力有什么不一样?
内存压力是对象太多,但都能被GC回收,调大堆内存后见效;内存泄漏是部分对象无法被回收,调大堆内存只延长了故障周期,最终依旧崩溃,鉴别方法是连续导出两次堆快照,对比同名对象实例数量的变化:数量持续增加说明泄漏已发生,数量稳定但总内存飙升则属于压力。
jmap命令执行不了或者权限不足怎么办?
jmap执行需要与目标JVM相同的操作系统用户身份,进程属于root而当前账号是应用用户时,先sudo -u <username> jmap ...执行,容器场景里务必用docker exec进入容器内部再操作,宿主机上直接执行jmap对容器内进程通常无效,如果jmap不可用或JDK版本过高被拒绝,使用jcmd <pid> GC.heap_dump替代。
图片来源于AI模型,如侵权请联系管理员。作者:酷小编,如若转载,请注明出处:https://www.kufanyun.com/ask/910941.html


评论列表(3条)
读了这篇文章,我深有感触。作者对使用的理解非常深刻,论述也很有逻辑性。内容既有理论深度,又有实践指导意义,确实是一篇值得细细品味的好文章。希望作者能继续创作更多优秀的作品!
这篇文章的内容非常有价值,我从中学习到了很多新的知识和观点。作者的写作风格简洁明了,却又不失深度,让人读起来很舒服。特别是使用部分,给了我很多新的思路。感谢分享这么好的内容!
这篇文章的内容非常有价值,我从中学习到了很多新的知识和观点。作者的写作风格简洁明了,却又不失深度,让人读起来很舒服。特别是使用部分,给了我很多新的思路。感谢分享这么好的内容!