Android—内存泄漏、GC及LeakCanary源码解析
內存抖動:內存頻繁的分配和回收,頻繁的GC會導致UI卡頓,嚴重的時候導致OOM。
內存泄露:程序在向系統申請分配內存空間后(new),在使用完畢后未釋放。結果導致一直占據該內存單元,我們和程序都無法再使用該內存單元,直到程序結束,這是內存泄露。
內存溢出(OOM):程序向系統申請的內存空間超出了系統能給的。比如內存只能分配一個int類型,我卻要塞給他一個long類型,系統就出現oom。又比如一車最多能坐5個人,你卻非要塞下10個,車就擠爆了。
大量的內存泄露會導致內存溢出(oom)。
引起內存泄漏的情況
1.對于使用了BraodcastReceiver,ContentObserver,File,游標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷毀時及時關閉或者注銷。
- 比如在Activity中register了一個BraodcastReceiver,但在Activity結束后沒有unregister該BraodcastReceiver。
- 資源性對象比如Cursor,Stream、File文件等往往都用了一些緩沖,我們在不使用的時候,應該及時關閉它們,以便它們的緩沖及時回收內存。它們的緩沖不僅存在于 java虛擬機內,還存在于java虛擬機外。如果我們僅僅是把它的引用設置為null,而不關閉它們,往往會造成內存泄漏。
- 對于資源性對象在不使用的時候,應該調用它的close()函數將其關閉掉,然后再設置為null。在我們的程序退出時一定要確保我們的資源性對象已經關閉。
2.內部類持有外部類引用,Handler,Runnable匿名內部類導致內存泄露,內部類天然持有外部類的實例引用,activity finish掉了,那么消息隊列的消息依舊會由handler進行處理,若此時handler聲明為內部類,那么就會導致activity無法回收,進而導activity泄露。??
- 在onDestroy方法中調用Handler的removeCallbacksAndMessages方法,清空消息。
- 靜態內部類不持有外部類的引用,使用靜態的handler不會導致activity的泄露,還要用WeakReference 包裹外部類的對象。弱引用:private WeakReference<Activity類名> weakReference;? 創建Handler時傳入activity對象。因為我們需要使用外部類的成員,可以通過"activity.?"獲取變量方法等,如果直接使用強引用,顯然會導致activity泄露。
- 將Handler放到抽取出來放入一個單獨的頂層類文件中。
- 不要讓生命周期長于Activity的對象持有到Activity的引用。
3.靜態內部類持有外部成員變量(或context):可以使用弱引用、使用ApplicationContext。
- 用 Activity 或Service的 Context,當這個 Context 所對應的 Activity 退出時,由于該 Context 的引用被單例對象所持有,其生命周期等于整個應用程序的生命周期,所以當前 Activity 退出時它的內存并不會被回收,這就造成泄漏了。
- 用 Application 的 Context,因為 Application 的生命周期就是整個應用的生命周期,所以這將沒有任何問題。
- 弱引用:private WeakReference<類名> weakReference;? 創建靜態內部類對象時傳入外部類。
4.集合中沒用的對象沒有及時remove。
- 我們通常把一些對象的引用加入到了集合容器(比如ArrayList)中,當我們不需要該對象時,并沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。
- 在退出程序之前,將集合里的東西clear,然后置為null,再退出程序。
5.WebView
WebView單獨存放在一個activity中,onDestory()中調用android.os.Process.killProcess(android.os.Process.myPid())等方法殺死進程。
activity泄漏可以使用LeakCanary。
內存泄露原因分析
在JAVA中JVM的棧記錄了方法的調用,每個線程擁有一個棧。在線程的運行過程當中,執行到一個新的方法調用,就在棧中增加一個內存單元,即幀(frame)。在frame中,保存有該方法調用的參數、局部變量和返回地址。然而JAVA中的局部變量只能是基本類型變量(int),或者對象的引用。所以在棧中只存放基本類型變量和對象的引用。引用的對象保存在堆中。
當某方法運行結束時,該方法對應的frame將會從棧中刪除,frame中所有局部變量和參數所占有的空間也隨之釋放。線程回到原方法繼續執行,當所有的棧都清空的時候,程序也就隨之運行結束。
而對于堆內存,堆存放著普通變量。在JAVA中堆內存不會隨著方法的結束而清空,所以在方法中定義了局部變量,在方法結束后變量依然存活在堆中。
綜上所述,棧(stack)可以自行清除不用的內存空間。但是如果我們不停的創建新對象,堆(heap)的內存空間就會被消耗盡。所以JAVA引入了垃圾回收(garbage collection,簡稱GC)去處理堆內存的回收。
1."垃圾的判定"
引用計數法
簡單理解就是記錄一個對象被引用的次數,一個引用被使用引用計數器就+1,反之就-1,當引用次數為0就說明是一個垃圾對象可以被回收了。
缺點:無法解決相互引用。棄用。
可達性分析法
”GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。
這個算法的基本思想是通過一系列稱為“GC Roots”的對象作為起始點,從這些節點向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈(即GC Roots到對象不可達)時,則證明此對象是不可用的。
GC管理的主要區域是Java堆,一般情況下只針對堆進行垃圾回收。方法區、棧和本地方法區不被GC所管理,因而選擇這些區域內的對象作為GC roots,被GC roots引用的對象不被GC回收。
- 虛擬機棧中棧楨中的局部變量(也叫局部變量表)中引用的對象
- 方法區中類的靜態變量、常量引用的對象
- 本地方法棧中?JNI (Native方法)引用的對象?
2.回收算法
標記–整理算法
標記-整理法是標記-清除法的一個改進版。在標記階段,該算法也將所有對象標記為存活和死亡兩種狀態;在第二個階段,沒有直接對死亡的對象進行清理,而是將所有存活的對象整理一下,放到另一處空間,以達到整理內存碎片的作用,然后把剩下的所有對象全部清除。
復制算法
該算法將內存平均分成兩部分,然后每次只使用其中的一部分,當這部分內存滿的時候,將內存中所有存活的對象復制到另一個內存中,然后將之前的內存清空,只使用這部分內存,循環下去。
分代回收算法:
新生代(復制算法):在堆中,一個Eden區和兩個Survivor區(From、To),比例為(8:1:1)。初始時新創建的對象會存放在Eden區,經過第一次GC后,將存活對象復制到To區。From則看對象年齡,每GC一次年齡+1,到年齡閾值(默認15歲)時會進入到老年代,沒有則復制到To區,此時會清理Eden區,清空From區,然后From和To區交換,既空的變為To區。當To區滿的情況,會把所有對象都移到老年代中。
老年代(標記整理法):在堆中,年老代主要存放經過幾次GC后的對象,內存大小相對會比較大,垃圾回收也相對沒有那么頻繁。
永久代:在方法區,放著一些被虛擬機加載的類信息,靜態變量,常量等數據。
由于堆內存中對象進行了分代處理,因此垃圾回收區域、時間也不一樣。GC有兩種類型:Minor GC和Full GC
Minor GC
在Eden 和 Survivor 區域回收內存被稱為 Minor GC。
觸發條件:一般情況下,當新對象生成,并且在Eden區滿時,就會觸發Minor GC,對Eden區域進行GC, 清除非存活對象,并且把尚且存活的對象移動到Survivor區。然后整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因為大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這里需要使用速度快、效率高的算法,使Eden去能盡快空閑出來。
Full GC
對整個堆進行整理,包括年輕代、年老代和永久代。
觸發條件:
- 當Eden區和From區滿時
- 老年代空間不足
- 方法區空間不足
- System.gc()被調用
- 由Eden區、From區向To區復制時,對象大小大于To可用內存,則把該對象轉存到老年代,且老年代的可用內存小于該對象大小。
GC垃圾收集器發展史
JDK1.8元空間代替永久代
元空間與永久代之間最大的區別在于:元空間并不在虛擬機中,而是使用本地內存:理論上取決于32位/64位系統可虛擬的內存大小,可見也不是無限制的,需要配置參數。
引用類型
從JDK 1.2版本開始,把對象的引用分為4種級別,從而使程序能更加靈活地控制對象的生命周期。這4種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。
1. 強引用(Strong reference)
實際編碼中最常見的一種引用類型。常見形式如:A a = new A();等。強引用本身存儲在棧內存中,其存儲指向對內存中對象的地址。如果一個對象具有強引用,則無論在什么情況下,GC都不會回收被引用的對象。
2. 軟引用(Soft Reference)
軟引用的一般使用形式如下:
表示一個對象處在有用但非必須的狀態。如果一個對象具有軟引用,在內存空間充足時,GC就不會回收該對象;當內存空間不足時,GC會回收該對象的內存(回收發生在OutOfMemoryError之前)。
3. 弱引用(Weak Reference)
同樣的,軟引用的一般使用形式如下:
無論當前內存是否緊缺,GC都將回收被弱引用關聯的對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被GC回收了,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中,以便在恰當的時候將該弱引用回收。
4. 虛引用(Phantom Reference)
虛引等同于沒有引用,這意味著在任何時候都可能被GC回收,設置虛引用的目的是為了被虛引用關聯的對象在被垃圾回收器回收時,能夠收到一個系統通知。
虛引用在使用時必須和引用隊列(ReferenceQueue)聯合使用。
ReferenceQueue queue=new ReferenceQueue(); PhantomReference pr=new PhantomReference(object.queue);LeakCanary
由Square開源的一款輕量第三方內存泄漏檢測工具,可以實時監測Activity,并提高內存泄漏信息。
原理:watch一個即將要銷毀的對象
使用方法:在Applicantion的子類里。
LeakCanary.install(this); public static @NonNull RefWatcher install(@NonNull Application application) {return refWatcher(application).listenerServiceClass(DisplayLeakService.class).excludedRefs(AndroidExcludedRefs.createAppDefaults().build()).buildAndInstall();}內部建立了一個refWatcher對象,通過refWatcher監聽activity回收情況。
-
DisplayService是發生內存泄漏時的通知服務
-
excludedRefs()是排除Android源碼出現的內存泄漏問題
最主要的是AndroidRefWatcherBuilder.buildAndInstall()
進入buildAndInstall()方法。
public @NonNull RefWatcher buildAndInstall() {if (LeakCanaryInternals.installedRefWatcher != null) {throw new UnsupportedOperationException("buildAndInstall() should only be called once.");}RefWatcher refWatcher = build();if (refWatcher != DISABLED) { // 根據app包名生成LeakCanary關聯應用if (enableDisplayLeakActivity) {LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);} // 監聽Activityif (watchActivities) {ActivityRefWatcher.install(context, refWatcher);} // 監聽Fragmentif (watchFragments) {FragmentRefWatcher.Helper.install(context, refWatcher);}}LeakCanaryInternals.installedRefWatcher = refWatcher;return refWatcher;}? ActivityRefWatcher.install(context, refWatcher);
public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {Application application = (Application) context.getApplicationContext();ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);// 通過在Application注冊監聽每個Activity的生命周期,然后轉發給RefWatcherapplication.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);}private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =new ActivityLifecycleCallbacksAdapter() {@Override public void onActivityDestroyed(Activity activity) {refWatcher.watch(activity);}};? FragmentRefWatcher.Helper.install(context, refWatcher);
public static void install(Context context, RefWatcher refWatcher) {List<FragmentRefWatcher> fragmentRefWatchers = new ArrayList<>();.......Helper helper = new Helper(fragmentRefWatchers);Application application = (Application) context.getApplicationContext();//給application注冊activityLifecycleCallbacks監聽application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks);}//在onActivityCreated的時候遍歷fragmentRefWatchers隊列給每個watcher調用watchFragments(activity)方法private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks =new ActivityLifecycleCallbacksAdapter() {@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {for (FragmentRefWatcher watcher : fragmentRefWatchers) {watcher.watchFragments(activity); }}};// 到watchFragments(activity)方法中private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =new FragmentManager.FragmentLifecycleCallbacks() {//在Fragment摧毀的時候調用watch方法@Override public void onFragmentViewDestroyed(FragmentManager fm, Fragment fragment) {View view = fragment.getView();if (view != null) {refWatcher.watch(view);}}@Overridepublic void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {refWatcher.watch(fragment);}};@Override public void watchFragments(Activity activity) {FragmentManager fragmentManager = activity.getFragmentManager(); //watchFragments方法里面又注冊了一層監聽fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true);}總結:
- 通過 Application.registerActivityLifecycleCallbacks() 監聽Activity的生命周期,在onActivityDestroyed階段調用RefWatcher的watch(Object)方法。
- 通過FragmentManager.registerFragmentLifecycleCallbacks() 監聽Fragment的生命周期并在onFragmentViewDestroyed和onFragmentDestroyed階段調用RefWatcher的watch(Object)方法。
我們得知監聽最后都是通過調用RefWatcher的watch(Object)方法,接下來到RefWatcher的watch(Object)方法里面看看。
//watchedReference傳入的就是activity或fragment對象public void watch(Object watchedReference, String referenceName) {if (this == DISABLED) {return;}checkNotNull(watchedReference, "watchedReference");checkNotNull(referenceName, "referenceName");final long watchStartNanoTime = System.nanoTime(); //為監聽生成唯一IDString key = UUID.randomUUID().toString(); //將key值存入一個隊列retainedKeys.add(key);//生成reference對象,封裝了watchedReference和key等。 //queue是Reference隊列,對象被回收了,那么對應的弱引用對象會在回收時被添加到queue中, //重點:KeyedWeakReference內部繼承了弱引用類,reference是弱引用對象,調用GC時就會被回收。final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue); //最重要的是這個方法,傳入了監聽對象ensureGoneAsync(watchStartNanoTime, reference);}//watchExecutor.execute方法確保了主線程空閑,生命周期走完后才會在子線程運行run方法private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {watchExecutor.execute(new Retryable() {@Override public Retryable.Result run() {return ensureGone(reference, watchStartNanoTime);}});}接下來看看ensureGone方法的具體實現
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {long gcStartNanoTime = System.nanoTime();long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);// 將queue隊列中對象的key從key隊列中移除,因為在queue隊列的reference已經被回收removeWeaklyReachableReferences();if (debuggerControl.isDebuggerAttached()) {// The debugger can create false leaks.return RETRY;}// 如果key隊列沒有該reference的key值,說明在某次GC已經回收該對象,沒有內存泄漏,不需要處理if (gone(reference)) {return DONE;}// 執行一次GCgcTrigger.runGc();// 再檢查removeWeaklyReachableReferences();// key隊列還有key值,說明已經內存泄漏,dumpif (!gone(reference)) {long startDumpHeap = System.nanoTime();long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);File heapDumpFile = heapDumper.dumpHeap();if (heapDumpFile == RETRY_LATER) {// Could not dump the heap.return RETRY;}long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key).referenceName(reference.name).watchDurationMs(watchDurationMs).gcDurationMs(gcDurationMs).heapDumpDurationMs(heapDumpDurationMs).build();heapdumpListener.analyze(heapDump);}return DONE;}private boolean gone(KeyedWeakReference reference) {return !retainedKeys.contains(reference.key);}ensureGone()主要將已經GC掉的弱引用對象從隊列中移除,然后判斷是否已經沒有,如果沒有跳出,有則會執行GC后再檢查,如果有說明內存泄漏,創建dump文件,分析dump文件。
RefWatcher.watch(Object)總結:
RefWatcher.watch(Object)會為每一個監聽引用對象(Activity或Fragment)提供唯一ID,RefWatcher類有兩個隊列分別存放reference對象和key,在主線程走完生命周期后通過 Retryable.run()在子線程檢測內存泄漏,監測是ensureGone()方法通過兩個隊列來判斷其是否reference對象是否GC回收。
LeakCanary總結:
LeakCanary調用install方法,內部創建RefWather對象,對象分別設置了對Activity和Fragment在Destroy階段的監聽,就是當Activity和Fragment在Destroy階段時調用watch方法傳入Activity或Fragment對象并生成相對應的唯一值key封裝成reference弱引用對象(注意是弱引用對象,因為弱引用對象GC后會被傳入Reference隊列),最后調用sureGone方法在主線程生命周期走完后,在子線程中進行檢查,先將Reference隊列的對象的key從key隊列中移出,再判斷現reference對象的key在不在key隊列里,不在則該對象已經被回收,否則再調用一次GC,再判斷一次,有內存泄漏便會進行 heap dump 的操作以獲取當前內存的 hprof。通過對hprof文件進行解析,計算出GC root的最短引用路徑,反饋異常。
總結
以上是生活随笔為你收集整理的Android—内存泄漏、GC及LeakCanary源码解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电子路考容易犯错的五大细节
- 下一篇: 推荐给开发人员的实用命令行工具