写点什么

Android 备忘录《内存泄漏》

用户头像
Android架构
关注
发布于: 2021 年 11 月 07 日

为了更好理解 GC 的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC 将不回收这些对象。如果某个对象(连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被 GC 回收。以下,我们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有一个有向图表示 JVM 的内存分配情况。



Java 使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么 GC 也是可以回收它们的。这种方式的优点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术是使用计数器,例如 COM 模型采用计数器方式管理构件,它与有向图相比,精度行低(很难处理循环引用的问题),但执行效率很高。

什么是 Java 中的内存泄露

在 Java 中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为 Java 中的内存泄漏,这些对象不会被 GC 所回收,然而它却占用内存。


在 C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于 C++中没有 GC,这些内存将永远收不回来。在 Java 中,这些不可达的对象都由 GC 负责回收,因此程序员不需要考虑这部分的内存泄露。


通过分析,我们得知,对于 C++,程序员需要自己管理边和顶点,而对于 Java 程序员只需要管理边就可以了(不需要管理顶点的释放)。通过这种方式,Java 提高了编程的效率。



因此,通过以上分析,我们知道在 Java 中也有内存泄漏,但范围比 C++要小一些。因为 Java 从语言上保证,任何对象都是可达的,所有的不可达对象都由 GC 管理。


对于程序员来说,GC 基本是透明的,不可见的。虽然,我们只有几个函数可以访问 GC,例如运行 GC 的函数 System.gc(),但是根据 Java 语言规范定义, 该函数不保证 JVM 的垃圾收集器一定会执行。因为,不同的 JVM 实现者可能使用不同的算法管理 GC。通常,GC 的线程的优先级别较低。JVM 调用 GC 的策略也有很多种,有的是内存使用到达一定程度时,GC 才开始工作,也有定时执行的,有的是平缓执行 GC,有的是中断式执行 GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC 的执行影响应用程序的性能,例如对于基于 Web 的实时系统,如网络游戏等,用户不希望 GC 突然中断应用程序执行而进行垃圾回收,那么我们需要调整 GC 的参数,让 GC 能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun 提供的 HotSpot JVM 就支持这一特性。


一个 Java 内存泄漏的典型例子,


Vector v = new Vector(10);


for (int i = 1; i < 100; i++) {


Object o = new Object();


v.add(o);


o = null;


}


在这个例子中,我们循环申请 Object 对象,并将所申请的对象放入一个 Vector 中,如果我们仅仅释放引用本身,那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到 Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。

Android 中常见的内存泄漏汇总

  • 集合类泄漏


集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。比如上面的典型例子就是其中一种情况,当然实际上我们在项目中肯定不会写这么 2B 的代码,但稍不注意还是很容易出现这种情况,比如我们都喜欢通过 HashMap 做一些缓存之类的事,这种情况就要多留一些心眼。


  • 单例造成的内存泄漏


由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。比如下面一个典型的例子


public class AppManager {


private static AppManager instance;


private Context context;


private AppManager(Context context) {


this.context = context;


}


public static AppManager getInstance(Context context) {


if (instance == null) {


instance = new AppManager(context);


}


return instance;


}


}


这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个 Context,所以这个 Context 的生命周期的长短至关重要:


1、如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。


2、如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。


正确的方式应该改为下面这种方式:


public class AppManager {


private static AppManager instance;


private Context context;


private AppManager(Context context) {


this.context = context.getApplicationContext();// 使用 Application 的 context


}


public static AppManager getInstance(Context context) {


if (instance ==


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


null) {


instance = new AppManager(context);


}


return instance;


}


}


或者这样写,连 Context 都不用传进来了:


在你的 Application 中添加一个静态方法,getContext() 返回 Application 的 context,


context = getApplicationContext();


/**


  • 获取全局的 context

  • @return 返回全局 context 对象


*/


public static Context getContext(){


return context;


}


public class AppManager {


private static AppManager instance;


private Context context;


private AppManager() {


this.context = MyApplication.getContext();// 使用 Application 的 context


}


public static AppManager getInstance() {


if (instance == null) {


instance = new AppManager();


}


return instance;


}


}


  • 静态变量导致内存泄露


静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。比如下面这样的情况,在 Activity 中为了避免重复的创建 info,将 sInfo 作为静态变量:


public class MainActivity extends AppCompatActivity {


private static Info sInfo;


@Override


protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);


setContentView(R.layout.activity_main);


if (sInfo != null) {


sInfo = new Info(this);


}


}


}


class Info {


public Info(Activity activity) {


}


}


Info 作为 Activity 的静态成员,并且持有 Activity 的引用,但是 sInfo 作为静态变量,生命周期肯定比 Activity 长。所以当 Activity 退出后,sInfo 仍然引用了 Activity,Activity 不能被回收,这就导致了内存泄露。


在 Android 开发中,静态持有很多时候都有可能因为其使用的生命周期不一致而导致内存泄露,所以我们在新建静态持有的变量的时候需要多考虑一下各个成员之间的引用关系,并且尽量少地使用静态持有的变量,以避免发生内存泄露。当然,我们也可以在适当的时候讲静态量重置为 null,使其不再持有引用,这样也可以避免内存泄露。


  • 非静态内部类导致内存泄露


非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。非静态内部类导致的内存泄露在 Android 开发中有一种典型的场景就是使用 Handler,很多开发者在使用 Handler 是这样写的:


public class MainActivity extends AppCompatActivity {


@Override


protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);


setContentView(R.layout.activity_main);


start();


}


private void start() {


Message msg = Message.obtain();


msg.what = 1;


mHandler.sendMessage(msg);


}


private Handler mHandler = new Handler() {


@Override


public void handleMessage(Message msg) {


if (msg.what == 1) {


// 做相应逻辑


}


}


};


}


也许有人会说,mHandler 并未作为静态变量持有 Activity 引用,生命周期可能不会比 Activity 长,应该不一定会导致内存泄露呢,显然不是这样的!


熟悉 Handler 消息机制的都知道,mHandler 会作为成员变量保存在发送的消息 msg 中,即 msg 持有 mHandler 的引用,而 mHandler 是 Activity 的非静态内部类实例,即 mHandler 持有 Activity 的引用,那么我们就可以理解为 msg 间接持有 Activity 的引用。msg 被发送后先放到消息队列 MessageQueue 中,然后等待 Looper 的轮询处理(MessageQueue 和 Looper 都是与线程相关联的,MessageQueue 是 Looper 引用的成员变量,而 Looper 是保存在 ThreadLocal 中的)。那么当 Activity 退出后,msg 可能仍然存在于消息对列 MessageQueue 中未处理或者正在处理,那么这样就会导致 Activity 无法被回收,以致发生 Activity 的内存泄露。通常在 Android 开发中如果要使用内部类,但又要规避内存泄露,一般都会采用静态内部类+弱引用的方式。


public class MainActivity extends AppCompatActivity {


private Handler mHandler;


@Override


protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);


setContentView(R.layout.activity_main);


mHandler = new MyHandler(this);


start();


}


private void start() {


Message msg = Message.obtain();


msg.what = 1;


mHandler.sendMessage(msg);


}


private static class MyHandler extends Handler {


private WeakReference<MainActivity> activityWeakReference;


public MyHandler(MainActivity activity) {


activityWeakReference = new WeakReference<>(activity);


}


@Override


public void handleMessage(Message msg) {


MainActivity activity = activityWeakReference.get();


if (activity != null) {


if (msg.what == 1) {


// 做相应逻辑


}


}


}


}


}


mHandler 通过弱引用的方式持有 Activity,当 GC 执行垃圾回收时,遇到 Activity 就会回收并释放所占据的内存单元。这样就不会发生内存泄露了。


上面的做法确实避免了 Activity 导致的内存泄露,发送的 msg 不再已经没有持有 Activity 的引用了,但是 msg 还是有可能存在消息队列 MessageQueue 中,所以更好的是在 Activity 销毁时就将 mHandler 的回调和发送的消息给移除掉。


@Override


protected void onDestroy() {


super.onDestroy();


mHandler.removeCallbacksAndMessages(null);


}


非静态内部类造成内存泄露还有一种情况就是使用 Thread 或者 AsyncTask。 比如在 Activity 中直接 new 一个子线程 Thread:


public class MainActivity extends AppCompatActivity {


@Override


protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);


setContentView(R.layout.activity_main);


new Thread(new Runnable() {


@Override


public void run() {


// 模拟相应耗时逻辑


try {


Thread.sleep(2000);


} catch (InterruptedException e) {


e.printStackTrace();


}


}


}).start();


}


}


或者直接新建 AsyncTask 异步任务:


public class MainActivity extends AppCompatActivity {


@Override


protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);


setContentView(R.layout.activity_main);


new AsyncTask<Void, Void, Void>() {


@Override


protected Void doInBackground(Void... params) {


// 模拟相应耗时逻辑


try {


Thread.sleep(2000);


} catch (InterruptedException e) {


e.printStackTrace();


}


return null;


}


}.execute();


}


}


很多初学者都会像上面这样新建线程和异步任务,殊不知这样的写法非常地不友好,这种方式新建的子线程 Thread 和 AsyncTask 都是匿名内部类对象,默认就隐式的持有外部 Activity 的引用,导致 Activity 内存泄露。要避免内存泄露的话还是需要像上面 Handler 一样使用静态内部类+弱应用的方式(代码就不列了,参考上面 Hanlder 的正确写法)。


  • 未取消注册或回调导致内存泄露


比如我们在 Activity 中注册广播,如果在 Activity 销毁后不取消注册,那么这个刚播会一直存在系统中,同上面所说的非静态内部类一样持有 Activity 引用,导致内存泄露。因此注册广播后在 Activity 销毁后一定要取消注册。


public class MainActivity extends AppCompatActivity {


@Override


protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);


setContentView(R.layout.activity_main);


this.registerReceiver(mReceiver, new IntentFilter());


}


private BroadcastReceiver mReceiver = new BroadcastReceiver() {


@Override


public void onReceive(Context context, Intent intent) {


// 接收到广播需要做的逻辑


}


};


@Override


protected void onDestroy() {


super.onDestroy();


this.unregisterReceiver(mReceiver);


}


}


在注册观察则模式的时候,如果不及时取消也会造成内存泄露。比如使用 Retrofit+RxJava 注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,所以需要记得在不用或者销毁的时候取消注册。


  • Timer 和 TimerTask 导致内存泄露


Timer 和 TimerTask 在 Android 中通常会被用来做一些计时或循环任务,比如实现无限轮播的 ViewPager:


public class MainActivity extends AppCompatActivity {


private ViewPager mViewPager;


private PagerAdapter mAdapter;


private Timer mTimer;


private TimerTask mTimerTask;


@Override


protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);


setContentView(R.layout.activity_main);


init();


mTimer.schedule(mTimerTask, 3000, 3000);


}


private void init() {


mViewPager = (ViewPager) findViewById(R.id.view_pager);


mAdapter = new ViewPagerAdapter();


mViewPager.setAdapter(mAdapter);

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android备忘录《内存泄漏》