主要讲解垃圾回收的算法
上面我们已经了解到了,JVM的体系的结构,这次我们来说一下垃圾回收的算法。
1. 最开始的想法,或者说垃圾回收的最容易想到的就是:引用计数(reference count)
我们记录每一个对象受到引用的次数,每增加一个引用就在这个对象对应的引用次数加一,每减少一次对对象的印象对应的引用次数就减一,回收的时候直接检查引用的次数,次数为零的时候直接的回收。这个有一个缺点就是循环引用的问题。但是还有程序在使用这种算法,
2. 针对每一个对象,使用引用计数不行,我们可以使用标记,这个就是标记-清楚(mark - sweep)算法。
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
这里需要说一下,引用的根节点是什么,为什么所有的,包括循环引用都能标记到?当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了,然后从这里就能够分析出那些对象是可达的,那些是不可达的对象。
形象的表示为:
第三种的算法是:复制算法,针对标记清除的的缺点改进的算法。
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
综合以上两种的有点,提出第四种算法:标记-整理算法:
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
上面说的可用的三种算法,在jvm的不同的区域应用的还是不一样的(JVM并不是只采取一种算法)
增量收集(Incremental Collecting):实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法的。
分代收集(Generational Collecting):基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青
代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。
按系统线程分
串行收集:串行收集使用单线程处理所有垃圾回收工作,因为无需多线程交互,实现容易,而且效率比较高。但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。
并行收集:并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上CPU数目越多,越能体现出并行收集器的优势。
并发收集:相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会因为堆越大而越长。
在当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法并没有什么新的思路,只是根据对象存活周期的不同将内存划分为几块,一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采取最适合的收集算法,在新生代中每次垃圾收集时都会发现大批对象死去,只有少量存活,就采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代因为存活率高,没有额外空间对它进行分配担保,就必须使用标记--清理或标记整理算法进行回收。