发布时间:2023-11-19 16:30
内存泄露(Memory Leak)简单来说,就是该释放或回收的资源没有主动去释放或回收,导致GC也无法回收,最后永远无法正常回收,造成系统资源浪费。内存泄露会积累放大影响,严重时会导致内存溢出,引起程序卡死崩溃等。
和内存泄露有关的常见场景如下:
下面将结合示例代码、MAT分析、源码,对以上常见情况逐一讲解。
定义一个最简单的非静态内部类,执行javac
编译其class文件:
尽管写在了一个java文件中,编译后仍然拆分成两个class文件,并且观察内部类的class文件,发现编译器自动生成了一个有参构造器,参数为外部类,因此,当在外部类中实例化内部类对象时,类加载器会调用该构造器传入外部类的this
引用,即非静态内部类持有外部引用。
当内部类的生命周期大于外部类时,外部类就无法进行销毁,导致内存泄露,下面举例说明:
在Activity中,通过非静态内部类的方式定义线程类,实例化其对象并运行,Activity主动调用finish
方法进行销毁,抓取hprof文件:
DemoActivity实例的GC最短路径:
InnerClass实例的GC最短路径:
由于mInnerClass
对象还在执行线程活动,它又持有外部类DemoActivity实例的引用,因此this$0
即外部DemoActivity的实例无法被回收。
综上,当我们使用非静态内部类时,一定要在外部类销毁时主动释放内部类的资源,避免因内部类生命周期大于外部类导致内存泄露,而对于像线程这种无法主动停止、释放资源的例子,可采用静态内部类+弱引用的方式实现。
定义一个匿名内部类,执行javac
编译其class文件:
观察匿名内部类的class文件,发现编译器自动生成了一个有参构造器,参数为外部类,结合上一节内容,可得知,匿名内部类同样持有外部类的实例,同样存在内存泄露的风险,原理相同,此处不再赘述。
在Activity中,以静态内部类的方式定义广播接收器,实例化其对象并注册广播,Activity主动调用finish
方法进行销毁,抓取hprof文件无任何内存泄露现象,分析registerReceiver
源码:
registerReceiver
方法具体实现位于ContextImpl类中mPackageInfo
会在ContextImpl对象实例化时赋值,因此通常是通过mPackageInfo
去获得ReceiverDispatcher实例,但是如果首次调用没有获取到实例,则仍然会创建该实例ReceiverDispatcher对象内部包含了当前调用方的主线程(mActivityThread
)和用于传给AMS进行注册的binder(mIIntentReceiver
)。
由于入参registered
为true,因此InnerReceiver在实例化时通过弱引用方式持有receiver对象,这也就解释了为什么我们的DemoActivity实例在销毁时没有出现mReceiver
对象的内存泄露。
虽然应用层没有泄露,但是框架层仍然有泄露问题,接下来看AMS中:
// Maximum number of receivers an app can register.
private static final int MAX_RECEIVERS_ALLOWED_PER_APP = 1000;
/**
* Keeps track of all IIntentReceivers that have been registered for broadcasts.
* Hash keys are the receiver IBinder, hash value is a ReceiverList.
*/
final HashMap<IBinder, ReceiverList> mRegisteredReceivers = new HashMap<>();
public Intent registerReceiver(IApplicationThread caller, String callerPackage,
IIntentReceiver receiver, IntentFilter filter, String permission, int userId,
int flags) {
// TODO
ReceiverList rl = mRegisteredReceivers.get(receiver.asBinder());
if (rl == null) {
rl = new ReceiverList(this, callerApp, callingPid, callingUid,
userId, receiver);
if (rl.app != null) {
// 如果该进程活跃的receiver数量达到1000,即AMS中该进程的接收器binder对象达到1000个,
// 则会抛出该异常,这就印证了反注册广播接收器的必要性
final int totalReceiversForApp = rl.app.receivers.size();
if (totalReceiversForApp >= MAX_RECEIVERS_ALLOWED_PER_APP) {
throw new IllegalStateException(\"Too many receivers, total of \"
+ totalReceiversForApp + \", registered for pid: \"
+ rl.pid + \", callerPackage: \" + callerPackage);
}
rl.app.receivers.add(rl);
} else {
// 如果进程已经挂掉,则销毁这些binder对象
try {
receiver.asBinder().linkToDeath(rl, 0);
} catch (RemoteException e) {
return sticky;
}
rl.linkedToDeath = true;
}
mRegisteredReceivers.put(receiver.asBinder(), rl);
} else if (rl.uid != callingUid) {
throw new IllegalArgumentException(
\"Receiver requested to register for uid \" + callingUid
+ \" was previously registered for uid \" + rl.uid
+ \" callerPackage is \" + callerPackage);
} else if (rl.pid != callingPid) {
throw new IllegalArgumentException(
\"Receiver requested to register for pid \" + callingPid
+ \" was previously registered for pid \" + rl.pid
+ \" callerPackage is \" + callerPackage);
} else if (rl.userId != userId) {
throw new IllegalArgumentException(
\"Receiver requested to register for user \" + userId
+ \" was previously registered for user \" + rl.userId
+ \" callerPackage is \" + callerPackage);
}
BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage,
permission, callingUid, userId, instantApp, visibleToInstantApps);
if (rl.containsFilter(filter)) {
Slog.w(TAG, \"Receiver with filter \" + filter
+ \" already registered for pid \" + rl.pid
+ \", callerPackage is \" + callerPackage);
} else {
rl.add(bf);
// TODO
mReceiverResolver.addFilter(bf);
}
}
mRegisteredReceivers
,key为传过来的IIntentReceiver的binder对象,而value是由这个IIntentReceiver构造的注册数组ReceiverList。终上所述,当我们在应用层忘记反注册广播接收器时,虽然应用层不会因此导致内存泄露,但是框架层仍然发生了泄露,且该泄露会累积影响,严重时导致应用异常崩溃。
除此之外,我们也明白了为什么自Android O开始,不允许应用注册静态广播,原因之一便是静态广播对系统资源的浪费,我们无法主动去反注册该接收器。
截取自developers-广播限制
正确的做法是,在适当的时候反注册广播,及时释放相关资源:
@Override
protected void onDestroy() {
super.onDestroy();
if (mReceiver != null) {
unregisterReceiver(mReceiver);
}
}
在Activity中,通过静态内部类的方式定义内容观察者,实例化其对象并注册监听,Activity主动调用finish
方法进行销毁,抓取hprof文件:
DemoObserver实例的GC最短路径:
此处发生内存泄露的mContentObserver
对象即mObserver
自身,其被ContentObserver内部的Transport对象(本质上是一个binder)反向引用:
分析registerContentObserver
源码:
然后调用到了框架层ContentService中:
private final ObserverNode mRootNode = new ObserverNode(\"\");
public void registerContentObserver(Uri uri, boolean notifyForDescendants,
IContentObserver observer, int userHandle, int targetSdkVersion) {
// TODO
mRootNode.addObserverLocked(uri, observer, notifyForDescendants, mRootNode,
uid, pid, userHandle);
// TODO
}
public static final class ObserverNode {
private class ObserverEntry implements IBinder.DeathRecipient {...}
private ArrayList<ObserverNode> mChildren = new ArrayList<ObserverNode>();
private ArrayList<ObserverEntry> mObservers = new ArrayList<ObserverEntry>();
public void addObserverLocked(Uri uri, IContentObserver observer,
boolean notifyForDescendants, Object observersLock,
int uid, int pid, int userHandle) {
addObserverLocked(uri, 0, observer, notifyForDescendants, observersLock,
uid, pid, userHandle);
}
private void addObserverLocked(Uri uri, int index, IContentObserver observer,
boolean notifyForDescendants, Object observersLock,
int uid, int pid, int userHandle) {
// If this is the leaf node add the observer
if (index == countUriSegments(uri)) {
mObservers.add(new ObserverEntry(observer, notifyForDescendants, observersLock,
uid, pid, userHandle, uri));
return;
}
// Look to see if the proper child already exists
String segment = getUriSegment(uri, index);
if (segment == null) {
throw new IllegalArgumentException(\"Invalid Uri (\" + uri + \") used for observer\");
}
int N = mChildren.size();
for (int i = 0; i < N; i++) {
ObserverNode node = mChildren.get(i);
if (node.mName.equals(segment)) {
node.addObserverLocked(uri, index + 1, observer, notifyForDescendants,
observersLock, uid, pid, userHandle);
return;
}
}
// No child found, create one
ObserverNode node = new ObserverNode(segment);
mChildren.add(node);
node.addObserverLocked(uri, index + 1, observer, notifyForDescendants,
observersLock, uid, pid, userHandle);
}
}
简而言之,将binder封装在ObserverEntry对象中,加入到mObservers
数组,也就是说,框架层持有了应用层注册时的observer对象的binder引用,也就间接地持有了observer对象的引用,导致该observer对象无法回收,引起内存泄露。因此,我们应该主动去反注册observer,释放其资源:
@Override
protected void onDestroy() {
super.onDestroy();
if (mObserver != null) {
getContentResolver().unregisterContentObserver(mObserver);
}
}
在Activity中,通过ContentResolver对象获取查询操作的Cursor对象,Activity主动调用finish
方法进行销毁,抓取hprof文件无任何内存泄露现象,首先分析Activity中的getContentResolver
方法:
getContentResolver
具体实现位于ContextImpl类中,返回的对象为静态内部类ApplicationContentResolver的实例:
其实例化位于构造方法中。
接下来分析ContentResolver类中query
源码:
public final @Nullable Cursor query(...) {
try {
// 默认构造方法实例化时mWrapped对象为null
if (mWrapped != null) {
return mWrapped.query(uri, projection, queryArgs, cancellationSignal);
}
} catch (RemoteException e) {
return null;
}
IContentProvider unstableProvider = acquireUnstableProvider(uri);
if (unstableProvider == null) {
return null;
}
IContentProvider stableProvider = null;
Cursor qCursor = null;
try {
// TODO
// 分两种不同情况去获取qCursor对象
try {
qCursor = unstableProvider.query(mPackageName, uri, projection,
queryArgs, remoteCancellationSignal);
} catch (DeadObjectException e) {
unstableProviderDied(unstableProvider);
stableProvider = acquireProvider(uri);
if (stableProvider == null) {
return null;
}
qCursor = stableProvider.query(
mPackageName, uri, projection, queryArgs, remoteCancellationSignal);
}
if (qCursor == null) {
return null;
}
// TODO
// Wrap the cursor object into CursorWrapperInner object.
final IContentProvider provider = (stableProvider != null) ? stableProvider
: acquireProvider(uri);
// 此处采用委派模式,CursorWrapperInner本身也是实现的Cursor接口
// 将qCursor对象包装成CursorWrapperInner对象
final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider);
stableProvider = null;
qCursor = null;
return wrapper;
// TODO
} catch (RemoteException e) {
// Arbitrary and not worth documenting, as Activity
// Manager will kill this process shortly anyway.
return null;
} finally {
// 及时回收无用资源
if (qCursor != null) {
qCursor.close();
}
if (cancellationSignal != null) {
cancellationSignal.setRemote(null);
}
if (unstableProvider != null) {
releaseUnstableProvider(unstableProvider);
}
if (stableProvider != null) {
releaseProvider(stableProvider);
}
}
}
其中的acquireUnstableProvider
和acquireProvider
方法均为ApplicationContentResolver中的调用,并且都是调用的ActivityThread的acquireProvider
方法:
ContentProviderHolder内部持有一个指向AMS中对应provider的IContentProvider
实例和用于跟应用层通信的binder对象connection
,其创建过程均位于框架层AMS中,此处不进行发散。
因此,当我们通过ContentResolver
对ContentProvider进行操作时,框架层会返回binder对象,应用层会将其包装为CursorWrapperInner对象,那么,按理说如果我们没有主动释放资源,框架层将会发生内存泄露,这个binder未被及时回收,但是实际却没有发生,于是我们继续分析包装类CursorWrapperInner:
其中重写了finalize
方法,这意味着当GC轮询到该对象时,会去执行close
方法,也就是Cursor的close
方法,对资源进行释放,回收binder。但是这样一来,问题就非常明显,尽管Android本身有这种保障机制进行回收,但其时机相对于主动释放,晚了许多,因此该方法中会抛出警告,提醒开发者及时调用close
主动释放Cursor资源。
所以,当我们使用Cursor时,应该在操作结束后及时关闭:
Cursor cursor = null;
try {
cursor = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder, null);
// TODO
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
在Activity中,以静态内部类的方式定义一个Handler,实例化为mHandler
并执行一个延时任务,在任务开始执行之前主动调用finish
方法进行销毁,抓取hprof文件:
StaticHandler实例的GC最短路径:
主线程消息队列中有一个Message实例持有一个target
强引用,这个target
即mHandler
,而Message实例也就是sendMessageDelayed
传入的那个对象。
下面将结合Handler源码分析:
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
// TODO
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
// TODO
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
// 此处Message反向引用了Handler对象
msg.target = this;
// TODO
return queue.enqueueMessage(msg, uptimeMillis);
}
/**
* post方法本质上是发送了一个匿名Message到消息队列中
*/
public final boolean post(@NonNull Runnable r) {
return sendMessageDelayed(getPostMessage(r), 0);
}
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
由Handler内部的调用链可知,Message实例会持有Handler实例的强引用,用于消息队列轮询到该消息时执行回调,只要该Message实例没有消费结束被回收,Handler实例便无法回收,导致该对象内存泄露。
正确的做法是在适当的时候主动移除Handler中剩余的消息,释放资源,以便回收:
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
自定义一个View,在Attach到窗口的回调中执行一个延时任务。在Activity布局中使用该自定义View,在延时任务开始执行之前主动调用finish
方法进行销毁,抓取hprof文件:
DemoTextView实例的GC最短路径:
DemoActivity实例的GC最短路径:
DemoRunnable实例的GC最短路径:
target
成员变量为ViewRootImpl中的内部类ViewRootHandler实例activity
对象,即DemoActivity实例mDemo
对象callback
成员变量指向了mRunnable
对象。因此只有当该Message实例被回收时,才能解引用,最终回收上述三个发生泄露的对象。
接下来结合View的源码分析,Message实例是如何导致mDemo
对象泄露:
public boolean postDelayed(Runnable action, long delayMillis) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.postDelayed(action, delayMillis);
}
// TODO
}
根据View的绘制流程和异步消息流程,此处的mAttachInfo
是在ViewRootImpl实例绘制View实例的第一帧时调用View的dispatchAttachedToWindow
方法赋值的,因此分析ViewRootImpl源码:
final class ViewRootHandler extends Handler {...}
final ViewRootHandler mHandler = new ViewRootHandler();
public ViewRootImpl(Context context, Display display) {
// TODO
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);
// TODO
}
mAttachInfo
中的成员变量mHandler
即ViewRootHandler的实例,因此间接持有ViewRootImpl实例的引用ViewRootImpl中有一个方法,用于和Activity实例建立关联:
public void setActivityConfigCallback(ActivityConfigCallback callback) {
mActivityConfigCallback = callback;
}
这个方法由PhoneWindow对象调用:
/** Notify when decor view is attached to window and {@link ViewRootImpl} is available. */
void onViewRootImplSet(ViewRootImpl viewRoot) {
viewRoot.setActivityConfigCallback(mActivityConfigCallback);
}
而这个方法又是由DecorView对象调用:
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// TODO
mWindow.onViewRootImplSet(getViewRootImpl());
}
再分析mActivityConfigCallback
对象的赋值过程,其位于PhoneWindow的构造方法中:
/**
* Constructor for main window of an activity.
*/
public PhoneWindow(Context context, Window preservedWindow,
ActivityConfigCallback activityConfigCallback) {
this(context);
// Only main activity windows use decor context, all the other windows depend on whatever
// context that was given to them.
mUseDecorContext = true;
if (preservedWindow != null) {
mDecor = (DecorView) preservedWindow.getDecorView();
// TODO
}
// TODO
mActivityConfigCallback = activityConfigCallback;
}
PhoneWindow对象的实例化过程位于Activity类中:
final void attach(...) {
mWindow = new PhoneWindow(this, window, activityConfigCallback);
}
由于以上部分涉及到Activity的加载机制,这里不再过多发散,以流程图的形式直接展示:
因此只有当通过View发送到主线程消息队列中的Message对象全部回收时,才能让Message对象中的target
和callback
等成员变量解引用,从而让ViewRootImpl实例解引用,最终让Activity实例和自定义View实例解引用,以便进行销毁。
同上一节,在适当的时候主动移除Handler中剩余的消息:
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeCallbacks(mRunnable);
}
在Activity中,以静态内部类的方式定义一个TimeTask,实例化为mTask
并通过Timer定时执行,主动调用finish
方法进行销毁,抓取hprof文件:
DemoTask实例的GC最短路径:
该mTask
对象发生内存泄露,其被TimerThread的实例所持有,分析Timer的源码:
private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);
public Timer() {
this(\"Timer-\" + serialNumber());
}
public Timer(boolean isDaemon) {
this(\"Timer-\" + serialNumber(), isDaemon);
}
public Timer(String name) {
thread.setName(name);
thread.start();
}
public Timer(String name, boolean isDaemon) {
thread.setName(name);
thread.setDaemon(isDaemon);
thread.start();
}
sched
接下来分析TimerThread源码:
queue
中的task因此,当我们需要销毁资源时,应该主动去调用方法,将调度的task移除,避免task泄露,但是销毁分两种方式,一种是Timer的cancel
,另一种是TimerTask的cancel
,下面我们分别进行介绍二者的区别。
先看Timer的cancel
:
public void cancel() {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.clear();
queue.notify(); // In case queue was already empty.
}
}
然后是TimerTask的cancel
:
public boolean cancel() {
synchronized(lock) {
boolean result = (state == SCHEDULED);
state = CANCELLED;
return result;
}
}
所以,当我们需要完全销毁所有资源时,可以直接销毁timer:
@Override
protected void onDestroy() {
super.onDestroy();
if (mTimer != null) {
mTimer.cancel();
}
}
在Activity中,以静态内部类的方式定义一个HandlerThread和Handler,实例化为mHandler
并执行一个延时任务,主动调用finish
方法进行销毁,抓取hprof文件:
DemoHandlerThread实例的GC最短路径:
DemoHandler实例的GC最短路径:
和Handler内存泄露原理一样,由于mThread
中存在未执行的Message实例,其内部持有mHandler
对象的引用,故在该Message实例执行结束之前,二者均无法回收。
下面分析HandlerThread的源码:
mLooper
因此,当我们需要销毁资源时,应该移除mHandler
中的消息,并退出线程:
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
mThread.quit();
}
在Activity中,以静态内部类的方式定义一个Thread,实例化为mThread
并执行任务,Activity主动调用finish
方法进行销毁,抓取hprof文件:
Thread实例的GC最短路径:
该mThread
对象发生内存泄露,分析Thread源码:
调用start
后,会通过JNI调用native层的创建方法:
这里对native层的Thread对象进行实例化,持有java层Thread对象的引用,并将内存地址指针赋值给java层的变量nativePeer
:
当线程没有被创建/执行,或者已经被销毁时,这个native指针等于0。
接下来分析Thread的销毁过程:
java层Thread类的stop
方法不再被支持用于强制结束线程,因为那将会导致一些不可预料的问题出现,例如:
数据库IO属于阻塞的、耗时的原子操作,如果在线程中强行中断,会引起严重问题
常见的做法是在主线程中调用子线程的join
方法,将并行执行的任务变为顺序串行执行,并且需要确保这样调用后子线程的生命周期不会影响主线程的资源回收。
当线程自然执行结束后,由native层主动进行销毁:
将java层的变量nativePeer
置为0,native层对java层解引用,之后,java层Thread对象便可以被GC回收。
java层Thread主要是做一些计时方面的操作、暴露一些api供上层调用,通过JNI调用native层方法,改变线程状态标志位,来影响CPU调度的时间片执行情况。而native层才是线程核心逻辑具体实现,控制线程整个生命周期和调度。
按照线程的设计,对于线程本身未执行结束造成的内存泄露,即子线程的执行无法被强制结束,我们只需要确保其不会对主线程或其他子线程的回收造成影响,当该子线程自然执行结束后,便可以被回收。另一方面,尽量避免这种使用方式,而是选择可控的方式进行程序设计:
run
方法中通过标志位进行控制,当需要结束线程时,改变标志位,及时执行结束在Activity中,通过BitmapFactory解析实例化Bitmap对象,Activity主动调用finish
方法进行销毁,抓取hprof文件无任何内存泄露现象,分析BitmapFactory中的decodeResource
方法:
nativeDecodeAsset
流程nativeDecodeStream
流程在native层,这俩方法最终都会走到doDecode
中,这个方法代码较长,且我自己也理解地不透彻,因此仅简单介绍该方法中的要点:
接下来,我们分析native层的Bitmap类。
对于创建位图:
直接调用了java层的位图构造方法创建对象:
recycle
,也不会出现内存泄露问题我们查看native层这个Finalizer:
直接销毁了对象。
接着,我们分析回收位图:
对位图中的像素数据进行释放,并重置对象的一些属性。
以上是通过分析BitmapFactory粗略了解到的关于位图的原理,除此之外,常用的实例化位图方式还有Bitmap中的createBitmap
方法,其最终调用的仍然是native层Bitmap的createBitmap
方法。
综上所述,当我们使用位图时,如果没有主动去回收对象,并不会造成内存泄露,原因是位图实例化时会通过NativeAllocationRegistry对象进行管控从而优化,但是如果我们主动回收,及时释放native层无用的像素数据,可以更好地节省内存,并且降低native层复用时才去回收像素数据的开销。
单例模式本身不会有内存泄露问题,但是由于其生命周期过长的特点,尤其是对于饿汉式单例模式,生命周期更是与整个进程一样长,如果未及时销毁释放不再使用的资源,会造成不必要的内存浪费,另一方面,对于进程保活的情况下,热启动时并不会重置单例对象中的变量,导致出现数据错误。
因此,对于单例模式:
类型 | 典型场景 | 原因 |
---|---|---|
内部类持有外部引用 | 1. 非静态内部类 2. 匿名内部类 |
和外部类为依赖关系,实例无法单独存活,内部类实例存活导致外部类实例无法回收 |
框架层持有binder引用 | 1. BroadcastReceiver 2. ContentObserver |
未主动对框架层解引用,导致binder对象泄露,框架层一定泄露,应用层可能泄露 |
其他 | 1. Handler-Message 2. TimerTask 3. 单例模式 |
生命周期过长未及时回收资源 |
类型 | 规避策略 |
---|---|
Cursor | 重写finalize,在GC轮询到时主动释放资源 |
Bitmap | 使用NativeAllocationRegistry,在GC可达时主动释放native资源 |