Android 垃圾回收机制★★★

1.垃圾回收机制

垃圾回收,也叫GC(Garbage Collection),指的是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

我们知道JVM的内存区域主要分为程序计数器、虚拟机栈、本地方法栈、方法区、堆。那么哪个才是GC作用的区域呢?答案是堆区,前面几块数据区域都不进行 GC。对象实例和数组都是在堆上分配的,GC 也主要对这两类数据进行回收。

一般来说,程序使用内存的方式遵循先向操作系统申请一块内存、使用内存、使用完毕之后释放内存归还给操作系统。在传统的C/C++等要求显式释放内存的编程语言中,记得在合适的时候释放内存。而Java等编程语言都提供了基于垃圾回收算法的内存管理机制,我们不需要手动释放对象的内存,JVM 中的垃圾回收器(Garbage Collector)会自动回收。

Android如今使用的虚拟机名叫Android Runtime,简称Art,而Art的其中一大职责就是负责垃圾回收。Art会在适当的时机触发GC操作,一旦进行GC操作,就会将一些不再使用的对象进行回收。

2.如何判定垃圾

目前主要有两种判定算法:引用计数算法和可达性分析算法。Art采用的是第二种算法。

①引用计数算法

引用计数算法通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加1,如果删除对该对象的引用,那么它的引用计数就减1,当该对象的引用计数为0时,该对象就会被回收。

需要说明的是,引用有四种类型分别是强引用、软引用、弱引用和虚引用。引用的类型会影响到垃圾的回收。

(1)强引用:通过new来创建一个新对象时返回的引用就是一个强引用,若一个对象通过一系列强引用可到达,它就是强可达的(strongly reachable),那么它就不可能被系统垃圾回收机制回收。

(2)软引用:垃圾回收机制运行时,系统内存空间足够不会被回收,不足够会被回收。软引用和弱引用的区别在于,若一个对象是弱引用可达,无论当前内存是否充足它都会被回收,而软引用可达的对象在内存不充足时才会被回收,因此软引用要比弱引用“强”一些;

(3)弱引用:垃圾回收机制运行时,不管系统内存是否足够,都会被回收。

(4)虚引用:几乎等于没有引用,以至于我们通过虚引用甚至无法获取到被引用的对象,虚引用存在的唯一作用就是当它指向的对象被回收后,虚引用本身会被加入到引用队列中,用作记录它指向的对象已被回收。

下面通过实例来演示和说明:

String obj = new String("Android");

该段代码先创建一个字符串Android,其内存分在堆中,并且这个时候"Android"有一个引用,就是obj,其指向字符串Android。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

如果此时将obj设置为null,这时候“Android”字符串的引用次数就为0了,在引用计数垃圾回收中,意味着此时就要进行垃圾回收了。

obj = null;

此时演示的示意图如下所示,即将进行垃圾回收。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

引用计数算法有一个致命问题就是不能解决循环引用问题。

②可达性分析算法

可达性算法的原理是以一系列叫做 GC Root 的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点(这样通过 GC Root 串成的一条线就叫引用链),直到所有的结点都遍历完毕。如果相关对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为垃圾,会被 GC 回收。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 如上图所示,用可达性算法可以解决Java对象循环引用导致的引用计数法无法回收的问题。因为从GC Root出发没有到达obj5和obj6的有效路径,所以obj5和obj6可以回收。

obj5和obj6对象可被回收,就一定会被GC回收吗?并不是,对象从判定可回收到回收需要经历下面两个阶段:

第一个阶段是可达性分析,分析该对象是否可达。

第二个阶段是当对象没有重写finalize()方法或者finalize()方法已经被调用过,虚拟机认为该对象不可以被救活,因此回收该对象。(finalize()方法在垃圾回收中的作用是,给该对象一次救活的机会)。当发生GC时,会先判断对象是否执行了 finalize 方法,如果未执行,则会先执行 finalize 方法,我们可以在此方法里将当前对象与 GC Roots 关联,这样执行 finalize 方法之后,GC 会再次判断对象是否可达,如果不可达,则会被回收,如果可达,则不回收!

注意: finalize 方法只会被执行一次,如果第一次执行 finalize 方法此对象变成了可达确实不会回收,但如果对象再次被 GC,则会忽略 finalize 方法,对象会被回收!这一点切记!

有了上面垃圾对象的判定,还要考虑一个问题,就是stop the world,因为垃圾回收的时候,需要整个的引用状态保持不变,否则判定是垃圾。所以,GC的时候,其他所有的程序执行都要处于暂停状态,卡住了。幸运的是,这个卡顿是非常短的,对程序的影响也是微乎其微,所以GC的卡顿问题由此而来,也是无可避免的。

3.GC Root对象

通过可达性算法,成功解决了引用计数所无法解决的问题-“循环依赖”,只要无法与 GC Root 建立直接或间接的连接,系统就会判定为可回收对象。这样就引申出了另一个问题,哪些属于 GC Root?

在 Java 语言中,可作为 GC Root 的对象包括以下4种:

①虚拟机栈(栈帧中的局部变量表)中引用的对象

②方法区中类静态属性引用的对象

③方法区中常量引用的对象

④本地方法栈中 JNI(即一般说的 Native 方法)引用的对象

注意:全局变量同静态变量不同,它不会被当作 GC Root。

下面分别介绍这4种GC Root:

①虚拟机栈(帧栈中局部变量表)中引用的对象

public class GCRootStackLocaltable {

    public GCRootStackLocaltable(String name){   

    }    

    public static void main(String[] args){

        GCRootStackLocaltable obj = new GCRootStackLocaltable("Localtable");

        obj = null;

    }  

}

上面实例中的obj即为GC Root,当obj置为null时,Localtable对象也断掉了与 GC Root 的引用链,该对象将被回收。

②方法区中类静态属性引用的对象

public class GCRootMethodAreaStaticPro {

    public static GCRootMethodAreaStaticPro instance; 

    public GCRootMethodAreaStaticPro(String name) {      

    }

    @SuppressWarnings("static-access")

    public static void main(String[] args){

        GCRootMethodAreaStaticPro obj = new GCRootMethodAreaStaticPro("Localtable");

        obj.instance = new GCRootMethodAreaStaticPro("staticProperty");

        obj = null;

    } 

}

此时上面实例中的obj为GC Root,当obj置为null后,经过GC垃圾回收,obj所指向的Localtable对象由于无法与 GC Root 建立关系被回收。而instance作为类的静态属性,也属于GC Root,staticProperty对象依然与 GC Root 建立着连接,所以此时 staticProperty 对象并不会被回收。注意这里的GC Root是谁。

③方法区中常量引用的对象

public class GCRootMethodAreaConstat {

    public static final GCRootMethodAreaConstat mFinalObj = new GCRootMethodAreaConstat("objFinal");                 

    public GCRootMethodAreaConstat(String name){      

    }  

    public static void main(String[] args){

        GCRootMethodAreaConstat obj = new GCRootMethodAreaConstat("Localtable");

        obj = null;

    }

}

此时实例中的mFinalObj为方法区中的常量的引用,作为GC Root使用。此时的obj也为GC Root(虚拟机栈局部变量表),当obj置为null后,Localtable与GC Root断开将会被回收,但是objFinal不会被回收。

④本地方法栈中引用的对象

所谓本地方法就是一个 java 调用非 java 代码的接口,该方法并非 Java 实现的,可能由 C 或 Python等其他语言实现的, Java 通过 JNI 来调用本地方法, 而本地方法是以库文件的形式存放的(在 WINDOWS 平台上是 DLL 文件形式,在 UNIX 机器上是 SO 文件形式)。通过调用本地的库文件的内部方法,使 JAVA 可以实现和本地机器的紧密联系,调用系统级的各接口方法。

任何 Native 接口都会使用某种本地方法栈,实现的本地方法接口是使用 C 连接模型的话,那么它的本地方法栈就是 C 栈。当线程调用 Java 方法时,虚拟机会创建一个新的栈帧并压入 Java 栈。然而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不再在线程的 Java 栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_19,color_FFFFFF,t_70,g_se,x_16

 JNIEXPORT void JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,jstring jmsg) {

...

   // 缓存String的class

   jclass jc = (*env)->FindClass(env, STRING_PATH);

}

如上代码所示,当 java 调用以上本地方法时,jc 会被本地方法栈压入栈中, jc 就是我们说的本地方法栈中 JNI 的对象引用,因此只会在此本地方法执行完成后才会被释放。

4.触发GC操作的原因

①GC_CONCURRENT: 当应用程序的堆内存快要满的时候,系统会自动触发GC操作来释放内存。

②GC_FOR_MALLOC: 当应用程序需要分配更多内存,可是现有内存已经不足的时候,系统会进行GC操作来释放内存。

③GC_HPROF_DUMP_HEAP: 当生成HPROF文件的时候,系统会进行GC操作。

④GC_EXPLICIT: 主动通知系统去进行GC操作,比如调用System.gc()方法来通知系统。或者在DDMS中,通过工具按钮也是可以显式地告诉系统进行GC操作的。(不要大量使用)

当应用程序空闲时,即没有应用线程在运行时,GC会被调用,Java垃圾回收线程就是一个典型的守护线程, 当我们的程序中不再有任何运行中的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动离开。 它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

只有程序需要更多额外内存或应用程序空闲时,垃圾回收机制才会进行垃圾回收;只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源(堆内存和方法区)。

程序无法精确控制垃圾回收的运行(但我们可以通知系统进行垃圾回收——System.gc(),但系统是否进行垃圾回收仍然不确定),只负责回收堆内存的对象,回收任何对象之前会调用它的finalize()方法。

对象的三种状态:

①可达状态:有一个以上的变量引用一个对象

②可恢复状态:不再有任何变量引用它,垃圾回收时系统会调用所有可恢复状态的对象的finalize()方法进行资源清理,如果重新有引用变量引用该对象会变为可达状态,否则进入不可达状态。

③不可达状态:没有变量引用,且finalize()方法也没使该对象变成可达状态,永久失去引用。

5.垃圾回收算法

常见的垃圾回收算法有引用计数法(Reference Counting)、标注并清理(Mark and Sweep GC)、拷贝(Copying GC)和逐代回收(Generational GC)等算法。Art采用了两种算法,标注并清理和拷贝GC。

①引用计数回收法(Reference Counting GC)

(Android系统不使用该算法)引用计数法的原理很简单,即记录每个对象被引用的次数。每当创建一个新的对象,或者将其它指针指向该对象时,引用计数都会累加一次;而每当将指向对象的指针移除时,引用计数都会递减一次,当引用次数降为0时,删除对象并回收内存。

通常对象的引用计数都会跟对象放在一起,系统在分配完对象的内存后,返回的对象指针会跳过引用计数部分。

b98fa27ca7a7467cb0f80d1844d78ffb.png

然而引用计数回收算法有一个很大的弱点,就是无法有效处理循环引用的问题,比如A和B对象同时互相引用,计数都是1,即使A、B不再被使用,JVM也不会检测到,而且每次对象被引用等操作时还要触发计数,因此额外开销在所难免。

②标记清除算法(Mark and Sweep GC)

(Android系统使用该算法)在这个算法中,程序在运行的过程中不停的创建新的对象并消耗内存,直到内存用光,这时再要创建新对象时,系统暂停其它组件的运行,触发GC线程启动垃圾回收过程。内存回收的原理很简单,就是从"GC Roots"集合开始,将内存整个遍历一次,保留所有可以被GC Roots直接或间接引用到的对象,而剩下的对象都当作垃圾被回收。

过程分两步:

第一步 Mark标记阶段:找到内存中所有GC Root对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。

第二步 Sweep清除阶段:当遍历完所有的GC Root之后,则将标记为垃圾的对象直接清除。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

注意:如果对象引用的层次过深,递归调用消耗完虚拟机内GC线程的栈空间,从而导致栈空间溢出(StackOverflow)异常,为了避免这种情况的发生,在具体实现时,通常是用一个叫做标注栈(Mark Stack)的数据结构来分解递归调用。一开始,标注栈(Mark Stack)的大小是固定的,但在一些极端情况下,如果标注栈的空间也不够的话,则会分配一个新的标注栈(Mark Stack),并将新老栈用链表连接起来。与引用计数法中对象的内存布局类似,对象是否被标注的标志也是保存在对象头里的,如下图:

7c37fcea4ba4440183255bbd9d1df084.png

标记清除算法的优点:实现简单,不需要将对象进行移动,而且很好的处理了引用计数中的循环引用问题,在内存足够的前提下,对程序几乎没有任何额外的性能开支。

缺点:这个算法在执行垃圾回收过程中,可能产生大量的内存碎片,提高了垃圾回收的频率。

③复制算法(Copying)

(Android系统使用该算法)这也是标注法的一个变种, GC内存堆实际上分成乒和乓两部分。一开始,所有的内存分配请求都由乒部分满足,其维护"下个对象分配的起始位置"指针,分配内存仅仅就是操作下这个指针而已,当乒的内存快用完时,采用标注(Mark)算法识别出存活的对象,并将它们拷贝到乓部分,后续的内存分配请求都在乓部分完成,而乓里的内存用完后,再切换回乒部分,使用内存就跟打乒乓球一样。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_17,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_17,color_FFFFFF,t_70,g_se,x_16也就是说,将现有的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

复制算法之前,内存分为 A/B 两块,并且当前只使用内存 A,内存的状况如下图所示:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

标记完之后,所有可达对象都被按次序复制到内存 B 中,并设置 B 为当前使用中的内存。内存状况如下图所示:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

复制算法的优点:内存分配速度快,按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。

缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

Art中标记复制算法的具体实现:

​如果内存中多数对象都是存活的,复制法将会产生大量的内存间复制的开销,而这正是因为该算法只把内存区域分为了两个区域,这就会导致出现复制绝大部分的存活对象只为了清理掉一小部分垃圾的情况,这种做法无异于在家里打扫卫生,为了些许灰尘,把灰尘所在一边的所有家具搬到没有灰尘的另一边后才打扫卫生,这是一种代价极其高昂的清理垃圾方法。因此,针对这种情况,Art采用的是该算法优化后的版本,把内存划分为多个区域(Region),一个区域大小为256KB,如下图所示:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 深绿色标明老年代中的存活对象,浅绿色标明新生代中的存活对象,红色标明待清理的垃圾,此外,老年代和新生代都聚集在各自的区域,并没有出现老年代和新生代混合在一个区域的情况。

这种做法显而易见的好处如下:

​ (1)当一个区域没有垃圾的时候,就可以不进行垃圾清理。

​ (2)当一个区域因为只有一两个垃圾而要进行垃圾清理的时候,代价也不会太过于高昂,因为一个区域大小才256KB,本来存储的对象就不多,因为一两个垃圾而复制三四个对象还是可以接受的,这就和在家里打扫卫生时因为扫把够不着椅子底下的灰尘,从而把椅子移开后才进行清理一样可以令人接受。

区域命名规则:

​ Evacuated:疏散;撤离;排泄;腾出(房子等)

(1)当一个区域有垃圾,需要被Evacuated的时候,Art则将该区域命名为Evacuated Region。

(2)当一个区域没有垃圾,不需要被Evacuated的时候,Art则将该区域命名为Unevacuated Region。

​(3)当一个区域没有存储对象的时候,Art则将该区域命名为Unused Region。

(4)当一个区域原先为Unused Region,但是要作为其它Evacuated Region中存活对象复制目的地的时候,Art则将该区域命名为Evacuation Region。(存活对象即那些没有被Art判定为垃圾的对象)

​举个例子,假设有两个区域,存储了对象的区域1和没有存储对象的区域2,Art在使用可达性分析算法后,发现区域1有垃圾,将区域1命名为Evacuated Region,但区域1里面还有存活对象,由于区域2没有存储对象,Art决定将这些存活对象要复制到区域2,那么此时区域2就会被Art命名为Evacuation Region。

Art垃圾回收算法的并发性:

​垃圾回收算法具有并发性,也就是说垃圾回收线程是与主线程并发进行的,在一个垃圾回收周期只有一次短暂的GC暂停,时间为几毫秒,所以用户大多数情况下是无法感知的,并不会出现”stop the world“现象。

④标记-压缩(整理)算法 (Mark-Compact)

这个是前面标注清理法的一个变种,系统在长时间运行的过程中,反复分配和释放内存很有可能会导致内存堆里的碎片过多,从而影响分配效率,因此有些采用此算法的实现(Android系统中并没有采用这个做法),在清理(SWEEP)过程中,还会执行内存中移动存活的对象,使其排列的更紧凑。在这种算法中,需要先从根节点开始对所有可达对象做一次标记,之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。最后,清理边界外所有的空间。因此标记压缩也分两步完成:

第一步 Mark标记阶段:找到内存中所有GC Root对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。

第二步 Compact 压缩阶段:将剩余存活对象按顺序压缩到内存的某一端。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

举例说明一下该算法:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 上图中可以被GC Root访问到的对象有A、C、D、E、F、H六个对象,为了避免内存碎片问题并满足快速分配对象的要求,GC线程移动这六个对象,使内存使用更为紧凑,如下图:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 由于GC线程移动了存活下来对象的内存位置,其必须更新其他线程中对这些对象的引用,由于A引用了E,移动之后,就必须更新这个引用,在更新过程中,必须中断正在使用A的线程,防止其访问到错误的内存位置而导致无法预料的错误。

标记压缩算法的优点:这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比较高。

缺点:所谓压缩操作,仍需要进行局部对象移动,所以一定程度上还是降低了效率。

3.JVM内存模型和分代回收策略

堆是JVM管理的内存中最大的一块。Java 虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代,这就是 JVM 的内存分代策略。

①新生代

新生成的对象优先存放在新生代中,新生代对象存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收 70%~95% 的空间,回收效率很高。新生代中一般采用的 GC 回收算法是复制算法。 新生代又可以继续细分为 3 部分:Eden、Survivor0(简称 S0)、Survivor1(简称S1)。这 3 部分按照 8:1:1 的比例来划分新生代。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

绝大多数刚刚被创建的对象会存放在 Eden 区。

当 Eden 区第一次满的时候,会进行垃圾回收(Minor GC)。首先将Eden区的垃圾对象回收清除,并将存活的对象复制到S0,此时S1是空的。

下一次 Eden 区满时,再执行一次垃圾回收。此次会将 Eden 和 S0 区中所有垃圾对象清除,并将存活对象复制到 S1,此时 S0 变为空。

如此反复在 S0 和 S1 之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。

注意:设置两个Survivor区最大的好处就是解决内存碎片化。

②老年代

一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。我们可以使用 -XX:PretenureSizeThreshold 来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。

老年代因为对象的生命周期较长,一般采用标记压缩的回收算法。等到"老一代对象池"也快要被填满时,虚拟机此时再在"老一代对象池"中执行垃圾回收过程释放内存(Major GC)。在逐代GC算法中,由于"年轻一代对象池"中的回收过程很快 – 只有很少的对象会存活,而执行时间较长的"老一代对象池"中的垃圾回收过程执行不频繁,实现了很好的平衡,因此大部分虚拟机,如JVM、.NET的CLR都采用这种算法。

注意:对于老年代可能存在这么一种情况,老年代中的对象有时候会引用到新生代对象。这时如果要执行新生代 GC,则可能需要查询整个老年代上可能存在引用新生代的情况,这显然是低效的。所以,老年代中维护了一个 512 byte 的 card table,所有老年代对象引用新生代对象的信息都记录在这里。每当新生代发生 GC 时,只需要检查这个 card table 即可,大大提高了性能。

举例说明:

由于每次GC都是在单独的对象池中执行的,当GC Root之一R3被释放后,在"年轻一代对象池"中执行GC过程时,R3所引用的对象f、g、h、i和j都会被当做垃圾回收掉,这样就导致"老一代对象池"中的对象c有一个无效引用。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 为了避免这种情况,在"年轻一代对象池"中执行GC过程时,也需要将对象C当做GC Root之一。一个名为"Card Table"的数据结构就是专门设计用来处理这种情况的,"Card Table"是一个位数组,每一个位都表示"老一代对象池"内存中一块4KB的区域(之所以取4KB,是因为大部分计算机系统中,内存页大小就是4KB)。当用户代码执行一个引用赋值时,虚拟机不会直接修改内存,而是先将被赋值的内存地址与"老一代对象池"的地址空间做一次比较,如果要修改的内存地址是"老一代对象池"中的地址,虚拟机会修改"Card Table"对应的位为 1,表示其对应的内存页已经修改过 - 不干净(dirty)了,如下图:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_10,color_FFFFFF,t_70,g_se,x_16

当需要在 "年轻一代对象池"中执行GC时, GC线程先查看"Card Table"中的位,找到不干净的内存页,将该内存页中的所有对象都加入GC Root。虽然初看起来,有点浪费, 但是据统计,通常从老一代的对象引用新一代对象的几率不超过1%,因此"Card Table"的算法是一小部分的时间损失换取空间。

对象进入老年代的四种情况:

①对象经过几次垃圾回收,熬到设定的年龄阈值(默认为15),就会晋升到老年代。

②大对象直接进入老年代(超过了JVM中-XX:PretenureSizeThreshold参数的设置)

所以在写程序的时候要尽量避免大对象,更要尽量避免朝生夕死的大对象,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来安置他们。

③Survivor区中如果有相同年龄的对象所占空间大于幸存者区的一半,那么年龄大于等于该年龄的对象就可以直接进入老年代。(动态对象年龄判定)

在一次新生代GC后,Survivor区域中的几个年龄对象加起来超过了Survivor区内存的一半,那么根据动态年龄判定规则,从最小的年龄加起,比如年龄1+年龄2+年龄3的对象大小总和,超过了Survivor区内存的一半,此时年龄3以上的对象就会晋升老年代。

④新生代GC后,存活下来的对象太多,Survivor区放不下,此时对象直接晋升老年代。(空间分配担保)

下面来看看垃圾回收时这些空间是如何进行交互的:

①首先,所有新生成的对象都是放在年轻代的Eden分区的,初始状态下两个Survivor分区都是空的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_17,color_FFFFFF,t_70,g_se,x_16

 ②当Eden区满的的时候,小垃圾收集Minor GC就会被触发。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_16,color_FFFFFF,t_70,g_se,x_16

 ③当Eden分区进行清理的时候,会把引用对象移动到第一个Survivor分区,无引用的对象删除。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_19,color_FFFFFF,t_70,g_se,x_16

 ④在下一个小垃圾收集的时候,在Eden分区中会发生同样的事情:无引用的对象被删除,引用对象被移动到另外一个Survivor分区(S1)。此外,从上次小垃圾收集过程中第一个Survivor分区(S0)移动过来的对象年龄增加,然后被移动到S1。当所有的幸存对象移动到S1以后,S0和Eden区都会被清理。注意到,此时的Survivor分区存储有不同年龄的对象。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_18,color_FFFFFF,t_70,g_se,x_16

⑤在下一个小垃圾收集,同样的过程反复进行。然而,此时Survivor分区的角色发生了互换,引用对象被移动到S0,幸存对象年龄增大。Eden和S1被清理。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_17,color_FFFFFF,t_70,g_se,x_16

⑥这幅图展示了从年轻代到老年代的提升。当进行一个小垃圾收集之后,如果此时年老对象到达了某一个年龄阈值(例子中使用的是8),JVM会把他们从年轻代提升到老年代。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_17,color_FFFFFF,t_70,g_se,x_16

⑦随着小垃圾收集的持续进行,对象将会被持续提升到老年代。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_11,color_FFFFFF,t_70,g_se,x_16

⑧这样几乎涵盖了年轻一代的整个过程。最终,在老年代将会进行大垃圾收集Major GC,这种收集方式会清理-压缩老年代空间。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_16,color_FFFFFF,t_70,g_se,x_16

也就是说,刚开始会先在新生代内部反复的清理,顽强不死的移到老生代清理,最后都清不出空间,就爆炸了。

4.内存优化,减少GC开销的措施

根据上述GC的机制,程序的运行会直接影响系统环境的变化,从而影响GC的触发。若不针对GC的特点进行设计和编码,就会出现内存驻留等一系列负面影响。为了避免这些影响,基本的原则就是尽可能地减少垃圾和减少GC过程中的开销。具体措施包括以下几个方面:

①不要显式调用System.gc()

此函数建议JVM进行主GC,虽然只是建议,而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。

②尽量减少临时对象的使用

临时对象在跳出函数调用后,会成为垃圾。少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。

③对象不用时最好显式置为Null

一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。

④尽量使用StringBuffer,而不用String来累加字符串

String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为每次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因为StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

⑤能用基本类型如int、long,就不用Integer、Long对象

基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

⑥尽量少用静态对象变量

静态变量属于全局变量,不会被GC回收,它们会一直占用内存。

⑦分散对象创建或删除的时间

集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的,它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

⑧bitmap、游标Cursor、IO或者文件流等不用时候,记得回收。尤其是图片的加载而占用内存较大,可以做图片质量或者物理大小的压缩。

⑨避免在频繁绘制的onDraw方法里创建对象

⑩设置过的监听不用时,及时移除。如在destroy时及时remove,尤其以addListener开头的,在destroy中都需要remove。

⑩HashMap由于创建时候内存生成16位存储Entry节点的数据,一个Entry占32B,也就是说即使里面没有任何元素,也要分配一块内存空间给它。且存储数据每次大于当前容量最大值,HashMap都会以*2容量的方式去扩容。所以在Android中,HashMap是比较费内存的。

⑩防止单例类长久持有不用对象的引用,导致对象无法回收,特别是传入上下文对象的单例类,可以尝试传入ApplicationComtext。

⑩非静态内部类导致内存泄露,比如Activity中创建Handler,可以尝试弱引用去拿到外部的对象引用。

⑩广播的注销,页面Activity退出时未执行完的Thread的注销或者Timer还在执行定时任务的注销等。

⑩Activity结束时,需要Cancel掉属性动画。

猜你喜欢

转载自blog.csdn.net/zenmela2011/article/details/123237796#comments_22878758