Java内存问题 及 LeakCanary 原理分析

前些天,有人问到 “开发过程中常见的内存泄漏都有哪些?”,一时脱口而出:静态的对象中(包括单例)持有一个生命周期较短的引用时,或内部类的子代码块对象的生命周期超过了外面代码的生命周期(如非静态内部类,线程),会导致这个短生命周期的对象内存泄漏。总之就是一个对象的生命周期结束(不再使用该对象)后,依然被某些对象所持有该对象强引用的场景就是内存泄漏。

这样回答很明显并不是问答人想要的都有哪些场景,所以这里抽时间整理了下内存相关的知识点,及LeakCanary工具的原理分析。

 

Java内存问题 及 LeakCanary 原理分析

在安卓等其他移动平台上,内存问题显得特别重要,想要做到虚拟机内存的高效利用,及内存问题的快速定位,了解下虚拟机内存模块及管理相关知识是很有必要的,这篇文章将从最基础的知识分析,内存问题的产生地方、原因、解决方案等原理。

一、运行时内存区域

Java内存问题 及 LeakCanary 原理分析

这里以Java虚拟机为例,将运行时内存区分为不同的区域,每个区域承担着不同的功能。

方法区
用户存储已被虚拟机加载的类信息,常量,静态常量,即时编译器编译后的代码等数据。异常状态 OutOfMemoryError,其中包含常量池和用户存放编译器生成的各种字面量和符号引用。


是JVM所管理的内存中最大的一块。唯一目的就是存放实例对象,几乎所有的对象实例都在这里分配。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。异常状态 OutOfMemoryError。

虚拟机栈
描述的是java方法执行的内存模型,每个方法在执行时都会创建一个栈帧,用户存储局部变量表,操作数栈,动态连接,方法出口等信息。每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 对这个区域定义了两种异常状态 OutOfMemoryError、*Error。

本地方法栈
虚拟机栈为虚拟机执行java方法,而本地方法栈为虚拟机使用到的Native方法服务。异常状态*Error、OutOfMemoryError。

程序计数器
一块较小的内存,当前线程所执行的字节码的行号指示器。字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

内存模型

Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量,这些变量是从主内存中拷贝而来。线程对变量的所有操作(读,写)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

为了保证内存可见性,常常利用volatile关键子特性来保证变量的可见性(并不能保证并发时原子性)。

二、内存如何回收

内存的分配

一个对象从被创建到回收,主要经历阶段有 1:创建阶段(Created)、2: 应用阶段(In Use)、3:不可见阶段(Invisible)、4:不可达阶段(Unreachable)、5:收集阶段(Collected)、6:终结阶段(、Finalized)、7:对象空间重分配阶段(De-allocated)。

内存的分配实在创建阶段,这个阶段要先用类加载器加载目标class,当通过加载器检测后,就开始为新对象分配内存。对象分配内存大小在类加载完成后便可以确定。
当初始化完成后,虚拟机还要对对象进行必要的设置,如那个类的实例,如何查找元数据、对象的GC年代等。

内存的回收(GC)

那些不可能再被任何途径使用的对象,需要被回收,否则内存迟早都会被消耗空。

GC机制主要是通过可达性分析法,通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链时,即GC Roots到对象不可达,则证明此对象是不可达的。

根据《深入理解Java虚拟机》书中描述,可作为GC Root的地方如下:

  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(Native方法)引用的对象。

当一个对象或几个相互引用的对象组没有任何引用链时,会被当成垃圾处理,可以进行回收。

如何一个对象在程序中已经不再使用,但是(强)引用还是会被其他对象持有,则称为内存泄漏。内存泄漏并不会使程序马上异常,但是多处的未处理的内存泄漏则可能导致内存溢出,造成不可预估的后果。

引用的分类

在JDK1.2之后,为了优化内存的利用及GC的效率,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用4种。

1、强引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

2、软引用,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。SoftReference表示软引用。

3、弱引用,只要有GC,无论当前内存是否足够,都会回收掉被弱引用关联的对象。WeakReference表示弱引用。

4、虚引用,这个引用存在的唯一目的就是在这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没关系。PhantomReference表示虚引用,需要搭配ReferenceQueue使用,检测对象回收情况。

关于JVM内存管理的一些建议

1、尽可能的手动将无用对象置为null,加快内存回收。
2、可考虑对象池技术生成可重用的对象,较少对象的生成。
3、合理利用四种引用。

三、内存泄漏

持有一个生命周期较短的引用时或内部的子模块对象的生命周期超过了外面模块的生命周期,即本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

内存泄漏是造成应用程序OOM的主要原因之一,尤其在像安卓这样的移动平台,难免会导致应用所需要的内存超过系统分配的内存限额,这就造成了内存溢出Error。

安卓平台常见的内存泄漏

1、静态成员变量持有外部(短周期临时)对象引用。 如单例类(类内部静态属性)持有一个activity(或其他短周期对象)引用时,导致被持有的对象内存无法释放。

2、内部类。当内部类与外部类生命周期不一致时,就会造成内存泄漏。如非静态内部类创建静态实例、Activity中的Handler或Thread等。

3、资源没有及时关闭。如数据库、IO流、Bitmap、注册的相关服务、webview、动画等。

4、集合内部Item没有置空。

5、方法块内不使用的对象,没有及时置空。

四、如何检测内存泄漏

Android Studio供了许多对App性能分析的工具,可以方便分析App性能。我们可以使用Memory Monitor和Heap Dump来观察内存的使用情况、使用Allocation Tracker来跟踪内存分配的情况,也可以通过这些工具来找到疑似发生内存泄漏的位置。

堆存储文件(hpof)可以使用DDMS或者Memory Monitor来生成,输出的文件格式为hpof,而MAT(Memory Analysis Tool)就是来分析堆存储文件的。

然而MAT工具分析内存问题并不是一件容易的事情,需要一定的经验区做引用链的分析,需要一定的门槛。
随着安卓技术生态的发展,LeakCanary 开源项目诞生了,只要几行代码引入目标项目,就可以自动分析hpof文件,把内存泄漏的地方展示出来。

五、LeakCanary原理解析

Java内存问题 及 LeakCanary 原理分析

A small leak will sink a great ship.

LeakCanary内存检测工具是由squar公司开源的著名项目,这里主要分析下源码实现原理。

基本原理

主要是在Activity的&onDestroy方法中,手动调用 GC,然后利用ReferenceQueue+WeakReference,来判断是否有释放不掉的引用,然后结合dump memory的hpof文件, 用HaHa分析出泄漏地方。

阅读全文请点击