1 一个小例子
首先,来看一个小例子。新创建一个Android工程项目,在Activity里面增加两个按钮,一个按钮用于产生内存泄漏,一个按钮用于对比将会把对象释放:
创建一个类LeakTest,用于该例子的相关测试业务,这里先省略相关方法的具体实现:
object LeakTest {
// 制造内存泄露
fun makeLeak(activity: Activity) {
...
}
// 及时释放对象
fun makeRelease(activityRef: WeakReference) {
...
}
// 执行垃圾回收
fun gc() {
...
}
}
对应的MainActivity代码如下:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun makeLeak(view: View) {
LeakTest.makeLeak(this)
finish()
}
fun makeRelease(view: View) {
LeakTest.makeRelease(WeakReference(this))
finish()
}
override fun onDestroy() {
super.onDestroy()
Log.i(LOG_CAT, "onDestroy: ")
LeakTest.gc()
}
}
从例子中可以看出,无论我们点击“内存泄露”还是“及时释放”按钮,都会调用finish,随之在MainActivity的onDestroy后面会执行垃圾回收,在垃圾回收之后,我们再来检查相关对象的结果:
先看makeLeak方法:
fun makeLeak(activity: Activity) {
thread {
Thread.sleep(5000)
Log.i(LOG_CAT, "makeLeak: activity: $activity")
}
}
其打印信息如下:
2021-01-21 17:38:50.978 16315-16315/com.example.leakcanaryanalyze I/My-LOG: onDestroy:
2021-01-21 17:38:52.054 16315-16400/com.example.leakcanaryanalyze I/akcanaryanalyz: Explicit concurrent copying GC freed 5432(286KB) AllocSpace objects, 1(20KB) LOS objects, 24% free, 3092KB/4122KB, paused 62us total 34.937ms
2021-01-21 17:38:55.385 16315-16398/com.example.leakcanaryanalyze I/My-LOG: makeLeak: activity: com.example.leakcanaryanalyze.MainActivity@8be98e5
这三条打印信息说明:
- 在onDestory方法后面执行了垃圾回收
- 由于activity在onDestory方法执行后就无效了,因此我们期望activity在被gc回收了。
- 第三行日志显示activity不为null,所以判断内存泄露了。
注:需要注意的是,thread{} 代码块可以看成是一个匿名内部类,内部类会讲外层的局部变量拷贝一份的,因此thread{}代码块持有了activity的实例。
换一种方式,执行makeRelease:
fun makeRelease(activityRef: WeakReference) {
thread {
Thread.sleep(5000)
Log.i(LOG_CAT, "makeRelease: activity: ${activityRef.get()}")
}
}
执行结果如下:
2021-01-21 17:41:50.336 16315-16315/com.example.leakcanaryanalyze I/My-LOG: onDestroy:
2021-01-21 17:41:51.385 16315-16460/com.example.leakcanaryanalyze I/akcanaryanalyz: Explicit concurrent copying GC freed 5428(319KB) AllocSpace objects, 1(20KB) LOS objects, 24% free, 3103KB/4137KB, paused 223us total 47.406ms
2021-01-21 17:41:54.745 16315-16458/com.example.leakcanaryanalyze I/My-LOG: makeRelease: activity: null
发现最后activity被成功回收了。
通过这个例子大概可以获得如下几点信息:
- 如果一个对象(例子中的activity)已经没用了,但是却驻留在内存中,那么这便是发生了内存泄露
- 那么肯定会有某种机制会去判定一个对象是否应该释放已经将它释放,这便是垃圾回收机制
- 通过WeakReference,可以做到不影响无用对象被释放
2 内存泄露
那么什么是内存泄露呢?一言以蔽之,如果一个对象没有用处了,但是它却不能被垃圾回收机制回收掉,这就是内存泄漏。常见的内存泄漏是将Activity的实例赋值给一个静态变量,或者存储在一个全局单例中。
关于Android常见的内存泄露原因可以参考这篇文章:Android内存泄漏的八种可能
3 垃圾回收机制
一般的垃圾回收算法有引用计数算法和可达性分析算法。引用计数法是这样的,在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器值为零的对象就是不可能再被使用的。然而,单纯的引用计数法将会带来循环引用的问题,例如下面例子,当本意是释放a和b时,却无法得到正确释放:
class A{
B b
}
class B {
A a
}
A a = new A();
B b = new B();
a.b = b;
b.a = a;
a = null;
b = null;
Java使用的是可达性分析算法(Reachability Analysis),这个算法的思路大致是:通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的画来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
图中对象5、6、7虽然互有关联,但是他们到GC Roots是不可达的,所以会判定为可被回收的。
4 GC Root的类型
在Java技术体系里,固定可作为GC Roots的对象包括一下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如某个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中类静态变量属性引用的对象,譬如Java类的引用类型静态变量
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。
- 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
5 引用类型和引用队列
Java分为四种引用类型:
- 强引用:也就是我们常使用的new一个对象然后直接赋值,对于这样的对象,只要对于GC Roots可达就不会被回收,如开头例子中的activity: Activity
- 软引用(SoftReference):它的使用方式是将强引用作为参数传入它的构造方法。对于软引用所持有的对象,当GC工作时,只有当内存不够时,会被回收。在Java中通常使用软引用实现缓存,但是在Android中,由于内存紧张,会对软引用执行更严格的策略,因此在Android中实现缓存需要使用LruCache。
- 弱引用(WeakReference):当GC工作时,无论是否可达,都会被回收。
- 虚引用(PhantomReference):最弱的一种引用关系,它的唯一作用只是为了能在这个对象被收集器回收时收到一个系统通知。
通过以上四种引用类型的分析,结合最开始的例子,我们知道,使用WeakReference即能够在对象的生命周期内获得它,也能够在垃圾回收时,不会阻碍对象被回收。
另外,除了强引用,其他三种引用类型的都还有一个带ReferenceQueue的构造方法,例如WeakReference:
public class WeakReference extends Reference {
public WeakReference(T referent) {
super(referent);
}
public WeakReference(T referent, ReferenceQueue super T> q) {
super(referent, q);
}
}
那么ReferenceQueue对象是做什么用的呢?它的作用是,当weakReference所指的引用对象被释放的时候,weakReference会将自己入队到ReferenceQueue中;
我们修改下第一个例子的makeRelease方法:
fun makeRelease(view: View) {
val referenceQueue = ReferenceQueue()
val weakReference = WeakReference(this, referenceQueue)
Log.i(LOG_CAT, "makeRelease: weakReference: $weakReference")
LeakTest.makeRelease(weakReference)
Log.i(LOG_CAT, "makeRelease: referenceQueue: " + referenceQueue.poll())
thread {
Thread.sleep(5500)
Log.i(LOG_CAT, "makeRelease: referenceQueue: " + referenceQueue.poll())
}
finish()
}
打印结果如下:
2021-01-22 16:03:13.482 29296-29296/com.example.leakcanaryanalyze I/My-LOG: makeRelease: weakReference: java.lang.ref.WeakReference@9b7b957
2021-01-22 16:03:13.485 29296-29296/com.example.leakcanaryanalyze I/My-LOG: makeRelease: referenceQueue: null
2021-01-22 16:03:14.128 29296-29296/com.example.leakcanaryanalyze I/My-LOG: onDestroy:
2021-01-22 16:03:15.191 29296-29522/com.example.leakcanaryanalyze I/akcanaryanalyz: Explicit concurrent copying GC freed 1620(166KB) AllocSpace objects, 0(0B) LOS objects, 24% free, 3058KB/4077KB, paused 63us total 30.015ms
2021-01-22 16:03:18.486 29296-29515/com.example.leakcanaryanalyze I/My-LOG: makeRelease: activity: null
2021-01-22 16:03:18.987 29296-29516/com.example.leakcanaryanalyze I/My-LOG: makeRelease: referenceQueue: java.lang.ref.WeakReference@9b7b957
在finish之前查看队列中的元素是为空的,等了5500ms后,这个时候activity已经被释放了,此时队列中存在WeakReference的对象,即我们之前的对象(@9b7b957);
有了WeakReference和ReferenceQueue,便可以监听一个对象是否有被正确回收了,这里还是以Activity为例,大概思路如下:
- 当一个Activity对象的onDestroy回调方法执行后,可以认定这个Activity对象无用了,应该被垃圾回收器回收。
- 在onDestory中,将该Activity对象交给一个WeakReference对象,另外创建一个ReferenceQueue对象传入WeakReference对象中。
- 执行垃圾回收机制,此时Activity对象会被回收
- 如果Activity对象被正确回收了,那么WeakReference对象就会将自己入队ReferenceQueue中,因此,如果ReferenceQueue不为空,则表明对象被回收了,反之则表示泄漏了。