实际上,垃圾收集器( GC , Garbage Collector )是和具体 JVM 实现紧密相关的,不同厂商( IBM 、 Oracle ),不同版本的 JVM ,提供的选择也不同。接下来,我来谈谈最主流的 Oracle JDK 。
1.Serial GC,它是最古老的垃圾收集器,“Serial”体现在其收集工作是单线程的,并且在进行垃圾收集过程中,会进入臭名昭著的“Stop-The-World”状态。当然,其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是 Client 模式下 JVM 的默认选项。
从年代的角度,通常将其老年代实现单独称作 Serial Old ,它采用了标记 - 整理( Mark-Compact )算法,区别于新生代的复制算法。Serial GC 的对应 JVM 参数是:
-XX:+UseSerialGC
2.ParNew GC,很明显是个新生代GC实现,它实际是Serial GC的多线程版本,最常见的应用场景是配合老年代的CMS GC工作,下面是对应参数
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
3.CMS(Concurrent Mark Sweep) GC,基于标记-清除(Mark-Sweep)算法,设计目标是尽量减少停顿时间,这一点对于Web等反应时间敏感的应用非常重要,一直到今天,仍然有很多系统使用 CMS GC 。但是, CMS 采用的标记 - 清除算法,存在着内存碎片化问题,所以难以避免在长时间运行等情况下发生 full GC ,导致恶劣的停顿。另外,既然强调了并发( Concurrent ), CMS 会占用更多 CPU 资源,并和用户线程争抢。
4.Parrallel GC,在早期JDK 8等版本中,它是server模式JVM的默认GC选择,也被称作是吞吐量优先的GC。它的算法和Serial GC比较相似,尽管实现要复杂的多,其特点是新生代和老年代 GC 都是并行进行的,在常见的服务器环境中更加高效。
开启选项是:
-XX:+UseParallelGC
另外, Parallel GC 引入了开发者友好的配置项,我们可以直接设置暂停时间或吞吐量等目标, JVM 会自动进行适应性调整,例如下面参数:
-XX:MaxGCPauseMillis=value
-XX:GCTimeRatio=N // GC时间和用户时间比例 = 1 / (N+1)
G1 GC这是一种兼顾吞吐量和停顿时间的GC实现,是Oracle JDK 9以后的默认GC选项。G1可以直观的设定停顿时间的目标,相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
G1 GC 仍然存在着年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个 region 。 Region 之间是复制算法,但整体上实际可看作是标记 - 整理( Mark-Compact )算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候, G1 的优势更加明显。
G1 吞吐量和停顿表现都非常不错,并且仍然在不断地完善,与此同时 CMS 已经在 JDK 9 中被标记为废弃( deprecated ),所以 G1 GC 值得你深入掌握。
垃圾收集的算法有哪些?如何判断一个对象是否可以回收?
垃圾收集器工作的基本流程。
垃圾收集的原理和基础概念
第一,自动垃圾收集的前提是清楚哪些内存可以被释放。这一点可以结合我前面对 Java 类加载和内存结构的分析,来思考一下。
主要就是两个方面,最主要部分就是对象实例,都是存储在堆上的;还有就是方法区中的元数据等信息,例如类型不再使用,卸载该 Java 类似乎是很合理的。
对于对象实例收集,主要是两种基本算法, 引用计数 和可达性分析。
1.引用计数算法,顾名思义,就是为对象添加一个引用计数,用于记录对象被引用的情况,如果计数为0,即表示对象可回收。这是很多语言的资源回收选择,例如因人工智能而更加火热的 Python ,它更是同时支持引用计数和垃圾收集机制。具体哪种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提高吞吐量的尝试。
Java 并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系。
2.另外就是Java选择的可达性分析,Java的各种引用关系,在某种程度上,将可达性问题还进一步复杂化,具体请参考 专栏第 4 讲 ,这种类型的垃圾收集通常叫作追踪性垃圾收集
(Tracing Garbage Collection)。其原理简单来说,就是将对象及其引用关系看作一个图,选定活动的对象作为 GC Roots,然后跟踪引用链条,如果一个对象和GC Roots之间不可达,也就是不存在引用链条,那么即可认为是可回收对象。 JVM 会把虚拟机栈和本地方法栈中正在引用的对象、静态属性引用的对象和常量,作为 GC Roots 。
方法区无用元数据的回收比较复杂,我简单梳理一下。还记得我对类加载器的分类吧,一般来说初始化类加载器加载的类型是不会进行类卸载( unload )的;而普通的类型的卸载,往往是要求相应自定义类加载器本身被回收,所以大量使用动态类型的场合,需要防止元数据区(或者早期的永久代)不会 OOM 。在 8u40 以后的 JDK 中,下面参数已经是默认的:
-XX:+ClassUnloadingWithConcurrentMark
第二,常见的垃圾收集算法,我认为总体上有个了解,理解相应的原理和优缺点,就已经足够了,其主要分为三类:
1.复制(Copying)算法,我前面讲到的新生代GC,基本都是基于复制算法,过程就如 专栏上一讲 所介绍的,将活着的对象复制到to区域,拷贝过程中将对象顺序放置,就可以避免内存碎片化。
这么做的代价是,既然要进行复制,既要提前预留内存空间,有一定的浪费;另外,对于 G1 这种分拆成为大量 region 的 GC ,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,这个开销也不小,不管是内存占用或者时间开销。
2.标记-清除(Mark-Sweep)算法,首先进行标记工作,标识出所有要回收的对象,然后进行清除。这么做除了标记、清除过程效率有限,另外就是不可避免的出现碎片化问题,这就导致其不适合特别大的堆;否则,一旦出现 Full GC ,暂停时间可能根本无法接受。
3.标记-整理(Mark-Compact),类似于标记-清除,但为避免内存碎片化,它会在清理过程中将对象移动,以确保移动后的对象占用连续的内存空间。
注意,这些只是基本的算法思路,实际 GC 实现过程要复杂的多,目前还在发展中的前沿 GC 都是复合算法,并且并行和并发兼备。
垃圾收集过程的理解
在垃圾收集的过程,对应到Eden、Survivor、Tenured等区域会发生什么变化呢?