JVM -- 垃圾收集

本文详细探讨了JVM的垃圾收集机制,包括强软弱虚引用的概念、GC类型(Minor、Major/Full GC)及其影响,STW(Stop the World)的必要性,TLAB的作用,逃逸分析的优化策略,以及各种垃圾收集器(Serial、ParNew、Parallel、CMS、G1)的工作原理、优缺点。此外,还介绍了记忆集和卡表在跨代引用中的作用,以及如何通过GC日志分析JVM性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

几个名词

强软弱须

在这里插入图片描述

强引用

  • 平时写的代码如Test obj = new Test();这种引用关系就是强引用
  • 就算会OOM也不会回收
    软引用
  • 内存不足的情况下才会回收
  • 如果发生了gc但是内存充足,依然不会回收
    弱引用
  • 只有发生gc就会回收
    虚引用
  • 形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
  • 虚引用主要用来跟踪对象被垃圾回收的活动。
  • 虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。
  • 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
  • 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。
  • 程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动
GC类型

分代就是优化GC性能,把那些朝生夕死(新生代)的与链接对象(老不死的老年代)分开做垃圾收集

Minor GC(年轻代GC)
  1. Eden区满的时候会触发MinorGC,
  2. Survivor不会触发GC,Survivor随着Eden区MinorGC时一同被清理
  3. 大多数对象都是朝生夕死MinorGC出现的频率相对较高,STW时间相对较短
Major GC/Full GC (老年代GC)
  1. 出现Major GC,经常会伴随Minor GC,老年代空间不足的时候会先尝试触发MinorGC,如果还不够再触发MajorGC
  2. MajorGC比MinorGC慢10倍以上,会导致STW时间更长
  3. MajorGC后空间还不足就会OOM
Full GC
  1. 调用System.gc(),系统建议执行FullGC,但是不一定执行
  2. 老年代空间不足
  3. 方法区空间不足
  4. MinorGC后进入老年代的平均占用内存大小大于老年代可用空间
  5. 从Eden区+s0复制到s1区时,s1区大小存不下该对象,则把该对象转存到老年代,且老年代的可用内存也小于该对象
STW(Stop the world)
GC停顿
  • 在执行GC时,jvm会停止其他用户线程的操作,防止产生新垃圾,会导致用户线程出现暂停,GC之后程序计数器和字节码执行引擎会继续从暂停处继续执行用户线程,对用户来说出现了短暂的卡顿,体验收到影响
为什么需要STW
  • GC是从线程的gc root出发,对这个gc root链路可达性分析,
  • 如果没有stw,一次GC从gc root出发的时候用户线程依然在运行,发现当前方法的gc root有应用,不是垃圾,但是稍后用户线程执行完成了,又变成了垃圾,导致这种情况的垃圾没法回收,这样会导致GC效果差,性能低下,所以出现STW,即GC停顿
TLAB(Thread Local Allocation Buffer))

在这里插入图片描述

  1. 堆内存共享,多线程情况下存在线程安全问题,加锁又会导致性能下降,所有未每个线程分配一个私有的缓存区域,包含在Eden空间内
  2. 多线程情况下,使用TLAB就可以避免线程安全问题,同时提升内存分配的吞吐量,因此被称为快速分配策略
  3. 使用-XX:UserTLAB 设置是否开启TLAB空间,默认开启TLAB且只占有Eden区的1%,但JVM将TLAB作为内存分配的首选,可以通过-XX:TLABWasteTargetPercent设置占比
  4. 如果TLAB分配失败,JVM就会尝试通过加锁机制确保数据的原子性,从而直接在Eden区分配内存
逃逸分析(Escape Analysis)

经过逃逸分析后发现,如果一个对象并没有逃逸出方法,就可能被优化为栈上分配,能使用局部变量的就不用定义为成员变量,-XX : +DoEscapeAnalysis 开启逃逸分析(默认开启)

  1. new 的对象在方法外被引用,逃逸
  2. return 出对象可能会在方法外使用,逃逸
  3. 成员变量赋值的可能发生方法外使用,逃逸
  4. 引用成员变量的值,逃逸
逃逸优化
  1. 栈上分配 : 将对分配转换为栈分配内存,逃逸分析没有逃逸的,方法结束,栈弹出就结束了,不需要对象回收,没有GC
  2. 同步省略 : 一个对象只能从一个线程被访问到,那么对这个对象的操作可以不再考虑同步
  3. 分离对象或标量替换 : 经过逃逸分析,发现一个对象不会被外界访问,经过JIT优化,就会把这个对象拆解为若干个成员变量来替换,标量替换为栈上分配提供更好的支持
    a. 标量(scalar)是指无法在分割更小的数据的数据,如java中的原始数据类型就是标量;
    b. 聚合量(aggreagate)可分解的数据

垃圾收集器

标记阶段标记的是存活对象,回收未被标记的对象。
在这里插入图片描述

Serial收集器
  • 串行垃圾收集器,即GC线程与用户线程先后运行,即GC时需要STW(暂停所有用户线程),直至GC结束才恢复用户线程的运行
  • 专注于收集年轻代,底层是复制算法
  • 相关参数:-XX:+UseSerialGC
    在这里插入图片描述
ParNew收集器
  • Serial收集器的多线程版本。GC时开启多线程收集
  • 唯一能与CMS收集器搭配使用的新生代收集器。
  • 相关参数:
    ○ -XX:+UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器
    ○ -XX:+UseParNewGC:强制指定使用ParNew
    ○ -XX:ParallelGCThreads:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同
    在这里插入图片描述
Parallel收集器
  • 关注吞吐量的收集器
    ○ 吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)
  • 相关参数:
    • -XX:MaxGCPauseMillis:是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。不过大家不要异想天开地认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:
      ■ 系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,
      ■ 原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
    • -XX:GCTimeRatio:一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率。
      ■ 如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1 /(1+19)),
      ■ 默认值为99,就是允许最大1%(即1 /(1+99))的垃圾收集时间。
    • -XX:+UseAdaptiveSizePolicy:一个开关参数,当这个参数打开之后,
      ■ 不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,
      ■ 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)

在这里插入图片描述

Serial Old收集器
  • Serial收集器的老年代版本。基于标记-整理算法实现,
  • 有两个用途:
    ○ 与Serial收集器、Parallel收集器搭配使用
    ○ 作为CMS收集器的后备方案
Parallel Old收集器
  • Parallel收集器的老年代版本。基于标记-整理算法实现。
CMS收集器
  • 聚焦低延迟。基于标记-清除算法实现(会内存碎片,Serial Old收集器兜底)。
  • CMS收集器是并发收集器,即在运行阶段用户线程依然在运行,会产生对象,所以CMS收集器不能等到老年代满了才触发,而是要提前触发,这个阈值是92%。
    ○ 这个阈值可以通过参数-XX:CMSInitiatingOccupancyFraction 设置
  • 相关参数:
    • -XX:+UseConcMarkSweepGC:手动开启CMS收集器
    • -XX:+CMSIncrementalMode:设置为增量模式
    • -XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
    • -XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
    • -XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收
    • -XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
    • -XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理
      在这里插入图片描述
CMS收集器工作分四个步骤
  1. 初始标记
    a. 会STW。只标记GC Roots直接关联的对象。
  2. 并发标记
    a. 不会STW。GC线程与用户线程并发运行。
    b. 会沿着GC Roots直接关联的对象链遍历整个对象图。需要的时间较长,但因为是与用户线程并发运行的,除了能感知到CPU飙升,不会出现卡顿现象。
  3. 重新标记
    a. 会STW。
    b. CMS垃圾收集器通过 写屏障+增量更新 记录了并发标记阶段新建立的引用关系,重新标记就是去遍历这个记录。
  4. 并发清除
    a. GC线程与用户线程并发运行,清理未被标记到的对象
    b. 默认启动的回收线程数 = (处理器核心数 + 3) / 4
CMS缺点
  • 运行期间会与用户线程抢夺CPU资源。当然,这是所有并发收集器的缺点
  • 无法处理浮动垃圾(标记结束后创建的对象)
  • 内存碎片
G1收集器
  • G1收集器与之前的所有收集器都不一样,它将堆分成了一个一个Region,
  • 这些Region用的时候才被赋予角色:Eden、from、to、humongous。
  • 一个region只能是一个角色,不存在一个region既是Eden又是from。
  • 整体上使用标记整理算法,局部会使用复制算法
  • g1垃圾收集器对应的堆区有2048个region,每个region默认大小是2m
  • 每个region的大小可通过参数-XX:G1HeapRegionSize设置,取值范围是2-32M。
  • 一个对象的大小超过region的一半则被认定为大对象,放到old区标记为H,如果对象大于region大小,则会用N个连续的region来存储。
  • 相关参数:
    • -XX:G1HeapRegionSize:设置region的大小
    • -XX:MaxGCPauseMillis:设置GC回收时允许的最大停顿时间(默认200ms)
    • -XX:+UseG1GC:开启g1
    • -XX:ConcGCThreads:设置并发标记、并发整理的gc线程数
    • -XX:ParallelGCThreads:STW期间并行执行的gc线程数

在这里插入图片描述

G1垃圾回收原理
  • 回收某个region的价值大小 = 回收获得的空间大小 + 回收所需时间
  • G1收集器会维护一个优先级列表,每个region按价值大小排序存放在这个优先级列表中。
  • 收集时优先收集价值更大的region,这就是G1名字的由来。
    在这里插入图片描述
G1收集器工作分四个步骤
  1. 初始标记
    a. 会STW。
    b. 做了两件事:
    ⅰ. 修改TAMS的值,TAMS以上的值为新创建的对象,默认标记为存活对象,即多标
    ⅱ. 标记GC Roots能直接关联到的对象
  2. root region scanning: 根据rset , 扫描整个表的 old的region区,
  3. 并发标记
    a. 耗时较长。GC线程与用户线程并发运行。
    b. 从GC roots能直接关联到的对象开始遍历整个对象图
  4. 最终标记
    a. 遍历 写屏障+SATB 记录下的旧的引用对象图
  5. 筛选回收
    a. 更新region的统计数据,对各个region的回收价值进行计算并排序,
    b. 然后根据用户设置的期望暂停时间的期望值生成回收集。
    c. 然后开始执行清除操作。将旧的region中的存活对象移动到新的Region中,清理这个旧的region,重置rset。
    d. 这个阶段需要STW。
G1缺点
  • 需要10%-20%的内存来存储G1收集器运行需要的数据,如cset、rset、卡表等
    ○ rset: 每个region都有一个叫rset的区域,代表其他region引用当前region对象的记录
    ○ cset: 本次GC需要清理的region集合记录
  • 运行期间会与用户线程抢夺CPU资源。这是所有并发收集器的缺点

记忆集-卡表

跨代引用
  • 新生代对老年代的引用
    ○ 发生gc后,新生代的对象被回收了,程序还能正常运行
  • 老年代对新生代的引用
    ○ gc的时候,新生代的对象被清理了
    ○ 老年代的对象去访问新生代的对象,会报错
    ○ 用记忆集与卡表解决这个问题
卡表
  • 记忆集是理论, 卡表是具体实现
  • 卡表中的卡页数量与region的数量是相同的, 有2048个卡页
  • 一个卡页有512B , 卡页中的每个字节对应region中的4KB = 2M / 512
    ○ region大小变化,卡页数不会变
    ○ 卡页中的1B映射的region的大小会变8kb = 4M/512
  • 这个4KB只要有一个对象存在对新生代的引用,这个标记就是1
    ○ 如果已经被标记了,就不标记了(优化)
    在这里插入图片描述

GC日志

  • 查看默认收集器
    java -XX:+PrintFlagsFinal -version | grep GC
  • 相关参数:
    • -XX:+PrintGC 输出GC日志
    • -XX:+PrintGCDetails 输出GC的详细日志
    • -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
    • -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2021-05-04T21:53:59.234+0800)
    • -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
    • -Xloggc:…/logs/gc.log 日志文件的输出路径
日志内容
  • gc类型:GC、Full GC
  • gc原因:Metadata GC Threshold、Last ditch collection……
  • gc前内存数据
  • gc后内存数据
  • 花费的时间:用户态、内核态、实际用时
[GC (Metadata GC Threshold) [PSYoungGen: 6398K->1133K(46592K)] 6398K->1141K(153088K), 0.0371218 secs] [Times: user=0.03 sys=0.01, real=0.04 secs] 

[Full GC (Metadata GC Threshold) [PSYoungGen: 1133K->0K(46592K)] [ParOldGen: 8K->1013K(76800K)] 1141K->1013K(123392K), [Metaspace: 4061K->4061K(1056768K)], 0.0815840 secs] [Times: user=0.13 sys=0.00, real=0.08 secs] 

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值