OOM的起點到終點

前言

1.問題及現象

線上日誌反饋內存溢出問題。根據用戶反饋,客戶操做一段時間以後,APP 內存溢出崩潰。html

2.分析過程

(1) 分析線上日誌,發現主要分兩種:java

第一種以下,多是某個死循環致使內存不停增大致使:web

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服務器

這兩種狀況下都沒法定位問題所在。app

經過觀察發生的版本信息,是從V9.5.6開始的,因而就開始查那期改動的功能(主要是webview優化,其餘的是SDK更新),因爲沒有線上出問題的機型,就找個相似的華爲機器操做webview,發現內存增加並不快,操做很長時間內存變化不大。jvm

(2) 分析用戶操做路徑ide

讓工程導出出現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;
        }
    }
複製代碼

參考:

Jvm 系列(二):Jvm 內存結構

理解StrongReference,SoftReference, WeakReference的區別

Java內部類詳解

性能優化工具(九)-LeakCanary

LeakCanary原理分析

相關文章
相關標籤/搜索