内存那点事

作为一个开发人员,我们的程序无时不刻不在跟内存打交道,那你真的理解程序所使用的内存吗?
 
背景
 
前几天,我的知识星球(有兴趣的欢迎加入:https://t.zsxq.com/EUn6IIE)的一个圈友咨询我一个问题:他已经将java启动参数设置为-Xms1g -Xmx1g,启动后,他动过top命令观察,发现其占用的内存远远不到1g。
 
如下这么简单的一个代码:
public class Main {
    public static void main(String[] args)throws Exception {
        System.in.read();//防止程序退出
    }
}    

  

其占用的内存却只有这些(使用top -pid命令查看)
 
这个问题呢,当时也是让我脑袋一愣,难道这是JVM做了什么特殊操作吗?读到这里的你也不放思考一下为什么。
 
虚拟地址空间
 
圈友这个问题引发了我更远的思考,于是不得已将大学毕业还给老师的知识重新拿出来分析。
 
如果你大学里学的东西还没还给老师,你应该还知道,咱们的程序进程,是运行在一个虚拟地址空间里。
 
它的寻址过程如下图:
cpu在读取某个地址时,其地址只是一个虚拟地址,由MMU设备将虚拟地址转换成实际的物理内存地址后,在进行读取操作。
 
你或许会好奇为啥使用虚拟地址,但当你看到如下好处后,你肯定会赞叹其牛逼的设计。
 
1、进程间相互隔离
 
如果没有虚拟地址,每个进程直接对物理内存进行操作,势必会存在各个进程相互影响而无法正常进行。
有了虚拟地址,不同进程的虚拟地址,可以映射到不同得物理地址,相互之间无干扰。
 
2、方便内存共享
 
上一个点我们说到了不同进程的虚拟地址,可以映射到不同的物理地址。其实不同进程的虚拟地址也可以映射到相同的物理地址以实现内存共享。
 
比如每个操作系统的进程,都会需要跟内核程序打交道。有了内存共享,多个进程间就可以共用内核程序,而不需要为每一个进程在物理内存里加载一份内核程序。
 
再比如动态链接库,也是通过共享内存实现物理内存中只加载一份的。
 
3、简化编译时的链接
 
由于进程使用的是虚拟地址,以32位机器为例,每个进程的访问范围都是0~4g的地址空间。当我们在编译源代码时,就可以为程序里的变量、方法分配这个虚拟地址,链接的时候就可以直接用这个虚拟地址实现链接。(如果你不理解什么是链接,你可以简单地理解为:将源代码里的方法调用的地方替换为该方法的内存地址)
 
如果没有虚拟地址,程序里的变量、方法的地址,只能是在程序被加载到内存时才能分配,链接也就无法在编译期进行。
 
如下这是一份Linux下进程所在虚拟地址空间里,不同区域的用途分配图:
 
所有linux下的进程都是这种固定的格式,每个区域都有固定的起始地址。JVM进程,本质上就是一个用c++写的普通进程,其地址空间布局也是这样,只不过它会对比如上边的运行时堆,进行更细的划分。
 
 
至于操作系统和硬件是如何管理虚拟地址空间到物理内存的映射,本篇就不做设计了,感兴趣的朋友可以自行阅读操作系统或者计算机系统的书籍。
 
进程使用内存
 
我们的进程,通过虚拟地址来操作内存。那当我们的进程在申请内存空间时,返回的内存地址自然也是虚拟内存地址。但我们申请的这块基于虚拟内存地址的内存,是否有对应的物理内存的分配呢?
 
在这里我们不妨做个简单地实验。我们写一段C程序,调用malloc申请一个1G的内存,然后使用top命令查看此进程所占用的内存空间:
#include <stdio.h>
#include <sys/malloc.h>
#include "unistd.h"
 
int main(int argc, const char * argv[]) {
 
    printf("pid is %d \n", getpid());
 
    long size = 1024*1024*1024;
    char *p = (char *)malloc(sizeof(char) * size);
 
    getchar();//不让程序退出
    return 0;
}  
编译运行,然后根据打印出的进程id,使用top -pid XXX命令查看内存占用情况。你会发现其内存使用远没有达到1G。换句话说,操作系统并没有马上为我们申请的这个虚拟地址空间分配对应大小的物理内存。
 
何时系统才会给我们的虚拟地址空间分配对应的物理内存呢?
 
我们不妨换个角度理解我们计算机中的物理内存:物理内存是虚拟地址空间内存的高速缓存。
 
在我们使用虚拟地址空间时,如果没有对应的物理内存,就会出现我们常见的缓存不命中的情况。专业术语叫缺页异常。这时内核的缺页异常处理程序,将会帮助我们分配物理内存,如果物理内存不足,它将会选择一个物理内存页作为牺牲,写回磁盘上,这也就是我们所说的交换分区。
 
到这里我们可以看出,我们进程中所使用的内存大小,与真正占用物理内存大小,没有绝对的相等关系。进程申请的内存还没有被使用时,会出现物理内存小于进程内存的情况;进程内存对应的物理内存被写回到交换分区时,也会出现进程内存大于实际物理内存的情况。
 
总结
到这里,Java进程启动时,其占用的内存小于Xms指定的内存大小,就可以说清楚了。它不是JVM的原因,而是操作系统管理进程内存空间的方式上的原因。
 

posted @ 2018-04-23 10:17  清泉^_^  阅读(721)  评论(7编辑  收藏  举报