【騰訊開源】iOS爆內存問題解決方案-OOMDetector組件

組件介紹

OOMDetector是手Q自研的IOS內存監控組件,騰訊內部目前已有多個App接入了OOMDetector,它主要有如下兩個功能:git

  • 爆內存堆棧統計:負責記錄進程內存分配堆棧和內存塊大小,在爆內存時Dump堆棧數據到磁盤github

  • 內存泄漏檢測:檢測內存泄漏,目前支持Malloc內存塊和OC對象的泄漏檢測算法

OOMDetector能夠快速幫助開發者發現和定位App爆內存問題和內存泄漏,組件目前已經在Github開源,源碼地址:https://github.com/Tencent/OOMDetector。緩存

背景

目前業內已有一些比較的IOS內存分析工具,下面逐個介紹這些工具的功能以及它們在使用上的不足。安全

Allocation

做爲IOS開發,咱們都很熟悉蘋果官方提供的Allocation內存分析工具,在開發調試階段,能夠用Allocation詳細分析App各模塊內存佔用。Allocation對App的內存監控比較全面,能監控到全部堆內存以及部分VM內存分配。雖然Allocation的功能比較強大,可是它也有比較明顯的使用侷限性,主要表現爲如下兩點:服務器

  • 沒法獨立在App運行,只能在調試階段鏈接Mac使用markdown

  • 性能較差,大型App開啓後容易引起卡死多線程

這兩點限制決定了Allocation只適合於在開發階段輔助分析代碼中存在的內存問題,而沒法直接對線上用戶的問題進行監控和定位。函數

FBAllocationTracker

FBAllocationTracker是Facebook開源的內存分析工具,它的原理是用 Method Swizzling替換本來的alloc方法,這樣能夠在App運行時記錄全部OC實例的分配信息,幫助App在運行階段發現一些OC對象的異常增加問題。相比Allocation,FBAllocationTracker對App性能影響較低,能夠在App中獨立運行。可是這個工具也有比較明顯的缺陷:工具

  • 監控範圍不夠全面,只能監控OC對象,不能監控C++對象和malloc內存塊以及VM內存

  • 沒有內存對象分配的堆棧信息,對於開發者來講很難只經過對象的類型和數量定位到內存增加的緣由

綜上所述,FBAllocationTracker雖然能獨立在App中運行,可是監控的內存範圍過小,同時記錄的對象信息也過於簡單,對於分析內存問題幫助十分有限。

內存問題一直是手Q的關注重點,爲了保證線上大盤用戶的內存質量,咱們但願有一款工具可以幫助監控和定位線上用戶的內存問題。基於這樣的背景,咱們團隊自研了OOMDetector組件。OOMDetector經過Hook系統底層的內存分配方法,可以記錄到進程全部內存分配的堆棧信息,同時組件可以在對性能流暢度影響不大的狀況下可以保證在App中獨立運行,能夠方便用於分析和監控線上用戶的內存問題(爆內存或者內存泄漏問題)。

組件原理

爆內存堆棧統計

爆內存堆棧監控原理

爆內存堆棧監控的實現原理如圖1所示,經過Hook IOS系統底層內存分配的相關方法(包括malloc_zone相關的堆內存分配以及vm_allocate對應的VM內存分配方法),跟蹤並記錄進程中每一個對象內存的分配信息,包括分配堆棧、累計分配次數、累計分配內存等,這些信息也會被緩存到進程內存中。在內存觸頂的時候,組件會定時Dump這些堆棧信息到本地磁盤,這樣若是程序爆內存了,就能夠將爆內存前Dump的堆棧數據上報到後臺服務器進行分析。

圖1 爆內存監控原理

性能挑戰

App的內存分配方法的調用頻率很是高,在大型App中可能高達10W/次每秒。要Hook這類方法對組件的性能來講是極大的挑戰,由於若是組件自己耗時的話就很容易致使App卡頓甚至卡死。在OOMDetector中,咱們對Hook方法代碼的執行效率進行了嚴格控制,也採起了一些策略對Hook方法中耗時較多的堆棧回溯和鎖等待進行了優化:

  • 優化堆棧回溯方法

對於堆棧回溯,系統提供了backtrace_symbols方法能夠直接獲取堆棧信息,可是這個方法特別耗時。因此咱們根據堆棧的回溯原理實現了更高效的堆棧回溯方法,優化後的方法在運行時只會獲取堆棧函數的地址信息,在回寫磁盤的時候再根據動態庫的地址範圍拼裝成如圖2所示堆棧格式(相似Crash堆棧),後臺服務器利用atos命令和符號表文件就能夠還原出對應的堆棧內容。經過這種方式能夠把耗時較高的符號還原工做放到服務器端,客戶端只須要執行耗時較少的堆棧函數地址回溯操做,優化後的堆棧回溯方法耗時低於1us。

圖2 堆棧格式

  • 優化鎖等待耗時

對於多線程的內存分配,爲了保證線程安全,堆棧數據的插入操做必需要上鎖。對於這種高頻調用的方法,鎖的性能是咱們最關心的指標。IOS開發中NSLock和@synchronized是比較經常使用的,那麼這兩種鎖的性能如何呢?

咱們經過測試代碼對IOS中經常使用的鎖進行了測試,總結了圖2所示的各類鎖的性能比較圖,根據圖3的測試結果,NSLock和@synchronized的性能要低於pthread_mutex,性能最好的是自旋鎖OSSpinLock。

自旋鎖的原理是,若是自旋鎖已經被別的執行單元保持,調用者就一直循環等待鎖的釋放。相比互斥鎖而言,自旋鎖不會引發調用者休眠,節省了線程休眠的狀態切換,因此有更高的效率,但代價是增長了cpu的使用率。對於咱們的場景,由於須要上鎖部分的代碼執行耗時較少,採用OSSpinLock的自旋鎖並不會顯著增長cpu的使用率,因此咱們優先考慮鎖的效率採用了OSSpinLock的方案。

圖3 各類鎖的性能比較

堆棧聚類和壓縮

以前提到,咱們的Hook方法會緩存每一個內存分配的堆棧數據。假設App的內存塊個數爲25W,堆棧平均深度20行,每一個堆棧地址採用8字節的整型數據存儲,那麼25W個堆棧數據將佔用40M的內存空間。顯然這樣的內存增加對於任何App都是不可承受的,因此咱們須要對組件的內存佔用進行優化。

咱們分析爆內存問題時候,只須要分析那些內存佔用較大的堆棧,基本不用關心那些內存佔用較小的堆棧。因此咱們的優化思路也很明確:只保留內存佔用較大的堆棧。要完成這個工做就必須對內存中全部堆棧先進行聚類合併,統計出每一個堆棧累計的內存值。

具體的優化策略如圖4所示,對於每一個記錄到的分配堆棧,首先經過md5算法將堆棧數據壓縮爲16字節的md5,經過md5值進行聚類,緩存中只保留16字節的md5數據,只有當某個堆棧的累計內存超過必定閥值時,纔會保留原始堆棧信息,這樣由於超過閥值的堆棧數量有限,堆棧原始信息佔用的空間幾乎就能夠忽略不計了。

圖4 堆棧聚類和壓縮原理

採用兩種方式能夠將堆棧下降到優化前的1/40左右,優化後的組件內存基本不會對App的內存形成太大影響。

數據Dump方案

前面提到,在內存觸頂後要將內存中的堆棧數據定時Dump到磁盤中,常規的方案是IO接口直接把數據寫入到磁盤。由於數據Dump的頻率較高,頻繁的IO操做會致使程序卡頓。由於數據Dump的操做是很是高頻的,因此咱們採用了效率更高的mmap方式。

mmap是一種內存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間。實現這樣的直接映射關係後,寫文件的過程進程不會有額外的文件的數據拷貝操做,避免了內核空間和用戶空間的頻繁切換,如圖5所示。根據咱們的代碼實測,向mmap映射空間寫數據的性能與直接寫內存一致,效率遠高於IO操做。

圖5 內存映射原理

那麼mmap的回寫時機是怎樣的?根據官方文檔描述,主要有以下時機:

  • 系統內存不足時

  • 進程crash時

  • 主動調用 msync時

mmap 在內存不足時會主動進行回寫操做,這樣的機制也保證咱們的監控組件能在程序爆內存前將緩存中的數據回寫到磁盤,從這一點看採用mmap的方式相比常規IO操做也有更強可靠性。

內存泄漏檢測

除了爆內存堆棧監控,OOMDetector還集成了內存泄漏檢測功能,可以檢測Malloc內存塊和OC對象的「無主內存泄漏」。所謂「無主內存泄漏」是指內存塊在進程內已經沒有引用卻沒法正常釋放的內存塊。

按照以前介紹的方案,OOMDetector能夠記錄到每個對象的分配堆棧信息,要從這些對象中找出 「泄漏對象」,咱們須要知道在程序可訪問的進程內存空間中,是否有「指針變量」指向對應的內存塊,那些在整個進程內存空間都沒有指針指向的內存塊,就是咱們要找的泄漏內存塊。如圖2所示,在IOS系統中,可能包含指針變量的內存區域有堆內存、棧內存、全局數據區和寄存器,OOMDetector 經過對這些區域遍歷掃描便可找到全部可能的「指針變量」,整個掃描流程結束後都沒有「指針變量」指向的內存塊便是泄漏內存塊。

爲了不內存訪問衝突,掃描過程須要掛起全部線程,整個過程會卡住程序1-2秒。由於掃描過程較爲耗時,這個功能目前主要用於App的測試階段,與自動化測試結合可快速高效的發現泄漏問題。

圖6 內存泄漏檢測原理

展望

開源只是開始,咱們後續仍會不斷對OOMDetector組件進行改進,也歡迎你們對組件多提意見。若是你的IOS應用也在受到內存問題困擾或者你也對IOS內存監控技術感興趣,那麼來了解下咱們的組件吧!

相關文章
相關標籤/搜索