《Java虚拟机》之垃圾收集器和内存分配策略

目录
一.概述
二.内存回收的实现
三.再谈引用
四.再论对象生死与否
【《Java虚拟机》之垃圾收集器和内存分配策略】五.垃圾收集算法

一.概述 在上一节中我们谈论到自动内存管理机制,很显然,对于虚拟机而言,其不断的创建对象和销毁对象的过程,必然有内存操作上的“得与失”。针对有限的内存资源,我们显然要做到资源最优化,那么这就是我们将要做到的内存回收,即实现垃圾收集器。在实现真正的内存回收前,我们不妨考虑下:
(1)哪些内存需要回收
(2)什么时候回收
(3)怎么实现回收
前面介绍了Java内存运行时区域的各部分,我们可以知道其中的程序计数器,虚拟机栈,本地方法栈3个区域都是随着线程生而生,随着线程灭而灭。在这几个区域里,内存的分配和回收都具备了确定可知性。而 对于存放对象实例的Java堆,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是处于动态的。显然,我们需要重点“照顾”这一部分内存。
二.内存回收的实现 在垃圾收集器要在Java堆中回收内存之前,其必然要知道哪些内存无用可以回收,哪些内存是有用无需回收的。在真正的垃圾回收动作之前,我们势必要明确,实现内存的回收,就是对已经“死亡”的对象实例在创建它时给它所分配的内存进行再利用。这又关联到对象的生死与否,以及如何判定其存活情况的具体实现。
2.1 引用计数算法 在许多的Java教材当中,往往是通过这样一个方法来判定对象的存活情况:给每一个对象都添加一个引用计数器,每当有一个地方需要引用它时,计数器的值就加1;在每一个引用失效时,计数器的值就会减1;在任何时刻计数器的值为0时的对象就是不可再被使用的,即将会作为“即将回收”的一员。这种引用计数算法的思路简单,实现效果也不错。但是在现在的主流虚拟机中,其并没有被采用,其主要是因为存在一个无法避免的问题————很难解决对象之间的相互循环引用问题

objA.instance=objB;
objB.instance=objA;
在这个例子当中,对象objA,objB都有字段instance,赋值令 objA.instance=objB和objB.instance=objA; 除此之外,这两个对象都无其他任何的引用,但实际上这两个对象已经不能再被访问了。它们因为相互引用着对方,致使两个对象的引用计数器值都不为0,所以引用计数器无法通知GC收集器来对它们回收。
2.2可达性分析算法 与引用计数算法不同,可达性分析算法较好的解决了多个对象相互引用的问题,并且作为主流的回收实现算法,其有各自独特的优点。可达性分析算法判定对象存活情况,都是通过一系列被称为“GCRoots”的对象节点起始,从这些节点出发,向下搜索(这有点像是树的结构),搜索所走过的路径叫做引用链(Reference Chain)。当一个对象不存在任何一条引用链使之到达GC Roots,那么换句话说,就是从GC Roots 到这个对象不可达,这个对象就为“死亡状态”。反之,如果对象存在任何一条引用链到达GC Roots,说明这个对象为“存活状态”。在这里要明白,是一个对象到GC Roots有引用链可达,才能作为“存活与否”的条件。而像是,两个对象之间有引用链相连,互相有关联,但是如果它们还是到GC Roots不可达,还是将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots 的对象包括以下几种:
  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈JIN(即一般说的Native方法)引用的对象
三.再谈引用 从上面分析都可以知道,无论是引用计数算法还是可达性分析算法,判定对象存活与否都与“引用”有关。在传统的Java引用定义中,如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就可以说这块内存代表着一个引用。这种纯粹而又狭义的定义,在jdk1.2中,对引用的概念做了扩充。将引用分为了四个类别:
  • 强引用(Strong Reference):普遍存在程序当中,类似“ Object obj=new Object()”。只要这种强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用(Soft Reference):用来描述一些有用但并非必需的对象。对于软引用相关联的对象,在系统将要发生内存溢出异常之前,会把这些对象列进回收范围内进行第二次回收,如果在这次回收过后内存仍然不够,这才会抛出内存溢出异常。
  • 弱引用(Weak Reference):用来描述一些非必需的对象。比起软引用来更为弱一些。被弱引用相关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论内存是否足够,都会回收掉只存在弱引用相关联的对象。
  • 虚引用(Phantom Reference):是最弱的引用关系。一个对象是否存在虚引用,完全不会对其生存时间造成影响 ,同时也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
四.再论对象生死与否 在经历过可达性分析过后,被判定为不可达的对象,将会被放置到一个“可以被回收”的集合中,但是并不意味着这些对象就一定是“非死不可”,这时它们处于一个“缓刑”阶段,还有最后一次向上起诉以期达到“自救”的可能。对于一个对象,从被创建到宣布其死亡的过程,至少要经历两次标记过程。如果对象在进行可达性分析后发现没有与GC Roots相关联的引用链,那它将会被第一次标记并且进行一次筛选,以此对象是否有必要执行finalize()方法为唯一标准。当对象没有覆盖finalize()方法,或者是finalize()方法之前已经被虚拟机调用过了的,那么这两种情况都会被虚拟机判定为“没有必要执行”。相反,如果这个对象被判定为“有必要执行”,那此对象将会被放置到一个称为“F-Queue”的队列中,并稍后由一个虚拟机自动建立的、低优先级的finalize线程中去执行。(这里的“执行”,意为虚拟机将会触发这个方法,但是并不承诺将会等待它运行结束,这是因为如果一个对象在finalize()方法中执行缓慢,或者是发生了死循环,会使队列中的其他对象永久处于等待状态,甚至是导致整个内存回收系统崩溃)。对象实现“自救”的最后一次机会就是finalize()方法,稍后GC将对F-Queue中的对象进行第二次小规模标记,对象想要实现“自我拯救”——只需重新与引用链上的任何一个对象建立关联即可,这样它才能将自己从第二次标记时的“即将回收”的集合中逃脱出来,否则就将真正的被实现回收了。在这里还有一点要注意的是,任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象将面临下一次回收,finalize()方法不会再被执行,难逃被回收的命运!
五.垃圾收集算法 5.1标记-清除算法
作为最基础的收集算法,Mark-Sweep算法分为两个模块,一个“标记”和一个“清除”。针对其不足,一是效率问题,执行标记和清除的效率都不高;二是空间问题,在标记清除过后将会产生大量不连续的内存碎片,碎片太多可能会导致在程序运行过程中需要分配较大的对象时,无法找到足够的内存空间而不得不提前触发一次垃圾回收动作。
5.2 复制算法
为解决效率问题,“复制”算法应运而生。它将整个可用内存划分为大小等量的两块,每次只使用其中的一块,当这一块内存用完了就将还存活的对象复制到另外一块内存上面,然后将已经使用过的内存空间一次清理掉。这样使得是对整个半区进行内存回收,可以忽略内存碎片的问题。只是这种代价太大了,每次只用一半的内存使用量。在现在商用虚拟机中,大都采用基于这种算法的改进版来回收新生代。将内存划分为一个较大的Eden空间和两块较小的Survivor空间,其比例约为8:1,有效解决内存浪费问题。
5.3标记-整理算法
Mark-Copmpact算法,过程与“标记-清除”算法相似。但并不是直接对可回收对象进行清理,而是让所有的对象都向一段移动,然后直接清理掉段边界以外的内存。
5.4分代收集算法
Generational Collection算法根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,利用各个年代的特点采取最恰当的收集算法。
在新生代中,每次垃圾收集时都有大批量的对象死亡,只有少量存活,采用复制算法合适。
在老年代中,对象的存活率高,没有额外的空间,就必须采用“标记-整理”或是“标记-清理”算法


>参考《深入理解Java虚拟机》
>争渡争渡,惊起一滩欧鹭。
==欲知后事如何,请见下回分解==

    推荐阅读