tomcat 内存占用问题排查
服务器2025-03-17 17:03:11问题: tomcat 的快照下来只能看到使用了90MB内存,而进程管理里面则看到用了500M内存。
可能的原因有几个方面:
-
堆外内存(Off-Heap Memory):Java的堆内存只是JVM管理的一部分,其他如DirectByteBuffer、NIO使用的Native内存,或者第三方库分配的堆外内存都可能占用大量空间。这些不会在hprof文件中显示,因为hprof主要记录堆内的对象。
-
JVM自身开销:比如元空间(Metaspace)、线程栈、JIT编译代码、GC相关数据结构等。这部分属于JVM的非堆内存,可能通过ManagementFactory中的MemoryPoolMXBean来查看,比如Metaspace、Code Cache等区域的使用情况。
-
Native库和JNI调用:如果应用使用了JNI调用本地库,这些库分配的内存不会被JVM统计,但会被系统计入进程的内存使用。这部分需要检查是否有本地代码分配了大量内存。
-
内存映射文件(MappedByteBuffer):使用内存映射文件时,文件会被映射到虚拟内存中,这部分可能被系统统计到进程的内存占用里,但不会计入堆或非堆内存。
-
线程数量:每个线程都有自己的栈空间,默认情况下每个线程栈可能占用1MB(取决于JVM参数),大量线程会显著增加内存使用。线程栈属于Native内存的一部分。
-
其他JVM内部结构:比如GC的卡表、符号表、内部数据结构等,这些可能需要通过NMT来追踪。
用户已经使用hprof,但hprof只能分析堆内存,所以剩下的内存需要从上述几个方面排查。接下来需要提供具体的排查步骤和工具。
首先,应该建议用户启用NMT来跟踪Native内存的使用情况。NMT可以详细展示JVM各部分的内存分配,包括线程、元空间、代码缓存、GC等。启用NMT需要JVM启动参数,用户可能已经按照之前的建议启用了,但可能需要更详细的输出。
其次,使用jcmd或jmap等工具来查看内存分布。例如,jcmd VM.native_memory detail可以给出详细的Native内存分配情况。
另外,检查线程数量,使用jstack或者jvisualvm查看线程数量,计算总栈内存占用。
对于JNI和本地库的情况,可能需要检查是否有第三方库或自行开发的JNI代码,导致Native内存泄漏。使用操作系统工具如pmap、top、ps等来查看进程的内存映射,找出大块的内存区域。
此外,内存映射文件(MappedByteBuffer)的使用情况可以通过NMT或者pmap来查看。
最后,建议用户结合多个工具,如NMT的输出、系统监控工具(如Linux的smem、proc文件系统)、以及分析JVM内存池的MXBean数据,来综合判断内存的去向。
需要注意的是,任务管理器显示的内存通常是指进程的常驻集大小(RSS),而JVM报告的内存可能只包括Java堆和部分Native内存,其他如mmap的文件、Native库分配的内存、线程栈等都会被计入RSS。因此,需要从多个维度来排查。
总结下来,用户的额外内存可能来源于Native内存的各个部分,需要通过NMT、系统工具和JVM内置工具来逐一排查。
Java 程序通过 hprof 文件只能观察到堆内存的使用情况(约 90MB),而任务管理器显示的 500MB 是进程总内存占用(包括堆内、堆外及 JVM 自身开销)。以下是额外内存的常见来源及排查方法:
一、可能占用额外内存的模块
| 类型 | 具体内容 | 排查工具 |
|---|---|---|
| 堆外内存 | DirectByteBuffer、JNI 分配的内存、第三方库(如 Netty 的 PooledByteBuf) | jcmd + NMT、pmap、jmap -dump |
| JVM 元数据 | Metaspace(类元数据)、Code Cache(JIT 编译代码) | MemoryPoolMXBean、NMT |
| 线程栈 | 每个线程默认占用 1MB(可通过 -Xss 调整),线程数过多会累积 | jstack、top -H、NMT |
| GC 相关结构 | 卡表、记忆集等垃圾回收数据结构 | NMT、GC 日志 |
| 本地库/Native 内存 | JNI 调用的 C/C++ 库分配的内存(如 OpenSSL、图像处理库) | Valgrind、系统工具(如 pmap) |
| 内存映射文件 | MappedByteBuffer 映射的文件(计入虚拟内存) | NMT、pmap |
二、排查步骤
1. 启用 Native Memory Tracking (NMT)
在 JVM 启动时添加参数:
-XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=detail -XX:+PrintNMTStatistics
通过以下命令查看 Native 内存分布:
jcmd <PID> VM.native_memory detail
输出示例:
Native Memory Tracking:
Total: reserved=1450145KB, committed=611097KB
- Java Heap: reserved=1048576KB, committed=1048576KB
- Class: reserved=1065940KB, committed=14904KB # Metaspace
- Thread: reserved=12345KB, committed=12345KB # 线程栈
- Code: reserved=257000KB, committed=12344KB # JIT 代码缓存
- GC: reserved=20480KB, committed=20480KB # GC 数据结构
- Internal: reserved=560KB, committed=560KB # 其他内部结构
重点关注 Class(Metaspace)、Thread(线程栈)、Code(JIT 代码缓存)等非堆内存。
2. 检查 Metaspace 和 Code Cache
通过 MemoryPoolMXBean 查询非堆内存:
List<MemoryPoolMXBean> pools = ManagementFactory.getMemoryPoolMXBeans();
for (MemoryPoolMXBean pool : pools) {
if (pool.getName().contains ("Metaspace") || pool.getName().contains ("Code Cache") ) {
System.out.println (pool.getName() + ": " + pool.getUsage().getCommitted() / 1024 + " KB");
}
}
3. 分析线程栈内存
- 查看线程数量:
jstack <PID> | grep "java.lang.Thread.State" | wc -l
- 计算总栈内存:
线程数 × 单个线程栈大小(默认 1MB,由 -Xss 参数控制)
4. 排查 DirectByteBuffer 等堆外内存
- 查看 DirectBuffer 使用量:
BufferPoolMXBean directBufferPool = ManagementFactory.getPlatformMXBeans (BufferPoolMXBean.class)
.stream().filter (b -> b.getName().equals ("direct") ).findFirst().orElse (null);
if (directBufferPool != null) {
System.out.println ("DirectBuffer Used: " + directBufferPool.getMemoryUsed() / 1024 / 1024 + " MB");
}
- 如果 DirectBuffer 占用高,检查代码中是否有未释放的 ByteBuffer.allocateDirect() 或 Netty 的 PooledByteBuf。
5. 检查内存映射文件 (MappedByteBuffer)
通过 pmap 查看进程内存映射:
pmap -x <PID> | less
查找标记为 mmap 的大块内存区域,可能对应文件映射。
6. JNI 和本地库内存泄漏
- 使用系统工具分析:
# Linux 查看进程内存映射
cat /proc/<PID>/maps # 按内存排序 pmap <PID> | sort -n -k2
- 若怀疑 JNI 库泄漏,可用 Valgrind 检测:
valgrind --tool=memcheck --leak-check=full java -jar your_app.jar
三、工具总结
| 工具 | 用途 |
|---|---|
| jcmd + NMT | 分析 JVM 内部 Native 内存分配 |
| pmap/proc/<PID>/maps | 查看进程的物理内存和虚拟内存映射 |
| jstack | 统计线程数及栈大小 |
| Eclipse MAT | 分析堆内存泄漏(hprof 文件) |
| Valgrind | 检测 JNI/C/C++ 内存泄漏 |
四、常见案例
-
Metaspace 溢出:动态生成大量类(如反射、CGLIB),调整 -XX:MaxMetaspaceSize。
-
DirectBuffer 未释放:未调用 ((DirectBuffer) buffer).cleaner().clean()。
-
线程数爆炸:调整 -Xss 减小栈大小,或优化线程池配置。
-
JNI 库泄漏:本地代码未释放 malloc 分配的内存。
通过上述方法,可以逐步定位到额外内存的具体去向。
上一篇:XStudio for stm32 (带标准库和HAL库)下一篇:xlang 5.0内置类大全

