在 4GB 物理記憶體的機器上,申請 8G 記憶體會怎麼樣?

作者:小林coding

電腦八股文刷題網站://xiaolincoding.com/

大家好,我是小林。

看到讀者在群里討論這些面試題:

其中,第一個問題「在 4GB 物理記憶體的機器上,申請 8G 記憶體會怎麼樣?」存在比較大的爭議,有人說會申請失敗,有的人說可以申請成功。

這個問題在沒有前置條件下,就說出答案就是耍流氓。這個問題要考慮三個前置條件:

  • 作業系統是 32 位的,還是 64 位的?
  • 申請完 8G 記憶體後會不會被使用?
  • 作業系統有沒有使用 Swap 機制?

所以,我們要分場景討論。

作業系統虛擬記憶體大小

應用程式通過 malloc 函數申請記憶體的時候,實際上申請的是虛擬記憶體,此時並不會分配物理記憶體。

當應用程式讀寫了這塊虛擬記憶體,CPU 就會去訪問這個虛擬記憶體, 這時會發現這個虛擬記憶體沒有映射到物理記憶體, CPU 就會產生缺頁中斷,進程會從用戶態切換到內核態,並將缺頁中斷交給內核的 Page Fault Handler (缺頁中斷函數)處理。

缺頁中斷處理函數會看是否有空閑的物理記憶體:

  • 如果有,就直接分配物理記憶體,並建立虛擬記憶體與物理記憶體之間的映射關係。
  • 如果沒有空閑的物理記憶體,那麼內核就會開始進行回收記憶體的工作,如果回收記憶體工作結束後,空閑的物理記憶體仍然無法滿足此次物理記憶體的申請,那麼內核就會放最後的大招了觸發 OOM (Out of Memory)機制。

32 位作業系統和 64 位作業系統的虛擬地址空間大小是不同的,在 Linux 作業系統中,虛擬地址空間的內部又被分為內核空間和用戶空間兩部分,如下所示:

通過這裡可以看出:

  • 32 位系統的內核空間佔用 1G,位於最高處,剩下的 3G 是用戶空間;
  • 64 位系統的內核空間和用戶空間都是 128T,分別佔據整個記憶體空間的最高和最低處,剩下的中間部分是未定義的。

現在可以回答這個問題了:在 32 位作業系統、4GB 物理記憶體的機器上,申請 8GB 記憶體,會怎麼樣?

因為 32 位作業系統,進程最多只能申請 3 GB 大小的虛擬記憶體空間,所以進程申請 8GB 記憶體的話,在申請虛擬記憶體階段就會失敗(我手上沒有 32 位作業系統測試,我估計失敗的原因是 OOM)。

在 64 位作業系統、4GB 物理記憶體的機器上,申請 8G 記憶體,會怎麼樣?

64 位作業系統,進程可以使用 128 TB 大小的虛擬記憶體空間,所以進程申請 8GB 記憶體是沒問題的,因為進程申請記憶體是申請虛擬記憶體,只要不讀寫這個虛擬記憶體,作業系統就不會分配物理記憶體。

我們可以簡單做個測試,我的伺服器是 64 位作業系統,但是物理記憶體只有 2 GB:

現在,我在機器上,連續申請 4 次 1 GB 記憶體,也就是一共申請了 4 GB 記憶體,注意下面程式碼只是單純分配了虛擬記憶體,並沒有使用該虛擬記憶體:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define MEM_SIZE 1024 * 1024 * 1024

int main() {
    char* addr[4];
    int i = 0;
    for(i = 0; i < 4; ++i) {
        addr[i] = (char*) malloc(MEM_SIZE);
        if(!addr[i]) {
            printf("執行 malloc 失敗, 錯誤:%s\n",strerror(errno));
          return -1;
        }
        printf("主執行緒調用malloc後,申請1gb大小得記憶體,此記憶體起始地址:0X%x\n", addr[i]);
    }
    
    //輸入任意字元後,才結束
    getchar();
    return 0;
}

然後運行這個程式碼,可以看到,我的物理記憶體雖然只有 2GB,但是程式正常分配了 4GB 大小的虛擬記憶體:

我們可以通過下面這條命令查看進程(test)的虛擬記憶體大小:

# ps aux | grep test
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      7797  0.0  0.0 4198540  352 pts/1    S+   16:58   0:00 ./test

其中,VSZ 就代表進程使用的虛擬記憶體大小,RSS 代表進程使用的物理記憶體大小。可以看到,VSZ 大小為 4198540,也就是 4GB 的虛擬記憶體。

Swap 機制的作用

前面討論在 32 位/64 位作業系統環境下,申請的虛擬記憶體超過物理記憶體後會怎麼樣?

  • 在 32 位作業系統,因為進程最大只能申請 3 GB 大小的虛擬記憶體,所以直接申請 8G 記憶體,會申請失敗。
  • 在 64 位作業系統,因為進程最大只能申請 128 TB 大小的虛擬記憶體,即使物理記憶體只有 4GB,申請 8G 記憶體也是沒問題,因為申請的記憶體是虛擬記憶體。

程式申請的虛擬記憶體,如果沒有被使用,它是不會佔用物理空間的。當訪問這塊虛擬記憶體後,作業系統才會進行物理記憶體分配。

如果申請物理記憶體大小超過了空閑物理記憶體大小,就要看作業系統有沒有開啟 Swap 機制:

  • 如果沒有開啟 Swap 機制,程式就會直接 OOM;
  • 如果有開啟 Swap 機制,程式可以正常運行。

什麼是 Swap 機制?

當系統的物理記憶體不夠用的時候,就需要將物理記憶體中的一部分空間釋放出來,以供當前運行的程式使用。那些被釋放的空間可能來自一些很長時間沒有什麼操作的程式,這些被釋放的空間會被臨時保存到磁碟,等到那些程式要運行時,再從磁碟中恢復保存的數據到記憶體中。

另外,當記憶體使用存在壓力的時候,會開始觸發記憶體回收行為,會把這些不常訪問的記憶體先寫到磁碟中,然後釋放這些記憶體,給其他更需要的進程使用。再次訪問這些記憶體時,重新從磁碟讀入記憶體就可以了。

這種,將記憶體數據換出磁碟,又從磁碟中恢複數據到記憶體的過程,就是 Swap 機制負責的。

Swap 就是把一塊磁碟空間或者本地文件,當成記憶體來使用,它包含換出和換入兩個過程:

  • 換出(Swap Out) ,是把進程暫時不用的記憶體數據存儲到磁碟中,並釋放這些數據佔用的記憶體;
  • 換入(Swap In),是在進程再次訪問這些記憶體的時候,把它們從磁碟讀到記憶體中來;

Swap 換入換出的過程如下圖:

使用 Swap 機制優點是,應用程式實際可以使用的記憶體空間將遠遠超過系統的物理記憶體。由於硬碟空間的價格遠比記憶體要低,因此這種方式無疑是經濟實惠的。當然,頻繁地讀寫硬碟,會顯著降低作業系統的運行速率,這也是 Swap 的弊端。

Linux 中的 Swap 機制會在記憶體不足和記憶體閑置的場景下觸發:

  • 記憶體不足:當系統需要的記憶體超過了可用的物理記憶體時,內核會將記憶體中不常使用的記憶體頁交換到磁碟上為當前進程讓出記憶體,保證正在執行的進程的可用性,這個記憶體回收的過程是強制的直接記憶體回收(Direct Page Reclaim)。直接記憶體回收是同步的過程,會阻塞當前申請記憶體的進程。
  • 記憶體閑置:應用程式在啟動階段使用的大量記憶體在啟動後往往都不會使用,通過後台運行的守護進程(kSwapd),我們可以將這部分只使用一次的記憶體交換到磁碟上為其他記憶體的申請預留空間。kSwapd 是 Linux 負責頁面置換(Page replacement)的守護進程,它也是負責交換閑置記憶體的主要進程,它會在空閑記憶體低於一定水位時,回收記憶體頁中的空閑記憶體保證系統中的其他進程可以儘快獲得申請的記憶體。kSwapd 是後台進程,所以回收記憶體的過程是非同步的,不會阻塞當前申請記憶體的進程。

Linux 提供了兩種不同的方法啟用 Swap,分別是 Swap 分區(Swap Partition)和 Swap 文件(Swapfile),開啟方法可以看這個資料:

  • Swap 分區是硬碟上的獨立區域,該區域只會用於交換分區,其他的文件不能存儲在該區域上,我們可以使用 Swapon -s 命令查看當前系統上的交換分區;
  • Swap 文件是文件系統中的特殊文件,它與文件系統中的其他文件也沒有太多的區別;

Swap 換入換出的是什麼類型的記憶體?

內核快取的文件數據,因為都有對應的磁碟文件,所以在回收文件數據的時候, 直接寫回到對應的文件就可以了。

但是像進程的堆、棧數據等,它們是沒有實際載體,這部分記憶體被稱為匿名頁。而且這部分記憶體很可能還要再次被訪問,所以不能直接釋放記憶體,於是就需要有一個能保存匿名頁的磁碟載體,這個載體就是 Swap 分區。

匿名頁回收的方式是通過 Linux 的 Swap 機制,Swap 會把不常訪問的記憶體先寫到磁碟中,然後釋放這些記憶體,給其他更需要的進程使用。再次訪問這些記憶體時,重新從磁碟讀入記憶體就可以了。

接下來,通過兩個實驗,看看申請的物理記憶體超過物理記憶體會怎樣?

  • 實驗一:沒有開啟 Swap 機制
  • 實驗二:有開啟 Swap 機制

實驗一:沒有開啟 Swap 機制

我的伺服器是 64 位作業系統,但是物理記憶體只有 2 GB,而且沒有 Swap 分區:

我們改一下前面的程式碼,使得在申請完 4GB 虛擬記憶體後,通過 memset 函數訪問這個虛擬記憶體,看看在沒有 Swap 分區的情況下,會發生什麼?

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define MEM_SIZE 1024 * 1024 * 1024

int main() {
    char* addr[4];
    int i = 0;
    for(i = 0; i < 4; ++i) {
        addr[i] = (char*) malloc(MEM_SIZE);
        if(!addr[i]) {
            printf("執行 malloc 失敗, 錯誤:%s\n",strerror(errno));
            return -1;
        }
        printf("主執行緒調用malloc後,申請1gb大小得記憶體,此記憶體起始地址:0X%x\n", addr[i]);
    }

    for(i = 0; i < 4; ++i) {
        printf("開始訪問第 %d 塊虛擬記憶體(每一塊虛擬記憶體為 1 GB)\n", i + 1);
        memset(addr[i], 0, MEM_SIZE);
    }
    
    //輸入任意字元後,才結束
    getchar();
    return 0;
}

運行結果:

可以看到,在訪問第 2 塊虛擬記憶體(每一塊虛擬記憶體是 1 GB)的時候,因為超過了機器的物理記憶體(2GB),進程(test)被作業系統殺掉了。

通過查看 message 系統日誌,可以發現該進程是被作業系統 OOM killer 機制殺掉了,日誌里報錯了 Out of memory,也就是發生 OOM(記憶體溢出錯誤)。

什麼是 OOM?

記憶體溢出(Out Of Memory,簡稱OOM)是指應用系統中存在無法回收的記憶體或使用的記憶體過多,最終使得程式運行要用到的記憶體大於能提供的最大記憶體。此時程式就運行不了,系統會提示記憶體溢出。

實驗二:有開啟 Swap 機制

我用我的 mac book pro 筆記型電腦做測試,我的筆記型電腦是 64 位作業系統,物理記憶體是 8 GB, 目前 Swap 分區大小為 1 GB(注意這個大小不是固定不變的,Swap 分區總大小是會動態變化的,當沒有使用 Swap 分區時,Swap 分區總大小是 0;當使用了 Swap 分區,Swap 分區總大小會增加至 1 GB;當 Swap 分區已使用的大小超過 1 GB 時;Swap 分區總大小就會增加到至 2 GB;當 Swap 分區已使用的大小超過 2 GB 時;Swap 分區總大小就增加至 3GB,如此往複。這個估計是 macos 自己實現的,Linux 的分區則是固定大小的,Swap 分區不會根據使用情況而自動增長)。

為了方便觀察磁碟 I/O 情況,我們改進一下前面的程式碼,分配完 32 GB虛擬記憶體後(筆記型電腦物理記憶體是 8 GB),通過一個 while 循環頻繁訪問虛擬記憶體,程式碼如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MEM_SIZE 32 * 1024 * 1024 * 1024

int main() {
    char* addr = (char*) malloc((long)MEM_SIZE);
    printf("主執行緒調用malloc後,目前共申請了 32gb 的虛擬記憶體\n");
    
    //循環頻繁訪問虛擬記憶體
    while(1) {
          printf("開始訪問 32gb 大小的虛擬記憶體...\n");
          memset(addr, 0, (long)MEM_SIZE);
    }
    return 0;
}

運行結果如下:

可以看到,在有 Swap 分區的情況下,即使筆記型電腦物理記憶體是 8 GB,申請並使用 32 GB 記憶體是沒問題,程式正常運行了,並沒有發生 OOM。

從下圖可以看到,進程的記憶體顯示 32 GB(這個不要理解為佔用的物理記憶體,理解為已被訪問的虛擬記憶體大小,也就是在物理記憶體呆過的記憶體大小),系統已使用的 Swap 分區達到 2.3 GB。

此時我的筆記型電腦電腦的磁碟開始出現「沙沙」的聲音,通過查看磁碟的 I/O 情況,可以看到磁碟 I/O 達到了一個峰值,非常高:

有了 Swap 分區,是不是意味著進程可以使用的記憶體是無上限的?

當然不是,我把上面的程式碼改成了申請 64GB 記憶體後,當進程申請完 64GB 虛擬記憶體後,使用到 56 GB (這個不要理解為佔用的物理記憶體,理解為已被訪問的虛擬記憶體大小,也就是在物理記憶體呆過的記憶體大小)的時候,進程就被系統 kill 掉了,如下圖:

當系統多次嘗試回收記憶體,還是無法滿足所需使用的記憶體大小,進程就會被系統 kill 掉了,意味著發生了 OOM (PS:我沒有在 macos 系統找到像 linux 系統里的 /var/log/message 系統日誌文件,所以無法通過查看日誌確認是否發生了 OOM)。

總結

至此, 驗證完成了。簡單總結下:

  • 在 32 位作業系統,因為進程最大只能申請 3 GB 大小的虛擬記憶體,所以直接申請 8G 記憶體,會申請失敗。
  • 在 64位 位作業系統,因為進程最大只能申請 128 TB 大小的虛擬記憶體,即使物理記憶體只有 4GB,申請 8G 記憶體也是沒問題,因為申請的記憶體是虛擬記憶體。如果這塊虛擬記憶體被訪問了,要看系統有沒有 Swap 分區:
    • 如果沒有 Swap 分區,因為物理空間不夠,進程會被作業系統殺掉,原因是 OOM(記憶體溢出);
    • 如果有 Swap 分區,即使物理記憶體只有 4GB,程式也能正常使用 8GB 的記憶體,進程可以正常運行;

系列記憶體管理文章