JVM调优 - 理解GC
Posted zero13_小葵司
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM调优 - 理解GC相关的知识,希望对你有一定的参考价值。
关于JVM调优系列
Hey Guys,我们将开启这个JVM的调优系列,那么什么是JVM调优呢?其实说起来我们能调的东西也不多,因为这个不是让我们去修改JVM的源码或者修改其GC算法,这里主要是去针对生产出现的OOM、卡顿、CPU飙升、假死等情况去排查处理,以及减少排查问题、GC带来的过多性能消耗影响到生产应用的运行。
这个系列我们依然先从基础入手,然后针对各种生产问题,去看看如何排查、定位、解决。
GC
GC就是垃圾回收,试想我们的系统在不断地创建新的对象与引用,这些都会占用内存空间,如果不对不再有用的对象进行清理,内存很快就会占满(OOM),或者一直有线程在不断占用CPU不被释放(CPU飙升)。
到这里,我们已经知道GC要解决的问题以及重要性了,但是通常情况下我们是不是没有太关注GC的问题呢?因为这个在JVM底层都帮我们做了,但是在高性能(高并发、低延迟)的要求下,GC常常是问题很关键的点,那么我们一起来看看吧。
什么是垃圾
要做一件事,我们首先要明确对象是谁,GC是垃圾回收,那么就要确定什么会被JVM定义为垃圾。
垃圾就是不再被引用的对象,那么怎么确定是否有被引用呢?这里有两种主要的方式:
- reference count:看看这个对象被引用了多少次,引用次数不为0便不回收,好处是执行比较快,坏处是如果出现循环引用会导致无法被回收。JS Python 是这种方式的代表。
- root searching:从根节点一个一个找是否有引用,java便是这样的。
垃圾回收算法
我们大概讲一下垃圾回收的算法,因为不同的GC类型,基本都是这三种算法的组合与变形。
Mark-Sweep 标记清除
这个算法最简洁快速,但是缺点是碎片化严重,因为我们标记出了这是垃圾后,就直接进行了清除。
Copying 拷贝
这个算法会对内存进行整理,留一半用一半,清理时把非垃圾的整理迁移到空的一半,把垃圾全清掉。
这样的好处是内存连续性,坏处是内存的浪费。
Mark-Compact 标记压缩
对有用的对象进行标记,然后进行整理压缩,再将垃圾进行回收,这在现在的GC重会使用比较多。
GC的模型类型
GC的算法主要就是上面三种,基于上面三种算法衍生出了几大类型以及其不同的实现,那么这些不同的类型与实现到底是在解决什么问题呢?其实他们想要解决的问题也就三大类:
- Heap 区间的内存占用(堆外辅助内存空间大小)
- 无 GC 情况下的吞吐量(读写屏障的影响)
- 延迟停顿(STW 时长)
有没有算法能针对这三点做到我全都要呢?这是比较难的,就像做项目想要又好又快又便宜一样。所以各种做法都在尝试去寻找三者之间的平衡最优解。
分代模型
传统的经典GC都是采用的分代模型,包括JDK 8,如果我们没有做过参数修改,其默认的GC方式Parallel也是分代模型。
分代模型将堆内存分为新生代与老年代,一个对象从创建开始,是如何被分配到新生代与老年代呢?GC又是如何回收这些内存呢?
我们看到比较多的以下类型都是分代模型,他们虽然可以交叉使用,但是一般成对使用,一个针对新生代一个针对老年代:
Serial 收集器、Serial Old 收集器
Parallel Scavenge 收集器、Parallel Old 收集器
ParNew 收集器、CMS 收集器
Parallel
我们以Parallel来讲讲分代模型,因为这是JDK 8 的默认方式。
- 新生代
- Eden区,一个新建的对象会先进入eden区(这里已经入堆了,如果在栈内便不再被使用,OS直接将其从栈中拿出删除,不需要进行GC);
- 如果一个对象过大,直接进入老年期,不进入新生代;
- 当eden重的对象在扫描时不被引用,则触发GC,如被引用则进入 survivor区;
- survivor区之间的迁移使用的拷贝算法变种,只拷贝活跃对象;
- GC发生在新生代区;
- 老年代
- 前面说的如果过大的新生对象也会直接进入老年代;
- 当每次扫描时,一个对象被引用,到15代时进入老年代(15代因其代数存储为4bit);
- 当老年代满了时,发生FGC;
CMS
CMS是一个承前启后的收集器,前面的收集器都是发生收集时,会停止业务逻辑处理,而CMS是并发处理,但是CMS会有两个严重问题:
- 浮动垃圾
- 错标,错标会更严重一些,看起来有点像ABA问题
分代分区模型
分代、分区模型的代表是 Garbage First(G1),这也是JDK 11的默认GC,虽然其最早出现是在JDK 7。
G1
G1也能并发进行垃圾回收,与CMS相比,其优点如下:
- G1在GC过程中会进行整理内存,不会产生很多内存碎片
- G1的STW更可控,可以指定可期望的GC停顿时间
G1 将 Java 堆空间分割成了若干相同大小的区域,即 region,包括 Eden、Survivor、Old、 Humongous 四种类型。其中,Humongous 是特殊的 Old 类型,专门放置大型对象,在G1中将内存区域划分为多个不连续的区域(Region),每个Region内部是连续的。
在划分的区域中H区(Humongous),这表示这些Region存储的是巨大对象(humongous object,H-obj),大小大于等于region一半的对象。
一个Region的大小可以通过参数-XX:G1HeapRegionSize复制代码设定,取值范围从1M到32M,且是2的指数。如果不设定,那么G1会根据Heap大小自动决定,JVM 会尽量划分2048个左右、同等大小的 Region。
分区模型
Shenandoah
分区模型的代表是 Shenandoah,Shenandoah 与 G1 有很多相似之处,比如都是基于 Region 的内存布局,都有用于存放大对象的 Humongous Region,默认回收策略也是优先处理回收价值最大的 Region。Shenandoah 使用连接矩阵 (Connection Matrix) 记录跨 Region 的引用关系,替换掉了 G1 中的记忆级 (Remembered Set)。Shenandoah 的内存模型是不分代的。
Shenandoah 主要目标是99.9%停顿小于10ms,暂停与堆大小无关,其发生GC的步骤大致如下:
Init Mark 启动并发标记。它为堆和应用程序线程准备并发标记,然后扫描根集。这是流程中的第一个暂停,最主要的消耗是根集扫描。因此,其持续时间取决于根集大小。
Concurrent Marking 遍历堆,并跟踪可访问的对象(三色标记对象)。此阶段与应用程序一起运行,其持续时间取决于活动对象的数量和堆中对象关系的结构。由于应用程序可以在此阶段自由分配新数据,因此在并发标记期间堆占用率会上升。这里主要运用的是SATB(snapshot-at-the-beginning)
Final Mark 通过清理所有等待的标记,更新队列,重新扫描根设置三个并发的来完成标记。(这里主要是处理一些SATB没有处理完的引用)它还通过确定要撤离的区域(收集集合),预先疏散一些根来初始化疏散,并且通常为下一阶段准备运行时间。这项工作的一部分可以在Concurrent Cleanup阶段同时完成。这是周期中的第二个暂停,这里消耗最主要的时间是清理队列并扫描根集。
Concurrent Cleanup 回收立即垃圾区域。 即在并发标记之后检测到的没有活动对象的区域。
Concurrent Evacuation 将对象集合从集合集复制到其他区域。这是与其他OpenJDK GC的主要区别。此阶段再次与应用程序一起运行,因此应用程序可以自由分配。其持续时间取决于为流程选择的集合集的大小。
Init Update Refs 初始化更新引用阶段。除了确保所有GC和应用程序线程都已完成疏散,然后为下一阶段准备GC之外,它几乎没有任何作用。这是周期中的第三次暂停,最短暂停。
Concurrent Update References 遍历堆,并更新对并发撤离期间移动的对象的引用。 这是与其他OpenJDK GC的主要区别。 它的持续时间取决于堆中的对象数,但不取决于对象图结构,因为它会线性扫描堆。此阶段与应用程序同时运行。
Final Update Refs 通过重新更新现有根集来完成更新引用阶段。它还从集合集中回收区域,因为现在堆没有对它们的(陈旧)对象的引用。这是循环中的最后一次暂停,其持续时间取决于根集的大小。
Concurrent Cleanup 回收集合集区域,现在没有引用。
————————————————
版权声明:本引用为CSDN博主「Aaron_涛」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_33330687/article/details/90314347
但是该方式需要大内存以及较高的CPU占用,可能也会导致发生GC时的吞吐量降低(GC都会有这样的问题)
分层分区模型
ZGC
ZGC是该类型的代表,但是目前还没有成熟应用,而其也因为RSS可能达到堆内存3倍的问题,对于其生产应用有一定的困难。
ZGC是为大内存、多cpu而生,它通过分区的思路来降低STW,但是实际生产运用还需要再观察观察。
下一篇内容
下一篇我们将讲述一下如何进行JVM调优,针对不同问题如何找出其问题并进行优化。
以上是关于JVM调优 - 理解GC的主要内容,如果未能解决你的问题,请参考以下文章