JVM02——垃圾回收

一、垃圾回收概述

什么是垃圾?

垃圾就是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。

如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出

Java自动内存管理

  • 自动内存管理,不需要参与内存的分配与回收,这样可以降低内存的泄露和内存溢出的风险
  • 自动内存管理机制,可以更专注于业务开发
  • 垃圾回收的区域:

image-20200822214505611

面试题

image-20200822210355562

image-20200822210340777

二、垃圾回收相关算法

标记阶段

引用计数算法

引用计数算法(Reference Counting):在对象中添加一个 引用计数器 ,对于A对象,每当有一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器减1,。只要引用计数器为0就表示A对象可以被回收了。

优点:

  • 实现简单,垃圾对象很容易识别;
  • 判定效率高,回收没有延迟性。

缺点:

  • 致命缺点:很难解决对象之间 循环引用 的问题,这也导致主流的Java虚拟机没有选用该算法管理内存

可达性分析算法

当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。

算法思路
  • GC Roots 为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为引用链(ReferenceChain)
  • 如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

image-20200823134917602

GC Roots

Java语言中,固定可作为 GC Roots 的对象包括以下几类元素:

  • 虚拟机栈中引用的对象

    • 比如:各个线程被调用的方法堆栈中使用到的参数、局部变量等
  • 本地方法栈内JNI(通常说的本地方法)引用的对象

  • 方法区中类静态属性引用的对象

    • 比如:Java类的引用类型静态变量
  • 方法区中常量引用的对象

    • 比如:字符串常量池(StringTable)里的引用
  • 所有被同步锁(synchronized关键字)持有的对象

  • Java虚拟机内部的引用

    • 比如:基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

小技巧:
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。

注意
  • 如果要使用可达性分析算法来判断内存是否可回收,那么 根节点枚举工作 必须在一个能保障一致性的快照中进行,不能出现分析过程中,根节点集合的对象引用关系还在发生变化。这点不满足的话分析结果的准确性就无法保证。

  • 这点也是导致GC进行时必须 Stop The World 的一个重要原因。

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

对象生存或死亡

对象的finalization机制

如果从所有的 GC Roots 都无法访问到某个对象,说明对象已经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,为此,定义虚拟机中的对象可能的三种状态。如下:

  • 可触及的:从根节点开始,可以到达这个对象。
  • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
  • 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可 触及的对象不可能被复活,因为finalize()只会被调用一次。

以上3种状态中,是由于 finalize() 方法的存在才进行的区分。只有在对象不可触及时才可以被回收。

具体过程

image-20200823144023394

虚拟机创建的 Finalizer线程会去触发 finalize()方法,但并不承诺一定会等待它运行结束。这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端地发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。

清除阶段

当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。

目前在JVM中比较常见的三种垃圾收集算法是标记-清除算法( Mark - Sweep)、复制算法(Copying)、标记一压缩算法(Mark - Compact )。

标记-清除算法

执行过程

当堆中的有效内存空间(available memory) 被耗尽的时候,就会停止整个程序(stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。

  • 标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
  • 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
缺点
  • 执行效率不高,如果大部分的对象需要回收,则需要大量的标记和清除动作。
  • 内存空间的碎片化问题。需要维护一个空闲列表。

image-20200823220111548

复制算法

核心思想

u = 使用中的内存块

n = 未使用的内存块

把内存空间分成两块,每次只使用其中的一块,在垃圾回收时将u中的对象复制到 n,之后清除u中的所有对象,交换两个内存块的角色,完成垃圾回收。

image-20200824092210116

优缺点

优点:

  • 实现简单,运行高效
  • 不会出现“碎片”问题。

缺点:

  • 需要两倍的内存空间。
  • 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。

特别的:

  • 适合可回收对象多的情况。

因为新生代中的对象具有 朝生夕死 的特点,因此并不需要按照 1:1 的比例来划分新生代的内存空间。使用的是“Apple式回收”:把新生代划分为较大的Eden空间和两块较小的Survivor空间

标记-整理算法

又称标记 - 压缩算法,主要是针对老年代的存活特征。

执行过程
  1. 标记过程和 标记 - 清除 算法一样;

  2. 让所有存活的对象都向内存空间一端移动;

  3. 清除边界外所有的空间。

image-20200824101521208

优缺点

image-20200824103047947

总结

image-20200824102959140

分代收集算法

image-20200824104545629

增量收集算法

为了避免在收集垃圾时的长时间的停顿(因为 Stop The World),可以允许垃圾收集器以分阶段的方式完成标记、清理或复制工作

缺点

使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降

三、垃圾回收相关概念

System.gc() 的理解

在默认情况下,通过System.gc ()或者Runtime.getRuntime().gc() 的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。

然而 System.gc() 调用附带一个免责声明,无法保证对垃圾收集器的调用。

内存溢出与内存泄漏

内存溢出(OOM)

OutOfMemory: 没有空闲内存,并且垃圾收集器也无法提供更多的内存

在抛出 OutOfMemoryError 之前,通常垃圾收集器都会被触发,尽可能清理出空间。

  • 例如:在引用机制分析中,涉及到JVM会去尝试回收 软引用指向的对象等
  • java.nio.BIts.reserveMemory() 方法中,可以看到 System.gc()被调用。

但并不是任何情况下都会触发垃圾收集。

  • 比如,分配的对象超大,超过了堆的最大值。就会直接报 OutOfMemoryError

内存泄漏(Memory Leak)

也称作存储渗漏严格来说:只有对象不会再被使用,但是 GC 又不能回收,才叫做内存泄漏。

但实际情况下,一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致00M,也可以叫做宽泛意义上的“内存泄漏”

内存泄漏会蚕食可用内存,直至耗尽所有内存,导致出现 OutOfMemoryError 异常。

image-20200824213556929

Stop The World

GC 事件发生过程中,会产生应用程序的停顿。停顿产生时,整个应用程序都会被暂停,没有任何响应。

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

需要减少STW的发生。

所有的GC都会有 STW 事件,只能说垃圾回收器越老越优秀,回收效率越来越高,尽可能的缩短了暂停时间。

安全点与安全区域

安全点

程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为安全点(Safepoint)
Safe Point的选择很重要,如果太少可能导致Gc等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令作为Safe Point,如方法调用循环跳转异常跳转等。

image-20200825180120500

安全区域

Safepoint 无法解决程序不执行的情况,例如线程处于 Sleep 状态或 Blocked 状态,这时候就需要安全区域Safe Region 来解决。

安全区域是指在一段代码片段中,引用关系不会发生变化,在该区域的任何位置都可以 GC。

实际执行时:

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

强引用

在代码中普遍存在的引用赋值,任何情况下,垃圾收集器都不会回收还在被强引用的对象

软引用

在即将要发生OOM时,才会回收这些对象。如果回收后内存还是不够,才会抛出 OOM

弱引用

被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。

WeakHashMap

虚引用

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

强引用(StrongReferenc)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),这4中引用强度依次减弱

四、垃圾回收器

评估GC的性能指标

  • 吞吐量:运行用户代码的时间占总运行时间的比例。
  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停时间。
  • 内存占用:Java 堆区所占用的内存大小。
  • 收集频率:相对于应用程序的执行,收集操作发生的频率。

不同的垃圾回收器概述

image-20200826230138175

7种经典收集器与垃圾分代之间的关系

image-20200827090432083

垃圾收集器的组合关系

image-20200827092414610

  1. 两个收集器之间有连线,表明它们可以搭配使用。
  2. 其中 Serial Old GC 作为 CMS GC 出现 Concurrent Mode Failure 失败后的后备预案。
  3. (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMSParNew+Serial Old这两个组合声明为废弃(EP 173),并在JDK 9中完全取消了这些组合的支持(EP214),即:移除。
  4. (绿色虚线)JDK14中,弃用Parallel ScavengeSerial Old GC 组合。
  5. (青色虚线)JDK14中,删除了CMS垃圾回收器。

如何查看默认的垃圾收集器

image-20200827093505384

Serial回收器:串行回收

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

image-20200827095954119

Serial回收器的优点是:

  • 简单而高效(与其它收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的。对于单核CPU来说,Serial由于没有线程交互的开销,因为有着更高的垃圾收集效率。
  • 在用户的桌面应用场景中,可用内存一般不大(几十MB至一两百MB),可以在较短时间内完成垃圾收集(几十ms至一百多ms),只要不频繁发生,使用串行回收器是可以接受的。
  • 在HotSpot虚拟机中,使用–XX:+UseSerialGc参数可以指定年轻代和老年代都使用串行收集器。等价于新生代用serial Gc,且老年代用serial old Gc

ParNew回收器:并行回收

ParNew 收集器实质上是 Serial 收集器的多线程并行版本,并没有多大的创新之处。

到现在官方更希望它完全被 G1 所取代,加上取消了Serial+CMSParNew+Serial Old的组合使用,到目前 ParNew 只能和 CMS 搭配使用。

image-20200827102704674

在单核心的环境下,ParNew收集器绝对不会有比Serial收集器更好的效果。

Parallel回收器:吞吐量优先

Parallel Scavenge 收集器与其他收集器的不同在于,其它收集器的关注点是尽可能减少 Stop The World 的时间,而 Parallel Scavenge 更关注达到一个可控制的吞吐量(Throughput)。

吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。

停顿时间越短就越适合与用户交互的程序,良好的响应速度能提升用户体验。

高吞吐量则可以最高效率利用处理器资源,尽快完成运算任务,主要适合后台运算的分析任务。例如工资支付、科学计算、日志分析的应用。

自适应调节策略Parallel ScavengeParNew 的一个重要区别。自适应调节策略 是指虚拟机会根据当前系统的运行情况,动态调整虚拟机参数以提供最合适的停顿时间或者最大的吞吐量。

image-20200827105448084

在Java8中,默认使用此垃圾收集器。

CMS回收器:低延迟

Concurrent-Mark-Sweep GC

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

目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
CMS收集器 就非常符合这类应用的需求。

CMS收集器是采用标记-清除 算法。

image-20200827112747789

  1. 初始标记(initial mark):仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快;
  2. 并发标记(concurrent mark):从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程比较耗时,但不需要 STW
  3. 重新标记(remark):修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
  4. 并发清除(concurrent sweep):清理删除掉标记阶段判断已经死亡的对象,由于不需要移动存活对象,所以这个阶段也不需要 STW,当然这也导致了只能使用Mark-Sweep 算法,会造成内存碎片问题,进而不能使用指针碰撞方法分配对象所需内存空间(使用 空闲列表 法)。

优缺点

优点:

  • 并发收集
  • 低延迟

缺点:

  • 使用“标记-清除”算法,会造成内存碎片。
  • CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
  • CMS收集器无法处理浮动垃圾。可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。在并发标记和并发清理阶段,用户线程还在继续运行,那么就会产生新的垃圾对象,但因为这些垃圾出现在标记过程之后,CMS无法在此次收集中处理掉它们,只能留在下一次GC时清理。这一部分垃圾就是 “浮动垃圾”。

G1回收器:区域化分代式

官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起了全功能收集器 的重任与期望。

G1是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器。是JDK9以后的默认垃圾收集器。

G1的优势与不足

优点:

  • 并行与并发:
    • 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW。
    • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况。
  • 分代收集
    • 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和survivor区,但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
    • 将堆空间划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理
  • 空间整合
    • G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。
      Region之间是标记-复制算法,但整体上可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
  • 可预测的停顿时间模型
    • 这是G1相对于CMS 的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
    • G1的具体回收思路是:G1收集器去跟踪各个Region里面的垃圾堆积的价值大小(价值即回收所获得的空间大小以及回收所需时间的经验值),然后在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内获取尽可能高的收集效率。

缺点:

  • 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的==内存占用(Footprint)还是程序运行时的额外执行负载(overload)==都要比CMS要高。
  • 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。

G1回收器的适应场景

  • 面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)
  • 最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;
  • 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次GC停顿时间不会过长)。
  • 用来替换掉JDK1.5中的CMS收集器;在下面的情况时,使用G1可能比CMS好:
    • 超过50%的Java堆被活动数据占用;
    • 对象分配频率或年代提升频率变化很大;
    • GC停顿时间过长(长于0.5至1秒)
  • HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。

Region:化整为零

image-20200827174341535

G1垃圾收集器中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。

如果一个Region装不下一个大对象,那么该对象会被放在N个连续的H区中。

Region区也使用了TLAB来保证多线程下对象内存空间的快速分配。

为什么要有H区?

对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区。

Remembered Set

  • 一个对象会被不同区域所引用。
  • 垃圾收集器在回收新生代的垃圾对象时,为了避免把整个老年代加入 GC Roots 扫描范围,建立了 记忆集(Remembered Set)。
  • 实际上,所有的部分区域收集行为的垃圾收集器,都会面临相同的问题。

解决方法:

  • 无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:
  • 每个Region都有一个对应的Remembered set
  • 每次Reference类型数据写操作时,都会产生一个写屏障(write Barrier)暂时中断操作。
  • 然后检查指向对象是否和Reference在不同的Region(对于其他收集器:检查老年代对象是否引用了新生代对象)。
  • 如果不同,通过卡表(CardTable)把相关引用信息记录到指向对象的所在Region对应的Remembered set中;
  • 当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set,就可以保证不进行全局扫描,也不会有遗漏。

image-20200827200611235

G1回收器垃圾回收过程

image-20200827202645919

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

image-20200827205412079

总结

image-20200827205523221

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值