阅读完《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》的第三章后,我对Java的垃圾收集和内存分配策略有了更深入的理解。以下是我对这部分内容的总结:
一、概述
- 垃圾收集的重要性
- 内存管理的关键:垃圾收集是Java自动内存管理的核心,它负责回收不再使用的对象,释放内存空间,确保程序的正常运行。
- 性能优化的关键:合理的垃圾收集策略能显著提高程序的性能,减少内存占用,避免内存泄漏和溢出错误。
- 内存分配的特点
- 动态性:Java堆和方法区的内存分配是动态的,程序在运行时根据需要分配内存。
- 不确定性:不同对象的内存需求和生存周期难以预测,这增加了内存管理的复杂性。
二、对象存活判定
- 引用计数算法
- 原理:通过在对象中添加引用计数器,每当有引用指向对象时计数器加一,引用失效时计数器减一。计数器为零时,对象可被回收。
- 可达性分析算法
- 原理:从GC Roots开始,通过引用链搜索对象,如果对象到GC Roots没有引用链相连,则可被回收。
- GC Roots对象
- 线程相关:包括虚拟机栈中局部变量表引用的对象、方法区中类静态属性引用的对象等。
- 虚拟机内部:如基本数据类型对应的Class对象、一些常驻的异常对象等。
- 再谈引用
- 引用类型:Java中有强引用、软引用、弱引用和虚引用四种。
- 引用作用
- 强引用:常见的引用类型,强引用存在时对象不会被回收。
- 软引用:用于描述非必须对象,内存不足时会被列入回收范围。
- 弱引用:比软引用更弱,对象生存到下一次垃圾收集。
- 虚引用:主要用于对象回收时的系统通知。
- 生存还是死亡
- 两次标记过程
- 第一次标记:对象在可达性分析后没有引用链相连会被第一次标记。
- Finalize阶段:有必要执行finalize()方法的对象会被放置在F-Queue队列,由Finalizer线程执行该方法,但该方法执行存在不确定性且不推荐使用。
- 第二次标记:执行finalize()方法后,对象进行第二次标记,若仍未逃脱则被回收。
- 两次标记过程
- 回收方法区
- 回收内容:主要回收废弃的常量和不再使用的类型。
- 类型卸载条件
- 实例回收:类的所有实例都被回收。
- 类加载器回收:加载类的类加载器被回收。
- Class对象引用:类对应的java.lang.Class对象没有被引用。
三、垃圾收集算法
- 分代收集理论
- 理论基础:基于对象的存活特性和垃圾收集成本提出,大多数对象朝生夕灭,老年代对象相对稳定,跨代引用较少。
- 分代假说
- 弱分代假说:绝大多数对象生命周期短。
- 强分代假说:熬过多次垃圾收集的对象更难消亡。
- 跨代引用假说:跨代引用占比极少。
- 常见算法
- 标记 - 清除算法
- 算法过程:标记出需要回收的对象,然后统一回收。
- 缺点:执行效率不稳定,会产生内存碎片化。
- 标记 - 复制算法
- 算法过程:将内存分为两块,使用其中一块,存活对象复制到另一块,然后清理原块内存。
- 优点:实现简单,运行高效,但空间浪费大。
- 标记 - 整理算法
- 算法过程:标记后移动存活对象,清理边界以外内存。
- 特点:可避免内存碎片化,但实现复杂。
- 标记 - 清除算法
四、HotSpot的算法细节实现
- 根节点枚举:通过OopMap数据结构快速准确完成GC Roots枚举。
- 安全点与安全区域
- 安全点:具有让程序长时间执行特征的指令处产生安全点,如方法调用、循环跳转等。
- 安全区域:在引用关系不发生变化的区域进行垃圾收集是安全的。
- 记忆集与卡表
- 记忆集:用于记录非收集区域指向收集区域的指针集合。
- 卡表:记忆集的一种具体实现,通过字节数组记录内存块使用情况。
- 写屏障:用于维护卡表状态,在对象赋值时更新卡表。
- 并发的可达性分析
- 并发问题:用户线程与收集器并发工作可能导致对象消失问题。
- 解决方法:通过增量更新和原始快照避免对象消失。
五、经典垃圾收集器
- 收集器介绍
- Serial收集器:单线程工作,基础且历史悠久,适用于客户端应用。
- ParNew收集器:Serial的多线程并行版本,曾广泛用于服务端。
- Parallel Scavenge收集器:侧重于吞吐量优化,适用于后台运算。
- Serial Old收集器:Serial的老年代版本,用于客户端。
- Parallel Old收集器:Parallel Scavenge的老年代版本,支持多线程并发收集。
- CMS收集器:以获取最短回收停顿时间为目标,适用于对响应时间要求高的场景。
- Garbage First收集器:面向服务端应用,采用分代和区域回收方式。
- 特点与适用场景
- 特点:不同收集器在算法实现、性能特点上各有差异。
- 适用场景:根据应用程序特点和性能要求选择合适的收集器。
六、低延迟垃圾收集器
- 背景与目标
- 延迟问题:传统垃圾收集器在处理大规模数据和高并发请求时,可能导致长时间停顿,影响应用响应性能。
- 低延迟目标:实现毫秒级的垃圾收集停顿时间,提高应用程序的响应性能。
- 具体收集器
- Shenandoah收集器:由非Oracle开发,通过并发标记和整理实现低延迟。
- ZGC收集器:Oracle开发,采用染色指针技术和区域化内存布局。
七、选择合适的垃圾收集器
- 考虑因素
- 应用程序特点:包括性能要求、内存使用模式、并发程度等。
- 硬件资源:如处理器数量、内存大小等。
- JDK版本:不同JDK版本提供的收集器不同。
- 权衡与决策
- 性能指标:综合考虑内存占用、吞吐量和延迟等指标。
- 实验与测试:通过实验和测试选择最适合的收集器。
八、实战:内存分配与回收策略
- 基本原则
- 对象分配区域:对象优先在Eden区分配,大对象直接进入老年代,长期存活对象进入老年代。
- 分配担保:Minor GC前检查老年代空间是否满足新生代对象分配需求。
- 代码验证
- 实验设置:通过编写代码设置不同参数验证内存分配和回收策略。
- 实验结果:分析结果了解不同参数对内存分配和回收的影响。
以下是一些与本章内容相关的图片示例:
-
Java虚拟机运行时数据区
-
HotSpot虚拟机对象内存布局
-
垃圾收集算法示意图
-
标记 - 清除算法
-
标记 - 复制算法
-
标记 - 整理算法
-
九、进一步学习建议
- 深入理解算法原理
- 参考书籍:《垃圾回收算法手册:自动内存管理的艺术》详细介绍了各种垃圾收集算法的原理和实现细节。
- 实践练习:通过编写代码和分析内存日志,深入理解算法的工作原理和性能特点。
- 研究垃圾收集器实现
- 参考书籍:《Java虚拟机规范》提供了关于Java虚拟机实现的详细规范。
- 源码分析:分析HotSpot虚拟机的源码,了解垃圾收集器的具体实现细节。
- 性能优化实践
- 实际项目:在实际项目中应用不同的垃圾收集器和内存分配策略,进行性能测试和调优。
- 性能监控工具:学习使用JDK自带的jstat、jmap等工具监控应用程序的内存使用情况和垃圾收集性能。
通过学习第三章的内容,我对Java的垃圾收集和内存分配策略有了更深入的理解,这将有助于我编写更高效、稳定的Java程序。在今后的学习和工作中,我将继续深入研究这些知识,不断提升自己的编程能力。