jdk9后加载lib/modules的方式
从jdk的代码里可以看出来,默认的实现加载lib/modules
是用mmap来加载的。
1 | class NativeImageBuffer { |
在jimage动态库里最终是一个cpp实现的ImageFileReader
来读取的。它在64位os上使用的是mmap方式:
通过共享内存启动多个jvm时会有好处:
- 减少内存占用
- 加快启动速度
突然有个想法,怎么验证多个jvm的确共享了内存?
下面来验证一下,思路是:
- 先获取进程的mmap信息
- 获取jvm进程映射
modules
的虚拟地址 - 从虚拟地址转换为物理地址
- 启动两个jvm进程,计算它们映射
modules
是否物理地址是一样的
linux下查看进程的mmap信息
- 使用
pmap -x $pid
命令 - 直接查看
cat /proc/$pid/maps
文件的内容
启动一个jshell之后,用pmap查看mmap信息,其中RSS(resident set size)列表示真实占用的内存。:
1 | $ pmap -x 24615 |
我们可以找到modules
文件的信息:
1 | 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的信息:
1 | * Bits 0-54 page frame number (PFN) if present |
只要把虚拟地址转换为pagemap文件里的offset,就可以读取具体的virtual page信息。计算方法是:
1 | // getpagesize()是系统调用 |
从offset里读取出来的64bit里,可以获取到page frame number,如果想要得到真正的物理地址,还需要再转换:
1 | // pageFrameNumber * getpagesize() 获取page的开始地址 |
虚拟地址转换物理地址的代码
参考这里的代码:https://github.com/cirosantilli/linux-kernel-module-cheat/blob/master/kernel_module/user/common.h
得到的一个从虚拟地址转换为物理地址的代码:
1 |
|
另外,收集到一些可以读取pagemap信息的工具:
检查两个jvm进程是否映射modules
的物理地址一致
先启动两个jshell
1
2
3$ jps
25105 jdk.internal.jshell.tool.JShellToolProvider
25142 jdk.internal.jshell.tool.JShellToolProvider把上面转换地址的代码保存为
mymap.c
,再编绎1
gcc mymap.c -o mymap
获取两个jvm的modules的虚拟地址,并转换为物理地址
1
2
3
4
5
6
7
8
9$ 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信息,可以通过下面的方式来获取内核源码,然后自己编绎:
1 | sudo apt-get source linux-image-$(uname -r) |
到tools/vm
目录下面,可以直接sudo make
编绎。
1 | sudo ./page-types -p 25105 |
jdk8及之前加载jar也是使用mmap的方式
在验证了jdk9加载lib/modules
之后,随便检查了下jdk8的进程,发现在加载jar包时,也是使用mmap的方式。
一个tomcat进程的map信息如下:
1 | $ pmap -x 27226 | grep 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-b09https://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包