这是一个很有意思的问题:假设我们现在有一台物理内存16G的机器,那么我们能否给运行于其上的Java虚拟机分配大于16G大小的堆呢?
从直觉上来说,这似乎有点不太可能。但是稍微有点操作系统知识的人就会意识到,这其实是可以的。因为当我们设置堆大小为17G的时候,其实并不是直接分配了17G的物理内存。而只是分配了17G的虚拟内存。这些虚拟内存尚没有映射到真实的物理内存上。
为了验证这个结果,做一个小实验。
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, world");
}
}
它会在我的Mac上运行,我的Mac物理内存16G:
很显然,我的机器已经占用了很多内存了,只剩下不多的东西。然后运行HelloWorld的main方法,设置参数:
-Xms17g -Xmx17g
将堆的大小固定为17G。
成功输出了结果:
现在我们进一步考虑这个问题,17G只是比16G大了一点点,也就是说,物理内存加上swap space,是大于17G的。如果我们将这个值设置得更加大,以至于堆大小比整个物理内存加上swap space还要大,会有什么情况呢?
设置参数
-Xms128g -Xmx128g
实际上,你可以将值设置得非常非常大,然后输出结果:
-Xms12800g -Xmx12800g
在这种设置下,依旧会输出"Hello World",但是中间有很长一段时间的卡顿,并且内存占用会一直上升,直到达到某个值:
为什么会是四十多G,这个问题我无法解释……因为我也不知道。
中间卡顿是因为操作系统分配虚拟内存。
那么又会有一个问题,堆究竟能设置到多大?
答案是,这取决于你的机器、操作系统和虚拟机。举例来说,在64位机器上运行32位的虚拟机,那么最大堆只能设置到4G——至少对于大多数的虚拟机来说4G就是上限了。
可以参考https://stackoverflow.com/questions/1434779/maximum-java-heap-size-of-a-32-bit-jvm-on-a-64-bit-os
理论上来说,虚拟机的堆大小只会受到虚拟内存地址空间大小的限制。
那么,什么时候操作系统才会真的分配物理内存给虚拟机呢?
答案就是,在内存第一次被使用的时候。这个使用并不是说我新建了一个对象,然后操作系统就唰的把所有物理内存分配好了,它只是按照虚拟机的需求,分配了一点点。(实际上是,只是把使用到的这部分内存按照页映射到了物理内存上)
虚拟机有一个参数可以控制整个过程,促使操作系统在虚拟机启动的时候就直接完成物理内存的绑定。使用参数:
-Xms128g -Xmx128g -XX:+AlwaysPreTouch
JVM直接就崩掉了:
Process finished with exit code 137 (interrupted by signal 9: SIGKILL)
实际上,在Linux上,只要加上整个-XX:+AlwaysPreTouch,操作系统就会在无法满足内存需求的时候直接kill掉JVM。最大值就是物理内存+swap大小-已经使用内存。
如果我真的将堆设置那么大,并且不使用-XX:+AlwaysPreTouch选项,那么JVM能够正常运行吗?
前面的HelloWorld其实很鸡贼,因为它真正运行所需的内存非常小。现在假设,我们的确需要一个128G大小的堆,但是我们的机器只有16G物理内存和16G swap space,那么在这台机器上运行虚拟机,它能正常工作吗?
答案是,它可能能够正常启动,但是肯定不能正常运行。因为虚拟机在启动的时候,并不是直接就使用了128G,只要启动时候所需要的内存不超过物理内存+swap space大小,就能启动。但是在运行的过程中,虚拟机不断的请求真的内存,那么就会超过32G,而后虚拟机就崩掉。这并不是OOM,而是在操作系统层面上的崩掉。
使用Swap space究竟好不好
答案是很糟糕,很不好。之前有一个小伙伴咨询过我一个GC停顿时间长的问题,就是因为swap space。在GC的时候,要沿着引用链查找所有的存活对象。如果这部分对象所在的内存被放到了swap space上,那么就会引起缺页中断。极端情况下,会出现不断置换页。一个页在开始的时候没被扫描到,交换到了swap space上,而后又被载入物理内存,因为立刻又扫描它了。
JVM使用了TLB的技术,所以其空间局部性还是很好的。
所以比较好的实践是,在Linux机器上将vm.swappiness参数设置到0,也就是不适用swap space。在使用大堆的情况下,这是一个非常隐蔽的坑。