free not return memory
個人博客:https://rebootcat.com/2020/11/05/free_mem/
內存泄露?
觀察到一臺機器上的內存使用量在程序啟動之后,持續增長,中間沒有出現內存恢復。懷疑是不是出現了內存泄露的問題?
然后使用相關的內存分析工具進行了分析:
- gperf
- valgrind (massif)
- 手工標記內存分配釋放
上述的分析結果均不能很肯定的得出是否內存泄露的結論。那么問題可能出現在哪里呢?
程序采用 c++ 編寫,大量使用了智能指針以及 new/delete,難道內存沒有成功釋放?亦或是內存釋放有什么條件?于是開始懷疑 free 是不是真的釋放了內存?
測試
既然懷疑 free 是不是真的釋放了內存,此處的釋放,是指程序內存占用下降,內存歸還給操作系統,那么直接寫一個簡單的例子進行驗證一下。
attention:
測試前,先關閉 swap:
# swapoff -a# free -htotal used free shared buff/cache available
Mem: 3.7G 2.5G 1.1G 8.8M 40M 959M
Swap: 0B 0B 0B
測試1
步驟如下:
- 循環分配大量內存
- block 程序,top 工具觀察進程內存占用情況
- 再循環釋放所有分配的內存
- block 程序,top 工具觀察進程內存占用情況
- 程序退出
上代碼:
#include <string>
#include <cstring>
#include <iostream>
#include <vector>
#include <malloc.h>void test(uint32_t num, uint32_t mem_size) {std::cout << "test: mem_size = " << mem_size << " total:" << num * mem_size / 1024.0 / 1024.0 << " MB" << std::endl;std::vector<char*> vec;// 3Gfor (uint32_t i =0; i < num; ++i) {char *ptr = new char[mem_size];vec.push_back(ptr);}std::cout << "allocate memory "<< num * mem_size / 1024.0 / 1024.0 << " MB done" << std::endl;for (auto& ptr : vec) {strncpy(ptr, "abcdefghij", mem_size);}std::cout << "input anything to continue delete all memory..." << std::endl;getchar();for (auto& ptr : vec) {delete ptr;ptr = nullptr;}std::cout << "release memory "<< num * mem_size / 1024.0 / 1024.0 << " MB done" << std::endl;
}int main(int argc, char *argv[]) {uint32_t mem_size = 100;if (argc >=2) {mem_size = std::atoi(argv[1]);}uint32_t num = (uint32_t)2 * 1024 * 1024 * 1024 / mem_size;test(num, mem_size);std::cout << "input anything to exit" << std::endl;getchar();return 0;
}
編譯:
g++ mem_test.cc -o mem_test -std=c++11
可以通過參數控制內存分配的大小,默認 100Byte:
./mem_test 100
./mem_test 500
./mem_test 1024
./mem_test 10240
過程就省略了,直接上觀察結果:
- 每次測試的虛擬內存大小是類似的,大概在 2G 左右
- 單次分配內存長度為 100 Byte,調用 free 后內存無明顯下降
- 單次分配內存長度為 500,1024,10240…,調用 free 后內存迅速下降接近 0%
- 多次測試臨界值為 120 Byte
以上測試反應出在不同的情況下, free 的行為有差異,但也說明,調用 free 之后內存是能夠立即被釋放給操作系統的(只不過有條件)。
那為什么會出現調用 free 之后內存沒有被釋放(至少看起來是)的情況呢?
測試2
代碼不變,還是上面的代碼,只不過現在啟動兩個同樣的程序:
- 分別以上述不同的參數啟動程序,讓程序執行到釋放所有內存之后,block 住
- 然后啟動第二個程序,用同樣的參數
- 觀察兩個進程是否都能存活
上述有一個條件假設:
total mem: 4G,實際情況可以調整代碼里分配內存的總量
過程也省略,直接上觀察結果:
- 單次分配內存 100 Byte,啟動第二個同樣的程序,出現 OOM (先啟動這個被 kill)
- 單次分配 500,1024,10240…, 啟動第二個同樣的程序,不會 OOM
上面的結果和測試1 的結果是吻合的,這能肯定的說明出現 OOM 的場景下,第一個進程的內存雖然完全釋放了,但是內存依然被該進程持有,操作系統無法把這部分已經調用 free 的內存重新分配給其他的進程(第二個進程)。
測試3
稍微調整一下上面的代碼,分配釋放的操作進行兩次,也就是上面的 test() 函數調用 2 次。
另外本次使用 valgrind(massif) 進行分析,此次單次內存分配大小為 100 Byte,也就是上面出現無法釋放內存的參數。
int main() {
...
test();test(); // call again
...
}
使用 valgrind 進行分析:
valgrind -v --tool=massif --detailed-freq=2 --time-unit=B --main-stacksize=24000000 --max-stackframe=24000000 --threshold=0.1 --massif-out-file=./massif.out ./mem_test
生成的文件 massif.out 使用 massif-visualizer 處理之后得到如下圖:
上圖就是內存分配的情況,從圖中可以很明顯的看到在第一次調用 test() 函數時,內存隨著分配而增長,隨著釋放而下降;第二次調用 test() 函數也是同樣的情況。
那這幅圖能說明什么呢?
第一次調用 test()后,按照測試2 的情況,內存雖然被釋放了,但是內存依然被進程持有,那么不應該出現內存下降的情況,但是從圖中看,確實是下降到接近 0 了,那么可以得出一個結論:
test() 至少是沒有內存泄露的,即分配的內存,都被釋放了(至少標記過釋放),也就是沒有出現野指針等內存泄露的情況。
那么問題就在于,既然沒有內存泄露,那為何內存依然被進程持有?不是已經調用 free 了嗎?
glibc malloc/free 實現
glibc malloc 底層調用的是 ptmalloc,這里就不深入 malloc/free 的實現細節了,網上可以找到很多資料。
下圖是 32 位程序的虛擬內存空間分布圖
原理
向操作系統申請內存涉及到兩個系統調用 sbrk 以及 mmap。關于這兩個系統調用的區別可以大致這么理解:
- ptmalloc 管理了兩塊堆內存,所以有可能會在兩個地方給用戶分配內存
- 這兩塊堆內存的區別就在于一個可以被循環利用,一個在釋放后立即歸還操作系統
- ptmalloc 使用 sbrk 來為第一塊內存區域 heap 進行內存分配,用戶釋放之后 ptmalloc 對這塊內存進行重新管理利用,進程依然持有這塊內存
- ptmalloc 使用 mmap 來為第二塊內存區域 sub-heap 進行內存分配,用戶釋放之后 ptmalloc 立即把這塊內存歸還給操作系統
- 要分配的內存只有達到一定大小(即 mmap 的閾值),ptmalloc 才會采用 mmap 進行內存分配,否則優先選擇 sbrk 分配后被重新管理的內存池
- mmap 的閾值可能是動態調整的,即 ptmalloc 根據自身內存管理情況,動態調整這個閾值
也就是說,ptmalloc 為了性能考慮,采用了兩種內存分配策略,也就是管理了兩種不同分配方式的堆內存。在分配內存小于一定值時就優先在 ptmalloc 維護的內存池里進行分配,這樣避免了直接向操作系統分配內存,減少系統調用次數;如果內存大于一定值時,就直接向操作系統申請內存,并且這段內存在釋放之后立即歸還操作系統;
這也就能解釋上面的幾個測試里,當單次分配的內存大小較大時,內存釋放后進程內存占用快速下降到 0%;當單次分配的內存大小較小時,內存釋放后其實沒有歸還給操作系統,二是被 ptmalloc 重新回收了,放到了內存池里進行循環利用,所以看到進程內存依然保持較高的占用;
另外關于 ptmalloc 對內存池的管理比較復雜,這里推薦一篇不錯的文章可以深度閱讀:
glibc內存管理ptmalloc源代碼分析
到這里,其實就已經比較明確了,free 之后內存釋放情況其實是跟分配的大小有關系的,并且隨著程序的運行,內存的持續分配和釋放,ptmalloc 的內存池應該能穩定在一定的值,從外面來看,進程的內存占用應該能動態穩定下來。
ptmalloc 的兩套分配策略各有優劣,使用內存池可以提高內存分配效率,但是可能出現內存暴漲的情況,但是最終會穩定在一定的值;使用 mmap 的方式分配內存不會出現內存暴漲的情況,釋放完之后理解歸還操作系統,但降低了內存分配的效率。
修改 malloc 參數
根據上面的討論,如果想要控制 malloc 的內存分配行為,那么其實是有辦法做到的。
我們可以通過下面這個函數來實現:
int mallopt(int param, int value);
https://man7.org/linux/man-pages/man3/mallopt.3.html
可以調整 M_TRIM_THRESHOLD,M_MMAP_THRESHOLD,M_TOP_PAD 和 M_MMAP_MAX 中的任意一個,關閉 mmap 分配閾值動態調整機制。
比如上面的測試1,當單次分配的內存 100 Byte 時,內存釋放之后進程內存占用依然較高的情況就能解決:
int main() {mallopt(M_MMAP_THRESHOLD, 64);mallopt(M_TRIM_THRESHOLD, 64);...
}
- M_MMAP_THRESHOLD: mmap 內存分配閾值
- M_RIM_THRESHOLD: mmap 收縮閾值
在 main 函數開始加上上面的兩句,調整 mmap 收縮閾值以及內存分配閾值。重新編譯運行,發現即使單次分配 100 Byte,內存釋放后,進程內存占用也快速下降到 0%。
補充:
int malloc_trim(size_t pad);
可以觸發 ptmalloc 對內存的緊縮,即歸還一部分內存給操作系統。
總結
進程內存占用較高的情況不一定是內存泄露造成的,可以通過長時間觀察內存占用是否能穩定下來進行判斷,如果內存占用能實現動態穩定,那么多半程序是沒有內存泄露的。
但是如果內存占用過高,對其他的進程產生了干擾,那么可以適當的調整一下 malloc 的參數,控制 malloc 的行為,避免 glibc 內存池過大,影響其他進程的運行。
Blog:
-
rebootcat.com
-
email: linuxcode2niki@gmail.com
2020-11-05 于杭州
By 史矛革
總結
以上是生活随笔為你收集整理的free not return memory的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux上隐藏进程名(初级版)
- 下一篇: 博客大事记之迁移博客到香港主机