翻译:https://www.baeldung.com/native-memory-tracking-in-jvm
你是否曾疑惑:为什么 Java 进程实际占用的内存,总是明显超过通过熟悉的 -Xms
和 -Xmx
指定的堆大小?出于多种原因和可能的优化,JVM 会在堆外再向操作系统申请额外 “本地内存”(native memory.)
。这些额外申请最终可能使进程占用的内存突破 -Xmx
的上限。
本教程将:
虽然 Java 堆通常是占用最大的一块,但 JVM 还会从本地内存中划出相当可观的区域,用来存放类元数据、JIT 生成的代码、内部数据结构
等。下面逐一说明。
为了保存已加载类的元数据,JVM 专门开辟了一块 非堆区域 Metaspace。
-Xms/-Xmx
控制,需用下列参数单独限制:
-XX:MetaspaceSize
、-XX:MaxMetaspaceSize
(Java 8 之前是 -XX:PermSize
、-XX:MaxPermSize
)每个线程创建时都会伴随一个栈,用于局部变量和方法调用。
-Xss
调整。JIT 把字节码编译为机器码后,放在一块叫做 Code Cache 的非堆区域。
通过 -XX:InitialCodeCacheSize
、-XX:ReservedCodeCacheSize
设定初始值与最大值。
各种 GC 算法都需要额外的本地数据结构,因此会再向操作系统申请内存。不同 GC 实现差异较大。
-XX:StringTableSize
指定。除 JVM 自身外,开发者也可通过 JNI 的 malloc
或 NIO 的 DirectByteBuffer
直接申请本地内存。
要列出与某概念相关的所有 -XX
参数,可执行:
$ java -XX:+PrintFlagsFinal -version | grep <关键字>
例如查询 Metaspace:
$ java -XX:+PrintFlagsFinal -version | grep Metaspace
通过启动参数打开 NMT:
-XX:NativeMemoryTracking=off|summary|detail
示例:
$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseG1GC -jar app.jar
$ jps -l
7858 app.jar
$ jcmd 7858 VM.native_memory
输出开头给出保留(reserved)与已提交(committed)内存:
Native Memory Tracking:
Total: reserved=1731124KB, committed=448152KB
可以看到,虽然堆只有 300 MB,但进程保留了近 1.7 GB,已提交约 440 MB。
Java Heap (reserved=307200KB, committed=307200KB)
(mmap: reserved=307200KB, committed=307200KB)
完全符合 -Xms300m -Xmx300m
的设置。
Class (reserved=1091407KB, committed=45815KB)
(classes #6566)
(malloc=10063KB #8519)
(mmap: reserved=1081344KB, committed=35752KB)
约 1 GB 保留、45 MB 已提交,用于加载 6566 个类。
Thread (reserved=37018KB, committed=37018KB)
(thread #37)
(stack: reserved=36864KB, committed=36864KB)
(malloc=112KB #190)
(arena=42KB #72)
37 个线程,每个栈约 1 MB,总计 36 MB 左右。
Code (reserved=251549KB, committed=14169KB)
(malloc=1949KB #3424)
(mmap: reserved=249600KB, committed=12220KB)
当前缓存了约 13 MB 机器码,上限可达 245 MB。
以 G1 为例:
GC (reserved=61771KB, committed=61771KB)
(malloc=17603KB #4501)
(mmap: reserved=44168KB, committed=44168KB)
约 60 MB 用于 G1 结构。
若改用 Serial GC:
GC (reserved=1034KB, committed=1034KB)
(malloc=26KB #158)
(mmap: reserved=1008KB, committed=1008KB)
仅 1 MB,但带来的 Stop-The-World 风险需自行权衡。
Symbol (reserved=10148KB, committed=10148KB)
(malloc=7295KB #66194)
(arena=2853KB #1)
约 10 MB 用于字符串表与常量池。
$ jcmd <pid> VM.native_memory baseline
$ jcmd <pid> VM.native_memory summary.diff
输出示例:
Total: reserved=1771487KB +3373KB, committed=491491KB +6873KB
...
+
表示增加,-
表示减少,便于快速定位泄漏或膨胀。
如需更细粒度的地址映射,可启动时改为:
-XX:NativeMemoryTracking=detail
本文梳理了 JVM 本地内存的主要组成部分及其调优参数,并示范了如何利用 NMT 进行在线监控。借助这些信息,我们可以更精准地调优应用、合理规划容器或物理机的内存配额。