GC问题分析与解决方案(美团技术文章笔记-待完整)
2021-02-01 16:18:06 0 举报
AI智能生成
美团公众号文章,学习整理
作者其他创作
大纲/内容
3.场景调优实践
分析和解决九种CMS中常见的GC问题场景
总结优化经验
建议
GC问题分析与解决方案(美团技术文章笔记-待完整)
GC问题
响应时间(RT)上涨四大表象,如何确定哪个是诱因
GC耗时增大
线程Block增多
慢查询增多
CPU负载高
GC问题影响因素
如何判断GC没有问题
使用CMS有哪些常见问题
1.建立知识体系
GC的基础知识
GC语义
Garbage Collection: 垃圾收集技术 名词
Garbage Collector:垃圾收集器 名词
Garbage Collecting: 垃圾收集动作 动词
Mutator
生产垃圾的角色,也就是我们的应用程序,垃圾制造者,通过 Allocator 进行 allocate 和 free。
TLAB (Thread Local Allocation Buffer)
基于 CAS 的独享线程(Mutator Threads)可以优先将对象分配在 Eden 中的一块内存,因为是 Java 线程独享的内存区没有锁竞争,所以分配速度更快,每个 TLAB 都是一个线程独享的。
卡表 (Card Table)
目的
卡表的本质是用来解决跨代引用的问题
作用
主要是用来标记卡页的状态,每个卡表项对应一个卡页,当卡页中一个对象引用有写操作时,写屏障将会标记对象所在的卡表状态改为 dirty
具体怎么解决的可以参考 StackOverflow 上的这个问题 how-actually-card-table-and-writer-barrier-works,或者研读一下 cardTableRS.app 中的源码。
JVM的内存结构
Java 8 的内存结构
GC 主要工作在 Heap 区和 MetaSpace 区(上图蓝色部分),在 Direct Memory 中,如果使用的是 DirectByteBuffer,那么在分配内存不够时则是 GC 通过 Cleaner#clean 间接管理。
垃圾收集的算法和收集器
任何自动内存管理系统都会面临的步骤:为新对象分配空间,然后收集垃圾对象空间
分配对象
Java 中对象地址操作主要使用 Unsafe 调用了 C 的 allocate 和 free 两个方法
空闲链表(free list)
通过额外的存储记录空闲的地址,将随机 IO 变为顺序 IO,但带来了额外的空间消耗
碰撞指针(bump pointer)
通过一个指针作为分界点,需要分配内存时,仅需把指针往空闲的一端移动与对象大小相等的距离,分配效率较高,但使用场景有限。
收集对象
识别垃圾
引用计数法(Reference Counting)
对每个对象的引用进行计数,每当有一个地方引用它时计数器 +1、引用失效则 -1,引用的计数放到对象头中,大于 0 的对象被认为是存活对象。
循环引用的问题可通过 Recycler 算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作
引用计数法是可以处理循环引用问题
早期的编程语言会采用此算法
可达性分析,又称引用链法(Tracing GC)
从 GC Root 开始进行对象搜索,可以被搜索到的对象即为可达对象,此时还不足以判断对象是否存活/死亡,需要经过多次标记才能更加准确地确定,整个连通图之外的对象便可以作为垃圾被回收掉。
Java 中主流的虚拟机均采用此算法。
收集算法(不同的收集器也是在不同场景下进行组合)
Mark-Sweep(标记-清除)
整个算法在不同的实现中会使用三色抽象(Tricolour Abstraction)、位图标记(BitMap)等技术来提高算法的效率,存活对象较多时较高效
Mark-Compact (标记-整理)
这个算法的主要目的就是解决在非移动式回收器中都会存在的碎片化问题
主要实现有双指针(Two-Finger)回收算法、滑动回收(Lisp2)算法和引线整理(Threaded Compaction)算法等
Copying(复制)
将空间分为两个大小相同的 From 和 To 两个半区,同一时间只会使用其中一个,每次进行回收时将一个半区的存活对象通过复制的方式转移到另一个半区
有递归(Robert R. Fenichel 和 Jerome C. Yochelson提出)和迭代(Cheney 提出)算法,以及解决了前两者递归栈、缓存行等问题的近似优先搜索算法。
优缺点:复制算法可以通过碰撞指针的方式进行快速地分配内存,但是也存在着空间利用率不高的缺点,另外就是存活对象比较大时复制的成本比较高
比较
否移动对象、空间和时间方面的一些对比,假设存活对象数量为 *L*、堆空间大小为 *H*
mark、sweep、compaction、copying 耗时比较
注意:GC 带来的开销不能只看 Collector 的耗时,还得看 Allocator 。如果能保证内存没碎片,分配就可以用 pointer bumping 方式,只需要挪一个指针就完成了分配,非常快。而如果内存有碎片就得用 freelist 之类的方式管理,分配速度通常会慢一些。
收集器
Hotspot VM 中主要有分代收集和分区收集两大类,逐渐向分区收集发展
分代收集器
ParNew
1.一款多线程的收集器,采用复制算法
2.主要工作在 Young 区,
3.可以通过 -XX:ParallelGCThreads 参数来控制收集的线程数,
4.整个过程都是 STW 的,常与 CMS 组合使用。
CMS (Concurrent Mark Sweep)
以获取最短回收停顿时间为目标,采用“标记-清除”算法
分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW
多数应用于互联网站或者 B/S 系统的服务器端上
JDK9 被标记弃用,JDK14 被删除,详情可见 JEP 363。
https://openjdk.java.net/jeps/363
分区收集器
G1
一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求
ZGC
JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。
Shenandoah
由 Red Hat 的一个团队负责开发,与 G1 类似,基于 Region 设计的垃圾收集器,但不需要 Remember Set 或者 Card Table 来记录跨 Region 引用,停顿时间和堆的大小没有任何关系。停顿时间与 ZGC 接近
不同收集器比较
常用收集器
目前使用最多的是 CMS 和 G1 收集器,二者都有分代的概念
CMS和G1内存结构
其他收集器
Metronome、Stopless、Staccato、Chicken、Clover 等实时回收器,Sapphire、Compressor、Pauseless 等并发复制/整理回收器Doligez-Leroy-Conthier 等标记整理回收器
常用工具
命令行终端
标准终端类
jps、jinfo、jstat、jstack、jmap
功能整合类
jcmd、vjtools、arthas、greys
可视化界面
简易
JConsole、JVisualvm、HA、GCHisto、GCViewer
进阶
MAT、JProfiler
命令行推荐 arthas ,可视化界面推荐 JProfiler
2.确定评价指标
问题
了解基本GC的评价方法
如何设定独立的系统指标
业务场景中判断GC是否存在问题的手段
设定评价标准
延迟(Latency)
也可以理解为最大停顿时间,即垃圾收集过程中一次 STW 的最长时间,越短越好,一定程度上可以接受频次的增大
GC 技术的主要发展方向。
吞吐量(Throughput)
应用系统的生命周期内,由于 GC 线程会占用 Mutator 当前可用的 CPU 时钟周期,吞吐量即为 Mutator 有效花费的时间占系统总运行时间的百分比例如系统运行了 100 min,GC 耗时 1 min,则系统吞吐量为 99%,吞吐量优先的收集器可以接受较长的停顿。
系统总运行时间 = Mutator有效花费时间 + GC消耗时间
吞吐量 = Mutator有效花费时间/系统总运行时间
一次停顿的时间不超过应用服务的 TP9999,GC 的吞吐量不小于 99.99%
Footprint(资源量大小测量)、反应速度等指标
多嵌入式系统则追求 Footprint。
读懂 GC Cause
几种典型的GC Cause
System.gc()
手动触发GC操作
CMS
CMS GC 在执行过程中的一些动作,重点关注 CMS Initial Mark 和 CMS Final Remark 两个 STW 阶段。
Promotion Failure
Old 区没有足够的空间分配给 Young 区晋升的对象(即使总可用内存足够大)
Concurrent Mode Failure
CMS GC 运行期间,Old 区预留的空间不足以分配给新的对象,此时收集器会发生退化,严重影响 GC 性能。
GCLocker Initiated GC
如果线程执行在 JNI 临界区时,刚好需要进行 GC,此时 GC Locker 将会阻止 GC 的发生,同时阻止其他线程进入 JNI 临界区,直到最后一个线程退出临界区时触发一次 GC。
判断是不是 GC 引发的问题
到底是结果(现象)还是原因,在一次 GC 问题处理的过程中,如何判断是 GC 导致的故障,还是系统本身引发 GC 问题
四种判断方法
时序分析
先发生的事件是根因的概率更大,通过监控手段分析各个指标的异常时间点,还原事件时间线
如先观察到 CPU 负载高(要有足够的时间 Gap),那么整个问题影响链就可能是:CPU 负载高 -> 慢查询增多 -> GC 耗时增大 -> 线程Block增多 -> RT 上涨。
概率分析
使用统计概率学,结合历史问题的经验进行推断,由近到远按类型分析
如过往慢查的问题比较多,那么整个问题影响链就可能是:慢查询增多 -> GC 耗时增大 -> CPU 负载高 -> 线程 Block 增多 -> RT上涨。
实验分析
通过故障演练等方式对问题现场进行模拟,触发其中部分条件(一个或多个),观察是否会发生问题
如只触发线程 Block 就会发生问题,那么整个问题影响链就可能是:线程Block增多 -> CPU 负载高 -> 慢查询增多 -> GC 耗时增大 -> RT 上涨。
反证分析
对其中某一表象进行反证分析,即判断表象的发不发生跟结果是否有相关性
例如我们从整个集群的角度观察到某些节点慢查和 CPU 都正常,但也出了问题,那么整个问题影响链就可能是:GC 耗时增大 -> 线程 Block 增多 -> RT 上涨
问题分类
Mutator 类型
IO 交互型
互联网上目前大部分的服务都属于该类型,例如分布式 RPC、MQ、HTTP 网关服务等,对内存要求并不大,大部分对象在 TP9999 的时间内都会死亡, Young 区越大越好
MEM 计算型
主要是分布式数据计算 Hadoop,分布式存储 HBase、Cassandra,自建的分布式缓存等,对内存要求高,对象存活时间长,Old 区越大越好。
Mutator 的类型根据对象存活时间比例图
GC 问题分类
Unexpected GC: 意外发生的 GC,实际上不需要发生,我们可以通过一些手段去避免。
Space Shock: 空间震荡问题
参见“场景一:动态扩容引起的空间震荡”
Explicit GC: 显示执行 GC 问题
参见“场景二:显式 GC 的去与留”
Partial GC:部分收集操作的 GC,只对某些分代/分区进行回收。
Young GC:分代收集里面的 Young 区收集动作,也可以叫做 Minor GC
ParNew Young GC 频繁
参见“场景四:过早晋升”
Old GC:分代收集里面的 Old 区收集动作,也可以叫做 Major GC
CMS Old GC 频繁
参见“场景五:CMS Old GC 频繁”
CMS Old GC 不频繁但单次耗时大
参见“场景六:单次 CMS Old GC 耗时长”
Full GC
全量收集的 GC,对整个堆进行回收,STW 时间会比较长,一旦发生,影响较大,也可以叫做 Major GC
参见“场景七:内存碎片&收集器退化”。
MetaSpace
元空间回收引发问题
参见“场景三:MetaSpace 区 OOM”。
Direct Memory
直接内存(也可以称作为堆外内存)回收引发问题
参见“场景八:堆外内存 OOM”
JNI
本地 Native 方法引发问题
参见“场景九:JNI 引发的 GC 问题”
排查难度
一个问题的解决难度跟它的常见程度成反比,大部分我们都可以通过各种搜索引擎找到类似的问题,然后用同样的手段尝试去解决。
当一个问题在各种网站上都找不到相似的问题时,那么可能会有两种情况
一种这不是一个问题
另一种就是遇到一个隐藏比较深的问题,遇到这种问题可能就要深入到源码级别去调试了
0 条评论
回复 删除
下一页