Java 垃圾回收机制(GC)

判断对象是否可回收的常见方法

引用计数算法(Reference Counting)

给对象中添加一个引用计数器,每当有一个地方引用时,计数器加 1;当引用失效时,计数器减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。

  • 优点:实现简单,判定效率高;
  • 缺点:很难解决对象之间的循环引用问题;

JVM 并不使用这种算法。

可达性分析算法(Reachability Analysis)

通过一些列称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径叫做引用链,当一个对象到 GC Roots 没有任何引用链与之相连时,则证明该对象是不可用的。

在 Java 语言中,可作为 GC Roots 的对象包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中 JNI(Native 方法)引用的对象;

二次标记

如果一个对象真正宣告 “死亡”,至少要经过两次标记过程:

  • 如果一个对象经过可达性分析算法判定为可回收对象,则它将会被第一次标记并进行一次筛选,筛选的条件是判断该对象是否有必要执行 finalize () 方法。当对象没有覆盖 finalize () 方法或者已经执行过一次 finalize () 方法,则虚拟将将判定为 “没有必要执行”。如果判定为 “有必要执行 finalize () 方法”,那么这个对象将被置入 F-Queue 队列,并在稍后由一个虚拟机自动建立的、低优先级的 Finalizer 线程区执行(仅仅是触发)它。

  • finalize () 方法是逃脱死亡的最后机会,稍后 GC 将堆 F-Queue 队列进行再一次的小规模标记,如果对象在 finalize () 方法中重新与引用链上的任何一个对象建立关联,那么在第二次标记的时候将会被移除 “即将回收” 的集合;如果对象这时候还没有逃脱,那么基本上对象宣告 “死亡” 并将被回收。

垃圾收集算法

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

算法分为 “标记” 和 “清除” 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

主要缺点:

  • 效率问题,标记和清除过程的效率都不高
  • 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作;

复制算法(Copying)

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点。

现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象 98% 是朝生夕死的,所以并不需要按照 1∶1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间两块较小的 Survivor 空间,每次使用 Eden 和其中的一块 Survivor。

当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地拷贝到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 的空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8∶1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10% 的内存是会被 “浪费” 的。

当然,98% 的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。如果另外一块 Survivor 空间没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。

标记整理算法(Mark-Compact)

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费 50% 的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100% 存活的极端情况,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,有人提出了另外一种 “标记 - 整理”(Mark-Compact)算法,标记过程仍然与 “标记 - 清除” 算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法(Generational Collection)

当前商业虚拟机的垃圾收集都采用 “分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。 一般是把 Java 堆分为新生代老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

  • 新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

  • 老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用 “标记 - 清理” 或 “标记 - 整理” 算法来进行回收。

垃圾收集器

Java 虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。

  • Serial 收集器(复制算法):新生代单线程收集器,标记和清理都是单线程,优点是简单高效。
  • Serial Old 收集器 (标记 - 整理算法):老年代单线程收集器,Serial 收集器的老年代版本。
  • ParNew 收集器 (停止 - 复制算法):新生代收集器,可以认为是 Serial 收集器的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现。
  • Parallel Scavenge 收集器 (停止 - 复制算法):并行收集器,追求高吞吐量,高效利用 CPU。吞吐量一般为 99%, 吞吐量 = 用户线程时间 /(用户线程时间 + GC 线程时间)。适合后台应用等对交互相应要求不高的场景。
  • Parallel Old 收集器 (停止 - 复制算法):Parallel Scavenge 收集器的老年代版本,并行收集器,吞吐量优先
  • CMS (Concurrent Mark Sweep) 收集器(标记 - 清理算法)****:高并发、低停顿,追求最短 GC 回收停顿时间,cpu 占用比较高,响应时间快,停顿时间短,多核 cpu 追求高响应时间的选择

 

1. Serial Garbage Collector

Serial Garbage Collector 通过暂停所有应用的线程来工作。它是为单线程工作环境而设计的。它中使用一个线程来进行垃圾回收。这种暂停应用线程来进行垃圾回收的方式可能不太适应服务器环境。它最适合简单的命令行程序。通过 -XX:+UseSerialGC 参数来选用 Serial Garbage Collector。

2. Parallel Garbage Collector

Parallel Garbage Collector 也被称为吞吐量收集器(throughput collector)。它是 Java 虚拟机的默认垃圾收集器。与 Serial Garbage Collector 不同,Parallel Garbage Collector 使用多个线程进行垃圾回收。与 Serial Garbage Collector 相似的地方时,它也是暂停所有的应用线程来进行垃圾回收。

3. CMS Garbage Collector

Concurrent Mark Sweep (CMS) Garbage Collector 使用多个线程来扫描堆内存来标记需要回收的实例,然后再清除被标记的实例。CMS Garbage Collector 只有在如下两种情景才会暂停所有的应用线程:

  • 当标记永久代内存空间中的对象时;
  • 当进行垃圾回收时,堆内存同步发生了一些变化。

相比 Parallel Garbage Collector,CMS Garbage Collector 使用更多的 CPU 资源来确保应用有一个更好的吞吐量。如果分配更多的 CPU 资源可以获得更好的性能,那么 CMS Garbage Collector 是一个更好的选择。 通过 XX:+USeParNewGC 参数来选用 CMS Garbage Collector。

4. G1 Garbage Collector

G1 Garbage Collector 用于大的堆内存区域。它将堆内存分割成多个独立区域(Region),然后并发地对它们进行垃圾回收。在释放内存后,G1 还可以压缩空闲的堆内存。但是,CMS Garbage Collector 是通过 “Stop The World (STW)” 来进行内存压缩的。G1 优先收集可回收更多内存的区域。Java 8 的改进:在用 G1 Garbage Collector 时,可以开启 -XX:+UseStringDeduplication 参数。它通过将重复的字符串移动到同一个 char 数组中来优化堆内存的使用。该选项在 Java 8u20 时引用进来。 通过 –XX:+UseG1GC 参数来选用 G1 Garbage Collector。

Java 虚拟机中的垃圾回收选项配置

下面是与 Java 收集器相关的 Java 虚拟机选项。

垃圾收集器选择

Option Description
-XX:+UseSerialGC Serial Garbage Collector
-XX:+UseParallelGC Parallel Garbage Collector
-XX:+UseConcMarkSweepGC CMS Garbage Collector
-XX:ParallelCMSThreads= CMS Collector – 使用的线程数
-XX:+UseG1GC G1 Gargbage Collector

垃圾回收优化选项

Option Description
-Xms 堆内存初始化尺寸
-Xmx 堆内存最大尺寸
-Xmn 新生代(Young Generation)的尺寸
-XX:PermSize 永久代(Permanent Generation)初始化尺寸
-XX:MaxPermSize 永久代(Permanent Generation)最大尺寸