从java9共享内存加载modules说起
jdk9后加載lib/modules的方式
從jdk的代碼里可以看出來,默認(rèn)的實(shí)現(xiàn)加載lib/modules是用mmap來加載的。
class NativeImageBuffer {static {java.security.AccessController.doPrivileged(new java.security.PrivilegedAction<Void>() {public Void run() {System.loadLibrary("jimage");return null;}});}native static ByteBuffer getNativeMap(String imagePath); }在jimage動態(tài)庫里最終是一個(gè)cpp實(shí)現(xiàn)的ImageFileReader來讀取的。它在64位os上使用的是mmap方式:
https://github.com/dmlloyd/openjdk/blob/jdk/jdk10/src/java.base/share/native/libjimage/imageFile.cpp#L44
啟動多個(gè)jvm時(shí)會有好處:
- 減少內(nèi)存占用
- 加快啟動速度
突然有個(gè)想法,怎么驗(yàn)證多個(gè)jvm的確共享了內(nèi)存?
下面來驗(yàn)證一下,思路是:
linux下查看進(jìn)程的mmap信息
啟動一個(gè)jshell之后,用pmap查看mmap信息,其中RSS(resident set size)列表示真實(shí)占用的內(nèi)存。:
$ pmap -x 24615 24615: jdk9/jdk-9.0.4/bin/jshell Address Kbytes RSS Dirty Mode Mapping 0000000000400000 4 4 0 r-x-- jshell 0000000000601000 4 4 4 rw--- jshell 000000000111b000 132 120 120 rw--- [ anon ] ... 00007f764192c000 88 64 0 r-x-- libnet.so 00007f7641942000 2048 0 0 ----- libnet.so 00007f7641b42000 4 4 4 rw--- libnet.so 00007f7641b43000 2496 588 588 rwx-- [ anon ] ... 00007f7650b43000 185076 9880 0 r--s- modules 00007f765c000000 5172 5124 5124 rw--- [ anon ]---------------- ------- ------- ------- total kB 2554068 128756 106560我們可以找到modules文件的信息:
00007f7650b43000 185076 9880 0 r--s- modules它的文件映射大小是185076kb,實(shí)際使用內(nèi)存大小是9880kb。
linux kernel關(guān)于pagemap的說明
上面我們獲取到了modules的虛擬地址,但是還需要轉(zhuǎn)換為物理地址。
正常來說一個(gè)進(jìn)程是沒有辦法知道它自己的虛擬地址對應(yīng)的是什么物理地址。不過我們用linux kernel提供的信息可以讀取,轉(zhuǎn)換為物理地址。
linux每個(gè)進(jìn)程都有個(gè)/proc/$pid/pagemap文件,里面記錄了內(nèi)存頁的信息:
https://www.kernel.org/doc/Documentation/vm/pagemap.txt
簡而言之,在pagemap里每一個(gè)virtual page都有一個(gè)對應(yīng)的64 bit的信息:
* Bits 0-54 page frame number (PFN) if present* Bits 0-4 swap type if swapped* Bits 5-54 swap offset if swapped* Bit 55 pte is soft-dirty (see Documentation/vm/soft-dirty.txt)* Bit 56 page exclusively mapped (since 4.2)* Bits 57-60 zero* Bit 61 page is file-page or shared-anon (since 3.5)* Bit 62 page swapped* Bit 63 page present只要把虛擬地址轉(zhuǎn)換為pagemap文件里的offset,就可以讀取具體的virtual page信息。計(jì)算方法是:
// getpagesize()是系統(tǒng)調(diào)用 // 64bit是8字節(jié) long virtualPageIndex = virtualAddress / getpagesize() offset = virtualPageIndex * 8從offset里讀取出來的64bit里,可以獲取到page frame number,如果想要得到真正的物理地址,還需要再轉(zhuǎn)換:
// pageFrameNumber * getpagesize() 獲取page的開始地址 // virtualAddress % getpagesize() 獲取到page里的偏移地址 long pageFrameNumber = // read from pagemap file physicalAddress = pageFrameNumber * getpagesize() + virtualAddress % getpagesize();虛擬地址轉(zhuǎn)換物理地址的代碼
參考這里的代碼:https://github.com/cirosantilli/linux-kernel-module-cheat/blob/master/kernel_module/user/common.h
得到的一個(gè)從虛擬地址轉(zhuǎn)換為物理地址的代碼:
#define _POSIX_C_SOURCE 200809L #include <fcntl.h> /* open */ #include <stdint.h> /* uint64_t */ #include <stdlib.h> /* size_t */ #include <unistd.h> /* pread, sysconf */int BUFSIZ = 1024;typedef struct {uint64_t pfn : 54;unsigned int soft_dirty : 1;unsigned int file_page : 1;unsigned int swapped : 1;unsigned int present : 1; } PagemapEntry;/* Parse the pagemap entry for the given virtual address.** @param[out] entry the parsed entry* @param[in] pagemap_fd file descriptor to an open /proc/pid/pagemap file* @param[in] vaddr virtual address to get entry for* @return 0 for success, 1 for failure*/ int pagemap_get_entry(PagemapEntry *entry, int pagemap_fd, uintptr_t vaddr) {size_t nread;ssize_t ret;uint64_t data;nread = 0;while (nread < sizeof(data)) {ret = pread(pagemap_fd, &data, sizeof(data),(vaddr / sysconf(_SC_PAGE_SIZE)) * sizeof(data) + nread);nread += ret;if (ret <= 0) {return 1;}}entry->pfn = data & (((uint64_t)1 << 54) - 1);entry->soft_dirty = (data >> 54) & 1;entry->file_page = (data >> 61) & 1;entry->swapped = (data >> 62) & 1;entry->present = (data >> 63) & 1;return 0; }/* Convert the given virtual address to physical using /proc/PID/pagemap.** @param[out] paddr physical address* @param[in] pid process to convert for* @param[in] vaddr virtual address to get entry for* @return 0 for success, 1 for failure*/ int virt_to_phys_user(uintptr_t *paddr, pid_t pid, uintptr_t vaddr) {char pagemap_file[BUFSIZ];int pagemap_fd;snprintf(pagemap_file, sizeof(pagemap_file), "/proc/%ju/pagemap", (uintmax_t)pid);pagemap_fd = open(pagemap_file, O_RDONLY);if (pagemap_fd < 0) {return 1;}PagemapEntry entry;if (pagemap_get_entry(&entry, pagemap_fd, vaddr)) {return 1;}close(pagemap_fd);*paddr = (entry.pfn * sysconf(_SC_PAGE_SIZE)) + (vaddr % sysconf(_SC_PAGE_SIZE));return 0; }int main(int argc, char ** argv){char *end;int pid;uintptr_t virt_addr;uintptr_t paddr;int return_code;pid = strtol(argv[1],&end, 10);virt_addr = strtol(argv[2], NULL, 16);return_code = virt_to_phys_user(&paddr, pid, virt_addr);if(return_code == 0)printf("Vaddr: 0x%lx, paddr: 0x%lx \n", virt_addr, paddr);elseprintf("error\n"); }另外,收集到一些可以讀取pagemap信息的工具:
- https://github.com/dwks/pagemap
檢查兩個(gè)jvm進(jìn)程是否映射modules的物理地址一致
$ jps
25105 jdk.internal.jshell.tool.JShellToolProvider
25142 jdk.internal.jshell.tool.JShellToolProvider
把上面轉(zhuǎn)換地址的代碼保存為mymap.c,再編繹
gcc mymap.c -o mymap獲取兩個(gè)jvm的modules的虛擬地址,并轉(zhuǎn)換為物理地址
$ pmap -x 25105 | grep modules 00007f82b4b43000 185076 9880 0 r--s- modules $ sudo ./mymap 25105 00007f82b4b43000 Vaddr: 0x7f82b4b43000, paddr: 0x33598000$ pmap -x 25142 | grep modules 00007ff220504000 185076 10064 0 r--s- modules $ sudo ./mymap 25142 00007ff220504000 Vaddr: 0x7ff220504000, paddr: 0x33598000可以看到兩個(gè)jvm進(jìn)程映射modules的物理地址是一樣的,證實(shí)了最開始的想法。
kernel 里的 page-types 工具
其實(shí)在kernel里自帶有一個(gè)工具page-types可以輸出一個(gè)page信息,可以通過下面的方式來獲取內(nèi)核源碼,然后自己編繹:
sudo apt-get source linux-image-$(uname -r) sudo apt-get build-dep linux-image-$(uname -r)到tools/vm目錄下面,可以直接sudo make編繹。
sudo ./page-types -p 25105 flags page-count MB symbolic-flags long-symbolic-flags 0x0000000000000000 2 0 ____________________________________ 0x0000000000400000 14819 57 ______________________t_____________ thp 0x0000000000000800 1 0 ___________M________________________ mmap 0x0000000000000828 33 0 ___U_l_____M________________________ uptodate,lru,mmap 0x000000000000086c 663 2 __RU_lA____M________________________ referenced,uptodate,lru,active,mmap 0x000000000000087c 2 0 __RUDlA____M________________________ referenced,uptodate,dirty,lru,active,mmap 0x0000000000005868 10415 40 ___U_lA____Ma_b_____________________ uptodate,lru,active,mmap,anonymous,swapbacked 0x0000000000405868 29 0 ___U_lA____Ma_b_______t_____________ uptodate,lru,active,mmap,anonymous,swapbacked,thp 0x000000000000586c 5 0 __RU_lA____Ma_b_____________________ referenced,uptodate,lru,active,mmap,anonymous,swapbacked 0x0000000000005878 356 1 ___UDlA____Ma_b_____________________ uptodate,dirty,lru,active,mmap,anonymous,swapbacked total 26325 102jdk8及之前加載jar也是使用mmap的方式
在驗(yàn)證了jdk9加載lib/modules之后,隨便檢查了下jdk8的進(jìn)程,發(fā)現(xiàn)在加載jar包時(shí),也是使用mmap的方式。
一個(gè)tomcat進(jìn)程的map信息如下:
$ pmap -x 27226 | grep jar ... 00007f42c00d4000 16 16 0 r--s- tomcat-dbcp.jar 00007f42c09b7000 1892 1892 0 r--s- rt.jar 00007f42c45e5000 76 76 0 r--s- catalina.jar 00007f42c45f8000 12 12 0 r--s- tomcat-i18n-es.jar 00007f42c47da000 4 4 0 r--s- sunec.jar 00007f42c47db000 8 8 0 r--s- websocket-api.jar 00007f42c47dd000 4 4 0 r--s- tomcat-juli.jar 00007f42c47de000 4 4 0 r--s- commons-daemon.jar 00007f42c47df000 4 4 0 r--s- bootstrap.jar可以發(fā)現(xiàn)一些有意思的點(diǎn):
- 所有jar包的Kbytes 和 RSS(resident set size)是相等的,也就是說整個(gè)jar包都被加載到共享內(nèi)存里了
- 從URLClassLoader的實(shí)現(xiàn)代碼來看,它在加載資源時(shí),需要掃描所有的jar包,所以會導(dǎo)致整個(gè)jar都要被加載到內(nèi)存里
- 對比jdk9里的modules,它的RSS并不是很高,原因是JImage的格式設(shè)計(jì)合理。所以jdk9后,jvm占用真實(shí)內(nèi)存會降低。
jdk8及之前的 sun.zip.disableMemoryMapping 參數(shù)
在jdk6里引入一個(gè) sun.zip.disableMemoryMapping參數(shù),禁止掉利用mmap來加載zip包。http://www.oracle.com/technetwork/java/javase/documentation/overview-156328.html#6u21-rev-b09
https://bugs.openjdk.java.net/browse/JDK-8175192 在jdk9里把這個(gè)參數(shù)去掉了。因?yàn)閖dk9之后,jdk本身存在lib/modules 這個(gè)文件里了。
總結(jié)
- linux下可以用pmap來獲取進(jìn)程mmap信息
- 通過讀取/proc/$pid/pagemap可以獲取到內(nèi)存頁的信息,并可以把虛擬地址轉(zhuǎn)換為物理地址
- jdk9把類都打包到lib/modules,也就是JImage格式,可以減少真實(shí)內(nèi)存占用
- jdk9多個(gè)jvm可以共用lib/modules映射的內(nèi)存
- 默認(rèn)情況下jdk8及以前是用mmap來加載jar包
總結(jié)
以上是生活随笔為你收集整理的从java9共享内存加载modules说起的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 移动金融客户端应用软件备案、中国支付清算
- 下一篇: OneFlow源码解析:静态图与运行时