3、JVM-垃圾回收机制与算法

概述

说起垃圾收集(Garbage Collection,GC),大部分人都把这项技术当作Java语言的伴生产物。事实上,GC的历史比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。当Lisp还在胚胎时期时,人们就在思考GC需要完成的3件事情:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

垃圾收集机制是Java的招牌能力,极大地提高了开发效率。如今,垃圾收 集几乎成为现代语言的标配,即使经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战,这当然也是面试的热点。

对象死亡算法

垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。
那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。

判断对象存活一般有两种方式:引用计数算法可达性分析算法

引用计数算法

Java 堆 中每个具体对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1。当引用失效时,即一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时,计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。

  • 优点: 实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
  • 缺点:
    ➢它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
    ➢每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
    ➢引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,Java的垃圾回收器中没有使用这类算法

可达性分析算法

可达性分析算法又叫根搜索算法,该算法的基本思想就是通过一系列称为GC Roots的对象作为起始点,从这些起始点开始往下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 对象之间没有任何引用链的时候(不可达),证明该对象是不可用的,于是就会被判定为可回收对象
在这里插入图片描述
在 Java 中可作为 GC Roots 的对象包含以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中 JNI(Native 方法)引用的对象。
  • 所有被同步锁synchronized持有的对象
  • Java虚拟机内部的引用
  • 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

对象标记

一个对象是否应该在垃圾回收器在GC时回收,至少要经历两次标记过程。

  • 第一次标记:

    如果对象在进行可达性分析后被判定为不可达对象,那么它将被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize() 方法。对象没有覆盖 finalize() 方法或者该对象的 finalize() 方法曾经被虚拟机调用过,则判定为没必要执行。

  • finalize()第二次标记:

    如果被判定为有必要执行 finalize() 方法,那么这个对象会被放置到一个 F-Queue 队列中,并在稍后由虚拟机自动创建的、低优先级的 Finalizer 线程去执行该对象的 finalize() 方法。但是虚拟机并不承诺会等待该方法结束,这样做是因为,如果一个对象的 finalize() 方法比较耗时或者发生了死循环,就可能导致 F-Queue 队列中的其他对象永远处于等待状态,甚至导致整个内存回收系统崩溃。finalize() 方法是对象逃脱死亡命运的最后一次机会,如果对象要在 finalize() 中挽救自己,只要重新与 GC Roots 引用链关联上就可以了。这样在第二次标记时它将被移除「即将回收」的集合,如果对象在这个时候还没有逃脱,那么它基本上就真的被回收了。

优化工具简介

  • MAT
    Memory Analyzer的简称,它是一 款功能强大的Java堆内存分析器。用于查找内存泄漏以及查看内存消耗情况。
    MAT是基于Eclipse开发的,是一款免费的性能分析工具。
    可以在http://www.eclipse org/mat/下载并使用MAT。

  • Java VisualVM
    java自带的工具位于:C:\Program Files\Java\jdk1.8.0_20\bin\ jvisualvm.exe

  • Jprofiler

Jprofiler使用案例

配置-XX:+HeapDumpOnOutOfMemoryError表示当JVM发生OOM时,自动生成DUMP文件

使用Jprofiler打开dump文件
在这里插入图片描述获取Dump文件
在这里插入图片描述
在这里插入图片描述

引用

无论是通过引用计数器还是通过可达性分析来判断对象是否可以被回收都设计到「引用」的概念。在 Java 中,根据引用关系的强弱不一样,将引用类型划为强引用(Strong Reference)软引用(Soft Reference)弱引用(Weak Reference)虚引用(Phantom Reference)

  • 强引用: Object obj = new Object()这种方式就是强引用,只要这种强引用存在,垃圾收集器就永远不会回收被引用的对象。

  • 软引用: 用来描述一些有用但非必须的对象。在 OOM 之前垃圾收集器会把这些被软引用的对象列入回收范围进行二次回收。如果本次回收之后还是内存不足才会触发 OOM。在 Java 中使用SoftReference 类来实现软引用。

  • 弱引用: 同软引用一样也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在 Java 中使用 WeakReference 类来实现。

  • 虚引用: 是最弱的一种引用关系,一个对象是否有虚引用的存在完全不影响对象的生存时间,也无法通过虚引用来获取一个对象的实例。一个对象使用虚引用的唯一目的是为了在被垃圾收集器回收时收到一个系统通知。在Java 中使用 PhantomReference 类来实现。

垃圾清除算法

标记-清除算法

标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段:

  • 标记阶段:标记出可以回收的对象。
  • 清除阶段:回收被标记的对象所占用的空间。

优点: 实现简单,不需要对象进行移动。

缺点: 标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。
在这里插入图片描述
注意:何为清除?

这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲 的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。

复制算法

为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。

  • 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
  • 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

复制算法的执行过程如下图所示
在这里插入图片描述
HotSpot 默认内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 EdenSurvivor 中还存活的对象一次性复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。如果另外一块 Survivor 空间没有足够空间存放上一次新生代收集下来存活的对象时,这些对象将直接通过分配担保机制进入老年代。

标记-整理算法

在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法Mark-Compact)算法,与标记-整理算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。

  • 优点:解决了标记-清理算法存在的内存碎片问题。
  • 缺点:仍需要进行局部对象移动,一定程度上降低了效率。

标记-整理算法的执行过程如下图所示
在这里插入图片描述

分代收集算法

当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。
一般包括年轻代老年代元空间,如图所示:
在这里插入图片描述
新生代(Young generation)

绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程我们称之为 minor GC

新生代 中存在一个Eden区和两个Survivor区。新对象会首先分配在Eden中(如果新对象过大,会直接分配在老年代中)。在GC中,Eden中的对象会被移动到Survivor中,直至对象满足一定的年纪(定义为熬过GC的次数),会被移动到老年代。

可以设置新生代和老年代的相对大小。这种方式的优点是新生代大小会随着整个堆大小动态扩展。参数 -XX:NewRatio 设置老年代与新生代的比例。例如 -XX:NewRatio=8 指定 老年代/新生代 为8/1. 老年代 占堆大小的 7/8 ,新生代 占堆大小的 1/8(默认即是 1/8)。

例如:

-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8

老年代(Old generation)

对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代要少得多。对象从老年代中消失的过程,可以称之为major GC(或者full GC)。

永久代(permanent generation)

像一些类的层级信息方法数据方法信息(如字节码,栈 和 变量大小)运行时常量池JDK7之后移出永久代),已确定的符号引用和虚方法表等等。它们几乎都是静态的并且很少被卸载和回收,在JDK8之前的HotSpot虚拟机中,类的这些永久的数据存放在一个叫做永久代的区域。

永久代一段连续的内存空间,我们在JVM启动之前可以通过设置-XX:MaxPermSize的值来控制永久代的大小。但是JDK8之后取消了永久代,这些元数据被移到了一个与堆不相连的称为元空间 (Metaspace) 的本地内存区域。

小结

JDK8堆内存一般是划分为年轻代和老年代,不同年代 根据自身特性采用不同的垃圾收集算法。

对于新生代,每次GC时都有大量的对象死亡,只有少量对象存活。考虑到复制成本低,适合采用复制算法。因此有了From SurvivorTo Survivor区域。

对于老年代,因为对象存活率高,没有额外的内存空间对它进行担保。因而适合采用标记-清理算法标记-整理算法进行回收。
在这里插入图片描述

垃圾回收相关概念

System.gc()的理解

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

回收案例:
在这里插入图片描述

内存溢出与内存泄漏

  • 内存溢出: Java虚拟机的堆内存不够
  • 内存泄漏: 也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏

Stop The World

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

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

安全点与安全区域

安全点(Safepoint)

程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为安全点Safepoint

Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择些执行时间较长的指令作为Safe Point, 如方法调用、循环跳转和异常跳转等。

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

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

安全区域(Safe Region)

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

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

垃圾收集器

收集器分类

从不同角度分析垃圾收集器,可以将GC分为不同的类型。

按线程数分,可以分为串行垃圾回收器并行垃圾回收器

  • 串行回收: 指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的Client模式下的JVM中。在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器
  • 并行收集: 可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了 Stop一the一world机制

按照工作模式分,可以分为并发式垃圾回收器独占式垃圾回收器

  • 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
  • 独占式垃圾回收器Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。

按碎片处理方式分,可分为压缩式垃圾回收器非压缩式垃圾回收器

  • 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。
  • 再分配对象空间使用: 指针碰撞
  • 非压缩式的垃圾回收器不进行这步操作。
  • 再分配对象空间使用: 空闲列表

按工作的内存区间分,又可分为年轻代垃圾回收器老年代垃圾回收器

性能指标

主要抓住两点:

  • 吞吐量
  • 暂停时间

吞吐量

吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/ (运行用户代码时间+垃圾收集时间)

比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%

这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的。吞吐量优先,意味着在单位时间内,STW的时间最短.

暂停时间

暂停时间是指一个时间段内应用程序线程暂停,让GC线程执行的状态

例如,GC期间100毫秒的暂停时间意味着在这100毫秒期间内没有应用程序线程是活动的。.

暂停时间优先,意味着尽可能让单次STW的时间最短

小结

高吞吐量低暂停时间是一对相互竞争的目标(矛盾)。

  • 因为如果选择以吞吐量优先,那么必然需要降低内存回收的执行频率,但是这样会导致GC需要更长的暂停时间来执行内存回收。
  • 相反的,如果选择以低延迟优先为原则,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收,但这又引起了年轻代内存的缩诚和导致程序吞吐量的下降。

一个GC算法只可能针对两个目标之一(即只专注于较大吞吐量或最小暂停时间),或.尝试找到一个二者的折衷。

现在标准: 在最大吞吐量优先的情况下,降低停顿时间。

收集器

七种垃圾回收器概述
在这里插入图片描述

JVM 中,具体实现有 SerialParNewParallel ScavengeCMSSerial Old(MSC)Parallel OldG1 等。

如果当垃圾回收器进行垃圾清理时,必须暂停其他所有的 工作线程,直到它完全收集结束。我们称这种需要暂停工作线程才能进行清理的策略为 Stop-the-World。以上回收器中, Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old 均采用的是 Stop-the-World 的策略。

垃圾收集器的组合关系

在这里插入图片描述

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

查看默认的垃圾收集器

一xx:+PrintCommandLineFlags: 查看命令行相关参数(包含使用的垃圾收集器)

输出

-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 

在这里插入图片描述

单线程垃圾回收器

Serial(-XX:+UseSerialGC)

Serial 回收器是最基本的 新生代 垃圾回收器,是 单线程 的垃圾回收器。由于垃圾清理时,Serial 回收器 不存在 线程间的切换,因此,特别是在单 CPU 的环境下,它的 垃圾清除效率 比较高。对于 Client 运行模式的程序,选择 Serial 回收器是一个不错的选择。

Serial 新生代回收器 采用的是 复制算法

总结

  • 这种垃圾收集器大家了解,现在已经不用串行的了。而且在限定单核cpu才可以用。现在都不是单核的了。
  • 对于交互较强的应用而言,这种垃圾收集器是不能接受的。T一般在Javaweb应用程序中是不会采用串行垃圾收集器的。
Serial Old(-XX:+UseSerialGC)

Serial Old 回收器是 Serial 回收器的 老年代版本,属于 单线程回收器,它使用 标记-整理 算法。对于 Server 模式下的虚拟机,在 JDK1.5 及其以前,它常与 Parallel Scavenge 回收器配合使用,达到较好的 吞吐量,另外它也是 CMS 回收器在 Concurrent Mode Failure 时的 后备方案。

多线程垃圾回收器(吞吐量优先)

ParNew(-XX:+UseParNewGC)

ParNew 回收器是在 Serial 回收器的基础上演化而来的,属于 Serial 回收器的 多线程版本,同样运行在 新生代区域。在实现上,两者共用很多代码。在不同运行环境下,根据 CPU 核数,开启 不同的线程数,从而达到 最优 的垃圾回收效果。对于那些 Server 模式的应用程序,如果考虑采用 CMS 作为 老年代回收器 时,ParNew 回收器是一个不错的选择。

ParNew 新生代回收器 采用的是 复制算法
一XX:ParallelGCThreads 限制线程数量,默认开启和CPU数据相同的线程数。.

Parallel Scavenge(-XX:+UseParallelGC)

ParNew 回收一样,Parallel Scavenge 回收器也是运行在 新生代区域,属于 多线程 的回收器。但不同的是,ParNew 回收器是通过控制 垃圾回收的线程数 来进行参数调整,而 Parallel Scavenge 回收器更关心的是 程序运行的吞吐量。即一段时间内,用户代码 运行时间 占 总运行时间 的百分比

Parallel Scavenge 新生代回收器 采用的是 复制算法

Parallel Old(-XX:+UseParallelOldGC)

Parallel Old 回收器是 Parallel Scavenge 回收器的 老生代版本,属于 多线程回收器,采用 标记-整理算法。Parallel Old 回收器和 Parallel Scavenge 回收器同样考虑了 吞吐量优先 这一指标,非常适合那些 注重吞吐量 和 CPU 资源敏感 的场合。

Parallel Old 老年代回收器 采用的是 标记 - 整理算法

parallel参数配置:

  • 一XX: +UseParallelGC手动指定 年轻代使用Parallel并行收集器执行内存回收任务。

  • 一XX: +UseParallel0ldGc手 动指定老年代都是使用并行回收收集器。
    分别适用于新生代和老年代。默认jdk8是开启的。
    上面两个参数,默认开启一个,另一个也会被开启。 (互相激活)

  • 一XX:ParallelGCThreads设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。在默认情况下,当CPU数量小于8个, Paralle lGCThreads 的值等于CPU数量。当CPU数量大于8个, ParallelGCThreads的值等于3+[5*CPU_ Count]/8]

  • 一XX :MaxGCPau3eMillis设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒。

  • 一XX:GCTimeRatio垃圾收集时间占总时间的比例用于衡量吞吐量的大小。
    取值范围(0, 100)。默认值99,也就是垃圾回收时间不超过1%
    与前一个一XX:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。

  • 一XX: +UseAdaptiveSizePolicy设 置Parallel Scavenge收 集器具有自适应调节策略在这种模式下,年轻代的大小、EdenSurvivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机自己完成调优工作。

其他的回收器(停顿时间优先)

CMS(-XX:+UseConcMarkSweepGC)

CMS(Concurrent Mark Sweep) 回收器是在 最短回收停顿时间 为前提的回收器,属于 多线程回收器,采用 标记-清除算法。

在这里插入图片描述
相比之前的回收器,CMS 回收器的运作过程比较复杂,分为四步:

  1. 初始标记(CMS initial mark

初始标记 仅仅是标记 GC Roots 内 直接关联 的对象。这个阶段 速度很快,需要 Stop the World

  1. 并发标记(CMS concurrent mark

并发标记 进行的是 GC Tracing,从 GC Roots 开始对堆进行 可达性分析,找出 存活对象。

  1. 重新标记(CMS remark

重新标记 阶段为了 修正 并发期间由于 用户进行运作 导致的 标记变动 的那一部分对象的 标记记录。这个阶段的 停顿时间 一般会比初始标记阶段 稍长一些,但远比 并发标记 的时间短,也需要 Stop The World

  1. 并发清除(CMS concurrent sweep

并发清除 阶段会清除垃圾对象。

缺点:

  • CMS回收器对CPU资源非常依赖
  • CMS回收器无法清除浮动垃圾

由于 CMS 回收器 清除已标记的垃圾 (处于最后一个阶段)时,用户线程 还在运行,因此会有新的垃圾产生。但是这部分垃圾 未被标记,在下一次GC 才能清除,因此被成为 浮动垃圾。

  • CMS 回收器采用的 标记清除 算法, 垃圾收集结束后残余大量空间碎片

JDK 后续版本中CMS的变化

  • JDK9新特性: CMS被标记为Deprecate

如果对JDK 9及以上版本的HotSpot虚拟机使用参数一XX:+UseConcMarkSweepGC来开启CMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃。

  • JDK14新特性: 删除CMS垃圾回收器

移除了CMS垃圾收集器,如果在JDK14中使用一XX: +UseConcMarkSweepGC的话,JVM不会报错,只是给出一个warning信息,但是不会exitJVM会自动回退以默认GC方式启动JVM

G1回收器(垃圾区域Region优先)

G1JDK 1.7 中正式投入使用的用于取代 CMS 的 压缩回收器。它虽然没有在物理上隔断 新生代 与 老生代,但是仍然属于分代垃圾回收器G1 仍然会区分 年轻代老年代,年轻代依然分有 Eden 区与 Survivor 区。

G1 首先将 堆 分为 大小相等Region,避免 全区域 的垃圾回收。然后追踪每个 Region 垃圾 堆积的价值大小,在后台维护一个 优先列表,根据允许的回收时间优先回收价值最大的 Region。同时 G1采用 Remembered Set 来存放 Region 之间的 对象引用 ,其他回收器中的 新生代 与 老年代 之间的对象引用,从而避免 全堆扫描G1 的分区示例如下图所示:

在这里插入图片描述

G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。主要用于存储大对象,如果超过1. 5个region,就放到H

设置H的原因:

对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GCG1的大多数行为都把H区作为老年代的一部分来看待。

记忆集与写屏障

一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用

解决方法:

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

G1的垃圾回收过程

G1 GC的垃圾回收过程主要包括如下三个环节:

  • 年轻代GC (Young GC )
    在这里插入图片描述
    在这里插入图片描述

  • 老年代并发标记过程( Concurrent Marking
    在这里插入图片描述

  • 混合回收(Mixed GC
    在这里插入图片描述

  • Full GC 它针对GC的评估失败提供了一种失败保护机制,即强力回收
    在这里插入图片描述

优化建议

  • 年轻代大小

    避免使用一Xmn一XX:NewRatio等相关选项显式设置年轻代大小

  • 固定年轻代的大小会覆盖暂停时间目标

  • 暂停时间目标不要太过严苛G1 GC的吞吐量目标是90%应用程序时间10%垃圾回收时间
    评估G1 GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示你愿意承受更多的垃圾回收开销,而这些会直接影响到吞吐量

参数设置

  • 一XX:+UseG1GC 手动指定使用G1收集器执行内存回收任务。
  • 一XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围是1MB32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000
  • 一XX:MaxGCPauseMillis 设置期望达到的最大Gc停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms
  • 一xX:ParallelGCThread 设置STW工作线程数的值。最多设置为8
  • 一XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
  • 一XX:Ini tiatingHeapOccupancyPercent设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45

GC日志分析命令

通过阅读GC日志,我们可以了解Java虛拟机内存分配与回收策略。内存分配与垃圾回收的参数列表

  • 一XX:+PrintGC 输出Gc日志。类似: 一verbose:gc
  • 一XX: +PrintGCDetails 输出GC的详细日志
  • 一XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
  • 一XX:+PrintGCDateStamps输出GC的时间戳(以日期的形式,如2013一05一04T21 :
    53:59.234+0800 )
  • 一XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
  • 一Xloggc:../logs/gc.log日志文件的输出路径

日志分析工具使用

常用的日志分析.工具有: GCViewer、GCEasy、GCHisto、GCLogViewer 、Hpjmeter、garbagecat等。

JVM调优

https://note.youdao.com/ynoteshare/index.html?id=5cc182642eb02bc64197788c7722baae&type=note&_time=1648633499202

https://note.youdao.com/ynoteshare/index.html?id=d8d6dc3589ffd9245d97bb7aa91af835&type=note&_time=1648633515821

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值