深入理解Java中的内存泄漏

2,543 阅读6分钟

理解Java中的内存泄漏,我们首先要清楚Java中的内存区域分配问题和内存回收的问题本文将分为三大部分介绍这些内容。

Java中的内存分配

Java中的内存区域主要分为线程共享的和线程私有的两大区域:

  • Java堆:在虚拟机启动时创建,是所有线程共享的一块内存区域。存放了所有的new出来的对象的实例和数组,对象的reference则在虚拟机栈上。
  • 方法区:与Java堆一样,是各个线程共享的内存区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据
  • 虚拟机栈:每个方法在执行过程中都会创建一个栈帧,用来保存局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从开始执行到执行结束都对应着一个栈帧在虚拟机中从入栈到出栈的过程。我们通常说的栈内存就是指的是虚拟机栈的本地变量表部分,主要存储了基本数据类型和对象的引用。
  • 程序计数器:是线程执行字节码文件位置的指示器,用于线程切换后,再次切换回来能准确执行上次执行到的字节码文件的位置。
  • 本地方法栈:和虚拟机栈类似,只是记录的是Native方法。

介绍了Java的内存分配问题,通过一段代码来进行一下总结

public static void main(String[] args) {
         Animal animal = new Animal();
    }
publci class Animal{
    public static String address = "the earth"
    public Animal(){
        
    }
}

在main方法中,我们new Animal(),首先,检查内存中是否加载了Animal.class,如果没有加载,则会先将Animal.class加载到方法区中,同时在方法区开辟一块内存区域,用于存储static 的"the earth"(这里更能清晰的理解static这个关键字,就是static修饰的变量是属于类的,而不是属于类的某一个对象的),随后在Java堆中开辟一块内存区域,用于存储new Animal()这个对象,在虚拟机栈中的animal会指向这个对象的地址。

Java中的内存回收机制

Java内存回收主要是指的是Java堆上的已经分配给对象的内存回收,判断Java堆上的内存是否被回收,主要是通过可达性分析算法:从一系列可以作为 GC Roots 的对象开始向下搜索,搜索走过的路径称为引用链,当GC回收时,一个对象没有通过引用链与任何GC Roots对象连接,则这个对象就可以被回收了。可作为GC Roots对象的有以下几种:

  • 虚拟机栈的本地变量表中引用的对象。
  • 方法区中静态属性和常量引用的对象。
  • 本地Native方法引用的对象。

需要注意的是GC Roots并不是一直可以作为GC Roots的,eg:

public void testGc(){
    Person person = new Person();
}

根据GC Roots的定义,new Person()这个对象被person所引用,person在虚拟机栈中,所以new Person()这个对象是作为了GC Roots的,但是当这个testGc()方法执行完成,person释放内存,new Person()这个对象就没有其他的引用,就不再是GC Roots。

Java中的内存泄漏

内存泄漏需要和内存溢出区分开来,内存泄漏是指:部分内存我们不再需要了,但是虚拟机不能回收,泄漏过多就会造成内存溢出。就是部分对象我们已经用不上了,但是这些对象和GC Roots存在一定程度上的引用关系,导致不能被垃圾回收机制回收。

几种常见的内存泄漏

  • 非静态内部类的静态实例
public class InnerStactivity extends AppCompatActivity {

    private static Object ininerClass;
    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_inner_stactivity);

    }

    class InnerClass{

    }

    public void startInnerClass(){

        ininerClass = new InnerClass();

    }

    /**
     * 创建了InnerClass的静态实例引用
     * @param view
     */
    public void createInner(View view) {

        startInnerClass();

    }
}

当static Object ininerClass指向了newInnerClass()这个对象时,这个对象就可以作为了GC Roots,同时非静态的内部类会持有外部类的引用,InnerStactivity就会在GC Roots的引用链上,当我们需要离开这个InnerStactivity,并且不再需要这个InnerStactivity时,这个InnerStactivity并不能被回收。

  • 匿名内部类的静态实例

  • Handler内存泄漏

public class HandlerActivity extends AppCompatActivity {

    Handler mHandler;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        mHandler= new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                show();
            }
        };
    }

    public void sendMessage(View view) {
        mHandler.sendEmptyMessageDelayed(1,1000*30);
        finish();
    }

    public void show(){

    }

}

可以看到我们这么写Android studio已经提示了警告,提示我们应该用static修饰handler对象,否则会造成内存的泄漏,这是不容易犯的错误。

Handler这么写之所以会出现内存泄漏是因为:Message会持有一个对Handler的引用,当这个Handler是非静态内部类的时候,又会持有一个对外部类的引用(比如Activity)。如果发送一条延时的Message,由于这个Message会长期存在于队列中,就会导致Handler长期持有对Activity的引用,从而引起视图和资源泄漏。当你发送一条延时的Mesaage,并且把这个Message保存在某些地方(例如静态结构中)备用,内存泄漏会变得更加严重。

我们现在都通过static修饰 Handler类,并通过弱引用来和当前界面的Activity交互,并在onDestory()中去除Handler的所有的消息来规避可能出现的内存泄漏。

public class HandlerImproveActivity extends AppCompatActivity {

    Handler mHandler;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        mHandler= new ImproveHandler(HandlerImproveActivity.this);
    }

    public void sendMessage(View view) {
        mHandler.sendEmptyMessageDelayed(1,1000*30);
        finish();
    }

    private static class ImproveHandler extends Handler{

        private WeakReference<HandlerImproveActivity> mActivity;

        public ImproveHandler(HandlerImproveActivity improveActivity) {
            this.mActivity = new WeakReference<HandlerImproveActivity>(improveActivity);
        }


        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (mActivity != null && mActivity.get() != null) {
                mActivity.get().show();
            }
        }
    }

    public void show(){

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if(mHandler != null){
            mHandler.removeCallbacksAndMessages(null);
        }

    }
}
  • 未正确使用Context 在开发中会有大量的使用到Context的地方,如果不是必须使用到Activity的Context(例如Dialog就必须使用到activity的Context),则都可以使用getApplicationContext()来替代。
 AlertDialog alertDialog= new AlertDialog.Builder(this).create();
 Toast.makeText(getApplicationContext(), "", Toast.LENGTH_SHORT).show();
  • 注册未取消: 在使用观察者模式的时候,在register后,完成时要即时unRegister监听器。在某个Activity界面使用Rxjava进行网络请求,在离开这个页面的时候一定要取消注册。
  • 资源对象未关闭: 资源对象比如Cursor、File等,不使用的时候应该关闭它们。把他们的引用置为null,而不关闭它们,往往会造成内存泄漏。因此,在资源对象不使用时,一定要确保它已经关闭,通常在finally语句中关闭,防止出现异常时,资源未被释放的问题。

Android Developer中关于如何管理内存的链接 developer.android.com/topic/perfo…