• 如何判断对象是否死亡(两种方法)。
  • 简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。
  • 如何判断一个常量是废弃常量
  • 如何判断一个类是无用的类
  • 垃圾收集有哪些算法,各自的特点?
  • HotSpot 为什么要分为新生代和老年代?
  • 常见的垃圾回收器有哪些?
  • 介绍一下 CMS,G1 收集器。 Minor Gc 和 Full GC 有什么不同呢?

一、垃圾标记阶段:对象存活判断

1 判断对象存活的算法

如何判断对象是否死亡?

  • 那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
  • 判断对象存活一般有两种方式:引用计数算法和可达性分析算法

1.1 引用计数算法

什么事引用计数算法?

  • 引用计数算法(Reference Counting)比较简单 ,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况

  • 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。

优缺点?

  • 实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
  • 缺点:增加额外的空间开销和时间开销,并且 无法解决循环引用问题

1.2 可达性分析算法

可达性分析算法的实现思路?
  • 这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,按照 从上至下 的方式 搜索被根对象集合所连接的目标对象是否可达

  • 使用可达性分析算法之后,内存中存活的对象都会被根对象集合直接或者间接连接,搜索走过的路径叫做 引用链

  • 如果目标对象没有任何引用链相连,则表示不可达,意味着该对象已经死亡,可以标记为垃圾对象。

  • 在可达性分析算法中,只有能够被根对象激活直接或间接连接的对象才是存活对象。

GC Roots的对象包含哪些?(重要)
  • 虚拟机栈引用的对象
    • 各个线程被调用的方法中的参数,局部变量
  • 本地方法栈(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象(jdk1.7之前,jdk1.7以及之后类变量在堆中,字符串常量池也是
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象
  • 虚拟机的内部引用
    • 基本数据类型的包装类,常驻的异常对象,系统类加载器
  • 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

其它可能的GC Roots (重要)

  • 除了这些固定的GCRoots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收
    • 比如对新生代进行垃圾回收,那么非新生代入如老年代,也应该被看作是GC Roots
  • 小技巧:由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。

注意事项

  • 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。不能垃圾回收的时候,引用还在发生变化。

  • 这点也是导致GC进行时必须 **“StopTheWorld"**的一个重要原因。

    • 即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的

1.3 finalization机制

什么是 finalization机制 ?
  • Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
  • 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法
  • finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
能否主动调用 对象的finalize()方法?

永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点:

  1. 在finalize()时可能会导致对象复活。
  2. finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
  3. 一个糟糕的finalize()会严重影响GC的性能。
虚拟机中对象的三种状态(可触及、可复活、不可触及)
  • 由于finalize ()方法的存在,虚拟机中的对象一般处于三种可能的状态
    • 可触及的:从根节点开始,可以到达这个对象。不是垃圾。
    • 可复活的:对象的所有引用都被释放(对象不可达),但是对象有可能在finalize()中复活。
    • 不可触及的对象不可达,并且对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及 的对象不可能被复活,因为 finalize()只会被调用一一次
  • 以上3种状态中,是由于finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。

1.4 判定对象是否可以回收的具体过程(重要)

判定一个对象是否可回收,至少要经过两次标记过程

  • 第一步:如果对象objA到GC Roots没有引用链,则进行第一次标记。对象不可达。

  • 第二步:进行筛选,判断此对象是否有必要执行finalize()方法

    • 1)如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为 不可触及 的。
    • 2) 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到 F-Queue队列 中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。
      • 可以在 jvisual 中查看
    • 3) finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合。
    • 4)之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize()方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize()方法只会被调用一次

2 再谈引用

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

JDK1.2以后对引用概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用,4中引用强度依次减弱。

  • 强引用:无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。强引用所指向的对象在任何情况下都不会被回收,JVM宁愿抛出OOM,也不会回收强引用所指向的对象。所以,强引用是造成Java内存泄漏的主要原因之一
  • 软引用(SoftReference)
    • 软引用是用来描述一 些还有用,但非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
      • 第一次回收的是不可达的对象。
      • 非强引用是不会导致内存溢出的,因为被回收了
    • 软引用通常用来实现内存敏感的缓存
  • 弱引用(WeakReference)
    • 只要GC就回收,发现即回收
    • 弱引用也是用来描述那些非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止
    • 弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收
  • 虚引用(PhantomReference)
    • 与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收
    • 虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虛引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。

二、垃圾收集算法

当成功区分出内存中存活对象和死亡对象之后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的空间。目前比较常用的算法有三种

  • 标记清除算法
  • 复制算法
  • 标记压缩算法

1 标记 – 清除(Mark-Sweep)算法(空闲列表)

标记 – 清除(Mark-Sweep)算法执行流程
当堆中的有效内存空间被耗尽时,就会停止整个程序(也称为Stop The World),然后进行两项工作,第一项是标记工作,第二项是清除工作。

  • 标记:Collector从引用的根节点开始遍历,标记所有的被引用的对象。一般是在对象的对象头Header中记录为可达对象
  • 清除:Collector对堆内存从头到尾进行线性遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收

优缺点

  • 优点:常用、简单
  • 缺点
    • 效率不算高(两次O(n))
    • 在进行GC的时候,需要停止整个应用程序,导致用户体验差
    • 这种方式清理出来的 空闲内存是不连续的,产生内存碎片需要维护一个空闲列表

空闲列表

标记 – 清除(Mark-Sweep)算法中的清除并不是真的置空,而是 把需要清除的对象地址保存在空闲的地址列表里 。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。当分配内存时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录

2 复制算法

复制算法执行流程

为了解决效率问题,“标记-复制”收集算法出现了。它可以 将内存分为大小相同的两块 ,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

优缺点

  • 优点
    • 没有标记和清除过程,实现简单,运行高效
    • 复制过去以后保证空间连续性不会出现“碎片”问题
  • 缺点
    • 此算法的缺点也是很明显的,就是需要两倍的内存空间。
    • 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
    • 特别的 如果系统中的可用对象很多,复制算法不会很理想。因为复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。

复制算法在新生代的使用

  • 在新生代,对常规应用的垃圾回收,一次通常可以回收70% – 99%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
  • from区和to区的对象都是 朝生夕死 的,兼顾了复制算法的优缺点。

3 标记 – 压缩算法(指针碰撞)

执行流程

  • 第一阶段和 标记 – 清除 算法一样,从根节点开始标记所有被引用对象
  • 第二阶段 将所有的存活对象压缩到内存的一端,按顺序排放
  • 之后,清理边界外所有的空间

补充说明

  • 标记 – 压缩算法 的最终效果等同于 标记 – 清除算法 执行完成后,再进行一次内存碎片整理,因此,也可以把它称为 标记 – 清除 – 压缩(Mark一 Sweep一Compact)算法。
  • 二者的本质差异在于标记 - 清除算法是一种非移动式的回收算法标记 - 压缩算法是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。
  • 可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

指针碰撞(Bump the Pointer )- 分配新内存

如果内存空间以规整和有序的方式分布,即已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式就叫做指针碰撞(Bump the Pointer) 。

优缺点

  • 优点
    • 消除了 标记 - 清除算法 当中内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
    • 消除了复制算法当中,内存减半的高额代价。
  • 缺点
    • 从效率上来说,标记 – 整理算法要低于复制算法。
    • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
    • 移动过程中,需要全程暂停用户应用程序。即: STW

4 分代收集算法

虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想, 只是根据对象存活周期的不同将内存分为几块 。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

延伸面试问题: HotSpot 为什么要分为新生代和老年代?

5 总结

标记清除算法复制算法标记压缩算法
时间复杂度中等最快最慢
空间复杂度少(堆积碎片)占用2倍(不堆积碎片)少(不堆积碎片)
内存碎片
移动对象
  • 效率上看,复制算法是当之无愧的老大,但浪费了大量的内存
  • 标记 – 整理算法比赋值算法多了一个标记阶段,比标记 – 清除算法 多了一个整理内存阶段

三、垃圾收集器

  • PaeNew + Serial Old 在JDK1.9被移除
  • PaeNew + CMS 在JDK10中CMS被移除
  • ParNew已成了孤家寡人,没必要用它了
  • CMS, 9废弃,14移除

1 新生代垃圾收集器

1.1 Serial 收集器

  • 它是一个单线程收集器了。Serial收集器采用复制算法、串行回收和"Stop-the-World"机制的方式执行内存回收。
  • 它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。

1.2 ParNew 收集器

  • ParNew收集器除了采用 并行回收 的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。
    • 年轻代中同样也是采用 复制算法
    • 老年代同样采用 标记压缩算法
    • “Stop- the-World”机制。
  • 它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

由于ParNew收集器是基于并行回收,那么是否可以断定ParNew收集器的回收效率在任何场景下都会比Serial收集器更高效?

  • ParNew 收集器运行在多CPU的环境下,由于可以充分利用多CPU、 多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量。
  • 但是在单个CPU的环境下,ParNew收 集器不比Serial收集器更高效。 虽然Serial收集器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。

设置使用ParNew

  • 通过选项-XX:+UseParNewGC手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
  • -XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数。

1.3 Parallel Scavenge 收集器(吞吐量优先)

  • 年轻代中同样也是采用 复制算法
  • 老年代同样采用 标记压缩算法
  • “Stop- the-World”机制。

Parallel Scavenge收集器的目标是 达到一个可控制的吞吐量(Throughput) ,它也被称为吞吐量优先的垃圾收集器。那么它和ParNew有什么区别?

  • 自适应调节策略 也是Parallel Scavenge 与ParNew一个重要区别。

  • 高吞吐量则可以高效率地利用CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些**执行批量处理、订单处理、工资支付、科学计算**的应用程序。

  • 在程序 吞吐量优 先的应用场景中, Parallel 收集器和Parallel 0ld收集器 的组合,在 Server模式 下的内存回收性能很不错。

  • 在Java8中,默认是此垃圾收集器

2 老年代垃圾收集器

2.1 Serial Old 收集器

  • 除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial 0ld收集器。 Serial Old收集器同样也采用了串行回收 和”Stop the World”机制,只不过内存回收算法使用的是 标记压缩算法
  • Serial 0ld是运行在**Client模式**下默认的老年代的垃圾回收器。
  • Serial 0ld在服务端(Linux)下主要有两个用途 – 见2.4
    • 与新生代的ParallelScavenge配合使用;
    • 作为老年代CMS收集器的后备垃圾收集方案

2.2 Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

2.2 CMS收集器(重要,以最短停顿时间为目标)

介绍一下CMS?
  • CMS(Concurrent Mark Sweep)收集器是一种以 获取最短回收停顿时间为目标 的收集器。它非常符合在注重用户体验的应用上使用。
  • CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
CMS工作流程

CMS执行流程主要有4个主要阶段

  • 初始标记暂停所有的其他线程,并记录下与 GC Root 直接相连的对象(只标记第一层,不是标记全部),速度很快 ;
  • 并发标记(Concurrent一Mark)阶段从GC Roots的 直接关联对象开始遍历整个对象图 的过程,这个过程 耗时较长 但是 不需要停顿用户线程,可以与垃圾收集线程一起并发运行。但是因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从”跟对象”开始向下追溯,并处理对象关联
  • 并发清除( Concurrent一Sweep)阶段:此阶段 清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于 不需要移动存活对象 ,所以这个阶段也是可以与用户线程同时并发的。
补充说明
  • CMS无法清理浮动垃圾:浮动垃圾指的是 并发标记和并发清理阶 段产生的新垃圾。
    • 假如并发标记阶段可达的对象变为不可达,那么该对象就成为了浮动垃圾,重新标记阶段必然无法处理
  • 尽管CMS收集器采用的是并发回收(非独占式),但是 在其初始化标记和再次标记这两个阶段中仍然需要执行“Stop一the一World”机制暂停程序中的工作线程
  • CMS收集器的垃圾收集算法采用的是 标记清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间 极有可能是不连续的一些内存块不可避免地将会产生一些内存碎片
为什么CMS解决不了浮动垃圾?

参考:https://blog.csdn.net/z69183787/article/details/111091898

优缺点
  • 优点:并发收集、低停顿
  • 缺点
    • 对 CPU 资源敏感;
    • 无法处理浮动垃圾;
    • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

3 Garbage First (G1)GC – 区域化分代式

3.1 G1 概述

介绍一下G1?
  • 因为G1是一个 并行回收器它把堆内存分割为很多不相关的区域(Region) (物理上不连续的) 。使用不同的Region来表示Eden、幸存者0区,幸存者1区,老年代等。
  • G1 GC有计划地避免了整堆回收。G1跟踪各个Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间),在后台维护一个优先列表,每次 根据允许的收集时间,优先回收价值最大的Region
  • 由于这种方式的 侧重点在于区间中垃圾数目,所以我们给G1一个名字:垃圾优先(Garbage First)
  • G1 (Garbage一First) 是一款面向服务端应用的垃圾收集器,主要针对配备 多核CPU及大容量内存的机器以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。
优缺点
  • 优点
  • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
  • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
  • 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
  • 可预测的停顿时间模型:G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
  • 缺点
    • 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint) 还是程序运行时的额外执行负载(overload) 都要比CMS要高。
    • 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用,上则发挥其优势。平衡点在6-8GB之间。
G1简单调优
  • 第一步:开启G1垃圾收集器
  • 第二步:设置堆的最大内存
  • 第三步:设置最大的停顿时间

3.2 G1 执行流程

3.2.1 RememberSet

一个对象被不同区域引用的问题

  • 一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?

    • 比如:Old区引用了Eden区,那么GC时遍历完年轻代后是否还需要遍历Old区寻找GCRoots,回收新生代也不得不同时扫描老年代?
  • 在其他的分代收集器,也存在这样的问题(而G1更突出),这样的话会降低Minor GC的效率。

解决方法 – RememberSet

  • 无论G1还是其他分代收集器,JVM都是使用Remembered Set 来避免全局扫描

  • 每个Region都有一个对应的Remembered Set;

  • 每次Reference类型数据写操作时,都会产生一个Write Barrier(写屏障) 暂时中断操作;

  • 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region (其他收集器:检查老年代对象是否引用了新生代对象) ;

  • 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;

  • 当进行垃圾收集时,在GC Roots的枚举范围加入Remembered Set ,就可以保证不进行全局扫描,也不会有遗漏。

3.2.2 执行流程
  • 年轻代GC (Young GC / Minor GC)

  • 老年代并发标记过程( Concurrent Marking),同时伴随年轻代GC

  • 混合回收(Mixed GC )

  • 如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。

具体回收流程

G1 回收过程,G1 回收器的运作过程大致可分为四个步骤:

  1. 初始标记(会STW) :仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并 没有额外的停顿
  2. 并发标记 :从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理在并发时有引用变动的对象。
  3. 最终标记(会STW) :对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象。
  4. 清理阶段(会STW) :更新Region的统计数据, 对各个Region的回收价值和成本进行排序 ,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,必须暂停用户线程,由多条回收器线程并行完成的。

补充知识

1 System.gc() 的理解

System.gc() 的理解

  • 在默认情况下,手动调用System.gc()或者RunTime.getRunTime().gc(),会显式出发 FullGC 同时对新生代和老年代进行回收,尝试释放垃圾。
  • 然而System.gc()调用附带一个免责声明无法保证对垃圾收集器的调用(无法保证马上触发GC)
  • JVM实现者可以通过system.gc()调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()。

2 内存溢出&内存泄漏

2.1 内存溢出(OOM)

什么是OOM?

  • javadoc中对OutOfMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存
  • 由于GC一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现O0M的情况。

内存没有空闲的情况分析

1.Java虚拟机的堆内存设置不够。

  • 比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理
  • 比如我们要处理比较可观的数据量,但是没有显式指定JVM堆大小或者指定数值偏小。我们可以通过参数一Xms、一Xmx来调整。

2.代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)

在抛出0utOfMemoryError之 前,通常垃圾收集器会被触发,尽其所能去清理出空间

  • 在抛出0utOfMemoryError之 前,通常垃圾收集器会被触发,尽其所能去清理出空间。
  • 但也不是在任何情况下垃圾收集器都会被触发的
    • 比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接拋出OutOfMemoryError

2.2 内存泄漏(Memory Leak) – 重要

什么是内存泄漏?

  • 严格来说只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏
  • GC Roots集合所连接的对象中有一些我们不想用了,但是又忘记断开了他们的引用,那么也无法被垃圾回收,就造成了内存泄漏
  • 但实际情况很多时候一些不太好的实践(或疏忽)会导致 对象的生命周期变得很长甚至导致内存溢出OOM ,也可以叫做宽泛意义上的“内存泄漏

内存泄漏举例(重要)

因为HotSpot虚拟机使用的是可达性分析算法,因此举引用计数算法的循环引用是不恰当的

  • 举例
    • 单例模式:单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
    • 一些提供close的资源未关闭导致内存泄漏 :如:数据库连接( dataSourse. getConnection()),网络连接(socket)和io连接必须手动close,否则是不能被回收的。

3 STW

Stop – the – World,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。

  • 可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。
    • 分析工作必须在一个能确保 一致性 的快照中进行,确保数据的一致性
    • 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
    • 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证

被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样, 所以我们需要减少STW的发生。

4 安全点与安全区域

4.1 安全点

什么是安全点?

  • 程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safepoint) ”
  • Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题

如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?

  • 抢先式中断: (目前没有虚拟机采用) 首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
  • 主动式中断: 设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。

4.2 安全区域

什么是安全区域?

Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint 。但是,程序“不执行”的时候呢?例如线程处于Sleep 状态或Blocked状态,这时候线程无法响应JVM的中断请求,“走” 到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。 我们也可以把Safe Region 看做是被扩展了的Safepoint。

执行流程

  1. 当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程;
  2. 当线程即将离开Safe Region时, 会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开SafeRegion的信号为止;

5 WeakHashMap – TODO

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

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :

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

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

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

运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?

  1. JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区的实现为永久代
  2. JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代 。
  3. JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

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

8 并行&并发

并行与并发概念

并发,指多个事情在同一时间段内同时发生了

并行,指多个实行在同一时间点同时发生了

并发的多个任务之间是互相抢占资源的

并行的多个任务之间是不互相抢占资源的

只有在多个CPU或一个CPU多核的情况下才是并行。否则都是并发。

垃圾回收器的并发与并行

  • 并行(Parallel) :指多条垃圾收集线程并行工作,但**此时用户线程仍处于等待状态**。

    • 如 ParNew、 Parallel Scavenge、 Parallel 0ld;
  • 串行(Serial)

    • 相较于并行的概念,单线程执行。
    • 如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收。回收完,再启动程序的线程。
  • 并发(Concurrent) :指**用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行**。

    • 用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上

GC的分类与性能指标 – TODO