当前位置: 首页 > news >正文

Java垃圾收集器与内存分配策略深度解析

在 Java 与 C++ 的世界里,内存动态分配与垃圾收集技术仿佛筑起了一道高墙。墙外的人渴望进入,享受自动内存管理的便利;而墙内的人却试图突破,追求更高的性能与控制力。今天,就让我们深入探讨 Java 的垃圾收集器与内存分配策略,一窥其背后的奥秘。

3.1 概述

垃圾收集(Garbage Collection,简称 GC)并非 Java 的专属产物。早在 1960 年,Lisp 语言就率先引入了内存动态分配与垃圾收集技术。当时,Lisp 的设计者 John McCarthy 就明确了垃圾收集需要解决的三大问题:哪些内存需要回收?何时回收?如何回收?

经过半个多世纪的发展,内存动态分配与回收技术已相当成熟。然而,当系统面临内存溢出、内存泄漏问题,或者垃圾收集成为性能瓶颈时,开发者仍需深入了解这些“自动化”技术的细节,以便进行有效的监控与优化。

在 Java 中,程序计数器、虚拟机栈、本地方法栈等区域的内存分配与回收具有确定性,随线程的生命周期而自然回收。而 Java 堆和方法区则截然不同,它们的内存分配与回收具有高度不确定性,这也是垃圾收集器重点关注的区域。

3.2 对象已死?

在 Java 堆中,垃圾收集器的核心任务是区分存活对象与死亡对象。那么,如何判断一个对象是否存活呢?

3.2.1 引用计数算法

引用计数算法是一种简单直观的判断方法。它为每个对象设置一个引用计数器,每当有引用指向该对象时,计数器加一;当引用失效时,计数器减一。当计数器为零时,表示该对象不再被使用,可以被回收。

然而,引用计数算法存在明显的缺陷。它无法解决对象之间的循环引用问题。例如,两个对象互相引用,即使它们已经无法被外部访问,它们的引用计数也永远不会为零,从而导致内存泄漏。

3.2.2 可达性分析算法

当前主流的编程语言(如 Java、C# 等)都采用可达性分析算法来判断对象是否存活。该算法从一组称为“GC Roots”的根对象出发,沿着引用链向下搜索。如果一个对象到 GC Roots 之间没有任何引用链相连,则该对象被认为是不可达的,可以被回收。

在 Java 中,以下对象可以作为 GC Roots:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。

  • 方法区中类静态属性引用的对象。

  • 方法区中常量引用的对象。

  • 本地方法栈中 JNI 引用的对象。

  • Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象、常驻异常对象等。

  • 所有被同步锁持有的对象。

  • 反映 Java 虚拟机内部情况的 JMX Bean、JVMTI 中注册的回调、本地代码缓存等。

3.2.3 再谈引用

从 JDK 1.2 开始,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用和虚引用四种,分别对应不同的内存回收策略。

  • 强引用:最传统的引用形式,如 Object obj = new Object()。只要强引用存在,垃圾收集器永远不会回收被引用的对象。

  • 软引用:用于描述一些有用但非必须的对象。当内存不足时,垃圾收集器会回收这些对象。适用于缓存场景。

  • 弱引用:比软引用更弱,被弱引用关联的对象只会在下一次垃圾收集时被回收。适用于监听器等场景。

  • 虚引用:最弱的引用形式,无法通过虚引用来获取对象实例。主要用于在对象被回收时接收通知。

3.2.4 生存还是死亡?

即使对象在可达性分析中被判定为不可达,也不一定会被立即回收。如果对象覆盖了 finalize() 方法,并且该方法尚未被虚拟机调用过,那么该对象会被放入 F-Queue 队列中,由 Finalizer 线程执行其 finalize() 方法。如果对象在 finalize() 方法中重新与引用链上的某个对象建立关联,那么它将被移出回收队列;否则,它将被回收。

需要注意的是,finalize() 方法的执行代价高昂,且存在不确定性。它并不等同于 C++ 中的析构函数,也不建议用于关闭外部资源等清理工作。现代 Java 开发中,应尽量避免使用 finalize() 方法。

3.2.5 回收方法区

方法区(如 HotSpot 虚拟机中的元空间或永久代)也可能进行垃圾收集,主要回收废弃的常量和不再使用的类。回收废弃常量相对简单,而判定一个类是否可以被回收则需要满足以下三个条件:

  • 该类的所有实例都已被回收。

  • 加载该类的类加载器已被回收。

  • 该类对应的 java.lang.Class 对象没有被引用。

HotSpot 虚拟机提供了 -XX:+PrintClassHistogram 参数用于查看类的加载情况,以及 -XX:+TraceClassLoading-XX:+TraceClassUnloading 参数用于跟踪类的加载与卸载。

3.3 垃圾收集算法

垃圾收集算法主要分为两大类:引用计数式垃圾收集和追踪式垃圾收集。由于引用计数式垃圾收集在主流 Java 虚拟机中未被采用,本文主要介绍追踪式垃圾收集算法及其相关理论。

3.3.1 分代收集理论

分代收集理论是现代垃圾收集器的核心设计思想。它基于以下两个分代假说:

  • 弱分代假说:绝大多数对象都是朝生夕灭的。

  • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

根据这两个假说,Java 堆被划分为新生代和老年代。新生代主要存储新创建的对象,这些对象通常具有较短的生命周期;老年代则存储经过多次垃圾收集后仍然存活的对象。

分代收集理论还引入了跨代引用假说:跨代引用相对于同代引用来说只占极少数。基于这一假说,垃圾收集器在进行新生代收集时,只需关注老年代中存在跨代引用的区域,而无需扫描整个老年代。这一机制通过“记忆集”(Remembered Set)实现。

3.3.2 标记-清除算法

标记-清除算法是最基础的垃圾收集算法,由 John McCarthy 在 1960 年提出。它分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,然后统一回收这些对象。

标记-清除算法的主要缺点是执行效率不稳定,且容易产生内存碎片。当堆中对象数量较多时,标记和清除过程的开销会显著增加。此外,标记-清除算法可能导致内存碎片化,影响后续大对象的分配。

3.3.3 标记-复制算法

标记-复制算法通过将内存划分为两块区域,每次只使用其中一块,当内存用完时,将存活对象复制到另一块区域,然后清理已使用过的内存。这种算法的优点是实现简单,运行高效,但缺点是会浪费一半的内存空间。

现代 Java 虚拟机(如 HotSpot)通常采用一种改进的标记-复制算法,称为“Appel 式回收”。它将新生代划分为一个较大的 Eden 空间和两个较小的 Survivor 空间。每次分配内存时,只使用 Eden 和其中一个 Survivor 空间。垃圾收集时,将 Eden 和 Survivor 中的存活对象复制到另一个 Survivor 空间,然后清理 Eden 和已使用的 Survivor 空间。

3.3.4 标记-整理算法

标记-整理算法主要用于老年代的垃圾收集。它在标记阶段与标记-清除算法相同,但在后续步骤中,将所有存活对象向内存空间一端移动,然后清理边界以外的内存。这种算法的优点是可以避免内存碎片化问题,但缺点是移动对象和更新引用的开销较大。

3.4 HotSpot 的算法细节实现

3.4.1 根节点枚举

根节点枚举是垃圾收集过程中的关键步骤。HotSpot 虚拟机通过 OopMap 数据结构快速准确地完成 GC Roots 枚举。OopMap 记录了对象内每个偏移量上数据的类型,以及栈和寄存器中引用的位置。这样,垃圾收集器在扫描时可以直接获取这些信息,而无需从头开始查找。

3.4.2 安全点

安全点是 HotSpot 虚拟机中用于暂停用户线程的机制。由于生成 OopMap 的成本较高,HotSpot 并非为每条指令都生成 OopMap,而是在特定位置记录这些信息,这些位置被称为安全点。当垃圾收集发生时,用户线程会运行到最近的安全点并暂停。

HotSpot 使用主动式中断机制,通过设置标志位让线程在安全点上主动中断挂起。轮询标志的操作被优化为一条汇编指令,以提高效率。

3.4.3 安全区域

安全区域用于解决线程处于 Sleep 或 Blocked 状态时无法响应垃圾收集的问题。线程进入安全区域时会标识自己,垃圾收集器在回收时可以忽略这些线程。线程离开安全区域时,会检查垃圾收集是否完成,若未完成则等待。

3.4.4 记忆集与卡表

记忆集用于记录从非收集区域指向收集区域的指针集合,以避免扫描整个老年代。卡表是记忆集的一种常见实现形式,通过字节数组记录卡页的状态。HotSpot 虚拟机中,卡表的每个元素对应一个卡页,卡页大小为 512 字节。当卡页内存在跨代引用时,对应的卡表元素会被标记为“脏”。

3.4.5 写屏障

写屏障用于维护卡表的状态。HotSpot 虚拟机通过写屏障技术,在引用类型字段赋值时更新卡表。写屏障分为写前屏障和写后屏障,HotSpot 主要使用写后屏障。

3.4.6 并发的可达性分析

并发的可达性分析是现代垃圾收集器的关键技术之一。它允许垃圾收集器与用户线程并发执行,从而减少停顿时间。HotSpot 虚拟机通过三色标记算法和写屏障技术实现并发可达性分析。

三色标记算法将对象分为白色、灰色和黑色三种状态,分别表示尚未访问、已访问但引用未扫描、已完全扫描。并发执行时,可能会出现“对象消失”问题,即原本存活的对象被误标为已消亡。为解决这一问题,HotSpot 虚拟机采用了增量更新和原始快照两种解决方案。

相关文章:

  • php数据库连接
  • Linux常见基础命令
  • Leetcode - 双周赛155
  • 超级好用的​​参数化3D CAD 建模​​图形库 (CadQuery库介绍)
  • 数字孪生的浪潮:从虚拟镜像到现实世界的 IT 变革
  • Rust 学习笔记:编程练习(一)
  • 计算机基础—(九道题)
  • 24体育NBA足球直播M28模板体育赛事直播源码
  • Rmarkdown输出为pdf的方法与问题解决
  • 从代码学习机器学习 - UMAP降维算法 scikit-learn版
  • Android 消息队列之MQTT的使用(二):会话+消息过期机制,设备远程控制,批量控制实现
  • JavaScript高级进阶(四)
  • Crusader Kings III 王国风云 3(十字军之王 3) [DLC 解锁] [Steam] [Windows SteamOS macOS]
  • Python(14)推导式
  • PCI/PXI 总线的可编程电阻卡
  • JVM模型、GC、OOM定位
  • leetcode 876. 链表的中间结点
  • 云上玩转DeepSeek系列之六:DeepSeek云端加速版发布,具备超高推理性能
  • SpringBoot实现接口防刷的5种高效方案详解
  • 安装qt4.8.7
  • 阿里千问3系列发布并开源:称成本大幅下降,性能超越DeepSeek-R1
  • 西班牙葡萄牙突发全国大停电,欧洲近年来最严重停电事故何以酿成
  • “85后”潘欢欢已任河南中豫融资担保有限公司总经理
  • 外交部:对伊朗拉贾伊港口爆炸事件遇难者表示深切哀悼
  • 四川在浙江公开招募200名退休教师,赴川支教帮扶
  • 央行副行长:增强外汇市场韧性,坚决对市场顺周期行为进行纠偏