目录
一.概述
二.内存回收的实现
三.再谈引用
四.再论对象生死与否
【《Java虚拟机》之垃圾收集器和内存分配策略】五.垃圾收集算法
一.概述 在上一节中我们谈论到自动内存管理机制,很显然,对于虚拟机而言,其不断的创建对象和销毁对象的过程,必然有内存操作上的“得与失”。针对有限的内存资源,我们显然要做到资源最优化,那么这就是我们将要做到的内存回收,即实现垃圾收集器。在实现真正的内存回收前,我们不妨考虑下:
(1)哪些内存需要回收
(2)什么时候回收
(3)怎么实现回收
前面介绍了Java内存运行时区域的各部分,我们可以知道其中的程序计数器,虚拟机栈,本地方法栈3个区域都是随着线程生而生,随着线程灭而灭。在这几个区域里,内存的分配和回收都具备了确定可知性。而 对于存放对象实例的Java堆,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是处于动态的。显然,我们需要重点“照顾”这一部分内存。
二.内存回收的实现 在垃圾收集器要在Java堆中回收内存之前,其必然要知道哪些内存无用可以回收,哪些内存是有用无需回收的。在真正的垃圾回收动作之前,我们势必要明确,实现内存的回收,就是对已经“死亡”的对象实例在创建它时给它所分配的内存进行再利用。这又关联到对象的生死与否,以及如何判定其存活情况的具体实现。
2.1 引用计数算法 在许多的Java教材当中,往往是通过这样一个方法来判定对象的存活情况:给每一个对象都添加一个引用计数器,每当有一个地方需要引用它时,计数器的值就加1;在每一个引用失效时,计数器的值就会减1;在任何时刻计数器的值为0时的对象就是不可再被使用的,即将会作为“即将回收”的一员。这种引用计数算法的思路简单,实现效果也不错。但是在现在的主流虚拟机中,其并没有被采用,其主要是因为存在一个无法避免的问题————很难解决对象之间的相互循环引用问题
objA.instance=objB;在这个例子当中,对象objA,objB都有字段instance,赋值令 objA.instance=objB和objB.instance=objA; 除此之外,这两个对象都无其他任何的引用,但实际上这两个对象已经不能再被访问了。它们因为相互引用着对方,致使两个对象的引用计数器值都不为0,所以引用计数器无法通知GC收集器来对它们回收。
objB.instance=objA;
2.2可达性分析算法 与引用计数算法不同,可达性分析算法较好的解决了多个对象相互引用的问题,并且作为主流的回收实现算法,其有各自独特的优点。可达性分析算法判定对象存活情况,都是通过一系列被称为“GCRoots”的对象节点起始,从这些节点出发,向下搜索(这有点像是树的结构),搜索所走过的路径叫做引用链(Reference Chain)。当一个对象不存在任何一条引用链使之到达GC Roots,那么换句话说,就是从GC Roots 到这个对象不可达,这个对象就为“死亡状态”。反之,如果对象存在任何一条引用链到达GC Roots,说明这个对象为“存活状态”。在这里要明白,是一个对象到GC Roots有引用链可达,才能作为“存活与否”的条件。而像是,两个对象之间有引用链相连,互相有关联,但是如果它们还是到GC Roots不可达,还是将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots 的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈JIN(即一般说的Native方法)引用的对象
- 强引用(Strong Reference):普遍存在程序当中,类似“ Object obj=new Object()”。只要这种强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
- 软引用(Soft Reference):用来描述一些有用但并非必需的对象。对于软引用相关联的对象,在系统将要发生内存溢出异常之前,会把这些对象列进回收范围内进行第二次回收,如果在这次回收过后内存仍然不够,这才会抛出内存溢出异常。
- 弱引用(Weak Reference):用来描述一些非必需的对象。比起软引用来更为弱一些。被弱引用相关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论内存是否足够,都会回收掉只存在弱引用相关联的对象。
- 虚引用(Phantom Reference):是最弱的引用关系。一个对象是否存在虚引用,完全不会对其生存时间造成影响 ,同时也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
五.垃圾收集算法 5.1标记-清除算法
作为最基础的收集算法,Mark-Sweep算法分为两个模块,一个“标记”和一个“清除”。针对其不足,一是效率问题,执行标记和清除的效率都不高;二是空间问题,在标记清除过后将会产生大量不连续的内存碎片,碎片太多可能会导致在程序运行过程中需要分配较大的对象时,无法找到足够的内存空间而不得不提前触发一次垃圾回收动作。
5.2 复制算法
为解决效率问题,“复制”算法应运而生。它将整个可用内存划分为大小等量的两块,每次只使用其中的一块,当这一块内存用完了就将还存活的对象复制到另外一块内存上面,然后将已经使用过的内存空间一次清理掉。这样使得是对整个半区进行内存回收,可以忽略内存碎片的问题。只是这种代价太大了,每次只用一半的内存使用量。在现在商用虚拟机中,大都采用基于这种算法的改进版来回收新生代。将内存划分为一个较大的Eden空间和两块较小的Survivor空间,其比例约为8:1,有效解决内存浪费问题。
5.3标记-整理算法
Mark-Copmpact算法,过程与“标记-清除”算法相似。但并不是直接对可回收对象进行清理,而是让所有的对象都向一段移动,然后直接清理掉段边界以外的内存。
5.4分代收集算法
Generational Collection算法根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,利用各个年代的特点采取最恰当的收集算法。
在新生代中,每次垃圾收集时都有大批量的对象死亡,只有少量存活,采用复制算法合适。
在老年代中,对象的存活率高,没有额外的空间,就必须采用“标记-整理”或是“标记-清理”算法
>参考《深入理解Java虚拟机》
>争渡争渡,惊起一滩欧鹭。
==欲知后事如何,请见下回分解==