Native Memory Tracking 与 RSS的差异问题

embedded/2025/2/6 19:17:19/

一 问题现象

前一段时间用nmt查看jvm进程的栈区占用的内存大小。测试代码如下

java">public class ThreadOOM {public static void main(String[] args) {int i = 1;while (i < 3000) {Thread thread = new TestThread();thread.start();System.out.println("thread : " + i);i++;}}
}class TestThread extends Thread {@Overridepublic void run() {while (true) {try {Thread.sleep(Long.MAX_VALUE);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}

启动命令

java">nohup java -Xms2G -Xmx2G -XX:MaxMetaspaceSize=512M -XX:NativeMemoryTracking=detail ThreadOOM &

用native memory tracking查看内存占用

jcmd 37898 VM.native_memory scale=MB
37898:Native Memory Tracking:Total: reserved=9366MB, committed=8211MB
-                 Java Heap (reserved=2048MB, committed=2048MB)(mmap: reserved=2048MB, committed=2048MB)-                     Class (reserved=1039MB, committed=12MB)(classes #433)(malloc=7MB #3218)(mmap: reserved=1032MB, committed=5MB)-                    Thread (reserved=6046MB, committed=6046MB)(thread #3017)(stack: reserved=6032MB, committed=6032MB)(malloc=10MB #18096)(arena=3MB #6029)-                      Code (reserved=130MB, committed=3MB)(mmap: reserved=130MB, committed=2MB)-                        GC (reserved=83MB, committed=83MB)(malloc=8MB #123)(mmap: reserved=75MB, committed=75MB)-                  Internal (reserved=17MB, committed=17MB)(malloc=17MB #34406)-                    Symbol (reserved=1MB, committed=1MB)(malloc=1MB #110)-    Native Memory Tracking (reserved=1MB, committed=1MB)(tracking overhead=1MB)

显示线程committed了6G左右,jvm总共committed了8G左右。
使用top查看,常驻物理内存(RES)才占用了139M,这个和nmt显示的差距太大了吧!commited内存不就应该是RES的大小吗?
在这里插入图片描述

二 jdk8申请内存的源码分析

我看的jdk的源码:https://github.com/openjdk/jdk
分支: jdk8-b120
文件位置: hotspot/src/os/linux/vm/os_linux.cpp

reserve内存

char* os::reserve_memory(size_t bytes, char* requested_addr,size_t alignment_hint) {return anon_mmap(requested_addr, bytes, (requested_addr != NULL));
}static char* anon_mmap(char* requested_addr, size_t bytes, bool fixed) {char * addr;int flags;flags = MAP_PRIVATE | MAP_NORESERVE | MAP_ANONYMOUS;if (fixed) {assert((uintptr_t)requested_addr % os::Linux::page_size() == 0, "unaligned address");flags |= MAP_FIXED;}// Map uncommitted pages PROT_READ and PROT_WRITE, change access// to PROT_EXEC if executable when we commit the page.addr = (char*)::mmap(requested_addr, bytes, PROT_READ|PROT_WRITE,flags, -1, 0);if (addr != MAP_FAILED) {if ((address)addr + bytes > _highest_vm_reserved_address) {_highest_vm_reserved_address = (address)addr + bytes;}}return addr == MAP_FAILED ? NULL : addr;
}

commit内存

// NOTE: Linux kernel does not really reserve the pages for us.
//       All it does is to check if there are enough free pages
//       left at the time of mmap(). This could be a potential
//       problem.
bool os::commit_memory(char* addr, size_t size, bool exec) {int prot = exec ? PROT_READ|PROT_WRITE|PROT_EXEC : PROT_READ|PROT_WRITE;uintptr_t res = (uintptr_t) ::mmap(addr, size, prot,MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0);return res != (uintptr_t) MAP_FAILED;
}

不管是reserve还是commit内存,背后都是调用mmap函数。只是保护位字段和flags标志位略有差异。

三 mmap函数分析

函数原型, 本文不展开详细讲

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

mmap主要做文件映射,也可以用来为进程申请内存。jdk显然是用来申请内存空间。但是这个系统函数调用后,os并不会立刻分配物理内存,而是等对申请到的内存块进行具体的读写之后再进行物理内存page实际分配。

测试代码

#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>int main() {int test = 0;size_t initial_size = 1024*1024*50;  // 初始大小为 50MBsize_t expanded_size = 1024*1024*512; // 扩展大小为 512MB// 创建映射区域void *ptr = mmap(NULL, initial_size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);if (ptr == MAP_FAILED) {perror("mmap");exit(EXIT_FAILURE);}printf("Initial size: %zuKB\n", initial_size / 1024);scanf("%d", &test);// 使用 mremap 扩展映射区域的大小void *new_ptr = mremap(ptr, initial_size, expanded_size, MREMAP_MAYMOVE);if (new_ptr == MAP_FAILED) {perror("mremap");exit(EXIT_FAILURE);}printf("Expanded size: %zuKB\n", expanded_size / 1024);scanf("%d", &test);// 使用新的映射区域进行读写操作...//使用10Mmemset(new_ptr, 0, 1024 * 1024 * 10);scanf("%d", &test);// 使用100Mmemset(new_ptr, 0, 1024 * 1024 * 100);scanf("%d", &test);// 解除映射if (munmap(new_ptr, expanded_size) == -1) {perror("munmap");exit(EXIT_FAILURE);}return 0;
}

3.1 执行mmap函数

mmap函数执行后,查看top输出,虚拟内存52M接近申请的50M,而RES仅有1M
在这里插入图片描述

3.2 执行mremap

mremap执行后,查看top输出,虚拟内存涨到了514MB,接近扩容申请的512MB,RES常驻内存不变
在这里插入图片描述

3.3 执行第一个memset

接着执行第一个memset,进行内存写入。这次发现虚拟内存不变,而RES物理内存增长了10MB,这和memset的内存大小一致
在这里插入图片描述

3.4 执行第2个memset

接着执行第二个memset,写入100MB(指针位置没有变化)。虚存没有变化,RES增加了90MB。
在这里插入图片描述
使用pmp命令分析

lvsheng@lvsheng:/proc/36287$ pmap -x  41422
41422:   ./mmap
Address           Kbytes     RSS   Dirty Mode  Mapping
0000c8e2d33a0000       4       4       0 r-x-- mmap
0000c8e2d33bf000       4       4       4 r---- mmap
0000c8e2d33c0000       4       4       4 rw--- mmap
0000c8e309014000     132       4       4 rw---   [ anon ]
0000e16bc8c00000  524288  102400  102400 rw---   [ anon ]
0000e16bebe20000    1640    1088       0 r-x-- libc.so.6
0000e16bebfba000      76       0       0 ----- libc.so.6
0000e16bebfcd000      12      12      12 r---- libc.so.6
0000e16bebfd0000       8       8       8 rw--- libc.so.6
0000e16bebfd2000      48      16      16 rw---   [ anon ]
0000e16bebfdf000     156     156       0 r-x-- ld-linux-aarch64.so.1
0000e16bec018000       8       8       8 rw---   [ anon ]
0000e16bec01a000       8       0       0 r----   [ anon ]
0000e16bec01c000       4       4       0 r-x--   [ anon ]
0000e16bec01d000       8       8       8 r---- ld-linux-aarch64.so.1
0000e16bec01f000       8       8       8 rw--- ld-linux-aarch64.so.1
0000fffff236c000     132      12      12 rw---   [ stack ]
---------------- ------- ------- -------
total kB          526540  103736  102484

512MB的虚拟内存,OS分配了100MB物理内存

四 总结

在这里插入图片描述

  1. jdk通过mmap申请内存后,操作系统为jvm分配的是虚拟内存,并没有分配实际的物理内存。
  2. 当Java应用程序实际写入时,OS会发生缺页中断, 将虚拟内存page映射到物理内存page。

这是linux的lazy allocation现象(你可以想象这样做的好处是什么?), 所以nmt和top的RES指标的差异会很明显

五 更进一步 one step further

先分配虚拟内存,等到实际使用的时候,才分配内存page,会对业务应用造成一定的性能问题。

  1. 实际分配物理内存,用户态需要切换到内核态
  2. OS内核需要遍历空闲内存链表,寻找一个合适的内存块(要看具体的操作系统的实现)
  3. 没有可用的内存块则需要swap out/in, 读写磁盘对性能影响较大
  4. 内核分配物理内存page后,进程本身的虚拟内存页表还需要维护

若想规避这样的性能影响,可以使用-XX:+AlwaysPreTouch(仅影响堆区)。这个参数会影响进程的启动时间。
这个参数的官网解释:

Pre-touch the Java heap during JVM initialization. Every page of the heap is thus demand-zeroed during initialization rather than incrementally during application execution.

翻译:

在JVM(Java虚拟机)初始化期间预先触碰Java堆。这样,堆中的每一页在初始化时就会被按需置零,而不是在应用执行过程中逐步进行。

修改文章开头的Java程序的启动参数, 并执行

nohup java -Xms2G -Xmx2G -XX:MaxMetaspaceSize=512M -XX:NativeMemoryTracking=detail -XX:+AlwaysPreTouch ThreadOOM &

查看top输出,RES的大小比堆的配置略大,说明生效了。
在这里插入图片描述
用pmap查看,有一个完整的2G的RSS内存块
在这里插入图片描述

参考文章


  1. https://blog.csdn.net/qq_41687938/article/details/119901916
  2. https://www.jianshu.com/p/a8356d03ac8f
  3. https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html
  4. https://www.man7.org/linux/man-pages/man2/mmap.2.html

http://www.ppmy.cn/embedded/160106.html

相关文章

BUU19 [BJDCTF2020]Easy MD51

题目 当点进去不知道干啥的时候&#xff1a;1.看源代码 2.抓包 3.看网络请求 F12 用bp抓包&#xff0c;在response消息头中有hint提示&#xff1a; select * from admin where passwordmd5($pass,true) 如果md5($pass,true)后是 or 1 那么整句话就是password or 1&a…

联想拯救者开机进入bios

如果你的联想拯救者&#xff08;Lenovo Legion&#xff09;笔记本电脑开机后直接进入 BIOS 设置界面&#xff0c;可能是以下原因之一导致的。以下是解决方法&#xff1a; 1. 检查启动顺序 进入 BIOS 后&#xff0c;找到 Boot&#xff08;启动&#xff09;选项卡。检查启动顺序…

【算法设计与分析】实验5:贪心算法—装载及背包问题

目录 一、实验目的 二、实验环境 三、实验内容 四、核心代码 五、记录与处理 六、思考与总结 七、完整报告和成果文件提取链接 一、实验目的 掌握贪心算法求解问题的思想&#xff1b;针对不同问题&#xff0c;会利用贪心算法进行问题建模、求解以及时间复杂度分析&#x…

MySQL 进阶专题:索引(索引原理/操作/优缺点/B+树)

在数据库的秋招面试中&#xff0c;索引&#xff08;Index&#xff09;是一个经典且高频的题目。索引的作用类似于书中的目录&#x1f4d6;&#xff0c;它能够显著加快数据库查询的速度。本文将深入探讨索引的概念、作用、优缺点以及背后的数据结构&#xff0c;帮助你从原理到应…

「全网最细 + 实战源码案例」设计模式——桥接模式

核心思想 桥接模式&#xff08;Bridge Pattern&#xff09;是一种结构型设计模式&#xff0c;将抽象部分与其实现部分分离&#xff0c;使它们可以独立变化。降低代码耦合度&#xff0c;避免类爆炸&#xff0c;提高代码的可扩展性。 结构 1. Implementation&#xff08;实现类…

【办公类-99-01】20250201学具PDF打印会缩小一圈——解决办法:换一个PDF阅读器

背景需求&#xff1a; 2024年1月13日&#xff0c;快要放寒假了&#xff0c;组长拿着我们班的打印好的一叠教案来调整。 “前面周计划下面的家园共育有调整&#xff0c;你自己看批注。” “还有你这个教案部分的模版有问题&#xff0c;太小&#xff08;窄&#xff09;了。考虑…

【前端】【Ts】【知识点总结】TypeScript知识总结

一、总体概述 TypeScript 是 JavaScript 的超集&#xff0c;主要通过静态类型检查和丰富的类型系统来提高代码的健壮性和可维护性。它涵盖了从基础数据类型到高级类型、从函数与对象的类型定义到类、接口、泛型、模块化及装饰器等众多知识点。掌握这些内容有助于编写更清晰、结…

无法连接到远程扩展主机服务器

有一次在VSCode上设置了监听调试&#xff0c;动了launch.json文件&#xff0c;下次再打开VSCode远程连接服务器时打不开文件&#xff0c;报错如下&#xff1a; 无法连接到远程扩展主机服务器 (错误: CodeError(AsyncPipeFailed(Os { code: 2, kind: NotFound, message: “No s…