歡迎光臨
每天分享高質量文章

從 Java9 共享記憶體載入 modules 說起

(點選上方公眾號,可快速關註)


來源: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的確共享了記憶體?

下麵來驗證一下,思路是:

  1. 先獲取行程的mmap資訊

  2. 獲取jvm行程對映modules的虛擬地址

  3. 從虛擬地址轉換為物理地址

  4. 啟動兩個jvm行程,計算它們對映modules是否物理地址是一樣的

linux下檢視行程的mmap資訊

  1. 使用pmap -x $pid命令

  2. 直接檢視 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

可以發現一些有意思的點:

  1. 所有jar包的Kbytes 和 RSS(resident set size)是相等的,也就是說整個jar包都被載入到共享記憶體裡了

  2. 從URLClassLoader的實現程式碼來看,它在載入資源時,需要掃描所有的jar包,所以會導致整個jar都要被載入到記憶體裡

  3. 對比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 這個檔案裡了。

總結

  1. linux下可以用pmap來獲取行程mmap資訊

  2. 透過讀取/proc/$pid/pagemap可以獲取到記憶體頁的資訊,並可以把虛擬地址轉換為物理地址

  3. jdk9把類都打包到lib/modules,也就是JImage格式,可以減少真實記憶體佔用

  4. jdk9多個jvm可以共用lib/modules對映的記憶體

  5. 預設情況下jdk8及以前是用mmap來載入jar包

看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂