天天看點

Android OOM問題及優化總結OOM和記憶體優化總結

業精于勤荒于嬉,寫文章練習表達能力,寫代碼練習基本工。

OOM和記憶體優化總結

什麼是OOM?

OOM 即 (java.lang.OutOfMemoryError), JVM沒有足夠記憶體給對象配置設定空間,超過jvm的堆空間最大值(-Xmx參數),此異常就會被觸發,導緻應用強制被殺死。

OOM原因?

對于java程式員來說,我們一般隻管建立對象,而對象的回收,我們很少操心,是因為JVM有垃圾回收器來定期執行GC,負責回收已經引用不到的無用對象來釋放記憶體。不過,我們的應用還是出現記憶體洩露或者記憶體溢出,這也是導緻oom的兩大因素。

記憶體洩露和記憶體溢出?

  • 記憶體洩露,是指之前配置設定的記憶體空間,不再使用,但也無法被垃圾回收器釋放,這種情況如果一直堆積,會導緻記憶體溢出。
  • 記憶體溢出,沒有足夠的記憶體來配置設定空間了,空間不夠了。

OOM發生在哪個區域?

我們先來認識一些jvm記憶體模型:

Android OOM問題及優化總結OOM和記憶體優化總結
按照JVM的規範,除了程式計數器區,其他區域都可能出現OOM。

jvm 記憶體配置設定與回收機制

我們平時寫java代碼,絕大部分的OOM都發生在堆區,是以着重了解一下堆空間對象的記憶體模型和回收機制。

Android OOM問題及優化總結OOM和記憶體優化總結
  • 堆空間劃分為,新生代(eden + S0 + S1), 老年代(Tenured)和永久代(Permanent),永久代java1.8已經取消,改為元空間。
  • 新建立的對象基本都配置設定在新生代eden區,大對象直接配置設定到老年代。
  • 一次GC後,eden區仍然存活的對象被移到S0;S0記憶體活的對象移到S1;S1記憶體活的對象被移到Tenured
  • 每一次GC,對象年齡增長一歲,到達15歲(預設,可設定),晉升到老年代。

對象被回收的判斷算法

  • 可達性分析 :以 GC Root 為分析的起點 , 查找對象的引用 , 如果找到一個對象 , 無法被 GC Root 直接或間接引用到 , 那麼該對象就可以被回收了 。
Android OOM問題及優化總結OOM和記憶體優化總結
  • 引用計數法 :給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1。

垃圾回收機制(GC)

垃圾回收算法:

  • 标記-清除
  • 标記-整理
  • 複制

垃圾回收主要在新生代和老年代工作,在分代收集模型中(上圖),不同的分代采用不同的收集算法:

  • eden和Survivor:通常是複制算法
  • Tenured:通常是标記整理算法,android是CMS

Android App 的記憶體限制

Android OOM問題及優化總結OOM和記憶體優化總結
  • Java Heap :Java申請的記憶體,受 vm.heapsize 大小限制。
  • native Heap : c/c++層申請的記憶體,不受 vm.heapsize 大小限制。

如何修改:

代碼位置 /frameworks/base/core/jni/AndroidRuntime.cpp
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote, bool primary_zygote)
{

/*
     * The default starting and maximum size of the heap.  Larger
     * values should be specified in a product property override.
     */
    parseRuntimeOption("dalvik.vm.heapstartsize", heapstartsizeOptsBuf, "-Xms", "4m");
    parseRuntimeOption("dalvik.vm.heapsize", heapsizeOptsBuf, "-Xmx", "16m");

    parseRuntimeOption("dalvik.vm.heapgrowthlimit", heapgrowthlimitOptsBuf, "-XX:HeapGrowthLimit=");
  
}
           

Android 抛出OOM的源頭

堆配置設定失敗

代碼位置:/art/runtime/gc/heap.cc
void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type) {
  // If we're in a stack overflow, do not create a new exception. It would require running the
  // constructor, which will of course still be in a stack overflow.
  if (self->IsHandlingStackOverflow()) {
    self->SetException(Runtime::Current()->GetPreAllocatedOutOfMemoryError());
    return;
  }

  std::ostringstream oss;
  size_t total_bytes_free = GetFreeMemory();
  oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free
      << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM";
  // If the allocation failed due to fragmentation, print out the largest continuous allocation.
  if (total_bytes_free >= byte_count) {
    space::AllocSpace* space = nullptr;
    if (allocator_type == kAllocatorTypeNonMoving) {
      space = non_moving_space_;
    } else if (allocator_type == kAllocatorTypeRosAlloc ||
               allocator_type == kAllocatorTypeDlMalloc) {
      space = main_space_;
    } else if (allocator_type == kAllocatorTypeBumpPointer ||
               allocator_type == kAllocatorTypeTLAB) {
      space = bump_pointer_space_;
    } else if (allocator_type == kAllocatorTypeRegion ||
               allocator_type == kAllocatorTypeRegionTLAB) {
      space = region_space_;
    }
    if (space != nullptr) {
      space->LogFragmentationAllocFailure(oss, byte_count);
    }
  }
  self->ThrowOutOfMemoryError(oss.str().c_str());
}

           

常用的記憶體分析指令

adb shell dumpsys meminfo [package name]

Android OOM問題及優化總結OOM和記憶體優化總結

adb shell procrank

記憶體占用排行榜,沒試成功,需要自己上傳sh。

adb shell cat /proc/meminfo

Android OOM問題及優化總結OOM和記憶體優化總結

adb shell free

Android OOM問題及優化總結OOM和記憶體優化總結

total = used + free

adb shell top

Android OOM問題及優化總結OOM和記憶體優化總結

記憶體洩露分析工具

Memory Analyzer Tool

下載下傳位址

1,抓取記憶體申請快照hprof檔案

adb shell am dumpheap com.xxx.xxx /data/local/tmp/14_54.hprof

2,轉換為标準hprof

hprof-conv 14_54.hprof 14_54_R.hprof

hprof-conv在sdk的platform-tools目錄中

3,使用mat打開

Android OOM問題及優化總結OOM和記憶體優化總結

首頁會有目前記憶體洩露的猜想,大對象,數量多的對象會被置為懷疑目标。

分析記憶體洩露

大對象分析

點選 Overview - Dominator ,對象按照占用空間大小排序

Android OOM問題及優化總結OOM和記憶體優化總結

重點排查排在上面的大對象,下圖中,很明顯竟然有多個相同activity,且保持引用了大對象,無法釋放,造成記憶體洩露。

Android OOM問題及優化總結OOM和記憶體優化總結
GC Root 引用鍊檢視

上圖中,選擇一項,右鍵 -> Paths to GC Roots–>exclude all phantom/weak/soft etc reference ,重點分析強引用。

Android OOM問題及優化總結OOM和記憶體優化總結

可以看到context引用鍊,定位到自己的代碼裡面,本例是一個類靜态持有了Activity的context,Activity銷毀後,context無法釋放造成記憶體洩露。

直方圖比較

如果問題是記憶體随着時間緩慢增長,單個hprof看不出來,那麼就隔一段時間再去一次快照,對兩個快照檔案進行比較,看那個對象一直在增長。

Android OOM問題及優化總結OOM和記憶體優化總結

之前分析問題的hprof找不到了,臨時找了兩個,上面這個圖檔僅供步驟參考,具體以實際操作為準。

Android Profile

打開Profile頁面,選擇監控的程式,點選MEMORY–>dump按鈕。

Android OOM問題及優化總結OOM和記憶體優化總結

同樣在android studio上可以檢視hprof進行分析。

Android OOM問題及優化總結OOM和記憶體優化總結

LeakCanary

github位址

引用

dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'
}
           

調用

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return;
        }
        LeakCanary.install(this);
    }
}
           

例子

public class MainActivity extends AppCompatActivity {

    private static int k = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(runnable).start();
    }


    public Runnable runnable = new Runnable() {
        @Override
        public void run() {
            while (true){
                MainActivity.k++;
                Log.d("TAG",""+k);
            }

        }
    };

}
           

發生記憶體洩露,進入這個頁面直接檢視。

Android OOM問題及優化總結OOM和記憶體優化總結

這個圖檔尺寸怎麼縮小?哭

記憶體洩露,記憶體溢出常見場景和解決方案總結

以下摘抄自他人總結的,在這裡做備份。

1、資源未關閉

對于資源性對象不再使用時,應該立即調用它的close()函數,将其關閉,然後再置為null。例如Bitmap

等資源未關閉會造成記憶體洩漏,此時我們應該在Activity銷毀時及時關閉。

2、注冊未登出

例如BraodcastReceiver、EventBus未登出造成的記憶體洩漏,我們應該在Activity銷毀時及時登出。

3、類的靜态變量持有大資料對象

盡量避免使用靜态變量存儲資料,特别是大資料對象,建議使用資料庫存儲。

4、單例造成的記憶體洩漏

優先使用Application的Context,如需使用Activity的Context,可以在傳入Context時使用弱引用進行封

裝,然後,在使用到的地方從弱引用中擷取Context,如果擷取不到,則直接return即可。

5、非靜态内部類的靜态執行個體

該執行個體的生命周期和應用一樣長,這就導緻該靜态執行個體一直持有該Activity的引用,Activity的記憶體資源

不能正常回收。如果需要使用Context,盡量使用Application Context。

6、Handler臨時性記憶體洩漏

Message發出之後存儲在MessageQueue中,在Message中存在一個target,它是Handler的一個引

用,Message在Queue中存在的時間過長,就會導緻Handler無法被回收。如果Handler是非靜态的,

則會導緻Activity或者Service不會被回收。并且消息隊列是在一個Looper線程中不斷地輪詢處理消息,

當這個Activity退出時,消息隊列中還有未處理的消息或者正在處理的消息,并且消息隊列中的Message

持有Handler執行個體的引用,Handler又持有Activity的引用,是以導緻該Activity的記憶體資源無法及時回

收,引發記憶體洩漏。解決方案如下所示:

​ 1、使用一個靜态Handler内部類,然後對Handler持有的對象(一般是Activity)使用弱引用,這

樣在回收時,也可以回收Handler持有的對象。

​ 2、在Activity的Destroy或者Stop時,應該移除消息隊列中的消息,避免Looper線程的消息隊列中

有待處理的消息需要處理。

需要注意的是,AsyncTask内部也是Handler機制,同樣存在記憶體洩漏風險,但其一般是臨時性的。對于

類似AsyncTask或是線程造成的記憶體洩漏,我們也可以将AsyncTask和Runnable類獨立出來或者使用靜

态内部類。

7、容器中的對象沒清理造成的記憶體洩漏

在退出程式之前,将集合裡的東西clear,然後置為null,再退出程式

8、WebView

WebView都存在記憶體洩漏的問題,在應用中隻要使用一次WebView,記憶體就不會被釋放掉。我們可以為

WebView開啟一個獨立的程序,使用AIDL與應用的主程序進行通信,WebView所在的程序可以根據業

務的需要選擇合适的時機進行銷毀,達到正常釋放記憶體的目的。

9、使用ListView時造成的記憶體洩漏

在構造Adapter時,使用緩存的convertView。

如有錯誤,請幫忙指出。

參考

Android記憶體優化之OOM

使用者指南 - android app性能優化大彙總(記憶體性能優化)

使用記憶體性能分析器檢視應用的記憶體使用情況

【Android 記憶體優化】記憶體抖動 ( 垃圾回收算法總結 | 分代收集算法補充 | 記憶體抖動排查 | 記憶體抖動操作 | 集合選擇 )

JVM記憶體管理:深入Java記憶體區域與OOM