找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1535

积分

0

好友

195

主题
发表于 2026-2-12 01:09:00 | 查看: 34| 回复: 0

在 JVM 中,程序计数器、虚拟机栈和本地方法栈这三个区域的生命周期与线程紧密绑定,其内存分配和释放的时机是明确的,因此不需要垃圾回收的介入。真正需要动态管理的是 Java 堆和方法区(元空间),它们的内存使用在程序运行期间不断变化,这也是垃圾回收器的主要工作区域。

对象引用

为了更好地管理对象的生命周期和辅助垃圾回收,Java 提供了四种不同强度的引用类型:

  • 强引用:最常见的引用形式,例如 Object obj = new Object();。只要强引用关系存在,垃圾收集器就永远不会回收被引用的对象,即使在内存不足时,JVM 宁愿抛出 OutOfMemoryError 异常也不会去回收它们。
  • 软引用:通过 SoftReference 类实现。被软引用关联的对象,在内存充足时不会被回收;只有在系统即将发生内存溢出异常之前,JVM 才会尝试回收这些对象。它常被用来实现内存敏感的缓存。
  • 弱引用:通过 WeakReference 类实现。无论当前内存是否充足,只要发生垃圾回收,弱引用所指向的对象都会被回收。它适用于构建那种不阻止其键被回收的映射表,例如 WeakHashMap
  • 虚引用:通过 PhantomReference 类实现,并且必须与引用队列 ReferenceQueue 联合使用。虚引用完全无法通过它获取到对象的实例,其唯一目的是在对象被回收时收到一个系统通知,常用于跟踪对象的垃圾回收过程或执行一些资源清理工作。
引用类型 被垃圾回收时机 用途 生存时间
强引用 从来不会 对象的一般状态 JVM 停止运行时终止
软引用 当内存不足时 对象缓存 内存不足时终止
弱引用 正常垃圾回收时 对象缓存 垃圾回收后终止
虚引用 正常垃圾回收时 跟踪对象的垃圾回收 垃圾回收后终止

垃圾对象判定方法

引用计数法

引用计数法为每个对象维护一个引用计数器。每当有一个地方引用它时,计数器加一;当引用失效时,计数器减一。任何时刻计数器为零的对象就是不可能再被使用的,即“垃圾”。

这种方法虽然简单高效,但存在一个致命的缺陷:它无法解决对象之间相互循环引用的问题。例如,对象 A 引用 B,对象 B 也引用 A,除此之外它们再无任何外部引用。此时它们的引用计数都不为零,但实际上这两个对象已经无法被程序访问。因此,主流的 JVM(如 HotSpot)都没有选用引用计数法来管理 内存管理

根搜索算法

根搜索算法,也称为可达性分析算法,是当前主流的对象存活判定方法。它的基本思路是通过一系列称为 “GC Roots” 的根对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链”。如果一个对象到 GC Roots 没有任何引用链相连,则证明此对象是不可用的,可以被判定为垃圾。

哪些对象可以作为 GC Roots 呢?通常包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 本地方法栈中 JNI(即 Native 方法)引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象,比如字符串常量池里的引用。
  • 被同步锁 synchronized 持有的对象。
  • JVM 内部的引用,如基本数据类型对应的 Class 对象、一些常驻的异常对象等。

实际上,即使在可达性分析算法中不可达的对象,也并非“非死不可”。它们会经历两次标记过程:第一次标记后,如果对象覆盖了 finalize() 方法且未被虚拟机调用过,它会被放入一个名为 F-Queue 的队列中。稍后由一个低优先级的 Finalizer 线程去执行队列中对象的 finalize() 方法。如果对象在 finalize() 中成功重新与引用链上的任何一个对象建立关联,那它将在第二次标记时被移出“即将回收”的集合,否则就会被真正回收。

垃圾回收算法

标记-清除算法

标记-清除算法 是最基础和直接的垃圾收集算法,它分为两个阶段:

  • 标记阶段:从 GC Roots 出发,遍历所有可达对象,并对这些存活对象进行标记。
  • 清除阶段:遍历整个堆内存,将未被标记的对象(即不可达对象)统一回收,释放其占用的内存空间。

GC前后内存对象状态对比

优点

  • 实现简单:逻辑清晰,易于理解和实现。
  • 适用于老年代:在对象存活率较高的区域(如老年代),无需频繁移动对象。

缺点

  • 产生内存碎片:清除后会产生大量不连续的内存碎片,可能导致后续无法找到足够大的连续空间来分配大对象,从而提前触发另一次垃圾收集。
  • 效率不稳定:标记和清除两个过程的效率都随对象数量的增加而下降,当堆中对象非常多时,性能开销较大。

JVM 应用:尽管存在碎片问题,标记-清除算法仍是许多现代垃圾收集器的基础。例如,CMS 收集器的核心就采用了(并发)标记-清除算法。

复制算法

复制算法 的出现是为了解决标记-清除算法导致的碎片化问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当正在使用的这块内存用完了,就将还存活着的对象复制到另一块空闲的内存上,然后把已使用过的内存空间一次清理掉。

复制算法内存回收过程

优点

  • 无内存碎片:每次回收后,存活对象被紧凑地排列在内存一端,分配新对象时只需要移动堆顶指针,非常高效。
  • 实现简单,运行高效:只需要遍历一次并复制存活对象,回收过程很快。
  • 适合对象生命周期短的场景:尤其适用于新生代,因为绝大部分对象都是“朝生夕死”,每次回收只需要复制少量的存活对象。

缺点

  • 内存利用率低:在任何时候,总有一半的内存是闲置的,空间代价高昂。
  • 不适合存活率高的区域:如果区域中大多数对象都是存活的,那么复制的开销会非常大。

JVM 应用:复制算法被广泛应用于 JVM 的新生代垃圾回收。为了优化空间利用率,HotSpot 虚拟机将新生代划分为一个较大的 Eden 区和两个较小的 Survivor 区(From 和 To),通常比例为 8:1:1。每次 Minor GC 时,将 Eden 和 From Survivor 中存活的对象复制到 To Survivor 区。

标记-整理算法

标记-整理算法 是针对老年代对象存活率高的特点,在标记-清除算法基础上进行的改进。它也分为三个阶段:

  • 标记阶段:与标记-清除相同,从 GC Roots 出发,遍历并标记所有存活对象。
  • 整理阶段:将所有存活的对象向内存空间的一端进行移动,使它们紧凑地排列在一起。
  • 清除阶段:直接清理掉边界以外的所有内存。

标记-整理算法内存回收过程

优点

  • 无内存碎片:存活对象被整理到连续的内存区域,解决了碎片化问题。
  • 适合存活率高的区域:在老年代中,对象大多长期存活,移动存活对象的成本相对于复制整个区域要低得多。

缺点

  • 整理开销大:移动存活对象并更新所有指向这些对象的引用,是一项需要暂停用户线程(Stop-The-World)的操作,且对象越多,暂停时间可能越长。
  • 实现复杂度高:相比前两种算法,其逻辑相对复杂。

JVM 应用:标记-整理算法是老年代回收器的常用选择,如 Serial Old 和 Parallel Old 收集器都采用了此算法。

分代回收机制

现代商用 JVM 的垃圾收集器大多采用了 分代回收机制。这个设计基于两个经验性的“分代假说”:

  1. 弱分代假说:绝大多数对象都是朝生夕死的,生命周期极短。
  2. 强分代假说:熬过越多次垃圾收集过程的对象,就越难以消亡。

基于这两个假说,JVM 将堆内存划分为新生代老年代,并对不同“代”采用不同的垃圾收集策略。

JVM堆内存分代结构示意图

  • 新生代:存放新创建的对象。区域进一步细分为一个 Eden 区和两个 Survivor 区(From/To)。新对象优先在 Eden 区分配。当 Eden 区满时,会触发一次 Minor GC(或 Young GC),采用复制算法将 Eden 和 From Survivor 中存活的对象复制到 To Survivor 区,并清空 Eden 和 From Survivor。每次在 Survivor 区中熬过一次 Minor GC,对象的年龄就增加一岁。当年龄达到阈值(默认15)或 Survivor 区空间不足时,对象会晋升到老年代。此外,过大的对象(可通过 -XX:PretenureSizeThreshold 参数设置)可能直接进入老年代。
  • 老年代:存放长期存活的对象。当老年代空间不足时(可能由对象晋升、大对象直接进入等触发),会触发 Major GC(或 Full GC),通常采用标记-清除标记-整理算法。Major GC 的速度一般比 Minor GC 慢10倍以上。
  • 方法区/元空间:用于存储已被虚拟机加载的类信息、常量、静态变量等。在 JDK 7 及之前通过“永久代”实现,JDK 8 开始被“元空间”取代。此区域的垃圾收集主要回收废弃的常量和不再使用的类型,发生在 Full GC 时,采用标记-清除算法,触发频率很低。
  • Full GC:指对整个堆(新生代 + 老年代)以及方法区进行的全面垃圾收集。通常由老年代满、方法区满、调用 System.gc() 等原因触发。Full GC 会暂停所有应用线程(Stop-The-World),停顿时间很长,应尽量避免。

分代回收是现代 JVM 大多数垃圾收集器(如 Serial, Parallel, CMS)的基础架构。注意,G1 收集器在逻辑上仍然分代,但在物理内存上是不连续的分区;而 ZGC 和 Shenandoah 则更进一步弱化了分代的概念。

JVM 常用垃圾收集器

下面是 HotSpot 虚拟机(JDK 8 ~ 17 常见版本)中几种主流的垃圾收集器:

垃圾回收器 适用代 并行 / 并发 是否 Stop-The-World 算法基础 优点 缺点 适用场景
Serial 新生代 单线程 是(全程) 复制算法 简单高效,内存开销小 单线程,暂停时间长 客户端模式、小型应用
Serial Old 老年代 单线程 是(全程) 标记-整理 与 Serial 配合使用 单线程,性能差 Client 模式默认;CMS 失败后备
ParNew 新生代 多线程并行 是(仅 STW 阶段) 复制算法 多线程加速 Minor GC 仅能与 CMS 配合 需低延迟 + CMS 老年代
Parallel Scavenge 新生代 多线程并行 复制算法 高吞吐量,自动调优 不关注停顿时间 后台计算、批处理任务
Parallel Old 老年代 多线程并行 标记-整理 高吞吐,与 Parallel Scavenge 搭配 停顿时间较长 吞吐优先型应用
CMS (Concurrent Mark Sweep) 老年代 并发(部分阶段) 初始标记 & 重新标记阶段 STW 标记-清除 低停顿,适合响应敏感应用 CPU 敏感、产生碎片、JDK 14+ 已移除 Web 服务、低延迟要求
G1 (Garbage-First) 整堆(逻辑分代) 并发 + 并行 有(但可预测) 分区 + 复制 + 标记-整理 可预测停顿时间、无碎片、大堆支持 配置复杂,小堆性能不如 CMS/Parallel JDK 9+ 默认,大堆(>4GB)、低延迟
ZGC 整堆 几乎全并发 极短(<1ms) 着色指针 + 读屏障 超低延迟(<10ms),支持 TB 级堆 JDK 15+ 才生产就绪,需较新硬件 超大堆、极致低延迟场景

说明

  • 并行:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
  • 并发:指垃圾收集线程与用户线程同时(或交替)执行,目的是缩短用户线程的停顿时间。

JDK 默认回收器演进

  • JDK 8:Parallel Scavenge (新生代) + Parallel Old (老年代)
  • JDK 9 ~ 13:G1
  • JDK 14+:G1 (ZGC 作为实验性功能或可选)

选择建议

  • 追求吞吐量:Parallel Scavenge + Parallel Old 组合。
  • 追求低延迟:对于通用场景,G1 是很好的选择;对于超大堆和极致低延迟场景,可以考虑 ZGC。
  • 资源受限或嵌入式环境:Serial + Serial Old 组合。

理解这些基础概念和收集器特点,是进行 JVM 性能调优的第一步。如果你想深入探讨某个特定收集器的工作原理或调优案例,欢迎在 云栈社区 与大家交流。




上一篇:嵌入式开发优化:SEGGER工具链 vs ARM GCC在CMake下的代码体积实测
下一篇:电子工程师如何系统学习硬件设计?从理论到实践的完整路线图
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 11:44 , Processed in 0.631211 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表