文章目录
-
- 垃圾回收机制
- Stop-the-World
- 垃圾收集器
-
- 垃圾收集器分类
- [Serial 收集器](#Serial 收集器)
- [Serial Old 收集器](#Serial Old 收集器)
- [ParNew 收集器](#ParNew 收集器)
- [Parallel Scavenge 收集器](#Parallel Scavenge 收集器)
- [Parallel Old 收集器](#Parallel Old 收集器)
- [CMS 收集器](#CMS 收集器)
-
- [CMS 收集器缺点](#CMS 收集器缺点)
- [G1 收集器](#G1 收集器)
-
- [G1 收集器特点](#G1 收集器特点)
- [G1 收集器的分代理念](#G1 收集器的分代理念)
- [G1 收集器运作过程](#G1 收集器运作过程)
垃圾回收机制
垃圾回收 (Garbage Collection,GC
),顾名思义就是释放垃圾占用的空间 ,当需要排查各种内存溢出问题、当垃圾收集成为系统达到更高并发的瓶颈时,我们就需要对这些"自动化"的技术实施必要的监控和调节。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
Stop-the-World
"Stop The World
"是 Java 垃圾收集中的一个重要概念。在垃圾收集过程中,JVM 会暂停所有的用户线程,这种暂停被称为"Stop The World"事件。这么做的主要原因是为了防止在垃圾收集过程中,用户线程修改了堆中的对象,导致垃圾收集器无法准确地收集垃圾。
"Stop The World
"事件会对 Java 应用的性能产生影响。如果停顿时间过长,就会导致应用的响应时间变长,对于对实时性要求较高的应用,如交易系统、游戏服务器等,这种情况是不能接受的。
因此,在选择和调优垃圾收集器时,需要考虑其停顿时间。Java 中的一些垃圾收集器,如 G1
和 ZGC
(下文有详细讲解),都会尽可能地减少了"Stop The World
"的时间,通过并发的垃圾收集,提高应用的响应性能。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java 垃圾收集器(Garbage Collector, GC)是 Java 虚拟机(JVM)的一部分,它自动管理内存,回收不再使用的对象所占用的内存空间。这有助于防止内存泄漏,并且使得开发人员可以更专注于业务逻辑的编写而不是内存管理。
没有万能的垃圾收集器,只有根据具体应用场景选择适合自己的垃圾收集器 。垃圾收集器是垃圾回收算法(如引用计数法、标记清除法、标记整理法、复制算法等)的具体实现。它的主要任务是识别并回收那些不再被程序使用的对象所占用的内存空间,从而避免内存泄漏和内存溢出的问题。
垃圾收集器分类
就目前来说,JVM 的垃圾收集器主要分为两大类:分代收集器 和分区收集器 ,分代收集器的代表是 CMS
,分区收集器的代表是 G1
和 ZGC
JDK 默认垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version
命令查看):
- JDK 8:
Parallel Scavenge
(新生代)+Parallel Old
(老年代) - JDK 9 ~ JDK20:
G1
Serial 收集器
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器。此收集器是一个单线程收集器了。
它的 "单线程" 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程 ( "STW Stop The World" ),直到它收集结束。
新生代采用标记-复制算法,老年代采用标记-整理算法。
Serial 收集器简单而高效(与其他收集器的单线程相比)。由于没有线程交互的开销,自然可以获得很高的单线程收集效率,对于运行在 Client 模式下的虚拟机来说是个不错的选择
Serial Old 收集器
Serial 收集器的老年代版本 ,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge
收集器搭配使用,另一种用途是作为 CMS
收集器的后备方案。
ParNew 收集器
ParNew 收集器是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
新生代采用标记-复制算法,老年代采用标记-整理算法。
并行和并发概念补充:
-
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
-
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。
Parallel Scavenge 收集器
Parallel Scavenge 收集器也是多线程收集器 ,其关注点是吞吐量(高效率的利用 CPU)。
是 JDK1.8 的默认收集器,可以使用
java -XX:+PrintCommandLineFlags -version
命令查看
吞吐量(Throughput)就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。
Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 此收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用标记-复制算法,老年代采用标记-整理算法。
Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本 。使用多线程和"标记-整理"算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge
收集器和 Parallel Old
收集器
CMS 收集器
CMS(Concurrent Mark Sweep)收集器 是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器 ,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的 Mark Sweep
这两个词可以看出,CMS 收集器是由 标记-清除
算法实现的。它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
-
初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快
-
并发标记: 同时开启 GC 和用户线程 ,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。
因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
-
重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
-
并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
这是一款优秀的垃圾收集器:并发收集、低停顿。但是它有下面三个明显的缺点:
CMS 收集器缺点
-
对 CPU 资源非常敏感,因此在 CPU 资源紧张的情况下,
CMS
的性能会大打折扣。默认情况下,CMS
启用的垃圾回收线程数是(CPU数量 + 3)/4
,当 CPU 数量很大时,启用的垃圾回收线程数占比就越小。但如果 CPU 数量很小,例如只有 2 个 CPU,垃圾回收线程占用就达到了 50%,这极大地降低系统的吞吐量,无法接受。 -
CMS
采用的是「标记-清除」算法,会产生大量的内存碎片,导致空间不连续,当出现大对象无法找到连续的内存空间时,就会触发一次Full GC
,这会导致系统的停顿时间变长。 -
CMS
无法处理浮动垃圾,当CMS
在进行垃圾回收的时候,应用程序还在不断地产生垃圾,这些垃圾会在CMS
垃圾回收结束之后产生,这些垃圾就是浮动垃圾,CMS
无法处理这些浮动垃圾,只能在下一次 GC 时清理掉。
G1 收集器
G1 (Garbage-First) 是一款面向服务器的垃圾收集器 ,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。引入了基于区域(Region)的垃圾回收策略。它是从 JDK 1.7 版本开始引入的。
G1垃圾收集器的主要目标是实现更短的停顿时间和更高的吞吐量。
G1 收集器特点
被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征,在 JDK 9 时取代 CMS
成为了默认的垃圾收集器。它具备以下特点:
-
增量 :
G1
可以以增量方式执行垃圾回收,这意味着它不需要一次性回收整个堆空间,而是可以逐步、增量地清理。有助于控制停顿时间,尤其是在处理大型堆时。 -
并行与并发 :
G1
能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短Stop-The-World
停顿时间。部分其他收集器原本需要停顿 Java 线程执行的GC
动作,G1
收集器仍然可以通过并发的方式让 java 程序继续执行。 -
分代收集 :虽然
G1
可以不需要其他收集器配合就能独立管理整个GC
堆,但是还是保留了分代的概念。 -
空间整合 :与
CMS
的"标记-清除"算法不同,G1 从整体来看是基于"标记-整理"算法实现 的收集器;从局部上来看是基于"标记-复制"算法实现的。 -
可预测的停顿 :这是
G1
相对于CMS
的另一个大优势,降低停顿时间是G1
和CMS
共同的关注点,但G1
除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
G1 收集器的分代理念
G1 是基于分代的思想进行设计的。它将堆内存分为多个大小相等的区域(Region),每个区域都可以是 Eden 区、Survivor 区或者 Old 区。
可以通过 -XX:G1HeapRegionSize=n
来设置 Region 的大小,可以设定为 1M、2M、4M、8M、16M、32M(不能超过)。
G1
有专门分配大对象的 Region
叫 Humongous
区,而不是让大对象直接进入老年代的 Region
中。在 G1
中,大对象的判定规则就是一个大对象超过了一个 Region
大小的 50% ,比如每个 Region
是 2M,只要一个对象超过了 1M,就会被放入 Humongous
中,而且一个大对象如果太大,可能会横跨多个 Region
来存放。
G1 收集器运作过程
它的设计思想是将堆内存划分为多个大小相等的区域 (Region),每个区域都可以是Eden
、Survivor
或 Old
区域。G1垃圾收集器通过并发、增量和并行的方式,以区域为粒度进行垃圾回收,其工作过程如下:
-
初始标记(Initial Mark):G1垃圾收集器会首先标记出GC Roots能直接关联到的对象,并记录下这些对象的存活状态。在此阶段,应用程序的执行会停顿下来。
-
并发标记(Concurrent Marking):G1垃圾收集器并发进行标记工作,在应用程序运行的同时,标记剩余的存活对象。在这个阶段,G1会进行跨区域的引用扫描,标记存活对象。
-
最终标记(Final Mark):在并发标记阶段结束后,G1会做一次最终标记来修正并发标记期间有可能发生的引用变化。该阶段的停顿时间会较短。
-
筛选回收 (Live Data Counting):G1根据各个区域内的垃圾量和存活对象数量等信息,选择最有价值的区域进行垃圾收集。这个阶段被称为G1的"Garbage-First"策略。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region (Garbage-First)
这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)
- 并发清理(Concurrent Cleanup) :G1进行并发 的垃圾清理工作,在应用程序运行的同时,回收垃圾区域中的无用对象。
G1垃圾收集器在整个垃圾回收过程中,会控制垃圾回收的停顿时间,尽量减少对应用程序的影响。它可以根据应用程序的需要动态调整各个阶段的时间比例,以达到更好的性能和吞吐量