当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些进行监控和调节
目录标题
一、对象已死
-
引用计数算法:有地方引用时计数器加一,引用失效时计数器减一,互相引用就无法回收
-
可达性分析
- GC Roots 的根对象作为起始节点集,搜索过程走过的路径称为“引用链”
- 如果某个对象到GC Roots间没有任何引用链相连,这个对象不可达时,就证明对象是不可能再被使用
- GC Roots对象
- 虚拟机栈中引用的对象:参数、局部变量、临时变量等
- 方法区中静态属性引用的对象、常量引用的对象:引用类型静态变量、 字符串常量池里的引用
- 本地方法栈中JNI引用的对象
- Java虚拟机内部的引用、所有被同步锁持有的对象等等
-
再谈引用
- 强引用:在程序代码之中普遍存在的引用赋值,垃圾收集器就永远不会回收掉被引用的对象
- 软引用:还有用,但非必需的对象,这些对象列进回收范围之中进行第二次回收
- 弱引用:非必需对象,生存到下一次垃圾收集发生为止
- 虚引用:最弱的引用关系,无法通过虚引用获得一个对象实例,在回收时收到一个系统通知
-
生存还是死亡
- 真正死亡需要经过两次标记:没有引用链为第一次,finalize()方法可以救活一次
-
回收方法区
- 方法区垃圾收集主要回收两部分:废弃的常量和不再使用的类型
- 废弃的常量:没有其他地方引用
- 不在使用的类型:所有实例已经被回收、加载该类的类加载器已经被回收、java.lang.Class对象没有任何地方被引用
二、垃圾收集算法
- 分代收集理论
- 弱分代假说:朝生夕灭
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
- 跨代引用假说:对象之间存在跨代引用
- 设计原则
- 根据年龄划分出不同区域,朝生夕灭的放在一起(只关注少量存活),难以消亡的对象放在一起(用较低的频率来回收这个区域)
- Minor GC(新生代收集)、Major GC(老年代收集,CMS)、Full GC、Mixed GC(混合收集,G1)
- 记忆集:把老年代划分成若干小块,标识出老年代代哪一块内存会存在跨代引用,Minor GC时将跨代引用的小块内存对象加入到GC Roots扫描
- 标记-清除算法:最基础的垃圾收集算法,执行效率不稳定、内存空间碎片化问题(下次分配没有足够的连续内存)
- 标记-复制算法:分成两块,一块用完了将存活的复制到另一块上,然后清理掉第一块,内存会缩小为原来的一半
- 大多数Java虚拟机都优先采用此算法去回收新生代、老年代一般不能直接选用这种算法
- Appel式回收:新生代分为一块较大的Eden空间和两块较小的Survivor空间,Eden和Survivor大小比例8:1,需要一个分配担保避免超容
- 标记-整理算法:存活的对象向内存空间一端移动,然后直接清理掉边界以外的内存
- 移动则内存回收时会更复杂,不移动则内存分配时会更复杂
- 和稀泥式:平时多用标记-清除算法,碎片化大的时候采用标记-整理算法
三、HotSpot的算法细节实现
- 根节点枚举
- OopMap数据结构存放对象引用:类加载动作完成的时候,HotSpot会把对象内偏移量上是什么类型的数据计算出来,扫描的时候可以直接用
- 安全点:GC Roots枚举的时候需要停顿线程,设置了安全点只能到达安全点后再停止
- 主动式中断:设置一个标志位,各个线程轮询,一旦发现中断标志就在最近的安全点上主动中断挂起
- 安全区域:线程不能走到安全的地方去中断挂起自己,就需要引入安全区域(线程要离开安全区域,要检查虚拟机是否已经完成根节点枚举)
记忆集与卡表- 记忆集:用来避免整个老年代加进GC Roots扫描范围
- 卡表:卡精度(记录精确到一块内存区域)
- 变脏:卡页内有一个对象的字段存在着跨代指针,标识就会变成1,就变脏了
- 写屏障:维护卡表状态
- 赋值前的部分的写屏障叫作写前屏障,赋值后就是写后屏障
- 并发的可达性分析
- 三色标记
- 白色:表示对象尚未被垃圾收集器访问过
- 黑色:表示对象已经被垃圾收集器访问过,所有引用都已经扫描过
- 灰色:表示对象已经被垃圾收集器访问过,至少存在一个引用还没有被扫描过
- 解决并发扫描时的对象消失问题:增量更新、原始快照
- 增量更新:黑色对象插入指向白色对象的引用后,变回灰色对象(CMS)
- 原始快照:无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索(G1、Shenandoah)
- 三色标记
四、经典垃圾收集器
- Serial收集器
- 最基础、历史最悠久、单线程工作的收集器
- 客户端模式下默认新生代收集器
- ParNew收集器
- Serial收集器的多线程并行版本、运行在服务端模式下的HotSpot虚拟机
- ParNew是激活CMS后(-XX: +UseConcMarkSweepGC)的默认新生代收集器(-XX: +UseParNewGC强制指定或禁用)
- 并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系
- 并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系
- Parallel Scavenge收集器
- 新生代收集器、基于标记-复制算法,并行收集的多线程收集器,达到一个可控制的吞吐量
- 吞吐量=运行用户代码时间 / (运行用户代码时间 + 运行垃圾收集时间)
- -XX: MaxGCPauseMillis控制最大垃圾收集停顿时间
- -XX: GCTimeRatio 直接设置吞吐量大小
- Serial Old收集器
- Serial收集器的老年代版本、单线程收集、标记-清除算法
- Parallel Old收集器
- Parallel Scavenge收集器的老年代版本、多线程并发收集、基于标记-整理
- 在注重吞吐量或处理器资源较为稀缺的场合,可以考虑Parallel Scavenge加Parallel Old收集器组合
- CMS收集器
- 以获取最短回收停顿时间为目标的收集器、基于标记-清除算法、并发收集、低停顿
- 四个步骤
- 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,速度快、对处理器资源非常敏感
- 并发标记:从GC Roots的直接关联对象开始遍历整个对象图过程,可以与垃圾收集线程一起并发运行
- 重新标记:修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录
- 并发清除:清理删除掉标记阶段判断的已经死亡的对象
- 初始标记、重新标记仍然需要“Stop The World”
- 回收线程数:(处理器核心数量 + 3)/ 4
- 无法处理“浮动垃圾”,并发标记和并发清理阶段,用户线程继续运行产生新的垃圾对象无法在当次收集中处理
- 必须预留一部分空间供并发收集时的程序运作使用,老年代用了68%空间后就会被激活
- Garbage First收集器
- 全功能的垃圾收集器、主要面向服务端应用的垃圾收集器、将内存回收“行为”与“实现”进行分离
- 面向堆内存任何部分来组成回收集进行回收,Mixed GC模式
- 衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大
- G1基于Region的堆内存布局,将Java堆划分成多个大小相等的独立区域(Region),每一个Region都可以根据需要扮演Eden、Survivor空间
- 将Region作为单次回收的最小单元
- 使用记忆集避免全堆作为GC Roots扫描
- 四个步骤
- 初始标记:仅仅标记一下GC Roots能直接关联到的对象
- 并发标记:从GC Roots开始对堆中对象进行可达性分析
- 最终标记:对用户线程做另一个短暂的暂停
- 筛选回收:负责更新Region的统计数据
- 大内存应用上G1能发挥优势
五、低延迟垃圾收集器
- 衡量垃圾收集器的重要指标:内存占用、吞吐量、延迟
5.1 Shenandoah收集器
- 实现一种能在任何堆内存大小下都可以把垃圾收集的停顿时间控制在十毫秒内
- 不仅要并发的垃圾标记,还要并发地进行对象清理后的整理动作
- 与G1不同之处
- 支持并发的整理算法
- 默认不使用分代收集
- 不用记忆集,改用连接矩阵的全局数据结构来记录跨Region的引用关系
- 工作过程
- 初始标记:与GC Roots直接关联的对象
- 并发标记:遍历对象图,标记出全部可达的对象
- 最终标记:处理剩余的SATB扫描
- 并发清理:整个区域内连一个存活对象都没有找到的Region
- 并发回收:将存活对象先复制一份到其他未被使用的Region之中
- 初始化引用更新:堆中所指向旧对象的引用修正到复制后到新地址
- 并发引用更新:真正开始进行引用更新操作
- 最终引用更新:修正存在于GC Roots中的引用
- 并发清理:整个回收集中所有的Region已再无存活对象
- 重要阶段:并发标记、并发回收、并发引用更新
- 转入指针:对象有一份新的副本时,只需要修改一处指针的值(旧对象上转发指针的位置,使其指向新对象)
5.2 ZGC收集器
- 基于Region内存布局,不设分代的,使用读屏障、染色指针和内存多重映射等实现并发的标记-整理算法
- ZGC的Region具有动态性–动态创建和销毁,以及动态的区域容量大小
- 小型Region:容量固定为2MB,用于放置小于256KB的小对象
- 中型Region:容量固定为32MB,放置大于256KB小于4MB的对象
- 大型Region:容量不固定,可以动态变化,必须是2MB整数倍,放置4MB或以上的大对象
- 标志性设计是采用的染色指针技术
- 运行过程
- 并发标记:并发标记是遍历对象图做可达性分析的阶段
- 并发预备重分配:根据特定的查询条件统计得出本次收集过程要清理那些Region,将这些Region组成重分配集
- 并发重分配:把重分配集中的存活对象复制到新的Region上,并为重分配到每个Region维护一个转发表
- 并发重映射:修改整个堆中指向重分配集中旧对象的所有引用
六、选择合适的垃圾收集器
- Epsilon:无操作的收集器
- 收集器的权衡:吞吐量、延迟、内存占用
- 虚拟机及垃圾收集器日志
- HotSpot所有功能的日志都收归到“-Xlog”参数上
- 参数总结
七、内存分配与回收策略
- 对象优先在Eden分配
- 对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
- 大对象直接进入老年代
- 大对象:大量连续内存空间的Java对象(很长的字符串,元素数量很庞大的数组),大对象意味着高额的内存复制开销
- HotSpot有:-XX: PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配,避免Eden区和两个Survivor区来回复制
- 长期存活的对象将进入老年代
- 虚拟机给每个对象定义了一个年龄计数器,每熬过一次Minor GC年龄加1岁
- 升到老年代年龄阈值 -XX: MaxTenuringThreshold设置
- 动态对象年龄判断
- 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象直接进入老年代
- 空间分配担保
- Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
- 不成立会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,然后冒险启动Minor GC
- 失败会触发Full GC
- Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间