短时间内创建大量对象,挤占 Eden 区,导致频繁 MinorGC,内存就会发生抖动。
MemoryProfile内存图为锯齿状,并伴随大量的白色垃圾桶。
常见引发的问题就是在最小一层循环里面创建大量对象
String s = "";
for (int i = 0; i < 10000; i++) {
s += "," + i; // 创建大量的字符串常量对象
}
通常会采取一些办法避免这类问题:
onMeasure
、onLayout
、onDraw
等方法中调用invalidate()
方法(通知主线程当前画面已过期,再线程空闲时重绘),且不要创建大量对象。Bitmap
的时候,试着把它们缓存在数组中实现复用。StringBuffer
或StringBuilder
来执行。长生命周期对象持有短生命周期对象的引用,导致分配的内存空间没有及时回收,使得可使用内存越来越少。
Memory Profiler
使用Memory Profiler 捕获堆转储之后,筛选出我们自己的对象。
在退出 LeakActivity
回到 MainActivity
之后,并且手动出发了一次GC后,LeakActivity
还在内存当中,表是发生了内存泄漏。
LeakActivity$1
和 LeakActivity$2
分别表示 LeakActivity
中第1个和第2个匿名内部类(编译的时候由系统自动起名为外部类名$1.class
)对 LeakActivity
的引用。
右边四列分别表示
如果 Shallow Size 和 Retained Size 都非常小并且相等,都可以认为是已经被回收的对象(空的 Activity,大概是270)。但是 LeakActivity
明显不是这样,也说明了 LeakActivity
发生了内存泄漏。
左边选中 LeakActivity
,右边 Instance 中一个一个去查看,现在只有一处,点击第一个,默认 Reference 以 Depth 排序,只看 Reference Depth 小于 Instance Depth 的行。右键 -> Jump to Source,则自动定位到代码。
如上图,展开发现是Handler引起的泄漏。
stact
变量所指向的引用,虚拟机会一直保留该引用对象,除非把 static
变量设置为 null
。
public class ToastUtil {
private static Toast toast;
public static void show(Context context, String message) {
if (toast == null) {
// 静态变量toast会一直持有Toast对象的引用,Toast对象将不会被回收
// 如果context为Activity Context,相应的Activity也会泄漏
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT);
} else {
toast.setText(message);
}
toast.show();
}
}
解决办法①: 将 makeText()
中的参数 context
改为context.getApplicationContext()
,长生命周期的 static Toast
持有的也是长生命周期的 Applaiction Context
,就不会内存泄漏了。
private static Activity sActivity;
@Override
protected void onCreate(Bundle savedInstanceState) {
// 静态变量sActivity会在APP生命周期内一直持有Activity的实例对象,内存泄漏
sActivity = this;
}
解决办法②:在 Acticity 生命周期结束时,让静态变量断开与其引用关系。即在 onDestory()
回调方法中,执行 sActivity = null
。
非静态内部类对象会持有外部类对象的引用,外部类的生命周期结束,但是仍可能被内部类所引用。
通过 Java在线反编译工具,并使用 Fernflower 反编译器,可以查看反编译内容:
public class Test {
// ① 匿名内部类实现接口并重写接口方法
Runnable runnable = new Runnable() {
@Override
public void run() {
}
};
// ② 匿名内部类重写类方法
Object object = new Object() {
};
// ③ 静态常量 + 匿名内部类实现接口并重写接口方法
static final Runnable runnable2 = new Runnable() {
@Override
public void run() {
}
};
// ④ 非静态内部类
class InnerClass {
void test() {
runnable.run();
}
}
// ⑤ 静态内部类
static class InnerClass2 {
}
}
// ⑥ 外部类
class OuterClass {
}
① Test$1.class
即 new Runnable() {}
的内容:
import com.company.Test;
class Test$1 implements Runnable {
// 持有外部类的引用
final Test this$0;
// 外部类的引用通过构造方法传入
Test$1(Test this$0) {
this.this$0 = this$0;
}
public void run() {
}
}
② Test$2.class
即 new Object() {}
的内容:
import com.company.Test;
class Test$2 {
// 持有外部类的引用
final Test this$0;
// 外部类的引用通过构造方法传入
Test$1(Test this$0) {
this.this$0 = this$0;
}
}
③ Test$3.class
即 static final Runnable runnable2 = new Runnable() {}
的内容:
class Test$3 implements Runnable {
public void run() {
}
}
④ Test$InnerClass.class
即 class InnerClass {}
的内容:
import com.company.Test;
class Test$InnerClass {
// 持有外部类的引用
final Test this$0;
// 外部类的引用通过构造方法传入
Test$InnerClass(Test this$0) {
this.this$0 = this$0;
}
// 通过外部类的引用,直接访问外部类的成员
void test() {
this.this$0.runnable.run();
}
}
⑤ Test$InnerClass2.class
即 static class InnerClass2 {}
的内容:
class Test$InnerClass2 {
// 不会持有外部类的引用
Test$InnerClass2() {
}
}
⑥ OuterClass.class
即 class OuterClass {}
的内容:
class OuterClass {
// 不会持有外部类的引用
OuterClass() {
}
}
【非静态内部类】
class Outer {
// 非静态内部类,默认持有外部类的引用,方法体内可直接调用外部类的成员和方法
class Inner1 {
/* ... */ }
// 静态内部类,不会持有外部类的引用,方法体内不可直接调用外部类的成员和方法
static class Inner2 {
/* ... */ }
}
【匿名内部类】
外部类名$1.class
、``外部类名$2.class`等以此类推。// 直接new一个接口,并重写成员方法
new Runnable() {
@Override
public void run() {
/* ... */ }
};
// 直接new一个抽象类,并重写成员方法
new AbstractSet<Object>(){
@NonNull
@Override
public Iterator<Object> iterator() {
/* ... */ }
@Override
public int size() {
/* ... */ }
};
// 直接new一个类,并重写成员方法
new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
/* ... */ }
};
// 直接new一个类,只写大括号,但不重写任何成员方法。抽象类、接口也同理
new Handler() {
};
【常见的 Handler 内存泄漏】
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
/* ... */ }
// new Handler()并重写方法,编译器会自动生成MainActivity$1.class的非静态内部类
// 因此new出的这个对象会默认持有MainActivity对象的引用,从而导致内存泄漏
// Activity退出后,msg可能还在MessageQueue中没被处理完毕,则Activity对象不能回收
};
}
解决办法:改为静态内部类+弱引用,因为静态内部类不需依赖外部类的成员和方法。
类似的还有直接 new Thread,new Runnable,new AsyncTask,new Listiner 等也可能造成内存泄漏。
public class MainActivity extends AppCompatActivity {
private Handler mHandler;
// 静态常量不会持有外部类的引用,不会内存泄漏,但是会生成匿名内部类
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() {
/* ... */ }
};
@Override
protected void onCreate(Bundle savedInstanceState) {
mHandler = new MyHandler(this);
}
@Override
protected void onDestroy() {
// 虽然不会导致Activity泄漏,但msg可能还在MessageQueue中,最好移除掉
mHandler.removeCallbacksAndMessages(null);
}
// 自己写一个静态内部类,不让编译器自动生成非静态内部类
private static class MyHandler extends Handler {
// 使用弱引用持有外部类的引用
private WeakReference<MainActivity> activityWeakReference;
public MyHandler(MainActivity activity) {
activityWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
/* ... */ }
}
}
单例的静态特性使得它的生命周期同App的生命周期一样长,如果单例一直持有一个短生命周期的引用,就容易导致内存泄漏。
public class Singleton {
// 静态变量一直持有Singleton对象
private static Singleton singleton = null;
// Singleton对象又持有Context对象
private Context mContext;
private Singleton(Context mContext) {
this.mContext = mContext;
}
public static Singleton getSingleton(Context context){
// 如果context为Activity Context,则Activity就始终无法回收,导致内存泄漏
if (null == singleton) singleton = new Singleton(context);
return singleton;
}
}
public class Singleton {
// 静态变量一直持有Singleton对象
private volatile static Singleton singleton = null;
// Singleton对象又持有Context对象
private Context mContext;
private Singleton(Context mContext) {
this.mContext = mContext;
}
public static Singleton getSingleton(Context context){
if (singleton == null) {
synchronized (Singleton.class){
if (singleton == null) {
// 如果context为Activity,则Activity内存泄漏
singleton = new Singleton(context);
}
}
}
return singleton;
}
}
解决办法①: 调用的时候传入 ApplicationContext 或改为 this.mContext = mContext.getApplicationContext();
。
解决办法②: 将该属性的引用方式改为弱引用。
public class Singleton {
private volatile static Singleton singleton = null;
// 使用弱引用,使Singleton对象持有Context对象
private WeakReference<Context> mContextWeakRef;
private Singleton(Context mContext) {
this.mContextWeakRef = new WeakReference<Context>(mContext);
}
public static Singleton getSingleton(Context context){
if (singleton == null) {
synchronized (Singleton.class){
if (singleton == null) {
// 如果context为Activity,则Activity内存泄漏
singleton = new Singleton(context);
}
}
}
return singleton;
}
}
使用系统服务时,会调用 getSystemService()
方法,还有可能注册系统服务的监听器,这两处都可能引起内存泄漏。类似的还有 BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap 等,也需要在 onDestry 中及时的关闭、注销或者释放内存。
// 直接调用getSystemService方法,往往是Activity,所以系统服务会一直持有它的引用
SensorManager sm = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
Sensor sensor = sm.getDefaultSensor(Sensor.TYPE_ALL);
// 设置监听器传入了Activity,同样会被系统服务一直持有它的引用
sm.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
解决办法:
getApplicationContext().getSystemService()
获取系统服务。onDestory
里面注销监听器,断开系统服务与 Activity 的引用关系。ApplicationContext
。动作 | Application | Service | Activity |
---|---|---|---|
启动Activity | ⭕️ New Task | ⭕️ New Task | ⭕️ |
弹出Dialog | ❌ | ❌ | ⭕️ |
实例化布局 | ⭕️ | ⭕️ | ⭕️ |
启动Service | ⭕️ | ⭕️ | ⭕️ |
绑定Service | ⭕️ | ⭕️ | ⭕️ |
发送广播 | ⭕️ | ⭕️ | ⭕️ |
注册广播接收器 | ⭕️ | ⭕️ | ⭕️ |
加载res资源 | ⭕️ | ⭕️ | ⭕️ |
生命周期比 Activity 长的内部类对象,且内部类使用了外部类的成员变量,则
不再使用的对象,显式地赋值为 null
,例如 Bitmap,先调用 recycle()
后,在赋值为 null
。
在 dependencies 中加入 debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'
,无需再添加任何调用代码。
当有内存泄漏发生时,LogCat 就会打印相关信息:
====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
333202 bytes retained by leaking objects
Displaying only 1 leak trace out of 2 with the same signature
Signature: d01f393e64386a95c368a4c4209c91c136f7720
┬───
│ GC Root: Local variable in native code
│
├─ android.os.HandlerThread instance
│ Leaking: NO (PathClassLoader↓ is not leaking)
│ Thread name: 'LeakCanary-Heap-Dump'
│ ↓ Thread.contextClassLoader
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (ToastUtil↓ is not leaking and A ClassLoader is never leaking)
│ ↓ ClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (ToastUtil↓ is not leaking)
│ ↓ Object[].[188]
├─ com.example.androidtest.ToastUtil class
│ Leaking: NO (a class is never leaking)
│ ↓ static ToastUtil.toast
│ ~~~~~
├─ android.widget.Toast instance
│ Leaking: YES (This toast is done showing (Toast.mTN.mWM != null && Toast.mTN.mView == null))
│ Retaining 166.6 kB in 1384 objects
│ mContext instance of com.example.androidtest.LeakActivity with mDestroyed = true
│ ↓ Toast.mContext
╰→ com.example.androidtest.LeakActivity instance
Leaking: YES (ObjectWatcher was watching this because com.example.androidtest.LeakActivity received
Activity#onDestroy() callback and Activity#mDestroyed is true)
Retaining 69.9 kB in 1256 objects
key = 1f27bf38-b690-4983-b3f1-64bd991e7f9f
watchDurationMillis = 7553
retainedDurationMillis = 2549
mApplication instance of android.app.Application
mBase instance of android.app.ContextImpl
====================================
0 LIBRARY LEAKS
A Library Leak is a leak caused by a known bug in 3rd party code that you do not have control over.
See https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#4-categorizing-leaks
====================================
0 UNREACHABLE OBJECTS
An unreachable object is still in memory but LeakCanary could not find a strong reference path
from GC roots.
====================================
METADATA
Please include this in bug reports and Stack Overflow questions.
Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: samsung
LeakCanary version: 2.6
App process name: com.example.androidtest
Stats: LruCache[maxSize=3000,hits=2715,misses=45083,hitRate=5%]
RandomAccess[bytes=2304319,reads=45083,travel=16121653814,range=16075212,size=20801120]
Heap dump reason: 5 retained objects, app is visible
Analysis duration: 3857 ms
Heap dump file path: /data/user/0/com.example.androidtest/cache/leakcanary/2021-03-02_13-54-08_891.hprof
Heap dump timestamp: 1614664454960
Heap dump duration: 1642 ms
====================================
原理
RefWatcher.watch()
创建了一个KeyedWeakReference
用于去观察对象。HeapAnalyzerService
被开启在一个独立的进程中,并且HeapAnalyzer
使用了HAHA
开源库解析了指定时刻的堆栈快照文件heap dump。heap dump
中,HeapAnalyzer
根据一个独特的引用key
找到了KeyedWeakReference
,并且定位了泄露的引用。HeapAnalyzer
为了确定是否有泄露,计算了到GC Roots的最短强引用路径,然后建立了导致泄露的链式引用。DisplayLeakService
,然后一个泄露通知便展现出来了。