文字大小:

tomcat 内存占用问题排查

服务器2025-03-17 17:03:11




问题: tomcat 的快照下来只能看到使用了90MB内存,而进程管理里面则看到用了500M内存。

分析:

可能的原因有几个方面:

  1. 堆外内存(Off-Heap Memory):Java的堆内存只是JVM管理的一部分,其他如DirectByteBuffer、NIO使用的Native内存,或者第三方库分配的堆外内存都可能占用大量空间。这些不会在hprof文件中显示,因为hprof主要记录堆内的对象。

  2. JVM自身开销:比如元空间(Metaspace)、线程栈、JIT编译代码、GC相关数据结构等。这部分属于JVM的非堆内存,可能通过ManagementFactory中的MemoryPoolMXBean来查看,比如Metaspace、Code Cache等区域的使用情况。

  3. Native库和JNI调用:如果应用使用了JNI调用本地库,这些库分配的内存不会被JVM统计,但会被系统计入进程的内存使用。这部分需要检查是否有本地代码分配了大量内存。

  4. 内存映射文件(MappedByteBuffer):使用内存映射文件时,文件会被映射到虚拟内存中,这部分可能被系统统计到进程的内存占用里,但不会计入堆或非堆内存。

  5. 线程数量:每个线程都有自己的栈空间,默认情况下每个线程栈可能占用1MB(取决于JVM参数),大量线程会显著增加内存使用。线程栈属于Native内存的一部分。

  6. 其他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++ 内存泄漏

 


四、常见案例

  1. Metaspace 溢出:动态生成大量类(如反射、CGLIB),调整 -XX:MaxMetaspaceSize。

  2. DirectBuffer 未释放:未调用 ((DirectBuffer) buffer).cleaner().clean()。

  3. 线程数爆炸:调整 -Xss 减小栈大小,或优化线程池配置。

  4. JNI 库泄漏:本地代码未释放 malloc 分配的内存。

通过上述方法,可以逐步定位到额外内存的具体去向。

 





上一篇:XStudio for stm32 (带标准库和HAL库)下一篇:xlang 5.0内置类大全

评论

写评论

点击刷新