Java内存泄漏总结

OOM in Java

Posted by S.L on June 13, 2020

本文主要总结一些Java中常见的内存泄漏的情况和注意事项。

常见问题

1.自定义ClassLoader:定义了很多或者里面加载的类很多但没有释放,所以ClassLoader也不会释放。

ClassLoad的特别之处在于它不仅涉及「常规」对象引用,还涉及元对象引用,比如字段、方法和类。 这意味着只要有对字段、方法、类或者ClassLoader的对象的引用,ClassLoader就会驻留在JVM中。 因为ClassLoader本身可以关联许多类及其静态字段,所以就有许多内存被泄露了。

2.静态变量引用非静态变量的instance:静态变量存在stack而不是heap中,并且处于GC的顶点root, 不会被回收,所以它强引用的非静态变量也不会被回收,如果静态变量引用一个对象是一个非常大的集合会很恐怖。

如果非要引用,使用弱引用WeakReference来引用,即外部对象被回收了本引用也会被回收, 它不会因为是Strong Reference而导致被引用的外部类无法被回收;

相反如果非静态实例引用静态实例则不会影响其被回收(如一个Model类里面有一些static的实例或者static final的常量),因为静态变量存在stack中,且是顶点, 但是如果引用的static变量是一个大的集合需要注意清除里面的数据,否则还是会导致OOM。

3.全局集合类型的释放不当:虽然删除了引用,但是没有在集合中remove掉该引用,即仍然被集合类持有一个强引用; 可以使用引用队列ReferenceQueue来get被回收的对象.

4.缓存没有设置边界:可以使用SoftReference来保证内存不够的时候被GC回收。

5.基于数组的泄漏:没有设置对应index位置的数据为null,在被重新set之前这个内存是泄漏状态

6.对象游离(floating):该用局部变量的就不要用全局变量,否则全局变量有一个强引用就不会被回收, 除非包含它的对象被回收,如果是个单例则会产生内存泄漏,可以使用SoftReference 作为全局变量或者直接使用局部变量。

7.内部类和外部模块等的引用: 内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。 此外还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B模块的一个方法如: public void registerMsg(Object b); 这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用, 这时候就需要注意模块B是否提供相应的操作去除引用。

8.各种连接的释放:如Connection、Statement、ResultSet等,Connection 永远都不会自动释放需要手动释放,后面两个关闭任何一个另外一个就会关闭; 如果是使用数据库连接池更要释放后者,用try-finally

9.监听器的使用:我们往往知道addInterceptor但在释放对象的时候往往忽视删除这些监听器。

10.当修改了集合里面对象的属性的时候再调用remove方法不起作用,如HashSet,因为hashcode已经改变, 找不到这个对应的元素了,造成了内存泄漏。

11.资源的释放如文件:往往使用了外部和内部的缓冲区,不要简单设置为null,先close再设置为null比较稳妥。

12.非静态内部类创建静态的实例造成OOM:非静态内部类会持有外部类的引用,如果外部类声明一个全局的static 的内部静态类的实例,那么它的生命周期和应用本身一样长,则导致内部类持有的外部类的引用一直存在, 进而导致外部类不能释放内存。

13.匿名内部类的使用:在Java中,非静态内部类和匿名类内部类都会潜在持有它们所属的外部类的引用, 但是静态内部类却不会。

匿名内部类

例子1:匿名内部类引发的内存泄漏

public class MainActivity extends AppCompatActivity {

    private final Handler handler = new Handler() { // 全局变量
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //每次都会创建一个新的线程去持有handler的引用去执行工作,直到执行完毕
        new Thread(new Runnable() {
            @Override
            public void run() {
                // ...
                handler.sendEmptyMessage(0x123);
            }
        });
    }
}

1、从Android的角度

当Android应用程序启动时,该应用程序的主线程会自动创建一个Looper对象和与之关联的MessageQueue。 当主线程中实例化一个Handler对象后,它就会自动与主线程Looper的MessageQueue关联起来。 所有发送到MessageQueue的Message都会持有Handler的引用,所以Looper会据此回调Handle的handleMessage() 方法来处理消息。只要MessageQueue中有未处理的Message,Looper就会不断的从中取出并交给Handler处理。 另外,主线程的Looper对象会伴随该应用程序的整个生命周期。

2、 Java角度

在Java中,非静态内部类和匿名类内部类都会潜在持有它们所属的外部类的引用,但是静态内部类却不会。

对上述的示例进行分析,当MainActivity结束时,未处理的消息持有handler的引用, 而handler又持有它所属的外部类也就是MainActivity的引用。 这条引用关系会一直保持直到消息得到处理,这样阻止了MainActivity被垃圾回收器回收,从而造成了内存泄漏。

解决方法:将Handler类独立出来或者使用静态内部类,这样便可以避免内存泄漏。

例子2:线程造成的内存泄漏

示例:AsyncTask和Runnable

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        new Thread(new MyRunnable()).start();
        new MyAsyncTask(this).execute();
    }

    class MyAsyncTask extends AsyncTask<Void, Void, Void> {

        public MyAsyncTask(Context context) {
            // ...
        }

        @Override
        protected Void doInBackground(Void... params) {
            // ...
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            // ...
        }
    }

    class MyRunnable implements Runnable {
        @Override
        public void run() {
            // ...
        }
    }
}

AsyncTaskRunnable都使用了匿名内部类,那么它们将持有其所在Activity的隐式引用。 如果任务在Activity销毁之前还未完成,那么将导致Activity的内存资源无法被回收,从而造成内存泄漏。

解决方法:将AsyncTask和Runnable类独立出来或者使用静态内部类,这样便可以避免内存泄漏。

不会导致OOM:

  • 1.变量循环引用,可能导致孤岛,但不会内存泄漏,GC效率会降低
  • 2.函数参数再引用,函数参数可以看做函数内的临时变量,对它进行的任何重新new操作都会在函数结束后被GC回收
  • 3.作用域范围内的变量,在超出作用域后会被GC回收,如方法内声明的变量,如static块内声明的变量

Reference

本文首次发布于 S.L’s Blog, 作者 @stuartlau , 转载请保留原文链接.