Java虚拟机垃圾收集器
一、判断对象存活的算法
1、引用计数(Reference Counting)算法
给对象添加一个引用计数器,每当有一个地方引用时,计数器加1。当引用失效时,计数器减1。当计数器的值为0的时候说该对象不可能再被使用。引用计数器算法的实现简单,效率高,比如微软的COM(Component Object Model)技术,Python等。但是Java虚拟机没有使用该技术,因为该技术难以解决对象循环依赖的问题,即A中有B,B中有A这种情况。
2、可达性分析(Reachability Anlysis)算法
通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到"GC Roots"没有任何引用链相连是,说明该对象不可用了,即该对象被判定为可回收对象。java中可作为GC Roots的对象:
1) 虚拟机栈(栈帧中的本地变量表)中引用的对象。
2) 方法区中类静态属性引用的对象。
3) 方法区中常量引用的对象。
4) 本地方法栈中JNI(一般指Native方法)引用的对象。
3、Java的几种引用
主要指4种引用:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),这四种引用强度依次逐渐减弱。
1)强引用:代码中普遍存在,如Object obj = new Object()这类引用,只要引用还存在,垃圾收集器永远不会回收掉被引用的对象。
2)软引用:描述一些有用但并非必须的对象。软引用关联的对象,在系统内存溢出之前,将会把这些对象列入回收范围进行第二次垃圾回收。如果这次回收还没有足够的内存,则抛出内存溢出异常。使用SoftReference类来实现。
3)弱引用:用来描述非必须的对象。弱引用关联的对象,只能生存到下一次垃圾回收之前。下一次垃圾回收的时候,无论当前内存是否足够,都会回收掉被弱引用关联的对象。使用WeakReference类来实现。
4)虚引用:一个对象是否有虚引用存在,完全不会对其生存时间造成影响。也无法通过一个虚引用来取的对象实例。为一个对象设置虚引用关联,唯一的目的就是在这个对象在被收集器回收时收到一个系通知。使用PhantomReference类来实现。
4、Java对象的生存或者死亡
在可达性分析中不可达的对象也并非"非死不可",要真正判断一个对象是否死亡,至少要经过两个阶段:第一个阶段是对象没有和GC Roots相连的引用链。那该对象会被第一次标记并进行第一次筛选,筛选条件为该对象有没有必要执行finalize()方法。当没有覆盖finalize()方法,或者finalize()方法已经被调用过,则判定为没有必要执行。
如果判定有必要执行finalize()方法,则虚拟机会将其放到一个F-Queue的队列中,并在稍后创建一个低优先级的Finalizer去执行它。这里的"执行"指的是触发这个方法,但不承诺等待该方法运行结束。这是为了防止线程一直等待,可能导致虚拟机奔溃。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC会对F-Queue中的对象进行第二次小规模标记。如果对象在此时成功的拯救自己,即把自己和GC Roots引用链连接上。则第二次标记时它将被移出"即将回收集合",如果对象这时候还没有逃脱,基本上就真的被回收了。注:任何对象的finalize()方法都只会被系统调用一次。
5、回收方法区
Java虚拟机规范说过可以不需要对方法区进行垃圾回收。在方法区中进行垃圾回收性价比比较低,在新生代中一次垃圾回收可以回收70%~95%的空间,而方法区的效率就远低于此了。永久代的回收主要分为两方面:废弃常量(JDK 1.7之前)和无用类。回收废弃常量和堆中的类似,只要没有和GC roots相连则可回收。而判定一个类是否是无用类的条件就比较苛刻了。需要满足三点:
1)该类所有实例都已被回收,即Java堆中不存在该类的任何实例。
2)加载该类的ClassLoader已经被回收。
3)该类对应的java.lang.Class类没有在任何地方被引用,无法在任何地方通过反射再访问该类的方法。
是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class和-XX:+TraceClassLoading、-XX:TraceClassUnLoading查看类的加载和卸载信息。其中-verbose:class和-XX:+TraceClassLoading可以在Product版中的虚拟机中使用,-XX:TraceClassUnLoading需要在FastDebug版本的虚拟机才支持。
在大量使用动态代理、反射、CGLib等大量使用ByteCode的框架,动态生成JSP以及OSGI这个频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
二、垃圾收集算法:
1、标记-清除(Mark_Sweep)
该算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记对象。该算法是最基础的算法,之所以说是最基础的算法是因为后续的算法都是基于该算法的不足之处进行改进得到的。这个算法有两个不足点:
1) 效率问题,标记和清除两个阶段效率都不高。
2) 空间问题,标记清除后会产生大量不连续的内存碎片,内存碎片太多可能会导致以后程序运行中,需要分配大对象时无法找到足够的连续内存空间,从而不得不提前触发另一次垃圾回收。
2、复制算法(Copying)
为了解决效率问题该算法出现了,它将可用内存按容量划分为等量的两块,每次只使用一块。当这块内存使用完了之后,就将存货的对象复制到另一块上,然后把使用过的这块内存一次清理掉。这样使得每次回收都是对半块内存进行回收,就不用考虑内存碎片的问题了。只要移动堆顶的指针,顺序分配内存就可以了,实现简单,运行高效。但是缺点就是造成了空间的浪费,一半的闲置内存代价太高。
现在的商业虚拟机都采用这种方式来回收新生代,经研究表明新生代的对象98%都是"朝生夕死",所以并不需要按照1:1的比例来划分。而是将内存划分为一块较大的Eden区和两块较小的Survivor区,每次只是用Eden区和一块Survivor区,当进行垃圾回收的时候,将Eden区和正在是用的Survivor区中存活的对象一次性拷贝到另一块Survivor区中,最后再清理掉Eden区和已使用过的Survivor区。HotSpot默认的Eden区和Survivor区的比例是8:1,即每次新生代中可用内存为(80%+10%),也就是说只有10%会被"浪费"。当然98%的对象可回收是一般场景下的数据,我们没法保证每次都只有不多于10%的对象存活,当Survivor不够时,我们需要依靠别的内存(这里指的是老年代)来担保(Handle Promotion)。当另一块Survivor没法存放上一次回收时的存活对象时,这些对象会直接通过分配担保进入老年代。
3、 标记-整理(Mark-Compact)
复制算法在对象存活率较高的情况下要进行较多的复制,效率比较低下。如果不想浪费50%的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存货的极端情况,所以老年代都不使用这种算法。根据老年代的特征,有人提出了"标记-整理"算法,该算法和"标记-清除"算法一样,但后续步骤不是直接对可回收对象进行清理,而是先将所有存活对象移动到一端,然后清理掉端边境以外的内存区域。
4、 分代收集算法(Generational Collection)
当前商业虚拟机都是采用的这种算法,这种算法只是根据对象的存活周期将内存划分为多块。一般是把虚拟机划分为新生代和老年代,这样就可以根据各个年代的特点采用合适的手机算法了。在新生代中,每次回收都会有大量的对象死去,少量存活,这样就可以采用复制算法了,只需要付出少量对象复制的代价就可以完成垃圾回收了。而在老年代中对象存活率高、没有额外的空间对它进行分配担保,即必须使用"标记-清除"或"标记-整理"算法了。