Java虚拟机之垃圾回收

时间:2022-12-27 15:09:53

    上一篇文章中介绍了Java虚拟机内存运行时区域的各个部分,其中程序计数器、Java虚拟机栈、本地方法栈3个区域都是线程私有的,因此当一个方法或线程结束时内存也随着被回收,并且每一个栈帧中分配多少内存基本上是在类结构确定下来的时候已经知道了,因此也不必考虑内存分配的问题。只有Java堆和方法区的内存分配和回收都是动态的,因此垃圾回收(Garbage Collection,GC)也主要在这两部分进行。

    要进行垃圾回收首先就是判定哪些对象需要被回收,即对象是alive还是dead。常用的判断对象存活的算法有:引用计数算法和可达性分析法。 

    对于引用计数法,每个对象都有一个引用计数器,每当对象被引用一次,计数器就加1,当引用失效时减1;计数器为0的对象就是不可能再被使用的。但是绝大多数的虚拟机都没有选用引用计数算法来管理内存,因为它很难解决两个对象出现循环引用的问题。

    可达性分析(Reachability Analysis)算法的基本思想是通过一系列成为“GC Roots”的对象作为起始点,从这些节点向下搜索,走过的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,就说明该对象是不可用的。此时会对对象做一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法,当该对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为”没有必要执行“。如果这个对象被判定为有必要执行finalize()方法,那么将会被放置在一个叫做F-Queue的队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次标记。如果这个时候对象还没有逃脱,那么基本上它就真的被回收了。需要注意的是,每个对象的finalize()方法只会被系统自动调用一次,如果对象面临下一次回收则不会被再次执行。

     下面介绍一下常用的垃圾回收算法。

1.标记-清除算法(Mark-Sweep)

    标记-清除算法是最基础的收集算法,后续的很多算法都是基于此思路进行改进得到的。首先标记出需要回收的对象,在标记完成后统一回收所有被标记的对象。她主要有两个不足:一是效率问题,标记和清除两个过程效率都不高;另一个是空间问题,标记清除之后会产生大量的空间碎片,当有大对象要进行分配而没有足够的连续内存时便不得不提前触发进行另一次垃圾收集动作。

2.复制算法(Copying)

    复制算法为了解决标记-清除算法的效率问题,它将可用内存按容量划分为两部分,每次使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后把使用过的内存空间一次清理掉,这样不会产生内存碎片等复杂情况。现在的商业虚拟机都采用这种方法来回收新生代,同时将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用其中一块Eden空间和一块Survivor空间。回收时将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,最后清理掉Eden和用过的Survivor空间。Hotspot默认Eden和Survivor的大小比例是8:1。如果每次回收时存活的对象多于10%,那么将会依赖老年代的空间进行分配担保。

3.标记-整理算法

    标记-整理算法主要运用在老年代,它的标记过程和标记-清除算法一样,但是接下来会把存活的对象向一端移动,然后直接清理掉边界以外的内存。

4.分代收集算法

    当前的商业虚拟机的垃圾收集都采用的是分代收集(Generational Collection)算法。它把Java堆分为新生代和老年代,在新生代采用复制算法,老年代中采用标记-清除或标记-整理算法来进行回收。