垃圾回收相关概念
System.gc()的理解
在默认情况下,会使用System.gc()
或者使用Runtime.getRuntime().gc()
的调用,会触发显示的Full GC
但是System.gc()
调用不一定会执行,也不确定在什么时候执行,它类似一个建议,希望垃圾回收器能够执行Full GC
JVM实现者可以通过System.gc()调用来决定JVM的GC行为,但是在一般情况下GC应该是自动进行的
而System.gc()
底层就是Runtime.getRuntime().gc()
,它们作用是一样的
我们可以使用System.runFinalization()
强制调用使用引用对象的finalize()
方法
内存溢出和内存泄露
内存溢出
没有空闲内存,并且垃圾回收器也无法提供更多内存,再往里面添加数据时会出现OOM
内存泄露
对象不会被程序使用,但是GC不能回收这些空间
尽管内存泄露不会立刻导致程序崩溃,但是一旦发生内存泄露,程序中的泄露内存越来越多,最终就会导致内存溢出
Java中的内存泄露,要从可达性分析算法开始说出内存泄露的情况,但是不能说出引用计数算法开始说出内存泄露
因为Java用的是可达性分析算法
在Java中:
1、比如我们使用某一个单例对象,并且这个对象引用了外部的某个对象,那么这个外部对象是不能被回收的
2、比如有些必须要关闭的资源迟迟不关闭,就会导致内存泄露,比如数据库连接、网络连接、I/O连接等
Stop The World
STW:在整个程序中发生GC时,指的是用户线程需要停止,造成出现这种卡死的状态
安全点和安全区域
安全点
程序不是在所有的时间都可以进行GC,只有特定位置才能停下来进行GC,那么这些特定位置就是安全点Safepoint
安全点的选择比较重要,假如太少那么GC等待时间就太长,假如太频繁就可能导致运行时的性能问题
大部分的指令执行都比较短暂,那么我们就要选择一些执行时间比较长的指令作为安全点,比如方法调用、循环跳转、异常跳转等
那么如何检查GC时,所有线程都跑到最近的安全点停顿下来了?
- 抢先式中断(目前没有虚拟机采用了):首先中断线程,假如还有线程不在安全点,那么就恢复线程,让线程跑到安全点
- 主动式中断:设置一个中断标志,各个线程跑到自己的安全点的时候主动轮询这个标志,假如标志为真,那么自己进行中断挂起
安全区域
有一些用户线程Sleep或者Blocked的时候,没有办法进行执行,这个时候也没有办法响应JVM的中断请求,那么JVM也不可能等待线程被唤醒
那么在这种情况下就需要安全区域,我们可以将安全区域看作扩展的安全点
安全区域指的是在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置GC都是安全的
Java中的引用
引用概述
我们希望能够有这样一类对象:当内存空间足够的时候,则保留在内存中。当内存对象在GC之后还是很紧张,那么就抛弃
这种对象类似我们生活中的装饰品,当空间足够的时候留着是锦上添花,不够的时候去掉也没什么
那么在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为:
- 强引用:Strong Reference
- 软引用:Soft Reference
- 弱引用:Weak Reference
- 虚引用:Phantom Reference
以上四种引用强度依次逐渐减弱
软引用、弱引用、虚引用、终结器引用其实都在包java.lang.ref
下
强引用
我们平常使用的都是强引用Object obj = new Object();
无论任何情况下,强引用关系只要在,那么垃圾收集器永远不会回收被引用的对象
软引用
假如有软引用关系,内存够那么就不回收,内存不足就回收软引用
弱引用
无论有足够的内存,弱引用关联的对象一定会被回收
也就是说,弱引用只能生存到下一次GC
虚引用
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用获得对象实例
他的唯一目的就是在这个对象被回收时获得一个系统通知
强引用:不被回收
我们平常使用的都是强引用Object obj = new Object();
无论任何情况下,强引用关系只要在,那么垃圾收集器永远不会回收被引用的对象,强引用也是造成内存泄露的原因之一
强引用的对象都是可触及的,也就是从GC Roots可达
软引用:内存不足即回收
假如有软引用关系,内存够那么就不回收,内存不足就回收软引用
这个意思是说当一次GC之后发现内存还是不够,那么就需要回收软引用,所以软引用被回收了,和OOM没啥关系
软引用一般用来实现内存敏感的数据,比如高速缓存就需要用到软引用,所以Java虚拟机会尽量让软引用的存活时间长一些,不会轻易清理
通过如下方式来建立一个软引用:
// 强引用
User u = new User();
// 软引用
User u1 = new SoftReference<User>(new User());
// 置空强引用,只留下一个软引用
u = null;
// 上面的三行代码等于这里的一行代码
User u2 = new SoftReference<User>(new User());
在Mybatis中其实用到了很多软引用做缓存
弱引用:发现即回收
无论有足够的内存,弱引用关联的对象一定会被回收
也就是说,弱引用只能生存到下一次GC,但是我们说的是只被弱引用关联
也就是说,假如这个对象还被强引用关联,那么还会存活
User u = new WeakReference<User>(new User());
弱引用其实也比较适合保存可有可无的缓存数据
我们有一个WeakHashMap
,其实继承的就是弱引用,避免了使用HashMap,可以避免OOM的出现
虚引用:对象跟踪
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用获得对象实例,也不能通过get拿到
他的唯一目的就是在这个对象被回收时获得一个系统通知
假如一个对象只有虚引用,那么基本和没有引用是一样的,他的唯一作用就是跟踪垃圾回收的过程,比如在这个过程中能够收到一个系统的通知
终结器引用
了解即可
这个终结器引用就是为了实现finalize()方法,不需要手动编码
垃圾回收器
GC分类和性能指标
垃圾收集器并没有在JVM规范中进行过多规定,可以由不同厂商、不同版本的JVM来实现
GC性能指标
1、吞吐量:运行用户代码的时间占总运行时间的比例
总运行的时间是程序运行时间+内存回收时间,我们希望吞吐量越大越好
2、垃圾收集开销:垃圾收集所占用时间和总运行时间的比例
3、暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间
我们肯定是希望暂停时间越小越好
4、收集频率:相对于应用程序的执行,手机操作发生的频率
5、内存占用:Java堆区所占用的内存大小
我们希望越大越好
6、快速:一个对象从诞生到被回收所经历的时间
我们说内存占用、暂停时间、吞吐量三者构成一个三角,三者不可能同时满足,但是一款优秀的垃圾收集器最多同时满足两项
在三项中,暂停时间的重要性日益凸显,因为随着硬件的发展,内存占用多可以容忍,硬件的提升也提高了吞吐量
所以我们主要抓住两点:吞吐量、暂停时间
但是我们说吞吐量和暂停时间是有矛盾的
要么注重低延迟,要么注重高吞吐量
现在的标准是在可控的暂停时间基础上,增加吞吐量
不同垃圾回收器概述
垃圾收集机制是Java的招牌能力,那么Java的垃圾收集器有几种
1、1993年,随着JDK1.3.1一起来的是串行方式的Serial GC,它是第一款GC。ParNew垃圾收集器是Serial收集器的多线程版本,也就是并行收集
2、2002年,Parallel GC和Concurrent Mark Sweep GC跟随JDK1.4.2一起发布
3、Parallel GC在JDK6之后变为HotSpot默认GC
4、2012年,在JDK1.7u4版本中G1可用
5、2017年,JDK9中G1变为默认的垃圾收集器,代替CMS
6、2018年,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟
7、2018年,JDK11发布,引入 Epsilon 垃圾回收器,又被称为 No-Op(无操作)回收器。同时引入ZGC:可伸缩的低延迟垃圾回收器(一直在测试中)
8、2019年,JDK12发布,增强G1。同时引入Shenandah GC:低停顿时间的GC
9、2019年,JDK13发布,增强ZGC
10、2020年,JDK14发布,删除CMS。扩展ZGC在macOS和Windows上的使用
七个经典的垃圾收集器
串行回收器:Serial、Serial Old
用户线程和垃圾收集线程交替执行,并且垃圾收集线程只有一个
并行回收器:ParNew、Parallel Scavenge、Parallel Old
用户线程和垃圾收集线程交替执行,只不过垃圾收集线程有了多个
并发回收器:GMS、G1
用户线程和垃圾收集线程同时执行
我们说这七个垃圾收集器是比较经典的,新的垃圾收集器没有说
七款经典垃圾回收器和垃圾分代之间的关系
- 新生代收集器:Serial、ParNew、Parallel Scavenge
- 老年代收集器:Serial Old、Parallel Old、CMS
- 整堆收集器:G1
垃圾回收器的组合关系
- 在JDK8中,
Serial GC - CMS GC
、ParNew GC - Serial Old GC
这两个的组合过时了,在JDK9彻底废除 CMS GC - Serial Old GC
是一个后备方案,因为CMS是并发收集器,用户线程和垃圾收集线程一起进行,所以假如CMS回收有点慢那么就跟不上垃圾产出速度了,所以需要一个后备方案Parallel Scavenge GC - Serial Old GC
在JDK14中过时了,未来一定会废弃CMS GC
在JDK14删除了
也就是说,在JDK8中,可以使用如下组合:
Parallel Scavenge GC - Parallel Old GC
:JDK8默认垃圾回收器ParNew GC - CMS GC
:可选Serial GC - Serial Old GC
:可选CMS GC - Serial Old GC
:老年代后备方案
注意我们这里说的组合是说明暂时不会过时的,有一些已经或者将要过时的这里就没有加入
我们说 ParNew 和 Parallel Scavenge都是并行的,搭配CMS为啥有两个结果 这是因为Parallel Scavenge在底层框架上使用的和其他的不同,所以 Parallel Scavenge - CMS就不能同时使用
在JDK9到目前JDK14中默认改变为了G1 GC来代替所有的组合选项,当然我们还是可以自定义组合选项的
查看默认的垃圾回收器
-XX:+PrintCommandLineFlags
:查看命令行相关参数,包括垃圾收集器jinfo -flag 相关垃圾回收器参数 进程ID
:使用命令行,可以间接查看
Serial回收器:串行回收
Serial收集器在HotSpot中Client模式下的默认新生代垃圾收集器,在单核CPU的性能还是不错的
Serial收集器采用复制算法、串行回收、STW机制的方式进行新生代的内存回收
Serial Old收集器同样采用串行回收、STW机制,只不过使用的是标记-压缩算法进行老年代的内存回收
Serial Old是在Client模式下的默认老年代垃圾回收器,在Server模式下有两个用途:
1、新生代的Parallel Scavenge配合
2、作为老年代CMS 的后备方案
Serial 优点:
1、在单核CPU简单高效
2、运行在Client模式下的虚拟机是不错的选择
在程序运行中指定Serial - Serial Old:
-XX:+UseSerialGC
ParNew回收器:并行回收
ParNew收集器可以说是Serial的多线程版本,但是也一样需要用户线程停止进入STW的状态
ParNew是很多JVM在server模式下的新生代的默认垃圾收集器
当然,在JDK8的时候,我们仍然可以采用 ParNew + CMS
或者 ParNew + Serial Old
注意在JDK9的时候ParNew - Serial Old
这组连接已经被完全删除了,在JDK14的时候CMS被移除了
并且我们说当配置低的场景我们使用Serial - Serial Old
并行的场景我们使用Parallel Scavenge - Parallel Old
ParNew的位置十分尴尬,完全没有必要使用ParNew
很明显的,ParNew在多CPU的时候比Serial的效率高,但是单CPU场景那么Serial的性能更好一些
我们可以使用
-XX:+UseParNewGC
来手动指定使用ParNew收集器执行新生代内存回收任务 使用-XX:ParallelGCThreads
限制并行的线程数量,默认开启和CPU相同的线程数
Parallel回收器:吞吐量优先
一句话概括Parallel收集器,那么就可以说是吞吐量优先
Parallel Scavenge收集器同样是基于复制算法、并行回收、STW机制
Parallel Scavenge和ParNew还是有所不同,Parallel Scavenge收集器的目标是达到一个可以控制的吞吐量,他也被称为吞吐量优先的垃圾收集器
自适应调节策略是Parallel Scavenge和ParNew一个重要的区别
Parallel Old则使用的是标记-压缩、并行回收、STW的机制,可以代替老年代的Serial Old
在Java8中默认是这个组合,他在高性能服务器的表现十分不错
-XX:+UseParallelGC
:手动指定年轻代使用Parallel并行收集器执行内存回收任务-XX:+UseParallelOldGC
:手动指定老年代使用Parallel并行收集器 在JDK8中,默认使用以上的两种收集器,这两个参数只要开启一个,另一个也会默认开启,互相激活
-XX:ParallelGCThreads
:设置年轻代并行收集器的线程数,一般我们最好设置和CPU的个数相等 默认情况下,当CPU个数<8,线程数就是CPU个数 当CPU>=8,线程数是3 + [ 5 * CPU_Count] / 8
-XX:MaxGCPauseMillis
:设置垃圾收集器最大停顿时间(也就是STW的时间),单位是毫秒 该参数设置需谨慎,因为收集器工作时会尽量调整堆大小和一些参数,让停顿时间保持在指定的时间内
-XX:GCTimeRatio
:垃圾收集时间占总时间的比例
-XX:+UseAdaptiveSizePolicy
:设置Parallel Scavenge收集器具有自适应调节策略 在这种模式下,年轻代的大小,Eden、Survivior区的比例、晋升老年代的对象年龄等参数会自动调整
CMS回收器:低延迟
CMS:Concurrent Mark Sweep,这是HotSpot虚拟机真正第一款真正意义上的并发收集器,第一次实现了让垃圾收集线程和用户线程同时工作
CMS的关注是尽可能缩短垃圾收集时用户线程的停顿时间,也就是减少STW的时间
我们说STW是不可能消失的,只可能让他尽可能地短,低延迟,非常适合与用户交互的部分
CMS采用的是标记-清除算法,而不是标记-压缩算法
但是我们之前讲过,因为Parallel的底层框架不同,导致CMS不能和Parallel进行合作,只能和Serial或者ParNew二选一
在G1出现之前,CMS使用还是十分广泛的,一直到今天还有很多系统使用CMS
那么因为它是能够和用户线程进行并发执行的垃圾收集器,所以他的执行流程会比较复杂
- 在初始标记阶段,仅仅只是标记出GC Roots能够直接关联的对象,所以在这里的速度十分快,所以STW的时间非常短
- 在并发标记阶段,从CG Roots的直接关联对象开始遍历整个对象图的过程,这里的过程耗时比较长,但是我们不需要停止用户线程,所以在用户的角度感觉不到正在进行GC
在重新标记阶段,是修正一些在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,这个过程比初始标记时间长一点,但是也远远短于并发标记时间
注意,这个重新标记阶段是确认并发标记阶段已经标记的那些垃圾是不是垃圾,而不是对GC Roots重新进行可达性分析
并发清理阶段,可以和用户线程并发执行,清理空间,因为是和用户线程并发处理,所以我们没办法改变对象的地址值,因此使用的是标记-清除算法而不是标记-压缩算法,并且这个阶段比较耗时,但是用户感觉不到
当然我们知道,标记-清除算法会产生一些碎片,那么我们可以后续考虑使用标记-压缩算法来整理一下内存空间
并且还有一件十分重要的事情,那就是我们的CMS必须要提前开启垃圾回收,而不是等到老年代几乎满了的时候开启回收
这是因为CMS是和用户线程并发执行的,用户线程随时都可能产生垃圾,这个时候假如垃圾还没有进行清除,那么会直接OOM
所以我们说的是,当堆内存的使用达到某一个阈值的时候,便开始进行回收
那么假如我们提前开启CMS之后,还是跟不上产生垃圾的速度,那么我们就可以使用CMS的备用方案:Serial Old来重新对来年代进行垃圾收集,但是这样STW的时间就比较长了
Serial Old是标记-压缩算法,这个还算是一个好消息
CMS是很好,但是碎片化的问题是一个致命的炸弹,有可能在一次业务高峰的时候就出现一次Full GC,然后就竟然启动了一个单线程的Serial Old,这个卡顿是我们不能忍受的
CMS缺点:
1、会产生内存碎片,导致内存空间放不下的时候提前触发Full GC
2、CMS对CPU资源非常敏感,在并发阶段虽然不会导致用户停顿,但是会让应用程序变慢
3、CMS无法处理浮动垃圾,也就是说在并发标记阶段出现新的垃圾对象是没有办法标记,只能等待下一次GC
-XX:+UseConcMarkSweepGC
:手动指定CMS收集器执行老年代垃圾收集 年轻代会自动使用ParNew垃圾收集器
-XX:CMSlnitiatingOccupanyFraction
:设置堆内存使用率的阈值 JDK5及以前默认是68,JDK6以上默认是92
-XX:+UseCMSCompactAtFullCollection
:用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生,只不过由于内存压缩整理过程中无法并发执行,所以停顿时间就会变长
-XX:CMSFullGCsBeforeCompaction
:在执行多少次Full GC之后对内存空间进行整理压缩
-XX:ParallelCMSThreads
:设置CMS线程数量
G1回收器:区域化分代式
随着我们的业务越来越复杂、庞大,用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化
为了适应现在不断扩大的内存和不断增长的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量
官方给G1设定的目标是在STW可控的情况下获得尽可能的吞吐量,所以担任了全功能收集器的野望
JDK9之后已经成为了默认垃圾回收器
G1和之前的组合不同,它既可以收集新生代,也可以收集老年代,它的思想是分割区域来实现垃圾收集
G1将堆内存分割为不同的区域(Region),使用不同的Region来表示Eden、Survivor0、Survivor1、Old Gen等
G1会去跟踪每一块Region,判断回收之后能够放开多少内存空间,假如能回收空间较大,那么这个region的价值就比较好
然后G1会根据每一个region的价值列出一张优先级表,根据每次允许收集的空间优先回收价值比较大的region
由于这种方式的侧重点在于回收垃圾比较多的region,所以我们说是垃圾优先
G1主要针对配置多核大容量内存的机器,在极高概率满足GC停顿的时间同时,配备高吞吐量的特征
优点:
1、并行和并发:G1兼具并行和并发
- 并行:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
- 并发:G1拥有与应用程序交替执行的能力
我们可以看到,G1有两种模式,当只有线程并行的时候,所有的垃圾回收器都不可避免地STW,即使是G1
2、分代收集:从分代上来看,G1仍然属于分代型垃圾回收器,但是它的分代不是按照我们传统的Eden,SurVivor,Old等,而是基于Region来进行的分代处理
3、兼顾新生代和老年代:因为是将堆使用了region分割,所以不存在什么新生代老年代了,转而兼顾整个堆
4、空间整合:G1将内存划分为一个个的region,内存的回收是以region作为基本单位的。region之间是复制算法,但是整体上来看是标记-压缩算法。不管怎样,G1是可以避免内存碎片的。
5、可以预测的停顿时间模型:G1除了追求低停顿之外,还可以建立一个可预测的停顿时间模型。让使用者明确指定一个在长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
6、其他的垃圾收集器都需要使用专门的JVM线程执行GC的多线程操作,但是G1可以使用应用线程承担后台运行的GC工作,这样会加速垃圾回收过程
缺点:相对于CMS,G1还不具备全方位、压倒式的优势。比如用户程序运行过程中,G1无论是为了垃圾收集而产生的内存占用还是程序运行的额外负担都要比CMS要高
-XX:+UseG1GC
:使用G1垃圾回收器,JDK9开始就默认使用了,假如是JDK8需要指定-XX:G1HeapRegionSize
:设置每一个Region的大小,值必须为2的幂次方,默认是堆的1/2000-XX:MaxGCPauseMillis
:设置期望达到的最大的GC停顿时间指标(JVM尽量实现,但不一定能达到),默认是200ms-XX:ParallelGCThread
:设置并行的时候STW的时候GC线程数的值,最多设置为8-XX:ConcGCThreads
:设置并发标记的线程数,将n设置为ParallelGCThread的1/4左右-XX:InitiatingHeapOccupancyPercent
:设置触发并发GC周期的Java堆占用率阈值,超过这个值触发GC,默认是45
一般来说,G1只需要设置三步
1、开启G1
2、设置堆的最大内存
3、设计最大停顿时间
G1提供了三种垃圾收集模式:YoungGC、Mixed GC、Full GC,在不同的模式下触发
G1的实际应用场景:
1、大内存,多处理器的机器
2、应用需要低延迟GC,并且堆比较大
3、用来替换CMS
Region:化整为零
所有的Region大小相同,并且在程序运行时不会改变
堆内存被Region划分,但是还是保留原来的策略:Region可以担任不同的角色,比如Eden、Survivor、Old等
但是注意,这里的Region在物理内存上不再是连续的了,并且Region的角色是可以转换的,比如Eden变为了Old
这里的Region在每一个角色期间只能放对应角色的内容,比如一个Region角色现在是Eden区域的,那么他就是放新生的对象 假如Region内存被回收了,那么这个region就可能被转换为了其他的角色,才可以放其他的角色内容
图中就是region的表现方式
空白区域表示未使用的内存空间
H代表的是Humongous,是G1新增的一块内存区域,主要存储大对象,超过0.5个region就放里面
在之前,大对象会直接放到老年代,但是假如它是一个短期存在的大对象,那么就会对垃圾收集产生负面影响 为了解决这个问题,G1划分出了一个Humongous区,为了存储大对象 假如一个H区存不下一个大对象,那么G1会寻找连续的H区来存储,为了找到连续的H区,有时候不得不启动Full GC
记忆集:Remembered Set
一个对象有可能被不同区域引用,这样就有可能会发生Eden区在引用,Old区也在引用
假如我在进行Yong GC的,那还得在老年代去判断是不是有引用这个对象,那么这样就比较崩溃,所以出现了记忆集这种东西
每一个Region都有一个对应的 Remembered Set,当其他Region引用了当前region的对象,那么当前region中的remembered set会记录其他region
所以当我们进行遍历的时候,只需要看一下region中的remember set即可
G1垃圾回收器的回收过程
G1垃圾回收器主要有以下三个环节
1、年轻代GC
2、老年代并发标记过程(这个过程有可能会继续进行年轻代的GC)
3、混合回收
假如有需要,单线程、独占式、高强度的Full GC还是会存在的,它针对GC的评估失败提供了一个失败保护机制,强力回收
一个Web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒会新分配大约2G内存。
G1会每45秒进行一次年轻代回收,每31小时整个堆使用率会达到45%,会开启老年代并发标记过程+年轻代回收
老年代标记完成之后会开始四到五次的混合回收
G1回收器建议
1、年轻代大小
- 避免使用
-Xmn
或者-XX:NewRatio
等选项显示设置年轻代大小 - 固定年轻代大小会覆盖暂停时间目标
2、暂停时间不要过于严苛
- G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间
- 暂停时间过于严苛会导致吞吐量下降
垃圾回收器总结
GC日志分析
-XX:+PrintGC
:输出GC日志,类似-verbose:gc
-XX:+PrintGCDetails
:输出GC详细日志-XX:+PrintGCTimeStamps
:输出GC时间戳(基准方式)-XX:+PrintGCDateStamps
:输出GC时间戳(日期方式)-XX:+PrintHeapAtGC
:进行GC的前后打印出堆的信息-Xloggc:../logs/gc.log
:日志文件的输出路径
垃圾回收器新发展
目前的默认选项G1在不断改进,比如JDK10之后 Full GC已经是并行运行
在Serverless等新场景下,Serial GC找到了新的舞台
CMS比较不幸,由于算法的缺陷,在JDK9标记为废弃,JDK14被移除
1、Epsilon:无操作的垃圾回收器,也就是只做内存分配,不做垃圾回收
2、ZGC:可伸缩的低延迟垃圾回收器
ZGC 和 Open JDK12的Shenandoah GC主打低延迟时间
Open JDK12的Shenandoah GC是第一款不受Oracle领导开发的HotSpot垃圾收集器,不可避免地受到官方排挤
Shenandoah GC旨在针对JVM上的内存回收实现低停顿的需求
ZGC可以说是令人震惊的、革命性的,在尽可能对吞吐量影响不大的情况下,实现在任何堆内存大小都可以将垃圾收集的停顿时间限制在十毫秒内,当然还是和实际请款有关