为了使文章尽量通俗易懂。在探究LeakCanary
之前,有必要补充些Java
引用的知识。
软引用、弱引用、虚引用-他们的特点及应用场景
为什么会有这4种引用
Java
中的引用的定义很传统:如果reference
类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。 这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。 我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。 很多系统的缓存功能都符合这样的应用场景。
说白了传统的两种应用没法描述对象生命周期中的多种状态,对象有哪些状态呢。
在Java
中,对象的生命周期包括以下几个阶段:
- 1.创建阶段(
Created
)- 2.应用阶段(
In Use
)- 3.不可见阶段(
Invisible
)- 4.不可达阶段(
Unreachable
)- 5.收集阶段(
Collected
)- 6.终结阶段(
Finalized
)- 7.对象空间重分配阶段(
De-allocated
)
创建阶段(
Created
)
在创建阶段系统通过下面的几个步骤来完成对象的创建过程
为对象分配存储空间
开始构造对象
从超类到子类对static成员进行初始化
超类成员变量按顺序初始化,递归调用超类的构造方法
子类成员变量按顺序初始化,子类构造方法调用
一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段
应用阶段(
In Use
)
对象至少被一个强引用持有着。
不可见阶段(Invisible)
当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该这些引用仍然是存在着的。
简单说就是程序的执行已经超出了该对象的作用域了。
不可达阶段(
Unreachable
)
对象处于不可达阶段是指该对象不再被任何强引用所持有。
与“不可见阶段”相比,“不可见阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被JVM
等系统下的某些已装载的静态变量或线程或JNI等强引用持有着,这些特殊的强引用被称为GC roo
。存在着这些GC root
会导致对象的内存泄露情况,无法被回收。
收集阶段(
Collected
)
当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重写了finalize()
方法,则会去执行该方法的终端操作。
这里要特别说明一下:不要重载finazlie()
方法!原因有两点:
- 会影响
JVM
的对象分配与回收速度
在分配该对象时,JVM
需要在垃圾回收器上注册该对象,以便在回收时能够执行该重载方法;在该方法的执行时需要消耗CPU
时间且在执行完该方法后才会重新执行回收操作,即至少需要垃圾回收器对该对象执行两次GC
。- 可能造成该对象的再次“复活”
在finalize()
方法中,如果有其它的强引用再次持有该对象,则会导致对象的状态由“收集阶段”又重新变为“应用阶段”。这个已经破坏了Java
对象的生命周期进程,且“复活”的对象不利用后续的代码管理。
终结阶段
当对象执行完finalize()
方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。
对象空间重新分配阶段
垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间重新分配阶段”。
哪4种,各有什么特点
- 强引用
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出
OutOfM moryError
错误,使程序异常终止,也不会靠随意回收具有强引用 对象来解决内存不足的问题。
- 软引用
软引用是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
/**
* 软引用何时被收集
* 运行参数 -Xmx200m -XX:+PrintGC
* Created by ccr at 2018/7/14.
*/
public class SoftReferenceDemo {
public static void main(String[] args) throws InterruptedException {
//100M的缓存数据
byte[] cacheData = new byte[100 * 1024 * 1024];
//将缓存数据用软引用持有
SoftReference<byte[]> cacheRef = new SoftReference<>(cacheData);
//将缓存数据的强引用去除
cacheData = null;
System.out.println("第一次GC前" + cacheData);
System.out.println("第一次GC前" + cacheRef.get());
//进行一次GC后查看对象的回收情况
System.gc();
//等待GC
Thread.sleep(500);
System.out.println("第一次GC后" + cacheData);
System.out.println("第一次GC后" + cacheRef.get());
//在分配一个120M的对象,看看缓存对象的回收情况
byte[] newData = new byte[120 * 1024 * 1024];
System.out.println("分配后" + cacheData);
System.out.println("分配后" + cacheRef.get());
}
}
第一次GC前null
第一次GC前[B@7d4991ad
[GC (System.gc()) 105728K->103248K(175104K), 0.0009623 secs]
[Full GC (System.gc()) 103248K->103139K(175104K), 0.0049909 secs]
第一次GC后null
第一次GC后[B@7d4991ad
[GC (Allocation Failure) 103805K->103171K(175104K), 0.0027889 secs]
[GC (Allocation Failure) 103171K->103171K(175104K), 0.0016018 secs]
[Full GC (Allocation Failure) 103171K->103136K(175104K), 0.0089988 secs]
[GC (Allocation Failure) 103136K->103136K(199680K), 0.0009408 secs]
[Full GC (Allocation Failure) 103136K->719K(128512K), 0.0082685 secs]
分配后null
分配后null
从上面的示例中就能看出,软引用关联的对象不会被
GC
回收。JVM
在分配空间时,若果Heap
空间不足,就会进行相应的GC
,但是这次GC
并不会收集软引用关联的对象,但是在JVM发现就算进行了一次回收后还是不足(Allocation Failure
),JVM
会尝试第二次GC
,回收软引用关联的对象。
像这种如果内存充足,
GC
时就保留,内存不够,GC
再来收集的功能很适合用在缓存的引用场景中。在使用缓存时有一个原则,如果缓存中有就从缓存获取,如果没有就从数据库中获取,缓存的存在是为了加快计算速度,如果因为缓存导致了内存不足进而整个程序崩溃,那就得不偿失了。
- 弱引用
弱引用也是用来描述非必须对象的,他的强度比软引用更弱一些,被弱引用关联的对象,在垃圾回收时,如果这个对象只被弱引用关联(没有任何强引用关联他),那么这个对象就会被回收。
/**
* 弱引用关联对象何时被回收
* Created by ccr at 2018/7/14.
*/
public class WeakReferenceDemo {
public static void main(String[] args) throws InterruptedException {
//100M的缓存数据
byte[] cacheData = new byte[100 * 1024 * 1024];
//将缓存数据用软引用持有
WeakReference<byte[]> cacheRef = new WeakReference<>(cacheData);
System.out.println("第一次GC前" + cacheData);
System.out.println("第一次GC前" + cacheRef.get());
//进行一次GC后查看对象的回收情况
System.gc();
//等待GC
Thread.sleep(500);
System.out.println("第一次GC后" + cacheData);
System.out.println("第一次GC后" + cacheRef.get());
//将缓存数据的强引用去除
cacheData = null;
System.gc();
//等待GC
Thread.sleep(500);
System.out.println("第二次GC后" + cacheData);
System.out.println("第二次GC后" + cacheRef.get());
}
}
第一次GC前[B@7d4991ad
第一次GC前[B@7d4991ad
第一次GC后[B@7d4991ad
第一次GC后[B@7d4991ad
第二次GC后null
第二次GC后null
从上面的代码中可以看出,弱引用关联的对象是否回收取决于这个对象有没有其他强引用指向它。这个确实很难理解,既然弱引用关联对象的存活周期和强引用差不多,那直接用强引用好了,干嘛费用弄出个弱引用呢?其实弱引用存在必然有他的应用场景。
static Map<Object,Object> container = new HashMap<>();
public static void putToContainer(Object key,Object value){
container.put(key,value);
}
public static void main(String[] args) {
//某个类中有这样一段代码
Object key = new Object();
Object value = new Object();
putToContainer(key,value);
//..........
/**
* 若干调用层次后程序员发现这个key指向的对象没有用了,
* 为了节省内存打算把这个对象抛弃,然而下面这个方式真的能把对象回收掉吗?
* 由于container对象中包含了这个对象的引用,所以这个对象不能按照程序员的意向进行回收.
* 并且由于在程序中的任何部分没有再出现这个键,所以,这个键 / 值 对无法从映射中删除。
* 很可能会造成内存泄漏。
*/
key = null;
}
下面一段话摘自《Java核心技术卷1》:
设计
WeakHashMap
类是为了解决一个有趣的问题。如果有一个值,对应的键已经不再 使用了, 将会出现什么情况呢? 假定对某个键的最后一次引用已经消亡,不再有任何途径引 用这个值的对象了。但是,由于在程序中的任何部分没有再出现这个键,所以,这个键 / 值 对无法从映射中删除。为什么垃圾回收器不能够删除它呢? 难道删除无用的对象不是垃圾回 收器的工作吗?
遗憾的是,事情没有这样简单。垃圾回收器跟踪活动的对象。只要映射对象是活动的, 其中的所有桶也是活动的, 它们不能被回收。因此,需要由程序负责从长期存活的映射表中 删除那些无用的值。 或者使用
WeakHashMap
完成这件事情。当对键的唯一引用来自散列条
目时, 这一数据结构将与垃圾回收器协同工作一起删除键 / 值对。
下面是这种机制的内部运行情况。
WeakHashMap
使用弱引用(weak references
) 保存键。WeakReference
对象将引用保存到另外一个对象中,在这里,就是散列键。对于这种类型的 对象,垃圾回收器用一种特有的方式进行处理。通常,如果垃圾回收器发现某个特定的对象 已经没有他人引用了,就将其回收。然而, 如果某个对象只能由WeakReference
引用, 垃圾 回收器仍然回收它,但要将引用这个对象的弱引用放人队列中。WeakHashMap
将周期性地检 查队列, 以便找出新添加的弱引用。一个弱引用进人队列意味着这个键不再被他人使用, 并 且已经被收集起来。于是,WeakHashMap
将删除对应的条目。
除了WeakHashMap
使用了弱引用,ThreadLocal
类中也是用了弱引用。
- 虚引用
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。虚引用和弱引用对关联对象的回收都不会产生影响,如果只有虚引用活着弱引用关联着对象,那么这个对象就会被回收。它们的不同之处在于弱引用的
get
方法,虚引用的get
方法始终返回null
,弱引用可以使用ReferenceQueue
,虚引用必须配合ReferenceQueue
使用。
jdk
中直接内存的回收就用到虚引用,由于jvm
自动内存管理的范围是堆内存,而直接内存是在堆内存之外(其实是内存映射文件,自行去理解虚拟内存空间的相关概念),所以直接内存的分配和回收都是有Unsafe
类去操作,java
在申请一块直接内存之后,会在堆内存分配一个对象保存这个堆外内存的引用,这个对象被垃圾收集器管理,一旦这个对象被回收,相应的用户线程会收到通知并对直接内存进行清理工作。
软引用、弱引用、虚引用的构造方法均可以传入一个ReferenceQueue
与之关联。在引用所指的对象被回收后,引用(reference
)本身将会被加入到ReferenceQueue
之中,此时引用所引用的对象reference.get()
已被回收 (reference
此时不为null
,reference.get()
此时为null
)。
所以,在一个非强引用所引用的对象回收时,如果引用reference
没有被加入到被关联的ReferenceQueue
中,则表示还有引用所引用的对象还没有被回收。如果判断一个对象的非强引用本该出现在ReferenceQueue
中,实际上却没有出现,则表示该对象发送内存泄漏。
LeakCanary
理论依据
当一个Activity
的onDestory
方法被执行后,说明该Activity
的生命周期已经走完,在下次GC
发生时,该Activity
对象应将被回收。
- 通过上面对引用的学习,可以考虑在
onDestory
发生时创建一个弱引用指向Activity
,并关联一个RefrenceQuence
,当Activity被正常回收,弱引用实例本身应该出现在该RefrenceQuence
中,否则便可以判断该Activity
存在内存泄漏。 - 通过
Application.registerActivityLifecycleCallbacks()
方法可以注册Activity
生命周期的监听,每当一个Activity调用onDestroy
进行页面销毁时,去获取到这个Activity
的弱引用并关联一个ReferenceQuence
,通过检测ReferenceQuence
中是否存在该弱引用判断这个Activity
对象是否正常回收。 - 当
onDestory
被调用后,初步观察到Activity
未被GC
正常回收时,手动触发一次GC
,由于手动发起GC
请求后并不会立即执行垃圾回收,所以需要在一定时延后再二次确认Activity
是否已经回收,如果再次判断Activity
对象未被回收,则表示Activity
存在内存泄漏。
源码解析
1.在导入依赖后使用如下方法便可以使用LeakCanary
进行Activity
内存泄漏分析:
MyApp.java
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
//判断是否在主进程中
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
//使用LeakCanary
LeakCanary.install(this);
}
}
LeakCanary 2.0
的初始化放在了自带的ContentProvider
中:ContentProvide
r的onCreate
的调用时机介于Application的attachBaseContext和onCreate
之间,LeakCanary 2.0
将LeakCanary
的初始化放在了自带的ContentProvider
的onCreate
函数中,将multiprocess
设为false
可以保证ContentProvider
只初始化一次,LeakCanary
也只初始化一次
2.进入LeakCanary#install(Application application)
LeakCanary.java
public final class LeakCanary {
/**
* Creates a {@link RefWatcher} that works out of the box, and starts watching activity
* references (on ICS+).
*/
public static RefWatcher install(Application application) {
return refWatcher(application)
.listenerServiceClass(DisplayLeakService.class)//内存泄漏后用于显示的线上泄漏信息
.excludedRefs(AndroidExcludedRefs.createAppDefaults().build())//白名单
.buildAndInstall();
}
/** Builder to create a customized {@link RefWatcher} with appropriate Android defaults. */
public static AndroidRefWatcherBuilder refWatcher(Context context) {
return new AndroidRefWatcherBuilder(context);
}
}
在
install(Application application)
内部,通过refWatcher
创建了一个AndroidRefWatcherBuilder
对象。
由命名可以看出这是个builder模式,在阅读这种设计模式的代码时,有一些小技巧:在build()
方法调用前基本进行的就是一些变量赋值的操作,只需要留意build()
返回的对象即可,所以这里重点关注buildAndInstall()
的调用。
3 深入AndroidRefWatcherBuilder#buildAndInstall()
-
AndroidRefWatcherBuilder
继承RefWatcherBuilder
,在buildAndInstall()
中创建一个RefWatcher
(引用勘探者)实例,并使用静态方法ActivityRefWatcher#installOnIcsPlus()
将Application.ActivityLifecycleCallbacks
对象注册到Application
对象当中,每当有Activity
调用onActivityDestroyed
方法时,程序将回调RefWatcher
的watch(Activity activity)
方法。 - 内存泄漏的判断、分析以及泄漏信息的显示均在
RefWatcher#watch(Activity activity)
完成。
AndroidRefWatcherBuilder.java
/**
* Creates a {@link RefWatcher} instance and starts watching activity references (on ICS+).
*/
public RefWatcher buildAndInstall() {
RefWatcher refWatcher = build();//构建引用勘探者
ActivityRefWatcher.installOnIcsPlus((Application) context, refWatcher);
}
return refWatcher;
}
//: ActivityRefWatcher.java
@TargetApi(ICE_CREAM_SANDWICH) public final class ActivityRefWatcher {
public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
if (SDK_INT < ICE_CREAM_SANDWICH) {
// If you need to support Android < ICS, override onDestroy() in your base activity.
return;
}
ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
//完成Activity生命周期的注册工作
activityRefWatcher.watchActivities();
}
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
new Application.ActivityLifecycleCallbacks() {
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override public void onActivityStarted(Activity activity) {
}
@Override public void onActivityResumed(Activity activity) {
}
@Override public void onActivityPaused(Activity activity) {
}
@Override public void onActivityStopped(Activity activity) {
}
@Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override public void onActivityDestroyed(Activity activity) {
ActivityRefWatcher.this.onActivityDestroyed(activity);
}
};
private final Application application;
private final RefWatcher refWatcher;
/**
* Constructs an {@link ActivityRefWatcher} that will make sure the activities are not leaking
* after they have been destroyed.
*/
public ActivityRefWatcher(Application application, final RefWatcher refWatcher) {
this.application = checkNotNull(application, "application");
this.refWatcher = checkNotNull(refWatcher, "refWatcher");
}
void onActivityDestroyed(Activity activity) {
refWatcher.watch(activity);
}
public void watchActivities() {
// Make sure you don't get installed twice.
stopWatchingActivities();
application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
}
public void stopWatchingActivities() {
application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks);
}
}
4 RefWatcher#watch(Activity activity)
分析
上文讲到内存泄漏的判断、分析以及泄漏信息的显示均在RefWatcher#watch(Activity activity)
完成,因此不得不去此方法中一探究竟。
在正式开始源码之前有必要交代一下:
KeyedWeakReference
是WeakReference
的一个子类,它的作用是持有一个Activity
的弱引用并为每一个Activity
实例绑定一个全局唯一的Key
(具体采用的方法是调用UUID.randomUUID().toString()
;)ReferenceQueue
是一个队列
public final class RefWatcher {
public static final RefWatcher DISABLED = new RefWatcherBuilder<>().build();
private final WatchExecutor watchExecutor;
private final DebuggerControl debuggerControl;
//GC触发器,手动发起GC
private final GcTrigger gcTrigger;
private final HeapDumper heapDumper;
//与各个Activity关联的唯一UUID容器
private final Set<String> retainedKeys;
//引用队列,当Activity被正常回收时,该Activity的弱引用将被放入其中
private final ReferenceQueue<Object> queue;
private final HeapDump.Listener heapdumpListener;
//白名单,白名单内的对象可以有效持有Activity,避开内存检测
private final ExcludedRefs excludedRefs;
RefWatcher(WatchExecutor watchExecutor, DebuggerControl debuggerControl, GcTrigger gcTrigger,
HeapDumper heapDumper, HeapDump.Listener heapdumpListener, ExcludedRefs excludedRefs) {
this.watchExecutor = checkNotNull(watchExecutor, "watchExecutor");
this.debuggerControl = checkNotNull(debuggerControl, "debuggerControl");
this.gcTrigger = checkNotNull(gcTrigger, "gcTrigger");
this.heapDumper = checkNotNull(heapDumper, "heapDumper");
this.heapdumpListener = checkNotNull(heapdumpListener, "heapdumpListener");
this.excludedRefs = checkNotNull(excludedRefs, "excludedRefs");
retainedKeys = new CopyOnWriteArraySet<>();
queue = new ReferenceQueue<>();
}
/**
* Identical to {@link #watch(Object, String)} with an empty string reference name.
*
* @see #watch(Object, String)
*/
public void watch(Object watchedReference) {
watch(watchedReference, "");
}
/**
* Watches the provided references and checks if it can be GCed. This method is non blocking,
* the check is done on the {@link WatchExecutor} this {@link RefWatcher} has been constructed
* with.
*
* @param referenceName An logical identifier for the watched object.
*/
public void watch(Object watchedReference, String referenceName) {
if (this == DISABLED) {
return;
}
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
final long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();
retainedKeys.add(key);
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);
ensureGoneAsync(watchStartNanoTime, reference);
}
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
watchExecutor.execute(new Retryable() {
@Override public Retryable.Result run() {
return ensureGone(reference, watchStartNanoTime);
}
});
}
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
//在retainedKeys中移除进入quene中的KeyedWeakReference所对应的UUID
removeWeaklyReachableReferences();
if (debuggerControl.isDebuggerAttached()) {
// The debugger can create false leaks.
return RETRY;
}
//若retainedKeys中没有对应的UUID
if (gone(reference)) {
return DONE;
}
//若retainedKeys中存在对应的UUID,发起手动GC,并睡眠100ms
gcTrigger.runGc();
//再次在retainedKeys中移除进入quene中的KeyedWeakReference所对应的UUID
removeWeaklyReachableReferences();
//若retainedKeys中仍然存在对应的UUID,则开始内存泄漏和展示信息的工作
if (!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);
//内存泄漏后的信息分享,最终在DisplayLeakService#onHeapAnalyzed中通过Notification显示处理
heapdumpListener.analyze(
new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
gcDurationMs, heapDumpDurationMs));
}
return DONE;
}
//弱引用对应的UUID是否已被移除
private boolean gone(KeyedWeakReference reference) {
return !retainedKeys.contains(reference.key);
}
private void removeWeaklyReachableReferences() {
// WeakReferences are enqueued as soon as the object to which they >point to becomes weakly
// reachable. This is before finalization or garbage collection has >actually happened.
KeyedWeakReference ref;
//从被回收的对象的弱引用队列中取出一个弱引用对象,并在retainedKeys中移除对应的UUID
while ((ref = (KeyedWeakReference) queue.poll()) != null) {
retainedKeys.remove(ref.key);
}
}
}
当
Activity
的onDestory
方法被调用后,LeakCanary
将在RefWatcher
的retainedKeys
加入一条全局唯一的UUID
,同时创建一个该Activityd
的弱引用对象KeyedWeakReference
,并将UUID
写入KeyedWeakReference
实例中,同时KeyedWeakReference
与引用队列queue
进行关联,这样当Activity
对象正常回收时,该弱引用对象将进入队列当中。
循环遍历获取queue
队列中的KeyedWeakReference
对象ref
,将ref
中的UUID
取出,在retainedKeys
中移除该UUID
。如果遍历完成后retainedKeys
中仍然存在该弱引用的UUID
,则说明该Activity
对象在onDestory
调用后没有被正常回收。此时通过GcTrigger
手动发起一次GC
,再等待100ms,然后再次判断Activity
是否被正常回收,如果没有被回收,则开始内存泄漏和展示信息的工作。
拓展
RefWatch
可以用作监控任意的普通对象,如下:
在demoInstacne
需要被回收的位置监控其是否被正常回收。
LeakCanaryInternals.installedRefWatcher.watch(demoInstance);