什么是内存泄漏?
Android虚拟机的垃圾回收采用的是根搜索算法。GC会从根节点(GC Roots)开始对heap进行遍历。到最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉。但是当对象不再被应用程序使用,仍然被生命周期长的对象引用,垃圾回收器无法回收。
内存泄露的根本原因:不再被使用的对象,因为一些不当的操作导致其被gc root持有无法被回收,最终内存泄漏。
常见的作为gc root的对象
开发里排查内存泄漏涉及比较多的gc root是:
静态引用、活动的线程
哪些情况会造成内存泄漏?
- 错误使用单例造成的内存泄漏
- Handler造成的内存泄漏
- 线程造成的内存泄漏
- 非静态内部类创建静态实例造成的内存泄漏
- 资源未关闭造成的内存泄漏
- 内部类造成的内存泄漏
静态成员引起内存泄漏
测试内部类持有外部类引用,内部类是静态的(GC-ROOT,将一直连着这个外部类实例)。
public class OutterClass {
private String name;
class Inner{
public void list(){
System.out.println("outter name is " + name);
}
}
}
public class OuterInnerActivity extends ComponentActivity {
// 静态的内部类
private static OutterClass.Inner inner;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
OutterClass outterClass = new OutterClass();
inner = outterClass.new Inner();
}
}
静态变量导致的内存泄露
public class OuterInnerActivity extends ComponentActivity {
private static Context context;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//静态变量导致内存泄漏
context = this;
}
}
声明static后,sContext的生命周期将和Application一样长,Activity即使退出到桌面,Application依然存在->sContext依然存在,GC此时想回收Activity却发现Activity仍然被sContext(GC-ROOT连接着),导致死活回收不了,内存泄露。
单例模式导致的内存泄露
public class DownloadManager2 {
private static DownloadManager2 instance;
private List<DownloadListener> mListeners = new ArrayList<>();
public interface DownloadListener {
void done();
}
public static DownloadManager2 getInstance(){
if (instance == null) {
instance = new DownloadManager2();
}
return instance;
}
public void register(DownloadListener downloadListener){
if (!mListeners.contains(downloadListener)) {
mListeners.add(downloadListener);
}
}
public void unregister(DownloadListener downloadListener){
if (mListeners.contains(downloadListener)) {
mListeners.remove(downloadListener);
}
}
}
public class StaticLeakActivity extends ComponentActivity implements DownloadManager2.DownloadListener{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//单例导致内存泄漏
DownloadManager2.getInstance().register(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
// 忘记 unregister
// DownloadManager2.getInstance().unregister(this);
}
@Override
public void done() {
}
}
模拟Handler造成的内存泄漏情景:
Handler的生命周期和Activity可能不一致:当Activity销毁时,消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler是内部类,持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。
public class ThirdActivity extends AppCompatActivity {
...
private Handler handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
Log.d("fish", "hello world");
}
};
}
在Java里,匿名内部类默认持有外部类引用,并且此处编译器会有提示:HandlerLeak。
推荐使用静态类来继承Handler,因为使用匿名内部类可能会有内存泄漏风险。
修复方法:
- 使用静态内部类实现Handler功能,静态内部类默认没有持有外部类引用。
- 使用弱引用WeakReference
3.在Activity退出的时候调用removeCallbacksAndMessages移除消息队列中所有消息和所有的Runnable。
项目中使用Handler可以写一个统一的Handler使用。
public class ThirdActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_third);
new MyHandler().sendEmptyMessageDelayed(2, 5000);
}
static class MyHandler extends Handler {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
Log.d("fish", "hello world");
}
}
}
将内部类与外部类代码编译为字节码可以看到,
内部类与匿名内部类的实例都有一个外部类类型的名为this$0的变量指向了外部类的对象,所以是持有外部类的引用。当内部类使用static修饰,就没有这个指向外部类对象的变量。
为什么非静态内部类持有外部类引用,静态内部类不持有外部引用。
这个问题非常简单,就像 static的方法只能调用static的东西,非static可以调用非static和static的一样。
静态内部类调不了外部类的成员变量了。
既然匿名内部类会发生泄漏,那为啥还需要匿名内部类呢?
无需重新定义新的具名类
符合条件的匿名内部类可以转为Lambda表达式,简洁
匿名内部类可以直接访问外部类引用
Handler 泄漏的本质原因
构造Handler对象时会绑定当前线程的Looper,Looper里持有MessageQueue引用
当前线程的Looper存储在Thread里的ThreadLocal
当Handler发送消息的时候,构造Message对象,而该Message对象持有Handler引用
Message对象将会被放置在MessageQueue里
由此推断,Thread将会间接持有Handler,而Handler又持有外部类引用,最终Thread将会间接持有外部类引用,导致了泄漏。
参考:
https://www.cnblogs.com/ldq2016/p/6626670.html
线程造成的内存泄漏
线程如果在Activity中使用匿名内部类,那么它对当前Activity都有一个隐式引用。如果Activity在销毁之前,线程任务还未完成, 那么将导致Activity的内存资源无法回收,造成内存泄漏。
public class ThirdActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_third);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(200000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}
}
这种线程导致的内存泄露问题应该如何解决呢?
有两种方式:
第一种:使用静态内部类替换匿名内部类
此种方式同Handler处理类似。
第二种:使用Lambda替换匿名内部类
代码:
public class ThirdActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_third);
new Thread(() -> {
try {
Thread.sleep(200000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
}
Lambda表达式没有隐式持有外部类,因此此种场景下不会有内存泄漏风险。
注册不当内存泄漏
模拟一个简单下载过程,首先定义一个下载管理类:
public class DownloadManager {
private DownloadManager() {
}
static class Inner {
private static final DownloadManager ins = new DownloadManager();
}
public static DownloadManager getIns() {
return Inner.ins;
}
private HashMap<String, DownloadListener> map = new HashMap();
//模拟注册
public void download(DownloadListener listener, String path) {
map.put(path, listener);
new Thread(() -> {
//模拟下载
listener.onSuc();
}).start();
}
}
interface DownloadListener {
void onSuc();
void onFail();
}
外部传入下载路径,下载成功后通知外界调用者:
public class ThirdActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_third);
DownloadManager.getIns().download(new DownloadListener() {
@Override
public void onSuc() {
//更新UI
}
@Override
public void onFail() {
}
}, "hello test");
}
}
因为需要在下载回调时更新UI,因此选择匿名内部类接收回调,而因为该匿名内部类DownloadListener被静态变量: DownloadManager.ins 持有。也就是说:
静态变量作为gc root,间接持有匿名内部类,最终持有Activity导致了泄漏。
如何规避此种场景下的内存泄漏呢?
1.静态内部类持有Activity弱引用
2.DownloadManager提供反注册方式,当Activity销毁时反注册从Map里移除回调
资源未关闭造成内存泄漏
无论什么时候当我们创建一个连接或打开一个流,JVM都会分配内存给这些资源。比如,数据库链接、输入流和session对象。
忘记关闭这些资源,会阻塞内存,从而导致GC无法进行清理。
使用RxBus等注册监听没有解除注册
Subscrib subscrib = RxBus.getDefault().subscribe(this, "XXX", new RxBus.Callback<String>() {
@Override
public void onEvent(String s) {
}
});
subscrib.unSubscrib();
匿名内部类、Lambda表达式是否存在泄漏问题
-
Java匿名内部类
会。显然匿名内部类构造函数形参里有外部类的类型,当构造匿名内部类时会传递进去并赋值给匿名内部类的成员变量。
Java的Lambda是否会泄漏
不会。Java Lambda并没有生成Class文件,而是通过INVOKEDYNAMIC 指令动态生成Runnable对象,最后传入Thread里。
可以看出,此时生成的Lambda并没有持有外部类引用。
若在Lambda内显式持有外部类引用,那么此时和Java 匿名内部类类似的,当外部类销毁的时候,如果Lambda被gc root 持有(间接/直接),那么将会发生内存泄漏Kotlin匿名内部类会导致泄漏吗?
示例:线程持有匿名内部类对象:
class FourActivity : AppCompatActivity() {
private lateinit var binding: ActivityFourBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityFourBinding.inflate(layoutInflater)
setContentView(binding.root)
Thread(object : Runnable {
override fun run() {
println("hello world")
}
}).start()
}
}
编译Kotlin 匿名内部类字节码:
不会。Kotlin 匿名内部类没有隐式持有外部类引用。
若在Kotlin 匿名内部类内显式持有外部类引用,那么此时和Java 匿名内部类类似的,当外部类销毁的时候,如果Lambda被gc root 持有(间接/直接),那么将会发生内存泄漏。
- Kotlin的Lambda是否会泄漏?
线程持有Lambda对象:
class FourActivity : AppCompatActivity() {
private lateinit var binding: ActivityFourBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityFourBinding.inflate(layoutInflater)
setContentView(binding.root)
Thread { println("hello world ") }
}
}
结果和上面Java lambda的描述一致。
内存泄漏总结
总结:只有Java的匿名内部类隐式持有外部类引用,有内存泄漏风险,其他的写法不会造成内存泄漏。
总结
如果某些单例需要使用到Context对象,推荐使用Application的context,不要使用Activity的context,否则容易导致内存泄露。单例对象的生命周期和Application一致,这样Application和单例对象就一起销毁。
优先使用静态内部类而不是非静态的,因为非静态内部类持有外部类引用可能导致垃圾回收失败。如果你的静态内部类需要宿主Activity的引用来执行某些东西,你要将这个引用封装在一个WeakReference中,避免意外导致Activity泄露,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
参考:
https://mp.weixin.qq.com/s/09dowu8FOON5pyOf3Y69WA
https://blog.csdn.net/weixin_61845324/article/details/131786085