作為一個開發人員,我們的程序無時不刻不在跟內存打交道,那你真的理解程序所使用的內存嗎?
背景
前幾天,我的知識星球(有興趣的歡迎加入:
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的原因,而是操作系統管理進程內存空間的方式上的原因。