
在 https://xie.infoq.cn/article/a97c795c86b4c6e5c721fa394 这篇文章中作者有个疑问:
使用top命令 和 NMT 分别查看的内存大小为什么不一致
top
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
36257 dou+ 20 0 10.8g 54200 17668 S 99.7 0.0 13:04.15 javaNative Memory Tracking:
Total: reserved=2813709KB, committed=1497485KB
- Java Heap (reserved=1048576KB, committed=1048576KB)
(mmap: reserved=1048576KB, committed=1048576KB)
- Class (reserved=1056899KB, committed=4995KB)
(classes #442)
(malloc=131KB #259)
(mmap: reserved=1056768KB, committed=4864KB)
- Thread (reserved=258568KB, committed=258568KB)
(thread #127)
(stack: reserved=258048KB, committed=258048KB)
(malloc=390KB #711)
(arena=130KB #234)
- Code (reserved=266273KB, committed=4001KB)
(malloc=33KB #309)
(mmap: reserved=266240KB, committed=3968KB)
- GC (reserved=164403KB, committed=164403KB)
(malloc=92723KB #6540)
(mmap: reserved=71680KB, committed=71680KB)
- Compiler (reserved=152KB, committed=152KB)
(malloc=4KB #36)
(arena=148KB #21)
- Internal (reserved=14859KB, committed=14859KB)
(malloc=14827KB #3632)
(mmap: reserved=32KB, committed=32KB)
- Symbol (reserved=1423KB, committed=1423KB)
(malloc=936KB #111)
(arena=488KB #1)
- Native Memory Tracking (reserved=330KB, committed=330KB)
(malloc=118KB #1641)
(tracking overhead=211KB)
- Arena Chunk (reserved=178KB, committed=178KB)
(malloc=178KB)
- Unknown (reserved=2048KB, committed=0KB)
(mmap: reserved=2048KB, committed=0KB)
......此时疑问就产生了,为什么 NMT 中的 committed ,即日志详情中 Total: reserved=2813709KB, committed=1497485KB 中的 1497485KB 与 top 中 RES 的大小 54200KB 存在如此大的差异?
解答这个问题?需要先理解TOP的VIRT 和 RES 含义,以及操作系统的内存管理

TOP命令中 VIRT 和 RES 两个区域做下解释:
VIRT(virtual memory usage):进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据,以及malloc、new分配的堆空间和分配的栈空间等,另外 VIRT = SWAP + RES,其中 swap 区域是需要内存置换的区域(可参考操作系统虚拟内存原理)。
RES(resident memory usage):又称为 RSS,是进程在 RAM 中真正占用的内存大小。RES 包含了它所链接的动态库被加载到物理内存中的内存、栈内存和堆内存。动态链接库占用的内存会被多个进程共享,所以RSS并不能准确反映单进程的内存占用情况。
RSS 内存区域包括了线程堆栈、直接内存、通过mmap映射的文件和JVM字节码等。详细说明参考: https://stackoverflow.com/questions/38597965/difference-between-resident-set-size-rss-and-java-total-committed-memory-nmt

RSS = Heap size + MetaSpace + OffHeap size
操作系统对内存的分配管理典型地分为两个阶段:保留(reserve)和提交(commit)。
保留阶段告知系统从某一地址开始到后面的 dwSize 大小的连续虚拟内存需要供程序使用,进程其他分配内存的操作不得使用这段内存;提交阶段将虚拟地址映射到对应的真实物理内存中,这样这块内存就可以正常使用
JVM 中所谓的 commit 内存,只是将内存 mmaped 映射为可读可写可执行的状态!而在 Linux 中,在分配内存时又是 lazy allocation 的机制,只有在进程真正访问时才分配真实的物理内存。所以 NMT 中所统计的 committed 并不是对应的真实的物理内存,自然与 RES 等统计方式无法对应起来。
JVM 为我们提供了一个参数 -XX:+AlwaysPreTouch,使我们可以在启动之初就按照内存页粒度都访问一遍 Heap,强制为其分配物理内存以减少运行时再分配内存造成的延迟(但是相应的会影响 JVM 进程初始化启动的时间)
开启 AlwaysPreTouch 参数后,NMT 统计的 commited 已经与 top 中的 RES 差不多了,之所以不完全相同是因为该参数只能 Pre-touch 分配 Java heap 的物理内存,至于其他的非 heap 的内存,还是受到 lazy allocation 机制的影响。
-XX:+AlwaysPreTouch 是 JVM 的一个重要参数,它的作用是在 JVM 启动时预先接触(pre-touch)所有堆内存页。
// JVM默认行为:延迟分配物理内存
-Xms4g -Xmx4g•JVM 启动时只保留4GB的虚拟地址空间•物理内存是按需分配的(懒分配)•当应用程序实际访问内存时,才会触发缺页中断,分配物理内存
// 启用预分配
-Xms4g -Xmx4g -XX:+AlwaysPreTouch•JVM 在启动过程中会立即分配并初始化所有4GB物理内存•通过写入每个内存页来强制操作系统分配物理内存
# 没有预分配:运行时可能遇到缺页中断
Page Fault -> 分配物理内存 -> 继续执行
# 有预分配:运行时无缺页中断,直接执行// 应用启动后的内存行为对比
public class MemoryIntensiveApp {
public void process() {
byte[] data = new byte[1024 * 1024]; // 1MB
// 没有预分配:这里可能触发缺页中断,导致延迟
// 有预分配:内存已准备好,无延迟
}
}# 测试启动时间差异
time java -Xms4g -Xmx4g -jar app.jar # 快速启动
time java -Xms4g -Xmx4g -XX:+AlwaysPreTouch -jar app.jar # 较慢启动结果示例:
•无预分配:启动时间 2-3 秒•有预分配:启动时间 10-15 秒(取决于堆大小)
// 内存密集型操作的性能对比
public class Benchmark {
public static void main(String[] args) {
long start = System.currentTimeMillis();
// 大量内存分配操作
for (int i = 0; i < 10000; i++) {
byte[] chunk = new byte[1024 * 64]; // 64KB
Arrays.fill(chunk, (byte)1); // 强制内存访问
}
long duration = System.currentTimeMillis() - start;
System.out.println("耗时: " + duration + "ms");
}
}# 1. 对延迟敏感的应用
java -Xms8g -Xmx8g -XX:+AlwaysPreTouch -jar trading-app.jar
# 2. 需要稳定性能的服务
java -Xms4g -Xmx4g -XX:+AlwaysPreTouch -jar web-service.jar
# 3. 内存密集型批处理
java -Xms16g -Xmx16g -XX:+AlwaysPreTouch -jar data-processor.jar# 1. 短期运行的应用
java -Xms1g -Xmx1g -jar short-lived-tool.jar # 不需要预分配
# 2. 内存受限的环境
# 预分配会立即占用所有内存,可能影响其他进程
# 3. 需要快速启动的场景
java -Xms2g -Xmx2g -jar cli-tool.jar # 快速启动更重要# 配合 G1GC 使用
java -Xms8g -Xmx8g \
-XX:+UseG1GC \
-XX:+AlwaysPreTouch \
-XX:MaxGCPauseMillis=200 \
-jar application.jar# Dockerfile 示例
FROM openjdk:11-jre
# 在容器中预分配内存有助于稳定性
CMD ["java", "-Xms1g", "-Xmx1g", "-XX:+AlwaysPreTouch", "-jar", "/app.jar"]# Kubernetes 配置
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: myapp
image: myapp:latest
resources:
requests:
memory: "2Gi" # 确保足够内存用于预分配
limits:
memory: "2Gi"
command:
- java
- -Xms1g
- -Xmx1g
- -XX:+AlwaysPreTouch
- -jar
- /app.jar# 使用 pmap 查看内存分配
pmap -x <pid> | grep total
# 使用 /proc 文件系统
cat /proc/<pid>/status | grep -E "VmSize|VmRSS"// 通过 JMX 监控内存
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
System.out.println("Heap Memory Usage: " + memoryBean.getHeapMemoryUsage());
System.out.println("Non-Heap Memory Usage: " + memoryBean.getNonHeapMemoryUsage());-XX:+AlwaysPreTouch 的核心价值:
•✅ 优点:提高运行时性能,减少缺页中断,稳定内存占用•❌ 缺点:增加启动时间,立即占用所有物理内存•🎯 适用:长期运行、对性能敏感的服务•⚠️ 注意:在内存受限或需要快速启动的环境中慎用
这个参数是用 启动时间换取运行时性能 的典型例子,需要根据具体应用场景权衡使用。
https://xie.infoq.cn/article/a97c795c86b4c6e5c721fa394