第3章 垃圾收集器与内存分配策略
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。
3.2对象已死?
GC在对堆进行回收之前,要先确定对象有哪些还“存活”着,哪些已经“死去”。
引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。但Java语言没有选用引用计数算法来管理内存,因为它很难解决对象之间的相互循环引用的问题。
根搜索算法:Java和C#等主流商用语言都是使用的根搜索算法(GC Roots Tracing)判定对象是否存活的。根搜索算法的基本思路就是,通过一系列的名为”GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到”GC Roots”没有引用链可达时,则证明此对象是不可用的,判定为可回收对象。
如上图所示,obj 5、obj 6、obj 7虽然互有关联,但他们到GC Roots是不可达的,所以将它们判定为是可回收对象。
在Java语言中,可作为GC Roots的对象包括以下几种:
1. 栈帧中的本地变量表中的引用对象
2. 方法区中的类静态属性引用对象
3. 方法区中的常量应用的对象
4. 本地方法栈中JNI(Native方法)的引用对象
再谈引用:JDK1.2以后,Java对引用(reference)的概念进行了扩充,将引用分为强(strong)引用、软(soft)引用、弱(weak)引用、虚(phantom)引用四种。四种引用强度依次减弱:
强引用:类似”Object obj = new Object()”这类的引用,只要有强引用在,垃圾收集器永远不会回收掉被引用的对象。
软引用:藐视一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出前,将会把这些对象列进回收范围中并进行第二次回收。
弱引用:描述非必需的对象,强度比软引用更弱。被弱引用关联的对象只能生存到下一次GC发生之前。
虚引用:最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
生存还是死亡?:在根搜索算法中不可达的对象,就暂时进入“缓刑”阶段。要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在根搜索后发现无与GC Roots相连的引用链,会被第一次标记并进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者方法已经被虚拟机调用过,则此两种情况都视为“没有必要执行”。
如果这个对象被判为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行。finalize()方法是对象逃脱被判死的最后一次机会,稍后GC将会对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中自救,只要与引用链上的任一对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量。否则,它就真的离死亡不远了。
finalize()方法是Java刚诞生时,为了使C/C++程序员更容易接受它而做出的一个妥协,但它运行代价高昂,不确定性大,无法保证各个对象的调用顺序。其实finalize()能做的所有工作,使用try-finally或其他方式也都可以做得更好更及时,大家完全可以忘掉这个方法的存在。
回收方法区:方法区一般不要求进行GC,而且在方法区进行的GC的“性价比”一般较低。永久代的GC主要回收两部分内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象非常相似,实现也比较简单。而要判定一个类是否是“无用的类”条件则苛刻许多,需要同时满足一下三个条件才能算是“无用的类”:
1. 该类的所有实例都已被回收,即Java堆中不存在该类的任何实例;
2. 加载该类的ClassLoader已经被回收;
3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述三个条件的无用类进行回收,但也只是可以,并不是必然会回收。
3.3垃圾收集算法:
标记-清除算法(Mark-Sweep):最基础的GC算法。实现分为两个阶段:首先,标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。说它基础是因为后续的收集算法都是基于这种思路改进而得到的。它主要有两个缺点:一是效率问题,标记和清楚过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片。
复制算法(Copying):为了解决效率问题而出现的GC算法。将可用内存划分为相等的两块,每次使用其中一块,当这一块用完了,就将还活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。此算法实现简单,运行高效,只是将内存折半使用的算法代价未免过高了一点。现在商业虚拟机都采用此算法来回收新生代。
标记-整理算法(Mark-Compact):首先标记对象,之后让所有的存货对象都向一端移动,然后直接清理掉端边界以外的内存。一般适用于存活率较高的老年代。
分代收集算法:当前商业虚拟机的GC都采用“分代收集”算法。根据不同对象的存活周期不同将内存划分为几块,根据各个年代的特点采用最合适的GC算法。