您的位置:首页 > 移动开发 > Android开发

Android内存泄露分析

2016-09-18 16:02 155 查看
本文参照自Android 内存泄漏总结,做了一些整理,后面对相关内容会继续完善。

常常提到Android内存泄露和内存溢出(OOM)等问题,内存泄漏大家都不陌生了,简单粗俗的讲,就是该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用导致 GC 不能回收,Android的davlik虚拟机和JVM类似,谈到这些问题,就有必要了解一下JVM的内存分配和GC机制。

篇幅有些长,大家可以分几节来看!

Java 内存分配策略



程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  经常有人把Java内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。其中所指的“堆”在后面会专门讲述,而所的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中的局部变量表部分。

  局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型),它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的引用或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError OutOfMemoryError异常。

Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

  Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap,幸好国内没翻译成“垃圾堆”)。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。如果从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。在本章中,我们仅仅针对内存区域的作用进行讨论,Java堆中的上述各个区域的分配和回收等细节将会是下一章的主题。

  根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9等)来说是不存在永久代的概念的。即使是HotSpot虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory来实现方法区的规划了。

Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun公司的BUG列表中,曾出现过的若干个严重的BUG就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。 根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。 Java虚拟机对Class文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。 既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

以上参照《Java虚拟机规范第二版》,对于详细的JVM内容,感兴趣的小伙伴可以去阅读《Java虚拟机规范第二版》。 

一般的,Java 程序运行时的内存分配我们可以粗略的分成三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。

静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

栈区 :通俗来讲,栈主要是用来执行程序的,所有的方法调用都是通过栈来进行的,所有的局部变量,形式参数都是从栈中分配内存空间的。Stack Pointer会自动指引你到放东西的位置,你所要做的只是把东西放下来就行。退出函数的时候,这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

堆区 : 又称动态内存分配,主要是来存放对象的,通常就是指在程序运行时直接 new 出来的内存。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。

栈与堆的区别:

在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。在堆中分配的内存,将由 Java 垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

举个例子:

public class Sample() {
int s1 = 0;
Sample mSample1 = new Sample();

public void method() {
int s2 = 1;
Sample mSample2 = new Sample();
}
}

Sample mSample3 = new Sample();


Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。

mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。

结论:

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。

成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。

了解了 Java 的内存分配之后,我们再来看看 Java 是怎么管理内存的。

GC机制和内存泄漏

Java的内存管理就是对象的分配和释放问题。在 Java 中,程序员需要通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。另外,对象的释放是由 GC 决定和执行的。在 Java 中,内存的分配是由程序完成的,而内存的释放是由 GC 完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,它也加重了JVM的工作。这也是 Java 程序运行速度较慢的原因之一。因为,GC 为了能够正确释放对象,GC 必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC 都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。



GC会选择一些它了解还存活的对象作为内存遍历的根节点(GC Roots),比方说thread stack中的变量,JNI中的全局变量,zygote中的对象(class loader加载)等,然后开始对heap进行遍历。到最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉。



Java内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到gc roots导致无法被GC回收。无用的对象占据着内存空间,使得实际可使用内存变小,形象地说法就是内存泄漏了。

简单说,如果一个对象,从它的根节点开始不可达的话,那么这个对象就是没有引用的了,是会被垃圾收集器回收的,其中,所谓的 “根节点” 往往是一个线程,比如主线程。因此,如果一个对象从它的根节点开始是可达的有引用的,但实际上它已经没有再使用了,是无用的,这样的对象就是内存泄漏的对象,它会在内存中占据我们应用程序原本就不是很多的内存,导致程序变慢,甚至内存溢出(OOM)程序崩溃。

Android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。Android为不同类型的进程分配了不同的内存使用上限,如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被kill掉,这使得仅仅自己的进程被kill掉,而不会影响其他进程.

在Java中,内存泄漏就是存在一些被分配的对象,这些对象具有下面两个特点:1、这些对象是可达的,即在有向图中,存在通路可以与其相连;2、这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。

通过分析,我们得知,对于C++,程序员需要自己管理边和顶点,而对于Java程序员只需要管理边就可以了(不需要管理顶点的释放)。通过这种方式,Java提高了编程的效率。



因此,通过以上分析,我们知道在Java中也有内存泄漏,但范围比C++要小一些。因为Java从语言上保证,任何对象都是可达的,所有的不可达对象都由GC管理。

对于程序员来说,GC基本是透明的,不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数
System.gc()
,但是根据Java语言规范定义, 该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。

同样给出一个 Java 内存泄漏的典型例子,

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}


在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个 Vector 中,如果我们仅仅释放引用本身,那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。

Android中常见的内存泄漏汇总

集合类泄漏

集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。比如上面的典型例子就是其中一种情况,当然实际上我们在项目中肯定不会写这么 2B 的代码,但稍不注意还是很容易出现这种情况,比如我们都喜欢通过 HashMap 做一些缓存之类的事,这种情况就要多留一些心眼。

单例造成的内存泄漏

由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。比如下面一个典型的例子,

public class AppManager {
private static AppManager instance;
private Context context;

private AppManager(Context context) {
this.context = context;
}

public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
}


这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个
Context
,所以这个
Context
的生命周期的长短至关重要:

1、如果此时传入的是
Application
Context
,因为
Application
的生命周期就是整个应用的生命周期,所以这将没有任何问题。

2、如果此时传入的是
Activity
Context
,当这个
Context
所对应的
Activity
退出时,由于该
Context
的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前
Activity
退出时它的内存并不会被回收,这就造成泄漏了。

正确的方式应该改为下面这种方式:

public class AppManager {
private static AppManager instance;
private Context context;

private AppManager(Context context) {
this.context = context.getApplicationContext();// 使用Application 的context
}

public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
}


或者这样写,连 Context 都不用传进来了:

在你的
Application
中添加一个静态方法,
getContext()
返回
Application
context


...

context = getApplicationContext();

...
/**
* 获取全局的context
* @return 返回全局context对象
*/
public static Context getContext(){
return context;
}

public class AppManager {
private static AppManager instance;
private Context context;

private AppManager() {
this.context = MyApplication.getContext();// 使用Application 的context
}

public static AppManager getInstance() {
if (instance == null) {
instance = new AppManager();
}
return instance;
}
}


匿名内部类/非静态内部类和异步线程

非静态内部类创建静态实例造成的内存泄漏

有的时候我们可能会在启动频繁的
Activity
中,为了避免重复创建相同的数据资源,可能会出现这种写法:

public class MainActivity extends AppCompatActivity {

private static TestResource mResource = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mManager == null){
mManager = new TestResource();
}
//...
}

class TestResource {//...
}


}

这样就在
Activity
内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。正确的做法为:

将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用
Context
,请按照上面推荐的使用
Application
Context
。当然,
Application
context
不是万能的,所以也不能随便乱用,对于有些地方则必须使用
Activity
Context
,对于
Application
Service
Activity
三者的
Context
的应用场景如下:



其中: NO1表示
Application
Service
可以启动一个
Activity
,不过需要创建一个新的 task 任务队列。而对于
Dialog
而言,只有在
Activity
中才能创建

匿名内部类

android
开发经常会继承实现
Activity/Fragment/View
,此时如果你使用了匿名类,并被异步线程持有了,那要小心了,如果没有任何措施这样一定会导致泄露

public class MainActivity extends Activity {
...
Runnable ref1 = new MyRunable();
Runnable ref2 = new Runnable() {
@Override
public void run() {

}
};
...
}


ref1和ref2的区别是,ref2使用了匿名内部类。我们来看看运行时这两个引用的内存:

可以看到,ref1没什么特别的。

但ref2这个匿名类的实现对象里面多了一个引用:

this$0这个引用指向
MainActivity.this
,也就是说当前的
MainActivity
实例会被ref2持有,如果将这个引用再传入一个异步线程,此线程和此Acitivity生命周期不一致的时候,就造成了
Activity
的泄露。

Handler 造成的内存泄漏

Handler
的使用造成的内存泄漏问题应该说是最为常见了,很多时候我们为了避免 ANR 而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回调等api都借助Handler来处理,但 Handler 不是万能的,对于
andler
的使用代码编写一不规范即有可能造成内存泄漏。另外,我们知道
Handler
Message
MessageQueue
都是相互关联在一起的,万一
Handler
发送的
Message
尚未被处理,则该 Message 及发送它的 Handler 对象将被线程
MessageQueue
一直持有。

由于
Handler
属于 TLS(Thread Local Storage) 变量, 生命周期和
Activity
是不一致的。因此这种实现方式一般很难保证跟
View
或者
Activity
的生命周期保持一致,故很容易导致无法正确释放。

举个例子:

public class SampleActivity extends Activity {

private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}

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

// Post a message and delay its execution for 10 minutes.
mLeakyHandler.postDelayed(new Runnable() {
@Override
public void run() { /* ... */ }
}, 1000 * 60 * 10);

// Go back to the previous Activity.
finish();
}
}


在该
SampleActivity
中声明了一个延迟10分钟执行的消息
Message
mLeakyHandler
将其
push
进了消息队列
MessageQueue
里。当该
Activity
finish()
掉时,延迟执行任务的
Message
还会继续存在于主线程中,它持有该
Activity
Handler
引用,所以此时
finish()
掉的
Activity
就不会被回收了从而造成内存泄漏(因
Handler
为非静态内部类,它会持有外部类的引用,在这里就是指
SampleActivity
)。

修复方法:在
Activity
中避免使用非静态内部类,比如上面我们将
Handler
声明为静态的,则其存活期跟
Activity
的生命周期就无关了。同时通过弱引用的方式引入
Activity
,避免直接将
Activity
作为
context
传进去,见下面代码:

public class SampleActivity extends Activity {

/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;

public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}

@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}

private final MyHandler mHandler = new MyHandler(this);

/**
* Instances of anonymous classes do not hold an implicit
* reference to their outer class when they are "static".
*/
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() { /* ... */ }
};

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

// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

// Go back to the previous Activity.
finish();
}
}


综述,即推荐使用静态内部类 +
WeakReference
这种方式。每次使用前注意判空。

前面提到了
WeakReference
,所以这里就简单的说一下
Java
对象的几种引用类型。

Java
对引用的分类有
Strong reference
,
SoftReference
,
WeakReference
,
PhatomReference
四种。

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

软/弱引用可以和一个引用队列(
ReferenceQueue
)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。

假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生
OutOfMemory
异常。这时,我们可以考虑使用软/弱引用技术来避免这个问题发生。以下就是高速缓冲器的雏形:

首先定义一个
ashMap
,保存软引用对象。

private Map <String, SoftReference<Bitmap>> imageCache = new HashMap <String, SoftReference<Bitmap>> ();


再来定义一个方法,保存Bitmap的软引用到
HashMap


使用软引用以后,在
OutOfMemory
异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。

如果只是想避免
OutOfMemory
异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。

另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。

ok,继续回到主题。前面所说的,创建一个静态Handler内部类,然后对
Handler
持有的对象使用弱引用,这样在回收时也可以回收
Handler
持有的对象,但是这样做虽然避免了
Activity
泄漏,不过
Looper
线程的消息队列中还是可能会有待处理的消息,所以我们在
Activity
Destroy
时或者
Stop
时应该移除消息队列
MessageQueue
中的消息。

下面几个方法都可以移除
Message


public final void removeCallbacks(Runnable r);

public final void removeCallbacks(Runnable r, Object token);

public final void removeCallbacksAndMessages(Object token);

public final void removeMessages(int what);

public final void removeMessages(int what, Object object);


尽量避免使用
static
成员变量

如果成员变量被声明为
static
,那我们都知道其生命周期将与整个app进程生命周期一样。

这会导致一系列问题,如果你的app进程设计上是长驻内存的,那即使app切到后台,这部分内存也不会被释放。按照现在手机app内存管理机制,占内存较大的后台进程将优先回收,因为如果此app做过进程互保保活,那会造成app在后台频繁重启。当手机安装了你参与开发的app以后一夜时间手机被消耗空了电量、流量,你的app不得不被用户卸载或者静默。

这里修复的方法是:

不要在类初始时初始化静态成员。可以考虑lazy初始化。

架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来。

避免
override finalize()


1、
finalize
方法被执行的时间不确定,不能依赖与它来释放紧缺的资源。时间不确定的原因是:

虚拟机调用GC的时间不确定

Finalize daemon
线程被调度到的时间不确定

详情见这里 深入分析过dalvik的代码

2、含有
Finalize
方法的
object
需要至少经过两轮GC才有可能被释放。

含有
finalize
方法的
object
是在 new 的时候由虚拟机生成了一个
finalize reference
在来引用到该Object的,而在
finalize
方法执行的时候,该
object
所对应的
finalize Reference
会被释放掉,即使在这个时候把该
object
复活(即用强引用引用住该 object ),再第二次被 GC 的时候由于没有了
finalize ·reference
与之对应,所以
finalize
方法不会再执行。

3、
finalize·
方法只会被执行一次,即使对象被复活,如果已经执行过了
finalize
方法,再次被 GC 时也不会再执行了,原因是:

资源未关闭造成的内存泄漏

对于使用了
BraodcastReceiver
ContentObserver
File
,游标
Cursor
Stream
Bitmap
等资源的使用,应该在
Activity
销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

一些不良代码造成的内存压力

有些代码并不造成内存泄露,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。

比如:

Bitmap
没调用
recycle()
方法,对于
Bitmap
对象在不使用时,我们应该先调用 recycle() 释放内存,然后才它设置为 null. 因为加载 Bitmap 对象的内存空间,一部分是 java 的,一部分 C 的(因为 Bitmap 分配的底层是通过 JNI 调用的 )。 而这个 recyle() 就是针对 C 部分的内存释放。

构造 Adapter 时,没有使用缓存的 convertView ,每次都在创建新的 converView。这里推荐使用 ViewHolder。

工具分析

Java 内存泄漏的分析工具有很多,但众所周知的要数 MAT(Memory Analysis Tools) 和 YourKit 了。由于篇幅问题,我这里就只对 MAT 的使用做一下介绍。–> MAT 的安装

MAT分析heap的总内存占用大小来初步判断是否存在泄露

打开 DDMS 工具,在左边 Devices 视图页面选中“Update Heap”图标,然后在右边切换到 Heap 视图,点击 Heap 视图中的“Cause GC”按钮,到此为止需检测的进程就可以被监视。

Heap视图中部有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:

进入某应用,不断的操作该应用,同时注意观察data object的Total Size值,正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况。

所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平;反之如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落。随着操作次数的增多Total Size的值会越来越大,直到到达一个上限后导致进程被杀掉。

MAT分析hprof来定位内存泄露的原因所在

这是出现内存泄露后使用MAT进行问题定位的有效手段。

A)Dump出内存泄露当时的内存镜像hprof,分析怀疑泄露的类:

B)分析持有此类对象引用的外部对象

C)分析这些持有引用的对象的GC路径

D)逐个分析每个对象的GC路径是否正常

从这个路径可以看出是一个
antiRadiationUtil
工具类对象持有了
MainActivity
的引用导致
MainActivity
无法释放。此时就要进入代码分析此时
antiRadiationUtil
的引用持有是否合理(如果
antiRadiationUtil
持有了
MainActivity
context
导致节目退出后
MainActivity
无法销毁,那一般都属于内存泄露了)。

MAT对比操作前后的hprof来定位内存泄露的根因所在

为查找内存泄漏,通常需要两个 Dump结果作对比,打开 Navigator History面板,将两个表的 Histogram结果都添加到 Compare Basket中去

A) 第一个HPROF 文件(usingFile > Open Heap Dump ).

B)打开Histogram view.

C)在NavigationHistory view里 (如果看不到就从Window >show view>MAT- Navigation History ), 右击histogram然后选择Add to Compare Basket .

D)打开第二个HPROF 文件然后重做步骤2和3.

E)切换到Compare Basket view, 然后点击Compare the Results (视图右上角的红色”!”图标)。

F)分析对比结果

可以看出两个hprof的数据对象对比结果。

通过这种方式可以快速定位到操作前后所持有的对象增量,从而进一步定位出当前操作导致内存泄露的具体原因是泄露了什么数据对象。

注意:

如果是用 MAT Eclipse 插件获取的 Dump文件,不需要经过转换则可在MAT中打开,Adt会自动进行转换。

而手机SDk Dump 出的文件要经过转换才能被 MAT识别,Android SDK提供了这个工具 hprof-conv (位于 sdk/tools下)

首先,要通过控制台进入到你的 android sdk tools 目录下执行以下命令:

./hprof-conv xxx-a.hprof xxx-b.hprof

例如 hprof-conv input.hprof out.hprof

此时才能将out.hprof放在eclipse的MAT中打开。

Ok,下面将给大家介绍一个屌炸天的工具 – LeakCanary 。

使用 LeakCanary 检测 Android 的内存泄漏

什么是 LeakCanary 呢?为什么选择它来检测 Android 的内存泄漏呢?

别急,让我来慢慢告诉大家!

LeakCanary 是国外一位大神 Pierre-Yves Ricau 开发的一个用于检测内存泄露的开源类库。一般情况下,在对战内存泄露中,我们都会经过以下几个关键步骤:

1、了解 OutOfMemoryError 情况。

2、重现问题。

3、在发生内存泄露的时候,把内存 Dump 出来。

4、在发生内存泄露的时候,把内存 Dump 出来。

5、计算这个对象到 GC roots 的最短强引用路径。

6、确定引用路径中的哪个引用是不该有的,然后修复问题。

很复杂对吧?

如果有一个类库能在发生 OOM 之前把这些事情全部都搞定,然后你只要修复这些问题就好了。LeakCanary 做的就是这件事情。你可以在 debug 包中轻松检测内存泄露。

一起来看这个例子(摘自 LeakCanary 中文使用说明,下面会附上所有的参考文档链接):

class Cat {
}

class Box {
Cat hiddenCat;
}

class Docker {
// 静态变量,将不会被回收,除非加载 Docker 类的 ClassLoader 被回收。
static Box container;
}

// ...

Box box = new Box();

// 薛定谔之猫
Cat schrodingerCat = new Cat();
box.hiddenCat = schrodingerCat;
Docker.container = box;


创建一个
RefWatcher
,监控对象引用情况。

// 我们期待薛定谔之猫很快就会消失(或者不消失),我们监控一下

refWatcher.watch(schrodingerCat);


当发现有内存泄露的时候,你会看到一个很漂亮的 leak trace 报告:

GC ROOT static Docker.container
references Box.hiddenCat
leaks Cat instance


我们知道,你很忙,每天都有一大堆需求。所以我们把这个事情弄得很简单,你只需要添加一行代码就行了。然后
LeakCanary
就会自动侦测
activity
的内存泄露了。

public class ExampleApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
LeakCanary.install(this);
}
}


然后你会在通知栏看到这样很漂亮的一个界面:

以很直白的方式将内存泄露展现在我们的面前。

Demo

一个非常简单的 LeakCanary demo: 一个非常简单的 LeakCanary demo: https://github.com/liaohuqiu/leakcanary-demo

接入

在 build.gradle 中加入引用,不同的编译使用不同的引用:

dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}


如何使用

使用 RefWatcher 监控那些本该被回收的对象。

RefWatcher refWatcher = {...};

// 监控
refWatcher.watch(schrodingerCat);


LeakCanary.install()
会返回一个预定义的
RefWatcher
,同时也会启用一个
ActivityRefWatcher
,用于自动监控调用
Activity.onDestroy()
之后泄露的
activity


在Application中进行配置 :

public class ExampleApplication extends Application {

public static RefWatcher getRefWatcher(Context context) {
ExampleApplication application = (ExampleApplication) context.getApplicationContext();
return application.refWatcher;
}

private RefWatcher refWatcher;

@Override
public void onCreate() {
super.onCreate();
refWatcher = LeakCanary.install(this);
}
}

使用 RefWatcher 监控 Fragment:
public abstract class BaseFragment extends Fragment {

@Override
public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
refWatcher.watch(this);
}
}

使用 RefWatcher 监控 Activity:
public class MainActivity extends AppCompatActivity {

......
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//在自己的应用初始Activity中加入如下两行代码
RefWatcher refWatcher = ExampleApplication.getRefWatcher(this);
refWatcher.watch(this);

textView = (TextView) findViewById(R.id.tv);
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startAsyncTask();
}
});

}

private void async() {

startAsyncTask();
}

private void startAsyncTask() {
// This async task is an anonymous class and therefore has a hidden reference to the outer
// class MainActivity. If the activity gets destroyed before the task finishes (e.g. rotation),
// the activity instance will leak.
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
// Do some slow work in background
SystemClock.sleep(20000);
return null;
}
}.execute();
}
}


工作机制

1.
RefWatcher.watch()
创建一个
KeyedWeakReference
到要被监控的对象。

2.然后在后台线程检查引用是否被清除,如果没有,调用GC。

3.如果引用还是未被清除,把
heap
内存
dump
APP
对应的文件系统中的一个 .hprof 文件中。

4.在另外一个进程中的
HeapAnalyzerService
有一个
HeapAnalyzer
使用
HAHA
解析这个文件。

5.得益于唯一的
reference key
,
HeapAnalyzer
找到
KeyedWeakReference
,定位内存泄露。

6.
HeapAnalyzer
计算 到
GC roots
的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。

7.引用链传递到 APP 进程中的
DisplayLeakService
, 并以通知的形式展示出来。

ok,这里就不再深入了,想要了解更多就到 作者
github
主页 这去哈。

总结

Activity
等组件的引用应该控制在
Activity
的生命周期之内; 如果不能就考虑使用
getApplicationContext
或者
getApplication
,以避免
Activity
被外部长生命周期的对象引用而泄露。

尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括
context
),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量。

对于生命周期比
Activity
长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:

将内部类改为静态内部类

静态内部类中使用弱引用来引用外部类的成员变量
Handler
的持有的引用对象最好使用弱引用,资源释放时也可以清空
Handler
里面的消息。比如在
Activity onStop
或者
onDestroy
的时候,取消掉该
Handler
对象的
Message
Runnable
.

在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完
Bitmap
后先调用
recycle()
,再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用
array.clear()
;
array = null
)等,最好遵循谁创建谁释放的原则。

正确关闭资源,对于使用了
BraodcastReceiver
ContentObserver
File
,游标
Cursor
Stream
Bitmap
等资源的使用,应该在
Activity
销毁时及时关闭或者注销。

保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息