“生存还是死亡”
如何来判定对象是否存活?针对这个问题书中给出了两种算法,分别是引用计数算法和可达性分析算法
引用计数算法
该算法的思路简单并且易于实现。我们给对象中添加一个引用计数器,当有一个地方引用它时,引用计数器就加一,当引用失效时,计数器减一,当计数器为0时就说明该对象不可能再被引用。
客观的评价,该算法判定效率很高,在很多情况下都是一种不错的算法,但是,至少主流的Java虚拟机并没有采用这种算法。原因是该算法无法解决对象之间的循环引用问题。
什么是循环引用呢?笔者认为就是对象之间的嵌套的引用,将对象B赋值给对象A中的字段instance,再将对象A赋值给对象B中的字段instance,这样就形成了嵌套的互相的引用关系,则引用计数器永远也不会为0,根据算法规定,两个对象自然也就不会被回收,但事实并不是这样,虚拟机成功的回收了两个对象(具体可以参考《深入理解Java虚拟机》63页的代码)。可达性分析算法
从图中可以直观的看出,判断对象是否存活可以检查该对象是否与GC Roots关联,或者说是从GC Roots出发,到对象是否存在一条路径,该路径也被称作引用链(Reference Chain)。如果可达,那么该对象可引用,如果对象不可达,那么该对象不可引用。
在Java语言中,可作为GC Roots的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
再探引用:
Java中的引用关系原本只有两种,引用和未被引用,这种定义是狭隘的,当我们需要描述像”鸡肋“这样的对象时,这种描述显得无能为力,因此我们引入了其他几种引用:
- 强引用(只要强引用依然存在,垃圾收集器就不会回收掉被引用的对象)
- 软引用(在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收)
- 弱引用(弱引用关联的对象只能生存到下一次垃圾收集发生之前)
- 虚引用(一个对象是否由虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例)
对象的死亡
在可达性分析算法中不可达,或者没有引用链的对象并不会立即死亡,要正式宣告对象的死亡,需要两个阶段:
第一个阶段,在可达性分析算法中被发现没有引用链,那么该对象将会被第一次标记并且进行一次筛选。下面我们来看一下筛选的过程,筛选的条件是该对象是否有必要执行finalize()方法。
当对象没有覆盖finalize()方法或者finalize()方法已被虚拟机调用过,那么,虚拟机将这两种情况视为”没有必要执行“。
当对象被判定为有必要执行finalize()方法,那么该对象将会被放置在一个叫做Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行(执行并不以为着虚拟机会等待它的结束),接着(第二个阶段),GC将会对F-Queue队列中的对象进行第二次小规模的标记,如果对象在finalize()方法中成功的进行引用(和引用链上的任何一个对象建立关联),那么该对象将会被移出”即将回收“集合,反之,该对象将会被GC回收。建议读者参考周志明老师的《深入理解java虚拟机》67页代码
回收方法区
方法区(永久代)的垃圾收集主要回收两部分内容:废弃常量和无用的类
我们先以一个实例来考察一下废弃常量的回收过程:假如一个字符串"Hello World!"已经进入了常量池中,但是当前系统没有任何一个String对象是叫做"Hello World!"的,换句话说,也就是没有一个String对象引用常量池中的”Hello World!"常量,也没有其他地方引用这个字面量,那么这时如果发生内存回收,这个常量就会被系统清理出常量池。
下面我们来看一下如何判定一个类是否是“无用的类”
- 该类所有的实例已经被回收,也就是Java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
常量池中的其他类(接口)、方法、字段的符号引用也与此类似
虚拟机可以对满足上述三个条件的无用类进行回收,但并不是和对象一样,不使用了就必然会回收,是否对类进行回收,虚拟机给出了相关的参数进行调节
垃圾收集算法
字不重要,主要看图↑
上图演示了标记-清除算法
顾名思义,标记-清除算法分为两个阶段:标记阶段和清除阶段
标记阶段前文已经介绍过,读者可以查看上文;清除阶段即清除已被标记了的对象
标记-清除算法的缺点主要由两个,第一:该算法标记和清除的两个过程的效率都不高;第二:该算法清除了可回收对象后会产生不连续的内存空间,在分配大的连续的对象时,就需要提前触发一次垃圾收集动作。
字不重要,主要看图↑
上图演示了复制算法
复制算法的基本思想是将内存空间分为大小相同的两部分,当前这半个内存用完时,将其中存活的对象复制到另一半内存中,然后一次性清理掉当前这一半内存空间。
该算法完美的规避了标记-清除算法的缺点,同时也浪费了大量的内存空间
补充:
HotSpot虚拟机是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中一块Survivor,当回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间,最后清理掉Eden和Survivor空间。HotSpot虚拟机采用分配的比例为8:1,也就是每次有效使用的空间为90%
当Survivor空间不足呢?虚拟机是如何处理的?这里会涉及到一个分配担保的问题(类似于银行贷款的数学模型),我们在下一篇博文中会详细介绍。
字不重要,主要看图↑
上图演示了标记-整理算法
标记-整理算法的提出和老年代有关。整个算法的执行过程和标记-清除算法一样,但是后续的步骤不是直接对已标记的对象进行清理,而是让所有存活的对象都向一边移动,接着直接清理掉端边界以外的内存。
分代收集算法
该算法汲取了复制算法和标记-整理算法的长处。我们将Java堆分为新生代和老年代,这样就可以根据各个年代的特点来选择算法:
新生代选择复制算法,老年代选择标记-整理算法。
HotSpot虚拟机如何发起内存回收?这个问题以后我会专门发一篇博文来进行简单的介绍,这里不详细介绍!