为Java虚拟机分配堆内存大于机器物理内存会怎么样?

之前在某个地方看到的一个问题,"如果为Java虚拟机指定的堆内存大于物理内存会怎么样?",今天正好又看到了HotSpot VM中关于为堆分配内存的源代码实现,顺便从源代码角度解答一下这个问题。

我们平时为堆分配内存时,会调用到os::reserve_memory()函数,这个函数的实现如下:

复制代码
char* os::reserve_memory(
  size_t bytes, char* addr, size_t alignment_hint) {
  char* result = pd_reserve_memory(bytes, addr, alignment_hint);
  return result;
}

char* os::pd_reserve_memory(
  size_t bytes, char* requested_addr,size_t alignment_hint) {
  return anon_mmap(requested_addr, bytes, (requested_addr != NULL));
} 

调用的anon_mmap()函数的实现如下:

复制代码
源代码位置:openjdk/hotspot/src/os/linux/vm/os_linux.cpp

// 如果参数fixed为true,则要求分配的内存基址从requested_addr开始,如果这个内存基地被
// 占用,则会发生重写,我们不对基址有要求,所以fixed的值为false,requested_addr的值
// 为NULL,如果有值的话,内存基址可能会从从requested_addr开始,不过这不是必须的
static char* anon_mmap(char* requested_addr, size_t bytes, bool fixed) {
  char * addr;
  int flags;
  
  flags = MAP_PRIVATE | MAP_NORESERVE | MAP_ANONYMOUS;
  if (fixed) {
    flags |= MAP_FIXED;
  }
  
  addr = (char*)::mmap(requested_addr, bytes, PROT_NONE,flags, -1, 0);
  
  ...
  
  return addr == MAP_FAILED ? NULL : addr;
}

默认情况下,内核会为匿名映射(如使用MAP_ANONYMOUS创建的映射)预先分配交换空间,确保物理内存不足时将数据换出。而MAP_NORESERVE会跳过此预留步骤,允许进程分配大于当前 可用物理内存+交换空间总和的内存区域。

我们看一下我本地机器的可用物理内存和交换空间的大小:

物理可用内存5.8G,Swap是4G

这里需要解释一下MAP_NORESERVE,表示"不申请交换空间"。由于Linux申请内存是两阶段提交,阶段一是申请到虚拟内存,当有访问到虚拟内存时才会触发第二阶段,为虚拟内存分配对应的物理内存。这里不申请交换空间,因为是处在阶段一,申请交换空间是一种浪费。

对于第一阶段的内存申请,由于申请的是虚拟内存,实际上64 位操作系统,进程可以使用 128 TB 大小的虚拟内存空间,所以进程申请一个远大于本机物理内存是没问题的,只要不读写这个虚拟内存,操作系统就不会分配物理内存。

假设调用anon_map()函数分配500G,实例如下:

复制代码
// 1G内存大小
size_t length = 1UL * 1024 * 1024 * 1024;
char*  c = anon_mmap(NULL,length * 500 ,false);

此时使用如下命令查看这个进程分配的虚拟空间:

复制代码
ps aux | grep -E "VSZ|test"

如下所示。

其中的VSZ显示了进程的虚拟地址空间大小为500G。这个内存已经远远大于了物理内存的大小了。

对于第二阶段来说,我们到底可以使用多大的物理内存呢?这要介绍一下Swap。

当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间会被临时保存到磁盘,等到那些程序要运行时,再从磁盘中恢复保存的数据到内存中。

另外,当内存使用存在压力的时候,会开始触发内存回收行为,会把这些不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。

这种,将内存数据换出磁盘,又从磁盘中恢复数据到内存的过程,就是 Swap 机制负责的。

我们先使用如下命令关闭Swap,然后为虚拟机分配堆的大小为6G,实际上可用的物理内存是5.8G,所以不出意外的,内存分配失败了。

这里在启动虚拟机时,添加了-XX:+AlwaysPreTouch参数,这个参数会按页访问内存,这样就能为虚拟内存分配对应的物理内存了。

我们现在开启Swap后,再为虚拟机分配6G堆大小,如下:

可以看到,程序运行成功。再看内存情况后会看到Swap使用了200多M的内存。

实际上不能太多的使用Swap,否则磁盘换入换出,整个系统会非常卡。所以Swap可以看成一种保障,一定程度上可保障在内存吃紧时不会杀掉进程,但是如果虚拟机开始使用Swap,通常会造成性能明显下降,由Swap引起的性能问题也不算少。

所以我们可不能认为,当内存敏感型的程序上线后,如果内存不足,可借用Swap来扩大内存提高程序运行效率。

更多可访问网站:JDK源码剖析网