《八》常见的垃圾回收器

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


常见的垃圾回收器

从jdk到现在jdk版本一共诞生了10种垃圾回收器
在这里插入图片描述

  • 内存分代模型
    前6种属于内存分代模型
    在这里插入图片描述
  • 内存不分代模型
    后面的3种为内存不分代的模型
    在这里插入图片描述
  • Epsilon
    他是什么都不干的垃圾回收器
    在这里插入图片描述

GC的演化

  • 随着内存的不断增长而演进
  • Java刚诞生的时候只有几兆 - 几十兆,最开始的垃圾回收器是Serial
    • Serial 单线程STW垃圾回收,年轻代
    • Serial Old 单线程STW垃圾回收,老年代
  • 当你的内存越来越大,几十兆 - 上百兆甚至到了1G的时候
    数据量小的时候,正常一个单线程垃圾回收没问题,但是当数据量越来越大的时候,垃圾回收Stop的时间越来越长,用户体验上非常不友好
    • Parallel Scavenge (ps)多线程STW垃圾回收,年轻代
    • Parallel Old (po)多线程STW垃圾回收,老年代
  • 内存越来越大,到了几十G
    开几个线程都不行,线程不是越多效率越高,到达某个阈值反而快速降低,因为CPU核限制了线程,一核最多同时执行一线程,派了一万的线程过来只会发生排队与切换的情况,线程数量过多线程的上下文的切换所占的资源超过了线程本身运行所占的资源,效率降低。
    • Concurrent GC(惊天地、泣鬼神)后面的CMS、ParNew、G1、ZGC、Shenandoah都涉及到了

Serial

a stop-the-world, copying collector which uses a single GC thread 新生代

(STW ,使用了一个单线程GC)
如果工作在年轻代,内存满了之后,(STW)业务线全部停止,把垃圾清除掉

Serial Old

a stop-the-world, mark-sweep-compact collector which uses a single GC thread 老年代

(STW ,使用了一个单线程GC)
如果工作在老年代,内存满了之后,(STW)业务线全部停止,把垃圾清除掉

为什么会有两个垃圾回收器Serial 与 Serial Old?

因为按现在的三种垃圾回收算法无论使用那种都会有问题的,为了综合运用,把Java的内存分解成为两个大的年代

  • new 新生代
  • old 老年代

堆大小 = 新生代 + 老年代。其中堆的大小可以通过参数 -Xms,-Xmx来指定
新生代(Young) 与老年代(Old)的比例的值是 1:2 (该值可以通过参数 -XX: NewRatio来指定),即: 新生代(Young) = 1/3的堆空间大小,老年代(Old) = 2/3的堆空间大小。

如何区分是新生代还是老年代?

刚刚诞生的对象优先往新生代里面进行分配(年轻的也就是刚new出来的)
老年代就是经历了多次垃圾回收检测 。任何一个对象经历了一次垃圾回收检测没有被扫描走说明该对象涨了一岁,+1,随着检测的次数越多,年龄越大,当年龄到达了一定阈值(岁数),他就升级成为老年代。

为什么这么设计呢?

原因是因为当一个创建的对象经历了非常多的垃圾回收检测后,还是没有没回收掉,说明这个是一个老顽固,当他是老顽固的时候(老顽固还扫它干嘛),明明知道无法去回收掉,我们就不要每次去扫描它了。把它扔到老年代里面去,啥时候内存不够的时候再去扫描它。
新new出来的对象仍在新生代里面,新生代频繁的进行扫描,效率会提升。

这样就可以根据不同对象的年代特点就可以针对性的使用不同的垃圾回收算法了

新生代与老年代使用垃圾回收算法(重点)
  • 老年代里面可以采用Mark-Compact算法、Mark-Sweep算法、两种混合算法等
  • 新生代里面采用Copying算法(重点
    • 新生代的Copying算法是根据工业实践统计出来的结果进行计算
    • 一般来说一次年轻代的回收可以回收掉90%的垃圾对象 我们称为MinGC或者YGC 例如我们的for 循环遍历不停的new对象,出了这个for循环之后,new出来的对象就回收掉了。
    • 如果一次回收掉90%内存空间,剩余10%就没有必要(使用Copying算法)分成两半了,太浪费空间了,那为什么要使用Copying算法呢
      • 这里是分摊比例为默认的8:1:1
        如下:
        在这里插入图片描述
      • 它是下面的方法来进行复制的
        1. 首先一个对象产生的时候优先放到伊甸区域 例如10个对象进来了扫描掉9个可回收的还剩一个内存空间没有回收掉(活着
          这里的活着就是C语言loading带出来的class对象还有引用,它指向这内存中的class文件,去访问class类的文件内容
          )的
        2. 活着的一个复制扔到survivor中,简称survivor1 (剩余的对象复制到别的地方引用地址会肯定会变化)
        3. 然后伊甸区域整体进行回收
        4. 现在再次进来10个对象,检测出来9个可以回收的一个没有回收掉(活着)的内存
        5. 这时候活着的对象就不复制到survivor1中
        6. 而是把伊甸区域没被回收的内存与survivor1一起扫描
        7. 把survivor1活着的与伊甸区活着的都放到另外一个survivor,简称survivor2
        8. 然后把伊甸区与survivor1的内存整体回收
        9. 下一次又把survivor2复制到上一步回收的survivor1的干净空间简称survivor3,。。。。。。以此类推
        10. 当年龄够了,就到老年代里面。默认情况下,阈值为15,可通过-XX:MaxTenuringThreshold参数来进行调节。

那么,为什么默认是15呢?

因为Mark Word中,每个对象头用一个4bit标志位来记录对象的年龄,而4bit标志位最大只能表示15。

注意:

  • 一个对象就是类加载的最终产品(封装类在Mate Space中的数据结构 ),它一开始优分配到栈空间,然后去往伊甸区域里面
  • Class对象是存放在堆区的,不是方法区,它是封装类在方法区内的数据结构,这点很多人容易犯错。

对象与类加载:
类对象的存放位置:
java.lang.Class 对象是用来表示类的元数据信息的对象,它包含了类的结构信息、方法信息等。
java.lang.Class 对象本身是存放在堆内存中的,而不是存放在方法区或元空间中。
类的元数据信息,如类的结构信息、常量池、方法信息等,会被加载到方法区(Java 8 之前)或元空间(Java 8 后)中。
关于混淆:
有时候会混淆类加载过程中的对象实例和类的元数据信息。对象实例存放在堆内存中,而类的元数据信息存放在方法区或元空间中。
java.lang.Class 对象是用来表示类的元数据信息的对象,它本身是一个普通的 Java 对象,存放在堆内存中。
总结:
对象实例是存放在堆内存中的,而类的元数据信息存放在方法区或元空间中。
java.lang.Class 对象是表示类的元数据信息的对象,它本身存放在堆内存中,用来描述类的结构信息、方法信息等。

  • 类的元数据(元数据并不是类的Class对象!Class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)才是存在方法区的。(在Java 8 中称为元空间(Meta Space),在Java 7 及以前称为永久代(PermGen space),无论怎么变其实都是方法区(Method Area)。
  • survivor装不下了,肯定扔到老年代里面,老年代是承受最终的地方。可以通过-XX:PretenureSizeThreshold参数来调节可以直接进入老年代的对象大小。
  • 对象分配不下了,我就要GC,老年代也会满,老年代满了会FGC(整体回收),新生代频繁的进行GC(新生代满了)

总结:Serial收集器是最基本、最早的收集器,Serial收集器是单一线程,就是在GC的时候STW(Stop The World),暂停所有用户线程,如果GC时间过长,用户可以感到卡顿。Serial Old也是单线程,作用于老年代。

  • 优点:简单高效,拥有很高的单线程收集效率 缺点:需要STW,暂停所有用户线程
  • 算法:Serial采用复制算法,Serial Old采用标记-整理算法

Parallel Scavenge (ps)

Parallel Scavenge (ps)多线程STW垃圾回收,年轻代

新生代收集器,也是复制算法,和ParNew一样并行的多线程收集器,更关注系统的吞吐量(吞吐量=(运行用户代码的时间)/(运行用户代码的时间+GC时间)),Parallel Scavange提供了两个参数用于精确控制吞吐量:

-XX:MaxGCPauseMillis //GC最大停顿毫秒数,必须大于0
-XX:GCTimeRation //设置吞吐量大小,大于0小于100,默认值为99

你会不会觉得把MaxGCPauseMillis设置小点就会让GC速度变快?答案是否定的,如果设置时间过小,Parallel Scavange会牺牲吞吐量和新生代空间来交换,比如新生代400Mb需要GC时间为100ms,设置成50ms了,那么就会把新生代调小为200Mb,这样肯定时间就降下来了,然而这种操作可能会降低吞吐量,原先10s触发一次GC,每次100ms,修改时间后变成5s触发一次GC,每次70ms,那么10ms触发两次GC的时间变成了140ms,吞吐量反而降低。 可以通过参数-XX:+UseAdaptiveSizePolicy开启自适应策略,这样我们不需要手动设置,虚拟机会根据运行情况动态调整。

Parallel Old (po)

Parallel Old (po)多线程STW垃圾回收,老年代
是Parallel Scavange的老年代版本,因为Parallel Scavange无法和CMS搭配使用,所以只能和Serial Old。自从Parallel Old出现,就有了Parallel Scavange+Parallel Old的组合,这是JDK1.8使用的,注重吞吐量的一组收集器。
日常生产环境使用的1.8.0环境,没有换过任何垃圾回收器的时候,默认垃圾回收器为ps + po
我怎么知道我用的是那种垃圾回收器呢?
敲命令 : java -XX:+PrintCommandLineFlags -version
他会把Java虚拟机启动的时候默认的参数给带出来
-XX:InitialHeapSize=263921536
-XX:MaxHeapSize=4222744576
-XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:-UseLargePagesIndividualAllocation
-XX:+UseParallelGC
java version “1.8.0_333”
Java™ SE Runtime Environment (build 1.8.0_333-b02)
Java HotSpot™ 64-Bit Server VM (build 25.333-b02, mixed mode)

  • -XX:+UseParallelGC
    指 定在 New Generation (新生代)使用 parallel collector, 并行收集 , 暂停 app threads, 同时启动多个垃圾回收 thread, 不能和 CMS gc 一起使用 . 系统吨吐量优先 , 但是会有较长长时间的 app pause, 后台系统任务可以使用此 gc

Concurrent GC

Concurrent GC的意思:
当我垃圾回收器开始干活的的时候,你的业务流程可以继续执行不需要Stop,GC线程与业务并发执行,边执行边回收垃圾

CMS (老年代)

concurrent mark sweep 并发的标记清除
a mostly concurrent, low-pause collector 一个多并发、低暂停的收集器

CMS不能与Parallel Scavenge一起使用

CMS的算法理论

cms的四个阶段

  1. 初始标记
    • 从根上开始找,先把根上部分找到,这个叫初始标记
    • 它依然是STW执行,但是只需要STW找到根上的对象,所以STW非常短
  2. 并发标记
    • 业务线程继续,垃圾回收器并发开始工作,标记着那些是垃圾
    • 注意它一定会产生错标
      1. 原来是个垃圾,运行过程中突然有引用指向过来,这时候就需要把标记给纠正过来 (缓存中命中问题,那些没有命中的需要被清理掉,这时候突然又被命中了,会出现这种情况)
      2. 原来不是垃圾,但是在运行过程中突然没有引用了,它又变成垃圾了,这时候需要把他标记成为垃圾
    • 三色标记算法
      • 用在并发阶段进行标记
      • 扫描过程:由于线程是隔一段进行运行,所以它需要把上一次运行阶段要保存下来。
      • 三色——黑色
        • 在标记追踪整个对象过程当中,如果一个对象它自己已经被扫描并且所以孩子也被扫描(根可达算法已经访问到它了并且它的孩子也标记过了)到了,他就是黑色
        • 当我下次垃圾回收线程再回来的时候黑色对象就不再去扫描它了
      • 三色——灰色
        • 在标记追踪整个对象过程当中,如果一个对象它自己本身被扫描到了,但是它的孩子还没有被扫描到,它就是灰色
        • 当我下次垃圾回收线程再回来的时候它不去扫描灰色对象本身,而是扫描它的孩子就可以了
      • 三色——白色
        • 白色是没有遍历到的节点 ,直接扫描
          在这里插入图片描述
      • 特殊情况:
        • 情况一:由于在整个标记的过程当中它的间断运行以及业务线程也在不间断运行,当我们经历一次垃圾扫描之后,还没来得及扫描白色对象的时候由于业务线程的运行,灰色 指向白色的引用消失了,就会导致扫描灰色对象的时候找它的孩子,扫描不到了,本来应该扫描到,但是现在没有扫描到这种情况不会产生严重后果,下一次扫描的话就会当初浮动垃圾给扫描出来
        • 情况二:最严重的情况,在我们扫描的过程当中,扫描完黑色的对象,扫描完灰色的对象,还没来得及扫描白色的对象的时候,这时候业务逻辑开始运行,灰色指向白色的引用消失了与此同时黑色指向白色的引用增加了,当下一次垃圾线程再次运行扫描的话,如果不对这些对象修复的话,现在黑色的指向白色,它不会在去扫描它的孩子(白色),灰色本来指向这白色可以扫描它的孩子白色,但它们的引用消失了,这时候就扫描不到白色的(那这个数据会不会被我们当成垃圾呢?会不修正会被清掉)怎么解决?如果有一个对象是黑色的,它指向了一个新的对象,这时候需要它黑色变为灰色
          在这里插入图片描述
    • CMS解决不了的BUG (Incremental Update的非常隐蔽的问题,CMS采用的这个算法有bug,并不是CMS有bug)并发标记产生漏标
      • 现在有两个垃圾回收线程,垃圾回收线程M1标记完一个A对象并且它的孩子也只标记了一半这时候该对象是灰色,这时候业务线程M2来了,它把A对象标记完的孩子对象指向一个白色的对象D,之前一个灰色对象B指向白色对象D的引用也消失了,这时候再次进来一个垃圾回收器线程M3,站在这个垃圾回收线程的角度上看,这个对象A应该为灰色,因为它孩子有了新引用,突然原来这个线程M1插进来了,会把A对象标为黑色,完蛋,这个D依然扫描不到。
        在这里插入图片描述
  3. 重新标记 (把漏标与错标的重新给修正过来)
    • 是一个纠正过程,纠正过程不能让原线程(业务线程)继续
    • 这里也是STW执行,但是标记错标的情况不是特别多,所以这里的STW还是不是太长的
    • 由于上述的CMS的漏标问题,所以在remark阶段,必须重头扫描
  4. 并发清理
    • 重新标记好了,就进行并发处理

注意:
CMS并发的,STW时间还是慢,是遇到的STW时间最长的,一旦产生STW,短着几小时,长着好几天
CMS的致命缺点:

  • 内存的碎片化 由于它产生的算法是mark sweep 所以他会碎片化,当有一个大的内存进来时就进不来了,这时候产生FGC,但是随着内存的越来越大一次FGC可能会扫描3天
  • 浮动垃圾

CMS会产生一些浮动垃圾,G1也会产生一些浮动垃圾,这些这都不重要,主要是它产生的漏标与错标问题

ParNew (新生代常与CMS一起使用)

a stop-the-world, copying collector which users multiple GC threads
it differs from “Parallel Scavenge” in that it has enhancements that make it usable with CMS(它与ParallelScavenge 本质区别的是它有一些增强与CMS更好的配合使用)

工作在年轻代的多线程垃圾回收器

problem of the old GCs
Serial GC
Parallel GC
PN + CMS
这些GC都有一个问题,操作必须扫描整个老年代或者新生代,不适合大内存 YO都是独立的内存快,大小必须提前确定

ParNew是Serial的多线程版本,实现了并行收集,原理跟Serial一致(并行指的是多个GC线程并行,但是用户线程还是暂停,并发指的是用户线程和GC线程同时执行)。ParNew默认开启和CPU个数相同的线程数进行回收。

  • 优点:
    在多CPU时,比Serial的效率高。 缺点:还是需要STW,单CPU时比Serial效率低
  • 算法:
    复制算法

Epsilon

什么都不干的垃圾回收器,jdk11才有的垃圾回收,它只是做个记录。
作用:

  • 主要用来开发JVM人员用来deBug使用,用来跟踪垃圾回收器回收
  • 第二种作用是假如我的内存超级大,即使我的程序执行完成也根本用不完这些内存,我干脆使用Epsilon,什么都不用干,什么程序运行完了,直接结束,整个内存空间消失,效率极高。

G1(GC tuning)

在这里插入图片描述
G1终于摒弃了分代模型。
G1是以优化GC停顿时间为目标的收集器,它尝试以高概率满足GC停顿时间为目标,同时实现高吞吐量。在G1中,将堆的整个内存布局做了修改,在G1中整个堆划分为多个大小相等的独立区域Region,虽然在逻辑上还保留了新生代和老年代,但是物理上已经隔离了
为什么?
因为随着内存的越来越大,就算怎么采用并发,速度都更不上,STW的时间越来越长,而且CMS固有的bug,需要必须改进,所以诞生了G1
分区算法 Region

  • 核心:部分回收

    • 我把堆空间分成一个一个小区域,在每个小区域里面,我只回收其中的几个,有那些区域快满了,我就先把它回收掉,理想的情况是:让堆空间永远有空的地方可用。
    • 需要连续内存放到Humongous里面,如果连不上了,就开始整理FullGC
    • G1的毛病在于,它的一次回收要把年轻代(YGC)全部回收完,当你的年轻代很大的时候,一次YGC产生的STW时间也是非常长,因此诞生了ZGC(分页算法)
  • G1物理上不分代,但是逻辑上分代,它在内存上的某块地方,逻辑上可能分为伊甸区,survivor区 ,但是在物理上,他是一个一个小区域。

  • G1里面的内存区域不是固定的E或者O,例如一开始在伊甸区域存在一个对象,被垃圾回收清除掉,下一次对象创建的是在这个内存空间可能是Old区域,内存区域不固定

这样划分以后,使得G1不必每次都区收集整个堆空间,而是以增量的方式来进行处理:每次只处理一部分内存块,称为此次GC的回收集。
G1的另一个创新是,在并发阶段估算每个小堆块存活对象的总数。
依据这些垃圾的数量,就可以计算出那些小块现在垃圾比较多,就可以优先去处理垃圾比较多的。进而提高整个垃圾回收的效率。
构建回收集的原则是:垃圾最多的小块会被优先收集。这也是G1名称的由来。
上图被划分成一组大小相同的Region,每个Region都是连续的虚拟内存范围,G1可以知道哪个Region区域内大部分是空的(估算每个小堆块存活对象的总数),这样就可以在每次允许的收集时间内优先回收价值最大的Region区域(根据回收所获得的空间大小以及回收需要的时间综合考虑)所以这就是G1叫做Garbage-First的原因。
G1的工作流程和CMS很相似,区别在最后的步骤。也有四步:

  • 初始标记:
    需要STW,标记下GC Roots关联的对象,并且修改TAMS(Next Top at Mark Start)的值,使得下一阶段并发运行时,能在正确可用的Region中创建对象。
  • 并发标记:
    和CMS一样,主要是进行GC Roots的向下搜索,找出存活对象进行标记。
  • 最终标记:
    需要STW,和CMS一样,这个阶段是修正并发标记期间因用户程序运行而导致变动的对象。
  • 筛选回收:
    对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间制定回收计划。

G1的第一个重点是为运行需要大堆且GC延迟有限的应用程序的用户提供解决方案,这就意味着堆大小约为6G或更大,并且稳定且可预测的暂停时间低于0.5秒。如果应用程序具备以下特性,可以考虑切换到G1收集器:

  • 超过50%的Java堆被实时数据占用
  • 对象分配率或提升率差异很大
  • 当前应用程序GC停顿时间超过0.5秒,又想缩短停顿时间
G1 GC–配置参数
-XX:+UseG1GC启用 G1 GC
-XX:G1NewSizePercent初始年轻代占整个 Java Heap 的大小,默认值为 5% 。最开始只是用非常少量的这种小的块来作为整个Young区。
-XX:G1MaxNewSizePercent最大年轻代占整个 Java Heap 的大小,默认值为 60%
-XX:G1HeapRegionSize设置每个 Region 的大小,单位 MB,需要为 1、2、4、8、16、32 中的某个值,默认是堆内存的1/2000。如果这个值设置比较大,那么大对象就可以进入 Region 了
-XX:ConcGCThreads与 Java 应用一起执行的 GC 线程数量,默认是 Java 线程的 1/4,减少这个参数的数值可能会提升并行回收的效率,提高系统内部吞吐量。如果这个数值过低,参与回收垃圾的线程不足,也会导致并行回收机制耗时加长
-XX:G1HeapWastePercent (G1堆浪费百分比)G1停止回收的最小内存大小,默认是堆大小的 5%(低水位)。GC 会收集所有的 Region 中的对象,但是如果下降到了 5%,就会停下来不再收集了。就是说,不必每次回收就把所有的垃圾都处理完,可以遗留少量的下次处理,这样也降低了单次消耗的时间
-XX:G1MixedGCCountTarget设置并行循环之后需要有多少个混合 GC 启动,默认值是 8 个。老年代 Regions的回收时间通常比年轻代的收集时间要长一些。所以如果混合收集器比较多,可以允许 G1 延长老年代的收集时间
-XX:+G1PrintRegionLivenessInfo:这个参数需要和-XX:+UnlockDiagnosticVMOptions 配合启动打印 JVM 的调试信息,每个Region 里的对象存活信息
-XX:G1ReservePercentG1 为了保留一些空间用于年代之间的提升,默认值是堆空间的 10%。因为大量执行回收的地方在年轻代(存活时间较短),所以如果你的应用里面有比较大的堆内存空间、比较多的大对象存活,这里需要保留一些内存
-XX:+G1SummarizeRSetStats这也是一个 VM 的调试信息。如果启用,会在 VM 退出的时候打印出 Rsets 的详细总结信息。如果启用就会阶段性地打印 Rsets 信息
-XX:+G1TraceConcRefinement这个也是一个 VM 的调试信息,如果启用,并行回收阶段的日志就会被详细打印出来
-XX:+GCTimeRatio这个参数就是计算花在 Java 应用线程上和花在 GC 线程上的时间比率,默认是 9,跟新生代内存的分配比例一致。这个参数主要的目的是让用户可以控制花在应用上的时间,G1 的计算公式是 100/(1+GCTimeRatio)。这样如果参数设置为9,则最多 10% 的时间会花在 GC 工作上面Parallel GC 的默认值是 99,表示 1% 的时间被用在 GC 上面,这是因为 Parallel GC 贯穿整个 GC,而 G1 则根据 Region 来进行划分,不需要全局性扫描整个内存堆
-XX:+UseStringDeduplication手动开启 Java String 对象的去重工作,这个是 JDK8u20 版本之后新增的参数,主要用于相同String 避免重复申请内存,节约 Region 的使用
-XX:MaxGCPauseMills预期 G1 每次执行 GC 操作的暂停时间,单位是毫秒,默认值是 200 毫秒,G1 会尽量保证控制在这个范围内
-XX:+InitiatingHeapOccupancyPercent(简称 IHOP)(启动堆占用百分比)G1 内部并行回收循环启动的阈值,默认为 Java Heap的 45%(高水位)。这个可以理解为老年代使用大于等于 45% 的时候,JVM 会启动垃圾回收。这个值非常重要,它决定了在什么时间启动老年代的并行回收

IHOP 为何重要:因为如果老年代只使用了比较少的时候就做回收,这时要回收的整个对象比较少,垃圾也相对比较少,就会执行的特别快。如果这个比例特别大,整个老年代现在使用的量特别大,这时做垃圾回收时间就会特别长。
在这里插入图片描述

如何选择收集器
  1. 串行收集器:Serial和Serial Old单线程收集,适用于内存较小的嵌入式设备。
  2. 并行收集器【吞吐量优先】:Parallel Scanvage+Parallel Old,适用于科学计算、后台处理等场景。
  3. 并行收集器【GC停顿时间优先】:CMS和G1,适用于对时间有要求的场景,例如Web应用。

ZGC(zero paused GC)

(分页算法)goLang的核心算法就是ZGC

  • 设计目标:
    • 支持TB级别 (4T,据说已经扩展到16T)
    • 最大GC停顿10ms
    • 内存增大,停顿时间不长
    • throughput 不超过15%的影响,据测算128G堆内存,单次GC最大停顿1.68ms,平均1.09ms
  • 核心算法:
    • 颜色指针:
      在这里插入图片描述
      • 它不在分代了,它每100ms的时候进行触发GC把那些内存满的区域给拎出来给回收掉(而且它使用复制算法进行回收不会产生碎片)。

Shenandoah

这是JDK12才开始引入的新的垃圾回收器,是著名的红帽RedHat开源的,它与ZGC的算法类似

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值