内存优化——内存泄露

2019-03-06  本文已影响0人  追寻米K

Java内存模型
在曾经的sun公司 制定的java虚拟机规范中,运行时内存模型,分为线程私有和共享数据区两大类,其中线程私有的数据区包含程序计数器、虚拟机栈、本地方法区,所有线程共享的数据区包含Java堆、方法区,在方法区内有一个常量池。

Java内存模型

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。
——因为它们属于方法中的变量,生命周期随方法而结束。
成员变量全部存储于堆中(包括基本数据类型,引用和引用的对象实体)
——因为它们属于类,类对象终究是要被new出来使用的。

我们说的内存泄露,是针对,也只针对堆内存,他们存放的就是引用指向的对象实体。

堆中几乎存放着Java世界中所有的对象实例,垃圾收集器在对堆回收之前,第一件事情就是要确定这些对象哪些还“存活”着,哪些对象已经“死去”

确定对象是否活着的方法有:
1、引用计数算法
1.1算法分析 
引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
1.2优缺点
优点:
  引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:
  无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.
1引用计数算法无法解决循环引用问题,例如:

public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
        
        object1.object = object2;
        object2.object = object1;
        
        object1 = null;
        object2 = null;
    }
}

最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。

2、可达性分析算法(主流方法)



可达性分析算法中,通过一系列的gc root为起始点,从一个GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
java中可作为GC Root的对象有
  1.虚拟机栈(本地变量表)中正在运行使用的引用
  2.方法区中静态属性引用的对象
  3. 方法区中常量引用的对象
4.本地方法栈JNI中引用的对象(Native对象)
上图中objD与objE到GC ROOT不可达,所以可以被回收。而其他的对gc root可达。

在说到内存的问题,我们都会提到一个关键词:引用

在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且生命周期较长的对象时候,可以尽量应用软引用和弱引用技术。

对于软引用和弱引用的选择,
如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。

内存泄漏就是 :堆内存中的长生命周期的对象持有短生命周期对象的引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄露的根本原因。

排查内存泄漏问题

在android中我们执行一段代码,比如进入了一个新的页面(Activity),这时候我们的内存使用肯定比在前一个页面大,而在界面finish返回后,如果内存没有回落,那么很有可能就是出现了内存泄漏。

从内存监控工具中观察内存曲线,是否存在不断上升的趋势且不会在程序返回时明显回落。这种方式可以发现最基本,也是最明显的内存泄露问题,对用户价值最大,操作难度小,性价比极高。因为他能发现很明显很严重的内存泄漏问题

我们可以通过AS的Memory Profile或者DDMS中的heap观察内存使用情况。

工具的使用

在Android Studio我们可以点击下面图标运行APP



然后我们能看到


点击memory后



① 强制执行垃圾收集事件的按钮。
② 捕获堆转储的按钮。
③ 记录内存分配的按钮。
④ 放大时间线的按钮。
⑤ 跳转到实时内存数据的按钮。
⑥ 事件时间线显示活动状态、用户输入事件和屏幕旋转事件。
⑦ 内存使用时间表,其中包括以下内容:
每个内存类别使用多少内存的堆栈图,如左边的y轴和顶部的颜色键所示。
虚线表示已分配对象的数量,如右侧y轴所示。

如果不知道哪儿出现了内存泄漏的时候,我们打开一堆的页面最后返回主页,然后gc(点上面的①),再dump下内存查看。


dump

然后等待一段时间会出现:



这个页面想要分析出什么很难,最多打开看一眼刚刚我们进入的Activity是否因为我们退出而回收。先按照包名来分组,把页面向下滚动一下。

Alloc Cout : 对象数(对象数异常就很可能存在泄露)
Shallow Size : 对象占用内存大小
Retained Set : 对象引用组占用内存大小(包含了这个对象引用的其他对象)

在某些android studio中



这个自动分析任务包含了两个内容,一个是检测Activity的泄漏,一个是检测重复字符串。
点击运行分析:

这里出现了MainActivity的泄漏。并且观察到这个MainActivity可能不止一个对象存在,可能是我们上次退出程序的时候发生了泄漏,导致它不能回收。

android studio 3.3中
点击一个存在泄露的对象,如某个activity,会出现如下:


不仅显示出每个具体的对象,在下面Reference还显示出在哪里被引用了,能直接定位到问题代码,比之前的版本要更强大了。

当然一次dump可能并不能发现内存泄漏,可能每次我们dump的结果都不同,那么就需要多试几次,然后结合代码来排查。
如果这里还不能确定发生了内存泄漏。
我们这时候可以借助一些更专业的工具Mat(Memory Analyzer Tool)来进行内存的分析,Memory Analyzer Tool基于eclipse,可以在eclispe上安装mat插件,可以直接下载:
http://www.eclipse.org/mat/downloads.php
先把这个内存快照保存为hprof文件。

保存快照按钮
android studio 3.3把鼠标放在Heap Dump上
3.3保存快照
在使用mat之前我们需要把快照hprof文件转换成MAT标准的hprof文件。转换工具在SDK/platform-tools/hprof-conv,把SDK/platform-tools配置环境变量,在快照文件目录运行工具。

-z 排除不是app的内存,比如Zygote
生成MAT标准的hprof文件:
hprof-conv -z src dst

生成之后多一个hprof文件

打开下载好的MAT:
在File→Open Heap Dump打开生成的hprof文件

圆形图显示的内存占用情况。
但是我们要分析具体哪里内存泄露了还需要看直方Histogram:
点击直方图Histogram
点击直方图Histogram
结合android Studio上面提示MainActivity的泄露,我们在Regex搜索MainActivity
搜索结果
右键一个MainActivity

List objects引用了哪些对象
Show objects by class 被哪些对象引用
我们选择第三个Merge Shortest Path to GC Roots
由前面的原理可知,内存要被回收就要跟GC Root断开,所以没有被回收的对象我们需要看看被哪些引用了。

有排除软引用、弱引用、虚引用。我们选着三个都排除的。

全部展开之后,就能看见对象被谁引用了,导致没有被回收。至于具体是什么原因导致的,还需要结合代码,具体分析。
LeakCanary库也能检测内存泄漏,但只能起到辅助作用
https://github.com/square/leakcanary

内存泄漏常见原因:

  try {
            FileOutputStream fos = new FileOutputStream("a.txt");
            fos.write("test".getBytes());
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }

现在这段代码是存在隐患的,因为如果write发生异常那么这个fos会因为没有close造成内存泄漏。
所以正确的方式应该是:

FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("a.txt");
            fos.write("test".getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (fos !=null){
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
  private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //TODO
        }
    };

这种创建Handler的方式会造成内存泄漏,由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏,所以另外一种做法为:

   private MyHandler mHandler = new MyHandler(this);
    private static class MyHandler extends Handler{
        private WeakReference<Context> reference;
        private MyHandler(Context context){
            reference = new WeakReference<>(context);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity activity = (MainActivity) reference.get();
            if (activity !=null){
                //TODO
            }
        }
    }

这样虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,所以我们在Activity的Destroy时或者Stop时应该移除消息队列中的消息。

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

这样写代码比较麻烦,还可以使用weakHandler库,跟Handler一样的用法。
https://github.com/badoo/android-weak-handler

上一篇 下一篇

猜你喜欢

热点阅读