从java9共享内存加载modules说起

本文涉及的产品
公网NAT网关,每月750个小时 15CU
简介: jdk9后加载lib/modules的方式从jdk的代码里可以看出来,默认的实现加载lib/modules是用mmap来加载的。

jdk9后加载lib/modules的方式

从jdk的代码里可以看出来,默认的实现加载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动态库里最终是一个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 <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);
    else
        printf("error\n");
}

另外,收集到一些可以读取pagemap信息的工具:

检查两个jvm进程是否映射modules的物理地址一致

  1. 先启动两个jshell

    $ jps
    25105 jdk.internal.jshell.tool.JShellToolProvider
    25142 jdk.internal.jshell.tool.JShellToolProvider
  2. 把上面转换地址的代码保存为mymap.c,再编绎

    gcc mymap.c -o mymap
  3. 获取两个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包的KbytesRSS(resident set size)是相等的,也就是说整个jar包都被加载到共享内存里了
  • 从URLClassLoader的实现代码来看,它在加载资源时,需要扫描所有的jar包,所以会导致整个jar都要被加载到内存里
  • 对比jdk9里的modules,它的RSS并不是很高,原因是JImage的格式设计合理。所以jdk9后,jvm占用真实内存会降低。

jdk8及之前的 sun.zip.disableMemoryMapping 参数

总结

  • linux下可以用pmap来获取进程mmap信息
  • 通过读取/proc/$pid/pagemap可以获取到内存页的信息,并可以把虚拟地址转换为物理地址
  • jdk9把类都打包到lib/modules,也就是JImage格式,可以减少真实内存占用
  • jdk9多个jvm可以共用lib/modules映射的内存
  • 默认情况下jdk8及以前是用mmap来加载jar包
相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
相关文章
|
11天前
|
缓存 安全 Java
Java并发编程进阶:深入理解Java内存模型
Java并发编程进阶:深入理解Java内存模型
25 0
|
10天前
|
存储 算法 Java
深入浅出Java内存管理
【8月更文挑战第28天】Java的内存管理是每个Java开发者都绕不过去的技术话题。本文将通过生动的比喻和直观的例子,带你走进Java内存管理的奇妙世界。我们将一起探索对象在Java虚拟机中的生命周期,了解栈与堆的区别,以及垃圾回收机制如何默默守护着我们的应用程序。准备好,我们即将启程!
36 14
|
2天前
|
算法 安全 Java
Java内存管理:深入理解垃圾收集器
在Java的世界里,内存管理是一块基石,它支撑着应用程序的稳定运行。本文将带你走进Java的垃圾收集器(GC),探索它是如何默默守护着我们的内存安全。我们将从垃圾收集的基本概念出发,逐步深入到不同垃圾收集器的工作机制,并通过实例分析它们在实际应用中的表现。文章不仅旨在提升你对Java内存管理的认识,更希望你能通过这些知识优化你的代码,让程序运行更加高效。
20 3
|
10天前
|
监控 算法 Java
Java内存管理:垃圾收集器的工作原理与调优实践
在Java的世界里,内存管理是一块神秘的领域。它像是一位默默无闻的守护者,确保程序顺畅运行而不被无用对象所困扰。本文将带你一探究竟,了解垃圾收集器如何在后台无声地工作,以及如何通过调优来提升系统性能。让我们一起走进Java内存管理的迷宫,寻找提高应用性能的秘诀。
|
8天前
|
Kubernetes Cloud Native Java
云原生之旅:从容器到微服务的演进之路Java 内存管理:垃圾收集器与性能调优
【8月更文挑战第30天】在数字化时代的浪潮中,企业如何乘风破浪?云原生技术提供了一个强有力的桨。本文将带你从容器技术的基石出发,探索微服务架构的奥秘,最终实现在云端自由翱翔的梦想。我们将一起见证代码如何转化为业务的翅膀,让你的应用在云海中高飞。
|
1天前
|
安全 Java API
【本地与Java无缝对接】JDK 22外部函数和内存API:JNI终结者,性能与安全双提升!
【9月更文挑战第6天】JDK 22的外部函数和内存API无疑是Java编程语言发展史上的一个重要里程碑。它不仅解决了JNI的诸多局限和挑战,还为Java与本地代码的互操作提供了更加高效、安全和简洁的解决方案。随着FFM API的逐渐成熟和完善,我们有理由相信,Java将在更多领域展现出其强大的生命力和竞争力。让我们共同期待Java编程新纪元的到来!
21 11
|
9天前
|
缓存 Java
Java内存管理秘籍:掌握强软弱幻四大引用,让代码效率翻倍!
【8月更文挑战第29天】在Java中,引用是连接对象与内存的桥梁,主要分为强引用、软引用、弱引用和幻象引用。强引用确保对象生命周期由引用控制,适用于普通对象;软引用在内存不足时可被回收,适合用于内存敏感的缓存;弱引用在无强引用时即可被回收,适用于弱关联如监听器列表;幻象引用需与引用队列配合使用,用于跟踪对象回收状态,适用于执行清理工作。合理使用不同类型的引用车可以提升程序性能和资源管理效率。
31 4
|
10天前
|
Java 编译器 开发者
深入浅出Java内存模型
【8月更文挑战第28天】Java内存模型(JMM)是理解Java并发编程不可或缺的一环。本文通过浅显易懂的方式,带你一探JMM的奥秘,从基本概念到工作原理,再到实际代码示例,逐步揭开Java内存模型的神秘面纱。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供新的视角和深入的理解。
|
11天前
|
存储 缓存 Java
Java内存模型(JMM)
Java内存模型(JMM)是一个抽象概念,用于规范程序中各种变量(实例字段、静态字段及数组元素)的访问方式,确保不同Java虚拟机(JVM)上的并发程序结果一致可靠。JMM定义了主存储器(所有线程共享)与工作存储器(线程私有)的概念,线程间通过主存储器进行通信。JMM具备三大特性:原子性(确保基本读写操作的不可分割)、可见性(确保一个线程对共享变量的修改对其他线程可见)、有序性(防止指令被处理器或编译器重排序影响程序逻辑)。通过这些特性,JMM解决了多线程环境下的数据一致性问题。
|
4天前
|
Java C++
Java内存区域于内存溢出异常
这篇文章详细解释了Java虚拟机的内存区域划分、各区域的作用以及可能遇到的内存溢出异常情况。
12 0

热门文章

最新文章

下一篇
DDNS