为什么Java进程使用的内存(RSS)比Heap Size大?

Java进程使用的虚拟内存确实比Java Heap要大很多。JVM包括很多子系统:垃圾收集器、类加载系统、JIT编译器等等,这些子系统各自都需要一定数量的RAM才能正常工作。

当一个Java进程运行时,也不仅仅是JVM在消耗RAM,很多本地库(Java类库中引用的本地库)可能需要分配原生内存,这些内存无法被JVM的Native Memory Tracking机制监控到。Java应用自身也可能通过DirectByteBuffers等类来使用堆外内存。

那么,当一个Java进程运行时,有哪些部分在消耗内存呢?这里我们只展示哪些可以被Native Memory Tracking监控到的部分。

一、JVM部分

Java Heap: 最明显的部分,Java对象在这个区域分配和回收,Heap的最大值由-Xmx决定。

Garbage Collector:GC的数据结构和算法需要额外的内存对堆内存进行管理。这些数据结构包括:Mark Bitmap、Mark Stack(用于跟踪存活的对象)、Remembered Sets(用于记录region之间的引用)等等。这些数据结构中的一些是可以直接调整的,例如:-XX:MarkStackSizeMax,其他的则依赖于堆的分布,例如:分区大小,-XX:G1HeapRegionSize,这个值越大Remembered Sets的值越小。不同的GC算法需要的额外内存是不同的,-XX:+UseSerialGC和-XX:+UseShenandoahGC需要较小的额外内存,G1和CMS则需要Heap size的10%作为额外内存。

Code Cache:用于存放动态生成的代码:JIT编译的方法、拦截器和运行时存根。这个区域的大小由-XX:ReservedCodeCacheSize确定(默认是240M)。使用-XX-TieredCompilation关掉多层编译,可以减少需要编译的代码,从而减少Code Cache的使用。

Compiler:JIT编译器需要一些内存来才能工作。这个值可以通过关闭多层编译或减少执行编译的线程数(-XX:CICompilerCount)来调整.

Class loading:类的元数据(方法的字节码、符号表、常量池、注解等)被存放在off-heap区域,也叫Metaspace。当前JVM进程加载了越多的类,就会使用越多的metaspace。通过设置-XX:MaxMetaspaceSize(默认是无限)或-XX:CompressedClassSpaceSize(默认是1G)可以限制元空间的大小

Symbol tables:JVM中维护了两个重要的哈希表:Symbol表包括类、方法、接口等语言元素的名称、签名、ID等,String table记录了被interned过的字符串的引用。如果Native Tracking表明String table使用了很大的内存,那么说明该Java应用存在对String.intern方法的滥用。

Threads:线程栈也会使用RAM,栈的大小由-Xss确定。默认是1个线程最大有1M的线程栈,幸运得失事情并没有这么糟糕——OS使用惰性策略分配内存页,实际上每个Java线程使用的RAM很小(一般80~200K)

二、堆外内存(Direct buffers)

Java应用可以通过ByteBuffer.allocateDirect显式申请堆外内存;默认的堆外内存大小是-Xmx,但是这个值可被-XX:MaxDirectMemorySize覆盖。在JDK11之前,Direct ByteBuffers被NMT(Native Memory Tracking)列举在other部分,可以通过JMC观察到堆外内存的使用情况。

除了DirectByteBuffers,MappedByteBuffers也会使用本地内存,MappedByteBuffers的作用是将文件内容映射到进程的虚拟内存中,NMT没有跟踪它们,想要限制这部分的大小并不容易,可以通过pmap -x 命令观察当前进程使用的实际大小:

Address           Kbytes    RSS    Dirty Mode  Mapping
...
00007f2b3e557000   39592   32956       0 r--s- some-file-17405-Index.db
00007f2b40c01000   39600   33092       0 r--s- some-file-17404-Index.db

三、本地库(Native libraries)

由System.loadLibrary加载的JNI代码也会按需分配RAM,并且这部分内存不受JVM管理。在这里需要关注的是Java类库,未关闭的Java资源会导致本地内存泄漏,典型的例子是:ZipInputStream或DirectoryStream。

JVMTI agent,特别是jdwp调试agent,也可能导致内存的过量使用(PS:去年写memory agent代码造成的内存泄漏记忆犹新)。

四、Allocator issues

一个Java进程可以通过系统调用(mmap)或标准库(malloc)方法来向OS申请内存。malloc自己又通过mmap来向OS申请比较大的内存,并通过自己的算法来管理这些内存,这可能会导致内存碎片,从而导致过量使用虚拟内存。jemalloc是另外一个内存分配器,它比常规的malloc分配器需要更少的footprint,因此可以在自己的C++代码中尝试使用jemalloc方法。

结论

无法准确统计一个Java进程使用的虚拟内存,因为有太多因素需要考虑,列举如下:

Total memory = Heap + Code Cache + Metaspace + Symbol tables +
               Other JVM structures + Thread stacks +
               Direct buffers + Mapped files +
               Native Libraries + Malloc overhead + ...

 

<think>好的,用户想知道如何在Linux中查看某个进程的堆外内存占用。首先,我需要回忆一下Linux内存管理的基本知识。堆外内存通常指的是由程序自己管理的内存,比如Java中的DirectByteBuffer,或者通过mmap分配的内存,这部分不在堆内存里,所以常规的top或ps命令可能无法直接显示。 根据用户提供的引用,引用[4]提到堆外内存泄漏的排查过程,并提到了glibc的64M问题,这可能涉及到内存分配器的情况。引用[5]给出了ps命令的一些用法,可以查看rsz和vsz,但可能不够详细。需要找到更精确的方法。 首先,常用的工具如top和ps可以查看进程的总体内存使用情况,但堆外内存可能包含在RES或VIRT中,但无法区分。这时候可能需要更专业的工具,比如pmap,它可以显示进程内存映射情况。pmap -x <pid>可以显示详细的内存段,其中anon部分可能包括堆外内存。 另外,/proc/<pid>/smaps文件提供了更详细的内存使用信息。通过分析smaps中的各个内存段,可以找到匿名内存块(Anonymous memory),这些可能对应堆外内存。用户需要手动统计这些部分的小,比如使用grep和awk命令。 还有,像Valgrind这样的内存分析工具可以用来检测内存泄漏,包括堆外内存。不过Valgrind可能会影响性能,不适合生产环境。此外,jcmd工具适用于Java进程,可以触发堆转储,帮助分析Native Memory Tracking(NMT)的信息,从而查看堆外内存使用情况。 用户可能是在处理Java应用的问题,或者使用其他语言编写的程序,需要不同的工具。例如,对于C/C++程序,可以使用jemalloc或tcmalloc来跟踪内存分配情况。这些分配器提供的内存分析工具可以帮助识别堆外内存使用。 总结一下,查看堆外内存的方法包括:使用pmap分析内存映射,检查/proc/<pid>/smaps文件,利用特定工具如jcmd(Java)、Valgrind、jemalloc等。同时,用户需要根据具体的应用类型选择合适的工具,并结合内存泄漏的常见原因进行排查,比如检查文件映射、直接内存分配等。</think>在 Linux 中查看进程的堆外内存占用需要结合特定工具和方法,以下是具体步骤和说明: --- ### 一、堆外内存的概念 堆外内存(Off-Heap Memory)指由程序直接管理且不通过标准堆内存分配器(如 `malloc` 或 `new`)申请的内存,例如: - Java 的 `DirectByteBuffer` - 通过 `mmap` 映射的文件内存 - 使用 `JNI` 调用的本地内存等 --- ### 二、查看堆外内存的方法 #### 1. 使用 `pmap` 命令 通过 `pmap` 分析进程内存映射: ```bash pmap -x <PID> | grep -i anon ``` - **作用**:显示进程的详细内存映射,`anon` 表示匿名内存块(可能是堆外内存)。 - **输出示例**: ``` Address Kbytes RSS Dirty Mode Mapping 00007f8d40000000 10240 0 0 rw--- [ anon ] ``` - **统计总堆外内存**: ```bash pmap -x <PID> | grep -i anon | awk '{sum += $3} END {print sum " KB"}' ``` #### 2. 分析 `/proc/<PID>/smaps` 查看进程的详细内存映射文件: ```bash cat /proc/<PID>/smaps | grep -A 10 'Anonymous' ``` - **关键字段**: - `Anonymous`:匿名内存(堆外内存可能包含在此类内存中)。 - `Rss`:实际驻留内存小。 - **统计匿名内存总量**: ```bash cat /proc/<PID>/smaps | grep 'Anonymous' | awk '{sum += $2} END {print sum " KB"}' ``` #### 3. 使用 `jcmd`(仅限 Java 进程) 对于 Java 应用,启用 Native Memory Tracking (NMT): 1. 启动时添加参数: ```bash java -XX:NativeMemoryTracking=detail ... ``` 2. 查看内存详情: ```bash jcmd <PID> VM.native_memory detail ``` - **输出示例**: ``` Native Memory Tracking: Total: reserved=5GB, committed=2GB - Java Heap (reserved=3GB, committed=1GB) - Class (reserved=1GB, committed=500MB) - Thread (reserved=200MB, committed=50MB) - Code (reserved=250MB, committed=100MB) - GC (reserved=300MB, committed=100MB) - Internal (reserved=100MB, committed=50MB) - Other (reserved=1GB, committed=500MB) # 堆外内存 ``` #### 4. 使用 `valgrind` 检测内存泄漏 通用内存检测工具: ```bash valgrind --tool=memcheck --leak-check=full ./your_program ``` - **适用场景**:开发环境调试,可追踪堆外内存泄漏。 --- ### 三、注意事项 1. **堆外内存RSS 的关系** 堆外内存会计入进程的 `RSS`(Resident Set Size),但需通过特定方法分离统计[^4]。 2. **文件映射内存** 使用 `mmap` 映射的文件可能占用堆外内存,可通过 `pmap` 输出的 `mapped file` 部分查看。 3. **性能工具推荐** 生产环境建议使用 `perf` 或 `bcc-tools`(如 `mallocstacks`)进行动态分析[^5]。 --- ### 四、示例场景 **排查 Java 堆外内存泄漏**: 1. 通过 `top` 找到高内存占用的 Java PID[^2]。 2. 使用 `jcmd <PID> VM.native_memory detail` 查看 `Other` 部分。 3. 结合 `pmap -x <PID>` 检查是否有异常的匿名内存块。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值