Java内存管理——垃圾收集器

 

  概述html


 

提及垃圾收集(Garbage Collection,GC),大部分人都把这项技术当作Java语言的伴生产物。事实上,GC的历史远远比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。当Lisp还在胚胎时期时,人们就在思考:程序员

  GC须要完成的三件事情:算法

     哪些内存须要回收?缓存

    何时回收?并发

    如何回收?函数

通过半个世纪的发展,内存的动态分配与内存回收技术已经至关成熟,一切看起来都进入了“自动化”时代,那为何咱们还要去了解GC和内存分配呢?答案很简单:当须要排查各类内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,咱们就须要对这些“自动化”的技术实施必要的监控和调节。高并发

  把时间从半个世纪之前拨回到如今,回到咱们熟悉的Java语言。第2章介绍了Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操做。每个栈帧中分配多少内存基本上是在类结构肯定下来时就已知的(尽管在运行期会由JIT编译器进行一些优化,但在本章基于概念模型的讨论中,大致上能够认为是编译期可知的),所以这几个区域的内存分配和回收都具有肯定性,在这几个区域内不须要过多考虑回收的问题,由于方法结束或线程结束时,内存天然就跟随着回收了而Java堆和方法区则不同,一个接口中的多个实现类须要的内存可能不同,一个方法中的多个分支须要的内存也可能不同,咱们只有在程序处于运行期间时才能知道会建立哪些对象,这部份内存的分配和回收都是动态的,垃圾收集器所关注的是这部份内存,本书后续讨论中的“内存”分配与回收也仅指这一部份内存。性能

 

  回收策略优化


 

  回收做用:   ui

   经过清除不用的对象来释放内存,同时垃圾收集的另一个重要做用就是消除堆内存空间的碎片

  1. 引用计数

  不少教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任什么时候刻计数器都为0的对象就是不可能再被使用的。所以A的回收可能会致使连锁反应。

  客观地说,引用计数算法(Reference Counting)的实现简单,断定效率也很高,在大部分状况下它都是一个不错的算法,也有一些比较著名的应用案例,例如微软的COM(Component Object Model)技术、使用ActionScript 3的FlashPlayer、Python语言以及在游戏脚本领域中被普遍应用的Squirrel中都使用了引用计数算法进行内存管理。可是,Java语言中没有选用引用计数算法来管理内存,其中最主要的缘由是它很难解决对象之间的相互循环引用的问题。

  优势:简单,快

  缺点:没法检测循环引用,好比A的子类a引用了A,A又引用了a,所以A和a永远不会被回收。这个缺点是致命的,所以如今这种策略已经不用。

  2. 根搜索算法 

  在主流的商用程序语言中(Java和C#,甚至包括前面提到的古老的Lisp),都是使用根搜索算法(GC Roots Tracing)断定对象是否存活的。这个算法的基本思路就是经过一系列的名为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来讲就是从GC Roots到这个对象不可达)时,则证实此对象是不可用的。如图3-1所示,对象object 五、object 六、object7虽然互相有关联,可是它们到GC Roots是不可达的,因此它们将会被断定为是可回收的对象。

  在Java语言里,可做为GC Roots的对象包括下面几种:

    虚拟机栈(栈帧中的本地变量表)中的引用的对象。

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

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

    本地方法栈中JNI(即通常说的Native方法)的引用的对象

  跟踪收集器一般使用两种策略来实现:

  1.压缩收集器:遍历的过程当中若是发现对象有效,则马上把对象越过空闲区滑动到堆的一端,这样堆的另外一端就出现了一个大的连续空闲区,从而消除了堆碎片。

  2.拷贝收集器:堆被分为大小相等的两个区域,任什么时候候都只使用其中一个区域。对象在同一个区域中分配,直到这个区域被耗尽。此时,程序执行被停止,堆被遍历,遍历时被标记为活动的对象被拷贝到另一个区域。这种作法用称之为“中止并拷贝”。

  这种作法的主要缺点是:太粗暴,要拷贝就都拷贝,粒度太大,总体性能不高。所以就有了更先进的“按代收集的收集器”

  3. 按代收集的收集器

  基于两个事实:

  1)大多数程序建立的大部分对象都有很短的生命周期。

  2)大多数程序都建立一些具备很是长生命周期的对象。

  所以按代收集策略就是在“中止并拷贝”策略基础之上,把对象按照寿命分为三六九等,不是一视同仁。它把堆划分为多个子堆,每个子堆为一“代”对象服务。最年幼的那一代进行最频繁的垃圾收集。没通过一次垃圾收集,存活下来的对象就会“成长”到更老的“代”,越是老的“代”对象数量应该越少,他们也越稳定,所以就能够采起很经济的策略来处理他们,简单“照顾”一下他们就行啦。这样总体的垃圾收集效率要比简单粗暴的“中止并拷贝”高。

  4.火车算法

  火车算法是用来替代按代收集策略的吗?不是的,能够说,火车算法是对按代收集策略的一个有力补充。咱们知道按代收集策略把堆划分为多个”代“,每一个代均可以指定最大size,可是”成熟对象空间“除外,”成熟对象空间“不能指定最大size,由于它是”最老“对象的最终也是惟一的归宿,除此以外,这些”老家伙“无处可去。而你没法肯定一个系统最终会有多少老对象挤进”成熟对象空间“。

  火车算法详细说明了按代收集的垃圾收集器的成熟对象空间的组织。火车算法的目的是为了在成熟对象空间提供限定时间的渐进收集。

  火车算法把成熟对象空间划分为固定长度的内存块,算法每次在一个块中单独执行。为何叫”火车算法“?这与算法组织这些块的方式有关。

  每块数据至关于一节车箱

  每个数据块属于一个集合,集合内的全部数据块已经进行排序,所以集合就比如是一列火车

  成熟对象空间又包含多个集合,所以就比如有多列火车,而成熟对象空间就比如是火车站。

  

  14中包含了若干个被按序标记的火车火车由随意多个一样被按序标记的车箱组成在这个例子中有两列火车每一个车箱最多能够存储三个对象每列火车能够包含任意多个车箱.

  火车的记忆集合是它全部车箱记忆集合的总和, 不包括那些来自其它火车的引用. 在图14中, 对象E是1.1车箱在引用集合中, 可是他不在1号火车的引用集合中. 由于垃圾回收算法老是从标记最小的车箱开始, 在更新引用集合的时候, 只有那些来自标记高的车箱的引用才被看做是. 所以, 对象E属于车箱1.1的记忆集合, 而对象C不在车箱1.2的记忆集合中.

  当垃圾回收器收集第一个车箱, 对象A须要保留下来, 因为是根引用指向它, 因此它会被拷贝到一个彻底新的火车中去. 因为对象B只有被A引用, 因此它会被拷贝到和A同一列火车中去. 这一点很重要, 由于经过这种方式, 自循环的垃圾对象结构最终被转移到同一列单独的火车中去了. 因为对象C被来自同一列火车的对象引用, 因此它被拷贝到了火车的最后去了. 如今第一个车箱空了, 能够被释放了. 经过第一遍回收, 火车站中的情况能够如图15所示

  

  记忆集合将会相应地进行更新第一列火车已经没有从外面(这里的外面指的是第一列火车之外)指向的引用的,因此在下一次回收中整个火车空间将会被案例的释放如图16所示.  

  

  任什么时候候在第一列火车中自循环的垃圾对象结构不会被拷贝到另外火车中去当全部不在这个自循环结构中的对象被拷贝到其它火车中后这列火车将会被释放这很容易理解但是是否能保证每个自循环的结构最终都会留在第一列火车中呢若是一个自循环结构分布在一些不一样的火车中那么在一系列迭代以后原来第二列火车会成为这个自循环结构的第一列火车而且结构体中的全部对象都会被分配到其它火车中去.(这里的其它火车指的是刚才自循环结构所占据的一些火车可是除去第一列火车.). 所以包含这个自循环结构对象火车数量会减小一个当火车数目达到1剩下的这个火车中包含了自循环结构的全部对象因而这个垃圾对象结构能够被正确的回收了.
17反映一个由4个对象组成的自循环结构.

  

  总体算法流程

  1.选择标号最小的火车.

  2.若是火车的记忆集合是空的, 释放整列火车并终止, 不然进行第三步操做.

  3.选择火车中标号最小的车箱.

  4.对于车箱记忆集合的每一个元素:

    若是它是一个被根引用引用的对象, 那么, 将拷贝到一列新的火车中去; 若是是一个被其它火车的对象指向的对象, 那么, 将它拷贝到这个指向它的火车中去.  

    假设有一些对象已经被保留下来了, 那么经过这些对象能够触及到的对象将会被拷贝到同一列火车中去.

  在这个步骤中, 有必要对受影响的引用集合进行相应地更新. 若是一个对象被来自多个火车的对象引用, 那么它能够被拷贝到任意一个火车去.

  释放车箱而且终止.

 

  再谈引用


 

  不管是经过引用计数算法判断对象的引用数量,仍是经过根搜索算法判断对象的引用链是否可达,断定对象是否存活都与“引用”有关。在JDK 1.2以前,Java中的引用的定义很传统:若是reference类型的数据中存储的数值表明的是另一块内存的起始地址,就称这块内存表明着一个引用。这种定义很纯粹,可是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之惋惜”的对象就显得无能为力。咱们但愿能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;若是内存在进行垃圾收集后仍是很是紧张,则能够抛弃这些对象。不少系统的缓存功能都符合这样的应用场景。

  在JDK 1.2以后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(WeakReference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。

  强引用就是指在程序代码之中广泛存在的,相似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象

  软引用用来描述一些还有用,但并不是必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常以前,将会把这些对象列进回收范围之中并进行第二次回收。若是此次回收仍是没有足够的内存,才会抛出内存溢出异常。在JDK 1.2以后,提供了SoftReference类来实现软引用。

  弱引用也是用来描述非必需对象的,可是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生以前。当垃圾收集器工做时,不管当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2以后,提供了WeakReference类来实现弱引用。

  虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,彻底不会对其生存时间构成影响,也没法经过虚引用来取得一个对象实例。为一个对象设置虚引用关联的惟一目的就是但愿能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2以后,提供了PhantomReference类来实现虚引用。

 

  生存仍是死亡?


 

  在根搜索算法中不可达的对象,也并不是是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:若是对象在进行根搜索后发现没有与GC Roots相链接的引用链,那它将会被第一次标记而且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种状况都视为“没有必要执行”。

  若是这个对象被断定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动创建的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样作的缘由是,若是一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的状况),将极可能会致使F-Queue队列中的其余对象永久处于等待状态,甚至致使整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,若是对象要在finalize()中成功拯救本身—只要从新与引用链上的任何一个对象创建关联便可,譬如把本身(this关键字)赋值给某个类变量或对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;若是对象这时候尚未逃脱,那它就真的离死不远了。从代码清单3-2中咱们能够看到一个对象的finalize()被执行,可是它仍然能够存活。

  从代码清单3-2的运行结果能够看到,SAVE_HOOK对象的finalize()方法确实被GC收集器触发过,而且在被收集前成功逃脱了。

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    public void isAlive(){
        System.out.println("yes,i am still alive!");
    }
    protected void finalize() throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
        //对象第一次成功拯救本身
        SAVE_HOOK = null;
        System.gc();
        // 由于Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no,i am dead!");
        }
        // 下面这段代码与上面的彻底相同,可是此次自救却失败了
        SAVE_HOOK = null;
        System.gc();
        // 由于Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no,i am dead!");
        }
    }
}

运行结果:

1 finalize method executed!
2 yes, i am still alive!
3 no, i am dead!

  另一个值得注意的地方就是,代码中有两段彻底同样的代码片断,执行结果倒是一次逃脱成功,一次失败,这是由于任何一个对象的finalize()方法都只会被系统自动调用一次,若是对象面临下一次回收,它的finalize()方法不会被再次执行,所以第二段代码的自救行动失败了。 须要特别说明的是,上面关于对象死亡时finalize()方法的描述可能带有悲情的艺术色彩,笔者并不鼓励你们使用这种方法来拯救对象。相反,笔者建议你们尽可能避免使用它,由于它不是C/C++中的析构函数,而是Java刚诞生时为了使C/C++程序员更容易接受它所作出的一个妥协。它的运行代价高昂,不肯定性大,没法保证各个对象的调用顺序。有些教材中提到它适合作“关闭外部资源”之类的工做,这彻底是对这种方法的用途的一种自我安慰。finalize()能作的全部工做,使用try-finally或其余方式均可以作得更好、更及时,你们彻底能够忘掉Java语言中还有这个方法的存在。

 

 

PS.

参考连接

http://www.cnblogs.com/gw811/archive/2012/10/19/2730258.html

http://www.cnblogs.com/wenfeng762/archive/2011/11/18/2137882.html

http://nileader.blog.51cto.com/1381108/402609

相关文章
相关标签/搜索