最近碰到一个问题,java程序运行过程中,被系统给强行杀死,因为内存占用过大。但是看监控堆内存并没有超出界限,而且还能被定期回收。经过排查研究发现是元空间占用的内存过大导致的。
首先要明白,java进程占用的内存并不等于java堆内存,堆内存只是其中一部分。
java 占用内存构成分析
| 内存区域 | 作用 |
|---|---|
| 堆内存-heap | 存储对象、数组等数据,通过参数-Xms/-Xmx控制 |
| 非堆内存 | 元空间Metaspace,JIT编译缓存、类加载数据 |
| 本地方法栈 | 每个线程的调用栈,默认每个线程1m-2m |
| 直接内存 | NIO使用的对外内存,默认无上限,不收-Xmx控制 |
| jvm自身运行内存 | 内核线程、GC线程、日志、监控等等 |
我们通过命令jstat -gc <PID> 1000 1可以查看JVM内存的详细使用信息,这个命令可以列出堆内存和非堆内存的使用情况。
jcmd <PID> VM.metaspace 查看元空间的详细信息
jstat -gcmetacapacity <PID>查看元空间的使用信息
通过上面两个命令最总到是元空间占用的内存太多,导致进程被系统杀死。
解决方案,我么你可以通过参数-XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=1G来限制元空间的大小。
我们还可以查看直接内存使用量
jcmd <PID> VM.native_memory detail
查看线程数量
jstack <PID> | grep "java.lang.Thread.State" | wc -l
如果我们需要进一步排查内存占用情况,我们可以使用以下排查命令
# 查看元空间详细使用
jstat -gcmetacapacity <PID>
# 导出堆快照分析类加载情况
jmap -dump:format=b,file=heap.hprof <PID>
# 用MAT(Memory Analyzer Tool)打开heap.hprof,查看“Class Loader”占用
如果我们想验证新添加的参数是否生效,可以执行一下命令
查看JVM参数是否生效
jcmd <PID> VM.flags | grep -E "Xmx|MetaspaceSize|DirectMemorySize"
我们还可以导出类加载器和方法元数据统计
# 1. 导出类加载器详细统计(替换为你的进程PID)
jmap -clstats <PID> > classloader_stats.txt
# 2. 查看元空间中类的分布(重点看类数量和占用)
# 过滤出占用最高的类(按类数量排序)
cat classloader_stats.txt | grep -E "ClassLoader|classes, bytes" | sort -k3 -r
# 3. 用jcmd查看元空间详细占用(包含方法元数据)
jcmd <PID> VM.metaspace > metaspace_detail.txt
# 查看metaspace_detail.txt,重点看"Class space used"和各模块占用
如果服务程序内存占用过高,临时又无法重启服务,可以采用临时方案让服务程序强制执行垃圾回收
# 强制触发JVM堆和元空间GC(仅临时缓解,无法根治泄漏)
jcmd <PID> GC.run