CMS + ParNew 总结
GC问题解决思路
- 建立知识体系:从JVM的内存结构到垃圾收集算法和收集器,学习GC基础知识,只有把知识融汇贯通才能更好的解决问题
- 确定评价标准:了解GC的评价方法,摸清如何设定独立系统的指标,以及在业务场景中判断GC是否存在问题
- 场景调优时间:运用掌握的知识和系统评价指标,分析与解决九种CMS中常见的问题,积累实际的优化经验
- 总结优化经验:对整体过程做总结,融会贯通到自己的知识体系之中,不断循环这个过程
GC基本概念
GC
GC本身有三种语义
- Garbage Collection:垃圾收集技术
- Garbage Collector:垃圾收集器
- Garbage Collecting:垃圾收集动作
Mutator
生产垃圾的角色,也就是我们的应用程序,垃圾制造者,通过Allocator进行allocate和free
TLAB
Thread Local Allocation Buffer,基于CAS的独享线程,可以优先将对象分配在Eden区域的一块内存,因为是java线程独显的内存区,不存在竞争,分配速度更快,每一个TLAB都是一个线程独享的
Card Table
主要是用来标记卡页的状态,每个卡表项对应一个卡页。当卡页中有一个引用对象有写操作的时候(引用类型字段赋值的那一刻),写屏障会标记对象所在的卡表状态为dirty,卡表的本质是用来解决跨代引用的问题的。
JVM 内域划分(JDK 1.8)
分配对象
java中对象地址操作主要使用了Unsafe类调用了C的allocate和Free两个方法,分配方法有两种
空闲链表
通过额外的存储记录空闲的地址,将随机I/O变为顺序I/O,但是带来了额外的空间消耗
碰撞指针
通过指针作为一个分界点,需要分配内存时,只需要将指针向后移动与对象大小相等的距离,分配效率较高,但是要求内存规整,使用场景有限
垃圾收集
识别算法
- 引用计数法:
对每个对象的引用进行计数,每当有一个地方引用它时计算器 +1,引用失效则计数器 -1,引用的计数放到对象头中,大于0的对象被认为是存活的对象。虽然循环引用问题可以解决,但是在多线程的情况下,维护计数器变更也需要进行昂贵的同步操作,性能较低,早期的编程语言采用的算法。
- 可达性分析:
从GC Roots开始枚举根节点,进行链路搜寻,可以被搜索到的对象为可达对象,整个连通图中没有被标记的对象便可作为垃圾回收掉
回收算法
- Mark-Sweep(标记-清除):
回收过程主要分为两个阶段,第一阶段为追踪(Tracing)阶段,即从 GC Root 开始遍历对象图,并标记(Mark)所遇到的每个对象,第二阶段为清除(Sweep)阶段,即回收器检查堆中每一个对象,并将所有未被标记的对象进行回收,整个过程不会发生对象移动。整个算法在不同的实现中会使用三色抽象(Tricolour Abstraction)、位图标记(BitMap)等技术来提高算法的效率,存活对象较多时较高效。
- Mark-Compact (标记-整理):
这个算法的主要目的就是解决在非移动式回收器中都会存在的碎片化问题,也分为两个阶段,第一阶段与 Mark-Sweep 类似,第二阶段则会对存活对象按照整理顺序(Compaction Order)进行整理。主要实现有双指针(Two-Finger)回收算法、滑动回收(Lisp2)算法和引线整理(Threaded Compaction)算法等。
- Copying(复制):
将空间分为两个大小相同的 From 和 To 两个半区,同一时间只会使用其中一个,每次进行回收时将一个半区的存活对象通过复制的方式转移到另一个半区。有递归(Robert R. Fenichel 和 Jerome C. Yochelson提出)和迭代(Cheney 提出)算法,以及解决了前两者递归栈、缓存行等问题的近似优先搜索算法。复制算法可以通过碰撞指针的方式进行快速地分配内存,但是也存在着空间利用率不高的缺点,另外就是存活对象比较大时复制的成本比较高。
三种算法在是否移动对象、空间和时间方面的一些对比,假设存活对象数量为 L、堆空间大小为 H,则:
收集器
ParNew
采用复制算法的新生代收集器,通过-XX:ParallelGCThreads参数来控制进行垃圾收集的线程数,整个过程都是STW的,常与CMS组合使用
CMS
以获取最短回收停顿时间为目标,采用“标记-清除”算法,分初始标记、并发标记、重新标记和并发清除四个过程,其中耗时最长的并发标记阶段与用户线程一同工作,初始标记和重新标记阶段是STW的,在内存不足的情况下会退化为Serial Old收集器,进行STW的收集
G1
应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。从G1开始,垃圾收集器的目标开始考虑适应程序的内存分配速率
常用工具
- JProfiler、阿里诊断工具arthas、在线平台GCeasy
GC评判标准
- 延迟(Latency):
可以理解为最大停顿时间,垃圾收集过程中的STW时间,越短越好,一定程度上可以接收频次的增大,GC计数的主要发展方向
- 吞吐率(ThroughPut):
应用系统的生命周期内,Mutator运行时间占用系统运行时间的百分比,系统运行了100min,GC耗时1 min,则系统的吞吐量为99%
目前各大互联网公司的系统基本都追求低延时,避免一次GC停顿的时间过长对用户体验造成影响,衡量指标主要为Ttp9999、Tstw,一次的停顿时间不超过应用服务的TP9999,GC的吞吐量不小于99.99%。假设某个服务的TP9999为80ms,平均GC停顿为30ms,那么该服务的最大停顿时间不要超过80ms,GC频率控制在GC停顿的10000倍以上。
GC Cause
收集GC Cause
添加JVM参数
-javaagent:"C:\Program Files\Java\xrebel.jar"
-XX:+UseConcMarkSweepGC
-XX:+PrintGCDetails
-XX:+PrintHeapAtGC
-Xloggc:D://gceasy//waterGC.log
在gceasy上可以分析GC的详细情况,此网站会把GC前后的图形变化,可能存在的内存泄漏,停顿时间过长的GC情况全部记录下来
GC需要注意的情况
Allocation Failure
一般是空间震荡引起的
System.GC()
手动触发System.GC的情况过多
CMS
重点关注CMS GC的Initial Mark和Final Mark阶段
Promotion Failure
old区域没有足够的空间分配给Young区晋升
Concurrent Mode Failure
CMS运行期间Old区预留的空间不足以分配新的对象,或者不足以共给CMS收集器使用,此时收集器会发生退化,严重影响GC的性能
判断是否是GC引发的问题
- 时序分析:通过监控手段分析各个指标异常的时间点,主要观察CPU复杂、慢SQL、线程Block、GC耗时增加几个方面,最先发生问题的一般是根因,另外结合GC report看看是否有明显的GC问题,比如内存泄漏无法回收,Old GC之后堆内存的变化过大等问题
Matutor类型解读
- IO交互型:目前互联网上的大部分服务器属于该类型,例如分布式RPC、MQ、HTTP网关服务等,对内存要求不高,大部分对象在TP9999时间内死亡,Young区域越大越好
- 计算密集型: 主要是分布式数据计算 Hadoop,分布式存储 HBase、Cassandra,自建的分布式缓存等,对内存要求高,对象存活时间长,Old 区越大越好
GC问题分类
- Space Shock:空间震荡
- Explicit GC:显示执行GC的问题
- Full GC:全量收集的GC,对整个堆进行收集,STW时间较长,一旦发生影响较大
- MetaSpace:元空间导致的收集问题
- Direct Memory:直接内存引发的收集问题
- JNI:本地Native方法引发的GC问题