个人关于xms/虚拟地址/物理地址的学习和理解

58 阅读5分钟

概述

每个进程都有自己的一套地址空间,32位系统就是4GB的虚拟地址空间

虚拟地址核心的好处主要由以下两点:

  • 如果cpu直接使用物理地址,那当前进程就有可能污染到属于其他进程的物理内存

  • 程序直接使用一套连续的虚拟地址空间,实际映射的物理地址不必是连续的,简化了代码开发工作。

    📜 示例:计算数组元素的间隔

    image-20251123184535463

cpu拿到虚拟地址后,通过MMU(内存管理单元) 转换为物理地址

分段式内存

虚拟内存和物理内存被分成了大小不一 的段,二者通过段表进行映射

存在问题:

  • 当物理内存不足时,可能会将一部分不活跃的内存数据转移到磁盘中,被称为swap ,如果一个段比较大,那swap就比较耗时
  • 进程被关闭时,释放资源,在段与段之间可能产生细小的裂缝,也就是内存碎片。而新启动的进程可能需要一片较大的连续内存空间,就会导致命名空闲内存大小足够,但由于是碎片化的内存,导致新进程无法正常分配内存

为了解决分段内存的问题,引入了分页式内存管理

一级分页式内存

将物理内存和虚拟内存划分为固定大小4KB的空间,称为一页

解决swap效率的问题: 回收内存时,不再以段为段位,而是可以将最近不活跃的页swap到磁盘,数据量更小,效率更高

解决内存碎片的问题: 新启动一个进程,只需要在虚拟地址空间中有一段连续的空间即可,映射到物理内存中,可以拆分成多个细小的页,分散在物理内存的各个区域

虚拟内存和物理内存之间通过页表进行映射

但是这种方式依然存在问题: 页表本身占用的空间可能太大了

举例:每个进程都有自己的虚拟地址空间有页表,一页是4kB,4GB的虚拟地址空间有1024 * 1024个页,而页表必须覆盖所有的虚拟地址空间,那么页表需要有1024 * 1024个页表项,也就是占用1024 * 1024 * 4B(每个页表项需要4字节的空间) = 4MB的空间,如果有100个进程,就需要100张页表,占用空间400MB,实际操作系统中运行的进程只会更多,占用的内存太大了

为什么单级页表必须覆盖所有的虚拟地址空间?

页表本身占用的是一段连续的内存空间,正因为其是连续的,才能使用 页表基地址 + 页号 * 4 (一个页表项占用宽度为1B,长度为4的内存空间)这个公式来定位指定的页表项。

假设页表不覆盖所有虚拟地址空间了,也就是说在页表的空间中存在空隙,那么通过 页表基地址 + 页号 * 4 来定位页表项的公式就无效了(可以理解为java 的数组的元素个数是固定的)

二级分页式内存

为了解决页表占用空间较大的问题,将原本的1024 * 1024 拆分成两级页表,一级页表中包含1024 个页表项,这部分必须一开始就创建好。一级页表中的某个页表项对应的二级页表中也有1024个页表项。

假设某个一级页表项对应的二级页表的内存根本没有被分配给进程使用,那这部分二级页表就不用被创建

实际上,进程启动时,一级页表完整创建,对应的二级页表按需创建,这么做就可以节约内存空间

类似的,还有三级页表等,原理类似,更加节约内存

-xms/-xmx

当我们设置xms == xmx = 2G,堆不会真的立即占用2G的物理内存。

操作系统会承诺给jvm 2G的虚拟地址空间给堆,堆的大小固定为2G,此时,这部分虚拟地址还没有对应的物理地址

随着java进程的不断使用,堆中的对象不断增加,某个虚拟地址首次被访问的时候,因为页表中查不到对应的物理地址,因此会触发缺页异常,等待操作系统将物理内存映射给虚拟地址(这部分的操作会增加耗时)

如果我们设置-xms < -xmx ,那么堆的大小就是不固定的,堆有一个扩大或者压缩的动态过程,不利于稳定gc,因此我们一般使用xms == xmx

默认情况下即使设置了xms == xmx,虚拟地址首次读写会触发缺页异常,增加java 进程的偶发耗时。对于金融系统这种比较注重耗时和稳定性的系统来说,我们可以设置参数-XX:+AlwaysPreTouch ,使得系统在启动的时候就遍历索引所有的虚拟地址,直接提前触发缺页异常把物理内存和虚拟地址映射一遍,这样后面访问虚拟地址的时候就不会再触发缺页异常增加耗时了

代价 就是java进程的启动时间会很慢