JVM垃圾回收机制

一、判断对象是否可以回收

  1. 引用计数法

    弊端:A对象->B对象 B对象->A对象 引用计数都为1,不能归0导致无法被垃圾回收

  2. 可达性分析算法

    • Java虚拟机中的垃圾回收器采用可达性分析算法来探索所有存活的对象

    • 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,即没有被根对象直接或间接引用可以进行回收

    • 哪些对象可以作为GC Root

      MAT工具,System Class , Native Stack(native方法) , Thread(活动线程) , Busy Monitor(加锁对象)

  3. 四种引用

    3.1 强引用

    • 只有所有GC Roots 对象都不通过 强引用 引用该对象时,该对象才能被垃圾回收
    • 如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null

    3.2 软引用(SoftReference)

    • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象
    • 可以配合引用队列(ReferenceQueue)来释放软引用自身

    3.3 弱引用(WeakReference)

    • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象(发现即回收)
    • 可以配合引用队列来释放弱引用自身
    • 一个对象被弱引用和这个对象没有引用,gc效果是一样的。它和完全没有引用的区别是,在垃圾回收之前我可以通过get访问到它。 弱引用在threadlocal中有实际应用场景

    3.4 虚引用(PhantomReference)

    • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler 线程调用虚引用相关方法释放直接内存

    3.5 终结器引用(FinalReference)

    • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用的对象暂时没有被回收),再由Finalizer线程(优先级很低)通过终结器引用找到被引用对象并调用它的finalize()方法,第二次GC时才能回收被引用对象。

二、垃圾回收算法

  1. 标记清除(Mark Sweep)
    • 速度快,记录回收对象的起始结束地址就行
    • 容易产生内存碎片,空间不连续
  2. 标记整理(Mark Compact)
    • 速度慢
    • 没有内存碎片
  3. 复制 (Copy)
    • 没有内存碎片
    • 需要占用双倍内存空间。先标记,然后从from复制存活的对象到to中,清理后,to和from在交换回来。

三、分代回收

将堆空间分为,新生代:伊甸园,幸存区From, 幸存区To 和 老年代

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发minor gc,通过可达性分析算法分析标记哪些对象可以被回收,伊甸园和from存活的对象使用copy算法复制到to中,存活的对象年龄+1并且交换from to
  • Minor GC 会引发 stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值,会晋升至老年代,最大寿命是15(对象头占4bit),默认和垃圾回收器有关
  • 当老年代空间不足,先尝试触发Minor GC,如果之后空间仍不足,触发Full GC,STW的时间更长

相关VM参数:

  • 堆初始大小:-Xms
  • 堆最大大小:-Xmx 或 -XX:MaxHeapSize=size
  • 新生代大小:-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size)
  • 幸存区比例(动态):-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
  • 幸存区比例:-XX:SurvivorRatio=ratio(默认值是8 新生代伊甸园占比8:1:1)
  • 晋升阈值:-XX:MaxTenuringThreshold=threshold(默认和垃圾回收器有关)
  • 晋升详情:-XX:+PrintTenuringDistribution
  • GC详情:-XX:+PrintGCDetails -verbose:gc
  • FullGC 前 MinorGC:-XX:+ScavengeBeforeFullGC
  • 默认新生代与老年代的比例为1:2可通过 –XX:NewRatio 配置

大对象:新生代内存不足,老年代内存充足直接晋升到老年代,不会触发垃圾回收。老年代空间不足会触发Minor GC 和Full GC 还是不足则抛出OOM。一个线程的OOM不会导致整个java进程结束。

四、垃圾回收器

  1. 串行

    • 单线程
    • 堆内存较小,CPU核数较少,适合个人电脑
    • -XX:+UseSerialGC=Serial(新生代-复制算法) + SerialOld(老年代-标记-整理算法)
  2. 吞吐量优先

    • 多线程
    • 堆内存较大,多核CPU
    • 让单位时间内,Stop the world的时间最短
    • -XX:+UseParallelGC(复制算法) ~ -XX:+UseParallelOldGC(标记-整理算法)(JDK1.8默认开启)
    • -XX:+UseAdaptiveSizePolicy(动态调整伊甸园和幸存区的比例、堆的大小、阈值)
    • -XX:GCTimeRatio=ratio (调整吞吐量的目标默认值99 1/(1+ratio),调整堆变大,可以设置为19)
    • -XX:MaxGCPauseMillis=ms(默认值200ms,垃圾回收暂停指标,调整堆变小)
    • -XX:ParallelGCThreads=n
  3. 响应时间优先(CMS)

    • 多线程
    • 堆内存较大,多核CPU
    • 垃圾回收时尽可能让单次的Stop the world的时间最短
    • -XX:+UseConcMarkSweepGC(并发-标记-清除算法 可以与用户线程并发执行,工作在老年代) ~ -XX:+UseParNewGC(工作在新生代 复制算法) ~ SerialOld(CMS并发失败时,补救措施,退化成SerialOld)
    • -XX:ParallelGCThreads=n(一般设置为CPU核数) ~ -XX:ConcGCThreads=threads(建议设置成ParallelGCThreads的四分之一)
    • -XX:CMSInitiatingOccupancyFraction=percent(控制垃圾回收时机,CMS垃圾回收的内存占比,未满就进行垃圾回收,预留空间给浮动垃圾)
    • -XX:+CMSScavengeBeforeRemark(在重新标记之前,先对新生代做一次垃圾回收)
    • 当老年代内存碎片过多,导致并发失败,CMS无法正常工作,就会退化为SerialOld,垃圾回收响应时间就会变得很长
    • CMS并发GC不是“Full GC”,只针对老年代
  4. G1垃圾回收器

    定义:Garbage First

    • 2004论文发布
    • 2009 JDK 6u14体验
    • 2012 JDK 7u4官方支持
    • 2017 JDK9 默认

    适用场景

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

    相关JVM参数

    • -XX:+UseG1GC
    • -XX:G1HeapRegionSize=size
    • -XX:MaxGCPauseMillis=time

    4.1 G1垃圾回收阶段

    ​ YoungCollection ——> YoungCollection + ConcurrentMark

    ​ YoungCollection + ConcurrentMark ——> MixedCollection

    ​ MixedCollection ——> YoungCollection

    4.2 YoungCollection

    • 会 Stop the world

    4.3 YoungCollection + CM

    • 在Young GC 时会进行GC Root的初始标记

    • 老年代占用对空间的比例达到阈值时,进行并发标记(不会STW),比例由JVM参数决定

      -XX:InitiatingHeapOccupancyPercent=percent(默认45%)

    4.4 MixedCollection

    ​ 会对E、S、O进行全面垃圾回收

    • 最终标记(Remark)会STW (之前并发标记可能漏掉一些正在运行用户线程产生的新垃圾)
    • 拷贝存活(Evacuation)会STW (不是所有老年代的区都会进行垃圾回收)
    • -XX:MaxGCPauseMillis=ms 老年代优先收集垃圾多的区从而达到目标

    4.5 FullGC

    • SerialGC
      • 新生代内存不足发生的垃圾收集 - minor gc
      • 老年代内存不足发生的垃圾收集 - full gc
    • ParallelGC
      • 新生代内存不足发生的垃圾收集 - minor gc
      • 老年代内存不足发生的垃圾收集 - full gc
    • CMS
      • 新生代内存不足发生的垃圾收集 - minor gc
      • 老年代内存不足 回收速度高于垃圾产生的速度就不是FullGC
    • G1
      • 新生代内存不足发生的垃圾收集 - minor gc
      • 老年代内存不足 回收速度高于垃圾产生的速度就不是FullGC

    4.6 YoungCollection跨代引用

    • 新生代回收的跨代引用(老年代引用新生代)问题
    • 卡表与Remembered Set(新生代)
    • 在引用变更时通过 post-write barrier + dirty card queue (记录脏卡)
    • concurrent refinement threads 更新 Remembered Set
    • 老年代采用卡表技术 每个card 大概512K 如果卡中引用新生代的对象,对应的卡标记为脏卡 好处是GCRoots遍历不用找整个老年代的对象

    4.7 Remark

    • pre-write barrier + satb_mark_queue

    4.8 JDK8u20 字符串去重

    • 优点:节省大量内存
    • 缺点:略微多占用了cpu时间,新生代回收时间略微增加
    • -XX:+UseStringDeduplication
    • 将所有新分配的字符串放入一个队列
    • 当新生代回收时,G1并发检查是否有字符串重复
    • 如果它们值一样,让它们引用同一个char[]
    • 注意,与String.intern() 不一样
      • String.intern()关注的是字符串对象
      • 而字符串去重关注的是char[]
      • 在JVM内部,使用了不同的字符串表

    4.9 JDK8u40 并发标记类卸载

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

    ​ -XX:+ClassUnloadingWithConcurrentMark 默认启用

    4.10 JDK8u50 回收巨型对象

    • 一个对象大于 region 的一半时,称之为巨型对象
    • G1不会对巨型对象进行拷贝
    • 回收时优先被考虑
    • G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉(没有老年代对象引用就可以被回收掉)

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

    • 并发标记必须在对空间占满前完成,否则退化为Full GC
    • JDK9之前需要使用 -XX:InitiatingHeapOccupancyPercent
    • JDK9可以动态调整
      • -XX:InitiatingHeapOccupancyPercent用来设置初始值
      • 进行数据采样并动态调整
      • 总会添加一个安全的空档空间
  5. 垃圾回收调优

    5.1 调优领域

    • 内存
    • 锁竞争
    • CPU占用
    • IO

    5.2 确定目标

    • 低延迟还是高吞吐量,选择合适的回收器
    • CMS,G1,ZGC 低延迟
    • ParallelGC 高吞吐量

    5.3 最快的GC是不发生GC

    • 查看Full GC 前后的内存占用 考虑下面几个问题
      • 数据是不是太多?
      • 数据表示是否太臃肿?
        • 对象图
        • 对象大小
      • 是否存在内存泄漏?

    5.4 新生代调优

    • 新生代的特点
      • 所有的new操作的内存分配非常廉价
        • TLAB thread-local allocation buffer
      • 死亡对象的回收代价是零
      • 大部分对象用过即死
      • Minor GC的时间远远低于Full GC
      • 新生代大到能够容纳所有【并发量*(请求-响应)】的数据
      • 幸存区大到能保留【当前活跃对象+需要晋升的对象】
      • 晋升阈值配置得当,让长时间存活对象尽快晋升
        • -XX:MaxTenuringThreshold=threshold
        • -XX:+PrintTenuringDistribution

    5.5 老年代调优

    以CMS为例

    • CMS的老年代内存越大越好(可避免浮动垃圾引起的并发失败)
    • 先尝试不做调优,如果没有Full GC 那么已经很OK了,否则先尝试调优新生代
    • 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4 ~1/3
      • -XX:CMSInitiatingOccupancyFraction=percent
posted @   fjhnb  阅读(73)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示