垃圾收集器(GC, Garbage Collector)与具体 JVM 实现有关。不同厂商,不同版本的 JVM,其所提供的 GC 也不同。
1. 常见GC
1.1 Serial GC
它是 JVM 在 client 模式下的默认设置。
“serial”体现在其收集活动是单线程的,且会进入 STW(Stop-The-World)状态。
特点是暂用资源少。
它采用 “标记-整理”算法(Mark - Compact)
JVM参数:-XX:+UseSerialGC
1.2 ParNew GC
针对新生代的GC。它是 Serial GC 的多线程版本。
一般配合老年代的 CMS GC 使用。如:
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
1.3 CMS GC (Concurrent Mark Sweep)
基于 “标记-清除” 算法(Mark - Sweep)。其设计目标是尽量减少停顿时间。
它的缺点是会产生内存碎片。应用长时间运行情况下会产生 Full GC,停顿严重。
另外,因为是并发模式进行的,CMS 会占用更多CPU资源(与用户线程争抢)
在 JDK9 中,该GC已被标记为废弃(deprecated)
1.4 Parallel GC
它是 JDK8 中 JVM 在 server 模式下的默认设置;被称为 “吞吐量优先” 的GC。
特点是,新生代和老年代 GC 都是并行的,在常见服务器环境中更高效。
JVM参数:
开启Parallel GC:-XX:+UseParallelGC
设置最长暂停时间:-XX:MaxGCPauseMillis=value
设置GC时间和用户时间比:-XX:GCTimeRatio=value
1.5 G1 GC
兼顾吞吐量和停顿时间的GC。这是 JDK9 之后 server 模式下的默认设置。
G1 未必能做到 CMS 在最好情况下的延时停顿,但最差情况要好得多。
G1 的堆内存分配有年代之分,但不是简单的条带式划分,而是一个个 Region。
Region 之间采用复制算法,但整体上是 “标记 - 整理” 算法,可以有效避免内存碎片。
当堆容量非常大时(大于 16G),G1 的优势很明显。
总体而言 G1 的吞吐量和停顿表现都不错,值得使用。
2. 垃圾收集的原理
2.1 识别哪些内存可以被释放
2.1.1 对于堆中的对象
有两种基本算法来判断某个对象是否可以被回收:
2.1.1.1 引用计数
记录每个对象被引用的数量(扇入);当该计数为0时,表示它可以被回收。
因为很难处理循环引用的场景,Java 采用的是可达性分析
2.1.1.2 可达性分析
将所有对象及其引用关系当作一个图;
把其中活动的对象作为 GC Roots (根引用);
通过追踪引用链判断一个对象是否从 GC Roots 可达;如果不可达,则表示该对象可以被回收。
JVM中,虚拟机栈和本地方法栈中的对象、静态属性引用的对象和常量,都属于 GC Roots
2.1.2 对于方法区中的元数据
回收类只能通过回收相应的类加载器实现;
而初始化类加载器只有在退出JVM时才回收,所以一般都是回收被自定义类加载器加载类。
一般在大量使用动态类型的场景下才会有必要回收类,以防止元数据区 OOM。
而且JDK 8u40 以后 G1 默认开启类卸载,所以总体来说不用太担心这方面的问题:
-XX:+ClassUnloadingWithConcurrentMark
(可通过设置 JVM 参数 -XX:+TraceClassUnloading 查看类型卸载信息)
2.2 常见基本垃圾收集算法
注:现实中 GC 所采用的算法是复合算法,且有并行和并发
2.2.1 复制
将活着的对象复制到另一块区域,且在这个过程中将这些对象放到连续内存空间,以此减少内存碎片。
缺点:因为要预留一块内存进行复制,这块内存区域不能用于实际的业务,所以有一定的浪费。
在 G1 GC 中还需额外维护 Region 之间的对象引用关系。
2.2.2 标记 - 清除 (Mark - Sweep)
先将所有需要被回收的对象标记出来,然后再进行清除。
缺点:容易出现内存碎片,Full GC 时暂停时间过长,不适合大堆。
2.2.3 标记 - 整理(Mark - Compact)
与 “标记 - 清除” 类似。但它会在清理过程中移动对象,使对象占用连续的内存空间。这样可以减少内存碎片。