admin管理员组

文章数量:1658475

INDEX

      • §1 垃圾回收算法
        • §1.1 引用计数器
        • §1.2 复制
        • §1.3 标记清除
        • §1.4 标记整理
      • §2 GC Root
      • §3 垃圾收集器
        • §3.1 查看默认垃圾收集器
        • §3.2 默认垃圾收集器及其分类
        • §3.3 默认垃圾收集器横比
        • §3.4 垃圾收集器参数配置与组合
        • §3.5 垃圾收集器的选择
      • §4 GC 类型
      • §5 GC 日志
        • §5.1 开启 GC 日志
        • §5.2 GC 日志与格式

§1 垃圾回收算法

§1.1 引用计数器

对象被引用时计数器 + 1,放弃引用时计数器 -1
java 通常不使用此方式

缺点:

  • 每次复制都要维护计数器
  • 循环引用时会出现内存泄漏
§1.2 复制

主要应用于 JVM 堆的新生代中
JVM 的堆中,新生代分为 3 个区域 Eden、FROM、TO

  • Eden 区满了触发 GC
  • GC 扫描 Eden 和 FROM 区对象,这些对象年龄 + 1
  • 存活的放入 TO 区
  • 清空 Eden 和 FROM
  • 交互 FROM 和 TO
  • 对象年龄达到 15 时,进入老年代

优点:
因为是整体复制,所以不产生内存碎片

缺点:
浪费空间
大对象复制时比较耗时

§1.3 标记清除

主要应用于 JVM 堆的老生带中
先标记需要回收的对象
然后统一清除

优点:
避免大批量复制对象,节省内存空间

缺点:
会产生内存碎片

衍生的动态分区分配规则
标记清除算法中会维护一个空闲列表,示意结构:
[x,y] -> [x,y] -> [x,y] ->
其中 x 是指内存偏移量
y 是 x 后连续的空闲空间的大小

因此在标记清除的算法下,对于分配内存空间衍生了以下算法

  • 首次适应算法:first-fit,遍历空闲列表时,遇到第一个空间足够的区域时立即分配给对象
  • 最佳适应算法:best-fit,找到恰好大小相等的块,分配给对象
  • 最差适应算法:worst-fit,找到空闲空间最大的块,从中分割出分配给对象的区域

实践中,通常使用 最佳适应 + 最差适应,先找最匹配的,如果没找到从最大里分割,这么做的目的是尽量不造成更多碎片

§1.4 标记整理

主要应用于 JVM 堆的老生代中,不浪费空间,但费时
先标记需要回收的对象
然后在此扫描将对象向一端压缩,使可用内存连续

优点:
避免内存碎片

缺点:
移动对象需要时间

§2 GC Root

什么是垃圾
内存中不再被使用的空间

如何判断

  • 引用计数法
  • 可达性分析

可达性分析

  • 可达性分析从 GC Root 开始
  • 根据引用关系遍历对象图
  • 能被遍历到的对象视为存活,否则视为死亡

可以作为 GC Root 的对象
GC Root 是一组明确正在被使用的对象

  • 虚拟机栈中引用的对象
    就是栈帧中局部变量表引用大的对象
  • 本地方法栈中引用的对象
  • 方法区中,类的静态属性引用的对象
  • 方法区中,常量引用的对象

可以这样理解:
JVM 内存模型一共 5 个区

  • 程序计数器只是一串指针,不存对象
  • 堆就是 GC 要遍历,肯定不能从自己开始
  • 虚拟机栈里,能确定的就是栈里的各种变量
  • 本地方法栈里,能确定的也是各种变量
  • 元空间主要存放类信息和常量池,类中的静态变量和常量池里的东西也是确定的

示例

public class GcRootsDemo {
    public static final AAA OBJ_1 = new AAA();// GC root
    private static AAA job2 = new AAA(); // GC root

    private AAA obj3 = new AAA(); // 只从这里看的话,不一定是垃圾,但不是 GC root

    public void m1(){
        AAA job4 = new AAA(); // 1
        AAA job5 = obj3; // 2
        System.out.println();
    }
    public static void main(String[] args) {
        new GcRootsDemo().m1();
    }
}

说明:

  • 若程序执行完 // 1,则 job4 是 GC Root 但 obj3 不是
  • 若程序执行完 // 2,则 obj3 是 GC Root ,因为虚拟机栈里的本地变量 job5 引用了它
  • 本地方法栈的情况和上述虚拟机栈类似

§3 垃圾收集器

  • 垃圾收集器是对垃圾回收算法的实现
    这就是 垃圾收集器与垃圾回收算法的关系
  • 没有完美的垃圾收集器,通常分情况使用,分代收集
§3.1 查看默认垃圾收集器

尚未运行的应用,可以通过 java -XX:+PrintCommandLineFlags 查看

运行中的应用,可以通过 jinfo -flags pid 查看

§3.2 默认垃圾收集器及其分类

分类

  • Serial 收集器 /ˈsɪriəl/

    • 串行收集器,如 Serial、Serial Old
    • 只使用 单线程 进行收集
    • 会暂停 所有应用线程
    • 适用于单线程应用,不适合服务器环境
    • 复制算法
  • Parallel 收集器 /ˈpærəlel/

    • 并行收集器,如 ParNew、Parallel Scavenge、Parallel Old
    • 使用 多个线程 进行收集
    • 会暂停 所有应用线程(所有线程并行处理 GC)
    • 适用于 科学计算、大数据处理 等 弱交互场景
    • 复制算法
    • JVM 默认 使用的垃圾收集器
  • CMS(Concurrent Mark Sweep)

    • 并发收集器
    • 使用 多个线程 进行收集
    • 不会暂停 所有应用线程(业务线程与 GC 线程并发处理执行)
    • 适用于 强交互(对响应时间有要求) 的场景,比如众多互联网公司
    • 标记清除,所以会产生碎片
  • G1

    • 将内存分割小块来收集
  • ZGC

默认垃圾收集器及部分常见参数
Serial Old
单线程,标记整理算法
是 JVM 在 client 模式的默认老年代垃圾收集器
Server 模式下

  • JDK 5 及之前的版本,搭配 Parallel Scavenge 使用,已过时并废除
  • 作为 CMS 垃圾收集器的备用垃圾收集器

Server 模式:JVM 在 win32 与 2C/2G 以下的其他系统上固定为 client 模式(不配作为服务器),其余都是 Server 模式

ParNew
相比 Serial ,ParNew 是它的多线程版
是 Server 模式下,很多 JVM 的默认新生代收集器
在较旧的系统中,常用于协同 CMS / Serial Old

常见参数

  • -XX:ParallelGCThread 指定线程数,默认 4

Parallel Scavenge
相比 ParNew,Parallel Scavenge 具有如下优点

  • Parallel Scavenge 相对于垃圾回收时间,更注重吞吐量
    吞吐量是指应用线程时间在总时间中的占比,因此 Parallel Scavenge 俗称 吞吐量垃圾收集器
  • Parallel Scavenge 具有 自适应调节策略
    Parallel Scavenge 会根据系统运行情况收集性能监控信息,动态调节停顿时间和最大吞吐量

常见参数

  • -XX:MaxGCPauseMillis 指定最大停顿时间
  • -XX:ParallelGCThread 指定线程数
    通常 8 核及以下,-XX:ParallelGCThread=核数
    8 核以上,超出 8 核的部分按 5/8 或 / 折算,即 8 + (cores - 8) * den,den 就是 那两个比例,通常 JVM 会根据机器和系统自动选择

CMS
CMS 收集器旨在 尽量降低停顿时间
相比 Parallel Scavenge,CMS 提供更短的停顿时间
需要注意:CMS 下,默认分代年龄的最大值是 6

CMS 的垃圾收集的 4 个阶段(但实际是 7 个步骤)

  • 初始标记 Initial Mark
    此阶段是 STW 的,不能从根上就乱
    标记符合要求的老年代对象(因为是老年代垃圾收集器,年轻代不管),包括

    • GC Root 直接引用的老年代对象
    • 从年轻代引用的老年代对象
  • 并发标记(以及 并发预清理 和 可终止并发预清理,这三步在本文中归类为同一个阶段) Concurren Mark + PreClean
    需要注意,因为并发标记阶段是并发的,对象间的引用关系在时刻变化,所以此阶段 100% 精准的标记所有对象是不现实的
    这就需要在后面增补一个阶段(最终标记),对并发标记阶段的工作做一个确认和订正,这个新阶段为了准确性,肯定是 STW 的
    因此并发标记阶段的主要目的实际上尽量减少下一个阶段的工作量,使最终标记阶段的 STW 时间尽可能短
    而使下一个阶段工作量尽量少的方法,是在并发标记阶段通过三步尽量准确的标记非垃圾对象

    • 并发标记开始,从初始标记的对象起,完成可达性算法,标记所有 非垃圾
    • 并发标记过程中引用关系发生变化的对象被标记为 dirty,dirty 对象是 非垃圾,但不再继续计算可达性
      这是因为是和应用线程并行的,并发标记过程中对象引用关系可能发生变化
      结合下文,这里可能有个坑:如何保证并发标记时没有引用变化,没被标记为 dirty 的对象,后面不会 dirty
      这种对象如果变为 dirty,从本文梳理的流程上看,是无法感知的,作者只能理解为这部分对象会在最终标记阶段修正
    • 并发预清理开始,标记 dirty 对象的区域为 dirty card,并从 dirty 对象开始计算可达性,可达的标记为 非垃圾
    • 并发可终止预处理开始,这个阶段是个循环
      此循环持续到发生abort的条件:重复的次数、多少量的工作、持续的时间等
      这个阶段用来做两件事
      • 扫描 FROM 和 TO 区的对象,变更对老年代对象的引用时,标记新的老年代可达对象为 非垃圾 (可能也是 dirty)
      • 扫描 dirty card 中的对象,继续标记可达的为 非垃圾
  • 最终标记 Remark
    此阶段是 STW 的,否则不能保证 100% 准确
    结合上一阶段的结果,做最后的确认和修正,包括如下内容

    • 扫描 GC Root 标记引用的老年代对象
    • 扫描新生代(应该主要是 FROM 和 TO),重新标记被引用的老年代对象
    • 扫描老年代的Dirty Card,重新标记,上一步的很多工作主要就在这一步获利
  • 并发清除 Concurrent Sweep
    正常的标记清除算法中的清除部分,因为需要清除的东西都提前标好了所以不用 STW
    重置本 GC 周期中变更的状态,比如 dirty card
    垃圾、dirty、非垃圾 也会称为白、灰、黑,整个上述过程也被称为 三色标记法

CMS 的缺点

  • 会产生内存碎片
    因为是基于标记清除算法
  • 对CPU资源非常敏感
    因为会占用线程做并发收集,严重时总吞吐量可能会变低
  • 不能处理浮动垃圾
    浮动垃圾就是并发标记过程中因为引用变化而新产生的垃圾,且未能作为垃圾识别

常见参数

  • -XX:CMSInitiatingOccupancyFraction 设置 GC 预留空间
    这是因为在并发的过程中,若内存本身不太充足,但应用线程还是造出一堆对象导致没有足够的内存来 GC
    预留空间是为了防止这个场景,空间不足认为 GC 有风险,出现 Concurrent Mode Failure ,启用备用的 Serial Old 收集器

CMS 相当于将在可达性算法的遍历过程中遇到的对象,划分为层次较深的和较浅的两个部分
层次较浅的部分,通过 STW 的初始标记和最终标记确定
层次较深的部分,通过并发标记三步骤持续跟进,并在最终标记中确认
但还是感觉有点怪,假设有个 GC Root,它直连了两个老生代对象,并在这两个对象间反复横跳。则,这两个对象不会被标记为 dirty
因为 dirty 的处理都依赖于第一次对 dirty 的标记,而第一次 dirty 的标记只在 并发标记这个步骤(不是阶段)中做一次,不循环
然后在确认阶段只能二选一?? 这俩对象如果后面还连着一大串呢??

G1
JDK 7u4 及以上的版本全面支持 G1 垃圾收集器
G1 服务端垃圾收集器,服务对象时多核大内存的机器
兼顾 高吞吐量GC 暂停时间目标的高概率达成(G1 允许设置一个时间长度,作为 GC 暂停时间目标)
设计目标如下:

  • 可以与应用线程并发操作,像 CMS 一样
  • 压缩可用空间,但不会因 GC 导致冗长的暂停时间
  • 需要 GC 暂停时间更加的可预期
  • 不能牺牲过多的吞吐量
  • 不能占用大量 java 堆

其他垃圾收集器的通理

  • 年轻代与老年代是 各自独立连续 的内存块
  • 年轻代的收集必须对一整个 Eden + FROM + TO 使用复制算法
  • 老年代的收集必须对一整个 老年代区域 进行完整扫描标记
  • 设计原则都是时 GC 尽可能的

相对于 其他垃圾收集器,G1

  • 以 垃圾第一(Garbage-First) 为原则,设计理念都不同
  • 不再一体化的看待堆中的内存空间,而是将它们视为众多小区域的集合
    • 堆内存划分为众多 region,region 大小一致
      region 大小可用值为 [1、2、4、8、16、32] M
      JVM 会默认将堆划分为 2048 个等大小 region
      G1 支持的最大内存为 32M * 2048 = 64G
    • region 可以切换角色,在不同时间中可能逻辑上属于 Eden、Survivor、Old 或 Humongous
      Survivor 就是其他垃圾收集器的 FROM 和 TO (其他收集器也称其为 s0、s1),但现在碎片化了,就没有必要区分了
      Humongous(巨大的) 是为 巨型对象 设计的特殊 region
      • 一个对象所需大的空间就能超过 region 一半的就视为 巨型对象
        其他垃圾收集器中,巨型对象会直接存放在老年代,但如果巨型对象是个短期对象,就会影响 GC 效率(因为回收的空间大)
        G1 中由于内存碎片化了,所以不会出现这个问题
      • 巨型对象 大到单个 Humongous region 放不下,就会将它放到 连续 Humongous region
        某些场景下,寻找、创造 连续 Humongous region 会导致 Full GC
  • 依然是分代收集器,但只是逻辑分代了
    对于逻辑上属于新生代的 region,垃圾收集时依然 STW
    对于逻辑上属于新生代的 region,通过将对象复制到其他 region 的方式进行清理(复制的同时完成堆的压缩,类似碎片整理)

相对于 CMS ,G1

  • 不会产生内存碎片
  • STW 更加可控,当然也能尽量缩短
    G1 在暂停时间上添加了预测机制,可以指定期望的暂停时间,
    上文 GC 暂停时间目标的高概率达成 的意思,就是尽最大可能,将 GC 暂停时间控制在这个预测时间之内

G1 对 Eden region 的收集

  • 现有的 Eden region 耗尽才会触发
    G1 会计算当前 Eden region 垃圾收集的时间
    若远小于 GC 暂停时间目标,则只会开垦(Space Reclamation) 新的 Eden region
    直到垃圾收集时间接近配置值,才会真的进入收集过程
  • 垃圾收集后会将非垃圾的对象复制 Survivor region
    并不是把整个 Eden region 升级为 Survivor region,而是收集其中活着的对象,复制到 Survivor region
    如果 Survivor region 也耗尽了,会复制到 Old region
  • 收集过程在小区域完成,并尽量使已开垦的 region 之间、未开垦的 region 之间连续
    因为只是对 region 级别内存空间的垃圾收集

常见参数

  • -XX:G1HeapRegionSize 设置 region 大小
    大小可以设置为 [1、2、4、8、16、32] M
  • -XX:MaxGCPauseMills 设置 GC 暂停时间的期望值
    即上文的 GC 暂停时间目标
  • -XX:InitiatingHeapOccupancyPercent 设置触发 GC 的堆占用空间阈值,后面的数字代表百分比
    默认值是 45,即若当前堆(因为还有未开垦的)已经被占用了 45% 触发 GC
  • -XX:ConcGCThreads 设置 GC 使用的并发线程数
    同 parallel 的一样
  • -XX:G1ReservePercent 设置 G1 预留空间的百分比
    默认值 10,取值范围 [0,50]
    G1 是基于标记整理算法的,垃圾收集过程需要一定的空间去完成复制
    设置此参数有利于降低 G1 收集器升级失败的可能性
§3.3 默认垃圾收集器横比
名字类别线程并发算法适用区域适用场景说明
Serial×复制新生代非服务器
Serial Old×标记整理老生代非服务器
ParNew×复制新生代弱交互场景Serial 的多线程版本
server模式下,很多 JVM 的默认新生代收集器
Parallel Scavenge×复制新生代弱交互场景注重吞吐量而不是降低垃圾收集时间
Parallel Old×标记整理老生代弱交互场景
CMSCMS多?标记清除老生代强交互场景JDK14 中凉了
G1G1标记整理新生代 + 老生代大内存场景STW 时间过短可能造成回收效果不理想
ZGCZGC复制(基于着色指针改进)新生代 + 老生代大内存场景
§3.4 垃圾收集器参数配置与组合

组合示意图,与已知过期废除

配置参数组合新生代收集器老生代收集器说明
-XX:+UseSerialGCDefNew + TenuredSerialSerial Old
-XX:+UseSerialOldGCPSYoungGen + TenuredParallel ScavengeSerial Old现已不可用
-XX:+UseParNewlGCParNew + TenuredParNewSerial OldJDK 9 废弃了 ParNew+Serial Old
JDK 14 移除了 CMS
所以 JDK 14+ 中 ParNew 实际没法用了
-XX:+UseParallelGCPSYoungGen + ParOldGenParallel ScavengeParallel Old默认垃圾收集组合,至少 JDK 8 是
-XX:+UseParallelOldGCPSYoungGen + ParOldGenParallel ScavengeParallel Old-XX:+UseParallelGC 互相激活
-XX:+UseConcMarkSweepGCParNew + CMS + TenuredParNewCMS
Serial Old(备胎)
会激活 -XX:+UseParNewlGC
同时选择 Serial Old 作为老生代收集出错时的后备
-XX:+UserG1GC


区域与垃圾收集器标记可能的值与含义见下表

标记含义对应垃圾收集器区域
DefNewDefault New GenerationSerial新生代
TenuredOldSerial Old老生代
ParNewParallel New GenerationParNew新生代
PSYoungGenParallel ScavengeParallel Scavenge新生代
ParOldGenParallel Old GenerationParallel Old老生代
CMSConcurrent Mark SweepCMS老生代
CMS
§3.5 垃圾收集器的选择

单核小内存单机:-XX:+UseSerialGC
多核大吞吐量弱交互:-XX:+UseParallelGC
多核低卡顿强交互: -XX:+UseConcMarkSweepGC-XX:+UseParNewlGC(老版本)

§4 GC 类型

Young GC(Minor GC)

  • 发生在新生代的 GC ,用于回收 Eden 和 From 区大的垃圾
  • Eden 区空间不足时触发
  • Minor GC 会频繁的发生,并且速度很快

Full GC

  • 发生在老年代的GC,用于回收 Eden、From 以及 老年代的垃圾
  • 在下列场景下触发
    • System.gc() 向 JVM 申请的 GC 就是 Full GC ,因此不建议使用
    • Minor GC 使用复制算法时,若新生代中包含大对象,FROM/TO 区放不下,会转存老年代,老年代空间不够用时触发 Full GC
    • 老年代中最大连续可用空间小于历代转存老年代对象的平均大小时触发
  • Full GC 不仅速度很慢,还会 Stop-The-World,即暂停整个应用
  • JVM 的调优和配置时,应结合实际情况尽量减少 Full GC

§5 GC 日志

§5.1 开启 GC 日志

通过 JVM 参数 -XX:+PrintGCDetails 开启 GC 日志
参考 基础 | JVM - [参数]

开启 GC 日志后,运行 java 时会先打印堆信息,如下图

§5.2 GC 日志与格式

GC 日志示例

[GC (Allocation Failure) [PSYoungGen: 512K->488K(1024K)] 512K->512K(1536K), 0.0011107 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 995K->504K(1024K)] 1019K->644K(1536K), 0.0006667 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1016K->504K(1024K)] 1156K->771K(1536K), 0.0005018 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 869K->512K(1024K)] 1136K->1003K(1536K), 0.0004885 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 512K->365K(1024K)] [ParOldGen: 491K->290K(512K)] 1003K->656K(1536K), [Metaspace: 3232K->3232K(1056768K)], 0.0027570 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 365K->353K(1024K)] [ParOldGen: 290K->286K(512K)] 656K->639K(1536K), [Metaspace: 3232K->3232K(1056768K)], 0.0027270 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

GC 日志格式
以第一行为例
完整 GC 日志 格式
[GC 信息][Times 信息]

[GC 信息] 格式

[GC (Allocation Failure) [PSYoungGen: 512K->488K(1024K)] 512K->512K(1536K), 0.0011107 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

[类型 (状态) [区域 GC 信息] 堆信息, GC 时间]

[类型 (状态)
GC 类型,是 Minor GC 还是 Full GC

[区域 GC 信息] 格式

[PSYoungGen: 512K->488K(1024K)]

[区域与垃圾收集器标记: 收集前大小->收集后大小(总大小)]

区域与垃圾收集器标记可能的值与含义参考 垃圾收集器参数配置与组合

堆信息 格式
512K->512K(1536K)
收集前大小->收集后大小(总大小)
堆信息排在新生代/老生带信息之后,元空间信息之前

GC 时间
本次 GC 使用的时间

[Times 信息] 格式
[Times: 用户耗时 系统耗时, 实际耗时]

本文标签: 基础JVMGC