JVM之垃圾回收机制

Posted by Pismery Liu on Friday, January 18, 2019

TOC

垃圾回收机制

JVM 内存分配和回收策略

JDK1.8 前堆内存示意图

如上图,堆内存分为新生代、老年代,永久代;而新生代又分为Eden, Survivor 1, Survivor 2; 注意:永久代在JDK1.8 后被整个移除,替换为元空间(Metaspace);

元空间与永久代的区别:元空间使用的是物理内存,直接受机器本身的物理内存限制;永久代使用的是JVM中堆内存的空间;两者都会出现OOM异常;

堆内存常见分配策略

对象优先在 Eden 区分配

在JVM中,存在不同类型的对象,如:存活时间特别短的方法中的局部对象,存活时间较长的对象,静态变量,常量等等;为了根据不同对象的特点选择合适的垃圾收集算法。现在主流的垃圾收集器都采用分代回收算法;这就是为什么需要将堆内存拆分为新生代、老年代,甚至新生代再分为一个Eden和两个Survivor区。注意:JDK11出现了一个实验性的新的垃圾回收器 ZGC 取消了分代的概念;

Minor GC 与 Full GC:

  • 新生代GC(Minor GC):指发生在新生代的GC,其特点是GC非常频繁,回收速度一般比较快;
  • 老年代GC(Major GC/Full GC):指发生在老年代的 GC,Full GC 会伴随至少一次的 Minor GC (非绝对),速度一般是比 Minor GC 慢 10 倍;

分配担保机制:把新生代的对象提前转移到老年代中去,以分配足够新生代空间给新的对象;

大对象直接进入老年代

大对象:需要大量连续空间的对象,如字符串,数组;

这个机制的目的是,避免大对象分配内存时,由于分配担保机制导致将对象提前复制到老年代,以提供大对象在新生代的空间。因此通过这个机制,避免了分配担保机制操作,减少复制带来的性能消耗。

长期存活的对象将进入老年代

如果对象在 Eden 区分配内存,JVM 通过给对象一个计数器来标记对象的年龄。当发生第一次 Minor GC 时,仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1 ;此后,每经过一次 Minor GC,对象年龄就加一,直到到达「年龄阀值」就晋升到老年代;「年龄阀值」可通过 -XX:MaxTenuringThreshold 来设置,默认是15;;

注意,并不是要求所有对象年龄到达「年龄阀值」才能进入老年代;如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到要求的年龄。这样的目的是更好适应各种内存情况;

判断对象是否死亡

判断对象是否死亡有两种方法:引用计数法,可达性分析法

引用计数法

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

引用计数法,实现简单,效率高,但是它很难解决对象相互循环引用的问题。因此主流的JVM都没有采用这种方法;

可达性分析法

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

引用

JDK1.2 之前引用的定义:如果reference类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。

JDK1.2 之后将引用分为:强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)

强引用

我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用;如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空 间不足,JVM 宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

软引用(SoftReference)

如果一个对象只具有软引用,如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

软引用可用来实现内存敏感的高速缓存。

弱引用(WeakReference)

与软引用相似,但是只要垃圾回收器发现弱引用对象,不管内存足够与否,都会回收对象。只是由于垃圾回收器是一个优先级很低的线程,不一定会很快发现那些只具有弱引用的对象。

同样弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

虚引用(PhantomReference)

虚引用,形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

虚引用与软引用和弱引用的一个区别在于:虚引用必须和「引用队列」(ReferenceQueue)联合使用。

虚引用主要用于跟踪对象被垃圾回收的活动。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的「引用队列」中。程序可以通过判断「引用队列」中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到「引用队列」,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

使用情况:在程序设计中,很少使用弱引用和虚引用;主要使用软引用,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

不可达的对象并非“非死不可”

在可达性分析法中不可达的对象,也并非是“非死不可”的;当第一只发现不可达,这时候对象处于缓刑阶段,要真正宣告一个对象死亡,至少要经历两次标记过程;执行流程大概如下:

  1. 对象第一次不可达,对对象进行一次标记;
  2. 检测对象是否需要执行 finalize 方法,若对象没有覆盖 finalize 方法或者 JVM 已经调用过 finalize 方法,则不需要执行;否则将需要执行 finalize 方法的对象加入 F-Queue 队列,使用一个低优先级的线程去执行对象的 finalize 方法;
  3. 执行完 finalized 方法,检测对象进行第二次标记,若对象已经可达,则对象复活;若对象仍然不可达,则回收对象;

垃圾收集算法

「标记-清除」算法

算法分为两个步骤:

  1. 标记出所有需要回收的对象
  2. 标记完成后统一回收所有被标记的对象

这是最基础的收集算法,但是有明显的空间问题,标记清除后带来大量的空间碎片;

示意图

复制算法

复制算法是将内存空间分为相同大小的两份,操作步骤如下:

  1. 每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去;
  2. 然后把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

算法的优点是效率比较高,逻辑简单;但是有一个明显的缺点就是需要2倍的内存空间;

示意图

「标记-整理」算法

「标记-整理」算法是根据老年代的对象特点而设计的算法;操作步骤与「标记-清除」算法类似,步骤如下:

  1. 标记出所有需要回收的对象
  2. 让所有存活的对象向一端移动
  3. 直接清理掉端边界以外的内存

算法的优点是不会产生空间碎片,有利于大对象的内存空间分配;确定是效率相对比较低;

示意图

分代收集算法

目前,流行的 JVM 垃圾收集算法均使用分代收集算法,即根据对象存活周期,大小等特点,将堆内存分为几个部分,对每一个部分分别使用相应合适的垃圾收集算法;

如在新生代中,一般都是存活周期短的小对象,每次都有大量对象需要被回收;所以采用复制算法,每次只需复制少量的存活对象就能完成垃圾回收;在老年代中,一般都是存活周期长的大对象,而且没有额外的空间对它分配担保;因此,必须采用「标记-清除」或「标记-整理」算法。

垃圾收集器

如果上述的收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现了。

首先,需要明确的一点是,目前没有一个放之四海而皆准的最好的收集器,我们需要了解各类收集器的特点,面对不同的应用程序,选择合适的收集器;

Serial 收集器

Serial(串行)收集器,这个收集器是一个「单线程」收集器;「单线程」不单单意味着只有一个线程来进行内存回收工作,同时它在工作时,其他所有的工作线程都要停止工作,即 Stop the world!!!

Serial(串行)收集器:新生代采用复制算法;老年代采用「标记-整理」算法;

因此,对于服务端程序来说,用户体验并不好;其适用场景可以是运行在Client模式下的虚拟机;

示意图

补充

以及还有 Serial Old 收集器,这个收集器是 Serial 收集器的老年代版本,它同样是一个单线程收集器。其主要用途如下:

  1. 在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用
  2. 作为CMS收集器的后备方案。

ParNew 收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了内存回收时是多线程运行以外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。

它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

ParNew 收集器:新生代采用复制算法;老年代采用「标记-整理」算法;

示意图

Parallel Scavenge 收集器

Parallel Scavenge 收集器设计的关注点是「吞吐量」,所谓「吞吐量」指CPU中用于运行用户代码的时间与CPU总消耗时间的比值;而其他的如 CMS 收集器等都是主要关注用户线程的停顿时间,其目的是提高用户体验;

Parallel Scavenge 收集器:新生代采用复制算法;老年代采用「标记-整理」算法;

启用参数如下:

使用Parallel收集器+ 老年代串行
-XX:+UseParallelGC 

//使用Parallel收集器+ 老年代并行
-XX:+UseParallelOldGC 

示意图

补充

Parallel Old 收集器(Parallel Scavenge收集器的老年代版本),在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。

CMS 收集器

CMS (Concurrent Mark Sweep) 收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程「基本上」同时工作。其设计目标是获取最短回收停顿时间;

从名字 Concurrent Mark Sweep 就能看出这是一个「标记-清除」算法,请留意前面的收集器都采用的是「标记-整理」算法;其执行步骤如下:

  1. 初始标记:暂停所有的其他线程,并记录下直接与root相连的对象,速度很快;
  2. 并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象。但是由于用户线程可能会不断的更新引用域,导致GC线程无法保证可达性分析的实时性;因此,这个算法里会跟踪记录这些发生引用更新的地方;
  3. 重新标记:为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短;
  4. 并发清除:开启用户线程,同时GC线程开始对为标记的区域做清扫。

从执行步骤可以看出,为什么前面说 CMS 收集器实现了与用户线程「基本上」同时工作;

CMS 收集器优点,并发收集,停顿时间短,用户体验好;同时也有以下缺点:

  1. 对 CPU 资源敏感,即容易耗尽 Cpu 资源;
  2. 由于使用「标记-清除」算法,所有会产生大量空间碎片;
  3. 不能处理「浮动垃圾」;

「浮动垃圾」指,由于并发标记过程中,是与用户线程一起运行的,而用户线程运行过程中,可能又会产生新的不可达对象。这个新的不可达对象就是「浮动垃圾」;而CMS 收集器不能同一次内存回收中清除「浮动垃圾」,需要等待下次内存回收才能清除这个不可达对象。

示意图

G1 收集器

G1(Garbage-First) 收集器是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。在 JDK1.7 中发布,其主要特定如下:

  1. 并行与并发:充分利用多核环境减少停顿时间;
  2. 分代收集:不需要配合其它收集器;
  3. 空间整合:整体上看属于标记整理算法,局部(region之间)数据复制算法,运作期间不会产生空间碎片;
  4. 停顿可预测:建立可以预测的停顿时间模型;

G1 收集器执行步骤大致分为:

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

其原理是维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也是 Garbage-First 名字的由来);这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率

面试题

如何判断对象是否死亡(两种方法)。

  1. 使用引用计数法,当引用计数器为零时,表示对象死亡;其主要缺点是难以解决循坏引用问题;
  2. 使用可达性分析算法,从对象向上寻找,若找不到任何到 GC Roots 的对象的引用链,则表示对象死亡;

简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。

  • 强引用:日常使用的就是强引用;内存不足时,JVM 宁可 OOM 也不会回收强引用对象;
  • 软引用:当内存充足时,JVM不会回收软引用对象;但是当内存不足时,会回收;
  • 弱引用:JVM 发现就会回收,由于 GC 线程的优先级很低,所以会存在一定时间;
  • 虚引用:完全虚有,JVM 直接视为没有引用;必须引用队列(ReferenceQueue)联合使用,主要用于跟踪对象被垃圾回收的活动;

如何判断一个常量是废弃常量

假如在常量池中存在字符串 “abc”,如果当前没有任何String对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,”abc” 就会被系统清理出常量池。

注意:JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池

如何判断一个类是无用的类

满足以下三个条件的就是无用的类:

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

注意:JVM 仅仅是可以对无用的类进行回收,并不是和对象一样不使用必然被回收;

垃圾收集有哪些算法,各自的特点?

「标记-清除」算法:最基本的收集算法;缺点:产生空间碎片; 「复制」算法:效率比较高,主要用于新生代的算法;缺点:需要两倍的内存空间; 「标记-整理」算法:主要用于老年代的算法,不会产生空间碎片,不适用于经常回收对象的场景;

HotSpot为什么要分为新生代和老年代?

JVM 中存在这不同类型的对象,如存活时间短的小对象和存活时间长的大对象,将不同类型的对象分开区域,采用相应的回收算法,能够更加方便高效处理多种应用场景。

常见的垃圾回收器有那些?

  • Serial 收集器
  • ParNew 收集器
  • Parallel Scavenge 收集器
  • CMS 收集器
  • G1 收集器

介绍一下 CMS, G1 收集器

如文。。。。

Minor GC 和 Full GC 有什么不同呢?

  • Minor GC 指发生在新生代的 GC; 其特点是频率高,速度快;
  • Full GC 指发生在老年代的 GC; 总是伴随至少一次的Minor GC;其特点是速度慢;

参考链接