文章目录
1 垃圾收集器
1.1 垃圾收集器管理的区域
垃圾收集器关注的是运行期才能确定的动态分配和回收的区域的内存。线程私有区域随线程生灭,不需要管理。
1.2 如何判断对象已死
1.2.1 引用计数算法
原理简单、判定效率高。但是对象之间相互引用容易导致内存泄漏,Java领域几乎没有选用这种算法的。
1.2.2 可达性分析算法
与根节点之间是否存在引用链、图论的观点是根节点到它是否可达。
1.3 哪些对象可以作为根节点
栈中引用的对象、静态属性引用的对象、Java虚拟机内部的引用(基本数据类型的Class对象、常驻异常类型、系统类加载器等)、反映Java虚拟机内部情况的对象(JMXBean、JVMTI等)、临时性加入的对象(分代收集和局部收集中被其他区域引用的对象)。
1.4 四种不同强度的引用
强引用、软引用、弱引用、虚引用。
1.5 finalize()方法
可以帮助已被标记死亡的对象重生,但是不推荐使用,try-finally或其他方式可以做的更好。
1.6 方法区的回收
主要是针对废弃的常量和不再使用的类型。
1.7 分代收集理论
Minor GC:只收集新生代,Major GC:只收集老年代,Full GC:收集整个Java堆和方法区。
1.8 跨代引用
被引用的新生代对象很容易晋升到老年代中。在新生代建立记忆集,把老年代分成若干小块,标识出哪一块存在跨代引用,进行Minor GC时,只扫描包含跨代引用的老年代块。记忆集会增加运行时开销,但总体上是划算的。
1.9 标记-清除算法
会产生大量的内存碎片
1.10 标记-复制算法
商用Java虚拟机大多采用这种算法,新生代中绝大部分对象活不过第一次垃圾收集,Appel式回收(把新生代分为一块Eden和两块Survivor),分配担保机制(Survivor存放不下上一次收集剩下的存活对象)。
1.11 标记-整理算法
让所有存活对象都向内存空间一端移动,然后直接清理掉边界以外的内存,移动对象的操作必须暂停用户线程。移动对象虽然会有很大的代价,但是解决了内存碎片化的问题,提高了程序的吞吐量。和稀泥式的解决内存碎片化问题的方案是平时都使用标记-清除算法,直到内存碎片化无法忍受时再采用标记-整理算法收集一次。
2 HotSpot算法细节实现
2.1 根节点枚举
必须在一个能保障一致性的快照中才能得以进行,否则无法保证结果准确性。
2.2 安全点
只在具有方法调用、循环跳转、异常跳转等功能还有创建对象等需要在堆上分配内存的地方(避免内存溢出)产生。让所有线程在垃圾收集时都跑到最近的安全点,主要有抢先式中断和主动式中断两种方式。
2.3 安全区域
实际上就是扩展拉伸了的安全点。在安全区域内,对象的引用关系不会发生改变,垃圾收集因此是安全的。
2.4 记忆集与卡表
卡表是记忆集的一种实现方式,CARD-TABLE的每一个元素都对应着一个卡页,一个卡页包含着多个对象,只要有一个对象存在跨代指针,就将对应卡表元素的值标为1(元素变脏Dirty),没有则标为0。垃圾收集时会扫描变脏的卡页中的对象。维护卡表会有一定的开销,但是与扫描整个堆相比代价要低很多。
2.5 写屏障
因为即时编译会将代码直接转换为机器指令流,所以卡表变脏的操作必须在机器码层面解决。写屏障可以看作虚拟机层面对引用类型赋值的AOP切面,可以执行额外的操作(更新卡表)。
2.6 并发的可达性分析
三色标记法:黑(本身及所有引用已被扫描)、白(未被访问)、灰(还有未被访问的引用)。对象消失的问题可以通过增量更新或者原始快照的方法解决(这两种方式都是通过写屏障来实现的)。
3 经典垃圾收集器
3.1 Serial
单线程、标记-复制算法、暂停用户线程、简单高效、额外内存消耗少、可获得最高的单线程收集效率。
3.2 ParNew
多线程、标记-复制算法、暂停用户线程、单核环境不如Serial。只有Serial和ParNew能与CMS配合工作。
3.3 Parallel Scavenge
多线程、标记-复制算法、暂停用户线程、吞吐量优先(可设置目标吞吐量和停顿时间)、自适应调节策略(根据性能分析的结果自动调整目标吞吐量和停顿时间)。
3.4 Serial Old
单线程、标记-整理算法、CMS(Concurrent Mode Failure)失败时的后备方案。
3.5 Parallel Old
多线程、标记-整理算法。在注重吞吐量或处理器资源较为稀缺的场景可考虑Parallel Scavenge加Parallel Old的组合。
3.6 CMS
3.6.1 使用场景
以最短停顿时间为目标,适合在注重用户体验的场景使用。
3.6.2 四个步骤
初始标记、并发标记、重新标记、并发清理。其中耗时较长的并发标记和并发清理都是和用户程序一起并发进行的,因此总体上是与用户线程并发执行。
3.6.3 优缺点
对处理器资源敏感、会导致应用程序变慢、降低吞吐量、无法处理浮动垃圾(并发阶段新创建的对象)。使用标记-清除算法会产生内存碎片的问题(可以隔一段时间进行一次带碎片整理功能的Full GC来解决碎片化的问题)。
3.7 Garbage First
开创了面向局部收集的设计思路和基于Region的内存布局形式。每个Region可扮演Eden、Survivor或老年代空间(不同的角色收集器会采用不同的策略处理)。Humongous Region专门用来存储大对象且大多被当做老年代的一部分。在后台维护一个优先级列表(各个Region的回收价值排序),根据用户设置的预期停顿时间来确定回收集。在延迟可控的情况下获得尽可能高的吞吐量,而并非纯粹追求低延迟。从G1开始垃圾收集器所追求的是应付内存分配的速率。
3.7.1 跨Region引用
每个Region都维护有自己的记忆集(使用写后屏障来维护),这会消耗大量的内存。
3.7.2 如何确保并发阶段收集线程与用户线程互不干扰
使用TAMS指针将Region中的一部分空间划分出来用于新对象的分配。使用原始快照的方式来解决对象消失的问题。
3.7.3 如何建立可靠的停顿预测模型
使用衰减均值理论来得出最佳的收集方式。
3.7.4 四个步骤
初始标记、并发标记、最终标记、筛选回收。在筛选回收阶段,先用标记-复制算法将存活对象复制到空的Region中,再清理掉旧Region空间。整体上使用标记-整理算法,局部采用标记-复制算法。
13.7.5 缺点
内存占用高、执行负载高。CMS的写屏障是同步操作,而G1是类似于消息队列的结构(异步处理)。
4 选择合适的垃圾收集器
4.1 关注点
吞吐量(数据分析、科学计算类只为尽快算出结果)、延迟时间(影响交互体验)、内存占用(客户端或者嵌入式应用内存较少)。
4.2 基础设施
硬件规格、系统架构、处理器数量、内存大小、Linux还是Windows等。
4.3 JDK发行商
版本号、发行公司、对应的《Java虚拟机规范》版本。
5 虚拟机及垃圾收集器日志
JDK9及以后,HotSpot所有功能的日志都收归到了“-Xlog”参数上。
6 内存分配与回收策略
对象优先在Eden分配(没有足够空间时进行一次MinorGC)、大对象直接进入老年代、长期存活的对象进入老年代(设置年龄阈值)、动态对象年龄判定(Survivor空间中低于或等于某年龄的所有对象的大小总和大于Survivor空间的一半,那么年龄大于该值的对象可直接进入老年代)。JDK6Update24之后只要老年代的连续空间大于新生代对象总大小或历次晋升的平均大小,就会进行MinorGC,否则将进行FullGC。