Java 内存模型与垃圾回收机制详解
在 Java 开发中,理解内存模型和垃圾回收机制是编写高效、稳定程序的基础。本文将深入解析 Java 内存模型的结构以及垃圾回收的核心原理与常用算法,帮助开发者更好地优化代码性能。
目录
一、Java 内存模型(JVM Memory Model)
1.1 内存区域划分
1.2堆(Heap)
1.3创建对象的内存分配
1.4元空间(Meta Space)
二、Java 垃圾回收机制
2.1 垃圾判定算法
2.2 垃圾回收算法
2.3 常见垃圾收集器
2.4 垃圾回收调优
三、总结
一、Java 内存模型(JVM Memory Model)
Java 内存模型(JVM)定义了 Java 程序中各种变量的访问规则,以及在 JVM 中将变量存储到内存和从内存中取出变量这样的底层细节。
1.1 内存区域划分
JVM 内存主要分为以下几个区域:
JDK 1.8之前分为:线程共享(Heap堆区、Method Area方法区)、线程私有(虚拟机栈、本地方法栈、程序计数器)
JDK 1.8以后分为:线程共享(Heap堆区、MetaSpace 元空间)、线程私有(虚拟机栈、本地方法栈、程序计数器)
-
程序计数器(Program Counter Register)
- 线程私有
- 指向当前线程正在执行的字节码指令的地址
- 如果执行的是 Native 方法,则计数器值为空
-
Java 虚拟机栈(Java Virtual Machine Stacks)
- 线程私有,生命周期与线程相同
- 每个方法执行时会创建一个栈帧(Stack Frame),存储局部变量表、操作数栈、动态链接、方法出口等信息
- 可能抛出 StackOverflowError(栈深度溢出)和 OutOfMemoryError(栈扩展失败)
-
本地方法栈(Native Method Stack)
- 与虚拟机栈类似,但为 Native 方法服务
- HotSpot 虚拟机将其与虚拟机栈合二为一
-
Java 堆(Java Heap)
- 所有线程共享,在虚拟机启动时创建
- 主要用于存储对象实例,几乎所有对象都在这里分配内存
- 是垃圾收集器管理的主要区域,也称为 "GC 堆"
- 可分为新生代(Eden 区、From Survivor 区、To Survivor 区)和老年代
-
方法区(Method Area)
- 所有线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- JDK8 及以后,元空间(Metaspace)替代了方法区,元空间使用本地内存作为直接内存
1.2堆(Heap)
Heap堆是JVM 所管理的内存中最大的一块区域,被所有线程共享的一块内存区域。堆区中存放对象实例,“几乎”所有的对象实例以及数组都在这里分配内存。
Heap堆是垃圾收集器GC(Garbage Collected)管理的主要区域,因此堆区也被称作GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 JVM中的堆区往往进行分代划分,例如:新生代 和 老年代。目的是更好地回收内存,或者更快地分配内存。
1.3创建对象的内存分配
创建一个新对象,在堆中的分配内存。
大部分情况下,对象会在 Eden 区生成,当 Eden 区装填满的时候,会触发 Young Garbage Collection,即 YGC垃圾回收的时候,在 Eden 区实现清除策略,没有被引用的对象则直接回收。
依然存活的对象会被移送到 Survivor 区。Survivor 区分为 s0 和 s1 两块内存区域。每次 YGC的时候,它们将存活的对象复制到未使用的Survivor 空间(s0 或 s1),然后将当前正在使用的空间完全清除,交换两块空间的使用状态。每次交换时,对象的年龄会加+1。
如果 YGC 要移送的对象大于 Survivor 区容量的上限,则直接移交给老年代。一个对象也不可能永远呆在新生代,在 JVM 中 一个对象从新生代晋升到老年代的阈值默认值是 15,可以在 Survivor区交换 14 次之后,晋升至老年代。
1.4元空间(Meta Space)
用于存放类信息、常量、静态变量、JIT即时编译器编译后的机器代码等数据等。例如:java.lang.Object类的元信息、Integer.MAX_VALUE常量等。
元空间的本质和永久代类似,都是对JVM规范中方法区的一种具体实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过运行参数来指定元空间的大小。
Java 8 中 PermGen永久代为什么被移出 HotSpot JVM?
●由于 PermGen 内存经常会溢出,容易抛出 java.lang.OutOfMemoryError: PermGen错误;
●移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代;
二、Java 垃圾回收机制
垃圾回收(Garbage Collection,GC)是 Java 的核心特性之一,它自动管理内存的分配与回收,减少了内存泄漏和野指针问题。
2.1 垃圾判定算法
垃圾回收的第一步是确定哪些对象已经不再被使用,常用的判定算法有:
-
引用计数法(Reference Counting)
- 给每个对象设置一个引用计数器
- 当对象被引用时,计数器加 1;引用失效时,计数器减 1
- 当计数器为 0 时,认为对象可回收
- 缺点:无法解决循环引用问题
-
可达性分析算法(Reachability Analysis)
- 以 "GC Roots" 为起点,向下搜索引用链
- 如果对象到 GC Roots 没有任何引用链相连,则证明此对象不可用
- 可作为 GC Roots 的对象:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI 引用的对象等
2.2 垃圾回收算法
确定了需要回收的对象后,就需要进行内存回收,常用的回收算法有:
-
标记 - 清除算法(Mark-Sweep)
- 分为 "标记" 和 "清除" 两个阶段
- 标记:标记出所有需要回收的对象
- 清除:回收被标记的对象所占用的内存空间
- 缺点:效率不高,会产生大量不连续的内存碎片
-
复制算法(Copying)
- 将内存分为大小相等的两块,每次只使用其中一块
- 当一块内存用完,将存活对象复制到另一块,然后清除已使用的那块
- 优点:效率高,不会产生内存碎片
- 缺点:内存利用率低
-
标记 - 整理算法(Mark-Compact)
- 标记阶段与标记 - 清除算法相同
- 整理阶段:将所有存活对象向一端移动,然后直接清理掉端边界以外的内存
- 适用于老年代
-
分代收集算法(Generational Collection)
- 根据对象的存活周期将内存划分为不同区域
- 新生代:对象存活时间短,采用复制算法
- 老年代:对象存活时间长,采用标记 - 清除或标记 - 整理算法
2.3 常见垃圾收集器
JVM 提供了多种垃圾收集器,各有特点:
1.Serial GC
Serial Old收集器同样是一个单线程收集器,采用“标记-整理”算法负责老年代的垃圾收集,主要用于客户端模式下的HotSpot虚拟机使用。
单线程收集,收集时暂停所有用户线程
简单高效,适用于 Client 模式
2.ParNew GC
ParNew 收集器是一个多线程的垃圾收集器。它是运行在 Server模式下的虚拟机的首要选择,可以与 Serial Old ,CMS 垃圾收集器一起搭配工作,采用“标记-复制”算法
Serial GC 的多线程版本
可与 CMS 配合使用
3.Parallel Scavenge GC
Parallel Scavenge 收集器是也是一款新生代收集器,使用“标记-复制”算法实现的多线程收集器
Parallel Scavenge 收集器预其它收集器的目标不同,CMS等其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间。但是Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。
关注吞吐量(吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间))
自适应调节策略
4.CMS(Concurrent Mark Sweep)GC
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法实现,是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
工作流程
整个过程包括四个步骤:
1初始标记(CMS initial mark):标记一下GC Roots 能直接关联到的对象,速度很快;
2并发标记(CMS concurrent mark):从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
3重新标记(CMS remark):重新标记阶段,是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间长,远远比并发标记阶段时间短
4并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
以获取最短回收停顿时间为目标
并发收集、低停顿
缺点:CPU 资源敏感、无法处理浮动垃圾、会产生内存碎片
5.G1(Garbage-First)GC
G1 ( Garbage-First ) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器、大容量内存的机器。它不再严格按照分代思想进行垃圾回收。
G1 采用局部性收集的思想,对于堆空间的划分,采用Region为单位的内存划分方式:
G1 垃圾回收器把堆划分成若干个大小相同的独立区域(Region)(按照JVM的实现,Region的数量不会超过2048个):每个 Region 都会代表某一种角色,H、S、E、O。E代表Eden区,S代表 Survivor 区,H代表的是 Humongous(G1用来分配大对象的区域,对于 Humongous 也分配不下的超大对象,会分配在连续的 N 个 Humongous中),剩余的深蓝色代表的是 Old 区,灰色的代表的是空闲的 region。
工作流程:
●初始标记(Initial Marking):这个阶段仅仅只是标记GC Roots能直接关联到的对象,让下一阶段用户程序并发运行时,能在正确的可用的Region中创建新对象,这阶段需要停顿线程,但是耗时很短。
●并发标记(Concurrent Marking):从GC Roots开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象,这阶段耗时较长,但是可以与用户程序并发执行。
●最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后遗留记录。
●筛选回收(Live Data Counting and Evacuation):负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。可以自由选择多个Region来构成【回收集】,然后把回收的那一部分Region中的存活对象==>复制==>到空的Region中,最后对那些Region进行清空
面向服务端应用
将堆内存划分为多个大小相等的独立区域
可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集
建立可预测的停顿时间模型
2.4 垃圾回收调优
垃圾回收调优的基本步骤:
- 监控 GC 状态,收集 GC 日志
- 分析日志,确定问题所在
- 调整 JVM 参数,优化 GC 性能
常用的 JVM 参数:
plaintext
// 设置堆初始大小和最大大小
-Xms2g -Xmx2g// 设置新生代大小
-Xmn512m// 设置老年代与新生代的比例
-XX:NewRatio=2// 设置Survivor区与Eden区的比例
-XX:SurvivorRatio=8// 指定垃圾收集器
-XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC// 打印GC日志
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:gc.log
三、总结
Java 内存模型和垃圾回收机制是 JVM 的核心组成部分,深入理解它们对于编写高性能 Java 程序至关重要:
- 熟悉内存区域划分有助于理解对象的生命周期和内存分配
- 了解垃圾回收原理可以帮助我们编写更易于 GC 处理的代码
- 合理配置 GC 参数能够显著提升应用性能
在实际开发中,我们应该根据应用特点选择合适的垃圾收集器,并通过监控和分析不断优化 JVM 参数,以达到最佳性能。