本文目录
Java 语言的引用类型有这几种
强引用只要有被引用,虚拟机 gc 的时候是不会清理的,我们平时写的代码没有特别指明什么引用累心,都是强引用。所以如果代码有缺陷,强引用无法被 gc,就会出现内存泄漏,Leak 到一定的程度,就会出现 OOM(Out Of Memory Error)。
Java 的内存泄漏和 C++ 不一样。Java 虚拟机有自己的垃圾回收器,采用引用计数的方式,按照一定的策略回收 Java 堆中被引用数为 0 的对象。Java 没有局部的对象,所有对象的内存都在堆里,所以理论上是不需要我们去关心释放问题的。如果出现了某个对象,从业务逻辑上看已经不再使用,但是却仍有其他对象对其持有强引用,那这个对象的堆内存就无法回收了。在 Android 上,泄漏的规模如果达到 DVM 的限制,就会发生 OOM。
这个内存的限制和具体的手机 ROM 有关,我用小米5发现可以到 500M,而用的一个很旧的古董机只能 100多M。
为什么对象业务逻辑上没有用了,却还会被其他对象引用了?
每个对象在业务逻辑上都是有生命周期的。如果生命周期长的对象引用了声明周期短的对象,那生命周期短的对象什么时候可以释放内存就要看这个生命周期长的对象什么时候把引用释放。
如果这个生命周期长的对象长期持有引用,生命周期短的对象就不会及时回收。如果这个生命周期长的对象持有了多个生命周期短的对象,那在内存上的表现,就会出现增长,如果控制不好就会 OOM。等生命周期长的对象释放这些引用了,比如这个生命周期长的对象可回收并被 gc 的时候,短暂泄漏的那些内存就会被释放。
如果代码没写好,把生命周期短的对象放到了静态变量中,又没有去释放,那很遗憾,这块内存就会一直被占用,就是真正意义上的泄漏了。
举个简单的例子,比如把 Drawable 放到了某个类的静态成员变量中
public void classA {
private static Drawable sDrawable;
public static setDrawable(Drawable drawable) {
sDrawble = drawable;
}
}
我们知道 Drawble 的创建是需要上下文 Context 的,而我们一般都会用当前 Activity 为 Context 传入。像上面的代码,会导致整个 Activity 在业务逻辑上退出后,内存无法被 gc 回收。
最好的做法是,不要这样写代码,换个方式。可是总是有业务或者算法实现上,就是要这样做,比如要建立缓存,又或者有个延时比较长的回调,这时候就可以采用软引用或者弱引用了。
我们决定用这两种引用来解决问题。具体使用哪种,则看业务场景的需要
但是使用的时候还是要注意,避免写出 NullPointerException 的概率空指针代码,比如
public void classB {
private final WeakReference weakObj;
public classB(Activity obj) {
weabObj = new WeakReference(obj);
}
void run() {
if (weakObj.get() != null && !weakObj.get().isFinishing()) {
...
}
}
为什么会有概率空指针呢,因为弱引用的回收时候是不确定的,甚至在一两行代码的执行期间都有可能发生。所以,在使用的时候,需要用一个局部变量的强引用取出,防止中途发生 gc 而出现异常,正确的写法
void run() {
Activity activity = weakObj.get();
if (activity != null && !activity.isFinishing()) {
...
}
}
根本上,写 Android 代码的时候一定要保持警觉,比如
这些内部类也会隐式持有当前对象的引用,有可能会发生泄漏。
而这些引用的具体场景具体分析。因为如果是一些性能差的低端机,内存紧张,gc 跑得很频繁,会在当前页面明明页面还在执行,那些引用就被回收掉了。在表现上可能就是,使用了弱引用的回调没有执行,或者取到的对象为 null。
软引用和弱引用虽然美好,但要慎用。