徹底理解記憶體洩漏,你學會了嗎?

2024.02.02

大家好,我是小風哥,今天跟大家聊一聊記憶體洩漏這個話題。

在這些文章講到內存申請時我很喜歡用停車場來做類比,內存申請就好比去停車場找停車位,找到停車位後你就可以把車停在這裡。

從這個類比看什麼是記憶體洩漏呢?內存洩漏看上去是停車場的車輛只進不出導致最終找不到停車位,從程式設計師的角度看就是內存只申請取不釋放,如果你去問,可能有不少人認為內存洩漏就是這麼回事。

然而這其實是不全面的。

申請過多內存

首先內存只申請不釋放未必就是內存洩漏,有可能是你的程序的確需要申請很多內存,這是正常的,然而如果是bug導致申請了很多內存,這就是內存洩漏了,或者也有人將其稱為space leak,意思是申請的內存超過了正常所需;不管是有意無意,總之在這種情況下你依然保持對這些內存的引用,因此你總可以找到這些內存並刪除它們,就看你刪不刪。

有很多情況會導致這個問題,像是重複使用的某個結構體/對象,當再次復用時沒有清理上一次使用遺留的資料、系統中存在cache,但cache的過期策略設定不得當等等。

記憶體無法刪除

另一類比較有趣的內存洩漏是說你申請了一些內存,但最終卻沒有什麼指向它們:

void memory_leak() {
  char* mem = (char*)malloc(1024);
  // just return
}
  • 1.
  • 2.
  • 3.
  • 4.

在這段程式碼中我們申請了1k內存,然而當memory_leak函數返回後你就再也不知道這段內存到底在哪裡了!

用停車場的範例來說就是有些駕駛太過土豪,家裡的車太多以至於把將車放在停車場這件事忘掉了,導致這些車根本就不會有人再開走,因此白白浪費停車位,並導致可用車位越來越少,而對於編程來說就是粗心大意的程式設計師申請了一些內存後最終「忘掉」了,再也不會有什麼東西(變數/指針)指向這些內存,因此在這種情況下你沒有辦法再找到這些記憶體並將其刪除。

記憶體碎片

這也算的上是一類特殊的內存洩漏,用停車場的例子來說就是兩個停車位中間停靠了一輛小型老年代步車,導致儘管這兩個停車位剩餘的空間足夠大但又恰好都沒有辦法再停靠一輛小汽車。

假定我們系統中寶貴的記憶體大小只有8字節,其中有兩個位元組已經分配出去了,就像這樣:

圖片圖片

現在,系統中空閒的記憶體是6位元組,下一次的記憶體申請需要分配5位元組,糟糕,我們已經沒有辦法再找到連續的5個位元組大小的記憶體空間了,儘管全部空間的記憶體還有6字節,這就是所謂的記憶體碎片問題。

而對於記憶體分配器來說如果發生這種情況那麼將不得不借助作業系統的幫助來擴大堆區,因此看起來我們的程式佔據的記憶體越來越多,儘管實際上程式可能並不需要那麼多內存,只是因為內存碎片的原因導致一部分內存無法被再次被利用起來。

然而對於現代作業系統尤其具備虛擬記憶體能力的系統來說,記憶體碎片問題通常可能並不會和我們想像的那樣嚴重,原因就在於分配的記憶體只需要在虛擬位址空間上連續而不必在實體記憶體上也連續,假定我們在虛擬記憶體位址空間需要存放「aabbccdd」這樣的字串,在虛擬位址空間上看這是連續的就像這樣:

圖片圖片

但在實體記憶體上可能是這樣存放的:

圖片圖片

可以看到,利用虛擬記憶體我們可以更加充分靈活的利用「邊邊角角」的實體內存,從而減少內存碎片帶來的影響。

關於虛擬記憶體更詳細的講解你可以參考《深入理解作業系統》虛擬記憶體一章,關於公眾號「碼農的荒島求生」並回覆「作業系統」即可。

如果你的程式需要重複申請很多物件/資料/結構體,並在最後一次性全部釋放,那麼記憶體池是一個避免記憶體碎片不錯的選擇,原理在於儘管從記憶體池的角度看會有碎片,但當我們以記憶體池大小為單位從堆區申請釋放記憶體時,這種碎片將不復存在。

內存洩漏帶來的問題

在現代作業系統中除非你的程式運行時間足夠長或者申請的內存足夠快足夠多否則內存洩漏可能並不是什麼大問題,你甚至可能都察覺不出來有內存洩漏,因為當進程運行結束後其佔據的記憶體會被作業系統收回,在這種情況下你可能不必過於關心這個問題,但對於長時間運行的伺服器端程式、資料庫程式、作業系統等,記憶體洩漏就屬於比較嚴重的問題了,因為這些程式必須時刻在線,任何微小的記憶體洩漏在時間的加持下都會非常明顯。

內存持續洩漏會發生什麼?

你的系統會慢到炸是有可能的。

記憶體的申請速度會對系統效能產生很大的影響,當系統記憶體不足時,記憶體分配器找到一塊滿足要求的空閒記憶體區塊將更加困難耗時更多,當程式消耗的記憶體超過實體記憶體大小時虛擬記憶體系統(如果有的話)開始發揮作用,將進程位址空間中不常用的一部分swap出去,此時系統效能將快速下降,表現出來的就是程式設計師運作變慢、卡頓。

當然,根據系統配置,像Linux系統,可能會將消耗記憶體很多的進程kill掉,這就是Out of Memory killer,簡稱oom killer。

記憶體洩漏檢測工具

記憶體洩漏問題通常比較難直接排查,尤其對於C/C++程式來說,這時我們將不得不借助必要的工具。

有一些專門的工具可以幫助你偵測記憶體洩漏,例如Valgrind、AddressSanitizer和MemorySanitizer。這些工具可以在運行時對程式進行檢查,識別出記憶體洩漏和其他記憶體錯誤。

此外針對特定的記憶體分配器,像jemalloc之類,這些記憶體分配器自帶記憶體偵測工具heap profile,能夠有效分析進程記憶體分配到了地方,並細化到函數級別,非常方便。