成为一名合格的 Java 程序员,JVM 是避不开的知识点。

这篇学习笔记的内容来自《深入理解Java虚拟机:JVM高级特性与最佳实践》。

回顾

上一篇文章讲了 JVM 的运行时数据区:

栈中存储的是栈帧,栈帧又包含一次方法调用的全部信息,在类的结构确定之时,栈帧的大小基本上就已经确定下来,而栈又是线程私有的,在线程存活的条件下,基本上不可能回收这一部分的内存,因此垃圾回收器通常不处理栈的内存空间。

同样的道理也可以应用于程序计数器和方法区,虽然方法区不是线程私有的,但是它加载的类信息都是有用的,除非极端情况下需要卸载类,否则没必要对这部分内存进行垃圾回收。

一、垃圾收集模块

堆里面存放的都是对象的实例,当实例的生命周期结束,垃圾收集器便需要回收这些“垃圾”,保持堆剩余空间的可用。

1.1 生存还是毁灭

对于一个实例,如何知道它是否被使用,这是垃圾收集器首先要解决的问题。

可以作为 GC Roots 的对象包括:

提示:引用计数法没有被 Java 采用,是因为 Java 允许 A 对象引用 B 对象的同时,B 对象也可以引用 A 对象,这种情况就是循环依赖,它会导致引用计数器永不为 0,造成 A 和 B 对象所在的内存空间泄漏,无法被回收。

1.2 引用分类

JDK 1.2 之前,引用就是 reference 类型所存储的数值对应堆里面的一个内存地址。之后,引用被扩充为四种类型:

1.3 自我救赎

当一个对象被垃圾收集器发现“不可达”时,会被标记一次,随后进行一次“背景审核”。审核的条件是,finalize() 方法是否被覆盖,或者已经被调用过。当条件成立,则虚拟机不调用当前对象的这个方法。

需要执行 finalize() 方法的对象,会被放入到名为 F-Queue 的队列中,再交由虚拟机自动建立的低优先级的 Finalizer 线程去处理。处理时,会触发 finalize() 方法,但不承诺会等待执行完毕。如果这一次标记成功的话,那么垃圾收集器才会真正回收这部分对象,否则还有机会“自我救赎”。逃脱被回收的方式就是重新建立与引用链上任何对象的关系,比如给某个对象的成员变量赋值 this。但“自我救赎”也只能够成功一次,因为 finalize() 方法只会被虚拟机调用一次。

1.4 永久代收集

方法区即永久代,这部分内存空间的垃圾收集效率较低,没有新生代那么有效。

1.5 算法

垃圾回收算法的实现有很多种,与做家务十分类似,每个人做家务的习惯不同,因此步骤与手法不尽相同,虚拟机的实现不同,垃圾回收算法也不一样。

1.6 实现

不同厂商有不同的 JVM 实现,不同的 JVM 所提供的垃圾收集算法也不一致,以下展示 Sun HotSpot 1.6 Update 22 垃圾收集器。

hotspot jvm 1.6 垃圾收集器

1.7 参数

虚拟机内置许多垃圾收集器,对于桌面应用、服务端应用或者其他应用的不同使用场景,可以选择不同的垃圾收集器组合,提供更好的用户体验(主要是回收垃圾时的停顿时间)。

-XX:+{参数} 描述
UseSerialGC 运行在 Client 模式下的默认值,开启则使用 Serial + Serial Old 组合
UseParNewGC 使用 ParNew + Serial Old 组合
UseConcMarkSweepGC 使用 ParNew + CMS + Serial Old 组合,Serial Old 是后备收集器
UseParallelGC 运行在 Server 模式下的默认值,开启则使用 Parallel Scavenge + Serial Old (PS MarkSweep)组合
UseParallelOldGC 使用 Parallel Scavenge + Parallel Old 组合
SurvivorRatio 新生代中 Survivor 与 Eden 的容量比值,默认为 8,即 Survivor:Eden = 1:8
PretenureSizeThreshold 对象大小超过阈值将直接在老年代分配
MaxTenuringThreshold 对象年龄超过阈值将晋升为老年代,年龄是指 Minor GC 之后,自动加一
UseAdaptiveSizePolicy 动态调整 Java 堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure 是否允许分配担保失败,即极端情况下,新生代中所有对象都存活,使得老年代剩余空间无法容纳
ParallelGCThreads 设置并行回收线程的数量
GCTimeRatio GC 时间占比,算法为 1:(1+N),默认值为 99,等于 1:100 的比率,也就是 1%,仅 Parallel Scavenge 生效
MaxGCPauseMills GC 最大停顿时间,仅 Parallel Scavenge 生效
CMSInitiatingOccupancyFraction 启动占用率,默认为 68%,即老年代空间使用超过 68% 时,触发垃圾收集,仅 CMS 生效
UseCMSCompactAtFullCollection 回收完成后是否进行内存碎片整理,仅 CMS 生效
CMSFullGCsBeforeCompaction 回收一定次数后是否进行内存碎片整理,仅 CMS 生效

二、内存分配

应对垃圾回收策略,了解对象在内存上的不同位置和存活时间,学会合理分配内存空间,优化代码提升性能。

2.1 优先分配

大部分情况下,对象在新生代的 Eden 区分配,当 Eden 区内存不足时,将发起一次 Minor GC。

2.2 大对象分配

需要大量连续内存空间的对象,则称之为大对象,通常是很长的字符和数组。

大对象在分配时,容易导致在内存还有足够使用的空间时,依然触发垃圾回收以整理出足够安置大对象的内存空间。所以一般情况下会设置一个阈值,当对象的大小超过阈值时,则直接在老年代中分配这个对象。需要注意的是,这个阈值只对 Serial 和 ParNew 两款垃圾收集器有效。

2.3 长期存活对象

当对象在 Minor GC 发生时标记为存活,那就会被转移到 Survivor to 区域,如果 Survivor from 中也有对象存在,那么会根据年龄阈值来判断去留,超过阈值则进入老年代,否则转移到 to 区域,随后 to 区域与 from 区域转换身份,再清理掉 Eden 区域和 Survivor to 区域。Survivor from 区域中的对象,每经历一次 Minor GC,它们的年龄都会增加 1 岁,默认的年龄阈值是 15 岁。

2.4 动态年龄判断

在 Survivor 空间中,相同年龄的对象,它们的大小总和超过 Survivor 空间的一半时,那么大于等于这个年龄值的对象就可以直接进入老年代,不需要达到年龄阈值。

2.5 空间担保

在 Minor GC 时,如果之前晋升到老年代的内存平均值大于老年代的剩余空间,那么就直接进行 Full GC,否则就去查看 HandlePromotionFailure 设置是否允许担保失败,如果允许,那就只进行 Minor GC,要是不允许,还是要改为 Ful GC。

空间担保是为了减少 Full GC 的频率,在允许担保失败的情况下,如果 Minor GC 失败(新生代中 Eden 空间存活对象过多,导致 Survivor 空间无法容纳),那么就需要老年代来担保这次失败,把存活对象直接放入老年代。如果老年代不足以容纳这些存活对象,那么最终再进行一次 Full GC。

总结

避免创建朝生夕灭的大对象,减少 Full GC 的频率,有利于提升用户体验,让程序更平稳更顺畅。

下一篇文章,我们来了解一下虚拟机性能监控与故障处理。