实际上,垃圾收集器(GC,Garbage Collector)是和具体 JVM 实现紧密相关的,不一样厂商(IBM、Oracle),不一样版本的JVM,提供的选择也不一样。接下来,我来谈谈最主流的 Oracle JDK。java
它是最古老的垃圾收集器,“Serial”体如今其收集工做是单线程的,而且在进
行垃圾收集过程当中,会进入臭名昭著的“Stop-The-World”状态。固然,其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,因此一直是 Client 模式
下 JVM 的默认选项。
从年代的角度,一般将其老年代实现单独称做 Serial Old,它采用了标记 - 整理(Mark-Compact)算法,区别于新生代的复制算法。
Serial GC 的对应 JVM 参数是:算法
-XX:+UseSerialGC
很明显是个新生代 GC 实现,它实际是 Serial GC 的多线程版本,最多见的应
用场景是配合老年代的 CMS GC 工做,下面是对应参数服务器
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
基于标记 - 清除(Mark-Sweep)算法,设计目标是尽可能减小停顿时间,这一点对于 Web 等反应时间敏感的应用很是重要,一直到今天,仍然有不少系统使用 CMS GC。可是,CMS 采用的标记 - 清除算法,存在着内存碎片化问题,因此难以免在长时间运行等状况下发生 full GC,致使恶劣的停顿。另外,既然强调了并发(Concurrent),CMS 会占用更多 CPU 资源,并和用户线程争抢。数据结构
在早期 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 之间是复制算法,但总体上实际可看做是标记 - 整理(MarkCompact)算法,能够有效地避免内存碎片,尤为是当
Java 堆很是大的时候,G1 的优点更加明显。less
G1 吞吐量和停顿表现都很是不错,而且仍然在不断地完善,与此同时 CMS 已经在 JDK 9 中被标记为废弃(deprecated),因此 G1 GC 值得你深刻掌握。性能
自动垃圾收集的前提是清楚哪些内存能够被释放。即如何判断一个对象是否能够回收。
主要就是两个方面,最主要部分就是对象实例,都是存储在堆上的;还有就是方法区中的元数据等信息,例如类型再也不使用,卸载该 Java 相似乎是很合理的。测试
对于对象实例收集,主要是两种基本算法,引用计数和可达性分析。云计算
引用计数算法,顾名思义,就是为对象添加一个引用计数,用于记录对象被引用的状况,若是计数为0,即表示对象可回收。这是不少语言的资源回收选择,例如因人工智能而更加火热的Python,它更是同时支持引用计数和垃圾收集机制。具体哪一种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提升吞吐量的尝试。
Java 并无选择引用计数,是由于其存在一个基本的难题,也就是很难处理循环引用关系。
另外就是 Java 选择的可达性分析,Java 的各类引用关系,在某种程度上,将可达性问题还进一步复杂化,这种类型的垃圾收集一般叫做追踪性垃圾收集(Tracing Garbage Collection)。其原理简单来讲,就是将对象及其引用关系看做一个图,选定活动的对象做为 GC Roots,而后跟踪引用链条,若是一个对象和 GC Roots 之间不可达,也就是不存在引用链条,那么便可认为是可回收对象。JVM 会把虚拟机栈和本地方法栈中正在引用的对象、静态属性引用的对象和常量,做为 GC Roots。
方法区无用元数据的回收比较复杂,简单梳理一下。通常来讲初始化类加载器加载的类型是不会进行类卸载(unload)的;而普通的类型的卸载,每每是要
求相应自定义类加载器自己被回收,因此大量使用动态类型的场合,须要防止元数据区(或者早期的永久代)不会 OOM。在 8u40 之后的 JDK 中,下面参数已是默认的:
-XX:+ClassUnloadingWithConcurrentMark
主要分为三类。
我前面讲到的新生代 GC,基本都是基于复制算法,将活着的对象复制到 to 区域,拷贝过程当中将对象顺序放置,就能够避免内存碎片化。
这么作的代价是,既然要进行复制,既要提早预留内存空间,有必定的浪费;另外,对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 须要维护 region 之间对象引用关系,这个开销也不小,无论是内存占用或者时间开销。
首先进行标记工做,标识出全部要回收的对象,而后进行清除。这么作除了标记、清除过程效率有限,另外就是不可避免的出现碎片化问题,这就致使其不适合特别大的堆;不然,一旦出现 Full GC,暂停时间可能根本没法接受。
相似于标记 - 清除,但为避免内存碎片化,它会在清理过程当中将对象移动,以确保移动后的对象占用连续的内存空间。
注意,这些只是基本的算法思路,实际 GC 实现过程要复杂的多,目前还在发展中的前沿 GC 都是复合算法,而且并行和并发兼备。
若是对这方面的算法有兴趣,能够参考一本比较有意思的书《垃圾回收的算法与实现》,虽然其内容并非围绕 Java 垃圾收集,可是对通用算法讲解比较形象。
第一,Java 应用不断建立对象,一般都是分配在 Eden 区域,当其空间占用达到必定阈值时,触发 minor GC。仍然被引用的对象(绿色方块)存活下来,被复制到 JVM 选择的 Survivor 区域,而没有被引用的对象(黄色方块)则被回收。注意,我给存活对象标记了“数字 1”,这是为了代表对象的存活时间。
第二, 通过一次 Minor GC,Eden 就会空闲下来,直到再次达到 Minor GC 触发条件,这时候,另一个 Survivor 区域则会成为 to 区域,Eden 区域的存活对象和 From 区域对象,都会被复制到 to 区域,而且存活的年龄计数会被加 1。
第三, 相似第二步的过程会发生不少次,直到有对象年龄计数达到阈值,这时候就会发生所谓的晋升(Promotion)过程,以下图所示,超过阈值的对象会被晋升到老年代。这个阈值是能够经过参数指定:
-XX:MaxTenuringThreshold=<N>
后面就是老年代 GC,具体取决于选择的 GC 选项,对应不一样的算法。下面是一个简单标记 - 整理算法过程示意图,老年代中的无用对象被清除后, GC 会将对象进行整理,以防止内存碎片化。
一般咱们把老年代 GC 叫做 Major GC,将对整个堆进行的清理叫做 Full GC,可是这个也没有那么绝对,由于不一样的老年代 GC 算法其实表现差别很大,例如 CMS,“concurrent”就体如今清理工做是与工做线程一块儿并发运行的。
GC 仍然处于飞速发展之中,目前的默认选项 G1 GC 在不断的进行改进,不少咱们原来认为的缺点,例如串行的 Full GC、Card Table 扫描的低效等,都已经被大幅改进,例如, JDK 10 之后,Full GC 已是并行运行,在不少场景下,其表现还略优于 Parallel GC 的并行 Full GC 实现。
即便是 Serial GC,虽然比较古老,可是简单的设计和实现未必就是过期的,它自己的开销,无论是 GC 相关数据结构的开销,仍是线程的开销,都是很是小的,因此随着云计算的兴起,在Serverless 等新的应用场景下,Serial GC 找到了新的舞台。
比较不幸的是 CMS GC,由于其算法的理论缺陷等缘由,虽然如今还有很是大的用户群体,可是已经被标记为废弃,若是没有组织主动承担 CMS 的维护,颇有可能会在将来版本移除。
若是你有关注目前尚处于开发中的 JDK 11,你会发现,JDK 又增长了两种全新的 GC 方式,分别是:
Epsilon GC
简单说就是个不作垃圾收集的 GC,彷佛有点奇怪,有的状况下,例如在进行
性能测试的时候,可能须要明确判断 GC 自己产生了多大的开销,这就是其典型应用场景。
ZGC
这是 Oracle 开源出来的一个超级 GC 实现,具有使人惊讶的扩展能力,好比支持 T bytes 级别的堆大小,而且保证绝大部分状况下,延迟都不会超过 10ms。虽然目前还处于实验阶段,仅支持 Linux 64 位的平台,但其已经表现出的能力和潜力都很是使人期待。
固然,其余厂商也提供了各类独具一格的 GC 实现,例如比较有名的低延迟 GC,Zing和Shenandoah等。
参考资料:
第27讲 | Java常见的垃圾收集器有哪些?