【JVM学习笔记】GC算法与垃圾收集器

3.1 概述

为什么要做GC

当需要排查各种内存溢出,、内存泄漏问题时,当垃圾回收成为系统达到更高并发量的瓶颈时,就需要对这些 “自动化” 的技术实施必要的监控和调节。

3.2 对象已死吗

在堆里面存放着 Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事就是确定这些对象之中哪些还【存活】着,哪些已经【死去】。

3.2.1 引用计数算法

概述:给对象添加一个引用计数器,每当有一个地方引用它时,计数器的值就加 1;当引用失效时,计数器的值就减一;任何时刻计数器为 0 的对象就是不可能再被使用的。【会出现循环引用问题】

循环引用:两个对象之间相互引用,除此之外,这两个对象再任何引用,实际上这两个对象已经被可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是计数算法无法通知 GC 收集器回收它们。

3.2.2 可达性分析算法

算法基本思路:通过一系列称为【GC Roots】的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,将会被回收。

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

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI (即一般说的 Native 方法)引用的对象
3.2.3 再谈引用

在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种,这四种引用强度依次逐渐减弱。

  • 强引用就是指在程序代码之中普遍存在的,类似于【Object obj = new Object()】这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用是用来描述一些还有用但非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列在回收范围内进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被软引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
3.2.4 生存还是死亡

即使在可达性算法中不可达的对象,也不是非死不可的,这时候它们暂时处于【缓刑】阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为【没有必要执行】。

如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的【执行】是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,

3.2.5 回收方法区

在方法区中进行垃圾收集的性价比很底;在堆中,尤其是在新生代中,常规应用进行一次垃圾回收一般可以回收70% ~ 95%的空间,而永久代的垃圾收集效率远低于此。

判定一个类是否可以被回收的条件:

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

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

算法不足:

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

为了解决标记清除中存在的效率问题,一种被称为【复制】的收集算法出现了,它将内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,但是代价过高,因为它将内存缩小为了原来的一半。

Eden : Survivor = 8 : 1

3.3.3 标记 - 整理算法

复制收集算法在对象存活率比较高时就要进行较多的复制操作,效率将会变低。

标记- 整理算法与标记 - 清除算法一样,但后续步骤不是直接对可回收对象进行整理,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

3.3.4 分代收集算法

这种算法是根据对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特带你采用最适合的收集算法。在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集,而老年代中因为对象存活率高,没有额外空间对它进行担保,就必须使用【标记 - 清除】或【标记 - 整理】算法来进行回收。

3.4 HotSpot 的算法实现
3.5 垃圾收集器
3.5.1 Serial 收集器

这是一个单线程的收集器,但它的 “单线程” 的意义并不仅仅说明它只会使用一个 CPU 或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

其优点有:简单而高效,对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

3.5.2 ParNew 收集器

这个收集器其实就是 Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括和 Serial 收集器完全一样。

ParNew 收集器在单 CPU 的环境中绝对不会有比 Serial 收集器更好的效果,甚至由于线程交互的开销,该收集器在通过超线程计数实现的两个 CPU 的环境中都不能百分百地保证可以超越 Serial 收集器。

3.5.3 Paraller Scavenge 收集器

CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量。所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值。

Parallel Scanvenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的 --XX:MaxGCPauseMills 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数

  • MaxGCPauseMills 参数允许的值是一个大于 0 的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值,不过随着停顿时间下降,吞吐量也降下来了。GC 停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。
  • GCTimeRatio 参数的值应当是一个大于 0 且小于 100 的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。
3.5.4 Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用【标记 - 整理】算法。这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。

3.5.5 Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和【标记 - 整理】算法。

3.5.6 CMS 收集器

CMS(Concurrent Mark-Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。是基于【标记 - 清除】算法实现的。

整个过程分为四个步骤:

  • 初试标记
  • 并发标记
  • 重新标记
  • 并发标记

初始标记仅仅是标记一下 GC Roots 能直接关联到的对象,速度很快,并发标记阶段就是进行 GC Roots Tracing 的过程,而重新标记阶段则是为了修正并发标记期间用户程序1继续运作而导致标记产生变动的那一部分对象的标记记录。

CMS 收集器的内存回收过程是与用户线程一起并发执行的。整个过程中耗时最长的并发标记和并发清楚过程线程都可以与用户线程一起工作。

优点:并发收集、低停顿

缺点:

  • CMS 收集器对 CPU 资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用一部分线程或者说是 CPU 资源,而导致应用程序变慢,总吞吐量会降低
  • CMS 收集器无法处理浮动垃圾。可能会出现 “Concurrent Mode Failure” 失败而导致另一次 Full GC 的产生。由于 CMS 并发清理阶段用户线程还在运行着,伴随着程序自然运行就还会有垃圾不断产生,只能留待下一次 GC 时再清理掉,这一部分垃圾就称为 “浮动垃圾”
  • 收集结束时会有大量空间碎片产生。CMS 是一款基于【标记 - 清除】算法实现的收集器,空间碎片过多时,会给分配对象带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次 Full GC
3.5.7 G1 收集器

G1 是一款面向服务端应用的垃圾收集器。

与其它 GC 收集器相比,G1 具备如下特点:

  • 并行和并发执行。G1 能够充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU 来缩短 Stop-The-World 停顿的时间,部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行
  • 分代收集。
  • 空间整合。与 CMS 的【标记 - 清理】算法不同,G1 从整体上来看是基于【标记 - 整理】算法实现的收集器,从局部来看是基于【复制】算法实现的
  • 可预测的停顿。G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能然使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒

使用 G1 收集器时,Java 堆的内存布局就与其他收集器有很大区别,它将整个 Java 堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但它们不再是物理隔离的了,它们都是一部分 Region 的集合。

G1 收集器之所以能够建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。

把 Java 堆分为多个 Region,垃圾收集就真的能以 Region 为单位进行了?

在 G1 收集器中。Region 之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用 Remenbered Set 来避免全堆扫描的。G1 中每个 Region 都有一个与之对应的 Remembered Set,虚拟机发现程序在对 Reference 类型的数据进行写操作时,会产生一个 Write Barrir 暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中(在分代的例子中就是检查是否老年代的对象引用了新生代中的对象),如果是,便通过 CardTable 把相关信息记录到被引用对象所属的 Region 的 Remenbered Set 之中,当进行内存回收时,在 GC 根节点的枚举范围内中加入 Remembered Set 即可保证不对全栈扫描也不会有遗漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值