一 问题现象
前一段时间用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 {
@Override
public 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; // 初始大小为 50MB
size_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);
// 使用新的映射区域进行读写操作...
//使用10M
memset(new_ptr, 0, 1024 * 1024 * 10);
scanf("%d", &test);
// 使用100M
memset(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