【GC收集器】和【内存分配与回收的策略】

垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现
不同的收集器应用的区域不同,到现在为止没有最好的收集器,也没有万能的收集器
现代垃圾收集器的演进大部分都是往减少停顿方向发展。
垃圾收集器设计出来都有目标的,有些是为了更高的吞吐,有些是为了更低的延迟。

1、Serial(串行)收集器
•Serail收集器采用标记复制算法,是“单线程”的,用于年轻代的回收器,他在进行垃圾收集时必须暂停其他的所有线程,直到收集结束
•随着收集器的发展,用户线程的停顿时间越来越短,但任然无法消除
•Serial收集器是虚拟机运行在Client模式下默认的新生代收集器
•对于单个CPU坏境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集,可以获得很高的单线程收集效率
2、ParNew收集器
•ParNew收集器是Serial收集器的多线程版本
•ParNew收集器是运行在Server模式下虚拟机中首选的新生代收集器
•在垃圾收集器中“并发”与“并行”的概念:
◾并行:多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
◾并发:用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行在另一个CPU上
3、Parallel(并行) Scavenge收集器
•Parallel Scavenge收集器是一个新生代收集器,采用复制算法
•Parallel Scavenge收集器的特点是他的关注点与其他收集器不同。其他收集器的目标是尽可能的缩短用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控的吞吐量 3.高吞吐量可以高效的利用CPU时间,尽快得完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
•GC停顿时间的缩短是以牺牲吞吐量和新生代空间来换取的
•Parallel Scavenge收集器也经常被称为“吞吐量优先”收集器
4、Serail Old收集器
•Serial Old收集器是Serail收集器的老年代版本,是一个单线程收集器,使用标记-整理算法
•Serail Old收集器主要用于Clinet模式下
•Serail Old收集器另一种用途是作为CMS收集器的后备预案
5、Parallel Old收集器
•Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法
•在注重吞吐量和CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器
6、CMS收集器(concurrent并发)
1.CMS收集器是一种以获取最短的回收停顿时间为目标的收集器
2.CMS收集器基于标记-清楚算法实现,分为四个步骤:初始标记、并发标记、重新标记、并发清除
3.步骤详解:
a.初始标记:标记一下GC Roots能直接关联到的对象,速度很快
b.并发标记:进行GC Roots Tracing,向下追溯所有可达对象。
c.重新标记:是为了修正那些在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,在这一阶段的停顿时间会比初始标记阶段稍长一点
d.并发清除:回收不可达对象,用户线程不会停顿,可能会不断产生垃圾,这些垃圾成为“浮动垃圾”。
4.从总体上说,CMS收集器的内存回收过程是与用户线程一起并发执行的
5.CMS收集器的缺点:
a.CMS收集器对CPU资源非常敏感,GC过程需要预留空间给用户线程。
b.CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full FC的产生
c.由于CMS收集器采用了标记-清除算法,所以在回收结束时会有大量空间碎片产生,碎片过多时,在给大对象分配内存时会有很大麻烦
jdk1.5时,CMS老年代内存占比68%开启,jdk1.6时 92%开启。
7、G1收集器(concurrent并发)
1.G1收集器是一款面向服务端应用的垃圾收集器,G1收集器具备以下特点:
a.并行与并发
b.分代收集
c.空间整合:从整体上来看是基于“标记-整理”算法实现的,在局部上是基于复制算法实现的
2.可预测的停顿
3.G1收集器将整个Java堆划分为多个大小相等的独立区域,虽然还保留有新生代和老生代的概念,但新生代和老生代不再是物理隔的了,他们是一部分Region的集合
4.G1收集器可以有计划地避免在整个Java堆中进行全区域的垃圾收集:跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
5.在G1收集器中,使用Remembered Set来避免全堆扫描
6.每个 region 都需要 RememberSet(记忆集) 来记录各region之间的引用。这个内存的开销其实还是挺大的,可能会占据整堆的20%或以上。G1 相对于 CMS 只有在大堆的场景下才有优势,CMS 比较伤的是 remark 阶段,如果堆太大需要扫描的东西太多。
G1垃圾回收过程:
1、年轻代GC(并发独占式垃圾回收,应用程序STW,启动多线程移动存活的对象到幸存者区或者老年代)
2、老年代并发标记过程(当堆内存达到一定值(默认45%)时,开启)
3、 混合回收(Mixed GC, 老年代标记完成立即触发混合回收,G1 GC从老年代移动存活的对象到空闲区域,G1回收部分Region老年代区域,同时伴随Yong GC)
例如:一个web应用,JAVA最大堆内存4G,每分钟响应1500次请求,每45秒会新分配大约2G的内存,G1会每45秒进行一次年轻代回收,每31个小时整个堆使用率到达45%,会开始老年代并发标记,标记完成开始四到五次的混合回收。
下列情况G1比CMS更好:
1、超过50%的Java堆被活动数据占用。
2、对象分配频率或年代提升频率变化很大。
3、GC停顿时间过长(长于0.5秒)。
8、ZGC收集器
Z Garbage Collector,即ZGC,基于Region内存布局,不设置分代, 使用了读屏障, 染色指针和内存多重映射等技术实现的可并发标记-整理算法的,是一个可伸缩的、低延迟的垃圾收集器,CMS和G1标记的是对象,而ZGC标记的是对象指针。
主要为了满足如下目标进行设计:

  • 停顿时间不会超过10ms
  • 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在10ms以下)
  • 可支持几百M,甚至几T的堆大小(jkd13已经可以支持16T)
    通过压测数据可以看出,相对比JDK15的ZGC,在16版本中,平均GC暂停时间约为0.05毫秒(50微秒),最大暂停时间约为0.5毫秒(500微秒),具有O(1) 暂停时间。换句话说,它们以几乎恒定的时间执行,并且不会随堆,活动集或根集大小(或与此相关的任何其他内容)的增加而增加。16版本中转发表(当ZGC移动对象时,该对象的新地址会被记录在转发表中,该表是在Java堆以外分配的数据结构。每个被选为移动集即需要压缩以释放内存的堆区域集一部分的堆区域都会得到一个与其关联的转发表。)的分配和初始化进行了优化,现在我们不再会多次(可能有几千次)调用 malloc/new 给每个表分配内存,而是一次性分配所有表的所需内存。着通常有助于避免分配开销和潜在的锁竞争,并显著地减少分配这些表所需的时间。
    标记整理算法,jdk16之前,预留出来,不用于Java 线程中的常规堆分配,只允许 gc 在移动对象时使用保留堆。
    jkd16中,ZGC同时使用这两种方法:
    ①当有可用的空堆区域时,不就地移动通常执行得更好。
    ②就地移动,可以保证移动过程即使在没有空堆区域可用时依然能成功完成。
    JDK16 ZGC优化:
    ①、内存对象重定位的优化(跨区复制,就地移动)。
    ②、转发表的分配及初始化的优化。

GC发展历史

有了虚拟机,就一定需要收集垃圾的机制,这就是Garbage collection,对应的产品我们称为Garbage Collector(垃圾回收器)。
1999年随JDK1.3.1一起来的是串行方式的Serial GC,它是第一款GC, parNew垃圾收集器是Serial收集器的多线程版本(并发)。
2002年:2月26日,Para11e1GC和Concurrent Mark Sweep随JDK1.4.2一起发布
Para11el GC在JDK6之后成为HotSpot虚拟机默认垃圾回收器。
2012年,在JDK1.7u4版本中,G1可用。
2017年,JDK9中G1变成默认的垃圾收集器。
2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。
20年9月,JDK11发布:引入Epsilon垃圾回收器,又被称为"No-Op(无操作)"回收器。同时,引人ZGC:可伸缩的低延迟垃圾回收器(Experimental),专注于减少暂停时间的同时仍然压缩堆。
2019年3月,JDK12发布:增强G1,自动返回未用堆内存给操作系统。同时,引入ShenandoahGC:低停顿时间的GC(Experimental)。
2019年9月,JDK13发布:增强ZGC,自动返回未用堆内存给操作系统。
2020年3月,JDK14发布:删除CMS垃圾回收器。扩展ZGC在macos和windows上的应用。
2020年9月,JDK15发布:ZGC由实验阶段变成生成使用阶段。
2021年3月,JDK16发布:ZGC标记-压缩两种移动模式无缝切换(跨区移动,就地移动)。STW时间缩短到1ms以内。

HotSpot虚拟机的垃圾收集器
图中展示了其中作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。
(1)jdk1.6及之前版本中与新生代的Parallel Scavenge收集器与Serial Old搭配。(Parallel Scavenge+Serial Old),作为老年代版本中使用CMS收集器的后备垃圾收集方案。
(2)jkd1.7、8默认新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。
(3)jdk9之后默认的垃圾回收器是G1。
(4)ZGC是jdk11出现,jdk15最新版本的ZGC现在可以投入生产了。简而言之,ZGC是一个可伸缩的低延迟垃圾收集器,最大GC暂停时间为10毫秒,能够处理从几兆字节到多TB的堆,最大吞吐量降低了15%。

PS MarkSweep则就是Parallel Old,通过jconsole可以查看默认回收器是PS MarkSweep,如下图:

比较如图:

内存分配与回收的策略
对象的内存分配,往大方向讲,就是在堆上分配,主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配,少数情况也可能会直接分配在老年代中,具体的分配规则取决于虚拟机自己的设置
1.对象优先在Eden区分配
•大多数情况下,对象优先在新生代的Eden区分配
•当Eden区没有足够的空间时,虚拟机将发起一次Minor GC 3.Minor GC与Full GC:
◾Minor GC:新生代GC,非常频繁,回收速度快
◾Fulll GC:老年代GC,又称为Major GC,经常会伴随一次Minor GC,速度比较慢
2.大对象直接进入老年代
•大对象是指需要大量连续的内存空间的Java对象
•虚拟机提供了一个参数:PretenureSizeThreshold,大于这个参数的对象将直接在老年代分配
3.长期存活的对象将进入老年代
1.虚拟机给每个对象定义了一个对象年龄计数器(Age),对象每经过一次Minor GC后仍然存活,且能被Survivor容纳的话,年龄就 +1 ,当年龄增加到一定程度(默认为15),就会被晋升到老年代中,这个阈值可以通过参数 MaxTenuringThreshold 来设置
4.动态对象年龄的判定
•为了更好的适应不同程序的内存状况,对象年龄不是必须到达阈值才会进入老年代
•当Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,或年龄大于等于该年龄的对象就可以直接进入老年代
5.空间分配担保
•在发生Minor GC之前,虚拟机会首先检查老年代可用最大内存空间是否大于新生代对象总空间—若大于,则会进行一次安全的Minor GC
•若上述条件不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,若不允许,则进行一次FULL GC
•若允许担保失败,则虚拟机会检查老年代最大可用的连续空间是否大于历次晋升到老年代的对象的平均大小,若大于,则进行一次冒险的Minor GC,否则进行一次FULL GC
•若担保失败,还是会进行一次FULL GC,之所以要冒险的原因是为了避免频繁的FULL GC

posted @ 2021-06-16 14:06  倔强的老铁  阅读(207)  评论(0编辑  收藏  举报