android--垃圾回收与内存优化

什么是垃圾回收
  • 对比C/C++这种需要自己管理内存的语言
  • java可以实现自动内存管理和回收
  • 垃圾回收器负责回收程序中已经不使用,但是仍然被各种对象占用的内存。

优点:将程序员从繁重、危险的内存管理工作中解放出来
缺点:可能会占用大量资源

垃圾回收机制

Android系统里面有一个Generational Heap Memory的模型,系统会根据内存中不同的内存数据类型分别执行不同的GC操作。

执行GC操作的时候,任何线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行。


通常来说,单个的GC并不会占用太多时间,但是大量不停的GC操作则会显著占用帧间隔时间(16ms)。如果在帧间隔时间里面做了过多的GC操作,那么自然其他类似计算,渲染等操作的可用时间就变得少了。

年轻代(young generation)

年轻代是所有新对象产生的地方。当年轻代内存空间被用完时,就会触发垃圾回收。这个垃圾回收叫做Minor GC。年轻代被分为3个部分——Enden区和两个Survivor区

  • 大多数新建的对象都位于Eden区。
  • 当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区。
  • Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区。这样在一段时间内,总会有一个空的survivor区。
  • 经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。
老年代(Old Generation)

年老代内存里包含了长期存活的对象和经过多次Minor GC后依然存活下来的对象。通常会在老年代内存被占满时进行垃圾回收。老年代的垃圾收集叫做Major GC。Major GC会花费更多的时间。

永久代(Permanent Generation)

存放方法区,方法区中有要加载的类信息、静态变量、final类型的常量、属性和方法信息。

导致GC频繁执行有两个原因:
1.内存抖动
2.瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。

Android内存检测工具

1. Memory Monitor
android monitor能做什么:
a. 实时查看应用的内存分配情况
b. 判断应用是否由于GC操作造成卡顿
c. 判断应用崩溃是否是因为超出了内存

如何使用 Memory Monitor
进入项目后,可以看到Android Studio的主面板左下角有一个Android Monitor标签:


点击Android Monitor标签,然后点击Monitor标签,当项目运行的时候即可查看内存实时数据
内存的实时数据

在Android Monitor中我们可以手动触发GC,下图中的小车子就是触发GC的按钮,一旦按下就会回收那些没有被引用的对象

利用 Memory Monitor可以发现的问题
1.发现Memory Churn内存抖动,内存抖动是因为大量的对象被创建又在短时间内马上被释放。


2.发现大内存对象分配的场景
3.发现内存不断增长的场景
4.确定卡顿问题是否因为执行了GC操作

2. Allocation Tracker
Allocation Tracker是android studio的一个内存分配跟踪器,使用之后能够了解在一定时间内的内存分配情况。

如何使用Allocation Tracker
点击下图中画红圈的按钮可以启动标记


再次点击停止追踪

随后自动生成一个alloc结尾的文件,这个文件就记录了这次追踪到的所有数据,然后会打开一个数据面板,面板左上角是所有历史数据文件列表,后面是详细信息

查看方式

  • Group by Method:用方法来分类我们的内存分配
  • Group by Allocator:用内存分配器来分类我们的内存分配

默认会以Group by Method来组织。首先以线程对象分类,默认以分配顺序来排序。Count表示分配了多少次内存,size表示内存大小



以Group by Allocator来查看内存分配的情况如下图。这种方式显示的好处,是我们很好的定位我们自己的代码的分析信息


Jump To Source按钮
如果我们想看内存分配的实际在源码中发生的地方,可以选择需要跳转的对象,点击该下图中红圈的按钮就能发现我们的源码

统计图标按钮
Jump To Source按钮右边的按钮为统计按钮,点击该按钮,会弹出一个新窗口,里面是一个酷炫的统计图标,有柱状图和轮胎图两种图形可供选择,默认是轮胎图,其中分配比例可以选择分配次数和占用内存大小,默认是大小Size


圆心是我们的起点处,如果把鼠标放到我图中标注的区域,会在右边显示当前指示的是什么线程以及具体信息。
默认打开的是全局信息,我们如果想看其中某个线程,详细信息,可以顺着某个扇面向外围滑动,当然如果你觉得不还是不清晰,可以双击该扇面全面展现该扇面的信息。如果想回到默认显示的圆,双击圆心空白处就可以。

3.Heap Viewer
Heap Viewer能实时查看App分配的内存大小和空闲内存大小。还可以用于发现发现Memory Leaks。但这个功能只限于Android5.0以上。

如何使用Heap Viewer
在Android studio工具栏中直接点击小机器人:


选中要检测的app,然后点击cause GC:

总览

列名 含义
Heap Size 堆栈分配给App的内存大小
Allocated 已分配使用的内存大小
Free 空闲的内存大小
%Used Allocated/Heap Size,使用率
Objects 对象数量

详情

类型 意义
free 空闲的对象
data object 数据对象,类类型对象,最主要的观察对象
class object 类类型的引用对象
1-byte array(byte[],boolean[]) 一个字节的数组对象
2-byte array(short[],char[]) 两个字节的数组对象
4-byte array(long[],double[]) 4个字节的数组对象
non-Java object 非Java对象

当我们点击某一行时,可以看到如下的柱状图:


横坐标是对象的内存大小,这些值随着不同对象是不同的,纵坐标是在某个内存大小上的对象的数量

那么如何用heap viewer来检测内存泄露呢?在需要检测内存泄漏的用例执行过后,手动GC下,然后观察总览中的allocted(也可以观察Allocated/Heap Size内存的情况),看看内存是不是会回到一个稳定值,多次操作后,只要内存是稳定在某个值,那么说明没有内存溢出的,如果发现内存在每次GC后,都在增长,不管是慢增长还是快速增长,都说明有内存泄漏的可能性。

4. LeakCanary
这是一个是一个开源的库。github地址为LeakCanary。使用之后能够直接在手机或者虚拟机发生明显的内存泄漏之后弹出通知栏,告知在哪个类中出现问题。
下图的例子是因为一个static静态的TextView在所在的Activity被销毁时没有被回收而引起的,只要把TextView前的static去掉就可以了。

避免内存泄露的方法
  1. 尽量不要让静态变量引用activity
  2. 使用WeakReference
  3. 使用静态内部类来代替内部类
  4. 静态内部类使用弱引用引用外部类
  5. 在声明周期结束的时候释放资源
减少内存使用的方法
  1. 使用更轻量的数据结构(比如SpareArray代替HashMap)
  2. 避免在onDraw方法中创建对象
  3. 对象池(Message.obtain())
  4. LRUCache
  5. Bitmap内存复用,压缩(inSampleSize, inBitmap)
  6. StringBuilder
实例分析(MemoryBugs)

首先打开应用,点击startActivityB按钮,等待一段时间后弹出通知,点击通知显示如下图



从图中可以看出内存泄露是因为当MainActivity关闭之后,sTextView仍然持有MainActivity引用,导致无法回收MainActivity的引用。

我们再点击dump java heap



弹出heap viewer显示如下,从图中可以看到MainActivity仍然存在于内存中。具体的解决办法就是取消将sTextView声明为static。



将sTextView声明为static后,将代码中的延迟从5000改为20000
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                System.out.println("post delayed may leak");
            }
        }, 20000);

发现当点击跳转到ActivityB之后,通知提示如下图



这是因为Handler仍然持有MainActivity的引用

于是习惯性的将Handler声明为静态内部类

    public static class MyHandler extends Handler {
        private WeakReference<MainActivity> mWeakReference;

        public MyHandler(MainActivity activity) {
            mWeakReference = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }
private MyHandler mHandler;

protected void onCreate(Bundle savedInstanceState) {
       mHandler = new MyHandler(this);
}

但是仍然出现了内存泄露,于是看了下内存泄露的具体原因,似乎是匿名接口Runnable持有了对MainActivity的引用,于是创建一个Runnable静态内部类

public static class MyRunnable implements Runnable{
        public final WeakReference<MainActivity> mMainActivityWeakReference;

        public MyRunnable(MainActivity activity) {
            mMainActivityWeakReference = new WeakReference<>(activity);
        }


        @Override
        public void run() {
            System.out.println("post delayed may leak");
        }
 }
private Runnable mRunnable;

protected void onCreate(Bundle savedInstanceState) {
       mRunnable = new MyRunnable(this);
}
mHandler.postDelayed(mRunnable, 20000);

这是后再利用Heap Viewer验证,发现MainActivity的引用已经不存在了

接下来点击start allocation按钮,为了方便查看循环是否完成,在startAllocationLargeNumbersOfObjects方法最后添加

Toast.makeText(this, "完成循环", Toast.LENGTH_SHORT).show();

点击后,内存图如下所示,



如果连续点击,则会出现内存抖动



点击前start allocation tracking,当出现提示完成循环后结束tracking,结果如下图,发现有10000个Rect对象和10000个StringBuilder对象

具体优化方法是,只在onCreate()方法中新建一个Rect对象,并且将要输出的String事先定义

private Rect mRect;
private String printString;
protected void onCreate(Bundle savedInstanceState) {
       mRect=new Rect(0, 0, 100, 100);
       printString= "-------: " + mRect.width();
}
private void startAllocationLargeNumbersOfObjects() {
      Toast.makeText(this, R.string.memorymonitor, Toast.LENGTH_SHORT).show();
      for (int i = 0; i < 10000; i++) {
         System.out.println(printString);
      }
  }

最后将MyView类中的onDraw()方法中新建对象的操作放到声明变量的时候进行

    private RectF rect = new RectF(0, 0, 100, 100);
    private Paint paint = new Paint();
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容