Java垃圾回收机制(GC)与内存模型

简介: 本文主要讲述JVM的内存模型和基本调优机制。

💡 摘要:你是否曾遭遇过OutOfMemoryError的困扰?是否好奇JVM如何自动管理内存?是否想了解不同垃圾回收器的选择策略?

别担心,垃圾回收是Java最重要的特性之一,它让开发者从繁琐的内存管理中解放出来。

本文将带你从JVM内存模型讲起,理解堆、栈、方法区等内存区域的作用。然后深入垃圾回收算法,学习标记-清除、复制、标记-整理等核心原理。

接着探索各种垃圾回收器的特点和适用场景,从Serial到G1再到ZGC。最后通过实战案例展示内存问题排查和性能优化。从内存分配到垃圾回收,从算法原理到调优实践,让你全面掌握Java内存管理的精髓。文末附常见内存问题和解法,助你写出高性能的Java应用。

一、JVM内存模型:运行时数据区

1. 内存区域概述

JVM内存结构图

text

JVM内存区域

├── 线程私有区域

│      ├── 程序计数器 (PC Register)

│      ├── Java虚拟机栈 (Java Stack)

│      └── 本地方法栈 (Native Method Stack)

├── 线程共享区域  

│      ├── 堆 (Heap)           // 对象实例存储

│      │      ├── 新生代 (Young Generation)

│      │      │      ├── Eden区

│      │      │      ├── Survivor0区

│      │      │      └── Survivor1区

│      │      └── 老年代 (Old Generation)

│      │

│      └── 方法区 (Method Area) // 类信息、常量、静态变量

│              └── 运行时常量池

└── 直接内存 (Direct Memory) // NIO使用的堆外内存

2. 各区域详细说明

堆(Heap):对象实例存储区域

java

// 所有对象实例和数组都在堆上分配内存

public class MemoryExample {

   public static void main(String[] args) {

       // 在堆上分配对象内存

       Object obj1 = new Object();  // 对象在Eden区分配

       Object obj2 = new Object();  // 另一个对象

       

       // 数组也在堆上分配

       int[] array = new int[1024]; // 数组对象

       

       // 大对象可能直接进入老年代

       byte[] largeObject = new byte[10 * 1024 * 1024]; // 10MB大对象

   }

}

虚拟机栈(Java Stack):线程私有的方法调用栈

java

public class StackExample {

   public static void main(String[] args) {

       int localVar = 42; // 局部变量在栈帧中

       String text = "hello"; // 引用在栈中,对象在堆中

       

       method1(); // 方法调用创建新的栈帧

   }

   

   static void method1() {

       double value = 3.14; // 局部变量

       method2(); // 嵌套调用

   }

   

   static void method2() {

       // 每个方法调用对应一个栈帧

       // 栈帧包含局部变量表、操作数栈、动态链接、方法返回地址

   }

}

方法区(Method Area):类元数据存储

java

public class ClassMetaExample {

   // 类信息存储在方法区

   private static final String CONSTANT = "constant"; // 常量在方法区

   private static int staticVar = 100; // 静态变量在方法区

   

   public void method() {

       // 方法字节码存储在方法区

   }

}

二、垃圾回收算法:理论基础

1. 引用计数法(Reference Counting)

基本原理

java

// 简化的引用计数示例

public class ReferenceCounting {

   private int count = 0;

   private Object data;

   

   public void setData(Object data) {

       if (this.data != null) {

           this.data.decrementCount(); // 减少旧引用计数

       }

       this.data = data;

       if (this.data != null) {

           this.data.incrementCount(); // 增加新引用计数

       }

   }

   

   // 问题:循环引用无法回收

}


class A { B ref; }

class B { A ref; }


// 创建循环引用

A a = new A();

B b = new B();

a.ref = b;

b.ref = a;


// 即使a和b不再使用,引用计数也不为0,无法回收

2. 可达性分析算法(Root Searching)

GC Roots对象包括

  • 虚拟机栈中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

java

public class ReachabilityExample {

   private static Object staticObj; // GC Root

   private final Object finalObj = new Object(); // GC Root

   

   public void method() {

       Object localObj = new Object(); // 局部变量,GC Root

       

       // 这些对象都是可达的

       Object obj1 = new Object();

       Object obj2 = new Object();

       

       obj1 = null; // obj1不可达,可回收

       System.gc(); // 建议GC,但不保证立即执行

   }

}

3. 标记-清除算法(Mark-Sweep)

过程描述

java

// 标记阶段:从GC Roots开始遍历,标记所有可达对象

标记对象(Object obj) {

   if (obj != null && !obj.isMarked()) {

       obj.mark = true;

       for (Object ref : obj.references) {

           标记对象(ref); // 递归标记

       }

   }

}


// 清除阶段:遍历堆,回收未标记对象

清除() {

   for (Object obj : heap) {

       if (!obj.isMarked()) {

           free(obj); // 释放内存

       } else {

           obj.unmark(); // 清除标记位

       }

   }

}

优缺点

  • ✅ 简单直接
  • 🔴 内存碎片问题
  • 🔴 效率较低

4. 复制算法(Copying)

新生代回收

java

// 新生代通常采用复制算法

public class CopyingAlgorithm {

   // 新生代分为Eden、Survivor0、Survivor1

   private Space eden = new Space("Eden", 80); // 80%

   private Space survivor0 = new Space("Survivor0", 10); // 10%

   private Space survivor1 = new Space("Survivor1", 10); // 10%

   

   private Space from = survivor0;

   private Space to = survivor1;

   

   public void minorGC() {

       // 1. 标记Eden和From Survivor中的存活对象

       List<Object> liveObjects = markLiveObjects(eden, from);

       

       // 2. 将存活对象复制到To Survivor

       copyToSurvivor(liveObjects, to);

       

       // 3. 清空Eden和From Survivor

       eden.clear();

       from.clear();

       

       // 4. 交换From和To

       Space temp = from;

       from = to;

       to = temp;

       

       // 5. 对象年龄增加,达到阈值(默认15)则晋升老年代

       for (Object obj : liveObjects) {

           obj.age++;

           if (obj.age > MAX_AGE) {

               promoteToOldGen(obj);

           }

       }

   }

}

5. 标记-整理算法(Mark-Compact)

老年代回收

java

public class MarkCompactAlgorithm {

   public void fullGC() {

       // 1. 标记阶段:标记所有存活对象

       markPhase();

       

       // 2. 整理阶段:移动存活对象,消除碎片

       compactPhase();

       

       // 3. 更新引用指针

       updateReferences();

   }

   

   private void compactPhase() {

       int freePointer = 0;

       for (Object obj : heap) {

           if (obj.isMarked()) {

               // 移动对象到空闲位置

               moveObject(obj, freePointer);

               freePointer += obj.size;

           }

       }

   }

}

三、垃圾回收器:实战选择

1. 串行收集器(Serial Collector)

特点:单线程,Stop-The-World

bash

# 启用串行收集器

java -XX:+UseSerialGC -Xms512m -Xmx512m -jar application.jar


# 适用场景:客户端应用,小堆内存

2. 并行收集器(Parallel Collector)

吞吐量优先

bash

# 启用并行收集器

java -XX:+UseParallelGC -XX:+UseParallelOldGC -Xms2g -Xmx2g -jar application.jar


# 调优参数

-XX:ParallelGCThreads=4      # GC线程数

-XX:MaxGCPauseMillis=200     # 最大暂停时间目标

-XX:GCTimeRatio=99           # GC时间与应用时间比率

3. CMS收集器(Concurrent Mark Sweep)

低延迟收集器

bash

# 启用CMS收集器

java -XX:+UseConcMarkSweepGC -Xms4g -Xmx4g -jar application.jar


# CMS调优参数

-XX:CMSInitiatingOccupancyFraction=75  # 老年代使用率触发阈值

-XX:+UseCMSInitiatingOccupancyOnly     # 仅使用占用率作为触发条件

-XX:+CMSScavengeBeforeRemark           # Remark前先做Young GC

CMS执行过程

  1. 初始标记(Initial Mark):Stop-The-World
  2. 并发标记(Concurrent Mark)
  3. 重新标记(Remark):Stop-The-World
  4. 并发清除(Concurrent Sweep)

4. G1收集器(Garbage First)

分区收集器

bash

# 启用G1收集器

java -XX:+UseG1GC -Xms8g -Xmx8g -jar application.jar


# G1调优参数

-XX:MaxGCPauseMillis=200        # 目标暂停时间

-XX:InitiatingHeapOccupancyPercent=45  # 堆使用率触发阈值

-XX:G1HeapRegionSize=4m         # 分区大小

G1内存布局

text

G1堆结构:

├── 多个等大小区域(Region)

│     ├── Eden Regions

│     ├── Survivor Regions  

│     ├── Old Regions

│     └── Humongous Regions(大对象)

└── 收集集合(CSet):本次GC要处理的区域

5. ZGC和Shenandoah

新一代低延迟收集器

bash

# 启用ZGC(JDK 15+)

java -XX:+UseZGC -Xms16g -Xmx16g -jar application.jar


# ZGC调优参数

-XX:ConcGCThreads=4         # 并发GC线程数

-XX:ParallelGCThreads=8     # 并行GC线程数


# 启用Shenandoah

java -XX:+UseShenandoahGC -Xms16g -Xmx16g -jar application.jar

四、内存分配与回收策略

1. 对象分配规则

对象分配过程

java

public class ObjectAllocation {

   public void createObjects() {

       // 1. 优先在Eden区分配

       Object obj1 = new Object(); // 在Eden分配

       

       // 2. 大对象直接进入老年代

       byte[] largeArray = new byte[1024 * 1024]; // 1MB以上可能直接进入老年代

       

       // 3. 长期存活的对象进入老年代

       for (int i = 0; i < 20; i++) {

           Object obj = new Object();

           // 经历多次GC后,存活对象会晋升老年代

       }

       

       // 4. 动态对象年龄判定

       // 如果Survivor中相同年龄对象大小超过Survivor空间一半,年龄≥该年龄的对象直接晋升

   }

}

2. 空间分配担保

GC前后的空间保证

java

public class SpaceGuarantee {

   public void beforeGC() {

       // Minor GC前检查:

       // 老年代最大可用空间 > 新生代所有对象总大小?

       // 是:确保Minor GC安全

       // 否:检查HandlePromotionFailure设置

       //  允许担保失败:继续Minor GC

       //  不允许担保失败:直接Full GC

   }

   

   public void afterGC() {

       // Minor GC后检查:

       // 如果Survivor空间不足以存放存活对象

       // 需要通过分配担保机制进入老年代

   }

}

五、实战:内存问题排查

1. 内存泄漏检测

常见内存泄漏场景

java

// 1. 静态集合引用

public class MemoryLeakExample {

   private static final List<Object> STATIC_LIST = new ArrayList<>();

   

   public void addToStaticList(Object obj) {

       STATIC_LIST.add(obj); // 对象永远无法回收

   }

}


// 2. 监听器未移除

public class ListenerLeak {

   private List<Listener> listeners = new ArrayList<>();

   

   public void addListener(Listener listener) {

       listeners.add(listener);

       // 但忘记提供remove方法

   }

}


// 3. 内部类引用外部类

public class Outer {

   private byte[] data = new byte[1024 * 1024];

   

   class Inner {

       // 隐式持有Outer.this引用

       void method() {

           System.out.println(data.length); // 访问外部类数据

       }

   }

   

   public Inner getInner() {

       return new Inner(); // 即使Outer不再使用,由于Inner引用也无法GC

   }

}

2. 内存分析工具使用

MAT内存分析

bash

# 生成堆转储文件

jmap -dump:live,format=b,file=heapdump.hprof <pid>


# 或者运行时输出堆转储

java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof -jar app.jar


# 使用MAT分析hprof文件

# 1. 查找最大对象

# 2. 分析支配树

# 3. 检查GC Roots引用链

jstat监控GC

bash

# 监控GC情况

jstat -gc <pid> 1s 10


# 输出说明:

# S0C/S1C: Survivor区容量

# S0U/S1U: Survivor区使用量

# EC/EU: Eden区容量/使用量  

# OC/OU: 老年代容量/使用量

# YGC/YGCT: Young GC次数/时间

# FGC/FGCT: Full GC次数/时间

六、性能优化实践

1. 堆大小调优

合理的堆配置

bash

# 生产环境推荐配置

java -Xms4g -Xmx4g           # 堆大小固定,避免动态调整

-XX:NewRatio=2               # 新生代:老年代=1:2

-XX:SurvivorRatio=8          # Eden:Survivor=8:1:1

-XX:MaxTenuringThreshold=15  # 晋升年龄阈值


# 根据应用特点调整

-XX:+UseAdaptiveSizePolicy   # 开启自适应大小策略(默认开启)

2. GC调优策略

根据应用类型选择GC

bash

# 吞吐量优先应用(后台计算)

-XX:+UseParallelGC -XX:+UseParallelOldGC

-XX:ParallelGCThreads=8

-XX:GCTimeRatio=99


# 低延迟应用(Web服务)

-XX:+UseG1GC

-XX:MaxGCPauseMillis=200

-XX:InitiatingHeapOccupancyPercent=45


# 大堆应用(16G+)

-XX:+UseZGC

-XX:ConcGCThreads=4

七、常见问题与解决方案

1. OutOfMemoryError排查

各种OOM及处理

java

// 1. Java heap space

// 原因:堆内存不足

// 解决:增大堆大小,检查内存泄漏


// 2. GC overhead limit exceeded

// 原因:GC效率太低,98%时间在做GC

// 解决:检查代码中的内存问题,调整GC参数


// 3. PermGen space / Metaspace

// 原因:类元数据空间不足

// 解决:增大Metaspace大小,检查类加载器泄漏


// 4. Unable to create new native thread

// 原因:线程数超出系统限制

// 解决:减少线程数,调整系统参数

2. GC性能问题

常见GC问题现象

  • 🔴 Full GC频繁:老年代空间不足,内存泄漏
  • 🔴 Young GC时间过长:Eden区过大,存活对象过多
  • 🔴 GC停顿时间过长:选择合适的低延迟收集器
  • 🔴 吞吐量下降:GC线程占用过多CPU时间

八、总结:GC最佳实践

1. 监控与调优流程

GC优化步骤

  1. 监控:使用jstat、GC日志监控GC情况
  2. 分析:识别问题(延迟高、吞吐量低、OOM等)
  3. 调优:调整堆大小、代比例、收集器参数
  4. 验证:对比调优前后效果
  5. 重复:持续监控和优化

2. 参数配置建议

生产环境推荐

bash

# 基本配置

-Xms4g -Xmx4g                  # 固定堆大小

-XX:+HeapDumpOnOutOfMemoryError # OOM时输出堆转储

-XX:HeapDumpPath=./logs         # 堆转储文件路径


# GC日志配置

-Xlog:gc*=info:file=gc.log:time,uptime,level,tags:filecount=10,filesize=10M


# 根据应用选择收集器

# 中小应用:G1GC

# 大堆低延迟:ZGC

# 吞吐量优先:ParallelGC

九、面试高频问题

❓1. JVM内存结构是怎样的?

:分为线程私有的程序计数器、虚拟机栈、本地方法栈,线程共享的堆、方法区。堆分为新生代和老年代,新生代又分为Eden和Survivor区。

❓2. 对象什么时候会进入老年代?

:年龄达到阈值(默认15)、大对象直接分配、Survivor区相同年龄对象超过一半空间。

❓3. G1和CMS的区别是什么?

:G1采用分区算法,可预测停顿时间,适合大堆;CMS采用标记-清除,追求低延迟,但容易产生碎片。

❓4. 如何排查内存泄漏?

:使用jmap生成堆转储,用MAT分析支配树和GC Roots引用链,查找无法回收的对象。

❓5. ZGC有什么特点?

:新一代低延迟收集器,停顿时间不超过10ms,支持TB级堆内存,基于Region和指针染色技术。

相关文章
|
2月前
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
208 3
|
3月前
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
20天前
|
Java 大数据 Go
从混沌到秩序:Java共享内存模型如何通过显式约束驯服并发?
并发编程旨在混乱中建立秩序。本文对比Java共享内存模型与Golang消息传递模型,剖析显式同步与隐式因果的哲学差异,揭示happens-before等机制如何保障内存可见性与数据一致性,展现两大范式的深层分野。(238字)
38 4
|
24天前
|
存储 缓存 Java
【深入浅出】揭秘Java内存模型(JMM):并发编程的基石
本文深入解析Java内存模型(JMM),揭示synchronized与volatile的底层原理,剖析主内存与工作内存、可见性、有序性等核心概念,助你理解并发编程三大难题及Happens-Before、内存屏障等解决方案,掌握多线程编程基石。
|
2月前
|
缓存 监控 Kubernetes
Java虚拟机内存溢出(Java Heap Space)问题处理方案
综上所述, 解决Java Heap Space溢出需从多角度综合施策; 包括但不限于配置调整、代码审查与优化以及系统设计层面改进; 同样也不能忽视运行期监控与预警设置之重要性; 及早发现潜在风险点并采取相应补救手段至关重要.
403 17
|
6月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
206 0
|
3月前
|
监控 Kubernetes Java
最新技术栈驱动的 Java 绿色计算与性能优化实操指南涵盖内存优化与能效提升实战技巧
本文介绍了基于Java 24+技术栈的绿色计算与性能优化实操指南。主要内容包括:1)JVM调优,如分代ZGC配置和结构化并发优化;2)代码级优化,包括向量API加速数据处理和零拷贝I/O;3)容器化环境优化,如K8s资源匹配和节能模式配置;4)监控分析工具使用。通过实践表明,这些优化能显著提升性能(响应时间降低40-60%)同时降低资源消耗(内存减少30-50%,CPU降低20-40%)和能耗(服务器功耗减少15-35%)。建议采用渐进式优化策略。
159 1
|
4月前
|
SQL 缓存 安全
深度理解 Java 内存模型:从并发基石到实践应用
本文深入解析 Java 内存模型(JMM),涵盖其在并发编程中的核心作用与实践应用。内容包括 JMM 解决的可见性、原子性和有序性问题,线程与内存的交互机制,volatile、synchronized 和 happens-before 等关键机制的使用,以及在单例模式、线程通信等场景中的实战案例。同时,还介绍了常见并发 Bug 的排查与解决方案,帮助开发者写出高效、线程安全的 Java 程序。
217 0
|
5月前
|
Java 物联网 数据处理
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%
Java Solon v3.2.0 是一款性能卓越的后端开发框架,新版本并发性能提升700%,内存占用节省50%。本文将从核心特性(如事件驱动模型与内存优化)、技术方案示例(Web应用搭建与数据库集成)到实际应用案例(电商平台与物联网平台)全面解析其优势与使用方法。通过简单代码示例和真实场景展示,帮助开发者快速掌握并应用于项目中,大幅提升系统性能与资源利用率。
152 6
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%