Java性能 -- GC
GC机制
回收区域
- JVM的内存区域中,程序计数器、虚拟机栈、本地方法栈是线程私有,随线程的创建而创建,销毁而销毁
- 栈中的栈帧随着方法的进入和退出进行入栈和出栈操作,每个栈帧分配多少内存基本是在类结构确定下来时就已知
- 因此,这三个区域的内存分配和回收都是具有确定性的
- 堆中的回收主要是对象回收,方法区的回收主要是废弃常量和无用类的回收
回收时机
- 当一个对象不再被引用,就代表该对象可以被回收
- 引用计数法:实现简单,判断效率高,但存在循环引用的问题
- 可达性分析算法:HotSpot VM
引用类型 | 功能特点 |
---|---|
强引用(Strong Reference) | 被强引用关联的对象,永远不会被垃圾回收器回收 |
软引用(Soft Reference) | 被软引用关联的对象,只有当系统将要发生内存溢出时,才会去回收软引用关联的对象 |
弱引用(Weak Reference) | 只被弱引用关联的对象,只要发生GC事件,就会被回收 |
虚引用(Phantom Reference) | 被虚引用关联的对象,唯一作用是在这个对象被回收时收到一个系统通知 |
回收特性
- 自动性
- Java提供了一个系统级的线程来跟踪每一块分配出去的内存空间
- 当JVM处于空闲循环时,GC线程会自动检查每一块分配出去的内存空间,然后自动回收每一块空闲的内存块
- 不可预期性
- 很难确定一个没有被引用的对象是否会被立即回收,有可能当程序结束时,该对象仍在内存中
- GC线程在JVM中是自动执行的,Java程序无法强制执行,
System.gc
也只是建议执行垃圾回收
GC算法
JVM提供了不同的GC算法来实现上面的GC机制
GC算法类型 | 优点 | 缺点 |
---|---|---|
Mark-Sweep | 不需要移动对象,简单高效 | 效率低、产生内存碎片 |
Copying | 简单高效,不会产生内存碎片 | 内存使用率低,有可能频繁复制 |
Mark-Compact | 综合了前两种算法的优点 | 仍需要移动局部对象 |
GC分类
- 不管是什么GC,都会有STW,只有时间长短的区别(CMS和G1的STW会短很多)
- 不用纠结于细分
Major GC
和Full GC
,一次Full GC将对新生代、老年代、元空间、堆外内存进行垃圾回收 - 触发Full GC的原因
- 当新生代晋升到老年代的对象大小,比目前老年代剩余的空间还大
- 当老年代的空间使用率超过某个阈值
- 当元空间不足时(JDK 7永久代不足)
- 调用
System.gc()
垃圾回收器
分类
组合
CMS / G1
CMS
- CMS是基于标记清除算法实现的,用于老年代GC
- CMS的GC周期主要由7个阶段组成,其中两个阶段会发生STW,其它阶段都是并发执行的
阶段 | 描述 |
---|---|
Initial Mark | STW,并行标记可直达的存活对象 |
Concurrent Mark | 1. 并发执行 2. 继续递归遍历老年代,并标记可直接或间接到达的所有老年代存活对象 3. 由于并发执行,对象可能发生变化,变化的对象所在的Card标识为Dirty |
Concurrent Preclean | 重新扫描前一阶段标记的Dirty对象,并标记被Dirty对象直接或间接引用的对象,然后清除Card标识 |
Concurrent Abortable Preclean | 标记可达的老年代对象,扫描处理Dirty Card中的对象 |
Fianl Remark | STW,重新扫描之前并发处理阶段的所有残留更新对象 |
Concurrent Sweep | 清理所有未被标记的死亡对象,回收被占用的空间 |
Concurrent Reset | 清理并恢复在CMS GC过程中的各种状态,重新初始化CMS相关数据结构 |
G1
- G1是基于标记整理算法实现的,是一个分代垃圾收集器,既负责新生代的GC,也负责老年代的GC
- G1之前的各个分代使用的是连续的虚拟内存地址,G1使用了Region的方式对堆内存进行划分
- 同样也分新生代和老年代,每一代使用的是N个不连续的Region内存块,每个Region占用一块连续的虚拟内存地址
- G1中还有一种称为Humongous区域,用于存储特别大的对象
- G1优化:一旦发现没有引用指向巨型对象,则可直接在新生代的Young GC中被回收掉
- G1分为Young GC、Mix GC和Full GC
- Young GC主要是在Eden区进行,当Eden区空间不足时,会触发一次Young GC
- 将Eden区数据转移到Surivivor,如果Surivivor空间不足,(包括Surivivor的数据)则会直接晋升到老年代
- Young GC是并行执行的,也会发生STW
- 当堆空间的占用率达到一定阈值后会触发G1 Mix GC(参数
-XX:InitiatingHeapOccupancyPercent
,默认45)- Mix GC主要包括4个阶段,其中只有并发标记阶段不会发生STW,其它阶段均会发生STW
- Young GC主要是在Eden区进行,当Eden区空间不足时,会触发一次Young GC
对比
- CMS主要集中在老年代的回收,而G1集中在分代回收,包括新生代的Young GC和老年代的Mix GC
- G1使用了Region的方式对堆内存进行划分,基于标记整理算法实现,整体减少了垃圾碎片的产生
- 在初始化标记阶段,搜索可达对象时使用到了Card Table,但实现方式不一样
- 在GC时,都是从GC Root开始搜索,有可能新生代引用到老年代对象,也有可能老年代引用到新生代对象
- 如果发生Young GC,除了从新生代扫描根对象,还需要从老年代扫描根对象,确认引用新生代对象的情况
- 这属于跨代处理,非常消耗性能
- 为了避免在回收新生代时跨代扫描整个老年代,CMS和G1都使用了Card Table来记录这些引用关系
- 只是G1在Card Table的基础上引入了RSet
- 每个Region初始化时,都会初始化一个RSet,RSet记录了_其它Region中的对象引用本Region对象的关系_
- CMS和G1在解决并发标记漏标的方式也不一样,CMS使用的是Incremental Update算法,G1使用的是SATB算法
- 在并发标记中,CMS和G1都是基于三色标记算法实现的
- 黑色:节点被遍历完成,且子节点都遍历完成
- 灰色:当前正在遍历的节点,且子节点还没有遍历
- 白色:还没有遍历到的节点,即灰色节点的子节点
- 漏标问题
- 当一个白色标记对象在GC被清理掉时,正好有一个对象引用了该白色标记对象,就会出现对象丢失的问题
- 如下图所示,
B->D
的引用断开,换成了A->D
的引用,由于A已经是黑色,JVM不会再扫描A及其子节点了- 如果不做处理,那么就会漏标D对象
- CMS:Incremental Update
- 只要在写屏障里发现一个白对象的引用被赋值到一个黑对象的字段里,那就把这个白对象变成灰色的
- G1:STAB
- STAB算法认为开始时所有能遍历到的对象都是需要标记的,即认为都是活的
- STAB的全称为
snapshot-at-the-beginning
,目的是为了维持并发GC的正确性 - GC正确性:保证存活的对象不被回收,即不存在漏标问题,即保证回收的都是真正的垃圾
- 如果标记过程是STW,那么GC的正确性肯定是能够保证的,但并发标记就不一定了
- STAB:在GC开始时对内存进行一个_对象图的逻辑快照_
- 只要被快照到的对象在整个GC过程中都是活的,即使该对象的引用稍后被修改或删除
- 同时新分配的对象也会被认为是活的,除此之外其它不可达的对象被认为是死的
- STAB能够保证真正存活的对象不会被GC误回收,但也造成了可以被回收的对象逃过了GC,导致了浮动垃圾
- 在并发标记中,CMS和G1都是基于三色标记算法实现的
- G1具备Pause Prediction Model,参数
-XX:MaxGCPauseMillis
,默认值为200ms- 根据模型统计出来的历史数据来预测下一次GC所需要的Region数量,_通过控制Region数来控制目标停顿时间_
GC性能指标
- 吞吐量
- 吞吐量 = 应用程序耗时 / (应用程序耗时 + GC耗时)
- 吞吐量一般不能低于95%
- 停顿时间
- 对于串行回收器来说,停顿时间可能会比较长
- 而使用并发回收器,由于垃圾收集器和应用程序交替运行,程序的停顿时间会变短
- 但效率很可能不如独占垃圾收集器,吞吐量也可能降低
- GC频率
- 增大堆内存空间可以有效降低GC频率,但也意味着堆积的回收对象越多,会增加回收时的停顿时间
- 可以适当地加大堆内存空间,保证正常的GC频率即可
GC日志分析
- JVM参数
-XX:+PrintGC
:输出GC日志-XX:+PrintGCDetails
:输出GC的详细日志-XX:+PrintGCTimeStamps
:输出GC的时间戳(以基准时间的形式)-XX:+PrintGCDateStamps
:输出GC的时间戳(以日期的形式,如2013-05-04T21:53:59.234+0800)-XX:+PrintHeapAtGC
:在进行GC的前后打印出堆的信息-Xloggc:../logs/gc.log
:GC日志文件的输出路径
- 分析工具
GC调优策略
降低Minor GC频率
- 由于新生代空间较小,Eden区很快被填满,就会导致频繁Minor GC,可以通过增大新生代空间来降低Minor GC频率
- 单次Minor GC时间由两部分组成:T1(扫描新生代)+ T2(复制存活对象)
- 假设一个对象在Eden区的存活时间为500ms,Minor GC的时间间隔为300ms
- 由于该对象依然存活,Minor GC的时间为T1+T2
- 如果增大新生代空间,Minor GC的时间间隔扩大到600ms
- 此时存活时间为500ms的对象会在Eden区被回收,不存在复制存活对象,此时Minor GC时间为2T1
- 新生代扩容后,Minor GC增加了T1,但减少了T2,通常在JVM中,_T2远大于T1_
- 小结
- 如果堆内存中长期对象很多,扩容新生代,反而会增加Minor GC的时间
- 如果堆内存中短期对象很多,扩容新生代,_单次Minor GC的时间不会显著增加_
- 单次Minor GC的时间更多地取决于GC后存活对象的数量,而非Eden区的大小
降低Full GC频率
- 由于堆内存空间不足或者老年代对象太多,会触发Full GC(带来上下文切换,增加系统的性能开销)
- 常见优化点:减少创建大对象、增大堆内存空间(初始化堆内存为最大堆内存)
选择合适的GC回收器
- 响应速度快:CMS、G1
- 吞吐量高:Parallel Scavenge
参考资料
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.