JVM垃圾回收

94 阅读5分钟

QA

什么场景下该使用什么垃圾回收策略?

  • 在对内存要求苛刻的场景:想办法提高对象的回收效率,多回收掉一些对象,腾出更多内存
  • 在CPU使用率高的情况下:降低高并发时垃圾回收的频率,让SPJ更多地去执行你的业务而不是垃圾回收

垃圾回收发生在哪些区域?

  • 堆:回收创建的对象
  • 方法区:回收废弃的常量以及不需要使用的类

对象在什么时候能够被回收?

引用计数法(循环引用失效)

  • 通过对象的引用计数器来判断该对象是否被引用(例如a引用b,b的引用计数器加一,当退出引用时则减一,当引用计数器为0时则可以被回收)

可达性分析

  • 以根对象( GC Roots)作为起点向下搜索,走过的路径被称为引用链(Reference Chain),如果某个对象到根对象没有引用链相连时,就认为这个对象是不可达的,可以回收

GC Roots包括那些对象?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象(局部变量)
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI (即Native方法)引用的对象

引用

  • 强引用( Strong Reference)
    • 形如Object obj = new Object()的引用
    • 只要强引用在,永远不会回收被引用的对象
  • 软引用( Soft Reference)
    • 形如SoftReference<String> sr = new SoftReference<>("hello")
    • 是用来描述一些有用但非必需的对象
    • 软引用关联的对象,只有在内存不足的时候才会回收加信
  • 弱引用
    • 形如WeakReference<String> sr = new WeakReference<>("hello")
    • 弱引用也是用来描述非必需对象的
    • 无论内存是否充足,都会回收被弱引用关联的对象
  • 虚引用( Phantom Reference)
    • 形如ReferenceQueue<String> queue = new ReferenceQueue<>();PhantomReference<String> pr = new PhantomReference<>("hello", queue);
    • 不影响对象的生命周期,如果一个对象只有虚引用,那么它就和没有任何引用二样,在往荷府候都可能被垃圾回收器回收。虚引用主要用来跟踪対豪被垃圾回收回收的活动,必须和引用队列(ReferenceQueue)配合使用,当垃级回收器准备回收一个对象时,如果发现它还复虑引用就会在回收对象的内存之前,把这个虚引用加入到之前关联的队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动

可达性算法注意点

  • 一个对象即时不可达,也不一定会被回收 Snipaste_2022-07-31_22-42-27.png

finalize()的建议

  • 避免使用finalize()方法,操作不当可能会导致问题

垃圾回收算法

基础垃圾回收算法

标记-清除(Mark-Sweep)

  • 标记需要回收的对象
  • 清理掉要回收的对象 Snipaste_2022-07-31_22-42-27.png

标记-整理(Mark-Compact)

  • 标记需要回收的对象
  • 把所有的存活对象压缩到内存的一端
  • 清理掉边界外的所有空间 Snipaste_2022-07-31_22-42-27.png

复制(Copy)

  • 把内存分为两块,每次只使用一块
  • 将正在使用的内存中的存活对象复制到未只用的内存中,然后清除掉正在使用的内存中的所有对象
  • 交换两个内存的角色,等待下一次回收 Snipaste_2022-07-31_22-42-27.png

三种算法对比

回收算法优点缺点
标记-清除实现简单存在内存碎片、分配内存速度会受影响
标记整理无碎片整理存在开销
复制性能好、无碎片内存利用率低

综合垃圾回收算法

分代收集算法

  • 把内存分成多个区域,不同区域使用不同的回收算法回收对象
  • 各种商业虚拟机堆内存的垃圾收集基本上都采用了分代收集
  • 根据对象的存活周期,吧内存分成多个区域,不同区域使用不同的回收算法回收对象

回收类型

  • 新生代回收(Minor GC | Young GC)
  • 老年代回收(Major GC)
  • 清理整个堆(Full GC)
  • Major GC ≈ Full GC

对象分配过程

  1. 当伊甸园(Eden)中存活的对象进过一次垃圾回收后会存活的对象会进入到From survivor或者To survivor中等待下一次垃圾回收
  2. 在From survivor和To survivor中的对象则采用复制回收算法进行回收,当这两个区域的对象在经历一次垃圾回收后存活的对象年龄就会增加1,当年龄到达15的阈值时则会进入到老年代(Tenured)
  3. 老年代(Tenured)则会采用标记清除或者标记整理进行回收

新建的对象不一定分配到伊甸园

  • 对象大于-XX:PretenureSizeThreshold,就会直接分配到老年代
  • 新对象空间不够(新生代采用复制算法,在伊甸园中分配大对象则会导致伊甸园和两个survivor区中存在大量拷贝)

对象不一定要达到年龄才进入老年代

  • 动态年龄:如果survivor空间中所有相同的年龄对象大小的总和大于survivor空间的一半,那么年龄大于等于改年龄的对象可以直接进入老年代

触发垃圾回收的条件-新生代(Minor GC)

  • 伊甸园中空间不足

触发垃圾回收的条件-老年代(Full GC)

  • 老年代空间不足
    1. 空间真的不足
    2. 内存碎片没有连续的空间
  • 源空间不足
  • 要晋升到老年代的对象所占用的空间大于老年代的剩余空间
  • 显示调用System.gc()
    1. 建议垃圾回收期执行垃圾回收
    2. -XX:+DisableExplicitGC 参数,忽略掉System.gc()的调用

分代的好处

  • 更有效的清除不再需要的对象
  • 提升了垃圾回收的效率

分代收集算法调优原则

  • 合理设置Survivor区域大小,避免内存浪费
  • 让GC尽量发生在新生代,尽量减少Full GC的发生

增量算法

  • 每次只收集一小片区域的内存空间的垃圾

堆内存JVM参数

参数作用作用
-XX:NewRatio=n老年代:新生代内存大小比值2
-XX:SurvivorRatio=n伊甸园:survivor区内存大小比值8
-XX:PretenureSizeThreshold=n对象大小该值就在老年代分配,0表示不做限制0
-Xms需要小堆内存-
-Xmx需要大堆内存-
-Xmn新生代大小-
-XX:+DisableExplicitGC忽略掉System.gc()的调用启用
-XX:NewSize=n新生代初始内存大小-
-XX:MaxNewSize=n新生代最大内存-

垃圾收集器

术语

  • Stop The World
    • 简写成为STW,移交全局停顿,Java代码停止运行,native代码继续运行,但不能与JVM进行交互
    • 原因:多半由于垃圾回收导致;也可能由Dump线程、死锁检查、Dump堆等导致
    • 危害:服务停止、没有响应;主从切换,危害生产环境
  • 并行收集vs并发收集
    • 并行收集:指多个垃圾回收集线程并行工作,但是收集过程中,用户线程(你的业务线程)还是等待状态
    • 并发收集:指用户线程与垃圾收集线程同时工作
  • 吞吐量
    • CPU用于运行用户代码的时间与CPU总消耗的比值
    • 公式:运行用户代码的时间/(运行用户代码时间+垃圾收集时间)

新生代收集器

Serial收集器

  • 最基本的、发展历史最悠久的收集器
  • 复制算法

特点

  • 单线程
  • 简 单、高效(相对于其他垃圾收集器单线程高效,因为没有和其他线程之前沟通的开销)
  • 收集过程全程Stop The World

适用场景

  • 客户端程序,应用以-client模式运行时,默认适用的就是Serial收集器
  • 单核机器

ParNew收集器

  • Serial收集的多线程版,除使用了多线程以外,其他和Serial收集器一样,包括:JVM参数,Stop The World的表现、垃圾收集算法都是一样的。

特点

  • 多线程
  • 可以使用-XX:ParallelGCThreads设置垃圾收集的线程数(CPU核数)

适用场景

  • 主要用来和CMS收集器配合使用

Parallel Scavenge收集器

  • 也叫吞吐量优先收集器
  • 采用的也是复制算法
  • 也是并行的多线程收集器,这一点和ParNew类似

特点

  • 可以达到一个可控制的吞吐量
    • -XX:MaxGCPauseMillis:控制最大的垃圾收集停顿时间(尽力)
    • -XX:GCTimeRatio: 设置吞吐量的大小,取值0-100,系统花费不超过1/(1+n)的时间用户垃圾收集
  • 自适应GC策略: 可用-XX:+UseAdptiveSizePolicy打开
    • 打开自适应策略后,无需手动设置新生代的大小(-Xmm)、Eden与Survivor区的比列(-XX:SurvivorRatio)等参数
    • 虚拟机会自动根据系统的运行状况收集性能监控信息,动态调整这些参数,从而达到最优的停顿时间以及追高的吞吐量

适用场景

  • 注重吞吐量的场景

老年代收集器

Serial Old收集器

  • Serial收集器的老年版本
  • 标记整理算法 MicrosoftTeams-image 2.png

适用场景

  • 可以和Serial/ParNew/Parallel Scavenge这三个新生代的垃圾收集器配合使用
  • CMS收集器出现故障的时候,会用Servial Old作为后备

Parallel Old收集器

  • Parallel Scavenge收集器的老年代版本
  • 标记整理算法 MicrosoftTeams-image 2.png

特点

  • 只能和Parallel Scavenge配合使用

适用场景

  • 关注吞吐量的场景

CMS收集器

  • CMS: Concurrent Mark Sweep 并发标记清除收集器
  • 并发收集器
  • 标记清除算法 MicrosoftTeams-image 2.png
  1. 初始标记(initial mark)
    • 标记GC Roots能直接关联到的对象
    • Stop The World (停留时间比较短)
  2. 并发标记(concurrent mark)
    • 当前阶段用户线程和标记线程并发执行
    • 找出所有GC Roots能关联到的对象
    • 并发执行,无 Stop The World
  3. 并发预清理(concurrent-preclean)
    • 重新标记那些在并发标记阶段,引用被更新的对象,从而减少后面重新标记阶段的工作量
    • 并发执行,无Stop The World
    • 可使用XX:-CMSPrecleaningEnabled关闭并发预清理阶段,默认打开
  4. 并发可中止的预清理阶段( concurrent-abortable-preclean )
    • 和并发预清理做的事情一样,并发执行,无Stop The World
    • 当Eden的使用量大于CMSScheduleRemarkEdenSizeThreshold 的阅值(默认2M )时,才会执行该阶段
    • 主要作用:允许我们能够控制预清理阶段的结束时机。比如扫描多长时间( CMSMaxAbortablePrecleanTime,默认5秒)或者Eden区使用占比达到一定阅值( CMSScheduleRemarkEdenPenetration ,默认50%)就结束本阶段
  5. 重新标记(remark)
    • 修正并发标记期间,因为用户程序继续运行,导致标记发生变动的那些对象的标记(问题:已经死亡的对象错误标记为存活,把存活的对象标记为死亡)
    • 一般来说,重新标记花费的时间会比初始标记阶段长一些,但比并发标记的时间短
    • 存在Stop The World
  6. 并发清除(concurrent sweep)
    • 基于标记结果,清除掉要清楚前面标记出来的垃圾
    • 并发执行,无Stop The World
  7. 并发重置(concurrent reset)
    • 清理本次CMS Gc的上下文信息,为下一次GC做准备

优点

  • Stop The World时间比较短
  • 大多数过程都是并发执行

缺点

  • CPU资源比较敏感
    • 并发阶段可能导致应用吞吐量的降低
  • 无法处理浮动垃圾
  • 不能等到老年代几乎满了才开始收集
    • 预留的内存不够 -> Concurrent Mode Failure -> Serial Old作为后备
    • 可以使用CMSInitiatingOccupancyFraction设置老年代占比达到多少就触发垃圾收集,默认68%
  • 内存碎片
    • 标记-清除导致碎片的产生
    • UserCMSCompactAtFullCollection:完成Full GC后是否要进行内存碎片整理,默认开启
    • CMSFullGCsBeforeCompation:进行几次Full GC后进行一次内存碎片整理,默认0

适用场景

  • 希望系统停顿时间短,响应速度的场景,比如各种服务器应用程序

CMS收集器-总结

  1. 初始标记
  2. 并发标记
  3. 并发预清理
  4. 并发可终止的预清理阶段
  5. 重新标记
  6. 并发清除
  7. 并发重置

G1收集器

  • Garbge First
  • 面向服务器端应用的垃圾收集器
  • 可以用在新生代和老年代

内存布局

Snipaste_2022-08-04_12-59-45.png

  • Humongous Region 是存放大对象的,当这个对象超过了Region的一半就会存放到Humongous Region中去
  • 如果一个对象超级大,一个Humongous Region存放不了,则会放在多个连续的Humongous Region中

Region

  • G1收集器将整个Java堆划分为多个大小想等的区域(Region)
  • 通过参数-XX:G1HeapRegion指定Region的大小
  • 取值范围为1MB ~ 32MB,应为2的N次幂

设计思想

  • 内存分块(Region)
  • 跟踪每个Region里面的垃圾堆的价值大小
  • 构建一个优先列表,根据允许的收集时间,优先回收价值高的Region

垃圾收集机制

Young GC

  • 所有的Eden Region都满了的时候,就会触发Young GC
  • 伊甸园里面的对象会转移到Survivor Region里面去
  • 原先Survivor Region中的对象转移到新的Survivor Region中,或者晋升到Old Region
  • 空闲Region会被放入空闲列表中,等待一次被使用

Mixed GC

  • 老年代大小占整个堆的百分比达到一定阈值(可用-XX:InitiatingHeapOccupancyPercent指定,默认45%),就触发
  • Mixed GC 会回收所有Young Region,同时回收部分Old Region Snipaste_2022-08-04_12-59-45.png
  • 初始标记(Initial Marking)
    • 标记GC Roots能直接关联到的对象,和CMS类似
    • 存在Stop The World(时间比较短)
  • 并发标记(Concurrent Marking)
    • 同CMS的并发标记
    • 并发执行,没有Stop The World
  • 最终标价(Final Marking)
    • 修正在并发标记期间引起的变动
    • 存在Stop The World
  • 筛选回收(Live Data Counting and Evacuation)
    • 对各个Region的回收价值和成本进行排序
    • 根据用户所期望的停顿时间(MaxGCPauseMillis)来制定回收计划,并选择一些Region回收
    • 回收过程
      • 选择一系列Region构成一个回收集
      • 把决定回收的Region中的存活对象复制到空的Region中
      • 删除掉需要回收的Region -> 无内存碎片
    • 存在Stop The World

Full GC

  • 复制对象内存不够,或者无法分配足够内存(比如巨型对象没有足够的连续分区)时,会触发Full GC
  • Full GC模式下,使用Serial Old模式(会长时间Stop The World)
  • G1优化原则: 尽量减少Full GC的发生
减少Full GC的思路
  • 增加预留内存(增大-XX:G1ReservePercent,默认为堆的10%)
  • 更早地回收垃圾(减少-XX:InitiatingHeapOccupancyPercent,老年代达到该值就触发Mixed GC,默认45%)
  • 增加并发阶段使用的线程数(增大-XX: ConcGcThreads)

特点

  • 可以作用在整个堆
  • 可控的停顿(MaxGCPauseMillis=200)
  • 无内存碎片

适用场景

  • 占用内存较大的应用(6G以上)
  • 替换CMS垃圾收集器

G1 or CMS

  • 对于JDK8 : 都可以用
    • 如果机器内存<=6G,建议用CMS,如果>6G,建议使用G1
  • 如果> JDK8: G1
    • CMS从JDK8 已经被废弃了

其他垃圾收集器

  • Shenandoah
  • ZGC
  • Epsilon

如何选择垃圾收集器

  • 关注的主要矛盾是什么?
  • 基础设施
  • JDK