日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

導語

本文將介紹幾種內存泄漏檢測工具,并通過實際例子介紹一種分析堆內存占用量的工具和方法,幫助定位內存膨脹問題。

背景

進程的內存管理是每一個開發者必須要考慮的問題,對于C++程序進程來說,出現問題很多情況下都與內存掛鉤。進程崩潰問題通常可以使用gdb等調試工具輕松排查并解決。而對于進程內存膨脹這類問題,原因通常有三個:

1.內存泄漏。

2.分配器管理的空閑內存較多而造成的內存空洞。

3.有未統計使用的未知內存占用。

內存泄漏問題可以使用一些工具來檢測。但是對于后兩種問題,卻一直沒有比較通用的方法去確定。本文將介紹幾種內存泄漏檢測工具,并通過實際例子介紹一種分析堆內存占用量的工具和方法,幫助定位內存膨脹問題。

常見內存問題的分析方法

對于內存泄露問題,目前已經有較成熟的工具進行檢測,這里簡單介紹兩個工具:AddressSanitizer和Valgrind。

AddressSanitizer是google開源項目,可以用來檢測內存泄漏和其他導致進程崩潰的內存問題。它的優勢在于造成的額外CPU占用很小,但是需要重新編譯項目,并且在編譯的時候添加-fsanitize=address選項。在程序運行時如果有任何內存問題,就會終止進程并且打印出詳細的錯誤信息。如果進程存在內存泄漏會在進程結束后,打印出所有泄漏的內存大小和申請這塊內存的調用棧,如下圖所示:

Linux進程內存用量分析之堆內存篇

 

AddressSanitizer檢測內存泄漏

另一個工具Valgrind的優勢在于不需要重新編譯,只需要在運行時加上valgrind --leak-check=yes即可。但是它的額外CPU開銷會更大,大約是AddressSanitizer的十倍,功能上也不及AddressSanitizer完善。下表是兩種工具功能和性能的比較:

Linux進程內存用量分析之堆內存篇

 

AddressSanitizer與Valgrind對比

這兩種工具不光能夠檢測內存泄漏,對于堆棧溢出等問題也有比較好的效果。對于這兩個工具更具體的介紹可以參照官方的使用文檔:

AddressSanitizer:https://github.com/google/sanitizers/wiki/AddressSanitizer

Valgrind:http://valgrind.org/

而對于后兩種原因,我們需要根據不同的分配器區別看待。常見的分配器有glibc默認的ptmalloc,google維護的tcmalloc以及facebook維護的jemalloc等。后兩者都自帶了內存分析工具(Heap Profiler),可以檢測內存泄漏,也可以打印出詳細的內存分配情況,對上述三個問題都有比較完善的排查方法,有興趣可以查看官方文檔,都講得比較詳細,這里不再介紹。而glibc默認的ptmalloc卻不自帶這樣的工具,一種排查方法是去了解ptmalloc的實現和結構以后編寫程序或者gdb腳本去分析進程的內存結構,我們接下來要介紹的一種內存分析工具就是以這種方法實現的。

針對ptmalloc的堆內存內用分析

1.環境

58自研的搜索引擎Esearch底層使用C++實現。Esearch在內存管理方面針對不同的場景會有不同的策略。對于對象生命周期有規律,高頻分配的場景,Esearch實現了定制的內存池進行管理,并且這些內存都會在日志中統計占用量。而對于對象生命周期不確定,大小不確定的場景,內存池的代價可能高于通用分配器(new/malloc),所以直接使用通用內存分配器來分配。

對于通用分配器的選擇,目前Esearch使用的是glibc2.12環境下默認的ptmalloc。之所以未使用tcmalloc或者jemalloc是因為經測試后發現后兩者在常見場景下內存占用比ptmalloc要高,而且Esearch中對于內存分配熱點已經使用了定制內存池,后兩種分配器的優勢其實并不明顯。對于Ptmalloc完整結構的介紹可以閱讀源碼或者參閱華庭的《glibc內存管理ptmalloc2源代碼分析》,這里只在用到時闡述一下原理,不做過多的介紹。

接下來我們通過一個例子來了解如何分析堆內存的用量。現在有一個realtime_searcher進程如下:

 

運行中的realtime_searcher進程

可見進程占用總物理內存27G,其中SHR內存占用18G,剩下的物理內存 約9G。

2.工具介紹

這里介紹一個非常強大的內存分析工具——core_analyzer。這是一個基 于core文件的內存分析工具,由Michael Yan開發和維護并且開源在github上。利用它可以對glibc層的ptmalloc結構進行分析,還原進程真實的內存結構。目前core_analyzer支持的glibc2.3,2.4,2.5,2.12-2.23版本下的ptmalloc實現,這些版本對應的ptmalloc結構其實都大同小異。

Core_analyzer工具提供了以下功能:

Linux進程內存用量分析之堆內存篇

 

Core_analyzer用戶界面

[0] 打印core文件的基本信息,包括各個線程的信息,和內存段的信息等。

[1] 水平搜索對象的引用

[2] 垂直搜索對象的引用鏈,直到找到符號表中有調試信息的對象。

[3] 打印線程共享對象

[4] 打印給定地址段的內容

[5] 通過地址查詢所屬chunk

[6] 打印所給的地址周圍的頁(意義不明)

[7] 打印整個堆的結構。該結構能夠與ptmalloc的結構相對應

[8]、[9]都是打印前N大的chunk,[9]還順便打印出引用鏈

[10] 根據它的名字為內存泄漏檢測,但實際使用發現不僅耗時而且不正確

[11] 退出

原本的core_analyzer功能就上述這些,但是在實際使用過程中發現它對于線程較多(大于32)的進程支持并不好,所以在對其進行了改造后,又順便添加了兩個功能:

[12] 以chunk大小分類,并按照占用總大小排序。

[13] 打印出所有的chunk。由于結果可能較大,所以打印在out.txt文件中。

項目原地址:https://github.com/yanqi27/core_analyzer.git

改動過后的工具開源在igit上:http://igit.58corp.com/jichenxuan/core_analyzer_fixed.git

3.分析方法

首先我們需要生成一個core文件。使用gcore命令可以不殺死進程同時生成core文件。執行命令:

gcore 12763

但是在生成core文件的過程中進程是暫停的,而且會將打開的mmap文件全部讀取到內存中,因為它本身是通過gdb attach pid&& gcore實現的,所以對線上的服務需要酌情使用。現在我們拿到了一個core.pid文件。執行

core_analyzerrealtime_searcher core.12763

第一個參數為可執行程序。之后會看到core_analyzer詢問core文件的main_arena和mp_地址。通過gdb命令print &main_arena 和 print &mp_可以拿到這兩值

Linux進程內存用量分析之堆內存篇

 

使用gdb打印main_arena和mp_的地址

如果安裝了glibc對應版本的調試信息,就可以看出這兩個變量分別為malloc_state和malloc_par,ptmalloc使用這兩個結構體管理分配區(arena),其中每一個分配區都是malloc_state的一個實例,而malloc_par則是參數管理,為靜態成員,全局有唯一的malloc_par實例。變量main_arena即為主分配區實例,ptmalloc中所有的分配區組織成一個環形鏈表的結構,如下圖(圖自http://core- analyzer.sourceforge.net/index_files/Page335.html),有了主分配區就可以遍歷這個鏈表,拿到所有的分配區信息。

Linux進程內存用量分析之堆內存篇

 

Ptmalloc結構

在當前環境下這兩個結構體分別如下圖所示,有興趣可以對照ptmalloc的結構了解各個成員的含義這里不多介紹。不同版本的ptmalloc實現在這兩個結構體的實現上有些許差異,但都是大同小異。

Linux進程內存用量分析之堆內存篇

 

Ptmalloc數據結構源碼

將上述兩個結構體的地址填入core_analyzer中,稍等一下加載過程,便可以看到它的功能界面。首先可以使用Print General Core Information可以看到每個線程的狀態,收到的信號等,以及整個進程的內存地址從低到高的布局。可以對進程狀態有個大致的了解。

Linux進程內存用量分析之堆內存篇

 

各個線程狀態

Linux進程內存用量分析之堆內存篇

 

進程內存布局

執行Heap Walk便可將ptmalloc層的完整結構打印出來。由于打印內容較多,我們拆開來看。

Linux進程內存用量分析之堆內存篇

 

打印ptmalloc堆結構

前邊幾行打印出記錄在malloc_par結構中的內容,也就是一些參數。我們重點關注一下n_mmaps和mmaped_mem這兩個值,就是說由ptmalloc申請的mmap chunk一共有70個,占用總空間為3213393920字節,記住這個值之后會分析到。

下邊從Main arena開始就是所有的分配區信息。

Main arena:主分配區信息。主分配區從heap段分配空間,調用brk/sbrk增長,地址連續,所以只有一個堆塊。

Dynamic arena:非主分配區信息。從mmap段開始分配空間(注意這里的mmap含義為進程地址空間的mmap段,有別與前文中的mmap調用,后邊還會有,注意區分),其下的每一行表示一個子堆(struct heap_info實例),每個子堆大小上限為64M,用完后需要調用mmap申請新的子堆,子堆以鏈表結構鏈在頭節點后。

每個子堆的信息從前到后依次為起止地址,共占用大小,使用中的chunk數量和占用大小,空閑的chunk數量和占用大小。

每個線程都會有自己私有的分配區,在分配空間時會先檢查該私有分配區是否被加鎖,如果沒有則加鎖分配,否則遍歷循環鏈表直到找到可用的分配區分配。極端情況可能遍歷完整個環形鏈表也找不到一個可用分配區,這種情況下就分配一個新的分配區鏈到循環鏈表中。可以認為分配區數量等于線程數量。于是我們就可以大致猜測這些分配區都對應什么線程。例如上圖中這些只有一個子堆的分配區可能就對應著空間占用較小的查詢線程或者一些控制線程,而下圖中的這個分配區有多個子堆,可能就對應著需要更多內存的文檔線程。

Linux進程內存用量分析之堆內存篇

 

含有多個子堆的分配區

注意看這個線程的子堆大小,明顯第一個子堆的總大小是小于64M的,而下邊的子堆大小接近64M,這是因為每個分配區下的子堆雖然是鏈表結構,但是在邏輯上仍然看作是一塊地址連續的堆空間,第一個子堆對應著堆頂,而堆的收縮就是通過模擬移動堆頂指針(實際上是由top_chunk管理)實現的。也就是說只有第一個子堆的堆頂釋放了,下邊的內存才可以釋放,就像下圖這樣。當第一個子堆完全釋放了,才可以釋放第二個和下邊的子堆。這就會造成如果下邊的子堆上的內存已經釋放,但是堆頂一直不釋放,ptmalloc就無法將釋放的內存歸還給操作系統,也就是會造成內存空洞現象。

Linux進程內存用量分析之堆內存篇

 

堆頂未釋放而造成的內存空洞

就上邊的例子來看,這個分配區大約有3M左右的內存空洞,比較正常,不是內存膨脹的主要來源。我們繼續往后看,后邊列出了所有的mmap段上申請的塊

Linux進程內存用量分析之堆內存篇

 

進程申請的mmap內存塊

注意這里的塊有些是調用malloc申請大于mmap_threashold的塊時由ptmalloc分配的,而有些則是代碼中直接使用mmap申請的空間。Core_analyzer無法對這兩類空間進行區分,就比如繼續看接下來打印的塊:

Linux進程內存用量分析之堆內存篇

 

含有較大的mmap塊

還記得上文中提到的n_mmaps和mmaped_mem兩個值嗎?這兩個值就對應了通過ptmalloc申請的mmap段空間,總大小為3G+,那我們至少我們能知道上圖中大于3G的塊都不是通過malloc接口申請的,而是通過mmap調用申請的。實際上這些較大的mmap塊就是打開的索引文件。

最后在打印的結尾處有一個匯總,說明了進程占用的總內存大小,以及使用中和空閑的內存大小,從這里也可以判斷內存空洞現象是否嚴重。在這個例子中,空閑內存占用為775MB,占總內存的2%左右,因為空閑內存可以被復用,所以不算太大。

Linux進程內存用量分析之堆內存篇

 

Ptmalloc層的內存統計

通過上邊的方法,我們便可將進程的內存布局一覽無余,并且可以判斷是否存在大量的內存空洞。接下來就需要繼續深究這些in-use內存都是由哪些對象在占用。我們可以先做個假設,所有大小相同的chunk極有可能是同一個類的實例,這個假設是接下來定位對象類型的前提,雖然這個假設不一定正確,但是大部分情況下是合理的。

執行Sort by Type,這個功能按照chunk大小給所有的chunk分類,并且按照占用總大小排序。如果core文件較大可能需要較長的時間,需要耐心等待,但是會記錄結果,也就是說只要跑出來一次,再次使用這個功能就直接輸出結果而不用再跑一遍了。打印出如下圖所示:

Linux進程內存用量分析之堆內存篇

 

內存塊分類排序

三列分別代表塊大小,該大小的塊個數和該大小塊占用的總空間。按照占用總空間是升序排列。可以看到較大的幾個塊數量都很少,前邊分析過這部分可能是索引文件,我們來加以驗證。執行Get All Blocks,這個功能會打印出所有的chunk的大小和起始地址,注意會打印在當前目錄下的out.txt中。

Linux進程內存用量分析之堆內存篇

 

所有地址塊的大小和起始地址

可以看到大小為4292967280的塊共有三個,拿其中一個起始地址(十進制表示),使用vertical search,該功能會遍歷所有的chunk,向上尋找引用到這個chunk的chunk,一直向上直到找到一個在符號表中有調試信息的對象。注意該過程可能耗費較長時間,需要耐心等待。

Linux進程內存用量分析之堆內存篇

 

地址塊引用鏈

上圖中顯示出多條調用鏈,這是由于對一個chunk的引用不只是有指針指向chunk的首部的情況,還可能指向chunk的內部。取上圖中最后一條調用鏈舉例,其意義為一塊56大小的chunk在偏移量為40的位置指向了4292967280這個塊中地址為0x7f8f802ec6a4的地方,而56大小塊的虛指針為_vptr,這樣,我們就可以通過gdb得到這個虛指針地址對應的符號為

使用gdb得到虛指針的符號表

從而得知這個56大小的塊對應的類是某個ArrayReader,該類作用為讀取正排索引值,內存結構如下圖。可以得知偏移量為40的地址存放的是一個指針指向打開的索引文件。于是驗證了前邊的猜測。

Linux進程內存用量分析之堆內存篇

 

 

ArrayReader數據結構

上邊屬于找到了直接引用該chunk的對象,而有些對象的引用可能是間接引用,中間通過好幾層才引用到要查找的chunk,例如下圖中就找到了一條間接引用鏈,但是定位方法跟直接引用是一樣,通過gdb命令info symbol可見上層vptr指向LRUCache類,而所查詢的chunk其實就是LRU緩存中雙向鏈表的節點。

Linux進程內存用量分析之堆內存篇

 

多層引用鏈以及頂層虛指針類型

如果還對結果存疑,可以使用Memory Pattern Analysis功能進一步驗證,這個功能會打印出所給地址空間存儲的內容,可以對照源碼一一確認存儲數據的值。例如確認一個HashNode的結構在內存中的布局:

Linux進程內存用量分析之堆內存篇

 

內存存儲值與代碼中的結構一一對應

通過這種方式,對占用內存較大的幾類chunk一一進行排查,便可知曉是那些對象在占用內存。但是不可避免有一些不同的類的大小卻一樣,前邊的假設會將它們看做同一個類,這就需要對這種大小的chunk進行抽樣排查,可以嘗試寫一些腳本輔助排查。

4.注意事項

上文中介紹了使用core_analyzer分析堆內存占用的方法。雖然core_analyzer功能很強大,但是也存在一些缺陷,使用時有些注意事項需要了解。

1)當前的原版core_analyzer最多支持到32個線程,而igit上改動過后的版本支持256個線程的進程,這部分以后可能會考慮優化,使其能夠根據線程數自我擴展。

2)引用搜索功能打印的結果只是潛在的引用鏈,因為對于例如某變量值剛好等于查詢地址這些情況,core_analyzer并不能區分。所幸的是這樣的情況極少。

3)使用時候一定要有耐心,對于較大的core文件core_analyzer確實需要耗費較長的時間處理。

4)[8]和[9]功能不要輕易嘗試,很慢,而且跑不出來。

5)內存泄漏檢測功能形同虛設,結果難以理解。建議還是使用上文中介紹的兩種工具來檢測內存泄漏。

6)Core_analyzer的使用前提是對應用的代碼已經有比較詳細的了解,至少要能分辨出代碼中哪些內存的分配是調用了mmap而不是malloc。

雖然core_analyzer的使用并不友好,但是目前為止,暫時還沒有發現有其他工具有如此強大的功能。使用其他工具諸如libheap等來定位占用內存的對象無疑等同于大海撈針。所以可以說core_analyzer的功能之強大足以掩蓋其用戶體驗差的問題。

內存優化建議

通過上述過程,我們已經能夠分析堆內存的占用并且定位到內存膨脹的原因,接下來就可以針對這些原因對癥下藥。由于每個應用的復雜性和場景都不盡相同,這里只提一些簡單的優化思路。

對于內存空洞現象比較嚴重的情況,由于ptmalloc自身的策略原因,不適合分配長生命周期的內存,可以考慮嘗試tcmalloc等其他分配器。如果非用不可,那一定要記得后分配的內存要先釋放,這樣才能夠使堆收縮。如果分配區的堆頂不釋放,那下邊的內存就都不會釋放。這在多線程進程中其實比較難實現,因為不能保證內存的申請和釋放在同一個線程。

另外可以通過調用malloc_trim()函數來手動收縮堆。官方文檔上介紹只能夠收縮主分配區的堆頂,但是經實踐和源碼分析發現,它其實可以遍歷所有的分配區,并且對其中的空閑內存調用madvise建議操作系統回收該部分內存。雖然操作系統是否真的回收就是另一回事了,但是在實際的實踐后發現,對內存空洞現象的改善效果明顯,可以酌情使用。

對于使用中的內存較多的情況,這類情況比較復雜,需要多加分析,尋找優化點。比如底層的數據結構是否有優化空間,尤其是對于實例比較多的類,例如哈希節點或者鏈表節點這些類,可能幾個字節的優化就能帶來較高的收益。還有在內存中盡量只保留有用的對象,無用的對象盡早釋放掉,這樣有利于內存空間的復用,一個例子就是過期的in-memory緩存要及時釋放掉。

如果使用定制內存池,一個細節就是內存池的分配器應該盡量避免再使用malloc/new分配空間,而是直接使用系統調用brk或者mmap申請空間。因為內存池通常會緩存一部分內存以提升分配效率,而如果保留的內存恰巧為分配區的堆頂,就會導致堆無法收縮從而造成比較嚴重的內存空洞問題。而且與底層分配器嵌套使用會使內存多次對齊,并且使內存塊多加一層首部,造成不必要的浪費。

 

總結

本文介紹了linux進程內存泄漏的檢測方法,并且通過例子介紹了一種分析堆內存占用的方法。在面對內存膨脹問題時,如果排除了內存泄漏原因,可能就會覺得再往下排查有些無從下手。但是core_analyzer為我們提供了一種便捷的分析內存占用的方式,雖然這種便捷是相對的,也不妨礙它成為我們面對內存難題時一個很好的選擇。

內存管理是所有C++開發者需要面對的難題。C++語言在提供給開發者較高的內存支配自由度的同時,也將內存管理問題丟給開發者。優秀的內存管理策略可以充分利用內存支配的特性,使程序效率飛升。而復雜的內存管理一旦出現問題,定位起來也會非常困難。這就需要我們去深入到底層,了解內存的布局,一點一點分析內存管理的結構。使用一些工具協助排查也許會收獲事半功倍的效果。當然除了本文介紹的方法外還有別的方法來分析內存問題,如果有更好的方法,也歡迎大家進行交流。

分享到:
標簽:內存 Linux
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定