第三章《垃圾收集器与内存分配策略》是《深入理解Java虚拟机》一书中非常重要的一章,详细介绍了Java虚拟机中的垃圾收集算法、垃圾收集器的实现、内存分配策略等内容。以下是这一章的详细分析:
3.1 概述
- 垃圾收集的重要性:垃圾收集(Garbage Collection, GC)是Java虚拟机自动内存管理的核心机制,可以自动回收不再使用的内存,避免内存泄漏和内存溢出问题。
- 垃圾收集的目标:最小化停顿时间、最大化吞吐量、最小化内存占用。
3.2 对象已死吗
3.2.1 引用计数算法
- 原理:每个对象包含一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1。当计数器为0时,对象不可达,可以被回收。
- 缺点:无法解决对象之间循环引用的问题。
3.2.2 可达性分析算法
- 原理:从一组称为“GC Roots”的对象开始,向下搜索,搜索路径所走过的对象称为“存活对象”,未被访问到的对象被认为是“不可达对象”,可以被回收。
- GC Roots:包括虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象。
3.2.3 再谈引用
- 强引用:最常见的引用类型,如
Object obj = new Object()
。只要强引用存在,垃圾收集器永远不会回收被引用的对象。 - 软引用:描述一些还有用但非必需的对象。内存紧张时,JVM会回收这些对象。使用
SoftReference
类实现。 - 弱引用:描述非必需的对象,但强度比软引用更弱。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。使用
WeakReference
类实现。 - 虚引用:也称为幽灵引用,无法通过虚引用来获取对象实例,虚引用的唯一用途是在对象被回收时收到一个系统通知。使用
PhantomReference
类实现。
3.2.4 生存还是死亡
- 对象的存活判断:通过可达性分析算法判断对象是否存活。
- 对象的 finalize() 方法:对象在垃圾收集前有机会执行
finalize()
方法,但不建议依赖此方法进行资源释放。
3.2.5 回收方法区
- 方法区垃圾收集:主要回收废弃的常量和不再使用的类型。判断一个类是否可以被回收的标准包括该类的所有实例都被回收、加载该类的类加载器被回收。
3.3 垃圾收集算法
3.3.1 标记-清除算法
- 原理:分为“标记”和“清除”两个阶段。首先标记所有需要回收的对象,然后统一回收这些对象。
- 缺点:标记和清除两个过程效率不高,会产生内存碎片。
3.3.2 复制算法
- 原理:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当一块内存用完时,就将还存活的对象复制到另一块上面,然后把已使用过的内存空间一次清理掉。
- 优点:每次都是对半个内存区域进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。
- 缺点:内存利用率不高,每次只使用一半的内存。
3.3.3 标记-整理算法
- 原理:标记过程与“标记-清除”算法一样,但后续步骤不是直接清理已死亡对象,而是将存活对象向一端移动,然后清理掉边界以外的内存。
- 优点:解决了内存碎片问题。
3.3.4 分代收集算法
- 原理:根据对象存活周期的不同将内存划分为几块。一般将Java堆分为新生代和老年代,新生代中对象存活率低,使用复制算法;老年代中对象存活率高,使用标记-整理算法。
3.4 HotSpot的算法实现
3.4.1 枚举根节点
- 根节点枚举:在垃圾收集时,首先需要通过根节点枚举来确定哪些对象是存活的。HotSpot使用安全点(Safe Point)技术来确保枚举的准确性。
3.4.2 安全点
- 安全点:程序执行的某些特定位置,虚拟机会在这些位置插入检查点,确保在这些点上进行根节点枚举时不会遗漏存活对象。
3.4.3 安全区域
- 安全区域:在长时间操作(如方法调用)期间,程序可能长时间不到达安全点。在这种情况下,虚拟机会将这段时间的操作视为一个整体,称为安全区域。在安全区域内,虚拟机可以随时中断执行,进行根节点枚举。
3.5 垃圾收集器
3.5.1 Serial收集器
- 特点:单线程收集器,适用于客户端模式下的默认新生代收集器。
- 优点:简单高效。
- 缺点:会停止所有用户线程,造成STW(Stop-The-World)。
3.5.2 ParNew收集器
- 特点:多线程收集器,实际上是Serial收集器的多线程版本。
- 优点:可以利用多核处理器的优势,提高垃圾收集效率。
- 缺点:同样会造成STW。
3.5.3 Parallel Scavenge收集器
- 特点:注重吞吐量的收集器,适用于多核环境。
- 优点:通过多线程并行工作,最大化CPU利用率,提高吞吐量。
- 缺点:停顿时间较长。
3.5.4 Serial Old收集器
- 特点:单线程收集器,适用于老年代。
- 优点:简单高效。
- 缺点:会造成STW。
3.5.5 Parallel Old收集器
- 特点:多线程收集器,适用于老年代。
- 优点:通过多线程并行工作,提高垃圾收集效率。
- 缺点:停顿时间较长。
3.5.6 CMS收集器
- 特点:以最短回收停顿时间为目标的收集器,适用于并发环境。
- 优点:停顿时间短。
- 缺点:对CPU资源敏感,可能会产生浮动垃圾,需要预留足够的内存空间。
3.5.7 G1收集器
- 特点:分代收集器,将整个Java堆划分为多个大小相等的独立区域(Region),可以灵活地进行垃圾收集。
- 优点:可以精确控制停顿时间,支持大内存。
- 缺点:初始标记、最终标记和筛选回收阶段仍会导致STW。
3.6 内存分配与回收策略
3.6.1 对象优先在Eden分配
- 原理:大多数对象都在Eden区分配内存,如果Eden区没有足够空间,则触发Minor GC。
3.6.2 大对象直接进入老年代
- 原理:大对象(如长字符串、大型数组)直接在老年代分配,避免在Eden区和Survivor区中多次复制。
3.6.3 长期存活的对象将进入老年代
- 原理:对象在Survivor区中经历多次Minor GC后,如果仍然存活,会被晋升到老年代。
3.6.4 动态对象年龄判定
- 原理:虚拟机不是永远要求对象在Survivor区中达到某个年龄阈值才能晋升到老年代,如果Survivor区中相同年龄的所有对象大小总和大于Survivor区空间的一半,年龄大于等于该年龄的对象可以直接晋升到老年代。
3.6.5 空间分配担保
- 原理:在发生Minor GC前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果大于,Minor GC可以确保安全;否则,虚拟机会查看HandlePromotionFailure设置值,如果允许担保失败,则尝试进行一次Full GC。
3.7 本章小结
- 总结:本章详细介绍了Java虚拟机中的垃圾收集算法、垃圾收集器的实现、内存分配策略等内容,帮助读者深入理解垃圾收集机制,为后续的性能调优和故障排除打下坚实的基础。
通过这一章的学习,读者可以更好地理解Java虚拟机的内存管理和垃圾收集机制,从而在实际开发中更加有效地优化应用程序的性能。