图解 JVM 垃圾回收(一)
- 1.前言
- 1.1 什么是垃圾
- 1.2 内存溢出和内存泄漏
- 2.垃圾回收的定义与重要性
- 3.GC 判断策略
- 3.1 引用计数算法
- 3.2 可达性分析算法
- 4.引用类型
- 5.垃圾回收算法
- 5.1 标记-复制(Copying)
- 5.2 标记-清除(Mark-Sweep)
- 5.3 标记-整理(Mark-Compact)
- 5.4 分代收集理论
- 5.5 垃圾回收阶段算法小结
1.前言
1.1 什么是垃圾
垃圾 是指运行程序中 没有任何引用指向的对象,需要被回收。
1.2 内存溢出和内存泄漏
内存溢出:经过垃圾回收之后,内存仍旧无法存储新创建的对象,内存不够溢出。
内存泄漏:又叫 “存储泄漏”,对象不会再被程序使用了,但是 GC 又不能回收它们。例如:IO 流不适用了但是没有被 Close、数据库连接 JDBC 没有被 Close。这些对象不会被回收就会占据内存,大量的此类对象存在,也是导致内存溢出的原因。
2.垃圾回收的定义与重要性
垃圾回收(Garbage Collection
,简称 GC
)是内存管理的核心组成部分,它负责自动回收不再使用的内存空间。在 Java 中,程序员不需要手动释放对象占用的内存,一旦对象不再被引用,垃圾回收器就会在适当的时机回收它们所占用的内存。这样可以避免 内存泄漏 和 野指针,从而大大减轻了程序员的负担,也使得 Java 成为一个相对安全、易于开发的编程语言。
- 防止内存泄漏:手动管理内存容易导致内存泄漏,而 GC 可以自动回收不再使用的对象,防止内存泄漏的发生。
- 提高开发效率:程序员不再需要关心内存释放的问题,可以更加集中精力在业务逻辑的实现上。
- 系统性能和稳定性:通过有效的垃圾回收策略,可以保证系统的性能和稳定性。
垃圾回收的基本步骤分两步:
- 1️⃣ 查找内存中不再使用的对象(GC 判断策略)
- 2️⃣ 释放这些对象占用的内存(GC 收集算法)
3.GC 判断策略
3.1 引用计数算法
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
public class ReferenceCountingGC {
public Object instance = null;
public static void main(String[] args) {
ReferenceCountingGC objectA = new ReferenceCountingGC();
ReferenceCountingGC objectB = new ReferenceCountingGC();
objectA.instance = objectB;
objectB.instance = objectA;
}
}
3.2 可达性分析算法
通过 GC Roots
作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
哪些对象可以作为 GC Roots
呢?
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
JNI
(Java Native Interface
)引用的对象
public void method() {
Object localVariable = new Object(); // localVariable 是 GC Roots
}
public class MyClass {
private static Object staticObject = new Object(); // staticObject 是 GC Roots
}
public class MyClass {
private static final String CONSTANT_STRING = "constant"; // CONSTANT_STRING 是 GC Roots
}
public synchronized void synchronizedMethod() {
// 当前对象(this)在执行同步方法时是 GC Roots
}
4.引用类型
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
Java 中有四种类型的引用,它们对垃圾回收的影响不同:
- 强引用(
Strong Reference
):最常见的引用类型,只要对象有强引用指向,它就不会被垃圾回收。 - 软引用(
Soft Reference
):软引用可以帮助垃圾回收器回收内存,只有在内存不足时,软引用指向的对象才会被回收。 - 弱引用(
Weak Reference
):弱引用指向的对象在下一次垃圾回收时会被回收,不管内存是否足够。 - 虚引用(
Phantom Reference
):虚引用的主要用途是跟踪对象被垃圾回收的状态,虚引用指向的对象总是可以被垃圾回收。
import java.lang.ref.*;
public class ReferenceTypes {
public static void main(String[] args) {
Object strongRef = new Object(); // 强引用
SoftReference<Object> softRef = new SoftReference<>(new Object()); // 软引用
WeakReference<Object> weakRef = new WeakReference<>(new Object()); // 弱引用
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), new ReferenceQueue<>()); // 虚引用
System.gc(); // 触发垃圾回收
System.out.println("Strong Reference: " + strongRef);
System.out.println("Soft Reference: " + softRef.get());
System.out.println("Weak Reference: " + weakRef.get());
System.out.println("Phantom Reference: " + phantomRef.get());
}
}
5.垃圾回收算法
5.1 标记-复制(Copying)
它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
- ✅ 优点: 减少内存碎片,提高空间利用率。
- ⭕ 缺点: 减半了可用的堆内存,可能增加垃圾回收的频率。
5.2 标记-清除(Mark-Sweep)
算法分为“标记”和“清除”阶段:
标记清除算法分为两个主要步骤:标记 和 清除。
- 标记阶段: 在标记阶段,垃圾回收器会从
GC Roots
开始,遍历所有可达的对象,并标记它们为活动对象。 - 清除阶段: 在清除阶段,垃圾回收器会遍历整个堆,回收所有未被标记的对象的内存。
有两个明显的问题:
- 效率问题:如果需要标记的对象太多,效率不高。
- 空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。
5.3 标记-整理(Mark-Compact)
标记整理算法是标记清除算法的改进版本。它在标记和清除的基础上增加了整理阶段,将所有活动对象向一端移动,从而消除内存碎片。
- ✅ 优点: 解决了内存碎片化问题,提高了空间利用率。
- ⭕ 缺点: 移动对象增加了额外的开销。
5.4 分代收集理论
当前虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
- 新生代(
Young Generation
):使用复制算法,因为新生代中的对象生命周期较短。 - 老年代(
Tenured Generation
):使用标记整理或标记清除算法,因为老年代中的对象生命周期较长,且数量较少。
1️⃣ 新生代的回收算法(以 复制 算法为主)
- 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
- 新生代内存按照
8
:
1
:
1
8:1:1
8:1:1 的比例分为一个
eden
区和两个survivor
(survivor0
,survivor1
)区(一般而言)。大部分对象在eden
区中生成。回收时先将eden
区存活对象复制到一个survivor0
区,然后清空eden
区,当这个survivor0
区也存放满了时,则将eden
区和survivor0
区存活对象复制到另一个survivor1
区,然后清空eden
和这个survivor0
区,此时survivor0
区是空的,然后将survivor0
区和survivor1
区交换,即保持survivor1
区为空, 如此往复。 - 当
survivor1
区不足以存放eden
和survivor0
的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC
(Major GC
),也就是新生代、老年代都进行回收。 - 新生代发生的 GC 也叫做
Minor GC
,Minor GC
发生频率比较高(不一定等eden
区满了才触发)。
2️⃣ 老年代的回收算法(以 标记-清除、标记-整理 为主)
- 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
- 内存比新生代也大很多(大概比例是
1
:
2
1:2
1:2),当老年代内存满时触发
Major GC
,Major GC
发生频率比较低,老年代对象存活时间比较长,存活率标记高。
3️⃣ 永久代(Permanet Generation
)的回收算法
JDK 1.8 及以后方法区的实现变成了元空间。
用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class,例如 Hibernate 等,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。永久代也称 方法区。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过根搜索算法来判断,但是对于无用的类则需要同时满足下面 3 个条件:
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
5.5 垃圾回收阶段算法小结
标记复制 | 标记清除 | 标记压缩 | |
---|---|---|---|
速率 | 最快 | 中 | 最慢 |
空间开销 | 两个大小相同的空间 | 少(会堆积碎片) | 少(不会碎片堆积) |
移动对象 | 是 | 否 | 是 |