关于Java内存泄漏的讨论

这是我在部门做的技术分享PPT,搬到CSDN上大家一起讨论讨论,有需要原件的可以私聊我哦。

       我想大家在平时的工作中,或多或少都遇到过或听到过内存泄漏,那今天我希望和大家一起讨论一下关于内存泄漏, 什么是内存泄漏?造成内存泄漏的原因?如何解决内存泄漏?以及如何避免内存泄漏等等。。。

主要从三个方面进行讨论:

Java类执行的过程;

Java中的内存泄漏;

Android内存泄露及其解决。

一、Java类执行的过程

        在分享内存泄漏之前,作为一个引子,先花一定时间看看java类执行的过程,能更好的理解内存泄漏。

1、这是我们今天的主角之一:

2、我们先来看看JVM体系结构:

我们知道一个JVM就是一个进程,一个应用程序。

扫描二维码关注公众号,回复: 5503674 查看本文章

文件验证器:验证class类编译的正不正确

ClassLoader:将class文件加载到JVM内存,转为Class对象

方法区:存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等,在方法区中有一个非常重要的部分就是运行时常量池,被线程共享

本地方法栈:支持native方法调用 程序计数器:用来记录当前正在执行的指令

Java栈:就是线程

Java堆:用来存放对象和数组(特殊的对象)。堆内存由多个线程共享

3、Account account = new Account();

这一语句的执行过程如下:

这里还应该提到双亲委派模型:

双亲委派模型的工作流程:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

4、account.add(30,40);

这一语句的执行过程如下:

栈帧:一个栈帧随着一个方法的调用开始而创建,这个方法调用完成而销毁。栈帧内存放者方法中的局部变量,操作数栈等数据。

Java栈也称作虚拟机栈(Java Vitual Machine Stack),JVM栈只对栈帧进行存储,压栈和出栈操作。Java栈是Java方法执行的内存模型。

5、Account的消失:

Java栈中的Account.class——main方法执行完毕,局部变量account消失 ;

Java堆中的Account对象—— 可达性分析算法;

硬盘上的Account.class文件——javac编译后重新覆盖消失。

可达性分析算法:通过一系列的GC roots的对象为起始点,开始向下搜索,当一个对象到GC Roots没有任何引用相连,则证明此对象为不可达,并将被判定为是可回收的对象,GC会回收。

引用计数法:逻辑非常简单,但是存在问题,java并不采用这种方式进行对象存活判断。引用计数法的逻辑是:在堆中存储对象时,在对象头处维护一个counter计数器,如果一个对象增加了一个引用与之相连,则将counter++。如果一个引用关系失效则counter–。如果一个对象的counter变为0,则说明该对象已经被废弃,不处于存活状态。

判断一个内存空间是否符合垃圾收集的标准有两个:一个是给对象赋予了空值null,再没有调用过;另一个是给对象赋予了新值,这样重新分配了内存空间。

那么那些点可以作为GC Roots呢?

一般来说,如下情况的对象可以作为GC Roots:

虚拟机栈(栈桢中的本地变量表)中的引用的对象

方法区中的类静态属性引用的对象

方法区中的常量引用的对象

本地方法栈中JNI(Native方法)的引用的对象

二、Java中的内存泄漏

内存泄漏(Memory Leak):是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。【来自百度百科】

内存溢出(Out Of Memory):是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。【来自百度百科】

(1)栈内存用来存储局部变量和方法调用。

(2)堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。

(3)方法区是存放类型数据的, 而堆则是存放运行时产生的对象的。 和C++不同的是, Java只能在堆中存放对象, 而不能在栈上分配对象, 所有运行时产生的对象全部都存放于堆中, 包括数组。 我们知道, 在Java中, 数组也是对象。一个JVM实例中只有一个堆, 所有线程共享堆中的数据(对象) 。

(4)new指令执行的结果就是在堆中分配内存, 并创建对象。

1、Java中的内存管理:

(1)内存管理就是对象的分配和释放问题。

(2)在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾回收器(GarbageCollection,GC)完成的,GC回收无用并且不再被其它对象引用的那些对象所占用的空间。

(3)Java的垃圾回收机制,是从程序的主要运行对象开始检查引用链,当遍历一遍后发现没有被引用的孤立对象就作为垃圾回收。

(4)java虚拟机总共分为五个区域,其中三个是线程私有:程序计数器,虚拟机栈,本地方法栈,两个是线程共享:堆,方法区。 线程私有的区域等到线程结束时(栈帧出栈时)会自动被释放,空间比较容易清理。而线程共享的java堆和方法区中的空间较大而且没有线程的回收容易产生很多垃圾信息,GC垃圾回收真正关心的就是这部分。

2、Java中内存泄漏的原因:

长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。具体可能有如下几种情况:

(1)静态集合类引起内存泄漏:

像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。例如:

 Static Vector v = new Vector(100)
    for (int i = 0; i<100; i++){
        Object o = new Object();
        v.add(o);
        o = null;
    }

如果仅仅释放引用本身(o=null),那么Vector 仍然引用Object对象,最简单的方法就是将Vector对象设置为null。

(2)监听器:

在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。

(3)资源未关闭引起的内存泄漏:

常见由于资源未关闭/注销导致内存泄露有:

数据库连接、网络连接和io连接;游标 Cursor;Stream;Bitmap;File ...

这些资源应该打开,用完后马上关闭,或者在对象销毁时及时关闭或者注销,否则这些资源将不会被GC回收,造成内存泄漏。

(4)外部模块的引用:

例如程序A 模块调用了B 模块的一个方法如:     

public void registerMsg(Object b);        

这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。

(5)单例模式:

不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式)。如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏,就如下面的例子:

class A{
    public A(){
        B.getInstance().setA(this);
    }
}
//B类采用单例模式
class B{
    private A a;

    private B(){}
    private static B instance=new B();
    public static B getInstance(){
        return instance;
    }

    public void setA(A a){
        this.a=a;
    }
}

三、Android内存泄露及其解决

内存泄露分析工具:

(1)在build.gradle中引入:        

debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.3'        

releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'

(2)在Application中使用:        

LeakCanary.install(this);

(1)对Heap Dump的分析就是对应用的内存使用进行分析,从而更加合理地使用内存。

(2)Heap Dump也叫堆转储文件,是一个Java进程在某个时间点上的内存快照。Heap Dump是有着多种类型的。不过总体上heap dump在触发快照的时候都保存了java对象和类的信息。通常在写heap dump文件前会触发一次FullGC,所以heap dump文件中保存的是FullGC后留下的对象信息。

(3)JVM 因为要执行GC而停止了应用程序的执行。当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态,直到GC任务完成。GC优化很多时候就是指减少Stop-the-world发生的时间。MinorGC\MajorGC\FullGC。

1、Handler和AsyncTask等非静态内部类引起的内存泄漏:

private Handler handler = new Handler();    

  public void showLoading(String msg) {

        handler.removeCallbacks(runnable);

        handler.postDelayed(runnable, 10000); // 延迟10秒发送

    }

    // 内存泄漏的原因:内部类持有外部类对象的引用!

注:new Handler():匿名内部类,持有外部类实例的引用。 当handler 没有被回收时,其外围Activity对象不能被回收。当Activity被用户关闭(finish),而此时handler 还未被回收,那么Activity对象就不会被回收,造成Activity内存泄露。 通过Handler发往消息队列的Message对象持有了Handler对象的引用。假如Message对象一直在消息队列中未被处理释放掉,你的Handler对象就不会被释放,进而你的Activity也不会被释放。

// 方法1:在关闭Activity时(onDestroy等函数中),取消还在排队的Message,GC可以释放handler,而不再持有外部类对象的引用!

// 方法1:在关闭Activity时(onDestroy等函数中),取消还在排队的Message,GC可以释放handler,而不再持有外部类对象的引用!

    protected void onDestroy() {
        super.onDestroy();
        ......
        handler.removeCallbacks(runnable);
        handler = null;
    }
// 方法2:直接使用静态内部类,就不会持有外部类对象的引用
(静态内部类 + WeakReference弱引用这种方式)

    private static class MyHandler extends Handler {
        private final WeakReference<BaseActivity> mActivity;

        public MyHandler(BaseActivity activity) {
            mActivity = new WeakReference<BaseActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            BaseActivity activity = mActivity.get();
            if (activity != null) {
                // ...
            }
        }
    }

    private final MyHandler mHandler = new MyHandler(this);

注:当Activity想关闭销毁时,mHandler对它的弱引用没有影响,该销毁销毁;当mHandler通过WeakReference拿不到Activity对象时,说明Activity已经销毁了,就不用处理了,相当于丢弃了消息。 强引用、软引用、弱引用、虚引用。

2、静态变量引起的内存泄漏:

// 一个静态变量moduleBeanCallback,持有Activity传过来的moduleCallback,当Activity想销毁时moduleBeanCallback还在,
    导致内存无法回收Activity,造成内存泄漏。

    private static ModuleBean.Result<List<ModuleBean.Data.Module>> moduleBeanCallback;

    public static void getModuleList(String appId, String clientId, String token, String[] tags,
                                     ModuleBean.Result<List<ModuleBean.Data.Module>> moduleCallback) {
        Observable.just(moduleCallback)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .map(result -> {
                    moduleBeanCallback = result;
    ...
                }).subscribe();
    }

解决办法:去掉 moduleBeanCallback。

3、静态方法引起的内存泄漏:

// 全局函数工具FuncUtils,init()静态方法初始化后一直持有SplashActivity 的引用,onDestory执行后造成内存泄漏。

    public class SplashActivity extends BaseActivity {
        private void load() {
        ...
            FuncUtils.init(this);
        }

        public class FuncUtils {
            public static void init(Context context) {
        ...
            }
        }

解决办法:寻找与该方法生命周期差不多的替代对象。

        FuncUtils.init(this.getApplicationContext()); // 使用应用程序的上下文

4、如何避免内存泄漏:

工具方面:

        1、使用LeakCanary等内存泄漏分析工具,检测 Android 的内存泄漏;

        2、使用Android Studio 的Monitor,监测内存使用情况。

代码方面:

       尽早释放无用对象的引用,设置为null;

      使用合适的Context,尽量使用ApplicationContext;

      在 Activity 的 Destroy 时或者 Stop 时应该移除消息队列 MessageQueue 中的消息;

      特别注意一些像 HashMap 、ArrayList 的集合对象,当它们被声明为 static 时,它们的生命周期就会和应用程序一样长;            特别注意事件监听和回调函数 。当一个监听器在使用的时候被注册,但不再使用之后却未被反注册;

四、参考文献

https://mp.weixin.qq.com/s?__biz=MzAxOTc0NzExNg==&mid=416976590&idx=1&sn=22823ada76d8cfd26a43e8d3a7b7a60e&mpshare=1&scene=1&srcid=1221S9kMAs53asl1YxTqtP7r#rd

https://www.jianshu.com/p/54b5da7c6816

        ...

猜你喜欢

转载自blog.csdn.net/Agg_bin/article/details/85303598
今日推荐