JVM垃圾回收

如何判断对象可以回收

  • 引用计数法
  • 可达性分析算法
    • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
    • 扫描堆中的对象,看是否能够沿着GC ROOT对象为起点的引用链找到该对象,找不到,可以回收
    • 那些对象可以作为GC ROOT
      • 当前正在运行的方法栈参数、静态变量、常量以及本地方法(Native)所引用的对象,都可以作为 GC Roots。
  • 四种引用
    • 强引用:从不回收,除非不可达
    • 软引用:内存不足时(图片缓存)
    • 弱引用:只要 GC 扫描到就回收
    • 虚引用:任何时候都可能被回收
    • 终接器引用:第一次入队,第二次再次判断引用类型,判断是否删除

软引用、弱引用引用的对象被回收后会放入引用队列,之后判断是否释放

回收算法

  • 标记+清除
    • 速度快
    • 内存碎片
  • 标记+整理
    • 速度慢
    • 没有内存碎片
  • 复制:划分两个区域,一个一直为空,一个用,发生GC后复制数据到另一侧
    • 没有内存碎片
    • 需要占用双倍内存空间

分代回收

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄加1并且交换from和to
  • minor gc会引发stop the world(STW),暂停其他用户线程,等垃圾回收结束用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升到老年代,最大寿命是15,当幸存区内存紧张,会提前进入老年代
  • 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍然不足,那么触发full gc,STW时间更长

当对象寿命达到15,就会晋升到老年代

当新生代和老年代空间都不足时会触发Full GC(整个清理)

相关VM参数

查表

子线程的out of memery 不会导致主线程结束

垃圾回收器

  1. 串行(-XX+UseSerialGC=Serial+serialOld)
    • 单线程
    • 堆内存较小,适合个人电脑
  2. 吞吐量优先(-XX:+UseParallelGC -XX:+UseParallelOldGC)
    • 多线程
    • 堆内存较大,多核cpu
    • 让单位时间内STW的时间最短
  3. 响应时间优先(-XX:UseConcMarkSweepGC -XX:+UseParNewGC SeriaOld)
    • 多线程
    • 堆内存较大,多核cpu
    • 尽可能让单次STW的时间最短


      串行参数解释
      Serial:工作在新生代(复制算法)
      SerialOld:工作在老年代(标记+整理)

吞吐量参数解释

parallel:并行(垃圾回收线程执行,用户线程阻塞)

useParallelGC:新生代,并行垃圾回收

useParallelOldGC:老年代

响应时间参数解释

concurrent:并发(用户线程和垃圾回收线程同时执行)

初始标记(用户线程阻塞)->并发标记(用户线程运行)->重新标记(并行)->清理(用户线程运行)

CMS可能并发失败,此时退化为串行垃圾回收器

CMS是作用于老年代的一种并发垃圾回收器(标记+清除)

浮动垃圾:并发运行时产生的垃圾

需要预留空间保留浮动垃圾

-XX:CMSInitiatingOccupancyFraction=percent

在并行标记时,先进行新生代垃圾回收

-XX:+CMSScavengeBeforeRemark

CMS 退化为 Full GC 主要有两种情况,一种是由于采用标记-清除算法导致内存碎片严重,大对象分配失败触发 Full GC;另一种是在并发标记期间产生大量浮动垃圾,导致回收速度跟不上分配速度,从而发生并发失败,最终退化为 Full GC。

G1垃圾回收器

适用场景:

  • 同时注重吞吐量和低延迟,默认的暂停目标是200ms
  • 超大堆内存,会将堆划分为多个大小相等的region
  • 整体上是标记+整理算法,两个区域之间是复制算法

1.8要开启 -XX:+UseG1GC

1.9之后为默认

G1垃圾回收阶段

G1 的垃圾回收主要分为 Young GC、并发标记和 Mixed GC 三个阶段。Young GC 会暂停用户线程,采用复制算法回收新生代对象。

当堆使用率达到一定阈值时,会触发并发标记,初始标记阶段会借助一次 Young GC 完成,随后进行并发标记和最终标记(Remark)。

在标记完成后,G1 会执行 Mixed GC,在回收新生代的同时选择部分回收价值较高的老年代 Region,而不是全部回收。

回收老年代不会全部回收,会根据暂停目标选择回收价值最高的区域

当垃圾回收速度<垃圾产生速度

会并发失败,退化为串行垃圾回收器

Young Collection跨代引用问题

老年代引用新生代问题

遍历老年代找到GC root很慢

解决:卡表(标记GC root脏卡)

  • 卡表与Remembered Set
  • 在引用变更时通过post-write barrier+dirty card queue
  • concurrent refinement threads 更新remembered Set

卡表:记录"哪里发生了引用变化"

RSet:记录"这些变化中,谁引用了我"

卡表用于记录堆中哪些 Card 发生了引用修改,而 RSet 则以 Card 为粒度,记录来自其他 Region 的哪些 Card 可能包含指向当前 Region 的引用。

Remark

初始标记:标记的是 GC Roots 直接指向的活对象。

并发标记:从初始标记出的对象开始,沿着引用链向下追踪,标记所有可达的对象。

Remark:标记的是 在并发期间被遗漏或新产生的活对象。

当引用被覆盖时,把"旧引用指向的对象"放入队列,并在 remark 阶段进行补标

总结:

在 G1 的并发标记过程中,当对象引用被覆盖时,写屏障会将旧引用对象记录到 SATB 队列中。在 remark 阶段,GC 会处理该队列,对其中对象进行重新标记(补标),以保证在标记开始时仍然可达的对象不会被遗漏。标记完成后,未被标记的对象才会在后续阶段被回收。

CMS 在并发标记阶段采用三色标记算法,为了避免黑对象指向白对象导致的漏标问题,引入了增量更新机制。当引用发生变化时,写屏障会将黑对象重新标记为灰色并加入队列,在重新标记阶段对这些对象重新扫描,从而保证当前对象图的正确性。

CMS G1
思想 增量更新 SATB
关注点 新引用(黑→白) 旧引用(被删除)
写屏障记录 黑对象本身 旧引用对象
是否重扫 ✅ 必须重扫 ❌ 不需要
是否允许黑→白 ❌ 不允许 ✅ 允许
是否可能漏标 ❌(修正后)
是否产生浮动垃圾

三色标记法:从 GC Roots 出发,如何一步步遍历整个对象图

在并发情况下如何保证不漏标:就是上面两个方法(增量更新(CMS)、SATB(G1))

CMS:

初始标记(STW)

并发标记

重新标记(STW)

并发清除

G1:

Initial Mark(STW,借助 Young GC)

并发标记

Remark(STW)

Cleanup

Mixed GC(STW,复制存活对象)

JDK 8u20字符串去重

优点:节省大量内存

缺点:略微多占用cpu时间,新生代回收时间略微增加

因为字符串是不可变对象,因此堆中字符串对象值相同,我们可以去重方(指向同一个对象)

  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果他们值一样,让他们引用用一个char[]
  • 注意,与String.intern()不一样
    • String.intern()关注的是字符串对象
    • 而字符串去重关注的是char[]
    • 在JVM内部,使用了不同的字符串表

JDK 8u40并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类

默认开启

JDK 8u60回收巨型对象

  • 一个对象大于region的一半时,称之为巨型对象
  • G1不会对巨型对象进行复制

在 G1 中,巨型对象的回收主要依赖并发标记阶段判断其可达性,一旦不可达,在后续阶段会整体释放对应的 Humongous Region

JDK9并发标记起始时间的调整

  • 并发标记必须在老年代空间不足前完成,否则退化为fullGC
  • JDK9之前需要使用-XX:InitiatingHeapOccupancyPercent
  • JSK9可以动态调整
    • -XX:InitiatingHeapOccupancyPercent来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空挡空间

JDK9更高效的回收

看oracle官方文档

GC调优

常见调优场景

  • 内存
  • 锁竞争
  • cpu占用
  • io

代码问题

查看fullgc前后内存占用,考虑以下几个问题

  • 数据是不是太多
    • resultSet=statement.executeQuery("select * from 大表")
  • 数据表示是否太臃肿
    • 对象图
    • 对象大小Integer 16字节 int 4字节
  • 是否存在内存泄露
    • 软、弱引用
    • 第三方缓存实现

新生代调优

  • 新生代的特点
    • 所有的new操作的内存分配非常廉价
      • TLAB thread-local allocation buffer
    • 死亡对象的回收对象是零
    • 大部分对象用过即死
    • Minor GC的时间远低于Full GC(相差1-2个数量级)

新生代越大越好吗:

如果内存大小确定,新生代太大导致老年代较小,触发fullGC概率大,建议占用堆内存的25%-50%

新生代能容量所有 {并发量*(请求-响应)} 的数据

幸存区大到能保留{当前活跃对象+需要晋升对象}

晋升阈值配饰得当,让长时间存活对象尽快晋升

老年代调优

CMS为例

  • CMS的老年代内存越大越好(避免浮动垃圾导致并发失败)
  • 先尝试不做调优,如果没有full GC那么已经...,否则先尝试调优新生代
  • 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~1/3
    • -XX:CMSInitiatingOccupancyFraction=precent

案例

  • 案例一:Full GC和Minor GC 频繁
    新生代可能太小
  • 案例二:请求高峰期发生Full GC ,并且暂停时间特别长(CMS)
    重新标记耗费时间太长,可以在FullGC之前触发MinorGC
  • 案例三:老年代充裕情况下,发生Full GC(JDK1.7 CMS)
    1.7之前,采用永久代作为方法区,永久代空间不足也会导致full GC
相关推荐
nianniannnn2 小时前
力扣 347. 前 K 个高频元素
c++·算法·leetcode
曹牧2 小时前
JDK 1.6 ,无法通过安全套接字层(SSL/TLS)加密建立数据库安全连接
java·开发语言·ssl
漫随流水2 小时前
c++编程:求阶乘和
数据结构·c++·算法
Frostnova丶2 小时前
LeetCode 2839. 判断通过操作能否让字符串相等 I
算法·leetcode
book123_0_992 小时前
Redis四种模式在Spring Boot框架下的配置
java
会编程的土豆2 小时前
【leetcode hot 100】二叉树3
算法·深度优先·图论
IT成长史2 小时前
Windows D盘安装Docker Desktop全流程(避坑+ECR镜像推送实战)
java·docker
一定要AK3 小时前
java基础
java·开发语言·笔记
ofoxcoding3 小时前
GPT-5.4 API 完全指南:性能实测、成本测算与接入方案(2026)
人工智能·gpt·算法·ai