作者:小林coding
計算機八股文刷題網站:https://xiaolincoding.com/
大家好,我是小林。
看到讀者在群裡討論這些面試題:
其中,第一個問題「在 4GB 實體記憶體的機器上,申請 8G 記憶體會怎麼樣?」存在比較大的爭議,有人說會申請失敗,有的人說可以申請成功。
這個問題在沒有前置條件下,就說出答案就是耍流氓。這個問題要考慮三個前置條件:
所以,我們要分場景討論。
應用程式通過 malloc 函數申請記憶體的時候,實際上申請的是虛擬記憶體,此時並不會分配實體記憶體。
當應用程式讀寫了這塊虛擬記憶體,CPU 就會去存取這個虛擬記憶體, 這時會發現這個虛擬記憶體沒有對映到實體記憶體, CPU 就會產生缺頁中斷,程序會從使用者態切換到核心態,並將缺頁中斷交給核心的 Page Fault Handler (缺頁中斷函數)處理。
缺頁中斷處理常式會看是否有空閒的實體記憶體:
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 的虛擬記憶體。
前面討論在 32 位/64 位元運算系統環境下,申請的虛擬記憶體超過實體記憶體後會怎麼樣?
程式申請的虛擬記憶體,如果沒有被使用,它是不會佔用物理空間的。當存取這塊虛擬記憶體後,作業系統才會進行實體記憶體分配。
如果申請實體記憶體大小超過了空閒實體記憶體大小,就要看作業系統有沒有開啟 Swap 機制:
什麼是 Swap 機制?
當系統的實體記憶體不夠用的時候,就需要將實體記憶體中的一部分空間釋放出來,以供當前執行的程式使用。那些被釋放的空間可能來自一些很長時間沒有什麼操作的程式,這些被釋放的空間會被臨時儲存到磁碟,等到那些程式要執行時,再從磁碟中恢復儲存的資料到記憶體中。
另外,當記憶體使用存在壓力的時候,會開始觸發記憶體回收行為,會把這些不常存取的記憶體先寫到磁碟中,然後釋放這些記憶體,給其他更需要的程序使用。再次存取這些記憶體時,重新從磁碟讀入記憶體就可以了。
這種,將記憶體資料換出磁碟,又從磁碟中恢復資料到記憶體的過程,就是 Swap 機制負責的。
Swap 就是把一塊磁碟空間或者本地檔案,當成記憶體來使用,它包含換出和換入兩個過程:
Swap 換入換出的過程如下圖:
使用 Swap 機制優點是,應用程式實際可以使用的記憶體空間將遠遠超過系統的實體記憶體。由於硬碟空間的價格遠比記憶體要低,因此這種方式無疑是經濟實惠的。當然,頻繁地讀寫硬碟,會顯著降低作業系統的執行速率,這也是 Swap 的弊端。
Linux 中的 Swap 機制會在記憶體不足和記憶體閒置的場景下觸發:
Linux 提供了兩種不同的方法啟用 Swap,分別是 Swap 分割區(Swap Partition)和 Swap 檔案(Swapfile),開啟方法可以看這個資料:
Swapon -s
命令檢視當前系統上的交換分割區;Swap 換入換出的是什麼型別的記憶體?
核心快取的檔案資料,因為都有對應的磁碟檔案,所以在回收檔案資料的時候, 直接寫回到對應的檔案就可以了。
但是像程序的堆、棧資料等,它們是沒有實際載體,這部分記憶體被稱為匿名頁。而且這部分記憶體很可能還要再次被存取,所以不能直接釋放記憶體,於是就需要有一個能儲存匿名頁的磁碟載體,這個載體就是 Swap 分割區。
匿名頁回收的方式是通過 Linux 的 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)是指應用系統中存在無法回收的記憶體或使用的記憶體過多,最終使得程式執行要用到的記憶體大於能提供的最大記憶體。此時程式就執行不了,系統會提示記憶體溢位。
我用我的 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)。
至此, 驗證完成了。簡單總結下: