【JVM学习笔记】第三篇 JVM垃圾回收

本文详细介绍了Java的垃圾回收机制,包括引用计数法和可达性分析算法,以及各种引用类型。讲解了常见的垃圾回收算法,如标记清除、标记整理和复制算法,并分析了它们的优缺点。此外,还阐述了分代垃圾回收策略,特别是新生代和老年代的管理。针对不同的场景,讨论了串行、吞吐量优先和响应时间优先的垃圾回收器,以及G1垃圾收集器的特点和参数调整。最后,文章提到了垃圾回收调优的目标和方法,包括内存、锁竞争、CPU占用和IO的优化,并提供了相关调优策略和注意事项。

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

3.1 如何判断对象可以垃圾回收

两种算法:引用计数法和可达性分析算法

  1. 引用计数法(Python解释器早期使用的垃圾回收策略)
    描述:只要一个对象被其他变量所引用,就让它的计数加1;如果某个变量不再引用它,就让它的计数减1。当它的引用数量为0时,记为垃圾并等待回收

    弊端:循环引用问题。也就是A对象引用B对象,B对象引用A对象,引用永不为0,永远无法回收

  2. 可达性分析算法(Java虚拟机采用的垃圾回收策略)

    描述:首先要确定一系列根对象(肯定不能被当成垃圾回收的对象,即GC Root对象),也就是先对堆内存中的所有对象进行一遍扫描,之后查看对象是否被根对象直接或间接引用(引用链),如果是则该对象不能被回收;如果不是则该对象可作为垃圾被回收

    GC Root对象:通过工具Memory Analyzer(专业内存分析工具)进行分析得知,GC Root对象主要有:

    • System Class(java.lang.Class)
    • Native Stack(本地方法栈)
    • Thread(活动线程)
    • Busy Monitor(锁对象)

    概括起来GCRoot对象就是:

    • 虚拟机栈中的局部变量表引用的对象
    • 方法区中类静态属性引用和常量引用对象
    • 本地方法栈中引用的对象

JVM中的几种引用:

  1. 强引用

    特点:GC时永远不会被回收

    描述:强引用是指创建一个对象并把这个对象赋给一个引用变量。比如:

    Object object = new Object();
    String str = "hello";
    

    强引用有引用变量指向时永远不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。强引用也是 导致内存泄露的主要原因

  2. 软引用

    特点:内存不足时(自动触发GC)会被回收

    描述:如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。在java中,用java.lang.ref.SoftReference类来表示。可以配合引用队列来释放软引用自身

  3. 弱引用

    特点:无论内存是否充足,只要进行GC都会被回收

    描述:弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以配合引用队列来释放弱引用自身

  4. 虚引用
    特点:如同虚设,和没有引用没什么区别

    描述:虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。使用时必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放内存。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收

  5. 终结器引用

    特点:无需手动编码,其内部配合引用队列使用

    描述:它用于实现对象的 finalize() 方法,也可以称为终结器引用。在GC时, 终结器引用入队。由Finalizer线程通过终结器引用找到被引用对象并调用它的 finalize() 方法,第二次GC时才能回收被引用对象

3.2 垃圾回收算法

常见回收算法:

  1. 标记清除(Mark Sweep)

    • 先标记:沿着GCRoot引用链扫描,把没有引用到的对象作为待回收的垃圾标记起来
    • 后清除:释放标记对象的内存,也就是将对象起止内存编号存入空闲地址列表,以便再次内存分配
    • 优势:垃圾回收速度快
    • 弊端:空间不连续,容易产生内存碎片
  2. 标记整理(Mark Compact)

    • 先标记:同上
    • 后整理:将存活着的对象整理在一起,覆盖掉之前标记的内存,使内存空间更紧凑
    • 优势:不会产生内存碎片
    • 弊端:工作量大,速度慢
  3. 复制(Copy)

    • 先分区:将内存区划成两块大小相等的区域(FROM区域和TO区域)

    • 再标记:同上

    • 复制并交换:将FROM区域的存活对象整理在一起复制到TO区域,并将两个区域交换(即FROM区域变成TO区域,TO区域变成FROM区域)

    • 优势:不会产生内存碎片

    • 弊端:会占用双倍的内存空间

小结:在JVM中以上三种回收算法其实都会涉及到,由JVM根据内存占用的情况进行决策具体使用哪些算法

3.3 分代垃圾回收

分代垃圾回收策略将堆内存划分为两个区域,即新生代老年代。其中新生代又由伊甸园、幸存区FROM和幸存区TO构成

划分理由:Java中有的对象需要长时间使用,所以就放在老年代;而那些用完之后就可以丢弃的对象就放在新生代。老年代的垃圾回收很久才发生一次,新生代的垃圾回收则比较频繁。这样一来,可以根据对象生命周期的不同特点实施不同的垃圾回收策略

分配与回收的具体过程:

  • 对象首先分配在伊甸园区域,如果对象所占空间大于伊甸园的空间小于老年代的空间,则直接将该对象晋升到老年代
  • 新生代空间不足时触发minor gc,伊甸园和from存活的对象使用copy算法复制到to中,存活的对象年龄加1,其余对象被回收,之后交换from和to
  • minor gc会引发stop the world,也就是会暂停所有其他用户线程等垃圾回收结束,用户线程才恢复运行。但minor gc速度很快,停顿时间较短
  • 当对象寿命超过阈值(最大15)时,会晋升到老年代
  • 当老年代空间不足时,会先触发一次minor gc,如果之后空间仍不足,则触发full gc(老年代存活的对象更多,且老年代不再采用复制算法作为垃圾回收算法,所以停顿时间更长)
  • 如果空间仍不足,则抛出OutOfMemoryException

附加知识:堆内存相关VM参数

含义参数
堆初始大小-Xms
堆最大大小-Xmx或-XX:MaxHeapSize=size
新生代大小-Xmn或(-XX:NewSize=size + -XX:MaxNewSize=size)
幸存区比例(动态)-XX:InitalSurvivorRatio=ratio和
-XX:+UseAdaptiveSizePolicy
幸存区比例-XX:InitalSurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=threshold
晋升详情-XX:+PrintTenuringDistribution
GC详情-XX:+PrintGCDetails -verbose:gc
FullGC前MinorGC-XX:+ScavengeBeforeFullGC

注意:一个线程内的堆内存溢出并不会使整个程序崩溃,只会抛出一个OOM异常

3.4 垃圾回收器

  1. 串行

    • 底层是一个单线程的垃圾回收器

    • 应用场景是堆内存较小,个人电脑(CPU个数较少)

    • 开启串行垃圾回收器的VM参数:

      -XX:+UserSerialGC=Serical+SerialOld

  2. 吞吐量优先(JDK1.8默认)

    • 多线程
    • 应用场景是堆内存较大,多核CPU
    • 注重让单位时间内STW的时间最短,即清理更彻底
    • 开启该回收器的参数为:
      -XX:+UseParallelGC
    • 优化参数:
      • -XX:ParallelGCThreads=n指定垃圾回收器的线程数

      • -XX:+UseAdaptiveSizePolicy动态调整新生代内伊甸园的大小

      • -XX:GCTimeRatio=ratio垃圾回收时间在总时间中的占比默认1%,开启此参数VM将会动态调整堆的大小来适应这个比值

      • -XX:MaxGCPauseMillis=ms设置单次最长垃圾回收时间,默认不超过200ms

  3. 响应时间优先

    • 多线程

    • 应用场景是堆内存较大,多核CPU

    • 注重尽可能让单次STW的时间最短,即清理更快

    • 是工作在老年代的垃圾回收器,能与用户线程并发进行,STW的时间短。但并发失败时会退化成一个单线程的串行垃圾回收器

    • 由于采用Mark Sweep算法进行垃圾清除,当产生的内存碎片过多无法再分配对象时,会导致并发失败(最大的问题)

    • 开启该回收器的VM参数为:-XX:UseConcMarkSweepGC

    • 优化参数:

      • -XX:ConcGCThreads=n指定垃圾回收器的线程数,一般为总CPU数 $\div$4

      • -XX:CMSInitiatingOccupancyFraction=percent指定为浮动垃圾预留的空间占比,超过这个比值将触发该垃圾回收器。这个值越小越早触发垃圾回收器

      • -XX:+CMSScavengeBeforeRemark在重新标记(第二次STW)之前先执行一次清理,避免重复标记,缩短STW的时间

  4. G1(JDK9及以上默认):

    • 新一代垃圾收集器,取代了CMS垃圾回收器
    • 适用场景:
      • 同时注重吞吐量和低延迟,默认的暂停目标是200ms
      • 适合超大堆内存,会将堆划分为多个大小相等的Region,每个区域都有可能成为伊甸园(Eden)、From区域、To区域和老年代
      • 整体上是标记+整理算法,两个区域之间是复制算法
    • 相关VM参数:
      • -XX:+UseG1GC启用G1回收器
      • -XX:G1HeapRegionSize=size设置划分堆内存的Region大小
      • -XX:MaxGCPauseMillis=time设置最大的垃圾回收时间,默认200ms
      • -XX:InitiatingHeapOccupancyPercent=percent设置老年代占用堆空间比例的阈值,当达到这一阈值时触发并发标记(不STW),默认45%

3.5 垃圾回收调优

  • 预备知识
    • 掌握GC相关VM参数,会基本的空间调整
    • 掌握相关工具
    • 调优跟应用、环境有关,没有放之四海而皆准的法则
  1. 调优领域

    1. 内存
    2. 锁竞争
    3. cpu占用
    4. io
  2. 确定目标

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

    注意:不同的垃圾回收器有不同的应用场景,低延迟适用于互联网项目(类似分时系统),高吞吐量适用于科学计算(类似批处理系统)

  3. 最快的GC是不发生GC
    查看FullGC前后的内存占用,要考虑的问题主要有:

    1. 数据是不是太多?
      • resultSet = statement.executeQuery("select * from 大表"); 这种情况就需要考虑加limit或者使用分页查询,否则当多个线程同时访问大批量数据时就会造成OOM异常
    2. 数据表示是不是太臃肿?
      • 拒绝对象图,用到哪个查哪个
      • 要了解对象大小对内存的影响:
        • 最简单的Object对象都要占16字节的内存
        • 包装类型Integer占24字节内存,而int类型只占4字节,所以能用基本类型就尽可能少用包装类型
    3. 是否存在内存泄漏?
      • 反面教材:不断向静态的Map对象中添加对象
      • 解决方法:使用软引用或弱引用,或使用第三方缓存(如redis)
  4. 新生代调优

    • 所有的new操作的内存分配都非常廉价
      • TLAB(thread-local allocation buffer)线程局部分配缓冲区
    • 死亡对象的回收代价是0
    • 大部分对象用过即死
    • Minor GC的时间远小于Full GC
    • 调优策略:
      1. 新生代并非越大越好,Oracle官方建议新生代大小应该占整个堆空间的25% ~ 50%。太小则频繁触发Minor GC,太大又会导致频繁触发Full GC
      2. 新生代能容纳所有【并发量 * (请求与响应)】的数据
      3. 幸存区大到能保留【当前活跃的对象 + 需要晋升对象】
      4. 晋升阈值配置得当,让长时间存活对象尽快晋升,而不应该长期保留在幸存区
        两个常用VM指令:
        • -XX:MaxTenuringThreshold=threshold 调整最大晋升阈值,推荐适当调小
        • -XX:+PrintTenuringDistribution 打印晋升对象的详细信息
  5. 老年代调优
    以CMS为例:

    • CMS的老年代内存越大越好
    • 先尝试不调优,如果没有Full GC那么已经很OK了,应该先调优新生代
    • 观察发生Full GC时老年代内存的占用,将老年代内存预设调大1/4 ~ 1/3
      • -XX:CMSInitiatingOccupancyFraction=percent指定为浮动垃圾预留的空间占比,超过这个比值将触发该垃圾回收器。一般设为70%~80%最佳

资料来源:b站视频《java中级程序员必会的教程,解密JVM【黑马程序员】》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值