前些天,有人問到 “開發(fā)過程中常見的內(nèi)存泄漏都有哪些?”,一時脫口而出:靜態(tài)的對象中(包括單例)持有一個生命周期較短的引用時,或內(nèi)部類的子代碼塊對象的生命周期超過了外面代碼的生命周期(如非靜態(tài)內(nèi)部類,線程),會導致這個短生命周期的對象內(nèi)存泄漏??傊褪且粋€對象的生命周期結束(不再使用該對象)后,依然被某些對象所持有該對象強引用的場景就是內(nèi)存泄漏。
這樣回答很明顯并不是問答人想要的都有哪些場景,所以這里抽時間整理了下內(nèi)存相關的知識點,及LeakCanary工具的原理分析。
Java內(nèi)存問題 及 LeakCanary 原理分析
在安卓等其他移動平臺上,內(nèi)存問題顯得特別重要,想要做到虛擬機內(nèi)存的高效利用,及內(nèi)存問題的快速定位,了解下虛擬機內(nèi)存模塊及管理相關知識是很有必要的,這篇文章將從最基礎的知識分析,內(nèi)存問題的產(chǎn)生地方、原因、解決方案等原理。
一、運行時內(nèi)存區(qū)域

這里以Java虛擬機為例,將運行時內(nèi)存區(qū)分為不同的區(qū)域,每個區(qū)域承擔著不同的功能。
方法區(qū)
用戶存儲已被虛擬機加載的類信息,常量,靜態(tài)常量,即時編譯器編譯后的代碼等數(shù)據(jù)。異常狀態(tài) OutOfMemoryError,其中包含常量池和用戶存放編譯器生成的各種字面量和符號引用。
堆
是JVM所管理的內(nèi)存中最大的一塊。唯一目的就是存放實例對象,幾乎所有的對象實例都在這里分配。Java堆是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱為“GC堆”。異常狀態(tài) OutOfMemoryError。
虛擬機棧
描述的是java方法執(zhí)行的內(nèi)存模型,每個方法在執(zhí)行時都會創(chuàng)建一個棧幀,用戶存儲局部變量表,操作數(shù)棧,動態(tài)連接,方法出口等信息。每一個方法從調(diào)用直至完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。 對這個區(qū)域定義了兩種異常狀態(tài) OutOfMemoryError、StackOverflowError。
本地方法棧
虛擬機棧為虛擬機執(zhí)行java方法,而本地方法棧為虛擬機使用到的Native方法服務。異常狀態(tài)StackOverFlowError、OutOfMemoryError。
程序計數(shù)器
一塊較小的內(nèi)存,當前線程所執(zhí)行的字節(jié)碼的行號指示器。字節(jié)碼解釋器工作時,就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令。
內(nèi)存模型
Java內(nèi)存模型規(guī)定了所有的變量都存儲在主內(nèi)存中。每條線程中還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了被該線程所使用到的變量,這些變量是從主內(nèi)存中拷貝而來。線程對變量的所有操作(讀,寫)都必須在工作內(nèi)存中進行。不同線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過主內(nèi)存來完成。
為了保證內(nèi)存可見性,常常利用volatile關鍵子特性來保證變量的可見性(并不能保證并發(fā)時原子性)。
二、內(nèi)存如何回收
內(nèi)存的分配
一個對象從被創(chuàng)建到回收,主要經(jīng)歷階段有 1:創(chuàng)建階段(Created)、2: 應用階段(In Use)、3:不可見階段(Invisible)、4:不可達階段(Unreachable)、5:收集階段(Collected)、6:終結階段(、Finalized)、7:對象空間重分配階段(De-allocated)。
內(nèi)存的分配實在創(chuàng)建階段,這個階段要先用類加載器加載目標class,當通過加載器檢測后,就開始為新對象分配內(nèi)存。對象分配內(nèi)存大小在類加載完成后便可以確定。
當初始化完成后,虛擬機還要對對象進行必要的設置,如那個類的實例,如何查找元數(shù)據(jù)、對象的GC年代等。
內(nèi)存的回收(GC)
那些不可能再被任何途徑使用的對象,需要被回收,否則內(nèi)存遲早都會被消耗空。
GC機制主要是通過可達性分析法,通過一系列稱為“GC Roots”的對象作為起始點,從這些節(jié)點向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈時,即GC Roots到對象不可達,則證明此對象是不可達的。
根據(jù)《深入理解Java虛擬機》書中描述,可作為GC Root的地方如下:
- 虛擬機棧(棧幀中的局部變量區(qū),也叫做局部變量表)中引用的對象。
- 方法區(qū)中的類靜態(tài)屬性引用的對象。
- 方法區(qū)中常量引用的對象。
- 本地方法棧中JNI(Native方法)引用的對象。
當一個對象或幾個相互引用的對象組沒有任何引用鏈時,會被當成垃圾處理,可以進行回收。
如何一個對象在程序中已經(jīng)不再使用,但是(強)引用還是會被其他對象持有,則稱為內(nèi)存泄漏。內(nèi)存泄漏并不會使程序馬上異常,但是多處的未處理的內(nèi)存泄漏則可能導致內(nèi)存溢出,造成不可預估的后果。
引用的分類
在JDK1.2之后,為了優(yōu)化內(nèi)存的利用及GC的效率,Java對引用的概念進行了擴充,將引用分為強引用、軟引用、弱引用、虛引用4種。
1、強引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
2、軟引用,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象列進回收范圍進行二次回收。如果這次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。SoftReference表示軟引用。
3、弱引用,只要有GC,無論當前內(nèi)存是否足夠,都會回收掉只被弱引用關聯(lián)的對象。WeakReference表示弱引用。
4、虛引用,這個引用存在的唯一目的就是在這個對象被收集器回收時收到一個系統(tǒng)通知,被虛引用關聯(lián)的對象,和其生存時間完全沒關系。PhantomReference表示虛引用,需要搭配ReferenceQueue使用,檢測對象回收情況。
關于JVM內(nèi)存管理的一些建議
1、盡可能的手動將無用對象置為null,加快內(nèi)存回收。
2、可考慮對象池技術生成可重用的對象,較少對象的生成。
3、合理利用四種引用。
三、內(nèi)存泄漏
持有一個生命周期較短的引用時或內(nèi)部的子模塊對象的生命周期超過了外面模塊的生命周期,即本該被回收的對象不能被回收而停留在堆內(nèi)存中,這就產(chǎn)生了內(nèi)存泄漏。
內(nèi)存泄漏是造成應用程序OOM的主要原因之一,尤其在像安卓這樣的移動平臺,難免會導致應用所需要的內(nèi)存超過系統(tǒng)分配的內(nèi)存限額,這就造成了內(nèi)存溢出Error。
安卓平臺常見的內(nèi)存泄漏
1、靜態(tài)成員變量持有外部(短周期臨時)對象引用。 如單例類(類內(nèi)部靜態(tài)屬性)持有一個activity(或其他短周期對象)引用時,導致被持有的對象內(nèi)存無法釋放。
2、內(nèi)部類。當內(nèi)部類與外部類生命周期不一致時,就會造成內(nèi)存泄漏。如非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)實例、Activity中的Handler或Thread等。
3、資源沒有及時關閉。如數(shù)據(jù)庫、IO流、Bitmap、注冊的相關服務、webview、動畫等。
4、集合內(nèi)部Item沒有置空。
5、方法塊內(nèi)不使用的對象,沒有及時置空。
四、如何檢測內(nèi)存泄漏
Android Studio供了許多對App性能分析的工具,可以方便分析App性能。我們可以使用Memory Monitor和Heap Dump來觀察內(nèi)存的使用情況、使用Allocation Tracker來跟蹤內(nèi)存分配的情況,也可以通過這些工具來找到疑似發(fā)生內(nèi)存泄漏的位置。
堆存儲文件(hpof)可以使用DDMS或者Memory Monitor來生成,輸出的文件格式為hpof,而MAT(Memory Analysis Tool)就是來分析堆存儲文件的。
然而MAT工具分析內(nèi)存問題并不是一件容易的事情,需要一定的經(jīng)驗區(qū)做引用鏈的分析,需要一定的門檻。
隨著安卓技術生態(tài)的發(fā)展,LeakCanary 開源項目誕生了,只要幾行代碼引入目標項目,就可以自動分析hpof文件,把內(nèi)存泄漏的地方展示出來。
五、LeakCanary原理解析

A small leak will sink a great ship.
LeakCanary內(nèi)存檢測工具是由squar公司開源的著名項目,這里主要分析下源碼實現(xiàn)原理。
基本原理
主要是在Activity的&onDestroy方法中,手動調(diào)用 GC,然后利用ReferenceQueue+WeakReference,來判斷是否有釋放不掉的引用,然后結合dump memory的hpof文件, 用HaHa分析出泄漏地方。
源碼分析
LeakCanary集成很方便,只要幾行代碼,所以可以從入口跟蹤代碼,分析原理
if (!LeakCanary.isInAnalyzerProcess(WeiboApplication.this)) {
LeakCanary.install(WeiboApplication.this);
}
public static RefWatcher install(Application application) {
return ((AndroidRefWatcherBuilder)refWatcher(application)
.listenerServiceClass(DisplayLeakService.class).excludedRefs(AndroidExcludedRefs.createAppDefaults().build()))//配置監(jiān)聽器及分析數(shù)據(jù)格式
.buildAndInstall();
}
從這里可看出,LeakCanary會單獨開一進程,用來執(zhí)行分析任務,和監(jiān)聽任務分開處理。
方法install中主要是構造來一個RefWatcher,
public RefWatcher buildAndInstall() {
RefWatcher refWatcher = this.build();
if(refWatcher != RefWatcher.DISABLED) {
LeakCanary.enableDisplayLeakActivity(this.context);
ActivityRefWatcher.install((Application)this.context, refWatcher);
}
return refWatcher;
}
public static void install(Application application, RefWatcher refWatcher) {
(new ActivityRefWatcher(application, refWatcher)).watchActivities();
}
private final ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacks() {
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
public void onActivityStarted(Activity activity) {}
public void onActivityResumed(Activity activity) {}
public void onActivityPaused(Activity activity) {}
public void onActivityStopped(Activity activity) { }
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
public void onActivityDestroyed(Activity activity) {
ActivityRefWatcher.this.onActivityDestroyed(activity);
}
};
void onActivityDestroyed(Activity activity) {
this.refWatcher.watch(activity);
}
具體監(jiān)聽的原理在于 Application 的registerActivityLifecycleCallbacks方法,該方法可以對應用內(nèi)所有 Activity 的生命周期做監(jiān)聽, LeakCanary只監(jiān)聽了Destroy方法。
在每個Activity的OnDestroy()方法中都會回調(diào)refWatcher.watch()方法,那我們找到的RefWatcher的實現(xiàn)類,看看具體做什么。
public void watch(Object watchedReference, String referenceName) {
if(this != DISABLED) {
Preconditions.checkNotNull(watchedReference, "watchedReference");
Preconditions.checkNotNull(referenceName, "referenceName");
long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();//保證key的唯一性
this.retainedKeys.add(key);
KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
this.ensureGoneAsync(watchStartNanoTime, reference);
}
}
final class KeyedWeakReference extends WeakReference<Object> {
public final String key;
public final String name;
KeyedWeakReference(Object referent, String key, String name, ReferenceQueue<Object> referenceQueue) {//ReferenceQueue類監(jiān)聽回收情況
super(Preconditions.checkNotNull(referent, "referent"), (ReferenceQueue)Preconditions.checkNotNull(referenceQueue, "referenceQueue"));
this.key = (String)Preconditions.checkNotNull(key, "key");
this.name = (String)Preconditions.checkNotNull(name, "name");
}
}
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
this.watchExecutor.execute(new Retryable() {
public Result run() {
return RefWatcher.this.ensureGone(reference, watchStartNanoTime);
}
});
}
KeyedWeakReference是WeakReference類的子類,用了 KeyedWeakReference(referent, key, name, ReferenceQueue<Object> )的構造方法,將監(jiān)聽的對象(activity)引用傳遞進來,并且New出一個ReferenceQueue來監(jiān)聽GC后 的回收情況。
以下代碼ensureGone()方法就是LeakCanary進行檢測回收的核心代碼:
Result ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = TimeUnit.NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
this.removeWeaklyReachableReferences();//先將引用嘗試從隊列中poll出來
if(this.debuggerControl.isDebuggerAttached()) {//規(guī)避調(diào)試模式
return Result.RETRY;
} else if(this.gone(reference)) {//檢測是否已經(jīng)回收
return Result.DONE;
} else {
//如果沒有被回收,則手動GC
this.gcTrigger.runGc();//手動GC方法
this.removeWeaklyReachableReferences();//再次嘗試poll,檢測是否被回收
if(!this.gone(reference)) {
// 還沒有被回收,則dump堆信息,調(diào)起分析進程進行分析
long startDumpHeap = System.nanoTime();
long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
File heapDumpFile = this.heapDumper.dumpHeap();
if(heapDumpFile == HeapDumper.RETRY_LATER) {
return Result.RETRY;//需要重試
}
long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
this.heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, this.excludedRefs, watchDurationMs, gcDurationMs, heapDumpDurationMs));
}
return Result.DONE;
}
}
private boolean gone(KeyedWeakReference reference) {
return !this.retainedKeys.contains(reference.key);
}
private void removeWeaklyReachableReferences() {
KeyedWeakReference ref;
while((ref = (KeyedWeakReference)this.queue.poll()) != null) {
this.retainedKeys.remove(ref.key);
}
}
方法ensureGone中通過檢測referenceQueue隊列中的引用情況,來判斷回收情況,通過手動GC來進一步確認回收情況。
整個過程肯定是個耗時卡UI的,整個過程會在WatchExecutor中執(zhí)行的,那WatchExecutor又是在哪里執(zhí)行的呢?
LeakCanary已經(jīng)利用Looper機制做了一定優(yōu)化,利用主線程空閑的時候執(zhí)行檢測任務,這里找到WatchExecutor的實現(xiàn)類,研究下原理:
public final class AndroidWatchExecutor implements WatchExecutor {
static final String LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump";
private final Handler mainHandler = new Handler(Looper.getMainLooper());
private final Handler backgroundHandler;
private final long initialDelayMillis;
private final long maxBackoffFactor;
public AndroidWatchExecutor(long initialDelayMillis) {
HandlerThread handlerThread = new HandlerThread("LeakCanary-Heap-Dump");
handlerThread.start();
this.backgroundHandler = new Handler(handlerThread.getLooper());
this.initialDelayMillis = initialDelayMillis;
this.maxBackoffFactor = 9223372036854775807L / initialDelayMillis;
}
public void execute(Retryable retryable) {
if(Looper.getMainLooper().getThread() == Thread.currentThread()) {
this.waitForIdle(retryable, 0);//需要在主線程中檢測
} else {
this.postWaitForIdle(retryable, 0);//post到主線程
}
}
void postWaitForIdle(final Retryable retryable, final int failedAttempts) {
this.mainHandler.post(new Runnable() {
public void run() {
AndroidWatchExecutor.this.waitForIdle(retryable, failedAttempts);
}
});
}
void waitForIdle(final Retryable retryable, final int failedAttempts) {
Looper.myQueue().addIdleHandler(new IdleHandler() {
public boolean queueIdle() {
AndroidWatchExecutor.this.postToBackgroundWithDelay(retryable, failedAttempts);//切換到子線程
return false;
}
});
}
void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
long exponentialBackoffFactor = (long)Math.min(Math.pow(2.0D, (double)failedAttempts), (double)this.maxBackoffFactor);
long delayMillis = this.initialDelayMillis * exponentialBackoffFactor;
this.backgroundHandler.postDelayed(new Runnable() {
public void run() {
Result result = retryable.run();//RefWatcher.this.ensureGone(reference, watchStartNanoTime)執(zhí)行
if(result == Result.RETRY) {
AndroidWatchExecutor.this.postWaitForIdle(retryable, failedAttempts + 1);
}
}
}, delayMillis);
}
}
這里用到了Handler相關知識,Looper中的MessageQueue有個mIdleHandlers隊列,在獲取下個要執(zhí)行的Message時,如果沒有發(fā)現(xiàn)可執(zhí)行的下個Msg,就會回調(diào)queueIdle()方法。
Message next() {
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
···
···//省略部分消息查找代碼
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
···
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {//返回false,則從隊列移除,下次空閑不會調(diào)用。
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;
// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}
其中的MessageQueue中加入一個IdleHandler,當線程空閑時,就會去調(diào)用queueIdle()函數(shù),如果返回值為True,那么后續(xù)空閑時會繼續(xù)的調(diào)用此函數(shù),否則不再調(diào)用;
知識點
1,用ActivityLifecycleCallbacks接口來檢測Activity生命周期
2,WeakReference + ReferenceQueue 來監(jiān)聽對象回收情況
3,Apolication中可通過processName判斷是否是任務執(zhí)行進程
4,MessageQueue中加入一個IdleHandler來得到主線程空閑回調(diào)
5,LeakCanary檢測只針對Activiy里的相關對象。其他類無法使用,還得用MAT原始方法
六、總結
內(nèi)存相關的問題基本問題回顧了下,發(fā)現(xiàn)技術細節(jié)越扒越多。想要得到技術的提高,對這些技術細節(jié)的掌握是必要的,只有長時間的積累扎實的技術細節(jié)基礎,才能讓自己的技術走的更高。
基礎知識對每個工程師發(fā)展的不同階段意義不同,理解的角度和深度也不同。至少自己來看,基礎知識是永遠值得學習和鞏固,來支撐技術的創(chuàng)新實踐。
歡迎轉載,請標明出處:常興E站 canking.win