如何判断一个对象是否死亡(即不可能再被任何途径使用)?通常有以下两种方法:
引用计数法(Reference Counting):为每个对象添加一个引用计数器,用来统计指向该对象的引用个数。当有地方引用它时,计数器加一;引用失效时减一。当某个对象的引用计数为零时,说明该对象已死亡,便可以被回收了。
可达性分析(Reachability Analysis)的基本思路如下:
通过一系列称为 GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为"引用链"(Reference Chain),若某个对象到 GC Roots 间没有任何引用链相连(或者用图论的话来说就是从 GC Roots 到这个对象不可达时)则证明此对象是不可能再被使用的。
示意图如下:
GC Roots 可理解为「堆外指向堆内的引用」。在 Java 技术体系中,固定可作为 GC Roots 的对象包括以下几种:
无论是引用计数法还是可达性分析算法,二者都离不开「引用」。JDK 1.2 之后,Java 中「引用」的概念得以扩充,主要分为以下四种:
Object obj = new Object()
《Java 虚拟机规范》并未要求虚拟机在方法区实现垃圾收集。方法区垃圾收集“性价比”较低。
但在大量使用反射、动态代理、CGLib 等字节码框架,动态生成 JSP 及 OSGi 这类频繁自定义类加载器的场景中,通常都需要 JVM 具备类型卸载的能力,以避免方法区内存压力过大。
方法区垃圾回收的主要内容包括:废弃的常量和不再使用的类型。它们的判定条件如下:
虚拟机参数:
# 是否对类型回收
-Xnoclassgc
# 查看类加载和卸载
-verbose:class -XX:+TraceClassLoading -XX:+TraceClassUnLoading
从如何判定对象消亡的角度出发,垃圾收集器可分为“引用计数式垃圾收集”(Reference Counting GC)和“追踪式垃圾收集”(Tracing GC)两大类,也称“直接垃圾收集”和“间接垃圾收集”。
这里的垃圾回收算法都属于“追踪式垃圾收集”的范畴。
当代商业虚拟机的垃圾收集器,多数都遵循了“分代收集”(Generational Collection)的理论。
分代收集理论,实质是一套符合大多数程序运行实际情况的经验法则,有如下三条:
根据以上几条规则,可以推测:
如何解决跨代引用问题? 依据跨代引用假说,为了解决极少数跨代引用,只需在新生代建立一个“记忆集(Remembered Set)”,把老年代划分为若干小块,标识出哪一块内存会存在跨代引用,此后发生 Minor GC 时,只有包含跨代引用的小块内存中的对象才会被加入到 GC Roots 进行扫描(避免扫描整个老年代)。
一些 GC 概念:
下面介绍常见的垃圾收集算法。
标记-清除(Mark-Sweep)算法:最早、最基础的算法,分为“标记”和“清除”两个阶段:
后续的收集算法大多都是以“标记-清除”算法为基础,对其缺点进行改进而得。
该算法的示意图如下:
优缺点分析:
图片来自:http://www.cnblogs.com/dolphin0520/p/3783345.html
简称复制(Copying)算法。现在的商用 Java 虚拟机大都优先采用了这种收集算法回收新生代。
“半区复制”(Semispace Copying)算法将可用内存按容量分为大小相等的两块,每次只使用其中的一块。当这一块用完时,就将还存活的对象复制到另外一块上,然后再把已使用的内存空间一次清理掉。
该算法主要为了解决“标记-清除”算法面对大量可回收对象时执行效率低的问题。
示意图如下:
“半区复制”算法的优缺点:
一般不需要按照 1:1 的比例划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。 HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1 (即“浪费”了 10% 的新生代空间)。
由于无法保证每次每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(大多指老年代)进行分配担保(Handle Promotion),也就是直接进入老年代(相当于“兜底方案”)。
复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。
标记-整理(Mark-Compact)算法:标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
标记-清除算法与标记-整理算法区别:前者是一种非移动式的回收算法,后者是移动式的。
示意图:
主要问题:
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法(不是一种具体的算法实现,可以理解为「组合模式」):根据对象存活周期的不同,将内存划分为几块。
一般把 Java 堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。
每次垃圾收集时,都有大批对象死去,只有少量存活,采用复制算法(只需复制少量存活对象)。
对象存活率较高,没有额外空间对它进行分配担保,必须使用“标记-清除”或“标记-整理”算法进行回收。
前面的收集算法只是内存回收的方法论,而垃圾收集器才是内存回收的具体实现(可理解为“接口”与“实现类”的关系)。
Serial 收集器是最基础、历史最悠久的收集器。特点:
运行示意图如下:
ParNew 收集器实质上是 Serial 收集器的多线程并行版本。
除了同时使用多条线程进行垃圾收集之外,其余的行为包括 Serial 收集器可用的所有控制参数(-XX:SurvivorRatio, -XX:PretenureSizeThreshold、-XX:HandlePromotionFailure 等)、收集算法、Stop The World、对象分配原则、回收策略等都与 Serial 收集器完全一致。
运行示意图:
# 注:JDK 9 取消了 -XX:+UseParNewGC 参数
-XX:+/-UseParNewGC
Parallel Scavenge 收集器是新生代收集器,也是使用标记-复制算法实现的、并行收集的多线程收集器,也称“吞吐量优先收集器”。
与 ParNew 类似,但关注点不同:
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),即运行用户代码时间所占比重。
响应速度快能提升用户体验;而吞吐量高则能更高效地利用 CPU 资源,尽快完成程序的计算任务(主要适合在后台运算而不需要太多交互的任务)。
运行示意图如下:
# 最大垃圾收集停顿时间(毫秒)
-XX:MaxGCPauseMillis
# 设置吞吐量(0~100)
-XX:GCTimeRatio
# 自适应调节策略
-XX:UseAdaptiveSizePolicy
Serial 收集器的老年代版本,单线程,使用“标记-整理”算法。主要用于客户端模式下的 HotSpot 虚拟机。
运行示意图如下:
Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,使用多线程和“标记-整理”算法实现。
运行示意图如下:
在注重吞吐量或者处理器资源较为稀缺的场合,都可以考虑 Parallel Scavenge + Parallel Old 收集器的组合。
CMS(Concurrent Mark Sweep)收集器是一种以「获取最短回收停顿时间」为目标的收集器。
它基于“标记-清除”算法实现,运作过程分为四步:
运行示意图如下:
目前很大一部分 Java 应用集中在互联网网站或者 B/S 系统的服务上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器非常符合这类应用的需求。
CMS 的主要优缺点
浮动垃圾(Floating Garbage):在 CMS 的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程以后,CMS 无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这部分垃圾就是“浮动垃圾”。
# 使用 CMS 收集器
-XX:+UseConcMarkSweepGC
# 老年代使用空间的比例(需根据实际情况权衡)
-XX:CMSInitiatingOccupancyFraction=80
# Full GC 时开启内存碎片的合并整理
-XX:+UseCMSCompactAtFullCollection
Garbage First(G1) 收集器是垃圾收集器发展史上里程碑式的成果,开创了「面向局部收集」的设计思路和「基于 Region」的内存布局形式。
G1 收集器的定位是「CMS 收集器的替代者和继承人」。由于实现较复杂,后文另行分析。这里只做简单描述:
此外,还有一些更为先进的低延迟收集器,比如 OracleJDK 11 加入的 ZGC,RedHat 公司的 Shenandoah 收集器。另外,还有一个有点“奇葩”的 Epsilon 收集器,等等。
衡量垃圾收集器优劣的指标主要有三个:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency)。此三者构成了一个「三元悖论」(类似分布式系统中的 CAP 原则),难以同时满足。
本文简要介绍了一些垃圾收集的相关概念,常用的垃圾收集算法以及经典的垃圾收集器。由于 G1 收集器实现稍复杂,因此后面单独分析。本文主要内容概括如下图: