Android从GC Root分析内存泄漏

排查内存泄漏问题时,就需要对GC和内存分配有必要的了解。

我们常说的垃圾回收机制中会提到GC Roots这个词,也就是Java虚拟机中所有引用的根对象。我们都知道,垃圾回收器不会回收GC Roots以及那些被它们间接引用的对象。但是对于GC Roots的定义却不是很清楚。它们都包括哪些对象呢?

一、判断可回收对象,理解跟搜索法

执行GC时通过判断对象是否存活来决定对象能否被回收。大家了解的算法有引用计数法、根搜索算法(可达性分析法),其中Java采用的就是根搜索算法。

这个引申一下引用计数法为什么没有作为回收算法

        //n1为obj1的引用计数器  n2为obj2的饮用计数
        //n1 = 0;n2 = 0

        Object obj1 = new Object(); //n1=1
        Object obj2;
        obj2 = obj1;    //n1=2; n2 = 1
        obj1 = null;    //n1=1; n2 = 1; 导致obj1不能被回收

      根搜索算法(GC Roots Tracing)的基本思路是通过一系列名为“GC Roots”的对象作为起始点,从这个节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论术语描述就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。在主流的商用程序语言中(Java、C#),都是使用根搜索算法判定对象是否存活的。

如下图右侧的‘对象1、2、2、3、4’都不能被GC RootB引用即不可达,可以在GC的时候被回收。

    在《Java编程思想》中可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中的常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)的引用的对象。

  Android JVM中GC Roots的大致分类

  • Class 由System Class Loader/Boot Class Loader加载的类对象,这些对象不会被回收。需要注意的是其它的Class Loader实例加载的类对象不一定是GC root,除非这个类对象恰好是其它形式的GC root;
  • Thread 线程,激活状态的线程;
  • Stack Local 栈中的对象。每个线程都会分配一个栈,栈中的局部变量或者参数都是GC root,因为它们的引用随时可能被用到;
  • JNI Local JNI中的局部变量和参数引用的对象;可能在JNI中定义的,也可能在虚拟机中定义
  • JNI Global JNI中的全局变量引用的对象;同上
  • Monitor Used 用于保证同步的对象,例如wait(),notify()中使用的对象、锁等。
  • Held by JVM JVM持有的对象。JVM为了特殊用途保留的对象,它与JVM的具体实现有关。比如有System Class Loader, 一些Exceptions对象,和一些其它的Class Loader。对于这些类,JVM也没有过多的信息。

参考:YourKit

我们将详细阐述可作为GC Roots引用对象的前三条,往下分析前需要先了解堆区、栈区、方法区、栈帧的概念。可以参考我的前一篇帖子

其中Boot Class Loader:

Android中的类加载器是BootClassLoader、PathClassLoader、DexClassLoader

BootClassLoader是虚拟机加载系统类需要用到的;

PathClassLoader是App加载自身dex文件中的类用到的;

DexClassLoader可以加载直接或间接包含dex文件的文件,如APK等。

说到这里就产生了疑问那平时的创建的Activity的子类是否为GC Root?

打印日志可以发现Activity的子类是PathClassLoader加载出来的, 它不满足由Boot Class Loader加载的条件说明它不属于第一条1所定义的gc_root。还记得gc_root有几种分类吗? 方法区中类的静态属性、方法区中常量引用的对象等等也是gc_root (为什么也是,你再思考一下1)。推测Activity的子类在特定条件下也会成为gc_root 比如你使用了一个静态变量引用了这个子类 。
关于这个大家有其他观点还望评论相互交流

 

三、实例分析

  • 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中的常量引用的对象。
    class Fruit {

        static int x = 10;
        static BigWaterMelon bigWaterMelon_1 = new BigWaterMelon(x);
        
        int y = 20;
        BigWaterMelon bigWaterMelon_2 = new BigWaterMelon(y);
        
        public static void main(String[] args) {
            
            final Fruit fruit = new Fruit();
            int z = 30;
            BigWaterMelon bigWaterMelon_3 = new BigWaterMelon(z);
            
            new Thread() {
                @Override
                public void run() {
                    int k = 100;
                    setWeight(k);
                }

                void setWeight(int waterMelonWeight) {
                    fruit.bigWaterMelon_2.weight = waterMelonWeight;
                }
            }.start();
        }
    }
    
    class BigWaterMelon {

        public BigWaterMelon(int weight) {
            this.weight = weight;
        }

        public int weight;
    }

由于方法区和堆内存的数据都是线程间共享的,所以线程Main Thread,New Thread和Another Thread都可以访问方法区中的静态变量以及访问这个变量所引用对象的实例变量。

栈内存中每个线程都有自己的虚拟机栈,每一个栈帧之间的数据就是线程独有的了,也就是说线程New Thread中setWeight方法是不能访问线程Main Thread中的局部变量bigWaterMelon_3,但是我们发现setWeight却访问了同为Main Thread局部变量的“fruit”,这是为什么呢?因为“fruit”被声明为final了。

(为什么内部类可以引用到final 修饰的局部变量)

当“fruit”被声明为final后,“fruit”会作为New Thread的构造函数的一个参数传入New Thread,也就是堆内存中Fruit$1对象中的实例变量val$fruit会引用“fruit”引用的对象,从而New Thread可以访问到Main Thread的局部变量“fruit”。

注:

val$fruit:表示当前对象的fruit字段

Test$1.class当一个类文件编译之后有很多类名字中有$符, 比如Test.class, Test$1.class, Test$2.class, Test$MyTest.class
$后面跟数字的类就是匿名类编译出来的结果.Test$MyTest.class则是内部类MyTest编译后得到的。

 

三.常踩的坑

1.单例模式

    public class AccountMananger {
        private Context context;
        private static AccountMananger instance = null;

        public static AccountMananger getInstance(Context context) {
            if (instance == null) {
                synchronized (AccountManager.class) {
                    if (instance == null) {
                        instance = new AccountMananger(context);
                    }
                }
            }
            return instance;
        }

        private AccountMananger(Context context) {
            context = context;
        }
    }

上面这段代码就很危险,static修饰的instance存在方法区中,被GC Root直接引用不会被回收。 单例对象持有一个Context,它可能是一个 Activity 也可能是一个 Service。context与GC Root断开后仍然被AccountMananger对象引用着导致context对象不能被回收。 Activity对象包括大量的布局和资源文件 一旦它被该单例持有,它所持有的资源在应用结束前都不会被释放。

修改的方法很简单:使用Application即可。

上面的context可能是Activity的引用,在这讨论一下

2.非静态内部类/匿名类 + 静态变量

public class MainActivity extends AppCompatActivity {
    private static MyHandler handler = new MyHandler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    public class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

        }
    }
}

非静态内部类会持有外部类的引用(所以它才可以直接访问外部类的成员变量)。上面代码中的static handler变量间接持有了MainActivity对象,并且static修饰的handler被GC Root引用这样就造成了内存泄漏。
解决的方法:然后将Handler改成静态内部类或者外部一个类。或者将将它放到弱引用中。

3.Thread

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        AsyncTasks asyncWork = new AsyncTasks(this);
        ExecutorService defaultExecutor = Executors.newCachedThreadPool();
        defaultExecutor.execute(asyncWork);
    }

    public static class AsyncTasks implements Runnable {
        private Context context;

        public AsyncTasks(Context context) {
            this.context = context;
        }

        @Override
        public void run() {
            while (true) ;
            //正常情况下,线程执行时间不会无限,但可能有5分钟,10分钟
        }
    }
}

线程中持有一个Activity对象,在这个线程活跃的时间内这个Activity对象都不会被释放。因此,其它线程中尽量不要持有Activity,Service等大对象。如果需要用到Context,尽量使用ApplicationContext

上面是总结了几个常见的内存泄漏写法,内存泄漏严重的话导致大量的垃圾数据不能回收,恶性循环频繁的GC导致性能下降用户体验下降,最终堆内存爆掉造成OOM。

与大家互相学习,如有错误的地方望大家指正

 

 

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值