上篇博客我们写到了 Java/Android 内存的分配以及相关 GC 的详细分析,这篇博客我们会继续分析 Android 中内存泄漏的检测以及相关案例,和 Android 的内存优化相关内容。
上篇:Android 性能优化之内存泄漏检测以及内存优化(上)。
中篇:Android 性能优化之内存泄漏检测以及内存优化(中)。
下篇:Android 性能优化之内存泄漏检测以及内存优化(下)。
转载请注明出处:http://blog.csdn.net/self_study/article/details/66969064
对技术感兴趣的同鞋加群544645972一起交流。
通过上篇博客我们了解了 Android JVM/ART 内存的相关知识和泄漏的原因,再来归类一下内存泄漏的源头,这里我们简单将其归为一下三类:
内存泄漏不像闪退的 BUG,排查起来相对要困难一些,比较极端的情况是当你的应用 OOM 才发现存在内存泄漏问题,到了这种情况才去排查处理问题的话,对用户的影响就太大了,为此我们应该在编码阶段尽早地发现问题,而不是拖到上线之后去影响用户体验,下面总结一下常用内存泄漏的定位和检测工具:
Lint 是 Android studio 自带的静态代码分析工具,使用起来也很方便,选中需要扫描的 module,然后点击顶部菜单栏 Analyze -> Inspect Code ,选择需要扫描的地方即可:
最后在 Performance 里面有一项是 Handler reference leaks,里面列出来了可能由于内部 Handler 对象持有外部 Activity 引用导致内存泄漏的地方,这些地方都可以根据实际的使用场景去排查一下,因为毕竟不是每个内部 Handler 对象都会导致内存泄漏。Lint 还可以自定义扫描规则,使用姿势很多很强大,感兴趣的可以去了解一下,除了 Lint 之外,还有像 FindBugs、Checkstyle 等静态代码分析工具也是很不错的。
StrictMode 是 Android 系统提供的 API,在开发环境下引入可以更早的暴露发现问题给开发者,于开发阶段解决它,StrictMode 最常被使用来检测在主线程中进行读写磁盘或者网络操作等耗时任务,把这些耗时任务放置于主线程会造成主线程阻塞卡顿甚至可能出现 ANR ,官方例子:
public void onCreate() {
if (DEVELOPER_MODE) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.penaltyDeath()
.build());
}
super.onCreate();
}
把上面这段代码放在早期初始化的 Application、Activity 或者其他应用组件的 onCreate 函数里面来启用 StrictMode 功能,一般 StrictMode 只是在测试环境下启用,到了线上环境就不要开启这个功能。启用 StrictMode 之后,在 logcat 过滤日志的地方加上 StrictMode 的过滤 tag,如果发现一堆红色告警的 log,说明可能就出现了内存泄漏或者其他的相关问题了:
比如上面这个就是因为调用 registerReceiver 之后忘记调用 unRegisterReceiver 导致的 activity 泄漏,根据错误信息便可以定位和修复问题。
LeakCanary 是一个 Android 内存泄漏检测的神器,正确使用可以大大减少内存泄漏和 OOM 问题,地址:
https://github.com/square/leakcanary
集成 LeakCanary 也很简单,在 build.gradle 文件中加入:
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
}
然后在 Application 类中添加下面代码:
public class ExampleApplication extends Application {
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code...
}
}
上面两步做完之后就算是集成了 LeakCanary 了,非常简单方便,如果程序出现了内存泄漏会弹出 notification,点击这个 notification 就会进入到下面这个界面,或者集成 LeakCanary 之后在桌面会有一个 LeakCanary 的图标,点击进去是所有的内存泄漏列表,点击其中一项同样是进入到下面界面:
这个界面就会详细展示引用持有链,一目了然,对于问题的解决方便了很多,堪称神器,更多实用姿势可以看看 LeakCanary FAQ。
还有一点需要提到的是,LeakCanary 在检测内存泄漏的时候会阻塞主界面,这是一点体验有点不爽的地方,但是这时候阻塞肯定是必要的,因为此时必须要挂起线程来获取当前堆的状态。然后也并不是每个 LeakCanary 提示的地方都有内存泄漏,这时候可能需要借助 MAT 等工具去具体分析。不过 LeakCanary 有一点非常好的地方是因为 Android 系统也会有一些内存泄漏,而 LeakCanary 对此则提供了一个 AndroidExcludedRefs 类来帮助我们排除这些问题。
可以通过命令 adb shell dumpsys meminfo [package name][-d]
来将指定 package name 的内存信息打印出来,-d
选项会额外打印信息,数据单位为 KB,这种模式可以通过 Activities 选项非常直观地看到 Activity 未释放导致的内存泄漏:
一般情况下我们只需要关心 PSS Total + Private Dirty。相关字段的含义如下:
Column | Description |
---|---|
Private Dirty/Clean | 这是你指定应用单独占用的内存数量,换句话说这个就是你应用完全被销毁之后系统可以回收的内存数。一般来说,最重要的部分就是 Privity Dirty 部分,因为这个是你指定进程单独占用的内存数,而且 Android 没有使用 Swap 策略(交换分区),所以这部分内存只能存在于 RAM 中。所有应用创建的 Dalvik 和 Native 堆都是 Private Dirty RAM,和 Zygote 进程共享的 Dalvik 和 Native 堆都是 Shared Dirty RAM。Private Dirty/Clean 的相同之处在于他们都只是被当前这个进程引用,区别在于 Clean 是内存中的页面一直没有被该进程写入修改(映射持久文件使用的内存页,例如 so 和 dex,因此如果内存紧张的话可能会被置换出去,下次再使用的时候重新读入即可)而 Dirty 则进行了写入修改,所以 Private Clean 部分内存进行写入操作之后就会被挪到 Private Dirty 中 |
Proportional Set Size (Pss) | 这个值是在应用所有占用 RAM 的基础上增加和其他应用进程共享的内存(比如同一个动态链接库被三个应用使用到,那么每个应用的 PSS 都会增加 so.size/3),如果只有你一个应用在使用这部分内存,那么这部分内存将直接加到 PSS 上。绝大多数情况下 PSS 都是大于等于 Private Dirty/Clean 数值 |
Swap Dirty | 某些 Android 设备可以开启 Swap 策略,Linux 下又一个策略可以把内存压缩之后交换到 RAM 里面的一个特殊区域,当使用之后又会把它解压缩之后重新交换进内存 |
Heap Size | 虚拟机分配的堆大小,数值上基本等于 Heap Alloc(已经分配的部分)+ Heap Free(可用部分) |
Heap Alloc | 统计的是虚拟机分配的所有应用实例的内存,也会将引用从 Zygote 共享的部分算进去,所以导致此值大于 Pss Total 和 Private Dirty |
Dalvik Heap | Dalvik 虚拟机使用的内存,Pss 包括了和其他应用共有的部分,而 Private Dirty 部分则是包含了应用自己分配和已经修改的 Zygote Allocation 页。包含 /dev/ashmem/dalvik-main space , /dev/ashmem/dalvik-allocspace , /dev/ashmem/dalvik-large object space , /dev/ashmem/dalvik-zygote space , /dev/ashmem/dalvik-non moving space 的 Pss 部分 |
Dalvik Other | 用于存放类的数据结构及关系以及管理 Dalvik 的内存(如, just-in-time compilation (JIT) and GC bookkeeping),随着类和函数数量,代码复杂度的增加而增加,除了 Dalvik Heap 外其他的以 /dev/ashmem/dalvik-* 开头的空间。 |
Ashmem | 共享内存 |
Other dev | 除了在 Dalvik Heap、Dalvik Other 、Ashmem 列出内存之外(5.1 版本之后也剔除了 /dev/kgsl-3d0 )其他以/dev/* 开头的内存区域 |
.so && .dex mmap | .so mmap 和 .dex mmap 是用来映射 so 和 dex 代码的内存,Pss Total 包含了和其他应用共享的框架代码和动态库,Private Clean 则是应用单独的代码部分。通常情况下实际映射的内存会更大,因为当前只加载了已经执行的代码在内存中。.so mmap 通常也会有 Private Dirty ,这是因为 so 库被加载到最终地址做了相关写入修改 |
.oat && .art mmap | 代码映像占用的内存,它是基于通常情况下多个应用共同使用的预加载类,这部分内存是多个应用共享,不会被单个应用影响。尽管 .art mmap 映像包含 Object 实例,它仍然不会计入您的堆大小。 |
EGL mtrack | 5.1 版本之前被称为Graphics , gralloc 分配的内存,主要是窗口系统,所有 Surface Buffer(4.1 版本之后开启了三倍缓冲,所以在没有 SurfaceView 和 TextureView 的情况下 EGL mtrack = 3 * 单个buffer的内存大小 ) 和 Atlas Buffer 的总和。按道理来说,Atlas Buffer 实际上是一个共享内存并且不应该被算入每个进程的内存占用里面,但是实际情况 Surface Buffer 和 Atlas Buffer 都被算入了,所以当评价一个应用的内存占用时,基本可以忽略该栏 |
GL mtrack | 5.1 版本之前被称为 GL,驱动上报的GL内存使用情况。 和 Gfx dev 加一起主要是 GL texture 大小,GL command buffer,固定的全局驱动程序 RAM 开销等的总和。 这部分内存可以在/d/kgsl/proc/PID/mem 里面查看。 |
Gfx dev | 5.1 版本加入,/dev/kgsl-3d0 Pss 部分内存区域占用,和 GL mtrack 一样都是 GPU 驱动上报的内存大小。用户驱动空间和内核驱动空间共享一块内存区域,在某些 Android 设备上可能会重复计算两次从而造成 Gfx dev 比实际的要大一些 |
Unknown | 系统无法将其分类到其他更具体的一个项中的任何 RAM 页。 其 Pss Total 与 Zygote共享 |
官方指引:https://developer.android.com/studio/command-line/dumpsys#meminfo
其他资料:https://blog.csdn.net/msf568834002/article/details/78881341
http://www.voidcn.com/article/p-wfedutyx-qx.html
https://unity3d.com/cn/learn/tutorials/topics/best-practices/android-memory-management
https://my.oschina.net/jerikc/blog/391907
Memory Profiler 是 Android Studio 自带的一个监控内存使用状态的工具,打开 Profiler 之后入口如下所示:
在 Android Profiler 点开之后 logcat 的右侧就是 Profiler 工具,其中可以检测内存、CPU、网络等内容,我们这里只用到了 Memory Profiler 功能,点击红色箭头所指的区域,就会 dump 此时此刻的 Memory 信息,并且生成一个 .hprof 文件,dump 完成之后会自动打开这个文件的显示界面,当然也可以点击最左侧的 Heap Dump 的保存按钮将 .hprof 文件保存下来,需要注意的是这个文件因为不是标准的Java SE HPROF 格式,所以如果需要在 MAT 上看就需要通过命令 hprof-conv heap-original.hprof heap-converted.hprof
进行转换:
接着我们来分析一下这个生成的 .hprof 文件所展示的信息:
首先左上角的下拉框,可以选择 App Heap、Image Heap 和 Zygote Heap,对应的就是上篇博客讲到的 Allocation Space,Image Space 和 Zygote Space,我们这里选择 Allocation Space,然后第二个选择 Arrange by package 这一项,展开之后就能看见一个树形结构了,然后继续展开我们应用包名的对应对象,就可以很清晰的看到有多少个 Activity 对象了,红框里面的信息解释如下:
Column | Description |
---|---|
Java | Dalvik Heap。从 Java 或 Kotlin 代码分配的对象内存 |
Native | Native Heap。从 C 或 C++ 代码分配的对象内存。即使应用中不使用 C++,也可能会看到此处使用的一些原生内存,因为 Android 框架使用原生内存代表您处理各种任务,如处理图像资源和其他图形时,即使只采用 Java 或 Kotlin 语言 |
Graphics | Gfxdev + EGL mtrack + GL mtrack。图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存(这是与 CPU 共享的内存,不是 GPU 专用内存) |
Stack | Stack。应用中的原生堆栈和 Java 堆栈使用的内存。 这通常与应用运行多少线程有关 |
Code | all mmaps。应用用于处理代码和资源(如 dex 字节码、已优化或已编译的 dex 码、.so 库和字体)的内存 |
Other | Other dev + Unknown。应用使用的系统不确定如何分类的内存 |
Allocated | 应用分配的 Java/Kotlin 对象数,没有计入 C 或 C++ 中分配的对象 |
Column | Description |
---|---|
Allocations | 指定的堆中实例数 |
Native Size | Native 层所有实例的总大小(以字节为单位) |
Shallow Size | 此堆中所有实例的总大小(以字节为单位) |
Retained Size | 为此类的所有实例而保留的内存总大小(以字节为单位),会大于等于 Shallow Size,代表该对象回收时可释放内存大小 |
Depth | 从 GC Root 到该对象的引用链路的最短步数 |
如果发现某个 Activity 对象存在多个,并且 GC 后也不会减少,那就大概率是该 Activity 泄漏了,点击展开 Refereces,从对象找到 GC Roots就可以找到是被谁强饮用无法释放了:
可以看到 Thread 对象持有了 SecondActivity 对象的引用,也就是 GC Root 持有了该 Activity 的引用,导致这个 Activity 无法回收,问题的根源我们就发现了,接下来去处理它就好了。
Android Memory Profiler 除了上面提到的记录展示某个时刻应用内存占用详情的功能外,还能获取某段时间内应用的对象分配情况,使用方式也很简单,找一个 8.0 及之上的 Android 手机,在 Memory Profiler 时间线上拖动一个起始和结束即可:
这样下方的 Live Allocation 框里面就是应用内存实时分配的详情,里面四个选项分别代表:
Column | Description |
---|---|
Allocations | 这段时间之内指定的堆中分配的实例数 |
DealLocations | 这段时间之内指定的堆中释放的实例数 |
Total Count | 此堆中所有实例的数量 |
Shallow Size | 此堆中所有实例的总大小(以字节为单位) |
MAT(Memory Analyzer Tools)是一个 Eclipse 插件,它是一个快速、功能丰富的 JAVA heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗,MAT 插件的下载地址:Eclipse Memory Analyzer Open Source Project,上面通过 Android studio 生成的 .hprof 文件因为格式稍有不同,所以我们需要现将 AS 生成的 .hprof 文件保存到本地之后,通过 hprof-conv heap-original.hprof heap-converted.hprof
命令进行转换。通过 MAT 去打开转换之后的这个文件:
用的最多的就是 Histogram 功能,点击 Actions 下的 Histogram 项就可以得到 Histogram 结果:
我们可以在左上角写入一个正则表达式,然后就可以对所有的 Class Name 进行筛选了,很方便,顶栏展示的信息 “Objects” 代表该类名对象的数量,剩下的 “Shallow Heap” 和 “Retained Heap” 则和 Android Memory Profiler 类似。咱们接着点击 SecondActivity,然后右键:
在弹出来的菜单中选择 List objects->with incoming references 将该类的实例全部列出来:
通过这个列表我们可以看到 SecondActivity@0x12faa900 这个对象被一个 this$00x12c65140 的匿名内部类对象持有,然后展开这一项,发现这个对象是一个 handler 对象:
快速定位找到这个对象没有被释放的原因,可以右键 Path to GC Roots->exclude all phantom/weak/soft etc. references 来显示出这个对象到 GC Root 的引用链,因为强引用才会导致对象无法释放,所以这里我们要排除其他三种引用:
这么处理之后的结果就很明显了:
一个非常明显的强引用持有链,GC Root 我们前面的博客中说到包含了线程,所以这里的 Thread 对象 GC Root 持有了 SecondActivity 的引用,导致该 Activity 无法被释放。
MAT 还有一个功能就是能够对比两个 .hprof 文件,将两个文件都添加到 Compare Basket 里面:
添加进去之后点击右上角的 ! 按钮,然后就会生成两个文件的对比:
同样适用正则表达式将需要的类筛选出来:
结果也很明显,退出 Activity 之后该 Activity 对象未被回收,仍然在内存中,或者可以调整对比选项让对比结果更加明显:
也可以对比两个对象集合,方法与此类似,都是将两个 Dump 结果中的对象集合添加到 Compare Basket 中去对比,找出差异后用 Histogram 查询的方法找出 GC Root,定位到具体的某个对象上。
我们来看看常见的导致内存泄漏的案例:
由于静态变量的生命周期和应用一样长,所以如果静态变量持有 Activity 或者 Activity 中 View 对象的应用,就会导致该静态变量一直直接或者间接持有 Activity 的引用,导致该 Activity 无法释放,从而引发内存泄漏,不过需要注意的是在大多数这种情况下由于静态变量只是持有了一个 Activity 的引用,所以导致的结果只是一个 Activity 对象未能在退出之后释放,这种问题一般不会导致 OOM 问题,只能通过上面介绍过的几种工具在开发中去观察发现。
这种问题的解决思路很简单,就是不让静态变量直接或者间接持有 Activity 的强引用,可以将其修改为 soft reference 或者 weak reference 等等之类的,或者如果可以的话将 Activity Context 更换为 Application Context,这样就能保证生命周期一致不会导致内存泄漏的问题了。
我们上面的 demo 中模拟的就是内部类对象持有外部类对象的引用导致外部类对象无法释放的问题,在 Java 中非静态内部类和匿名内部类会持有他们所属外部类对象的引用,如果这个非静态内部类对象或者匿名内部类对象被一个耗时的线程(或者其他 GC Root)直接或者间接的引用,甚至这些内部类对象本身就在做一些耗时操作,这样就会导致这个内部类对象直接或者间接无法释放,内部类对象无法释放,外部类的对象也就无法释放造成内存泄漏,而且如果无法释放的对象积累起来就会造成 OOM,示例代码如下所示:
public class SecondActivity extends AppCompatActivity{
private Handler handler;
private Bitmap bitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pic);//decode 一个大图来模拟内存无法释放导致的崩溃
findViewById(R.id.btn_second).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
handler.sendEmptyMessage(0);
}
}).start();
}
}
这个问题的解决方法可以根据实际情况进行选择:
这个很好理解,在一个错误的地方使用 Activity Context,造成 Activity Context 被静态变量长时间引用导致无法释放而引发的内存泄漏,这个问题的处理方式也很简单,如果可以的话修改为 Application Context 或者将强引用变成其他引用。
资源性对象比如(Cursor,File 文件等)往往都用了一些缓冲,我们在不使用的时候应该及时关闭它们,以便它们的缓冲对象被及时回收,这些缓冲不仅存在于 java 虚拟机内,还存在于 java 虚拟机外,如果我们仅仅是把它的引用设置为 null 而不关闭它们,往往会造成内存泄漏。但是有些资源性对象,比如 SQLiteCursor(在析构函数 finalize(),如果我们没有关闭它,它自己会调 close() 关闭),如果我们没有关闭它系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的 close() 函数,将其关闭掉,然后再置为 null,在我们的程序退出时一定要确保我们的资源性对象已经关闭。
程序中经常会进行查询数据库的操作,但是经常会有使用完毕 Cursor 后没有关闭的情况,如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会出现内存问题,这样就会给以后的测试和问题排查带来困难和风险,示例代码:
Cursor cursor = getContentResolver().query(uri...);
if (cursor.moveToNext()) {
... ...
}
更正代码:
Cursor cursor = null;
try {
cursor = getContentResolver().query(uri...);
if (cursor != null && cursor.moveToNext()) {
... ...
}
} finally {
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
//ignore this
}
}
}
在实际开发过程中难免会有把对象添加到集合容器(比如 ArrayList)中的需求,如果在一个对象使用结束之后未将该对象从该容器中移除掉,就会造成该对象不能被正确回收,从而造成内存泄漏,解决办法当然就是在使用完之后将该对象从容器中移除。
具体的可以看看我的这篇博客:android WebView详解,常见漏洞详解和安全源码(下)。
一些 Android 程序可能引用我们的 Android 程序的对象(比如注册机制),即使我们的 Android 程序已经结束了,但是别的应用程序仍然还持有对我们 Android 程序某个对象的引用,这样也会造成内存不能被回收,比如调用 registerReceiver 后未调用unregisterReceiver。假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息,则可以在 LockScreen 中定义一个 PhoneStateListener 的对象,同时将它注册到 TelephonyManager 服务中,对于 LockScreen 对象,当需要显示锁屏界面的时候就会创建一个 LockScreen 对象,而当锁屏界面消失的时候 LockScreen 对象就会被释放掉,但是如果在释放 LockScreen 对象的时候忘记取消我们之前注册的 PhoneStateListener 对象,则会间接导致 LockScreen 无法被回收,如果不断的使锁屏界面显示和消失,则最终会由于大量的 LockScreen 对象没有办法被回收而引起 OOM,虽然有些系统程序本身好像是可以自动取消注册的(当然不及时),但是我们还是应该在程序结束时明确的取消注册。
还有一种情况是因为频繁的内存分配和释放,导致内存区域里面存在很多碎片,当这些碎片足够多,new 一个大对象的时候,所有的碎片中没有一个碎片足够大以分配给这个对象,但是所有的碎片空间加起来又是足够的时候,就会出现 OOM,而且这种 OOM 从某种意义上讲,是完全能够避免的。
由于产生内存碎片的场景很多,从 Memory Profiler 来看,下面场景的内存抖动是很容易产生内存碎片的:
最常见产生内存抖动的例子就是在 ListView 的 getView 方法中未复用 convertView 导致 View 的频繁创建和释放,针对这个问题的处理方式那当然就是复用 convertView;或者是 String 拼接创建大量小的对象(比如在一些频繁调用的地方打字符串拼接的 log 的时候);如果是其他的问题,就需要通过 Memory Profiler 去观察内存的实时分配释放情况,找到内存抖动的地方修复它,或者如果当出现下面这种情况下的 OOM 时,也是由于内存碎片导致无法分配内存:
出现上面这种类型的 Crash 时就要去分析应用里面是不是存在大量分配释放对象的地方了。
内存优化请看下篇:Android 性能优化之内存泄漏检测以及内存优化(下)。
http://blog.csdn.net/luoshengyang/article/details/42555483
http://blog.csdn.net/luoshengyang/article/details/41688319
http://blog.csdn.net/luoshengyang/article/details/42492621
http://blog.csdn.net/luoshengyang/article/details/41338251
http://blog.csdn.net/luoshengyang/article/details/41581063
https://mp.weixin.qq.com/s?__biz=MzA4MzEwOTkyMQ==&mid=2667377215&idx=1&sn=26e3e9ec5f4cf3e7ed1e90a0790cc071&chksm=84f32371b384aa67166a3ff60e3f8ffdfbeed17b4c8b46b538d5a3eec524c9d0bcac33951a1a&scene=0&key=c2240201df732cf062d22d3cf95164740442d817864520af90bb0e71fa51102f2e91475a4f597ec20653c59d305c8a3e518d3f575d419dfcf8fb63a776e0d9fa6d3a9a6a52e84fedf3f467fe4af1ba8b&ascene=0&uin=Mjg5MDI3NjQ2Mg%3D%3D&devicetype=iMac+MacBookPro11%2C4+OSX+OSX+10.12.3+build(16D32)&version=12010310&nettype=WIFI&fontScale=100&pass_ticket=Upl17Ws6QQsmZSia%2F%2B0xkZs9DYxAJBQicqh8rcaxYUjcu3ztlJUPxYrQKML%2BUtuf
http://geek.csdn.net/news/detail/127226
http://www.jianshu.com/p/216b03c22bb8
https://zhuanlan.zhihu.com/p/25213586
https://joyrun.github.io/2016/08/08/AndroidMemoryLeak/
http://www.cnblogs.com/larack/p/6071209.html
https://source.android.com/devices/tech/dalvik/gc-debug.html
http://blog.csdn.net/high2011/article/details/53138202
http://gityuan.com/2015/10/03/Android-GC/
http://www.ayqy.net/blog/android-gc-log解读/
https://developer.android.com/studio/profile/investigate-ram.html
https://zhuanlan.zhihu.com/p/26043999
https://blog.csdn.net/msf568834002/article/details/78881341