天天看點

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

作者:小林coding

計算機八股文刷題網站:https://xiaolincoding.com/

大家好,我是小林。

看到讀者在群裡讨論這些面試題:

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

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

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

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

是以,我們要分場景讨論。

作業系統虛拟記憶體大小

應用程式通過 malloc 函數申請記憶體的時候,實際上申請的是虛拟記憶體,此時并不會配置設定實體記憶體。

當應用程式讀寫了這塊虛拟記憶體,CPU 就會去通路這個虛拟記憶體, 這時會發現這個虛拟記憶體沒有映射到實體記憶體, CPU 就會産生缺頁中斷,程序會從使用者态切換到核心态,并将缺頁中斷交給核心的 Page Fault Handler (缺頁中斷函數)處理。

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

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

32 位作業系統和 64 位作業系統的虛拟位址空間大小是不同的,在 Linux 作業系統中,虛拟位址空間的内部又被分為核心空間和使用者空間兩部分,如下所示:

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

通過這裡可以看出:

  • 32

    位系統的核心空間占用

    1G

    ,位于最高處,剩下的

    3G

    是使用者空間;
  • 64

    位系統的核心空間和使用者空間都是

    128T

    ,分别占據整個記憶體空間的最高和最低處,剩下的中間部分是未定義的。
現在可以回答這個問題了:在 32 位作業系統、4GB 實體記憶體的機器上,申請 8GB 記憶體,會怎麼樣?

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

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

64 位作業系統,程序可以使用 128 TB 大小的虛拟記憶體空間,是以程序申請 8GB 記憶體是沒問題的,因為程序申請記憶體是申請虛拟記憶體,隻要不讀寫這個虛拟記憶體,作業系統就不會配置設定實體記憶體。

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

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

現在,我在機器上,連續申請 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 大小的虛拟記憶體:

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

我們可以通過下面這條指令檢視程序(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 換入換出的過程如下圖:

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

使用 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 實體記憶體的機器上,申請 8G 記憶體會怎麼樣?

我們改一下前面的代碼,使得在申請完 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;
}
           

運作結果:

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

可以看到,在通路第 2 塊虛拟記憶體(每一塊虛拟記憶體是 1 GB)的時候,因為超過了機器的實體記憶體(2GB),程序(test)被作業系統殺掉了。

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

在 4GB 實體記憶體的機器上,申請 8G 記憶體會怎麼樣?
什麼是 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 分區不會根據使用情況而自動增長)。

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

為了友善觀察磁盤 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;
}
           

運作結果如下:

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

可以看到,在有 Swap 分區的情況下,即使筆記本實體記憶體是 8 GB,申請并使用 32 GB 記憶體是沒問題,程式正常運作了,并沒有發生 OOM。

從下圖可以看到,程序的記憶體顯示 32 GB(這個不要了解為占用的實體記憶體,了解為已被通路的虛拟記憶體大小,也就是在實體記憶體呆過的記憶體大小),系統已使用的 Swap 分區達到 2.3 GB。

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

此時我的筆記本電腦的磁盤開始出現“沙沙”的聲音,通過檢視磁盤的 I/O 情況,可以看到磁盤 I/O 達到了一個峰值,非常高:

在 4GB 實體記憶體的機器上,申請 8G 記憶體會怎麼樣?
有了 Swap 分區,是不是意味着程序可以使用的記憶體是無上限的?

當然不是,我把上面的代碼改成了申請 64GB 記憶體後,當程序申請完 64GB 虛拟記憶體後,使用到 56 GB (這個不要了解為占用的實體記憶體,了解為已被通路的虛拟記憶體大小,也就是在實體記憶體呆過的記憶體大小)的時候,程序就被系統 kill 掉了,如下圖:

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

當系統多次嘗試回收記憶體,還是無法滿足所需使用的記憶體大小,程序就會被系統 kill 掉了,意味着發生了 OOM (PS:我沒有在 macos 系統找到像 linux 系統裡的 /var/log/message 系統日志檔案,是以無法通過檢視日志确認是否發生了 OOM)。

總結

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

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

系列記憶體管理文章

  • 為什麼要有虛拟記憶體?
  • malloc是如何配置設定記憶體的?
  • 記憶體滿了,會發生什麼?
  • 在 4GB 實體記憶體的機器上,申請 8G 記憶體會怎麼樣?

微信搜尋公衆号:「小林coding」 ,回複「圖解」即可免費獲得「圖解網絡、圖解系統、圖解MySQL、圖解Redis」PDF 電子書