一、概述
关于垃圾回收(Garbage Collection, GC),需要考虑完成的三件事情:
1.哪些内存需要回收?
2.什么时候回收?
3.如何回收
二、对象已死吗
即垃圾收集器在堆进行回收前,第一件事情就是要确认堆中哪些对象还“存活”着,哪些已经“死去”(即不可能通过任何途径使用对象)。
引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用了。
引用计数算法的优点是实现简单,判定效率高,但至少在主流的 Java 虚拟机里没有选择引用计数算法来管理内存,因为它很难解决对象之间相互循环引用的问题。
可达性分析算法
这个算法的基本思路就是通过一系列的称为 “GC Roots” 的对象作为起始点,从这个节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(图论上讲,就是从 GC Roots)到这个对象不可达时,则证明此对象是不可用的。
在 Java 语言中,可作为 GC Roots 的对象保护以下几种:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象。
2.方法区中类静态属性引用的对象。
3.方法区中常量引用的对象。
4.本地方法中 JNI(即一般说的 Native 方法)引用的对象。
再谈引用
在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为强引用 (Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用强度依次逐渐减弱。
- 强引用就是指程序代码之中普遍存在的,类似 “ Object obj = new Object() ” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收。如果这次回收还是没有足够内存,才会抛出内存溢出异常。在 JDK 1.2 之后,提供了 SoftReference 类来实现软引用。
- 弱引用也是用来描述非必须对象的,但强度比软引用更弱一点,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够都会回收掉只被弱引用关联的对象。在 JDK 1.2 之后,提供了 WeakReference 类来实现弱引用。
- 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知。在 JDK 1.2 之后,提供了 PhantomReference 类来实现弱引用。
三、垃圾收集算法
标记-清除算法(Mark-Sweep)
算法分为 ”标记” 和 “清除” 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。这个算法是最基础的收集算法,但它有两个主要的不足:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能导致以后程序运行需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。标记-清除算法执行过程如下图:
复制算法(Coping)
它把内存按容量划分成大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。这使得每次都是对半区进行内存回收,内存分配时也就不用考虑内存碎片的问题了,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,代价高昂。复制算法执行如下图:
标记-整理算法(Mark-Compact)
标记-整理算法的标记过程与 “标记-清除”算法一样,只是后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存,该算法示意图如下:
分代收集算法(Generational Collection)
当前商业虚拟机的垃圾收集都是采用的分代收集算法,这个算法的实现就是根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分成新生代和老年代,再根据各个年代的特点采用最合适的收集算法。在新生代中,每次垃圾收集都有大批对象死去,只有少量存活,所以选用复制算法。而老年代中因为对象存活率高、没有额外空间对它进行担保,就必须使用 “标记-清理” 或者 “标记-整理”算法来进行回收。
四、HotSpot的算法实现
在新生代垃圾收集时,将内存分为一块较大的 Eden 和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor 。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%。当 进行垃圾收集Survivor 空间不够用时,需要依赖老年代进行分配担保。
五、垃圾收集器
Serial 收集器
历史最悠久,是一个单线程的垃圾收集器,并且在它进行垃圾收集时必须暂停其他所有工作线程,直到它收集结束。优点是简单高效。
ParNew 收集器
ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。
Parallel Scavenge 收集器
是一个新生代的收集器,它是一个采用复制算法,并行的多线程收集器。其关注点主要是达到一个可控制的吞吐量。
Serial Old 收集器
Serial 的老年代版本,采用 “标记-整理”算法
Parallel Old 收集器
Parallel Scavenge 的老年代版本
CMS 收集器
全称 Concurrent Mark Sweep,是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。优点是并发收集、低停顿。
G1(Garbage-First) 收集器
是一款面向服务端应用的收集器。与其他 GC 收集器相比,G1 具有如下特点:
- 并行与并发
- 分代收集
- 空间整合
- 可预测的停顿
以上内容是对《深入理解JVM虚拟机》一书中关于 Java 的垃圾回收介绍的整理,以后有更多自己的理解的话,我再补充。