火山引擎MARS-APMPlus 應用性能監控幫助客戶Java OOM崩潰率下降80%

 
本文將會從Java內存基礎開始,詳細介紹“基於Hprof內存快照的線上Java OOM 歸因方案”的底層原理與技術細節,歡迎接入MARS-APMPlus 應用性能監控使用。
作者:字節跳動終端技術——王濤

一、前言

如何定位和解決Android App因爲內存不足(Java OOM)引發的線上問題一直是業界的難題。崩潰場景能抓取到的常規信息中並不包括內存分配詳情——不瞭解內存被誰持有,自然也無法追查內存不足的根源。
針對這個問題,Client Infra和頭條抖音等業務方合作,通過一系列技術調研,自研了一套 基於Hprof內存快照的線上Java OOM 歸因方案,在內部廣泛應用並取得了極佳的效果。曾幫助Helo在一個雙月內 優化了80%的Java OOM問題,次日存留增長了2+%
火山引擎 MARS-APMPlus 應用性能監控平臺對外提供該解決方案後,美篇作爲早期接入客戶,也同樣取得了雙月週期 減少80% Java OOM的好成績,深受客戶好評。
接下來本文將會從Java內存基礎開始,詳細介紹方案的底層原理與技術細節。希望大家能通過方案瞭解MARS-APMPlus 應用性能監控平臺,加入我們的 「 MARS-APMPlus 應用性能監控企業助力行動 」幫助團隊打造極致的用戶體驗

二、Java 內存基礎

2.1 Java 內存優化的重要性

內存是計算機的稀缺資源,操作系統本身也通過虛擬內存等方式來充分的使用內存資源。
如果Java 堆內存佔用過多,JVM頻繁GC會引起App的卡頓,影響App的易用性 。
更嚴重的Java 堆內存使用超過虛擬機限制會導致OOM崩潰,影響App的可用性 。
從App的易用性和可用性來說,Java內存的優化還是十分重要的,特別是用戶使用應用的崩潰問題,應該得到有效解決。

2.2 爲什麼會Java OOM崩潰

Java OOM,全稱是 Java Out Of Memory,字面意思是說Java 虛擬機的內存用完。Java有一個相關的異常類 java.lang.OutOfMemoryError,官方有如下說明:
Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector. 
就是說,當Java 虛擬機沒有更多的內存可以爲對象分配空間,垃圾回收器也沒有更多的空間可以回收時,就會拋出這個Error。
這裏面有幾個關鍵點,理解這幾個關鍵點,我們就會理解爲什麼會發生Java OOM崩潰
  • Java虛擬機都有哪些內存區域
  • 垃圾回收器是如何工作回收內存的
  • 每個對象佔據多大的內存空間
  • Java 虛擬機當前的內存空間狀態以及OOM是如何發生的
下面會以簡潔的方式介紹這幾個關鍵的知識點。

2.1.1 Java虛擬機的內存區域

Java 虛擬機在執行 Java 程序的過程中會把它管理的內存劃分成若干個不同的數據區域,如下圖所示:
下面是每個區域的一個概要說明:
名稱
說明
是否線程間共享
PC Register
稱爲程序計數器, 看作是當前線程所執行的字節碼的行號指示器
JVM Stack
也稱爲虛擬機棧,記錄每個棧幀(Frame)中的局部變量、方法返回地址等
Native Method Stack
本地 (原生) 方法棧,是調用操作系統原生本地方法時,所需要的內存區域

Heap

堆內存區,也是 GC 垃圾回收的主要場所,用於存放類的實例對象
Method Area
方法區,主要存放類結構、類成員定義,static 靜態成員等
Runtime Constant Pool
運行時常量池,比如:字符串等
其中我們需要重點關注的是線程間共享的 Heep堆內存區域。這部分區域是GC垃圾回收的主要場所,用於存放類的實例對象。我們最常見的Java OOM都是因爲堆內存使用超出虛擬機最大可用內存閾值導致的崩潰。垃圾回收機制也是針對堆內存部分。

2.1.2 垃圾回收器是如何工作回收內存的

Java 虛擬機有自動內存管理機制,通過垃圾回收器來管理內存,一旦確定程序不再使用某塊內存,它就會將該內存回收。
垃圾回收器當前主要通過可達性分析算法判斷一個對象是否可以被回收:通過一系列稱爲GC Roots的對象作爲起點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連(即對象到GC Roots不可達),則證明此對象已死、可回收。下圖灰色部分即爲可回收的內存對象。
GC Roots是可以從堆外部訪問的對象,例如Java線程當前活躍的棧幀裏指向GC堆裏的對象的引用,就是當前正在被調用方法的引用類型的參數和局部變量等。
垃圾回收有不同的收集算法,和不同類型的垃圾收集器,這裏只是概述背景不再詳細說明。是否可回收的核心是判斷一個對象是否到GC Roots不可達,不可達則對象會被回收釋放內存空間。
這裏我們知道了一個對象在什麼情況下被回收的。如果在內存裏沒有被回收,那就是因爲有GC Root對它持有引用。在內存充足並有足夠大的連續空間時,虛擬機會創建對象正常分配內存。

2.1.3 對象佔據多大的內存空間

上面我們知道了一個對象是如何被回收的,那麼內存中的對象到底佔據多大的內存呢。這裏會先介紹一個概念 Dominator Tree支配樹, Dominator Tree有以下幾個定義:
  • 對象X Dominator(支配)對象Y,當且僅當在對象樹中所有到達Y的路徑都必須經過X
  • 對象Y的直接Dominator,是指在對象引用關係中距離Y最近的Dominator
  • Dominator tree利用對象引用關係構建出來
對象引用關係和 Dominator tree的對應關係如下:
如上圖,因爲A和B都引用到C,所以A釋放時,C內存不會被釋放。所以C這塊內存不會被計算到A或者B的Retained Size中,因此,對象樹在轉換成 Dominator tree時,會A、B、C三個是平級的。
將對象引用關係轉換成 Dominator Tree能幫助我們快速的發現佔用內存最大的塊,也能幫我們分析對象之間的依賴關係。
根據支配關係,對象大小有兩個定義Retained Size和Shallow Size:
  • Shallow Size:對象本身佔用內存的大小。也就是對象頭加成員變量(不是成員變量的值)的總和,如一個引用佔用32或64bit,一個integer佔4bytes,Long佔8bytes等。常規對象(非數組)的Shallow Size 由其成員變量的數量和類型決定,數組的 Shallow Size 由數組元素的類型(對象類型、基本類型)和數組長度決定。例如E的Shallow Size,只是自身大小和他引用的G沒有關係。
  • Retained Size:對象被垃圾回收器回收後能被GC從內存中移除的所有對象內存大小之和。相對於Shallow Size,Retained Size可以更精確的反映一個對象實際佔用的大小(若該對象釋放,Retained Size都可以被釋放)。例如E到C的引用鏈斷開後,會釋放E、G這2個對象。這2個對象的所佔內存之和就是E的Retained Size。
這裏我們就知道了如果要優化內存或者解決泄露,優先關注 Retained Size 較大的對象,因爲Retained Size大的對象所能釋放的內存空間更大。

2.1.4 Java OOM的發生

學習了內存區域,垃圾回收機制,以及對象所佔用的內存空間大小,那麼Java OOM 到底是如何發生的呢。下面我們來看一個Java OOM異常時候的信息:
java.lang.OutOfMemoryError: Failed to allocate a 65552 byte allocation with 23992 free bytes and 23KB until OOM, max allowed footprint 536870912, growth limit 536870912
OutOfMemoryError拋出的地方在系統源碼文件/runtime/gc/heap.cc
//方法

void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type) 

//異常信息

  oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free

      << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM,"

      << " target footprint " << target_footprint_.load(std::memory_order_relaxed)

      << ", growth limit "

      << growth_limit_;
看上面異常日誌,Java 虛擬機堆內存只剩下23992字節,無法分配65552字節的空間,拋出 OutOfMemoryError異常。
Android可以通過如下接口獲取到當前虛擬機的內存狀態。
  • Runtime.getRuntime().maxMemory() : 當前虛擬機實例的內存使用上限
  • Runtime.getRuntime().totalMemory() : 當前已經申請的內存,包括已經使用的和還沒有使用的
  • Runtime.getRuntime().freeMemory() : totalMemory中已經申請但是尚未使用的部分
  • used=totalMemory() - freeMemory(): 已經申請並且正在使用的部分
  • totalFree=maxMemory()-used: Java虛擬機還可以使用的部分
下圖表述了內存指標之間的關係:
如果可用的內存無法提供分配對象所需的空間,則會產生 OutOfMemoryError異常。
本文主要講解最常遇到的Java 堆內存用盡導致的OOM問題解決方案。由於線程數據超限,虛擬內存用盡導致的OOM並不在當前的解決方案內。

2.3 Java內存相關工具

針對Java 堆內存問題,當前業界已經提供了一些分析Java內存的工具,內部也做了一些接入和測試
工具名稱
介紹
優點
缺點
MAT
The Eclipse Memory Analyzer is a fast and feature-rich that helps you find memory leaks and reduce memory consumption.
分析功能強大
線下分析,需要自己採集Hprof文件
 
LeakCanary
 
LeakCanary is a memory leak detection library for Android.
可以接入App自動分析
線下分析,主要分析內存泄露
 
Android Studio Memory Profiler
可幫助用戶識別可能會導致應用卡頓、凍結甚至崩潰的內存泄露和內存抖動
可以動態內存監控,也可以靜態內存分析
線下分析,需要App debug模式
經過測試這些工具很難滿足產品解決Java OOM的需求,主要存在以下問題:
  • 都是線下工具,線下復現Java OOM問題困難
  • 自動化程度低,只能手動操作分析內存問題
  • 都是單點工具,只能分析單個hprof文件,沒法聚合找到核心問題

三、Java OOM歸因方案

由於業界已有工具無法滿足解決線上Java OOM問題的需要,內部調研開發了一套基於Hprof內存文件的線上Java OOM歸因方案,解決已有工具的痛點,可以高效解決線上Java OOM問題。工具擁有以下特點:
  • 高度還原場景:可以拿到Java OOM時候的場景內存數據
  • 自動化分析:可以自動化進行內存數據分析
  • 聚合找到核心問題:可以根據問題特徵聚合發現核心問題
  • 隱私安全:因爲是線上監控,所以需要滿足用戶隱私安全的要求
因爲方案是根據Hprof內存文件進行設計,在進行詳細方案講解之前,先介紹一下Hprof內存文件。

3.1 Hprof基礎知識

3.1.1 Hprof介紹

Hprof最初是由J2SE支持的一種二進制堆轉儲格式,Hprof文件保存了當前java堆上所有的內存使用信息(包括但不限於Class類信息、對象信息、引用關係等等),能夠完整的反映虛擬機當前的內存狀態。

3.1.2 Hprof結構

Head:
Record:
Hprof文件由Fixed Head和一系列的Record組成,Record包含字符串信息、類信息、棧信息、GC Root信息、對象信息。每個Record都是由1個字節的Tag、4個字節的Time、4個字節的Length和Body組成,Tag表示該Record的類型,Body部分爲該Record的內容,長度爲Length。

3.1.3 Hprof文件使用

Android Studio Memory Profiler、 LeakCanary、MAT 等工具分析內存信息和引用鏈都是依賴Hprof文件。
Android可以dump獲取到Hprof內存文件,我們當前的方案也是基於獲取到的Hprof文件來分析內存問題進行歸因。

3.2 方案概要

方案架構圖
上圖列出了客戶端、後端、和前端的工作內容:
  1. SDK:負責Hprof文件的採集、裁剪、壓縮及上報等
  2. 服務端:Hprof文件存儲、還原、自動分析、結果Retrace、issue聚合,自動分配
  3. 前端:問題展示包括內存泄露、大對象、類大對象
方案流程圖
這個圖比較清晰的介紹了方案的整個流程,業務方只需要接入SDK,就可以在平臺查看核心內存問題,其他都是無感知的。

3.3 方案原理

下面會講解方案核心流程的原理

3.2.1 內存文件端上dump

OOM 時候dump:
SDK 默認是在Java OOM 時dump內存快照。端SDK會註冊主進程的 UncaughtExceptionHandler,同時判斷是 Java OOM異常 ,然後會進行內存快照的 dump 操作。
Android中可以通過Debug.dumpHprofData()獲取到一個Hprof文件,也支持使用Tailor通過xHook在 native 層 hook dump 同時裁剪的方式。
  • OOM之後還要再進行 dump 操作確實會容易dump失敗。
  • OOM時候App崩潰不可用,dump操作會在崩潰時候導致卡頓。
內存觸頂子進程dump:
通過fork系統調用創建子進程,這樣子進程就有父進程的拷貝,我們把耗時的dump操作在子進程做就可以了。這樣就提高了dump的成功率,也對App用戶交互無感知。當前也支持在平臺配置內存觸頂子進程dump的模式。內存觸頂是指當前內存使用佔最大內存的比例,默認是80%,支持配置。
當前默認依然使用Java OOM時候dump,因爲這時更能還原內存嚴重不當使用的真實場景。

3.2.2 內存文件的裁剪和還原

裁剪的原因:
  • 規避隱私風險:Hprof保存了執行Dump時刻Java堆上所有的內存信息,包括存在內存中的賬戶信息等,這些敏感信息必須裁剪掉。
  • 減小文件大小:因爲堆內存不足而OOM的時候獲到的Hprof文件,約等於設備單進程最大可用內存,一般文件比較大有幾百M,大文件上傳浪費用戶流量、帶寬以及導致上報成功率降低。
裁剪還原原理:
分析解決Java OOM問題,我們主要關心對象的大小,以及它的引用鏈。對於Hprof裏面的更多信息,例如圖片像素數據,具體的字符串內容等我們並不關注,而且屬於隱私數據,這部分數據是我們可以裁剪的。
  1. 根據Hprof文件的格式進行分析,分析我們不需要關注的數據塊
  2. 將文件映射進內存,根據文件格式找到想要裁剪的數據塊
  3. 再次寫入文件的時候不要寫第2步找到的數據塊
  4. 這樣產生的Hprof文件就是裁剪後的
實際裁剪掉的數據主要包括 String 的數組以及 Bitmap 對應的 mBuffer 數組(像素信息),這兩部分涉及敏感信息且佔據空間較大。其他更多裁剪內容不再詳細說明。
上報到服務器的裁剪後Hprof文件,根據我們已知的裁剪方式,對裁剪的內容進行空字符填充還原。還原後的Hprof文件格式和裁剪之前相同。並不影響MAT等工具進行內存分析。
 
裁剪效果:
隱私安全:裁剪後的字符串和圖片像素等數據已經爲空
 
Hprof文件大小變化明顯: 頭條裁剪前後數據平均值對比 355M-> 44M

3.2.3 內存文件的自動化解析

當服務端接收到上報的內存快照之後會進行自動的分析,直接定位內存核心問題,分析之後的結果主要包含三部分:
  • 內存泄露
  • 大對象
  • 小對象
分析 Hprof 文件需要首先將 Hprof 文件按照格式進行解析, 並根據解析後數據構建引用關係圖
我們參考業界已經存在的Hprof解析實現,包括MAT,LeakCanary等,實現了 一套Hprof內存快照自動解析庫
下面講解這三部分內容是如何定義的,如何解析的,解析了哪些數據用來歸因,平臺效果如何。

3.2.3.1 內存泄露

內存泄露是在計算機中,由於疏忽或錯誤造成程序未能釋放已經不再使用的內存,是需要修復的問題。
例如Activity生命週期已經結束,執行了onDestroy(),但是依然存在到GC Root的引用鏈,導致Activity無法被GC回收。這個Activity 就可以認定爲內存泄露。
根據 Retained Size大小我們可以判斷Activity的 泄露問題嚴重程度,越大越應該被優先解決。
根據 GC引用鏈我們可以判斷這個 Activity泄露的原因,被誰持有導致的泄露,確認如何解決。
如何判定泄露:
通過分析 Activity 的源碼發現Activity調用 onDestroy 之後一個變量的值會發生變化,通過這個變量我們可以判斷 Activity 是不是走了 onDestroy,如果走了那說明這個 Activity 對象存在屬於泄露,沒有走則說明屬於正常使用。
private boolean mDestroyed;

final void performDestroy() {

    mDestroyed = true;

    xxx

}
通過Hprof解析庫找出Activity的實例,並對其mDestroyed屬性進行判斷是否爲true。這樣就找到了泄露的Activity。
引用鏈和Retained大小:
找到了泄露對象之後我們需要知道它究竟是被誰引用導致不能釋放,上文已經介紹 Java 的垃圾回收機制通過可達性分析算法判斷對象是否存活,一個對象能不能被回收就看 GC Root 到它之間有沒有強引用鏈。泄露的對象和 GCRoot 之間必然是存在強引用鏈。
根據Hprof中的實例信息解析成描述引用關係的圖結構後,使用經典的圖搜索算法即可找到泄漏的對象到 GCRoot 的強引用鏈了,同時計算出對象的Retained Size大小。
泄露展示效果:
泄露類和導致它泄露的 引用鏈非常直觀展現了出來,可以通過 斷掉引用鏈來解決泄露
所有發現的泄漏問題都應該被解決修復,上面的case是因爲靜態變量持有了Activity導致,這裏的mContext可以通過替換爲Application來解決。

 

3.2.3.2 大對象

大對象:顧名思義就是比較大的對象,前面背景知識裏說的 RetainSize 較大的對象,也就是釋放掉之後總共可以回收的較大的對象。
大對象標準:目前判斷的依據是 RetainSize 大於 1 M的對象會被當做大對象,然後去找引用鏈。
if (object != null && object.getRetainedHeapSize() > MINIMAL){

    // 算作大對象

 }
同時我們會計算大對象持有了誰導致他比較大,也是通過大對象持有的變量來計算判斷。
大對象展示效果:
根據引用鏈判斷這個大對象被誰持有引用,是否屬於泄露可以修復。如果屬於正常使用,判斷大對象持有對象有誰,是否緩存過大可以清除部分緩存。下圖可以看到內存緩存過大,Retained Size達到210M可以清除部分緩存優化。
大對象往往是導致Java OOM的核心問題,關注高頻出現的Retained Size超大的大對象,優化後對Java OOM有非常好的優化效果。

 

3.2.3.3 類大對象

一個對象雖然比較小,但是它特別多,對象加在一起比較大也是需要我們重點關注的。例如一個對象只佔10kb,但是如果內存裏有2000個對象實例,總的內存佔用也是特別大的。
當前類大對象的默認定義:對象實例數量超過10,Retained Size超過20M的類。
我們會解析出這部分類大對象,然後計算出他們的引用鏈。
類大對象展示效果:
上面的case是說類ArticleCell的對象有364個,總Retained Size 是51.29M。其中280個被MainActivity所持有。所以如果要優化ArticleCell的內存佔用,可以優化MainActivity裏面的引用。

3.2.4 聚合和Retrace

3.2.4.1 聚合

通過聚合我們可以找到同類問題,並把高頻問題體現出來優先解決,達到四兩撥千斤的效果。
泄露是通過泄露類和引用它的業務代碼作爲聚合特徵來進行聚合。
大對象是通過大對象類和引用它的業務代碼作爲聚合特徵來進行聚合。
類大對象是根據類名來進行聚合。
泄露的聚合效果如下,可以根據排序直接定位到高頻泄露的Acitvity。

3.2.4.2 Retrace

類名和引用鏈Retrace:
Hprof文件是混淆後的數據,對於解析出來的類名和引用鏈可以和崩潰一樣通過符號表進行自動解析還原。
Hprof文件Retrace:
爲了更好的分析單點問題,平臺也提供了單點自動化分析數據展示和單點原始Hprof文件下載的能力。
 
對於下載下來的Hprof原始文件也是混淆後的,客戶端分析非常不友好,是否可以把Hprof文件也進行反混淆還原呢。
當然可以,當前開發了一個Hprof文件Retrace工具,可以解析Hprof文件,讀取類、變量和方法等數據,根據符號表還原成Retrace後的Hprof文件,線下分析更方便。
通過平臺下載下來的Hprof就是經過填充還原Hprof結構,並且自動Retrace後的Hprof文件。
解析前:
解析後:

3.2.5 自動分配

對於分析出來的問題,只分析出來還不足夠,並沒有實現閉環,我們需要通知到相應的同學去解決纔可以,否則需要有同學來手動分配線上問題,比較浪費精力。
因此需要有自動分配的能力,內部通過解析聚合後issue的泄露Class,去代碼倉庫或者根據配置找這個 Class 的Owner,發送 Lark 通知給這位同學。
 
當前在火山引擎的MARS-APMPlus 應用性能監控無法獲取用戶的倉庫解析Class Owner,暫時無法自動分配。

3.2.6 總結

以上就是這套 基於Hprof內存快照的線上 Java OOM 歸因方案的原理介紹,這套方案實現了 高度場景還原自動化內存分析自動聚合Retrace、並實現了 隱私安全。
接入後優先分析解決聚合後的TOP問題,包括頻繁的泄露和頻繁出現的大對象,對Java OOM指標會有非常明顯的優化效果。
 

四、優化效果

4.1 內部效果

當前該解決方案已在字節內部廣泛應用。包括頭條抖音在內的數十個App業務方在接入後,Java OOM均有明顯優化。以Helo爲例,一個雙月內 優化了80+%的Java OOM問題,次日存留增長了2+%,效果顯著。

4.2 外部效果

當前這套方案已經在 火山引擎 MARS-APM Plus應用性能監控上線,美篇作爲早期客戶,在一個雙月的優化後 Java OOM 降低了80%用戶卡頓率也下降了80%,優化效果非常明顯。

五、接入使用

MARS-APM Plus應用性能監控企業助力行動MARS-APM Plus 當前開始了企業助力行動,可以免費試用,歡迎註冊試用產品,發現並解決Java OOM問題。MARS-APM Plus除了支持對App進行監控,也支持對SDK進行穩定性監控和自定義事件打點。
進羣:掃碼進羣,會有同學對接如何開通 MARS-APM Plus應用性能監控服務。
MARS-APM Plus 應用性能監控爲企業提供 針對應用的品質、性能以及自定義 埋點 APM  應用性能監控服務,幫助團隊打造極致的用戶體驗。基於海量數據的聚合分析,平臺可幫助客戶發現多類異常問題,並及時報警,做分配處理,同時平臺提供了豐富的歸因能力,包括且不限於異常分析、多維分析、自定義上報、單點日誌查詢等,結合靈活的報表能力可瞭解各類指標的趨勢變化。APM Plus 已服務了抖音、今日頭條、TikTok 等多個超大規模用戶量級移動 App。
 
 
當前講解的Java OOM解決方案,只是MARS-APM Plus 應用性能監控的一個功能模塊,還有更多的能力會在後續進一步講解,也歡迎同學搶先接入試用。
 

🔥  火山引擎 APMPlus 應用性能監控是火山引擎應用開發套件 MARS 下的性能監控產品。我們通過先進的數據採集與監控技術,爲企業提供全鏈路的應用性能監控服務,助力企業提升異常問題排查與解決的效率。目前產品正在免費公測階段,👉  戳這裏瞭解更多產品信息。歡迎大家進行試用!
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章