JVM垃圾回收

内容纲要

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)配合使用,当垃级回收器准备回收一个对象时,如果发现它还复虑引用就会在回收对象的内存之前,把这个虚引用加入到之前关联的队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动

可达性算法注意点

  • 一个对象即时不可达,也不一定会被回收

finalize()的建议

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

垃圾回收算法

基础垃圾回收算法

标记-清除(Mark-Sweep)

  • 标记清除法( Mark Sweep )算法分成标记和清除两个阶段先标记出要回收的对象,然后统一回收这些对象
  • 优点是简单
  • 缺点是:
    • 效率不高,标记和清除的效率都不高
    • 标记清除后会产生大量不连续的内存碎片,从而导致在分配大对象时触发GC

标记-整理(Mark-Compact)

  • 标记整理算法( Mark-Compact ) :由于复制算法在存活对象比较多的时候,效率较低,且有空间浪费,因此老年代一般不会选用复制算法,老年代多选用标记整理算法
  • 标记过程跟标记清除一样,但后续不是直接清除可回收对象,而是让所有存活对象都向一端移动,然后直接清除边界以外的内存

复制(Copy)--新生代存活区使用该算法

  • 复制算法( Copying) : 把内存分成两块完全相同的区域,每次使用其中一块,当一块使用完了,就把这块上还存活的对象拷贝到另外一块,然后把这块清除掉
  • 把内存分为两块,每次只使用一块
  • 将正在使用的内存中的存活对象复制到未使用的内存中,然后清除掉正在使用的内存中的所有对象
  • 交换两个内存的角色,等待下一次回收
  • JVM实际实现中,是将内存分为一块较大的Eden区和两块较小的Survivor空间,每次使用Eden和一块Survivor ,回收时,把存活的对象复制到另块Survivor
  • HotSpot默认的Eden和Survivor比是8:1 ,也就是每次能用90%的新生代空间
  • 如果Survivor空间不够,就要依赖老年代进行分配担保,把放不下的对象直接进入老年代
    • 分配担保是:当新生代进行垃圾回收后,新生代的存活区放置不下,那么需要把这些对象放置到老年代去的策略,也就是老年代为新生代的GC做空间分配担保,步骤如下:
      1. 在发生MinorGC前,JVM会检查老年代的最大可用的连续空间,是否大于新生代所有对象的总空间,如果大于,可以确保MinorGC是安全的
      2. 如果小于,那么JVM会检查是否设置了允许担保失败如果允许,则继续检查老年代最大可用的连续空间,是否大于历次晋升到老年代对象的平均大小
      3. 如果大于,则尝试进行一次MinorGC
      4. 如果不大于,则改做一次Full GC

三种算法对比

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

综合垃圾回收算法

分代收集算法

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

回收类型

  • 新生代回收(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总消耗的比值
    • 公式:运行用户代码的时间/(运行用户代码时间+垃圾收集时间)

串行收集器

  • 优点是简单,对于单cpu ,由于没有多线程的交互开销,可能更高效,是默认的Client模式下的新生代收集器
  • 使用XX:+UseSerialGC来开启,会使用: Serial + SerialOld的收集器组合
  • 新生代使用复制算法,老年代使用标记-整理算法

并行收集器

  • ParNew (并行)收集器:使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-World
  • 在并发能力好的CPU环境里,它停顿的时间要比串行收集器短;但对于单cpu或并发能力较弱的CPU,由于多线程的交互开销,可能比串行回收器更差
  • 是Server模式下首选的新生代收集器,且能和CMS收集器配合使用
  • 不再使用-XX:+UseParNewGC来单独开启

新生代收集器

Serial收集器

  • 最基本的、发展历史最悠久的收集器
  • 复制算法
  • safepoint垃圾回收安全点

特点

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

适用场景

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

ParNew收集器

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

特点

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

适用场景

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

Parallel Scavenge收集器

描述

  • 新生代Parallel Scavenge收集器/Parallel Old收集器:是一个应用于新生代的、使用复制算法的、并行的收集器

  • 跟ParNew很类似,但更关注吞吐量,能最高效率的利用CPU ,适合运行后台应用

  • 使用-XX:+UseParallelGC来开启

    • 使用-XX:+ UseParallelOldG来开启老年代使用ParallelOld收集器,使用Parallel Scavenge + Parallel Old的收集器组合
  • 也叫吞吐量优先收集器

  • 采用的也是复制算法

  • 也是并行的多线程收集器,这一点和ParNew类似

特点

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

适用场景

  • 注重吞吐量的场景

老年代收集器

Serial Old收集器

  • Serial收集器的老年版本
  • 标记整理算法

适用场景

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

Parallel Old收集器

  • Parallel Scavenge收集器的老年代版本
  • 标记整理算法

特点

  • 只能和Parallel Scavenge配合使用

适用场景

  • 关注吞吐量的场景

CMS收集器

  • CMS ( Concurrent Mark and Sweep并发标记清除)收集器分为:初始标记:只标记GC Roots能直接关联到的对象;并发标记:进行GC Roots Tracing的过程
  • 并发收集器
  • 标记清除算法

    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收集器

  • G1 ( Garbage-First )收集器:是一款面向服务端应用的收集器,与其它收集器相比,具有如下特点:
    1. G1把内存划分成多个独立的区域(Region)
    2. G1仍采用分代思想,保留了新生代和老年代,但它们不再是物理隔离的,而是一部分Region的集合 ,且不需要Region是连续的
    3. 新生代和老年代属于逻辑上的隔离
    4. G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW
    5. G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片
    6. G1的停顿可预测,能明确指定在一个时间段内 , 消耗在垃圾收集上的时间不能超过多长时间(时间设置不合理很容易产生频繁垃圾回收)
    7. G1跟踪各个Region里面垃圾堆的价值大小(回收那些块能够释放的区域最大或那些块里面的垃圾最多),在后台维护一个优先列表,每次根据允许的时间来回收价值最大的区域,从而保证在有限时间内的高效收集

内存布局

  • 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
  • 初始标记(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
THE END
分享
二维码
< <上一篇
下一篇>>