第七章 GC基础:自动内存管理的哲学
引言
垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。关于垃圾收集有三个经典问题:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
垃圾收集机制是Java的招牌能力,极大地提高了开发效率。如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中。
1. 垃圾回收的基本概念
1.1 什么是垃圾?
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
An object is considered garbage when it can no longer be reached from any pointer in the running program.
如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用,甚至可能导致内存溢出。
1.2 为什么需要GC?
对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完。除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片,将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象。
1.3 Java中垃圾回收的重点区域
flowchart TB
subgraph JVM内存结构
subgraph 堆内存["堆内存(主要GC区域)"]
direction TB
YG["年轻代<br/>- Eden区<br/>- Survivor0<br/>- Survivor1"]
OG["老年代<br/>- 长期存活对象"]
end
subgraph 非堆内存
direction TB
MC["方法区/元空间<br/>- 类信息<br/>- 常量池<br/>- 静态变量"]
PC["程序计数器<br/>(不需要GC)"]
Stack["虚拟机栈<br/>(不需要GC)"]
NMS["本地方法栈<br/>(不需要GC)"]
end
end
subgraph GC频率
direction LR
F1["频繁收集Young区"]
F2["较少收集Old区"]
F3["基本不动方法区"]
end
YG -.->|频繁| F1
OG -.->|较少| F2
MC -.->|很少| F3
classDef gcArea fill:#ffcccc,stroke:#ff0000,stroke-width:2px
classDef noGcArea fill:#ccffcc,stroke:#00ff00,stroke-width:2px
classDef frequency fill:#ffffcc,stroke:#ffaa00,stroke-width:2px
class YG,OG,MC gcArea
class PC,Stack,NMS noGcArea
class F1,F2,F3 frequency
classDiagram
class JVM {
+类装载器子系统
+运行时数据区
+执行引擎
+本地方法接口
+本地方法库
}
class 运行时数据区 {
+方法区
+Java栈
+本地方法栈
+堆
+程序计数器
}
class GC的作用区域 {
<<区域>>
方法区
堆
}
JVM --> 运行时数据区
运行时数据区 --> GC的作用区域
运行时数据区 --> 方法区
运行时数据区 --> Java栈
运行时数据区 --> 本地方法栈
运行时数据区 --> 堆
运行时数据区 --> 程序计数器
JVM --> 执行引擎
JVM --> 本地方法接口
JVM --> 本地方法库
类装载器子系统 --> JVM
垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收。其中,Java堆是垃圾收集器的工作重点。
从次数上讲:
- 频繁收集Young区
- 较少收集Old区
- 基本不动Perm区(或元空间)
2. 垃圾判别算法
在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。
2.1 引用计数算法
引用计数算法比较简单,对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。
原理:
- 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1
- 当引用失效时,引用计数器就减1
- 只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收
优缺点:
- 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性
- 缺点:
- 需要单独的字段存储计数器,增加了存储空间的开销
- 每次赋值都需要更新计数器,增加了时间开销
- 无法处理循环引用的情况(致命缺陷)
2.2 可达性分析算法
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决循环引用的问题,防止内存泄漏的发生。
基本思路:
- 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象
flowchart TB
subgraph "可达性分析算法示例"
direction TB
subgraph "GC Roots"
direction TB
Root1["线程栈变量"]
Root2["静态变量"]
Root3["常量池"]
end
subgraph "堆内存对象"
direction TB
Obj1["对象A"]
Obj2["对象B"]
Obj3["对象C"]
Obj4["对象D"]
Obj5["对象E"]
Obj6["对象F"]
end
Root1 --> Obj1
Root2 --> Obj2
Root3 --> Obj3
Obj1 --> Obj4
Obj2 --> Obj5
subgraph "分析结果"
direction LR
Reachable["可达对象: A,B,C,D,E"]
Unreachable["不可达对象: F"]
end
end
classDef root fill:#FFD700,stroke:#FF8C00,stroke-width:2px
classDef reachable fill:#90EE90,stroke:#006400,stroke-width:2px
classDef unreachable fill:#FFB6C1,stroke:#DC143C,stroke-width:2px
classDef result fill:#E6E6FA,stroke:#4B0082,stroke-width:2px
class Root1,Root2,Root3 root
class Obj1,Obj2,Obj3,Obj4,Obj5 reachable
class Obj6 unreachable
class Reachable,Unreachable result
2.3 GC Roots
在Java语言中,GC Roots包括以下几类元素:
flowchart LR
subgraph "GC Roots详细分类"
direction TB
subgraph "线程相关"
VMStack["虚拟机栈引用<br/>• 方法参数<br/>• 局部变量<br/>• 临时变量"]
NativeStack["本地方法栈引用<br/>• JNI引用对象<br/>• Native方法参数"]
end
subgraph "类和方法区"
StaticRef["静态属性引用<br/>• 类的static变量<br/>• 静态集合对象"]
ConstantRef["常量引用<br/>• 字符串常量池<br/>• Class对象引用"]
end
subgraph "同步和系统"
SyncLock["同步锁持有<br/>• synchronized对象<br/>• 锁监视器"]
JVMInternal["JVM内部引用<br/>• 系统类加载器<br/>• 异常对象<br/>• JMXBean"]
end
end
subgraph "Root判断原则"
Principle["指针保存堆对象<br/>但自身不在堆中<br/>↓<br/>就是GC Root"]
end
VMStack --> Principle
NativeStack --> Principle
StaticRef --> Principle
ConstantRef --> Principle
SyncLock --> Principle
JVMInternal --> Principle
classDef threadRelated fill:#e1f5fe
classDef methodArea fill:#f3e5f5
classDef systemSync fill:#e8f5e8
classDef principle fill:#fff3e0
class VMStack,NativeStack threadRelated
class StaticRef,ConstantRef methodArea
class SyncLock,JVMInternal systemSync
class Principle principle
- 虚拟机栈中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等
- 本地方法栈内JNI引用的对象
- 类静态属性引用的对象:Java类的引用类型静态变量
- 方法区中常量引用的对象:字符串常量池(String Table)里的引用
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用:基本数据类型对应的Class对象,一些常驻的异常对象等
小技巧: 由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。
3. 垃圾清除算法
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间。
3.1 标记-清除算法(Mark-Sweep)
执行过程:
- 标记:Collector从引用根节点开始遍历,标记所有被引用的对象
- 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收
flowchart TB
subgraph "标记-清除算法执行过程"
direction TB
subgraph "步骤1: 初始堆内存状态"
direction LR
A1["对象A"]
B1["对象B"]
C1["对象C"]
D1["对象D"]
E1["对象E"]
end
subgraph "步骤2: 标记阶段 - 从GC Roots开始标记存活对象"
direction LR
A2["对象A ✓"]
B2["对象B"]
C2["对象C ✓"]
D2["对象D"]
E2["对象E ✓"]
end
subgraph "步骤3: 清除阶段 - 回收未标记对象"
direction LR
A3["对象A"]
B3["[空闲]"]
C3["对象C"]
D3["[空闲]"]
E3["对象E"]
end
subgraph "算法特点"
direction TB
Feature1["✓ 不需要移动对象"]
Feature2["✗ 产生内存碎片"]
Feature3["✓ 实现简单"]
Feature4["✗ 效率不稳定"]
end
end
classDef object fill:#87CEEB,stroke:#4682B4,stroke-width:2px
classDef marked fill:#90EE90,stroke:#006400,stroke-width:2px
classDef unmarked fill:#FFB6C1,stroke:#DC143C,stroke-width:2px
classDef free fill:#F5F5F5,stroke:#696969,stroke-width:2px
classDef feature fill:#FFFFE0,stroke:#DAA520,stroke-width:2px
class A1,B1,C1,D1,E1 object
class A2,C2,E2 marked
class B2,D2 unmarked
class A3,C3,E3 marked
class B3,D3 free
class Feature1,Feature2,Feature3,Feature4 feature
缺点:
- 效率比较低:递归与全堆对象遍历两次
- 在进行GC的时候,需要停止整个应用程序,导致用户体验差
- 这种方式清理出来的空闲内存是不连续的,产生内存碎片
3.2 复制算法(Copying)
核心思想: 将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色。
flowchart TD
subgraph "复制算法执行过程"
direction TB
subgraph "步骤1: 初始状态 - Eden区满触发GC"
direction LR
subgraph "Eden区(已满)"
direction TB
A1["对象A"]
B1["对象B"]
C1["对象C"]
D1["对象D"]
end
subgraph "Survivor0(To区)"
direction TB
Empty1["空闲"]
Empty2["空闲"]
end
end
subgraph "步骤2: 复制存活对象到To区"
direction LR
subgraph "Eden区"
direction TB
A2["对象A ✓"]
B2["对象B ✗"]
C2["对象C ✓"]
D2["对象D ✗"]
end
subgraph "Survivor0(To区)"
direction TB
A3["对象A"]
C3["对象C"]
end
end
subgraph "步骤3: 清空Eden区,交换Survivor角色"
direction LR
subgraph "Eden区(已清空)"
direction TB
Empty3["空闲"]
Empty4["空闲"]
Empty5["空闲"]
Empty6["空闲"]
end
subgraph "Survivor0(From区)"
direction TB
A4["对象A"]
C4["对象C"]
end
end
subgraph "算法优势"
direction TB
Advantage1["✓ 无内存碎片"]
Advantage2["✓ 分配效率高"]
Advantage3["✗ 空间利用率50%"]
Advantage4["✓ 适合存活率低的场景"]
end
end
classDef alive fill:#90EE90,stroke:#006400,stroke-width:2px
classDef dead fill:#FFB6C1,stroke:#DC143C,stroke-width:2px
classDef empty fill:#F5F5F5,stroke:#696969,stroke-width:2px
classDef advantage fill:#FFFFE0,stroke:#DAA520,stroke-width:2px
class A1,A2,A3,A4,C1,C2,C3,C4 alive
class B1,B2,D1,D2 dead
class Empty1,Empty2,Empty3,Empty4,Empty5,Empty6 empty
class Advantage1,Advantage2,Advantage3,Advantage4 advantage
优缺点:
- 优点:
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现"碎片"问题
- 缺点:
- 需要两倍的内存空间
- 如果系统中的存活对象很多,复制算法不会很理想
应用场景: 在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间,回收性价比很高。
3.3 标记-整理算法(Mark-Compact)
执行过程:
- 第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象
- 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放
- 之后,清理边界外所有的空间
flowchart TB
subgraph "标记-整理算法执行过程"
direction TB
subgraph "步骤1: 初始状态 - 老年代内存碎片化"
direction LR
A1["对象A"]
B1["对象B"]
C1["对象C"]
D1["对象D"]
E1["对象E"]
F1["对象F"]
end
subgraph "步骤2: 标记阶段 - 标记存活对象"
direction LR
A2["对象A ✓"]
B2["对象B ✗"]
C2["对象C ✓"]
D2["对象D ✗"]
E2["对象E ✓"]
F2["对象F ✗"]
end
subgraph "步骤3: 整理阶段 - 移动存活对象"
direction LR
A3["对象A"]
C3["对象C"]
E3["对象E"]
Free1["空闲"]
Free2["空闲"]
Free3["空闲"]
end
subgraph "步骤4: 最终状态 - 连续的空闲空间"
direction LR
A4["对象A"]
C4["对象C"]
E4["对象E"]
FreeBlock["连续空闲空间"]
end
subgraph "算法特点"
direction TB
Feature1["✓ 消除内存碎片"]
Feature2["✓ 空间利用率高"]
Feature3["✗ 需要移动对象"]
Feature4["✗ 更新引用开销大"]
end
end
classDef object fill:#87CEEB,stroke:#4682B4,stroke-width:2px
classDef alive fill:#90EE90,stroke:#006400,stroke-width:2px
classDef dead fill:#FFB6C1,stroke:#DC143C,stroke-width:2px
classDef free fill:#F5F5F5,stroke:#696969,stroke-width:2px
classDef feature fill:#FFFFE0,stroke:#DAA520,stroke-width:2px
class A1,B1,C1,D1,E1,F1 object
class A2,C2,E2,A3,C3,E3,A4,C4,E4 alive
class B2,D2,F2 dead
class Free1,Free2,Free3,FreeBlock free
class Feature1,Feature2,Feature3,Feature4 feature
优缺点:
- 优点:
- 消除了标记-清除算法当中,内存区域分散的缺点
- 消除了复制算法当中,内存减半的高额代价
- 缺点:
- 效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址
- 移动过程中,需要全程暂停用户应用程序(STW)
3.4 三种算法对比
flowchart LR
A[算法对比] --> B[标记-清除]
A --> C[标记-整理]
A --> D[复制]
B --> B1["速度:中等"]
B --> B2["空间开销:少<br/>(但会堆积碎片)"]
B --> B3["移动对象:否"]
B --> B4["适用:老年代"]
C --> C1["速度:最慢"]
C --> C2["空间开销:少<br/>(不堆积碎片)"]
C --> C3["移动对象:是"]
C --> C4["适用:老年代"]
D --> D1["速度:最快"]
D --> D2["空间开销:2倍大小<br/>(不堆积碎片)"]
D --> D3["移动对象:是"]
D --> D4["适用:新生代"]
classDef markSweep fill:#FFE4E1,stroke:#DC143C,stroke-width:2px
classDef markCompact fill:#E0FFFF,stroke:#008B8B,stroke-width:2px
classDef copying fill:#F0FFF0,stroke:#228B22,stroke-width:2px
classDef comparison fill:#FFF8DC,stroke:#DAA520,stroke-width:2px
class A comparison
class B,B1,B2,B3,B4 markSweep
class C,C1,C2,C3,C4 markCompact
class D,D1,D2,D3,D4 copying
| 算法 | 速度 | 空间开销 | 移动对象 | 适用场景 |
|---|---|---|---|---|
| 标记-清除 | 中等 | 少(但会堆积碎片) | 否 | 老年代 |
| 标记-整理 | 最慢 | 少(不堆积碎片) | 是 | 老年代 |
| 复制 | 最快 | 通常为2倍空间(不堆积碎片) | 是 | 新生代 |
3.5 分代收集算法
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
分代特点:
- 年轻代(Young Gen):
- 区域相对老年代较小,对象生命周期短、存活率低,回收频繁
- 适合使用复制算法,回收整理速度最快
- 老年代(Tenured Gen):
- 区域较大,对象生命周期长、存活率高,回收不及年轻代频繁
- 适合使用标记-清除或标记-整理算法
4. 垃圾回收相关概念
4.1 System.gc()
在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收。
注意事项:
System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用- 一般情况下,垃圾回收应该是自动进行的,无须手动触发
- 开发中不要使用
System.gc(),会导致Stop-the-world的发生
4.2 内存溢出与内存泄漏
内存溢出(OutOfMemoryError)
内存溢出是指垃圾回收已经跟不上内存消耗的速度,没有空闲内存,并且垃圾收集器也无法提供更多内存。
原因:
- Java虚拟机的堆内存设置不够
- 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集
内存泄漏(Memory Leak)
严格来说,只有对象不会再被程序用到了,但是GC又不能回收它们的情况,才叫内存泄漏。
flowchart TB
subgraph "内存泄漏示例"
direction TB
ObjectX["对象X(长生命周期)"] --> ObjectY["对象Y(短生命周期)"]
subgraph "生命周期对比"
direction LR
LifeX["X生命周期: ████████████"]
LifeY["Y生命周期: ████"]
end
Problem["问题:Y生命周期结束后<br/>由于被X引用无法回收"]
end
classDef longLife fill:#FFE4E1,stroke:#DC143C,stroke-width:2px
classDef shortLife fill:#E0FFFF,stroke:#008B8B,stroke-width:2px
classDef problem fill:#FFFFE0,stroke:#DAA520,stroke-width:2px
class ObjectX,LifeX longLife
class ObjectY,LifeY shortLife
class Problem problem
flowchart LR
X[对象X] --> Y[对象Y]
subgraph 生命周期对比
direction TB
X_life[X-生命周期] -->|长于| Y_life[Y-生命周期]
end
问题["Y生命周期结束后,垃圾回收不会回收Y对象"]
Java中内存泄漏的常见情况:
- 静态集合类
- 单例模式
- 内部类持有外部类
- 各种连接未关闭(数据库连接、网络连接和IO连接等)
- 变量不合理的作用域
- 改变哈希值
- 缓存泄漏
- 监听器和回调
4.3 Stop-The-World (STW)
STW指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应。
特点:
- 可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿
- STW事件和采用哪款GC无关,所有的GC都有这个事件
- 哪怕是G1也不能完全避免STW情况发生,只能说垃圾回收器越来越优秀,尽可能地缩短了暂停时间
4.4 垃圾回收的并行与并发
并行(Parallel)
指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
串行(Serial)
相较于并行的概念,单线程执行。如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收。
并发(Concurrent)
指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。
gantt
title 单处理器并发执行示例
dateFormat X
axisFormat %s
section CPU时间片
线程1执行 :t1, 0, 2
线程2执行 :t2, 2, 4
线程3执行 :t3, 4, 6
线程1执行 :t4, 6, 8
线程2执行 :t5, 8, 10
线程3执行 :t6, 10, 12
flowchart LR
A[单处理器] --> B[并发执行]
B --> C[线程1]
B --> D[线程2]
B --> E[线程3]
4.5 四种引用类型
- 强引用(StrongReference):最传统的"引用"的定义,无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
- 软引用(SoftReference):在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收
- 弱引用(WeakReference):被弱引用关联的对象只能生存到下一次垃圾收集之前
- 虚引用(PhantomReference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,唯一目的就是能在这个对象被收集器回收时收到一个系统通知
5. 垃圾收集器分类与评估
5.1 GC分类
按线程数分
- 串行垃圾回收器:同一时间段内只允许有一个CPU用于执行垃圾回收操作
- 并行垃圾回收器:可以运用多个CPU同时执行垃圾回收
gantt
title 串行 vs 并行垃圾回收对比
dateFormat X
axisFormat %s
section 串行GC
GC线程执行 :gc1, 0, 4
应用线程恢复 :app1, 4, 8
section 并行GC
GC线程1 :gc2, 0, 2
GC线程2 :gc3, 0, 2
GC线程3 :gc4, 0, 2
应用线程恢复 :app2, 2, 8
flowchart TB
subgraph 串行
A[GC线程] --> B[应用线程]
end
subgraph 并行
C[GC线程] --> D[应用线程]
C --> E[应用线程]
C --> F[应用线程]
end
note["在单核CPU下并行可能更慢"]
按工作模式分
- 并发式垃圾回收器:与应用程序线程交替工作,以尽可能减少应用程序的停顿时间
- 独占式垃圾回收器:一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束
gantt
title STW vs 并发垃圾回收对比
dateFormat X
axisFormat %s
section STW方式
应用线程运行 :app1, 0, 3
STW-GC执行 :stw, 3, 7
应用线程恢复 :app2, 7, 10
section 并发方式
应用线程运行 :concurrent_app, 0, 10
并发GC执行 :concurrent_gc, 2, 8
按碎片处理方式分
- 压缩式垃圾回收器:会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片
- 非压缩式垃圾回收器:不进行这步操作
5.2 GC评估指标
吞吐量
程序的运行时间与总时间的比例。
flowchart TB
subgraph "吞吐量计算公式"
direction TB
Formula["吞吐量 = 运行用户代码时间 ÷ (运行用户代码时间 + 垃圾收集时间)"]
subgraph "示例计算"
direction TB
Example1["用户代码运行: 95秒"]
Example2["垃圾收集时间: 5秒"]
Result["吞吐量 = 95 ÷ (95 + 5) = 95%"]
end
end
Formula --> Example1
Example1 --> Example2
Example2 --> Result
classDef formula fill:#E6E6FA,stroke:#4B0082,stroke-width:2px
classDef example fill:#F0FFF0,stroke:#228B22,stroke-width:2px
classDef result fill:#FFE4E1,stroke:#DC143C,stroke-width:2px
class Formula formula
class Example1,Example2 example
class Result result
flowchart LR
A["吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)"]
暂停时间
执行垃圾收集时,程序的工作线程被暂停的时间。
flowchart LR
subgraph "暂停时间对比示例"
direction TB
subgraph "低暂停时间GC"
direction LR
A1["应用运行 2s"] --> P1["GC暂停 10ms"]
P1 --> A2["应用运行 2s"]
P2["总暂停: 10ms"]
end
subgraph "高暂停时间GC"
direction LR
B1["应用运行 4s"] --> P3["GC暂停 200ms"]
P3 --> B2["应用运行 4s"]
P4["总暂停: 200ms"]
end
subgraph "暂停时间影响"
direction TB
Impact1["用户体验影响"]
Impact2["响应时间要求"]
Impact3["实时性要求"]
end
end
classDef lowPause fill:#90EE90,stroke:#006400,stroke-width:2px
classDef highPause fill:#FFB6C1,stroke:#DC143C,stroke-width:2px
classDef impact fill:#FFFFE0,stroke:#DAA520,stroke-width:2px
class A1,A2,P1,P2 lowPause
class B1,B2,P3,P4 highPause
class Impact1,Impact2,Impact3 impact
性能权衡:
- 高吞吐量较好,让应用程序的最终用户感觉只有应用程序线程在做"生产性"工作
- 低暂停时间较好,从最终用户的角度来看不管是GC还是其他原因导致应用被挂起始终是不好的
- 现在JVM调优标准:在最大吞吐量优先的情况下,降低停顿时间
5.3 垃圾收集器组合关系
不同的垃圾收集器可以组合使用,但并非所有组合都是有效的:
flowchart TB
subgraph 年轻代收集器
Serial_GC[Serial GC]
ParNew_GC[ParNew GC]
Parallel_Scavenge[Parallel Scavenge GC]
G1_Young[G1 GC]
end
subgraph 老年代收集器
CMS[CMS GC]
Parallel_Old[Parallel Old GC]
Serial_Old[Serial Old GC]
G1_Old[G1 GC]
end
Serial_GC --> Serial_Old
ParNew_GC --> CMS
ParNew_GC --> Serial_Old
Parallel_Scavenge --> Parallel_Old
Parallel_Scavenge --> Serial_Old
G1_Young --> G1_Old
classDef youngGen fill:#E8F5E8,stroke:#228B22,stroke-width:2px
classDef oldGen fill:#FFE4E1,stroke:#DC143C,stroke-width:2px
classDef unified fill:#E6E6FA,stroke:#4B0082,stroke-width:2px
class Serial_GC,ParNew_GC,Parallel_Scavenge youngGen
class CMS,Parallel_Old,Serial_Old oldGen
class G1_Young,G1_Old unified
有效组合:
- Serial + Serial Old
- ParNew + CMS
- ParNew + Serial Old(作为CMS的后备方案)
- Parallel Scavenge + Parallel Old
- Parallel Scavenge + Serial Old
- G1(新生代和老年代统一)
- ZGC(新生代和老年代统一)
注意事项:
- JDK 8默认使用Parallel Scavenge + Parallel Old
- JDK 9开始默认使用G1
- 某些组合在新版本JDK中已被废弃
6. 经典垃圾收集器
6.1 Serial GC:串行回收
概述
- Serial收集器是最基本、历史最悠久的垃圾收集器,JDK1.3之前回收新生代唯一的选择
- Serial收集器作为HotSpot中Client模式下的默认新生代垃圾收集器
- Serial收集器采用复制算法、串行回收和"Stop-the-World"机制的方式执行内存回收
- 除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器
sequenceDiagram
participant CPU0
participant CPU1
participant CPU2
participant CPU3
participant GC as Serial GC线程
loop 正常执行
CPU0->>用户线程1: 运行
CPU1->>用户线程2: 运行
CPU2->>用户线程3: 运行
CPU3->>用户线程4: 运行
end
Note over CPU0,CPU3: Stop The World (STW)
GC->>CPU0: 执行GC
GC->>CPU1: 执行GC
GC->>CPU2: 执行GC
GC->>CPU3: 执行GC
loop 恢复执行
CPU0->>用户线程1: 运行
CPU1->>用户线程2: 运行
CPU2->>用户线程3: 运行
CPU3->>用户线程4: 运行
end
特点
- 优势:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率
- 在用户的桌面应用场景中,可用内存一般不大(几十MB至一两百MB),可以在较短时间内完成垃圾收集(几十ms至一百多ms),只要不频繁发生,使用串行回收器是可以接受的
参数配置
-XX:+UseSerialGC:指定年轻代和老年代都使用串行收集器- 等价于新生代用Serial GC,且老年代用Serial Old GC
6.2 ParNew GC:并行回收
概述
- 如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本
- Par是Parallel的缩写,New:只能处理的是新生代
- ParNew收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别
- ParNew是很多JVM运行在Server模式下新生代的默认垃圾收集器
sequenceDiagram
participant App as 应用线程
participant GC1 as GC线程1
participant GC2 as GC线程2
participant GC3 as GC线程3
participant Heap as 堆内存
Note over App,Heap: ParNew GC执行流程
App->>Heap: 对象分配触发GC
Note over App: STW开始
par 并行标记阶段
GC1->>Heap: 标记存活对象(区域1)
and
GC2->>Heap: 标记存活对象(区域2)
and
GC3->>Heap: 标记存活对象(区域3)
end
par 并行复制阶段
GC1->>Heap: 复制存活对象(区域1)
and
GC2->>Heap: 复制存活对象(区域2)
and
GC3->>Heap: 复制存活对象(区域3)
end
Note over GC1,GC3: 清理Eden和From空间
Note over App: STW结束
App->>Heap: 继续对象分配
性能对比
- 多CPU环境:ParNew收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量
- 单CPU环境:ParNew收集器不比Serial收集器更高效,虽然Serial收集器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销
- 重要特性:除Serial外,目前只有ParNew GC能与CMS收集器配合工作
参数配置
-XX:+UseParNewGC:手动指定使用ParNew收集器执行内存回收任务,表示年轻代使用并行收集器,不影响老年代-XX:ParallelGCThreads:限制线程数量,默认开启和CPU数据相同的线程数
6.3 Parallel Scavenge GC:吞吐量优先
sequenceDiagram
participant App as 应用线程
participant GC1 as GC线程1
participant GC2 as GC线程2
participant GC3 as GC线程3
participant GC4 as GC线程4
participant Heap as 堆内存
participant Monitor as 吞吐量监控
Note over App,Monitor: Parallel Scavenge GC执行流程
App->>Heap: 对象分配触发GC
Monitor->>Monitor: 计算当前吞吐量
Note over App: STW开始
par 自适应并行回收
GC1->>Heap: 标记+复制(区域1)
and
GC2->>Heap: 标记+复制(区域2)
and
GC3->>Heap: 标记+复制(区域3)
and
GC4->>Heap: 标记+复制(区域4)
end
Note over GC1,GC4: 根据吞吐量目标调整线程数
Monitor->>Monitor: 记录GC时间和频率
Note over App: STW结束
Monitor->>Monitor: 自适应调整堆大小
App->>Heap: 继续对象分配
概述
- HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法、并行回收和"Stop the World"机制
- 和ParNew收集器不同,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器
- 自适应调节策略也是Parallel Scavenge与ParNew一个重要区别
- 主要适合在后台运算而不需要太多交互的任务,因此,常见在服务器环境中使用
- 在Java8中,默认是此垃圾收集器
参数配置
-XX:+UseParallelGC:手动指定年轻代使用Parallel并行收集器执行内存回收任务-XX:+UseParallelOldGC:手动指定老年代都是使用并行回收收集器-XX:ParallelGCThreads:设置年轻代并行收集器的线程数-XX:MaxGCPauseMillis:设置垃圾收集器最大停顿时间(即STW的时间)-XX:GCTimeRatio:垃圾收集时间占总时间的比例-XX:+UseAdaptiveSizePolicy:设置Parallel Scavenge收集器具有自适应调节策略
6.4 Serial Old GC:老年代串行回收
sequenceDiagram
participant App as 应用线程
participant GC as GC线程
participant Old as 老年代
participant Heap as 堆内存
Note over App,Heap: Serial Old GC执行流程
App->>Old: 老年代空间不足
Note over App: STW开始 - 暂停所有应用线程
GC->>Old: 1. 标记阶段:遍历所有存活对象
Note over GC: 从GC Roots开始标记
GC->>Old: 2. 整理阶段:移动存活对象
Note over GC: 将存活对象向一端移动
GC->>Old: 3. 清理阶段:清除未标记对象
Note over GC: 清理边界外的所有对象
GC->>Heap: 4. 更新对象引用
Note over GC: 更新所有指向移动对象的引用
Note over App: STW结束 - 恢复应用线程
App->>Old: 继续对象分配
概述
- Serial Old是Serial收集器的老年代版本
- 同样是一个单线程收集器,使用标记-整理算法
- 这个收集器的主要意义也是在于给Client模式下的虚拟机使用
应用场景
- Client模式:作为老年代收集器,与新生代的Serial收集器搭配使用
- Server模式:主要有两个用途:
- 与新生代的Parallel Scavenge配合使用
- 作为老年代CMS收集器的后备垃圾收集方案
6.5 Parallel Old GC:老年代并行回收
sequenceDiagram
participant App as 应用线程
participant GC1 as GC线程1
participant GC2 as GC线程2
participant GC3 as GC线程3
participant GC4 as GC线程4
participant Old as 老年代
participant Heap as 堆内存
Note over App,Heap: Parallel Old GC执行流程
App->>Old: 老年代空间不足
Note over App: STW开始 - 暂停所有应用线程
par 并行标记阶段
GC1->>Old: 标记存活对象(区域1)
and
GC2->>Old: 标记存活对象(区域2)
and
GC3->>Old: 标记存活对象(区域3)
and
GC4->>Old: 标记存活对象(区域4)
end
par 并行整理阶段
GC1->>Old: 整理对象(区域1)
and
GC2->>Old: 整理对象(区域2)
and
GC3->>Old: 整理对象(区域3)
and
GC4->>Old: 整理对象(区域4)
end
Note over GC1,GC4: 同步更新对象引用
Note over App: STW结束 - 恢复应用线程
App->>Old: 继续对象分配
概述
- Parallel Old是Parallel Scavenge收集器的老年代版本
- 使用多线程和标记-整理算法
- 在JDK 1.6中才开始提供
- 在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器
组合优势
- 在程序吞吐量优先的应用场景中,Parallel收集器和Parallel Old收集器的组合,在Server模式下的内存回收性能很不错
- 这个组合的吞吐量甚至还不如ParNew加CMS的组合"给力"
7. GC日志分析
7.1 GC日志参数
-verbose:gc:输出gc日志信息,默认输出到标准输出-XX:+PrintGC:输出GC日志,类似于-verbose:gc-XX:+PrintGCDetails:在发生垃圾回收时打印内存回收详细的日志-XX:+PrintGCTimeStamps:输出GC发生时的时间戳-Xloggc:<file>:把GC日志写入到一个文件中去
7.2 GC日志分析工具
GCeasy
- 官网地址:gceasy.io/
- 在线的GC日志分析器
- 可以通过GC日志分析进行内存泄漏检测、GC暂停原因分析、JVM配置建议优化等功能
GCViewer
- 免费的、开源的分析小工具
- 用于可视化查看由SUN/Oracle、IBM、HP和BEA Java虚拟机产生的垃圾收集器的日志
8. 总结
8.1 核心要点回顾
垃圾回收是Java虚拟机自动内存管理的核心机制,本章我们深入学习了:
垃圾判别算法:
- 引用计数算法:简单但无法处理循环引用
- 可达性分析算法:通过GC Roots判断对象可达性,是主流方案
垃圾清除算法:
- 标记-清除:基础算法,会产生内存碎片
- 复制算法:效率高但空间利用率低,适用于新生代
- 标记-整理:解决碎片问题但效率较低,适用于老年代
基础垃圾收集器:
- Serial系列:单线程,适用于Client模式
- ParNew:多线程版本的Serial,可与CMS配合
- Parallel系列:注重吞吐量,JDK8默认选择
8.2 实践指导原则
-
选择合适的垃圾收集器:根据应用场景选择最适合的GC组合
- 桌面应用:Serial + Serial Old
- 服务器应用:Parallel Scavenge + Parallel Old
- 低延迟要求:ParNew + CMS
-
优化GC参数:通过调整参数提升应用性能
- 合理设置堆大小和分代比例
- 根据应用特点调整GC触发条件
- 监控GC频率和停顿时间
-
分析GC日志:及时发现和解决内存问题
- 关注GC频率和停顿时间
- 分析内存使用模式
- 识别内存泄漏征象
-
避免内存泄漏:编写更高质量的Java代码
- 及时释放不需要的对象引用
- 注意静态集合的使用
- 合理使用缓存机制
8.3 技术发展趋势
垃圾回收技术仍在不断发展,从传统的Serial、Parallel收集器,到低延迟的CMS、G1,再到新一代的ZGC、Shenandoah,每一代技术都在追求:
- 更高的吞吐量:减少GC开销,提升应用性能
- 更低的延迟:缩短停顿时间,改善用户体验
- 更好的可预测性:提供稳定可控的性能表现
- 更简单的调优:减少参数配置的复杂性
理解这些基础概念和原理,将为我们深入学习高级垃圾收集器(如G1、ZGC等)打下坚实的基础,也为我们在实际项目中进行性能调优提供理论支撑。