序、慢慢来才是最快的方法。
LeakCanary是Square的开源库,通过弱引用方式侦查Activity或Fragment对象的生命周期,若发现内存泄漏自动 dump Hprof文件,通过HAHA库得到泄露的最短路径,最后通过Notification展示。
简单说就是在在Activity对象onDestory的时候,新建一个WeakReference对象指向Activity对象,如果Activity对象被垃圾回收的话,WeakReference对象就会进入引用序列的ReferenceQueue。
所以我们只需要在Activity对象OnDestory之后去查看ReferenceQueue序列是否有该WeakReference对象即可。
第一次观察是Activity的onDestory5秒后,如果发现ReferenceQueue对来还没有WeakReference对象,就进入第二次观察,如果有了,就证明没有泄漏,第二次观察跟第一次观察相比区别在于会先进行垃圾回收,在进行ReferenceQueue序列的观察。
通过查看LeakCanary的文档,发现1.x和2.x的版本差异还是比较大的。算是一个比较大的升级。
内存泄漏:对象被持有导致无法释放或不能按照对象正常生命周期进行释放的。
内存泄露(Memory Leaks)指不再使用的对象或数据没有被回收,随着内存泄漏的堆积,应用性能会逐渐变差,甚至发生 OOM 奔溃。在 Android 应用中的内存泄漏可以分为 2 类:
VM内存大致分为5个区,程序计数器,虚拟机栈,本地方法栈,堆,方法区。
顾名思义跟PC寄存器作用类似,每个线程独立存在,生命周期与线程一致。指示当前执行的方法,内存很小,忽略不计,没有垃圾。
栈空间,每个线程独立存在,保存方法参数或者方法内对象的引用。生命周期结束,比如方法执行完毕后内存会被释放,所以不需要垃圾管理。
与虚拟机栈类似,对应native方法。不需要垃圾管理。
对象的实际存储区域,比如在方法内new一个局部变量,在堆开辟内存,引用保存在虚拟机栈。也是垃圾管理的最主要的区域。
class文件和常量(JDK7开始字符串常量池在堆区)存储区域,属于垃圾管理范围。
PS:其中程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作,这几个区域内不需要过多考虑回收的问题。
而堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的就是这部分内存。
记录每个引用的被引用个数,当引用个数为0时代表成为垃圾,应该被清理。
优点:
缺点:
JVM 把内存中所有的对象之间的引用关系看作一张图,通过一组名为”GC Root"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,最后通过判断对象的引用链是否可达来决定对象是否可以被回收。
在 Java 中,有以下几种对象可以作为 GC Root:
同的虚拟机实现有着不同的 GC 实现机制,但是一般情况下每一种 GC 实现都会在以下两种情况下触发垃圾回收。
从”GC Roots”集合开始,将内存整个遍历一次,保留所有可以被 GC Roots 直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收,过程分两步。
将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
需要先从根节点开始对所有可达对象做一次标记,之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。最后,清理边界外所有的空间。因此标记压缩也分两步完成:
Java 虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代,这就是 JVM 的内存分代策略。注意: 在 HotSpot 中除了新生代和老年代,还有永久代
分代回收的中心思想就是:对于新创建的对象会在新生代中分配内存,此区域的对象生命周期一般较短。如果经过多次回收仍然存活下来,则将它们转移到老年代中。
新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收 70%~95% 的空间,回收效率很高。新生代中因为要进行一些复制操作,所以一般采用的 GC 回收算法是复制算法。
新生代又可以继续细分为 3 部分:Eden、Survivor0(简称 S0)、Survivor1(简称S1)。这 3 部分按照 8:1:1 的比例来划分新生代。这 3 块区域的内存分配过程如下:
绝大多数刚刚被创建的对象会存放在 Eden 区。
当 Eden 区第一次满的时候,会进行垃圾回收。首先将 Eden区的垃圾对象回收清除,并将存活的对象复制到 S0,此时 S1是空的。
下一次 Eden 区满时,再执行一次垃圾回收。此次会将 Eden和 S0区中所有垃圾对象清除,并将存活对象复制到 S1,此时 S0变为空。
如此反复在 S0 和 S1之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。
一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。
我们可以使用 -XX:PretenureSizeThreshold 来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。老年代因为对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法。
永久代主要存放Java类和方法对象。从Java 8开始,永久代已被元空间替代。
LeakCanary进行判断对象是否泄漏是通过新建一个WeakReference对象指向Activity对象,如果Activity对象被垃圾回收的话,WeakReference对象就会进入引用序列的ReferenceQueue。
所以我们只需要在Activity对象OnDestory之后去查看ReferenceQueue序列是否有该WeakReference对象即可。
强引用是最常用的引用。如果一个对象具有强引用,那 GC(Gabage Collection,垃圾回收)绝不会回收它。
Object obj = new Object(); // 强引用
当内存空间不足,Java虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
如果一个对象只具有软引用,在内存空间足够的时候,垃圾回收器是不会回收它的;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
PS:软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。
当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候你就可以使用弱引用。这个引用不会在对象的垃圾回收判断中产生任何附加的影响。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
虚引用与其他几种引用都不同,它并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。
PS:LeakCanary里面使用的就是弱引用(WeakReference)来管理监控对象的回收情况
示例代码:
class KeyedWeakReference extends WeakReference
Handler的IdleHandler是Android开发中用来处理消息队列空闲时的回调接口。当消息队列中,没有新的消息要处理时,IdleHandler将被调用。
IdleHandler的主要作用
异步处理任务,当主线程空闲时,可以利用IdleHandler在后台线程中执行耗时操作,避免阻塞主线程,提高应用程序的并发性能和响应速度。
也就是说在主线程中没有可处理的Message(.next无messge handler时)回去判断是否有IdleHandelr,如果有就会调用IdleHandler接口实现的方法。
在源码中发现,LeakCanary需要GC来确认是否存在内存泄漏,而GC会阻塞线程,所以在IdleHandler的接口实现里面做GC以及后续操作。
源码:
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
watchExecutor.execute(new Retryable() {
@Override public Retryable.Result run() {
return ensureGone(reference, watchStartNanoTime);
}
});
}
@Override public void execute(Retryable retryable) {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
waitForIdle(retryable, 0);
} else {
postWaitForIdle(retryable, 0);
}
}
private void waitForIdle(final Retryable retryable, final int failedAttempts) {
// This needs to be called from the main thread.
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override public boolean queueIdle() {
postToBackgroundWithDelay(retryable, failedAttempts);
return false;
}
});
}
LeakCanary里面会获取hprof文件。通过
Debug.dumpHprofData(String fileName)
然后使用haha库来分析Android heap dumps的库。通过它解析获取引用链。
是一份存储了当前时刻内存信息的文件,即内存快照,可以方便我们进行内存分析。
里面包含了类信息、栈信息、堆信息。其中堆信息是我们重点关注的重点。
堆信息里面包含了所有对象:线程对象,类对象,实例对象,对象数组对象,原始类型数组对象。
其中类对象可以获取该类的常量,静态变量,实例变量等信息。
从获取hprof文件开始:AndroidHeapDumper的dumpHeap方发调用了Debug的方法
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
分析Hprof文件是从ServiceHeapDumpListener开始
@Override public void analyze(HeapDump heapDump) {
checkNotNull(heapDump, "heapDump");
HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
}
HeapAnalyzerService是IntentService的子类。IntentService的特点是当启动IntentService多次时,每个耗时操作将以队列的方式在IntentService的onHandleIntent回调方法中依次执行,执行完自动结束。
@Override protected void onHandleIntentInForeground(@Nullable Intent intent) {
if (intent == null) {
CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
return;
}
String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);
HeapAnalyzer heapAnalyzer =
new HeapAnalyzer(heapDump.excludedRefs, this, heapDump.reachabilityInspectorClasses);
AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey,
heapDump.computeRetainedHeapSize);
AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
}
heapAnalyzer.checkForLeak
public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey,
boolean computeRetainedSize) {
long analysisStartNanoTime = System.nanoTime();
//.....省略部分代码
try {
//使用 MemoryMappedFileBuffer 和 HprofParser 分别来读取解析生成快照
//
HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
HprofParser parser = new HprofParser(buffer);
Snapshot snapshot = parser.parse();
deduplicateGcRoots(snapshot);
Instance leakingRef = findLeakingReference(referenceKey, snapshot);
// False alarm, weak reference was cleared in between key check and heap dump.
if (leakingRef == null) {
return noLeak(since(analysisStartNanoTime));
}
return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
} catch (Throwable e) {
return failure(e, since(analysisStartNanoTime));
}
}
findLeakReference是查找引用调用链
private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
Instance leakingRef, boolean computeRetainedSize) {
ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);
LeakTrace leakTrace = buildLeakTrace(result.leakingNode);
String className = leakingRef.getClassObj().getClassName();
long retainedSize;
if (computeRetainedSize) {
listener.onProgressUpdate(COMPUTING_DOMINATORS);
// Side effect: computes retained size.
snapshot.computeDominators();
Instance leakingInstance = result.leakingNode.instance;
retainedSize = leakingInstance.getTotalRetainedSize();
// TODO: check O sources and see what happened to android.graphics.Bitmap.mBuffer
if (SDK_INT <= N_MR1) {
listener.onProgressUpdate(COMPUTING_BITMAP_SIZE);
retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
}
} else {
retainedSize = AnalysisResult.RETAINED_HEAP_SKIPPED;
}
return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
since(analysisStartNanoTime));
}
最终leakTrace.toString 就是泄漏对象的调用链。
PS:之前在极客时间张绍文老师的课程中做过一个检测大Bitmap对象的练习。查找Bitmap对象和获取调用链的时候,仿照的就是LeakCanary分析获取调用链的逻辑。
Android 开源库 #7 为什么各大厂自研的内存泄漏检测框架都要参考 LeakCanary?因为它是真强啊!
被问到:如何检测线上内存泄漏,通过 LeakCanary 探究!
快手KOOM高性能线上解决方案
04 | 内存优化(下):内存优化这件事,应该从哪里着手?
Chapter04 Bitmap检测