【JVM】内存分配与回收原则
在 Java 开发中,自动内存管理是 JVM 的核心能力之一,而内存分配与回收的策略直接影响程序的性能和稳定性。本文将详细解析 JVM 的内存分配机制、对象回收规则以及背后的设计思想,帮助开发者更好地理解 JVM 的 "自动化" 内存管理逻辑。
一、内存分配的核心原则
JVM 的内存分配遵循 "分代思想",即根据对象的存活周期将堆内存划分为新生代和老年代,并针对不同区域采用不同的分配策略。
1. 对象优先在 Eden 区分配
大多数情况下,新创建的对象会首先被分配到新生代的 Eden 区。当 Eden 区没有足够空间时,JVM 会触发一次 Minor GC(新生代垃圾回收)。
public class GCTest {public static void main(String[] args) {// 分配30MB对象(大于Eden区默认大小)byte[] allocation1 = new byte[30900*1024];}
}
通过-XX:+PrintGCDetails
参数运行后可以观察到:当 Eden 区无法容纳大对象时,会触发 Minor GC,若 Survivor 区仍无法容纳,则会通过分配担保机制将对象直接晋升到老年代。
2. 大对象直接进入老年代
大对象(需要大量连续内存的对象,如长字符串、数组)会被直接分配到老年代,这是为了避免大对象在新生代频繁复制导致的性能损耗。
不同垃圾回收器对 "大对象" 的判定标准不同:
- G1 收集器:根据
-XX:G1HeapRegionSize
设置的区域大小和-XX:G1MixedGCLiveThresholdPercent
阈值判定 - Parallel Scavenge 收集器:由虚拟机根据堆内存情况动态决定,无固定阈值
3. 长期存活的对象进入老年代
JVM 为每个对象维护一个 "年龄计数器",用于判断对象是否应晋升到老年代:
- 对象在 Eden 区出生,经过第一次 Minor GC 后存活并被移至 Survivor 区,年龄设为 1
- 每在 Survivor 区熬过一次 Minor GC,年龄增加 1 岁
- 当年龄达到阈值(默认 15 岁,可通过
-XX:MaxTenuringThreshold
设置)时,晋升到老年代
动态年龄调整:
虚拟机并非严格按固定年龄阈值晋升对象。当 Survivor 区中相同年龄的对象总大小超过 Survivor 空间的 50%(可通过-XX:TargetSurvivorRatio
调整)时,所有年龄大于或等于该年龄的对象会直接晋升到老年代。
注意:不同收集器的默认阈值不同,CMS 收集器默认阈值为 6,Parallel 收集器默认 15。
二、内存回收的触发机制
JVM 的垃圾回收(GC)按回收范围可分为 Partial GC(部分回收)和 Full GC(整堆回收),其中 Partial GC 又可细分为:
- Young GC:仅回收新生代
- Old GC:仅回收老年代(仅 CMS 支持)
- Mixed GC:回收新生代 + 部分老年代(仅 G1 支持)
1. Young GC 触发条件
当新生代的 Eden 区分配满时,触发 Young GC。此时会有部分存活对象晋升到老年代,因此 Young GC 后老年代占用量可能上升。
2. Full GC 触发条件
Full GC 会回收整个堆空间,触发成本较高,常见触发场景包括:
- 老年代空间不足
- 方法区(元空间)内存不足
- 调用
System.gc()
(建议避免) - 空间分配担保失败
3. 空间分配担保机制
为确保 Minor GC 的安全性,JVM 会在 Minor GC 前进行空间分配担保检查:
- JDK 6 Update 24 前:检查老年代最大可用连续空间是否大于新生代对象总大小或历次晋升平均大小,否则触发 Full GC
- JDK 6 Update 24 后:只要老年代连续空间大于新生代总大小或历次晋升平均大小,就进行 Minor GC,否则触发 Full GC
三、对象存活的判断方法
垃圾回收的前提是准确判断对象是否存活,JVM 主要采用两种判断算法:
1. 引用计数法
- 原理:为每个对象设置引用计数器,引用增加时 + 1,引用失效时 - 1,计数器为 0 则标记为可回收
- 缺陷:无法解决对象循环引用问题,因此主流 JVM 未采用
2. 可达性分析算法
- 原理:以 "GC Roots" 为起点,遍历引用链,不可达的对象被标记为可回收
- GC Roots 包括:
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 同步锁持有的对象
对象死亡的两次标记:
- 第一次标记:可达性分析后不可达的对象
- 第二次标记:检查对象是否需要执行
finalize()
方法,若无需执行或已执行,则判定为死亡
注意:
finalize()
方法已被 JDK 9 标记为过时,建议避免使用。
四、引用类型与回收策略
JDK 1.2 后将引用分为四类,强度依次减弱,影响对象的回收时机:
引用类型 | 特点 | 应用场景 | |
---|---|---|---|
强引用 | Object obj = new Object() | 普通对象引用,内存不足时不会回收 | 一般对象引用 |
软引用 | SoftReference | 内存不足时会被回收 | 内存敏感的缓存 |
弱引用 | WeakReference | 无论内存是否充足,GC 时都会回收 | 临时缓存 |
虚引用 | PhantomReference | 不影响对象生命周期,仅用于跟踪回收 | 管理直接内存 |
软引用和弱引用可配合ReferenceQueue
使用,当引用对象被回收时,引用实例会被加入队列,便于后续处理。
五、实战建议与最佳实践
- 避免创建大对象:大对象直接进入老年代,可能频繁触发 Full GC
- 合理设置新生代大小:新生代过小会导致 Young GC 频繁,过大则会延长 GC 时间
- 选择合适的垃圾收集器:
- 追求吞吐量:选择 Parallel Scavenge + Parallel Old
- 追求低延迟:选择 G1 或 ZGC
- 监控 GC 性能:通过
-XX:+PrintGCDetails
、-XX:+PrintGCLogs
等参数分析 GC 日志 - 慎用
System.gc()
:该方法仅为建议,可能触发 Full GC 影响性能
六、总结
JVM 的内存分配与回收机制是自动内存管理的核心,其设计遵循 "分代收集" 思想,通过不同区域的针对性策略实现高效的内存管理。理解这些原则有助于开发者写出更优的代码,避免常见的内存问题(如 OOM),并在必要时进行有效的性能调优。
掌握内存分配规律、GC 触发机制和引用类型特性,是深入理解 JVM 的重要一步,也是应对高并发、高性能场景的必备知识。