一、概述

Java程序运行过程中,会产生很多对象,而这些对象又占用JVM的内存,当程序中不再用到这些对象时,就需要由JVM对内存进行回收。而不同的垃圾回收方式则有不同的性能表现,因此出现了垃圾回收算法。利用这些垃圾回收算法的特性,又实现了多种垃圾回收器。
Java虚拟机运行时数据区——参考《深入理解Java虚拟机》
线程共享数据区:堆、方法区,多线程共享,不确定是否被某个线程所引用对象,所以需要进行垃圾回收。
线程隔离的数据区:程序计数器、虚拟机栈、本地方法栈,绑定在某个线程上,当线程销毁则自动移除,不需要垃圾回收。

二、垃圾判定

当一个对象不被使用时,则认为是需要回收的垃圾对象。

1.引用计数法

为对象添加一个计数器,记录当前对象被引用的次数,当引用次数为0时则认为是垃圾对象。
缺点:当a和b两个对象相互引用时,引用数永远不为0,而a和b这两个对象同时不被其他对象所引用(是垃圾对象),无法正确判断垃圾对象。因为循环引用的存在,所以Java虚拟机不采用此方法。

  1. public class Test {
  2. public Object instance = null;
  3. public static void main(String[] args) {
  4. Test a = new Test();
  5. Test b = new Test();
  6. a.instance = b;
  7. b.instance = a;
  8. a = null;
  9. b = null;
  10. doSomething();
  11. }
  12. }

2.可达性分析法

以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。Java 虚拟机使用该算法来判断对象是否可被回收。
GC Roots 包含:

  • 虚拟机栈中局部变量表中引用的对象
  • 本地方法栈中 JNI 中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象

可达性分析法
如上图所示,Object4做为一个永远不可达的孤岛,被认定为是垃圾对象。

三、垃圾回收算法

1. 标记-清除法

标记-清除法,顾名思义分为两个步骤:先将需要回收的对象打上标记,然后再对标记过的内存进行回收。
缺点:标记、清除过程需要遍历两次内存,效率不高。在回收之后会产生大量不连续的内存碎片,导致分配大对象时没有足够的连续空间。标记-清除法

2. 标记-整理法

在标记-清除法的基础上做优化,在标记-清除之后将所有存活的对象整理到一块儿。
缺点:需要移动大量内存空间,效率较低。
标记-整理法

3. 复制法

顾名思义,复制法是将存活对象复制到另一个内存空间,然后再清除原本的内存空间。
Hotspot中年轻代的Eden和两个Survivor就是采用复制法进行回收,对Eden进行回收时将存活对象复制到Survivor中,然后清空Eden。两个Survivor交替使用,同一时刻只有一个Survivor中存在对象。当发生垃圾回收时,将Survivor From中的存活对象复制到Survivor To中,最后再清空Survivor From的所有对象,如此往复。
复制法(此图有点问题)

4. 分代法

基于Java对象的特征可以区分为两种对象:朝生夕死、长期存活。
针对这种特征,产生了一种分代回收的思想,将堆内存划分为两大块:年轻代、老年代。
年轻代的Eden、Survivor采用了复制法。
老年代采用标记-清除和标记-整理法,通常情况使用标记-清除法,当连续内存空间不足时使用标记-整理法,避免频繁整理耗时。

四、垃圾回收器

image.png
图中存在连线的回收器表示可以搭配使用,其中JDK1.8将Serial+CMS、ParNew+SerialOld这两个组合声明为废弃(JEP173),并在JDK1.9中完全取消了这些组合的支持(JEP214)。
ps:这里不列举低延迟垃圾处理器(Shenandoah、ZGC)以及“无操作”的Epsilon收集器。

1. Serial GC

-XX:+UseSerialGC
开启SerialGC收集器(串行-年轻代-标记-复制)
SerialGC(串行垃圾收集器)使用单线程对年轻代采用复制法进行回收,在CPU核数较少的服务器中非常有用,一般用于内存占用较低的小型应用程序。
SerialGC是最基础、历史最悠久的垃圾收集器,在JDK1.3.1之前是Hotspot虚拟机新生代收集器的唯一选择。至今也是HotSpot在Client模式下默认的新生代收集器。

2. Serial Old GC

-XX:+UseSerialGC
开启SerialOld收集器(串行-老年代-标记-整理)
是SerialGC的老年代版本 ,采用标记-整理法进行回收。可与ParallelScavenge搭配使用。是CMS收集器失败时的备选方案,在并发收集器发生Concurrent Mode Failure时使用。

3. ParNew GC

-XX:+UseParNewGC
开启ParNew收集器(并行-年轻代-标记-复制)
ParNewGC实质上是SerialGC的多线程并行版本,除SerialGC外,只有它能和CMS收集器配合工作进行年轻代垃圾收集。

4. Parallel GC(Parallel Scavenge )

-XX:+UseParallelGC
JDK1.8默认开启Parallel收集器(并行-年轻代-标记-复制)
-XX:MaxGCPauseMillis
设置最大垃圾收集停顿时间
-XX:GCTimeRatio
设置吞吐量大小
-XX:ParallelGCThreads
设置开启的回收线程数
ParallelGC(并行垃圾收集器)与SerialGC是相似的,区别在于它为年轻代的垃圾收集创建了N个线程,其中N不能超过CPU核数。
ParallelGC是一款新生代收集器,基于复制法实现。目标是达到一个可控制的吞吐量。
所谓吞吐量即:GC 垃圾回收 - 图7
并行垃圾收集器也被称为吞吐量垃圾收集器,因为他采用多个CPU来回收年轻代以提高GC效率。
并行垃圾收集器使用单线程回收老年代。
通过java -XX:+PrintCommandLineFlags命令查看jvm默认参数,你会发现JDK1.8默认的垃圾回收器是UseParallelGC。
image.png

5. Parallel Old GC

-XX:+UseParallelOldGC
开启ParallelOld收集器(并行-老年代-标记-整理)
ParallelOldGC是ParallelGC的老年代版本,支持多线程并行收集,基于标记整理法。

6. Concurrent Mark Sweep (CMS) Collector

-XX:+UseConcMarkSweepGC
开启CMS收集器(并行-老年代-标记-清除-整理)
-XX:ParallelCMSThreads=n
设置CMS收集器中的线程数,默认值为GC 垃圾回收 - 图9
-XX:CMSInitiatingOccu-pancyFraction
设置CMS收集器的触发百分比,默认JDK1.6是92%,JDK1.5是68%
-XX:UseCMSCompactAtFullCollection
用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,默认开启
-XX:CMSFullGCsBeforeCompaction
设置n次FullGC后下一次FullGC进行碎片整理,默认是0,JDK1.9后废弃
CMS收集器(并发低停顿收集器)对老年代进行垃圾收集,采用标记清除法。CMS收集器在年轻代上使用与ParallelGC(并行收集器)相同的算法。 适用于不能接受长时间暂停的响应性应用程序,常用基于浏览器的B/S系统的服务端上。
CMS收集器的四个步骤:

  • 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
  • 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
  • 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
  • 并发清除:不需要停顿。

在耗时最长的并发标记和并发清除过程中收集器线程与用户线程一起工作,不需要停顿。
CMS收集器的三个缺点:

  • 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
  • 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
  • 标记 - 清除算法导致的空间碎片,会出现老年代剩余足够的空间,但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次 Full GC。

    7. G1 Garbage Collector (Garbage First)

    -XX:+UseG1GC
    开启G1收集器(并发-全功能的垃圾收集器)
    -XX:MaxGCPauseMillis=200
    设置GC时最大暂停时间
    -XX:G1HeapRegionSize
    设定Region的大小取值为1MB~32MB且为2的N次幂

    7.1 背景

    G1垃圾收集器在JDK1.6开始实验,JDK1.7正式支持,JDK1.8时被称为“全功能的垃圾收集器”,JDK1.9宣布G1取代ParallelGC称为默认服务端收集器(CMS则被声明为Deprecate)。它的长期目标是取代CMS收集器(目前已经实现过半)。
    2004年Sun实验室发表的一篇关于G1的论文,至到2012年4月JDK 7 Update 4发布,将近十年的时间才从一篇论文发展到可商用。

    7.2 基本介绍

    G1收集器是一个并行、并发和增量压缩的低暂停垃圾收集器。G1不像其他收集器那样工作,也没有年轻代和老年代的概念。它将堆空间划分为多个大小相等的堆区域。当调用垃圾收集时,它首先收集存活数据较少的区域,因此称为“garbage first”。
    适用于大内存、多CPU的服务器,适用于追求低延迟的企业级应用系统,内存的分配率或晋升率差异很大的场景。
    G1取消了新生代和老年代的内存隔离,取而代之的是将Java堆划分为若干个区域(Region),这些区域仍然属于分代收集器。
    新生代分为若干个Eden区和Survivor区,将存活对象拷贝到老年代或者Survivor空间。
    老年代分为若干个Old区和Humongous区,Old区用于存放普通的长期存活对象,而Humongous区用于存放大对象(超过一个Region容量的50%),而超过整个Region容量的超大对象将被存放在N个连续的HumongousRegion中。G1的大多数行为都把Humongous作为老年代的一部分来看待。
    G1收集器通过将存活对象从一个区域复制到另外一个区域实现垃圾回收,这样既可以避免内存碎片又提高了回收效率。

    7.3 回收策略

    G1提供了两种GC模式:

    • YoungGC 主要是Eden区进行GC,它在Eden区耗尽时触发,Eden区的存活对象移动到Survivor区,如果Survivor区域空间不够,会直接晋升到老年代空间。Survivor区遵循对象年龄晋升,也有部分数据直接晋升到老年代中。最终Eden区被清空。
    • MixedGC 不仅对新生代进行垃圾回收,同时也回收部分被后台线程扫描标记的老年代区域。

特殊情况(并发模式失败、晋升失败或者疏散失败、巨型对象分配失败)下G1会触发FullGC,这时会退化使用Serial收集器完成垃圾清理工作,它使用单线程,暂停时间将达到秒级。
MixedGC
MixedGC后
将Region作为最小回收单元,每次收集到的内存空间都是Regina大小的整数倍,避免了碎片空间。
G1收集器会跟踪各个Region中的垃圾堆积大小,回收释放的内存空间越大,回收所需花费的时间越少,则认为是越有价值回收的Region。根据回收价值进行优先级排列,每次在用户允许的停顿时间内优先处理回收价值最高的哪些Region(也是Garbage First名称的由来),在有限时间内获取尽可能高的收集效率。

PS:Garbage First收集器有非常多的扩展内容,可以单独开篇讲解。

五、内存分配与回收

当前JVM中堆采用分代法进行回收,回收方式包括两种:

  • Minor GC:仅回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
  • Full GC:回收老年代和新生代,老年代对象存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

    1.强软弱虚引用

    发生垃圾回收时,根据不同的引用级别会有不同的垃圾回收表现。

    1.1强引用

    被强引用关联的对象不会被回收。

    1. Object obj = new Object();

    1.2软引用

    被软引用关联的对象在内存不够的情况下会被回收。

    1. public static void main(String[] args) {
    2. // m是强引用,传入一个10M的byte数组
    3. SoftReference m = new SoftReference<>(new byte[1024 * 1024 * 10]);
    4. // m中的byte数组是软引用
    5. System.out.println(m.get());//输出对象
    6. System.gc();
    7. try {
    8. Thread.sleep(500);
    9. } catch (InterruptedException e) {
    10. e.printStackTrace();
    11. }
    12. // gc后还在
    13. System.out.println(m.get());//输出对象
    14. // 新建一个11M的byte数组,10M+11M超过了最大堆内存20M,回收掉软引用
    15. byte[] b = new byte[1024 * 1024 * 11];
    16. // m中的byte数组被回收了
    17. System.out.println(m.get());//输出为null
    18. }

    1.3弱引用

    被弱引用的对象,一旦遇上GC就会被回收掉。

    1. public static void main(String[] args) {
    2. //创建一个软引用对象WeakReference,引用M类的实例
    3. WeakReference<M> w = new WeakReference<>(new M());
    4. System.out.println(w.get());//输出对象
    5. System.gc();
    6. System.out.println(w.get());//输出为null
    7. }

    1.4虚引用

    又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。可使用 PhantomReference 类来创建虚引用,在java.nio中有应用(堆外内存)DirectByteBuffer中的Cleaner继承了PhantomReference 。

    2.finalize()

    即使在可达性分析算法中被判定为不可达对象也不是立即回收的,这时会先被标记,随后会进行一次筛选,判断对象是否需要执行finalize()方法,若没有重写finalize()方法或者finalize()方法已经被虚拟机调用过,那么就没有必要执行。如果有必要执行finalize()方法,则会被放到F-Queue中,并在稍后由虚拟机自动建立的低调度优先级的Finalizer线程去触发它们的finalize()方法(但不承诺等待方法执行结束)。在finalize()方法中对象与GC Roots引用链上重新建立关联即可存活,但只有一次机会(复活后再次不可达则不能复活)。

    3. 方法区的回收

    -Xnoclassgc
    HotSpot中设置是否对类型进行回收
    -XX:+TraceClass-Loading
    Product版HotSpot虚拟机支持查看类加载信息
    -XX:+TraceClassUnLoading
    FastDebug版HotSpot虚拟机支持查看类卸载信息
    因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。主要是对常量池的回收和对类的卸载。为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。
    类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:

  • 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。

  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

    参考文献

    cyc2018 Java虚拟机篇
    Java (JVM) Memory Model – Memory Management in Java
    Oracle G1GC官方说明
    深入理解 Java G1 垃圾收集器GC调优
    Java引用类型之虚引用PhantomReference
    《深入理解Java虚拟机》第三版