Skip to content

JVM_垃圾回收机制

视频学习: https://www.bilibili.com/video/BV1sp4y1Y7ap

文档学习:略

什么是可回收垃圾对象

gc过程

gc 判断对象是否需要回收有两种判定方法

引用计数算法

引用计数算法,被引用时计数+1,释放引用时-1,计数为0就回收(简单不推荐,解决不了循环引用)

可达性分析算法

将“GCRoots”对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象

GCRoots 根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等

  • 可达性分析算法,就是从gc roots开始往下搜索,走过的对象就是引用链,没有任何引用链时,就是不可达对象,就会被回收(gc roots包含虚拟机栈中引用对象,方法区中静态属性/常量引用对象,本地方法栈中引用对象)
    • 如果对象重写了finalize(),并将自身赋予某个引用,那么这个对象不会被回收
    • 可以自己调用system.gc或Runtime.getRuntime().gc,都是像系统提交申请进行gc操作,但不会立即gc

比较

  • System.gc()调用起来更方便,但是会给应用带来不必要的性能问题。还可以通过参数 -XX:+DisableExplicitGC.禁止显示调用gc。
  • Runtime.getRuntime() 用来与Java运行时进行交互,调用该方法会建议JVM 花费精力回收不再使用的对象。
GC Root 对象

哪些对象可以作为 GC Root 对象

GCRoots 根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等

垃圾回收算法

  • 1、标记-清除算法,给回收对象标记,然后清除对象,缺点效率不高清除后空间不连续
    • 位置不连续,容易产生内存碎片
  • 2、标记-整理算法,存活对象整理到一块,清除存活对象以外的数据,缺点整理过程执行较多复制操作,算法效率降低
    • 没有碎片,效率较低
  • 3、复制算法,就是把回收对象复制到半块地方,原来的地方直接清空,缺点内存缩小一半
    • 没有碎片,浪费空间

标记-清除算法

给回收对象标记,然后清除对象,缺点效率不高清除后空间不连续

  • 位置不连续,容易产生内存碎片

image.png

复制算法

把回收对象复制到半块地方,原来的地方直接清空,缺点内存缩小一半

  • 没有碎片,浪费空间

image.png

标记-整理算法

存活对象整理到一块,清除存活对象以外的数据,缺点整理过程执行较多复制操作,算法效率降低

  • 没有碎片,效率较低

image.png

JVM 内存模型

一般会有分代模型和分区模型,在 JDK 1.8 的时候使用的是 分代模型

运行时数据区

  • 线程私有

  • 线程共享

image.png

堆内存

JDK 1.8 堆内存使用内存模型是分代模型

    • 年轻代 1/3
      • Eden 8
        • 一般是朝思夕死的对象,MInor GC 的时候大部分存活的对象不多
      • Survivor
        • From 1
        • To 1
    • 老年代 2/3
      • 当对象经历过 15 次 GC,会将该对象从幸存区放入到老年代

image.png

垃圾回收过程

新创建的对象,放入到 Eden 区,根据可达性分析,判断对象是否可以被回收,如果不可被回收,会通过 复制算法 复制到 to 区域

当对象经历过 15 次 GC,会将该对象从幸存区放入到老年代

  • Minor GC/Young GC
  • Full GC

image.png

在堆中的对象,首先执行引擎会开启

一、堆的区域划分

  • 1.堆被分为了两份:新生代和老年代【1:2】
  • 2.对于新生代,内部又被分为了三个区域。Eden区,幸存者区survivor(分成from和to)【8:1:1】

二、对象回收分代回收策略

  • 1.新创建的对象,都会先分配到 eden 区
  • 2.当伊甸园内存不足,标记伊甸园与from(现阶段没有)的存活对象
  • 3.将存活对象采用复制算法复制到to中,复制完毕后,伊甸园和from内存都得到释放
  • 4.经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将其复制到from区
  • 5.当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会提前晋升)

MinorGC、Mixed GC、FullGC的区别是什么

  • MinorGC【young GC】发生在新生代的垃圾回收,暂停时间短(STW)
  • Mixed GC新生代+老年代部分区域的垃圾回收,G1收集器特有
  • FuGC:新生代+老年代完整垃圾回收,暂停时间长(STW),应尽力避免

stw(stop the world)停止用户线程(停止事件)

Full GC 触发:

Full GC(Full Garbage Collection)是指对整个堆内存(包括新生代和老年代)进行垃圾回收的过程。

Full GC通常比单独的新生代GC或老年代GC要慢,因为它需要检查整个堆。(stw 停顿时间会久一些)

触发Full GC的情况可能包括但不限于以下几种:(常见的几种情况)【了解,后面整理一下】

  1. 老年代空间不足: 当老年代的内存占用接近其最大值时,JVM可能会执行Full GC来释放内存。
  2. 永久代(PermGen)空间不足(Java 8之前): 永久代用于存储类元数据,当永久代空间不足时,会触发Full GC。
  3. CMS垃圾回收后的清理: 使用CMS(Concurrent Mark-Sweep)垃圾回收器时,如果并发清理后老年代空间仍然不足,可能会触发Full GC。
  4. 系统属性设置: 通过JVM参数(如-XX:+HeapDumpOnOutOfMemoryError)可以设置在 OOM(Out of Memory)异常时进行Full GC。
  5. 显式调用System.gc(): 虽然调用System.gc()并不保证一定会执行Full GC,但这会是一个提示给JVM,可能会触发一次Full GC。
  6. JVM监控到大量的对象死亡和内存回收: 如果新生代GC后,大量的对象从新生代晋升到老年代,JVM可能会认为需要进行Full GC。
  7. 堆外内存的回收: 如果使用了直接内存(如NIO缓冲区),JVM可能会在清理堆外内存时触发Full GC。
  8. GC策略和垃圾回收器的选择: 不同的垃圾回收器和GC策略可能在特定条件下触发Full GC。
  9. 空间分配担保(Space Allocation Guarantee)失败: 在GC后,如果新生代的内存分配失败,JVM可能会尝试进行Full GC。
  10. 元空间(Metaspace)达到阈值(Java 8及之后): 元空间用于存储类元数据,当达到一定阈值时,可能会触发Full GC。
  11. 堆内存分配策略: 根据JVM的堆内存分配策略,某些情况下可能会执行Full GC。
  12. 应用程序特定的行为: 某些应用程序在特定条件下可能会请求执行Full GC

GC 垃圾回收

而系统自己会自动进行gc,在堆区划分俩个空间

image.png

新生对象在新生代的伊甸区创建,如果伊甸区满了,会进行minor gc操作,仅除新生代 未被回收会进入幸存区(from),(大对象直接进入老年区)

在幸存区的对象,每次伊甸区满了进行minor gc操作,存活的计数加1,且从from区复制到to区,from和to交换位置够岁数(15)的对象就直接进入老年代,直到老年代满了,会先执行一次minor gc操作,空间还是不足,就执行full gc,清除整个堆

其中每次gc都会触发stw(stop the world)耗时minor<majoi<full,所以要尽量避免full gc

ps

如果是CMS GC会有老年代后的单独执行major gc操作(非整堆,执行前也会执行一次minor gc比minor gc慢10倍)

如果是G1 GC会进行mixed gc,混合gc,回收整个新生代和部分老年代

垃圾回收调优工具

visualvm

visualvm 插件下载

这里展示一下相关的使用

工具下载地址: https://visualvm.github.io/index.html

插件 下载界面: https://visualvm.github.io/pluginscenters.html

image.png

下载地址: https://visualvm.github.io/archive/uc/8u20/updates.html

image.png

选择 工具 → 插件 → 已下载 → 选择安装 → 重启

image.png

测试类

public class MemoryTest {  
  
    byte[] arr = new byte[1024 * 25];  
  
    public static void main(String[] args) throws InterruptedException {  
        List<MemoryTest> ms = new ArrayList<>();  
  
        while (true){  
            ms.add(new MemoryTest());  
            Thread.sleep(20);  
        }  
    }  
}

然后看一下 GC 堆内存图

image.png

垃圾收集器

参考: https://www.bilibili.com/video/BV1sp4y1Y7ap

常用垃圾收集器

image.png

Serial 、Serial Old

在 JDK 1.0,1.2 早期的时候,当时使用的垃圾收集器主要使用的是 Serial + Serial Old 这一对

  • Serial 、Serial Old
    • Serial 和 Serial Old 是 单线程垃圾收集器,在GC时,只允许⼀个 线程进⾏
    • Serial ⽤在年轻代采⽤的是 复制算法、Serial Old ⽤在⽼年代采⽤ 的是 标记整理算法
    • 在单核处理器的情况下,简单⾼效,但是多核处理器下⽆法发挥 多核的性能不推荐使⽤,适合 100M以内 内存

image.png

Parallel、Parallel Old

Parallel Scavenge + Parallel Old 是 JDK8 默认使用的垃圾收集器

  • Parallel、Parallel Old
    • Parallel 和 Parallel Old 是 多线程垃圾收集器,是serial系列的多线 程版本
    • Parallel ⽤在年轻代采⽤的是 复制算法,Parallel Old采⽤的是 标 记整理算法
    • 关注点在于吞吐量,⽐较适合CPU密集型场景,⼀般 4G以下 内存 推荐使⽤

image.png

ParNew 、CMS

  • ParNew 与 Parallel类似,只是为了配合 CMS 才出现的

  • ParNew ⽤在年轻代采⽤的是 复制算法, CMS ⽤在⽼年代采⽤的 是 标记清除算法

  • CMS关注点是最⼤停顿时间,也就是**⽤户的体验度,⽐较适合 4~8G 内存的情况使⽤**

  • CMS的运作步骤

    • 初始标记
      • STW,从GC Root出发,只标记直接引⽤对象(不包含内部 成员变量相关的间接引⽤对象)
    • 并发标记
      • 从GC Root的直接引⽤对象出发,遍历整个对象图进⾏标 记,耗时较⻓,由于⽤户线程和GC线程都在运⾏着,所以 会有多标、漏标的问题
      • 多标
        • 多标 就是本应该是垃圾对象,但是由于⽤户线程还 在运⾏,所以没来及去清除标记
      • 漏标
        • 漏标 就是 新来的对象引⽤了GC Root链上的对象, 但是由于⽤户线程还在运⾏,没来得及标记为⾮垃 圾,被GC误清除
        • 漏标 的处理⽅案主要是 三⾊标记算法,主要分为 增量更新 和 原始快照
          • 三⾊标记主要是分为三种颜⾊,分别是⿊⾊、⽩ ⾊、灰⾊
            • ⿊⾊对象 表示 当前对象的引⽤对象图都扫描 完了
            • 灰⾊对象 表示 当前对象的引⽤对象图只扫描 了⼀部分
            • ⽩⾊对象 表示 当前对象的引⽤对象图没扫描
          • 增量更新 是通过 记录下⿊⾊对象新增的⽩⾊对 象引⽤关系,将⿊⾊对象回退到灰⾊对象,重新 深度扫描⼀次
          • 原始快照 是通过 记录下灰⾊对象删除的⽩⾊对 象的引⽤关系,以灰⾊对象为根简单扫描⼀下, 将⽩⾊对象标记为⿊⾊对象,当作浮动垃圾处 理,等待下⼀轮GC
            • 浮动垃圾
              • 浮动垃圾 就是在 并发标记 和 并发清理 阶 段产⽣的垃圾,对GC最终效果影响不 ⼤,只要等待下⼀轮GC处理就⾏
    • 重新标记
      • STW,对 并发标记 过程中产⽣状态改变的对象进⾏修正, 这⾥对于 漏标 的问题采⽤的是 三⾊标记算法 中的 增量更 新 来做的重新标记
    • 并发清理
      • 对未标记的对象进⾏清理,这⾥因为没有进⾏STW,所以 对于新增对象会被标记为⿊⾊对象
    • 并发重置
      • 将对象的标记位进⾏重置,进⾏下⼀轮GC

CMS:ConcurrentMarkSweep 并发标记清除

CMS 的出现通过有效减少 STW 的间隔时间(大概百ms左右)

image.png

CMS 的执行:初始标记 → 并发标记(中间由于是并行的,会存在多标、漏标的问题) → 重新标记 → 并发清理

image.png|500

G1

JDK1.8 之后的内存模型采用了分区模型的方式

从JDK 9开始,CMS GC已经被标记为废弃,并在JDK 14中被完全移除。因此,对于使用JDK 9及以后版本的应用,推荐使用G1 GC或ZGC等其他低延迟垃圾回收器

分区的话会进行回收部分区域(不一定是回收全部区域)

  • G1 跟以往的垃圾收集器有点不同,它对于分代的概念不是物理分 代⽽是逻辑分代了,它将堆默认分成了2048个region,每个region 在每次GC结束后都会有不同的⻆⾊,并且相对于以往的⼤对象, 它也有专⻔的⼀个 Humogous区 来存放,倘若⼀个 Humogous区 放不下⼤对象会⽤连续的⼏个region来存放,对于G1从region来 看采⽤的是复制算法,但是从整体上来看是标记整理算法,G1 和 CMS 的出发点⼀样,但是 G1 ⽐ CMS 更加先进,可控的最⼤停顿 时间,⼀般建议需要500ms以内停顿或者内存超过8G的可以去使 ⽤
  • G1的运作步骤
    • 初始标记 和 CMS的初始标记 ⼀样
    • 并发标记 和 CMS的并发标记 ⼀样
    • 最终标记 和 CMS的重新标记 ⼀样,但是这⾥对于 漏标 的对 象采⽤ 原始快照 的⽅式进⾏处理
    • 筛选回收
      • STW对未标记的 region 进⾏清理,此时会将每个 region 区域 回收价值和成本进⾏排序,根据⽤户的预期停顿时间进⾏ ⽐较来选择合适的回收⽅式,此时并不会把所有垃圾对象 进⾏回收,因为考虑到预期停顿时间,所以只会回收接近 于这个时间的region,剩余的region等待下⼀轮GC进⾏回 收

常用参数

一些常用的JVM参数,用于调整垃圾收集器的行为:

选择垃圾收集器:

-XX:+UseSerialGC:选择串行收集器,适用于单核处理器或小型应用。
-XX:+UseParallelGC:选择Parallel GC,适用于多核处理器的新生代收集。
-XX:+UseConcMarkSweepGC:使用CMS GC进行老年代收集,与Parallel GC配合使用。
-XX:+UseG1GC:使用G1 GC,适用于大堆内存和需要低延迟的应用。
-XX:+UseZGC:使用Z Garbage Collector,适用于需要极低停顿时间的场景。

其中,-XX:+UseConcMarkSweepGC 命令中,老年代使用 的是 CMS GC与Serial Old GC的收集器组合,Serial Old 会作为 CMS GC的后备收集器,当 CMS 挂了后顶上。

设置堆大小:

-Xms<size>:设置JVM启动时的初始堆内存大小。
-Xmx<size>:设置JVM最大堆内存大小。

设置新生代大小:

-Xmn<size>:设置新生代的大小。

设置Eden区与Survivor区的比例:

-XX:SurvivorRatio=<ratio>:设置Survivor区与Eden区的比例。

设置老年代占堆内存的最大比例:

-XX:OldPLABSizePercent=<percent>:设置老年代并行垃圾回收时的PLAB(Promotion Local Allocation Buffer)区域占堆内存的百分比。

设置GC日志参数:

-Xlog:gc:开启GC日志记录。
-Xlog:gc:记录所有GC的详细信息。

设置CMS GC参数:

-XX:+UseCMSInitiatingOccupancyOnly:设置CMS启动的内存占用阈值。
-XX:CMSInitiatingOccupancyFraction=<percent>:设置老年代使用到达一定比例时触发CMS。

设置G1 GC参数:

-XX:G1HeapRegionSize=<region-size>:设置G1的Region大小。
-XX:MaxGCPauseMillis=<time>:设置G1最大GC停顿时间目标。

设置ZGC参数:

-XX:ZSize=<size>:设置ZGC的Z-Heap大小。

堆外内存设置:

-XX:MaxDirectMemorySize=<size>:设置直接内存(堆外内存)的最大值。

垃圾收集器的并行线程数:

-XX:ParallelGCThreads=<number-of-threads>:设置Parallel GC使用的线程数。
-XX:ConcGCThreads=<number-of-threads>:设置CMS的并发标记线程数。

软引用和弱引用的设置:

-XX:SoftRefLRUPolicyMSPerMB=<millis-per-mb>:设置软引用对象在每MB堆内存中存活的最长时间。
-XX:+DisableExplicitGC:禁用System.gc()调用。

元空间(Metaspace)设置:

-XX:MetaspaceSize=<size>:设置类元数据区域的初始大小。
-XX:MaxMetaspaceSize=<size>:设置类元数据区域的最大大小。

堆Dump和分析:

-XX:+HeapDumpOnOutOfMemoryError:当发生OOM时,生成堆转储。
-XX:HeapDumpPath=<directory>:设置堆转储文件的路径。

这些参数可以根据应用程序的具体需求进行调整。在生产环境中,建议进行充分的测试,以确保所选设置不会对性能产生负面影响。此外,随着JDK版本的更新,某些参数可能会被弃用或替换,因此需要查阅对应版本的官方文档以获取最新的信息。

JVM 调优

参考:

一般面试的时候,会说一下背景

工具可以说是内部的监控的工具,然后看到这个现象。

参考说辞

现象

  • 晚上8点是我们的业务高峰,一到高峰的时候,发现TP99耗时会变高,有明显的毛刺,通过排查发现内存使用率也会增大,然后再释放,其他各项指标正常,于是怀疑是GC导致的,观察服务器的GC情况,发现 youngGC 情况如下,大概每5分钟,GC55次,峰值最高可以达到 220 次。 FullGC 比较频繁,每5分钟大慨0.5次,峰值8次。
  • 那么问题在于 Fullgc 频繁,而且 youngGC 峰值也很高。

原因分析

  • FullGC频繁,那么会触发 stop the world。此时会导致我们的系统进行停顿,这个可能是导致我们的系统 tp99 耗时上升的主要原因。由于并发很高,我们的 YoungGc 频繁,那么可能会造成,我们有些本应该在YoungGC就回收的对象,没有回收成功,直接进入了老年代,由于对象的晋升,导致了我们的老年代继续触发 FullGC。于是峰值变高。

优化目标

  • 1、Young GC 次数减少
  • 2、Young GC 耗时减少
  • 3、FullGC 不超过 6 次一天
  • 4、FullGC 耗时减少

优化思路

  • 1、先看垃圾收集器
    • 我们的 jdk 版本为8,并且这个服务未指定特定的收集器,所以走的是我们默认的收集器组台,年轻代为 Parallel Scavenge,老年代为 Parallel Old。
    • 这两种并行收集器的组台提高了系统的吞吐量,而不是低延迟配北
    • 我们首先应该换一个低延迟的收集器。低延迟的组合,我们选择 ParNew 与 CMS 的组台,如果 Jdk 的版本高,其实也可以选择 GI 或者 ZGC 。
  • 2、年轻代参数设置
    • -Xms4096M -Xmx4096M -Xmn1024M,以上的配置只配堆的大小,
    • 像我们年轻代的占比都是走的默认的,-XX:SurvivorRatio=8,,也就是4G * 0.2。总共 0.8 G。
    • 这个就比较小了,观察了一下老年代的对象占用空间,大概是1.5g, 也就是说有一些堆空间其实是空闲的。那么当我们年轻代的空间小,而且并发大的时候,年轻代的对象会激增,并且晋升到老年代。然而收集器Parallel Old 又会导致stw,无法与用户线程并行,那么就会造成我们的服务停顿,TP99升高。
  • 3、元数据区
    • jdk 1.8 后,原来的永久代变为元数据区,如果我们没有指定元数据区的大小。其默认的初始值只有21 M,
    • 那么我们如果是动态代理的对象比较多,就会导致我们的元数据区进行 GC 回收,元数据区的回收也会触发 Full GC,
    • 再次导致我们的 stw。所以我们观看了一下元数据的常驻对象的大小,大概是 1 00M 左右,所以我们直接用参数指定元数据区的大小为 256 M。我们的元数据区的最大容量也同时指定为256M, 防止其进行动态调整。
  • 4、并发预处理
    • 在Full GC发生时,会产生我们的 GCroot 追踪。老年代与年轻代之间又会存在跨年龄引用,如果我们在CMS收集器进行收集之前,进行一次重新标记,其实会减少我们的对象扫描,减少我们的 Full GC 时间。所以我们就让进行FullGC之前,强制做一次MinorGC。我们配置如下参数XX:+CMSScavengeBeforeRemark,这样就减少了我们要扫描的对象,减少了Remart时间。

最终方案

image.png

相关引申

  • TP99
    • TP99是指一组数据从小到大排列,处于99%位置的数据的值。 在工程性能指标中,TP99可以用来表示满足百分之九十九的网络请求所需的最低耗时。
    • 比如我调用别人的一个方法函数,1小时内调用了1w次,监控中显示tp99是200ms,这个意思就是: 百分之99%的调用,都可以在200ms内返回结果。