1 GC判定
1.1 引用计数法
对象内部维护有一个被其他对象引用的引用计数,当这个引用计数为 0 的时候,表示对象可以被回收。
引用计数法存在一个问题——循环引用,加入 a 引用 b,b 同时也引用 a,那么就存在 ab 的引用计数都不为 0 的情况。
1.2 可达性分析算法
一个对象的根节点,不是 GC Roots 的话,就可以被回收。
Java 中可作为 GC Roots 的对象包括:
- 虚拟机栈(本地变量表)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(Native 方法)引用的对象
1.3 引用类型
Java 中有四种引用类型,默认是强引用类型:
- 强引用:宁愿 crash,也不回收
- 软引用:内存不够就回收 SoftReference(适合缓存)
- 弱引用:扫描到就回收 WeakReference
- 虚引用:随时回收 PhantomReference
1.4 生存还是死亡
在可达性分析算法中,如果没有与 GC Roots 引用链连接,会经历两次标记。
- 第一次标记并进行筛选,看是否有必要执行 finalize()方法,如果对象没有覆盖 finalize() 或已调用过,则“没必要执行”。
- 如果有必要执行 finalize(),则将对象放在 F-Queue 队列中,用低优先级线程去触发。如果对象在 finalize() 中与引用链任何对象建立关系,则不会被回收,其它的就被回收。
建议忘掉 finalize(),使用 try-finally
1.5 回收方法区(永久代)
主要回收两部分内容:废弃常量和无用的类
- 废弃常量
没有被引用时,就会被清除 - 无用的类
要同时满足以下三个条件- 所有的类实例都被回收
- 该类的 classloader 被回收
- 该类对应的 class 对象没有在任何地方被引用,也没有通过反射访问类的方法
满足条件可以回收,不是必然回收
2 GC 算法
回收算法
- 标记-清除(基础)
- 复制(新生代,效率高,90%,需担保)
- 标记-整理(老年代,100%)
分代收集
- 新生代
- 老年代
2.1 标记-清除
最基础的收集算法,其它算法都是对它不足的改进
1)两个阶段
- 标记(GC 判定)
- 清除
2)两点不足
- 效率不高
- 空间问题
产生大量不连续内存碎片,空间不够后会触发 GC
2.2 复制算法
解决效率问题。
1)策略
将内存分为两块,每次都使用其中一块,当一块用完了,就将存活的对象复制到另一块,然后把使用过的一次清理掉。
代价是内存缩小为原来一半。
2)实现
用来回收新生代。新生代 98% 对象“朝生夕死”,所以不是 1:1 划分内存。
将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。回收时将存活的对象一次性复制到另一块 Survivor,清剩下的。
Hotspot 默认 Eden:Survivor:Survivor = 8:1:1,若回收时 Survivor 空间不够,通过老年代进行分配担保。
2.3 标记-整理
老年代,应对 100% 对象存活
先标记可以被回收的对象,然后,让存活的对象向一端移动,最后直接清理掉另一端的内存。
2.4 分代收集算法
将内存根据生命周期分为几种,一般为新生代和老年代,然后根据特性,选择不同的回收算法。
3 算法实现
HotSpot的算法实现
3.1 枚举根节点
GC Roots 的节点主要是在全局性的引用(常量、类静态属性)与执行上下文(栈帧中的本地变量表)中,若逐个检查很耗时。
GC 进行时必须停顿所有 Java 执行线程(Stop The World)。
并不需要一个不漏地检查完所有引用位置,HotSpot 用 OopMap 来记录,GC 扫描时就直接知道了。
3.2 安全点
并非在所有地方都能停顿下来 GC,因为为每条指令都生成 OopMap 太浪费,只在特定安全点执行。
只有到达安全点才能暂停,GC。
有抢先式中断和主动式中断两种。通常用主动式中断
3.3 安全区域
被扩展的安全点,主要处理“不执行”的线程,Sleep 或 Blocked
4 垃圾收集器
4.1 新生代
1) 串行(Serial)
单线程,复制算法。Java 9 以前默认收集器。
2) 并行(ParNew)
Serial 的多线程版本。
3) 吞吐量优先(Parallel Scavenge)
多线程,复制算法,侧重于吞吐量的控制。
可以启用自适应调节策略,不用手工设置新生代大小等参数。
4.2 老年代
1) Serial Old
单线程,标记整理。Java 9 以前,Client 模式下默认收集器。
2) Parallel Old
多线程,标记整理,吞吐量优先
3) 并发标记清除(CMS)
并发收集、低停顿,“标记-清除”算法:
- 初始标记:标记 GC Roots 能关联到的对象
- 并发标记:GC Roots Tracing
- 重新标记:修正并发标记期间产生变动的标记记录
- 并发清除:
三个缺点:
- 对CPU资源敏感
- 无法处理浮动垃圾:并发清除阶段产生的。若内存不足失败了临时启用 Serial Old 收集器来重新收集老年代。
- 标记清除碎片空间多
4.3 新一代收集器
1) G1
Java 7 开始可以自主配置 G1 垃圾收集器,Java 9 开始作为默认的垃圾收集器,替代了之前默认使用的 Parallel GC
- 并行与并发:可多 cpu
- 分代收集:可独立管理整个 GC 堆
- 空间整合:整体看是“标记-整理”,局部是“复制”
- 可预测的停顿:避免全区域的 GC
- 将整个 Java 堆划分为多个大小相等的独立区域(Region),新生代和老年代不再物理隔离,都是一部分 Region(不需要连续)的集合。
- 步骤: 初始标记、并发标记、最终标记、筛选回收
2) ZGC
【todo】Java 11 的新功能,承诺在数TB的堆上具有非常低的暂停时间。
- 着色指针
- 多重映射
- 读屏障
- 标记
- 重定位
4.4 理解 GC 日志
33.123: [GC [DefNew 3124K->123K(3333K), 0.0025925 secs] 2342K->122K(123232K), 0.34235 secs
33.123:
:代表 GC 发生时间。[GC
、[FULL GC
:代表停顿类型。
[FULL GC
一般是分配担保失出现 STW,若是调用 Sysrem.gc() 会显示[FULL GC(System)
[DefNew
、[Tenured
、[Perm
:表示 GC 发生的区域。
[DefNew
是 Serial 收集器的新生代,[ParNew
是 ParNew 收集器的新生代,PSYoungGen
是 Parallel Scavenge 收集器新生代- 方括号里的
3124K->123K(3333K)
含义是该区域的 GC 前后量和总量。 - 方括号外的
2342K->122K(123232K)
含义是 Java 堆的 GC 前后量和总量。 0.34235 secs
:GC 时间
4.5 垃圾收集器参数总结
1) 串行 GC 相关
- -XX:+UseSerialGC:使用 Serial + Serial Old
- -XX:SurvivorRatio:Eden 区与 Survivor 区的容量比值,默认为 8
- -XX:PretenureSizeThreshold:晋升老年代对象大小的阈值
- -XX:MaxTenuringThreshold:晋升到老年代对象的年龄
2) 并行 GC 相关
- -XX:+UseParNewGC:使用 ParNew + Serial Old
- -XX:+UseParallelOldGC:使用 Parallel Scavenge + Parallel Old
- -XX:ParallelGCThreads:并行 GC 进行内存回收的线程数
- -XX:MaxGCPauseMillis:GC的最大停顿时间
- -XX:GCTimeRatio:吞吐量,默认99,即允许1%的GC时间
- -XX:+UseAdaptiveSizePolicy:动态调整java堆中各个区域的大小以及进入老年代的年龄
3) CMS 相关
- -XX:+UseConcMarkSweepGC:使用 ParNew + CMS + Serial Old
- -XX:ParallelCMSThreads:设定 CMS 的线程数量
- -XX:CMSInitiatingOccupancyFraction:设置 CMS 收集器在老年代空间被使用多少后触发,默认为 68%
- -XX:+UseCMSCompactAtFullCollection:设置 CMS 收集器完成垃圾收集后是否要进行一次内存碎片的整理
- -XX:CMSFullGCsBeforeCompaction:设定进行多少次 CMS 垃圾回收后,进行一次内存压缩
- -XX:+CMSClassUnloadingEnabled:允许对类元数据区进行回收
- -XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动 CMS 回收
- -XX:UseCMSInitiatingOccupancyOnlyn:达阈值的时候才进行 CMS 回收
- -XX:+CMSIncrementalMode:使用增量模式,比较适合单 CPU。在 JDK 8 中废弃,并将在 JDK 9 中移除。
4) 与G1回收期相关的参数
- -XX:+UseG1GC:使用 G1
- -XX:MaxGCPauseMillis:最大垃圾收集停顿时间
- -XX:GCPauseIntervalMillis:停顿间隔时间
5) TLAB相关
- -XX:+UseTLAB:开启 TLAB 分配
- -XX:+PrintTLAB:打印 TLAB 相关分配信息
- -XX:TLABSize:设置 TLAB 大小
- -XX:+ResizeTLAB:自动调整 TLAB 大小
6) 其他参数
- -XX:+DisableExplicitGC:是否关闭手动 System.gc
- -XX:+ExplicitGCInvokesConcurrent:使用并发方式处理显式 GC
5 内存分配与回收策略
内存管理两个问题:分配内存、回收内存
分配就是在堆上分配,主要在新生代的 Eden 区上,少数情况也会直接分配在老年代中。
5.1 对象优先在 Eden 分配
Eden没有足够空间时会发起一次Minor GC
5.2 大对象直接进入老年代
大对象是只需要大量连续内存空间的对象(如长字符串、数组),应当避免“朝生夕灭”的短命大对象。
5.3 长期存活的对象将进入老年代
每个对象有个对象年龄计数器,当对象在 Eden 出生并经过第一次 Minor GC 后存活,并能容纳在 Survivor,就会被移到 Survivor。每度过一次 minor gc,岁数+1,当过了一定值(默认 15)的时候,就移入老年代
5.4 动态对象年龄判定
如果 survivor 中相同年龄的对象的大小达到总大小的一半,年龄大于或等于这个年龄的对象直接进入老生代,无需等到年龄门槛
5.5 空间分配担保
Minor GC 前看老年代剩余空间够不够装新生代失败的,不够就 FULL GC