内存区域划分——垃圾回收

一、为什么要有垃圾回收?​

在编程语言发展初期,开发者需要手动管理内存:申请内存空间、使用完毕后手动释放。但这种方式存在两大致命问题:​

  1. 内存泄漏:忘记释放不再使用的内存,导致内存占用持续增长,最终程序崩溃;
  1. 野指针访问:释放内存后未清空指针,后续误操作该指针会引发程序异常(如崩溃、数据错乱)。

垃圾回收(Garbage Collection,GC)的核心目标是 自动识别并释放 "无用对象" 占用的内存,解放开发者的内存管理负担,同时避免内存泄漏和野指针问题,保障程序稳定性和内存使用效率。简单说:GC 是内存的 "自动清洁工",让开发者专注业务逻辑而非底层内存操作。​

二、垃圾回收主要回收哪个内存区域?​

以 Java、Python 等主流语言的内存模型为例,GC 的回收重点是 堆内存(Heap),次要关注方法区(元空间 / 永久代),栈内存(虚拟机栈、本地方法栈)通常不参与 GC。​

  • 堆内存:所有对象实例(如new Object())和数组都存储在堆中,是内存占用最大、对象生命周期最复杂的区域 ------ 有的对象仅使用一次就无用,有的则会持续存活到程序结束。GC 的核心工作就是扫描堆内存,筛选出 "死亡对象" 并回收其空间。
  • 方法区:存储类信息、常量、静态变量等,部分场景下也会产生垃圾(如动态卸载类、常量池过期数据),因此部分 GC(如 Java 的 G1、ZGC)会兼顾方法区的回收。
  • 栈内存:栈帧随方法调用创建、方法结束销毁,生命周期与线程执行流程强绑定,无需 GC 介入,自动出栈释放。

结论:堆内存是垃圾回收的 "主战场"。​

三、标记的过程:如何识别 "垃圾对象"?​

GC 的第一步是 "标记"------ 区分 "存活对象" 和 "垃圾对象",核心逻辑是:可达的对象为存活,不可达的为垃圾。主流标记算法有两种:​

  1. 引用计数法(简单但有缺陷)​
  • 原理:给每个对象维护一个 "引用计数器",被引用时计数器 + 1,引用失效时 - 1;当计数器为 0 时,标记为垃圾。
  • 优点:实现简单、标记效率高,实时性强(无需暂停程序)。
  • 缺陷:无法解决 "循环引用" 问题(如 A 引用 B,B 引用 A,两者均无其他外部引用,计数器仍为 1,无法被标记回收),因此主流语言(Java、Python)已弃用。
  1. 可达性分析算法(主流方案)​
  • 原理:以 "GC Roots" 为起点,遍历对象引用链;能被遍历到的对象为 "可达对象"(存活),遍历不到的为 "不可达对象"(垃圾)。
  • GC Roots 的常见类型:
  • 虚拟机栈中局部变量表引用的对象(如方法内定义的变量);
  • 方法区中静态变量、常量引用的对象;
  • 本地方法栈中 Native 方法引用的对象;
  • 活跃线程引用的对象。
  • 优点:能解决循环引用问题,是 Java、C# 等语言的核心标记算法。
  • 注意:标记过程需要 "暂停用户线程"(Stop The World,STW),避免遍历过程中引用关系变化导致标记错误(后续 GC 优化的核心方向之一是减少 STW 时间)。

四、回收的过程:如何释放垃圾对象的内存?​

标记完成后,第二步是 "回收"------ 释放垃圾对象占用的内存,同时整理内存碎片(避免碎片过多导致无法分配大对象)。主流回收算法有三种:​

  1. 标记 - 清除算法(基础但低效)​
  • 流程:① 标记所有垃圾对象;② 直接清除垃圾对象,释放内存。
  • 优点:实现简单,无需移动对象。
  • 缺陷:
  • 产生大量内存碎片(释放的内存块大小不一,分散在堆中);
  • 清除效率低(需遍历整个堆查找垃圾对象)。
  1. 标记 - 复制算法(高效无碎片)​
  • 流程:① 将堆内存划分为两个大小相等的区域(From 区和 To 区);② 标记存活对象,将其复制到 To 区;③ 清空 From 区,交换 From 和 To 区的角色(下次回收时 From 区变为 To 区,反之)。
  • 优点:无内存碎片,回收效率高(只需复制存活对象,存活对象少的时候效率极高)。
  • 缺陷:堆内存利用率低(仅一半区域可用),适合 "存活对象少、垃圾多" 的场景(如新生代)。
  • 应用:Java 的 Serial GC、ParNew GC 的新生代回收均采用此算法。
  1. 标记 - 整理算法(适合老年代)​
  • 流程:① 标记所有存活对象;② 将存活对象向堆的一端移动,集中排列;③ 清除堆另一端的所有垃圾对象,释放连续内存。
  • 优点:无内存碎片,堆内存利用率 100%。
  • 缺陷:需要移动对象,会增加 STW 时间(移动对象后需更新所有引用该对象的指针)。
  • 应用:适合 "存活对象多、垃圾少" 的场景(如老年代),Java 的 CMS GC 老年代、G1 GC 均会用到。

补充:分代收集算法(实际应用的组合方案)​

上述三种算法均有优缺点,因此主流 GC(如 Java 的 HotSpot VM)采用 "分代收集"------ 根据对象生命周期长短,将堆内存分为新生代和老年代,分别使用合适的算法:​

  • 新生代:对象生命周期短(创建后很快成为垃圾),存活对象少 → 用 "标记 - 复制算法"(高效无碎片);
  • 老年代:对象生命周期长(长期存活),存活对象多 → 用 "标记 - 清除算法" 或 "标记 - 整理算法"(避免频繁复制的开销)。

五、垃圾回收器的典型实现​

垃圾回收器是 GC 算法的具体工程实现,不同语言、不同虚拟机有不同的实现方案,以下是主流语言的典型 GC:​

  1. Java HotSpot VM 的经典垃圾回收器​

Java 的 GC 生态最完善,不同回收器针对不同场景优化:​

  • Serial GC(串行 GC):单线程执行 GC,STW 时间长,适合单 CPU、小型应用(如嵌入式设备),JDK8 默认新生代 GC。
  • ParNew GC(并行新生代 GC):Serial GC 的多线程版本,新生代用标记 - 复制,老年代需配合 CMS,适合多 CPU 服务器,缩短 STW 时间。
  • CMS GC(并发标记清除):目标是 "低延迟",老年代采用并发标记 + 清除(仅初始标记和重新标记阶段 STW),适合对响应时间敏感的应用(如 Web 服务);缺陷是产生碎片、占用 CPU 资源。
  • G1 GC(区域化分代式):JDK9 及以上默认 GC,将堆分为多个大小相等的 Region(区域),兼顾新生代和老年代回收,采用 "标记 - 整理"+"复制" 混合算法,可预测 STW 时间,适合大堆内存(如 10GB+)。
  • ZGC/Shenandoah GC(低延迟 GC):专为超大堆、超低延迟设计(STW 时间毫秒级),支持 TB 级内存,适合金融、电商等核心业务。
  1. Python 的 GC 实现​

Python 采用 "引用计数为主,分代回收为辅":​

  • 日常垃圾回收依赖引用计数,循环引用通过 "分代回收" 解决(将对象分为 3 代,越久未回收的对象扫描频率越低)。
  • 优点:兼顾简单性和实用性,避免纯引用计数的循环引用问题。
  1. Go 的 GC 实现​

Go 的 GC 是 "并发标记清除 + 三色标记":​

  • 无需分代,采用三色标记(白色:未标记;灰色:待遍历;黑色:已标记),配合 "写屏障" 避免并发标记时的引用变化,STW 时间极短(Go 1.19 后可控制在微秒级),适合高并发场景。

六、总结​

垃圾回收的核心逻辑可概括为:通过 "标记" 识别无用对象,通过 "回收" 释放内存,通过 "分代 / 并发" 优化性能。其本质是在 "自动化内存管理" 和 "程序性能" 之间寻找平衡 ------ 优秀的 GC 既能解放开发者,又能最小化对程序运行的影响。​

不同语言 / 虚拟机的 GC 实现各有侧重(如 Java 追求通用性,Go 追求低延迟,Python 追求简单性),实际开发中需根据应用场景选择合适的 GC 方案(如大内存应用优先 G1/ZGC,低延迟应用优先 CMS/Shenandoah)。

相关推荐
金銀銅鐵1 小时前
[Java] JDK 9 新变化之 Convenience Factory Methods for Collections
java·后端
用户7406696136252 小时前
入门并理解Java模块化系统(JPMS)
java
金銀銅鐵2 小时前
[Java] 用 Swing 生成一个最大公约数计算器
java·后端
小安同学iter2 小时前
SQL50+Hot100系列(11.7)
java·算法·leetcode·hot100·sql50
_dindong2 小时前
笔试强训:Week-4
数据结构·c++·笔记·学习·算法·哈希算法·散列表
yivifu3 小时前
JavaScript Selection API详解
java·前端·javascript
zizisuo3 小时前
16000+字!Java集合笔记
java·开发语言
星释3 小时前
Rust 练习册 :Nucleotide Codons与生物信息学
开发语言·算法·rust
BeingACoder3 小时前
【SAA】SpringAI Alibaba学习笔记(二):提示词Prompt
java·人工智能·spring boot·笔记·prompt·saa·springai