读书《深入理解Java虚拟机》垃圾回收

72 阅读6分钟
graph TD
  垃圾回收 --> 垃圾收集区域
  垃圾回收 --> 判断对象已死
  垃圾回收 --> 垃圾收集算法
  垃圾回收 --> 垃圾收集类型
  垃圾回收 --> 垃圾收集器

垃圾收集区域

JVM 的运行时数据区中,程序计数器、虚拟机栈和本地方法栈随线程生命周期变化,内存分配与回收具有确定性,无需特殊垃圾回收处理。比如虚拟机栈中的栈帧大小通常在编译期就可以确定,随着方法调用的执行,栈帧按需入栈和出栈,内存的分配和回收是自动并且高效的。

Java 堆和方法区的内存需求在运行时动态决定,垃圾收集器主要针对这两个区域进行工作, 回收不再使用的对象和无用的类加载信息等,以管理内存资源并防止内存泄漏。

判定对象可收集

graph LR
  root["GC Root"]
  判定对象可回收 --> 引用计数法
  判定对象可回收 --> 可达性分析
  可达性分析 --> root
  可达性分析 --> 对象引用
  • 引用计数法:存在循环引用问题,使用它该方法的性能开销和解决缺陷的复杂度代价太大。
  • 可达性分析法(Java使用):从一系列可作为 GC Root 的根节点开始搜索,凡是在引用链上的对象,就不会被垃圾回收。

可以作为 GC Root 的对象

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  2. 方法区中类静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象。

Java对象的引用

  • 强引用:直接赋值或 new 创建的对象引用,只要强引用存在,对象绝不会被回收;
  • 软引用:使用 SoftReference 类实现,代表有用但非必要的对象。当内存不足时,会在发生OOM 前尝试回收这些对象;
  • 弱引用:使用 WeakReference 类实现,其引用的对象在下次回收时必定会被回收,无论内存是否充足;
  • 虚引用:使用 PhantomReference 类实现,设置虚引用不影响对象的生命周期,对象在任何时候都可能被回收,其存在的目的是为了在对象被回收时提供一种通知机制。

方法区的废弃常量和无用的类

在 Java 虚拟机规范中不要求虚拟机在方法区实现垃圾收集,且性价比很低,效率也低。

  • 判断废弃常量:当常量池中的某个字面量不再被任何对象引用,即没有存活的引用指向它时,会在内存回收过程中被清理出常量池。

  • 判断无用的类的 3 个条件:

    • 该类所有实例都已经被回收,也就是 Java 堆中不存在该类的任何实例;
    • 加载该类的 ClassLoader 已经被回收;
    • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

graph LR
  垃圾收集算法 --> 标记-清除算法
  垃圾收集算法 --> 复制算法
  垃圾收集算法 --> 标记-整理算法

标记-清除算法

标记所有需要回收的对象,再统一回收所有被标记的对象。是最基础的收集算法。不足之处:

  1. 标记和清除两个过程效率都不高;
  2. 因为垃圾对象是不连续的,清除后会造成空间碎片,在后续需要分配大对象时,如无法找到足够大的连续空间将再次触发垃圾收集,频繁触发导致效率更低。

复制算法

为了解决效率问题,将内存分为两块区域,每次只使用其中一块,当一块用完时将存活对象移到另一块,将剩下的垃圾对象清理掉。不足之处是每次只能用一块区域,导致空间利用率降低。

标记-整理算法

标记所有需要回收的对象,将存活对象移到一端,然后直接清理掉端边界以外的内存。解决了空间碎片和空间浪费的问题,但每次触发时都需要暂停所有用户线程,很影响用户体验,特别是对老年代进行垃圾回收时,耗时几乎是年轻代的10倍多。

垃圾收集类型

  • Minor/Young GC:发生在年轻代的垃圾收集,频率高,速度快;
  • Major/Old GC:发生在老年代的垃圾收集,频率低,速度一般会比年轻代 GC 慢 10 倍以上;
  • Full GC:发生在整个堆和方法区的垃圾收集,速度更慢,会导致应用出现长时间的 STW 停顿,频繁触发堆系统性能影响较大。

内存分配

对象的内存分配首先考虑的是新生代的 Eden 区。启用本地线程分配缓冲(TLAB)时,对象会优先在对应线程的 TLAB 区域内分配。当Eden区无法容纳新创建的对象,特别是当对象大小超过了剩余空间,或是对象大小超过 -XX:PretenureSizeThreshold 指定的阈值时,虚拟机会触发一次 Minor GC。

Minor GC 过程中,Eden 区和其中一个 Survivor 区(例如 S0)中存活的对象会被复制到另一个 Survivor 区(例如 S1)。每次 Young GC 后,对象年龄加 1。经过多次 Minor GC(默认是 15 次),如果对象仍存活且年龄达到阈值,则会被晋升至老年代。而在 Survivor 区之间的对象复制过程中,始终遵循 from 区的对象复制到当前空闲的 to 区的原则。

垃圾收集器

HotSpotVM的垃圾收集器.jpg

  1. Serial

    • 单线程,使用复制算法,响应速度优先,停顿时间短
    • 适用于单 cpu 环境下的 client 模式
  2. ParNew

    • 多线程版Serial,使用复制算法
    • 使用多核并行回收,提高吞吐量,多线程并发时可能会增加系统暂停时间
    • 一般与CMS配合使用与服务器端环境,适用于需要高吞吐量但能接受一定程度停顿的应用
  3. Serial Old

    • 串行,使用标记压缩算法,响应速度优先
    • 单线程回收,处理大内存时效率低
    • 单 CPU 环境下的 Client 模式,CMS 的后备预案。
  4. Parallel Scavenge

    • 多线程并行,使用复制算法,吞吐量优先
    • 可自定义 GC 策略,通过参数调节达到优化吞吐量的目的,会带来较长的停顿时间
    • 适用于在后台运算而不需要太多交互的任务。
  5. Parallel Old

    • 多线程并行,使用标记压缩算法,吞吐量优先
    • 提供比 Serial Old 更高的并行回收能力,提升老年代的吞吐量,停顿时间较长
    • 与 PS 搭配,适用于在后台运算而不需要太多交互的任务。
  6. CMS

    • 并发,使用标记清除算法,响应速度优先
    • 并发标记阶段不会停止用户程序,减少 STW
    • 但内存碎片化严重,容易导致频繁的 Full GC。
    • 适用于对响应时间敏感的应用,如互联网服务
  7. G1

    • 并发,使用并发标记+压缩算法,将整个堆划分为多个 Region,并具有预测性停顿时间模型
    • 全局并发标记和局部并发整理,实现更细粒度的内存管理,目标是尽量平衡吞吐量和停顿时间
    • 但复杂度相对较高,对机器配置有一定要求,内存越大优势越明显
    • 适用于大型服务器应用,堆内存超过6GB以上,需要低停顿时间切内存资源充足的情况