背景
前幾天,我的知識(shí)星球(有興趣的歡迎加入https://t.zsxq.com/EUn6IIE)的一個(gè)圈友咨詢我一個(gè)問題:他已經(jīng)將java啟動(dòng)參數(shù)設(shè)置為-Xms1g -Xmx1g,啟動(dòng)后,他動(dòng)過top命令觀察,發(fā)現(xiàn)其占用的內(nèi)存遠(yuǎn)遠(yuǎn)不到1g。
如下這么簡(jiǎn)單的一個(gè)代碼:
public class Main {
public static void main(String[] args)throws Exception {
System.in.read();//防止程序退出
}
}
其占用的內(nèi)存卻只有這些(使用top -pid命令查看)
這個(gè)問題呢,當(dāng)時(shí)也是讓我腦袋一愣,難道這是JVM做了什么特殊操作嗎?讀到這里的你也不放思考一下為什么。
虛擬地址空間
圈友這個(gè)問題引發(fā)了我更遠(yuǎn)的思考,于是不得已將大學(xué)畢業(yè)還給老師的知識(shí)重新拿出來分析。
如果你大學(xué)里學(xué)的東西還沒還給老師,你應(yīng)該還知道,咱們的程序進(jìn)程,是運(yùn)行在一個(gè)虛擬地址空間里。
它的尋址過程如下圖:
cpu在讀取某個(gè)地址時(shí),其地址只是一個(gè)虛擬地址,由MMU設(shè)備將虛擬地址轉(zhuǎn)換成實(shí)際的物理內(nèi)存地址后,在進(jìn)行讀取操作。
你或許會(huì)好奇為啥使用虛擬地址,但當(dāng)你看到如下好處后,你肯定會(huì)贊嘆其牛逼的設(shè)計(jì)。
1、進(jìn)程間相互隔離
如果沒有虛擬地址,每個(gè)進(jìn)程直接對(duì)物理內(nèi)存進(jìn)行操作,勢(shì)必會(huì)存在各個(gè)進(jìn)程相互影響而無法正常進(jìn)行。
有了虛擬地址,不同進(jìn)程的虛擬地址,可以映射到不同得物理地址,相互之間無干擾。
2、方便內(nèi)存共享
上一個(gè)點(diǎn)我們說到了不同進(jìn)程的虛擬地址,可以映射到不同的物理地址。其實(shí)不同進(jìn)程的虛擬地址也可以映射到相同的物理地址以實(shí)現(xiàn)內(nèi)存共享。
比如每個(gè)操作系統(tǒng)的進(jìn)程,都會(huì)需要跟內(nèi)核程序打交道。有了內(nèi)存共享,多個(gè)進(jìn)程間就可以共用內(nèi)核程序,而不需要為每一個(gè)進(jìn)程在物理內(nèi)存里加載一份內(nèi)核程序。
再比如動(dòng)態(tài)鏈接庫,也是通過共享內(nèi)存實(shí)現(xiàn)物理內(nèi)存中只加載一份的。
3、簡(jiǎn)化編譯時(shí)的鏈接
由于進(jìn)程使用的是虛擬地址,以32位機(jī)器為例,每個(gè)進(jìn)程的訪問范圍都是0~4g的地址空間。當(dāng)我們?cè)诰幾g源代碼時(shí),就可以為程序里的變量、方法分配這個(gè)虛擬地址,鏈接的時(shí)候就可以直接用這個(gè)虛擬地址實(shí)現(xiàn)鏈接。(如果你不理解什么是鏈接,你可以簡(jiǎn)單地理解為:將源代碼里的方法調(diào)用的地方替換為該方法的內(nèi)存地址)
如果沒有虛擬地址,程序里的變量、方法的地址,只能是在程序被加載到內(nèi)存時(shí)才能分配,鏈接也就無法在編譯期進(jìn)行。
如下這是一份Linux下進(jìn)程所在虛擬地址空間里,不同區(qū)域的用途分配圖:
所有l(wèi)inux下的進(jìn)程都是這種固定的格式,每個(gè)區(qū)域都有固定的起始地址。JVM進(jìn)程,本質(zhì)上就是一個(gè)用c++寫的普通進(jìn)程,其地址空間布局也是這樣,只不過它會(huì)對(duì)比如上邊的運(yùn)行時(shí)堆,進(jìn)行更細(xì)的劃分。
至于操作系統(tǒng)和硬件是如何管理虛擬地址空間到物理內(nèi)存的映射,本篇就不做設(shè)計(jì)了,感興趣的朋友可以自行閱讀操作系統(tǒng)或者計(jì)算機(jī)系統(tǒng)的書籍。
進(jìn)程使用內(nèi)存
我們的進(jìn)程,通過虛擬地址來操作內(nèi)存。那當(dāng)我們的進(jìn)程在申請(qǐng)內(nèi)存空間時(shí),返回的內(nèi)存地址自然也是虛擬內(nèi)存地址。但我們申請(qǐng)的這塊基于虛擬內(nèi)存地址的內(nèi)存,是否有對(duì)應(yīng)的物理內(nèi)存的分配呢?
在這里我們不妨做個(gè)簡(jiǎn)單地實(shí)驗(yàn)。我們寫一段C程序,調(diào)用malloc申請(qǐng)一個(gè)1G的內(nèi)存,然后使用top命令查看此進(jìn)程所占用的內(nèi)存空間:
#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;
}
編譯運(yùn)行,然后根據(jù)打印出的進(jìn)程id,使用top -pid XXX命令查看內(nèi)存占用情況。你會(huì)發(fā)現(xiàn)其內(nèi)存使用遠(yuǎn)沒有達(dá)到1G。換句話說,操作系統(tǒng)并沒有馬上為我們申請(qǐng)的這個(gè)虛擬地址空間分配對(duì)應(yīng)大小的物理內(nèi)存。
何時(shí)系統(tǒng)才會(huì)給我們的虛擬地址空間分配對(duì)應(yīng)的物理內(nèi)存呢?
我們不妨換個(gè)角度理解我們計(jì)算機(jī)中的物理內(nèi)存:物理內(nèi)存是虛擬地址空間內(nèi)存的高速緩存。
在我們使用虛擬地址空間時(shí),如果沒有對(duì)應(yīng)的物理內(nèi)存,就會(huì)出現(xiàn)我們常見的緩存不命中的情況。專業(yè)術(shù)語叫缺頁異常。這時(shí)內(nèi)核的缺頁異常處理程序,將會(huì)幫助我們分配物理內(nèi)存,如果物理內(nèi)存不足,它將會(huì)選擇一個(gè)物理內(nèi)存頁作為犧牲,寫回磁盤上,這也就是我們所說的交換分區(qū)。
到這里我們可以看出,我們進(jìn)程中所使用的內(nèi)存大小,與真正占用物理內(nèi)存大小,沒有絕對(duì)的相等關(guān)系。進(jìn)程申請(qǐng)的內(nèi)存還沒有被使用時(shí),會(huì)出現(xiàn)物理內(nèi)存小于進(jìn)程內(nèi)存的情況;進(jìn)程內(nèi)存對(duì)應(yīng)的物理內(nèi)存被寫回到交換分區(qū)時(shí),也會(huì)出現(xiàn)進(jìn)程內(nèi)存大于實(shí)際物理內(nèi)存的情況。
總結(jié)
到這里,Java進(jìn)程啟動(dòng)時(shí),其占用的內(nèi)存小于Xms指定的內(nèi)存大小,就可以說清楚了。它不是JVM的原因,而是操作系統(tǒng)管理進(jìn)程內(nèi)存空間的方式上的原因。