一 问题现象
前一段时间用nmt查看jvm进程的栈区占用的内存大小。测试代码如下
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);}}}
}
启动命令
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)
显示线程占用了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函数
三 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物理内存
四 总结
- jdk通过mmap申请内存后,操作系统分配的虚拟内存,并没有分配实际的物理内存。
- 当Java应用程序实际写入时,OS才会分配物理内存。
所以nmt和top的RES指标的差异会很明显
参考文章
- https://blog.csdn.net/qq_41687938/article/details/119901916