(點選上方公眾號,可快速關註)
來源:hengyunabc,
blog.csdn.net/hengyunabc/article/details/79354041
Jdk9後載入lib/modules的方式
從jdk的程式碼裡可以看出來,預設的實現載入lib/modules是用mmap來載入的。
class NativeImageBuffer {
static {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction
() { public Void run() {
System.loadLibrary(“jimage”);
return null;
}
});
}
native static ByteBuffer getNativeMap(String imagePath);
}
在jimage動態庫裡最終是一個cpp實現的ImageFileReader來讀取的。它在64位os上使用的是mmap方式:
https://github.com/dmlloyd/openjdk/blob/jdk/jdk10/src/java.base/share/native/libjimage/imageFile.cpp#L44
啟動多個jvm時會有好處:
-
減少記憶體佔用
-
加快啟動速度
突然有個想法,怎麼驗證多個jvm的確共享了記憶體?
下麵來驗證一下,思路是:
-
先獲取行程的mmap資訊
-
獲取jvm行程對映modules的虛擬地址
-
從虛擬地址轉換為物理地址
-
啟動兩個jvm行程,計算它們對映modules是否物理地址是一樣的
linux下檢視行程的mmap資訊
-
使用pmap -x $pid命令
-
直接檢視 cat /proc/$pid/maps檔案的內容
啟動一個jshell之後,用pmap檢視mmap資訊,其中RSS(resident set size)串列示真實佔用的記憶體。:
$ 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,實際使用記憶體大小是9880kb。
linux kernel關於pagemap的說明
上面我們獲取到了modules的虛擬地址,但是還需要轉換為物理地址。
正常來說一個行程是沒有辦法知道它自己的虛擬地址對應的是什麼物理地址。不過我們用linux kernel提供的資訊可以讀取,轉換為物理地址。
linux每個行程都有個/proc/$pid/pagemap檔案,裡面記錄了記憶體頁的資訊:
https://www.kernel.org/doc/Documentation/vm/pagemap.txt
簡而言之,在pagemap裡每一個virtual page都有一個對應的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
只要把虛擬地址轉換為pagemap檔案裡的offset,就可以讀取具體的virtual page資訊。計算方法是:
// getpagesize()是系統呼叫
// 64bit是8位元組
long virtualPageIndex = virtualAddress / getpagesize()
offset = virtualPageIndex * 8
從offset裡讀取出來的64bit裡,可以獲取到page frame number,如果想要得到真正的物理地址,還需要再轉換:
// pageFrameNumber * getpagesize() 獲取page的開始地址
// virtualAddress % getpagesize() 獲取到page裡的偏移地址
long pageFrameNumber = // read from pagemap file
physicalAddress = pageFrameNumber * getpagesize() + virtualAddress % getpagesize();
虛擬地址轉換物理地址的程式碼
參考這裡的程式碼:https://github.com/cirosantilli/linux-kernel-module-cheat/blob/master/kernel_module/user/common.h
得到的一個從虛擬地址轉換為物理地址的程式碼:
#define _POSIX_C_SOURCE 200809L
#include
/* open */ #include
/* uint64_t */ #include
/* size_t */ #include
/* 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);
else
printf(“error\n”);
}
另外,收集到一些可以讀取pagemap資訊的工具:
https://github.com/dwks/pagemap
檢查兩個jvm行程是否對映modules的物理地址一致
先啟動兩個jshell
$ jps
25105 jdk.internal.jshell.tool.JShellToolProvider
25142 jdk.internal.jshell.tool.JShellToolProvider
把上面轉換地址的程式碼儲存為mymap.c,再編繹
gcc mymap.c -o mymap
獲取兩個jvm的modules的虛擬地址,並轉換為物理地址、、
$ 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
可以看到兩個jvm行程對映modules的物理地址是一樣的,證實了最開始的想法。
kernel 裡的 page-types 工具
其實在kernel裡自帶有一個工具page-types可以輸出一個page資訊,可以透過下麵的方式來獲取核心原始碼,然後自己編繹:
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 102
Jdk8及之前載入jar也是使用mmap的方式
在驗證了jdk9載入lib/modules之後,隨便檢查了下jdk8的行程,發現在載入jar包時,也是使用mmap的方式。
一個tomcat行程的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
可以發現一些有意思的點:
-
所有jar包的Kbytes 和 RSS(resident set size)是相等的,也就是說整個jar包都被載入到共享記憶體裡了
-
從URLClassLoader的實現程式碼來看,它在載入資源時,需要掃描所有的jar包,所以會導致整個jar都要被載入到記憶體裡
-
對比jdk9裡的modules,它的RSS並不是很高,原因是JImage的格式設計合理。所以jdk9後,jvm佔用真實記憶體會降低。
jdk8及之前的 sun.zip.disableMemoryMapping 引數
-
在jdk6裡引入一個 sun.zip.disableMemoryMapping引數,禁止掉利用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裡把這個引數去掉了。因為jdk9之後,jdk本身存在lib/modules 這個檔案裡了。
總結
-
linux下可以用pmap來獲取行程mmap資訊
-
透過讀取/proc/$pid/pagemap可以獲取到記憶體頁的資訊,並可以把虛擬地址轉換為物理地址
-
jdk9把類都打包到lib/modules,也就是JImage格式,可以減少真實記憶體佔用
-
jdk9多個jvm可以共用lib/modules對映的記憶體
-
預設情況下jdk8及以前是用mmap來載入jar包
看完本文有收穫?請轉發分享給更多人
關註「ImportNew」,提升Java技能