JVM GC 概览
什么是 GC
GC(Garbage Collection,垃圾回收)是一种自动内存管理机制,用于自动回收不再使用的内存资源。它不是 Java 独有的机制,C#、Go 等语言的虚拟机也实现了 GC。
GC 为程序带来的好处是:可以避免复杂困难、高风险的手动内存管理,减轻开发者的负担。
GC 也有坏处:无法适应嵌入式开发或者高性能应用开发。这些场景下额外的内存消耗和 CPU 占用是无法接受的。GC 也会带来一定的延时,可能会影响实时性,比较难适用于游戏等高实时性场景。
GC 算法
垃圾判别算法:
- 引用计数法:该算法认为,没有引用指向的对象,就是垃圾。这种算法无法避免循环引用造成的内存泄漏。
- 可达性分析算法:通过从某个 Root 出发,遍历能够通过引用达到的所有对象,当所有 Root 被遍历完,没有被遍历到的对象则视为垃圾。这种算法被采用得比较普遍。
垃圾回收算法:
Java 的一些回收器会对堆内存进行分代,分别命名新生代和老年代,新生代用于存放新创建的对象和在 GC 中存活次数未到达阈值的对象,老年代用于存放存活次数超过阈值的对象。
- 标记清除算法:通过可达性分析对对象进行标记,随后清除未标记的对象。这种算法回收的速度很快,但是会导致内存碎片化。
- 标记整理算法:在标记清除的基础上,将存活的对象向一端移动。这种算法不会导致内存碎片化,但是速度较慢。通常用于老年代的回收,因为老年代的对象存活率高,不易导致大规模整理。
- 复制算法:将内存分为两块区域,一部分放被 GC 的对象,另一部分放存活的对象,每次对被 GC 的部分进行可达性分析,将存活的对象直接依次放入存活区域,从而避免数据整理的开销。通常用于新生代的垃圾回收。一般新生区每次 GC 会死亡 80% ~ 90% 的对象,因此 Java 默认对新生代区域做出 8:1:1 (Eden、Survivor0、Survivor1) 的划分,Survivor0、Survivor1 会轮流作为存活对象的存储区域。
垃圾回收器
并行和并发
并行(Parallel):指多条垃圾收集线程并行工作,但用户线程处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行。
吞吐量(Throughput)
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即
吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
Minor GC 和 Full GC
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,回收速度也比较快。
老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
新生代垃圾回收器
- Serial New 串行回收器。
采用复制算法。适用于单核嵌入设备。它是一个单线程收集器,在进行垃圾收集时,必须暂停其他所有的工作线程。
Serial 收集器也是 HotSpot 虚拟机运行在 Client 模式下的默认的新生代收集器。在用户的桌面应用场景中,分配给虚拟机管理的内存一般不会很大,停顿时间完全可以控制在几十毫秒最多一百毫秒以内。这种情况下,简单高效的单线程收集对 CPU 的利用效率更高。 - Parallel New 并行回收器。
除了并行执行回收任务之外,与 Serial New 别无二致。回收仍然产生 STW(Stop The World,GC 导致整个进程暂停的时间)。单核情况、核心数量少的情况下效率比 Serial 更低,核心数量多的情况下则更高效。 - Parallel Scavenge 回收器。
与 Parallel New 相同,使用复制算法、多线程并行执行。Parallel Scavenge 专注于提高吞吐量,允许更长的 STW 时间,但能让应用长时间无干扰运行,适合在后台运算而不需要太多交互的应用。Parallel Scavenge 支持 GC 自适应调节(使用参数 -XX:+UseAdaptiveSizePolicy 开启),JVM 可以自动调整 GC 参数(Eden / Survivor 比例、晋升阈值等),以实现吞吐量控制。
老年代垃圾回收器
- Serial Old 收集器。
使用标记-整理算法,单线程执行回收。此收集器的主要意义也是单核嵌入式设备或者 Client 模式下的虚拟机使用。如果在Server 模式下,它还有两大用途:① 在JDK1.5 以及之前版本(Parallel Old诞生以前)是 Parallel Scavenge 的唯一搭配选择。② 作为CMS收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。 - Parallel Old 收集器
使用标记-整理算法,多线程并行执行。常与 Parallel Scavenge 配合使用,在注重吞吐量以及CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。 - CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短停顿时间为目标的收集器,特点是流程中耗时较长的部分并发执行。它适合重视服务的响应速度的互联网应用。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。CMS 的执行流程分为以下四步:
① 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要 STW。
② 并发标记:从初始标记的对象开始,追踪所有可达对象,在整个过程中耗时最长。
③ 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要 STW。
④ 并发清除:因为是并发执行的,所以无法整理,只能清除。
CMS 的缺点很明显,一方面,并发执行对 CPU 资源很敏感,如果 CPU 性能不足,即便程序不会有明显的停顿,仍然会感受到运行缓慢严重,吞吐量下降。另一方面,标记清除算法会带来严重的内存碎片化,最终仍需要 Full GC 作为碎片化过度的对策。 - G1 收集器
全称 Garbage First GC,于 Java 7 引入,在 Java 9 取代 CMS 成为默认垃圾收集器。G1 的特点是 STW 时间可预测、内存碎片比 CMS 更少。G1 并不是专门的老年代回收器,它同时负责老年代和新生代的全部 GC 任务。G1 对堆内存分代不再是物理上的,而是逻辑上的。它将堆内存分为若干 Region,这些 Region 初始为空闲区域,在运行时被按需分配为不同类型(Eden、Survivor0/1、大型对象存储区、老年代区)。对于 Eden、Survivor0/1,采用复制算法,使用一片空闲 Region 放存活的对象,被 GC 的 Region 在回收完成后释放为空闲 Region。每当新生代内存耗尽,将暂停用户线程并执行标记和复制算法。当老年代内存空间达到阈值,与 CMS 相同,执行初始标记、并发标记、重新标记。标记完成后,优先选择存活数量少的区域释放,直到 STW 时间耗尽。在这个过程中,新生代也会被 GC,因此这个过程被称为 "mixed GC"。与 CMS 相同,如果并行标记的速度赶不上内存消耗的速度,会触发 Full GC。 - ZGC 收集器
超大型堆 JVM 使用的低延迟回收器。垃圾回收全程几乎全部并发,STW(Stop The World) 时间很短,适用于超大堆、低延迟场景。缺点是多线程调度会产生很高的 CPU 开销,复杂的设计也导致内存占用升高。
有哪些 GC Roots
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 静态属性引用的对象
- 常量对象