自動識別Android不合理的記憶體分配
寫在前面
Android開發中我們常常會遇到不合理的記憶體分配導致的問題,或是頻繁GC,或是OOM。按照常規的套路我們需要開啟Android Studio錄製記憶體分配或者dump記憶體,然後人工分析,逐個排查問題所在。這些方法是官方提供的能力,可以幫助我們排查問題,但難免有些繁瑣,效率比較低。
如果可以自動識別出不合理的Java(含Kotlin)物件分配,這樣繁瑣的工作將會變得簡單。
本文介紹了一種在Art虛擬機器上實時記錄物件分配的實現方案,基於此方案就可以實現不合理物件分配的自動化的識別。
常規方案對比分析
方案 | 優勢 | 不足 |
Dump記憶體 | 可以自動化 | 無法反映出記憶體分配的過程 |
錄製物件分配 | 可以看到每次記憶體分配的情況 | 需要手動啟動,無法自動化 |
位元組碼插樁 | 可以自動化 | 無法記錄不在業務程式碼內的記憶體分配 |
Dump記憶體和位元組碼插樁的方案都無法覆蓋執行過程中記憶體分配的過程,無法滿足自動識別的訴求。而錄製的方案目前主要的問題是,不能自動化,如果能實現錄製記憶體分配的自動化,就可以完成我們想要做的事情。
讓錄製物件分配自動化
1. 模仿
建立ADB連線、構造請求這些都是IDE做的事情,我們需要模擬IDE做這些事情嗎?不需要。我們只需要關注DdmVmInternal是怎麼做的即可,很幸運,Android系統原始碼的一段測試程式碼直接告訴了我們如何反射呼叫DdmVmInternal提供的能力,原始碼位置在<android src>/art/test/098-ddmc/src/Main.java,這裡程式碼就不貼了。
2. 轉折
最多隻能65535條記錄(size的型別是雙位元組無符號數)。
錄製時對效能影響很小,但每次獲取錄製記錄時特別慢(開發機實測JDWP封包5秒以上,解包處理10秒以上)。
每次獲取到的記錄可能有重複,要使用這個資料需要額外做合併去重的操作。
3. 突破
DdmVmInternal的實現是放在native層的,順藤摸瓜,我們找到了虛擬機器裡實現記憶體分配錄製的原始碼,此處是Android5.1的原始碼,其他版本有差異,後面會講到。
方案 | 優勢 | 不足 |
PLT Hook | 修改PLT表的跳轉地址,風險低,易操作 | 使用場景有限,只能Hook一些被外部呼叫的函式 |
Inline Hook | 彙編指令級別修改,幾乎能修改所有邏輯 | 修改彙編指令涉及繁瑣的指令修復工作,有一定門檻 |
讓物件分配可被跟蹤
1. 分配了多大記憶體
2. 這是什麼物件
你也許已經發現RecordAllocation還有一個引數是art::mirror::Class*,這是Java裡Class在虛擬機器裡的映象,我們知道Java裡拿到Class,就能直接呼叫getName方法知道這個類是什麼。然鵝,在虛擬機器的原始碼裡,GetName函式有是有,但是是行內函數,我們沒有辦法拿到這個函式的地址。
這個咋整?不要方,我們繼續看原始碼,就在不遠處,有一個叫個GetDescriptor的函式。
3. 它是怎麼分配的
要知道一個物件是怎麼分配的,我們需要拿到它的呼叫棧,Ok,我們來看看虛擬機器裡面怎麼做的。
古人說“山重水複疑無路,柳暗花明又一村”。既然原始碼層面不能給我們更多的啟示了,那回頭想想平時會怎麼做。是的,我們在寫Java程式碼的時候,如果要獲得當前的呼叫棧,一般就直接Thread.currentThread().getStackTrace()。既然這麼容易,那我們直接在native層透過jni呼叫java的方法不就可以拿到呼叫棧了嗎?事實也正是如此。於是,整個流程順下來就是這樣的。
4. 發現不合理的物件分配
全版本支援
是否可以把前面的方案直接應用在Android 6.x-9.x呢?答案是沒那麼容易。我們先來看下後續版本虛擬機器裡的一些改動。
1. 繞過so訪問許可權問題
1.1 獲得so基址
我們知道,Android是基於Linux的作業系統,Linux作業系統每個程式都有一個maps檔案記錄了所有模組在記憶體裡起始地址,路徑是/proc/<pid>/maps,這裡pid就是程式的pid,訪問自己程式用別名/proc/self/maps也可以。這個檔案很關鍵,我們看看它裡面是什麼。
1.2 搜尋函式地址 之 函式特徵
1.3 搜尋函式地址 之 解析ELF
2. 突如其來的SIGILL
這就是剛好大1的原因。我們看到IDA反編譯出來的RecordAllocation函式也可以清楚的看到,確實一條指令是2個位元組,所以我們在實現的時候,要把搜尋出來的地址做加1的修正。
3. 透過art::mirror::Object獲取類名
mirror::Object是Java裡Object在虛擬機器的映象,那我們是否有辦法透過mirror::Object拿到Java的Object的引用呢?透過搜尋以mirror::Object作為引數的函式,我找到了突破口。
業務實踐
我們的業務已經開始嘗試用NewMonkey做自動化測試,檢測到不合理的分配記憶體的場景,就記錄並上報。
參考文章
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559354/viewspace-2655900/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 簡單理解動態記憶體分配和靜態記憶體分配的區別記憶體
- 動態記憶體分配記憶體
- javascript堆疊記憶體分配的區別JavaScript記憶體
- C++ 動態記憶體分配C++記憶體
- 動態分配記憶體地址(.NET)記憶體
- C++動態記憶體分配C++記憶體
- 自動共享記憶體管理 自動記憶體管理 手工記憶體管理記憶體
- Android O 8.0 以上 bitmap記憶體分配Android記憶體
- C語言(動態記憶體分配)C語言記憶體
- JavaScript記憶體分配JavaScript記憶體
- JVM記憶體分配JVM記憶體
- java記憶體分配Java記憶體
- 記憶體動態分配與釋放,malloc和new區別記憶體
- 垃圾收集器與記憶體分配策略_記憶體分配策略記憶體
- 記憶體分配的確定記憶體
- weblogic的記憶體分配Web記憶體
- 記憶體分配策略中,堆和棧的區別記憶體
- 記憶體的分配與釋放,記憶體洩漏記憶體
- JVM 記憶體模型 記憶體分配,JVM鎖JVM記憶體模型
- C++ 指標動態記憶體分配C++指標記憶體
- 自動記憶體管理記憶體
- 小計:引用型別記憶體分配問題型別記憶體
- 探索iOS記憶體分配iOS記憶體
- Java 記憶體分配策略Java記憶體
- java jvm 記憶體分配JavaJVM記憶體
- [C++]記憶體分配C++記憶體
- Java記憶體分配和String型別的深度解析Java記憶體型別
- oracle的自動記憶體管理Oracle記憶體
- C語言的記憶體分配C語言記憶體
- 物件的建立與記憶體分配物件記憶體
- go是如何分配記憶體的?Go記憶體
- 控制C++的記憶體分配C++記憶體
- 記憶體分配的隱藏成本記憶體
- Oracle的記憶體分配和使用Oracle記憶體
- C中的記憶體分配模型記憶體模型
- linux記憶體管理(一)實體記憶體的組織和記憶體分配Linux記憶體
- malloc,calloc,realloc等記憶體分配函式區別記憶體函式
- Android 記憶體抖動Android記憶體