內存那點事

背景

前幾天,我的知識星球(有興趣的歡迎加入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命令查看)

image

這個問題呢,當時也是讓我腦袋一愣,難道這是JVM做了什麼特殊操作嗎?讀到這裏的你也不放思考一下爲什麼。

虛擬地址空間

圈友這個問題引發了我更遠的思考,於是不得已將大學畢業還給老師的知識重新拿出來分析。

如果你大學裏學的東西還沒還給老師,你應該還知道,咱們的程序進程,是運行在一個虛擬地址空間裏。

它的尋址過程如下圖:

image

cpu在讀取某個地址時,其地址只是一個虛擬地址,由MMU設備將虛擬地址轉換成實際的物理內存地址後,在進行讀取操作。

你或許會好奇爲啥使用虛擬地址,但當你看到如下好處後,你肯定會讚歎其牛逼的設計。

1、進程間相互隔離

如果沒有虛擬地址,每個進程直接對物理內存進行操作,勢必會存在各個進程相互影響而無法正常進行。

有了虛擬地址,不同進程的虛擬地址,可以映射到不同得物理地址,相互之間無干擾。

2、方便內存共享

上一個點我們說到了不同進程的虛擬地址,可以映射到不同的物理地址。其實不同進程的虛擬地址也可以映射到相同的物理地址以實現內存共享。

比如每個操作系統的進程,都會需要跟內核程序打交道。有了內存共享,多個進程間就可以共用內核程序,而不需要爲每一個進程在物理內存里加載一份內核程序。

再比如動態鏈接庫,也是通過共享內存實現物理內存中只加載一份的。

3、簡化編譯時的鏈接

由於進程使用的是虛擬地址,以32位機器爲例,每個進程的訪問範圍都是0~4g的地址空間。當我們在編譯源代碼時,就可以爲程序裏的變量、方法分配這個虛擬地址,鏈接的時候就可以直接用這個虛擬地址實現鏈接。(如果你不理解什麼是鏈接,你可以簡單地理解爲:將源代碼裏的方法調用的地方替換爲該方法的內存地址)

如果沒有虛擬地址,程序裏的變量、方法的地址,只能是在程序被加載到內存時才能分配,鏈接也就無法在編譯期進行。

如下這是一份Linux下進程所在虛擬地址空間裏,不同區域的用途分配圖:

image

所有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的原因,而是操作系統管理進程內存空間的方式上的原因。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章