内存泄漏
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
为什么会内存泄漏
不同场景下产生内存泄漏的原因都不一样,但基本原因都是相似的:
申请了资源但在释放之前丢失了对该资源的引用,导致资源在程序运行结束前一直无法释放
这里的资源可以是内存、File Descriptor等等。
C系语言中的内存泄漏
C系语言一般都是自己管理内存,很容易产生内存泄漏。
常见的内存泄漏场景:
- 内存没有释放,直接将指针赋值为nullptr
class Test {
public:
int* a = new int(1);
~Test() {
delete a;
cout << "已经析构" << endl;
}
};
int main(){
auto* test = new Test();
test = nullptr;
//内存泄漏,程序终止前,test实例永远无法被回收
while (1)
{
//do other things
}
return 0;
}
- 函数中开辟的内存,返回前没有释放,但却丢失了引用
class Test {
public:
int* a = new int(1);
~Test() {
delete a;
cout << "已经析构" << endl;
}
};
void leak(){
//函数返回后,test指针被销毁,丢失引用,但new Test()占用的内存不会被释放
auto* test = new Test();
//delete test;
}
int main(){
leak();
//do other things
return 0;
}
- 打开的文件没有关闭
这里主要是指file descriptor没有close,导致系统认为一直有进程引用该文件,便不会释放对应资源。这里的文件不仅仅指磁盘上的文件,还包括socket等,因为Unix下一切皆文件。
Java中的内存泄漏
Java拥有虚拟机帮我们自动管理内存,会自动回收不再使用的对象。除了File等外部资源没有释放以外,很少会因为对内存丢失引用而导致内存泄漏。
但是正因为虚拟机帮我们自动管理内存,我们无法直接(或者很不方便)的告诉虚拟机哪些对象我们已经不再使用,从而导致某些对象虽然我们已经永远不会再使用,但虚拟机却认为我们还在使用而不进行回收。
要说清楚这个问题我们要先了解一下基本的GC原理。
常用的内存自动回收算法
引用计数法
顾名思义,我们会统计对象引用的数量,每有一个引用count就加1,取消引用就-1;
如果数量为0就代表该对象已经不再使用,便可以回收。
缺点:会产生循环引用。
下面会用C++来实现一个简单的引用计数指针来说明原理
实现简单的引用计数:
template <class T> class CountReference {
private:
class Wrapper {
public:
T* _ptr;
int refCount;
Wrapper(T* ptr) : _ptr(ptr), refCount(1) {}
~Wrapper() {
delete this->_ptr;
}
};
Wrapper* wrapper = nullptr;
public:
CountReference() {}
CountReference(T* ptr) { this->wrapper = new Wrapper(ptr); }
CountReference(const CountReference& other) {
this->wrapper = other.wrapper;
this->wrapper->refCount++;
}
CountReference& operator=(const CountReference& other) {
if (this->wrapper == other.wrapper) {
return *this;
}
if (this->wrapper != nullptr && --this->wrapper->refCount == 0) {
delete this->wrapper;
}
this->wrapper = other.wrapper;
if (this->wrapper != nullptr) {
this->wrapper->refCount++;
}
return *this;
}
T& operator*() {
return *this->wrapper->_ptr;
}
T* operator->() {
return this->wrapper->_ptr;
}
~CountReference() {
if (--this->wrapper->refCount == 0) {
delete this->wrapper;
}
else {
cout << "目前还有" << (this->wrapper->refCount) << "个引用" << endl;
}
}
};
测试:
class Test {
public:
int* a = new int(1);
~Test() {
delete a;
cout << "已经析构" << endl;
}
};
void leak() {
CountReference<Test> re(new Test());
}
int main() {
leak();
// do other things
return 0;
}
运行结果:
缺点:循环引用
class TestA {
public:
CountReference<TestB> cb;
~TestA() {
cout << "已经析构A" << endl;
}
};
class TestB {
public:
CountReference<TestA> ca;
~TestB(){
cout << "已经析构B" << endl;
}
};
void leak() {
CountReference<TestA> a(new TestA());
CountReference<TestB> b(new TestB());
a->cb = b;
b->ca = a;
}
int main() {
leak();
// do other things
return 0;
}
函数调用后,Test对象无法被析构导致内存泄漏。
可达性分析
GC堆内维持一组GC Root对象,如果某个对象通过GC Root引用链无法到达,说明可以被GC回收。
Java中常用的GC Root对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。
- 方法区中类静态属性引用的对象;java 类的引用类型静态变量。
- 方法区中常量引用的对象;比如:字符串常量池里的引用。
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
- JVM 的内部引用(class 对象、异常对象 NullPointException、OutofMemoryError,系统类加载器)。(非重点)
- 所有被同步锁(synchronized 关键)持有的对象。(非重点)
- JVM 内部的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。(非重点)
- JVM 实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收只回收部分代的对象,这个后续会细讲,先大致了解概念)。(非重点)
安卓中常见的内存泄漏
Activity导致的内存泄露
Activity从回退栈中弹出时系统就不可达,再次进入页面就会创建一个新的Activity实例。如果Activity在回退栈中弹出时有其它对象强引用着,就会对象无法被GC回收,导致内存泄漏。
注意:
- 所以不要随意的将Activity作为Context传递给其它安卓生命周期之外的Java对象,如果一个对象需要Context时要先考虑自己的生命周期,是需要Activity还是ApplicationContext。
- 不要轻易让静态变量持有Activity的Context,除非该Activity的生命周期和应用一样。
Handler导致的泄漏
Handler原理
Looper、Handler、Message、MessageQueue都是相互关联在一起的。
其中Looper通过ThreadLocal和每个线程进行绑定,其中MainLooper是系统直接创建好的,其它线程要通过Looper.prepare()创建,绑定到调用的线程。每个Looper都会有一个MessageQueue。
//Looper.java
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
//消息队列
final MessageQueue mQueue;
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
每次创建Handler时我们都会传入对应的Looper,然后Handler就与对应的线程进行绑定。
当我们调用Handler.sendMessage(Message)时会把Handler放入到对应的MessageQueue,Looper不断的从MessageQueue中取出消息进行处理。
//Handler.java
//消息队列,对应的是Looper中的queue
final MessageQueue mQueue;
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
mLooper = looper;
//复制queue
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
//将Handler绑定到Message中
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
当我们调用Looper.loop()时,就会用一个死循环,不断的从队列中取出消息并分发给Handler
//Looper.java
public static void loop() {
final Looper me = myLooper();
//...
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
}
private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride) {
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}
//...
try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
}
//...
return true;
}
注意加黑的代码,其中的Message.target就是调用sendMessage(Message)时的Handler。我们在sendMessage的时候会将让Message持有一个Handler的引用,只有这样,我们才能在Looper中追踪到是哪个Handler发送的消息并dispatch给它。因为一个线程中可能有多个Handler。
原因分析
上面分析过Handler的原理后简单看一下常用的使用方式:
//匿名内部类会持有外部类的引用
val handler = object: Handler(this.mainLooper){
override fun handleMessage(msg: Message) {
when(msg.what){
else-> { /*do something*/ }
}
}
}
//另外线程中发生消息
Thread {
Thread.sleep(1000)
handler.sendMessage(Message())
} .start()
这里我们创建了一个匿名内部类,而Java/Kotlin中的匿名内部类都会持有一个外部类的引用(匿名内部类可以任意访问外部的变量)。这就意味着,如果这个匿名对象不被回收,外部的Activity就永远不会被回收。
上面说过,Message中会持有Handler的引用,而Message会被放在MessageQueue中,可能延迟执行,或者send时Activity已经提前destroy,就会导致Activity无法被回收,引用链如下:
Activity <-- Handler <-- Message <-- MessageQueue
注意:
严格来说,Handler导致的内存泄漏并不会一直占用内存,因为Message不会一直在队列中,时间到了就会被处理。当Message从MessageQueue中取出被处理掉后,下次GC时Activity就可以被回收。
当然,如果其间内频繁的重建Activity就会导致内存占用快速上升甚至导致OOM。
解决方案
知道内存泄漏的原理后处理方案也就很简单了:
- 使用静态内部类
匿名内部类对外部类的引用导致的Activity无法被回收,那么我们可以使用静态内部类来避免对外部类的引用。
但我们在Activity中使用Handler的主要目的就是用来和Activity交互,如果不使用Activity就有点本末倒置了,所以还要再加一个WeakReference
- 使用WeakReference
WeakReference的作用下面会讲,现在只要知道,该引用不会阻止GC回收就行了。
public static class MyHandler extends Handler {
private WeakReference<MainActivity2> activityWeakReference;
public MyHandler(Looper looper, MainActivity2 activity) {
super(looper);
this.activityWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(@NonNull Message msg) {
MainActivity2 activity2 = activityWeakReference.get();
if (activity2 != null) {
//...
}
}
}
Kotlin中默认的就是静态内部类,直接使用即可
class MyHandler(
looper: Looper,
activity: MainActivity
) : Handler(looper) {
private val reference = WeakReference(activity)
override fun handleMessage(msg: Message) {
reference.get()?.apply {
//...
}
}
}
- 清除消息队列
上面的两个方法可以解决Activity不被回收的问题,但是Handler仍然由于引用存在而不会被回收,如果Activity销毁后我们不再需要处理消息,最好把消息队列清空。这样引用链就不存在了,可以直接回收掉Handler。
override fun onDestroy() {
super.onDestroy()
//清除该Handler的消息
handler.removeCallbacksAndMessages()
}
Java的四种引用
虽然JVM不支持手动管理内存的释放,但是却给我们提供了四种引用类型,用来标识哪些对象可回收。这四种引用分别是:强引用、软引用、弱引用、虚引用。
StrongReference
我们正常使用的引用都是强引用,从GCRoot强引用链可达的对象是绝对不会被GC回收的。
SoftReference
相比于强引用的永远不回收。当虚拟机内存紧缺时会保证在抛出OOM之前先回收掉软引用的指向的对象。
var softReference = new SoftReference<>(new Test());
var test = softReference.get();
//当回收掉时,get()会返回null
if (test != null){
test.test();
}
注意:OpenJdk的doc中建议使用软引用来实现缓存,
但是android的doc中修改为:避免使用软引用实现缓存,给出的理由是,虚拟机对待软引用时不知道该回收还是扩展内存。当回收太早时会重新加载数据,产生不必要的操作。回收太晚又会浪费内存。
WeakReference
弱引用比软引用更弱,表明指向的对象可以被随时回收掉而不用等到内存紧缺时才回收。使用方法和软引用一样。
PhantomReference
虚引用比较特殊,它不是用来引用对象的,而更像是对finalize的一种替代,用来自定义对象被销毁时清除资源。因为原有的finalize方法有很多问题,尤其是其中一个对象可以重新让自己被强引用而避免被回收,且finalize不保证一定会执行。
虚引用其中的对象无法被获取,get()会一直返回null。
如何从引用中判断对象是否被回收?
上面我们使用软引用和虚引用时都是通过判断get()是否null来确定对象是否已经被回收,但是虚引用却无法使用,因为虚引用一直都返回null。
- ReferenceQueue
实际上,对于每种引用,当指向的对象被回收时都会将自身添加到一个关联的ReferenceQueue里。
var queue = new ReferenceQueue<Test>();
var softReference = new SoftReference<>(new Test(),queue);
//对象已经被回收
while (queue.poll() != null){
queue.remove();
}
- boolean Reference.refersTo(Object)
Java16添加的新方法,用来判断该引用是否指向传入的对象,当传入null,且返回true时,说明已经被回收。
利用以上两种方法我们可以用来监听一个对象是否已经被回收。