前言
1.問題及現象
線上日誌反饋記憶體溢位問題。根據使用者反饋,客戶操作一段時間之後,APP 記憶體溢位崩潰。
2.分析過程
(1) 分析線上日誌,發現主要分兩種:
第一種如下,可能是某個死迴圈導致記憶體不停增大導致:
java.lang.OutOfMemoryError: OutOfMemoryError thrown while trying to throw OutOfMemoryError; no stack trace available
第二種如下,壓死駱駝的最後一根稻草:
java.lang.OutOfMemoryError: Failed to allocate a 16 byte allocation with 0 free bytes and 3GB until OOM, max allowed footprint 536872136, growth limit 536870912
這兩種情況下都無法定位問題所在。
通過觀察發生的版本資訊,是從V9.5.6開始的,於是就開始查那期改動的功能(主要是webview優化,其他的是SDK更新),由於沒有線上出問題的機型,就找個類似的華為機器操作webview,發現記憶體增長並不快,操作很長時間記憶體變化不大。
(2) 分析使用者操作路徑
讓工程匯出出現OOM問題使用者最近的操作記錄,埋點,頁面停留,崩潰日誌,觀察進入了哪些頁面,但是照著操作路徑操作,也沒發現崩潰。
(3) 統計出現問題的機型,前五都是華為的,系統版本7.0以上,記憶體3GB以上。
(4) 使用LeakCanary + Android Profiler排查洩漏的地方,檢視線上日誌的時候,偶然發現測試有一部手機出現了問題,華為P8,然後使用工具根據使用者操作路徑查各個模組洩漏問題,發現委託登入有個持續的10M洩漏,比較大的洩漏還有開屏廣告頁和行情登入頁面34M。這個手機給APP分配的可用記憶體最大為256M,操作幾次登入頁面就崩潰了。
開屏頁:
private ScheduledFuture<?> mTaskFuture = null;
private void gotoMain() {
...
AdCountDownTask adCountDownTask = new AdCountDownTask();
HexinThreadPool.cancelTaskFutre(mTaskFuture, true);
mTaskFuture = HexinThreadPool.getThreadPool().sheduleWithFixedDelay(xxx);
handler.sendEmptyMessageDelayed(xxx, xxx);
}
protected void onDestory() {
...
if (mTaskFuture != null) {
HexinThreadPool.cancelTaskFutre(mTaskFuture, true);
mTaskFuture = null;
}
}
複製程式碼
委託登入
public class AdsCT {
private Runnable runable = new Runnable {
// 輪播圖片
handler.postDelay(runnable, time);
}
public void onRemove() {
handler.removeCallbacksAndMessages(null);
}
}
public class WeituoLogin {
public void onRemove() {
adsCt.onRemove();
}
}
複製程式碼
(5) 修改查出來比較大的記憶體洩漏,券商給之前出問題的兩個使用者單獨安裝,發現還是存在,券商反饋有個使用者安裝9.4.5的一直都沒問題,升級到最新之後就出現問題了,現在懷疑還是9.4.6哪個模組導致的。
一:OOM是什麼?
全稱“Out Of Memory”,翻譯成中文就是“記憶體用完了”,來源於java.lang.OutOfMemoryError。
二:OOM是哪裡報錯的?
Java記憶體模型
JVM記憶體結構主要有三大塊:堆記憶體、方法區和棧。堆記憶體是JVM中最大的一塊由年輕代和老年代組成,而年輕代記憶體又被分成三部分,Eden空間、From Survivor空間、To Survivor空間,預設情況下年輕代按照8:1:1的比例來分配;方法區儲存類資訊、常量、靜態變數等資料,是執行緒共享的區域,為與Java堆區分,方法區還有一個別名Non-Heap(非堆);棧又分為java虛擬機器棧和本地方法棧主要用於方法的執行。
三:為什麼會發生OOM?
概念
記憶體洩露:申請使用完的記憶體沒有釋放,導致虛擬機器不能再次使用該記憶體,此時這段記憶體就洩露了,因為申請者不用了,而又不能被虛擬機器分配給別人用。
記憶體溢位:申請的記憶體超出了JVM能提供的記憶體大小,此時稱之為溢位。
GC機制
可達性分析(Reachability Analysis):從GC Roots開始向下搜尋,搜尋所走過的路徑稱為引用鏈。當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。不可達物件。
申請一塊記憶體時,如果當前可用記憶體不足,則會出發一次GC,然後再次申請,此時如果記憶體還不足,則會丟擲OOM。
四:OOM常見例項和解決方案
1.內部類持有外部引用-Handler,Context
// 錯誤示例,內部類持有外部物件導致洩漏
private class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
}
// 修改後,在activity onDestory()的時候,呼叫myHandler.removeCallbacksAndMessages(null);
private static class MyHandler extends Handler {
private WeakReference<MainActivity> mainActivityWeakReference;
public MyHandler(MainActivity mainActivity) {
mainActivityWeakReference = new WeakReference<MainActivity>(mainActivity);
}
@Override
public void handleMessage(Message msg) {
MainActivity mainActivity = mainActivityWeakReference.get();
if (mainActivity == null) {
return;
}
mainActivity.jump(null);
super.handleMessage(msg);
}
}
// context被執行緒持有
private void registerException(final Context context) {
Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
String appName = context.getResources().getString(R.string.app_name);
}
};
}
複製程式碼
jvm四種引用
strong soft weak phantom(其實還有一種FinalReference,這個由jvm自己使用,外部無法呼叫到),主要的區別體現在gc上的處理,如下:
Strong型別,也就是正常使用的型別,不需要顯示定義,只要沒有任何引用就可以回收
SoftReference型別,如果一個物件只剩下一個soft引用,在jvm記憶體不足的時候會將這個物件進行回收
WeakReference型別,如果物件只剩下一個weak引用,那gc的時候就會回收。和SoftReference都可以用來實現cache
PhantomReference型別,
2.資源沒有關閉-資料庫,流
private void testStream() {
InputStream in = null;
try {
in = new FileInputStream("xxx.xml");
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
in = null;
}
}
}
複製程式碼
3.靜態變數持有物件
單例持有Context,監聽器等
五:OOM檢查工具-LeakCanary原理
1.監聽物件
Activity的監聽,是在Activity onDestory()執行之後再看是否被回收,在Application建立的時候,註冊了監聽
Application.ActivityLifecycleCallbacks lifecycleCallbacks = new..{
...
@Override public void onActivityDestroyed(Activity activity) {
ActivityRefWatcher.this.onActivityDestroyed(activity);
}
}
複製程式碼
工程中檢視Page是否被釋放,可以在onRemove()的時候使用
RefWatcher watch(Object obj)
複製程式碼
2.如何判斷物件是否被釋放?
使用的WeakReference+ReferenceQueue實現
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);
複製程式碼
如果軟引用或弱引用所引用的物件被垃圾回收器回收,Java虛擬機器就會把這個軟引用或弱引用加入到與之關聯的引用佇列中,由此可判斷物件是否被回收。
3.怎麼讓系統回收物件?
@Override public void runGc() {
Runtime.getRuntime().gc();
enqueueReferences();
System.runFinalization(); //強制呼叫已經失去引用的物件的finalize方法,確保釋放例項佔用的全部資源。
}
private void enqueueReferences() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new AssertionError();
}
}
};
複製程式碼
4.如何分析物件洩漏?
(1) 對記憶體檔案進行分析,由於這個過程比較耗時,因此最終會把這個工作交給執行在另外一個程式中的HeapAnalyzerService來執行。
(2) 利用HAHA將之前dump出來的記憶體檔案解析成Snapshot物件,解析得到的Snapshot物件直觀上和我們使用MAT進行記憶體分析時候羅列出記憶體中各個物件的結構很相似,它通過物件之間的引用鏈關係構成了一棵樹,我們可以在這個樹種查詢到各個物件的資訊,包括它的Class物件資訊、記憶體地址、持有的引用及被持有的引用關係等。
(3) 為了能夠準確找到被洩漏物件,LeakCanary通過被洩漏物件的弱引用來在Snapshot中定位它。因為,如果一個物件被洩漏,一定也可以在記憶體中找到這個物件的弱引用,再通過弱引用物件的reference就可以直接定位被洩漏物件。
(4) 在Snapshot中找到一條有效的到被洩漏物件之間的引用路徑,從被洩露的物件開始,採用的方法是類似於廣度優先的搜尋策略,將持有它引用的節點(父節點),加入到一個FIFO佇列中,一層一層向上尋找,哪條路徑最先到達GCRoot就表示它應該是一條最短路徑。
(5) 將之前查詢的最短路徑轉換成最後需要顯示的LeakTrace物件,這個物件中包括了一個由路徑上各個節點LeakTraceElement組成的連結串列,代表了檢查到的最短洩漏路徑。最後一個步驟就是將這些結果封裝成AnalysisResult物件然後交給DisplayLeakService進行處理。這個service主要的工作是將檢查結果寫入檔案,以便之後能夠直接看到最近幾次記憶體洩露的分析結果,同時以notification的方式通知使用者檢測到了一次記憶體洩漏。使用者還可以繼承這個service類來並實現afterDefaultHandling來自定義對檢查結果的處理,比如將結果上傳剛到伺服器等。
六:問題
1.OOM可以被捕獲嗎?
某些情況下是可以的,比如說區域性申請一個很大的記憶體,如果造成了OOM,在catch的地方釋放掉,程式還是可以繼續往下執行的,但是如果捕獲之後釋放不掉,也還是會崩潰。
2.內部類為什麼能訪問外部類成員?
編譯器會預設為成員內部類新增了一個指向外部類物件的引用,那麼這個引用是如何賦初值的呢?
public com.xxx.Outter$Inner(com.cxh.test2.Outter);
複製程式碼
我們在定義的內部類的構造器是無參構造器,編譯器還是會預設新增一個引數,該引數的型別為指向外部類物件的一個引用,所以成員內部類中的Outter this&0 指標便指向了外部類物件,因此可以在成員內部類中隨意訪問外部類的成員。從這裡也間接說明了成員內部類是依賴於外部類的,如果沒有建立外部類的物件,則無法對Outter this&0引用進行初始化賦值,也就無法建立成員內部類的物件了。
3.為什麼區域性內部類和匿名內部類只能訪問區域性final變數?
外部類和內部類編譯後會生成兩個.class檔案,如果區域性變數的值在編譯期間就可以確定,則直接在匿名內部裡面建立一個拷貝。如果區域性變數的值無法在編譯期間確定,則通過構造器傳參的方式來對拷貝進行初始化賦值。 拷貝的話,如果一個值改變了,就會造成資料不一致性,為了解決這個問題,java編譯器就限定必須將變數a限制為final變數,不允許對變數a進行更改(對於引用型別的變數,是不允許指向新的物件),這樣資料不一致性的問題就得以解決了。
4.靜態變數會被回收嗎?finalize用法?
private static StaticObj staticObj = new StaticObj();
static class StaticObj {
@Override
protected void finalize() throws Throwable {
staticObj = new StaticObj();
super.finalize();
}
}
複製程式碼
5.System.gc()和Runtime.getRuntime().gc()區別?
// System.gc()
/**
* If we just ran finalization, we might want to do a GC to free the finalized objects.
* This lets us do gc/runFinlization/gc sequences but prevents back to back System.gc().
*/
private static boolean justRanFinalization;
public static void gc() {
boolean shouldRunGC;
synchronized (LOCK) {
shouldRunGC = justRanFinalization;
if (shouldRunGC) {
justRanFinalization = false;
} else {
runGC = true;
}
}
if (shouldRunGC) {
Runtime.getRuntime().gc();
}
}
public static void runFinalization() {
boolean shouldRunGC;
synchronized (LOCK) {
shouldRunGC = runGC;
runGC = false;
}
if (shouldRunGC) {
Runtime.getRuntime().gc();
}
Runtime.getRuntime().runFinalization();
synchronized (LOCK) {
justRanFinalization = true;
}
}
複製程式碼
參考: