概述
垃圾回收机制简介
什么是垃圾回收机制?
1. 先来聊聊有关垃圾回收机制的历史吧
- 历史①:垃圾回收机制也称为自动内存管理,旨在由虚拟机自动管理内存空间而不需要开发者的介入
- 历史②:Lisp 语言是世界第一门采用动态内存分配和垃圾收集技术的语言
- 历史③:手动垃圾回收与自动垃圾回收
- 手动垃圾回收
- 代表语言:C/C++
- 定义:[开发者必须手动调用方法去回收内存中的垃圾]{.blue}
- 优点:[开发者能够直接对内存进行更加灵活的管理,更加清楚内存分配的细节]{.red}
- 缺点:[频繁地手动进行内存分配和垃圾回收是十分麻烦的]{.green}
- 自动管理:
- 代表语言:Java、C#
- 定义:[虚拟机自动回收堆空间中的垃圾,不再需要程序员关心]{.blue}
- 优点:[虚拟机代替开发者对堆空间进行管理,开发者能够更加集中于应用开发]{.red}
- 缺点:[堆空间出现溢出问题,开发者难以直接对其进行管理,只能够通过配置虚拟机参数间接管控]{.green}
- 手动垃圾回收
2. 了解历史之后,再来看看垃圾回收机制的主要任务吧
- 核心任务
- [为新对象分配相应的内存空间]{.red}
- [两种分配方式(堆空间-对象创建 已经提到过):① 指针碰撞 ② 空闲列表]{.red}
- [采用的对象的分配方式取决于回收堆空间的方式]{.red}
- [标记内存空间的垃圾并对垃圾进行清除]{.red}
- 方式:两个阶段采用的不同算法
- [标记算法:可达性分析]{.orange}
- [清除算法:引用计数法、标记-清除、标记-整理、标记-复制]{.orange}
- 区域:[频繁收集新生代,较少收集老年代,基本不动方法区]{.red}
- [新生代:只针对新生代的收集称为 Minor GC / Young GC]{.orange}
- [老年代:只针对老年代的收集称为 Major GC]{.orange}
- [整堆:针对方法区和堆区的收集称为 Full GC]{.orange}
- 方式:两个阶段采用的不同算法
- [为新对象分配相应的内存空间]{.red}
- 其余任务:负责堆的管理和布局、与解释器和编译的协作、与监控子系统协作等等职责
3. 垃圾回收的垃圾指的是什么?垃圾回收真的只回收垃圾吗?
- 核心:垃圾回收机制主要回收 [内存空间中的垃圾和非必要的对象]{.red}
- 垃圾(主要):[不再被任何变量引用的对象就是垃圾]{.red}
- 非必要的对象(次要):[仅被软引用、弱引用、虚引用指向的对象]{.red}(引用类型)

为什么需要垃圾回收机制?
- 核心:为了保证程序能够正常高效地运作
:::info
① 连续内存分配是操作系统中的概念,可以阅读我写的 操作系统有关内存的笔记 进行了解
② 内存碎片的概念也是操作系统中的,可以细分为内部碎片和外部碎片,也可以参考我的操作系统笔记进行了解
:::
什么时候执行垃圾回收?
- 主动发起:手动调用 System.gc() 等相关方法
- 虚拟机并不会立刻开始执行垃圾回收:
- [① 垃圾回收线程优先级太低 ]{.red}
- [② 需要等待所有线程进入安全区域或者安全点才会开始回收]{.red}
- 虚拟机并不会立刻开始执行垃圾回收:
- 被动发起:[新生代中的伊甸园区或者老年代将要 用尽 的时候]{.red}
- [新生代中的幸存者区不会触发垃圾回收机制的执行]{.red}
- 分区算法
垃圾回收机制如何执行?
垃圾回收器:
垃圾回收器是虚拟机中执行垃圾回收机制的部分
分类:[7 大经典垃圾回收器 + 3 大前沿的垃圾回收器]{.blue}
过程:
- [标记阶段:判断哪些对象不再被变量引用并标记为垃圾]{.red}
- [清理阶段:清除那些被标记为垃圾的对象]{.red}
细节:[所有垃圾回收器在执行的过程中的两个阶段都会造成用户线程暂停(STW)]{.red}
- 采用不同的设计方式的垃圾回收器带来的 STW 时延并不一致
- STW 将会在后面的细节中详细介绍
垃圾回收细节
引用类型
为什么引用还需细分类型?
- 核心:[尽可能应对所有可能面临的应用场景]{.red}
- 场景:[如果我们希望某些对象能够在内存充足的时候存活,而在内存紧张的时候被回收呢?(缓存)]{.blue}
- 普通的引用显然无法满足这个要求,普通的引用只要指向某个对象,那么这个对象就不可能被回收
- 我们也不可能根据内存的情况,手动地解除引用,毕竟内存是由虚拟机维护的
- 基于这些可能面临的场景,才会进一步提出细分引用的类型
五种引用类型

:::info
① JDK 1.2 对引用的概念进行了扩充并且新增了四种引用类型:软引用、弱引用、虚引用、终结器引用
② 新增的三种引用类型都归属于反射包下(java.lang.ref)
:::
前提:
- 没有被引用指向的对象是一定会被回收的
- [被引用指向的对象也是有可能被对象回收的]{.blue}
强引用(Strong Referrence)
定义:默认对象都是被强引用所指向的(采用常规创建对象的方式得到的变量都是强引用)
特点:[只要被强引用指向的对象绝对不会被垃圾回收器回收]{.red}
细节:99 % 的开发中都是使用的强引用
// 配置虚拟机参数 -XX:PrintGCDetails 年轻代没有任何变化
public static void main(String[] args) throws InterruptedException
{
// 两个强引用同时指向一个实例对象
StringBuilder str1 = new StringBuilder("Hello, World");
StringBuilder str2 = str1;
// 解除其中一个引用
str1 = null;
// 主动调用垃圾回收:对象显然不会被回收
System.gc();
// 确保垃圾回收线程可以被执行
Thread.sleep(2000);
// 测试两者的内容: 前者为 null, 后者为 Hello World
System.out.println(str1);
System.out.println(str2);
}
软引用(Soft Referrence)
定义:用于描述那些有用但是非必须的对象
特点:[软引用指向的对象只有内存空间非常紧张的情况下才会被垃圾回收器回收]{.red}
细节:[可以用于实现高速缓存(Spring、Mybatis 缓存底层就是采用这种引用实现的)]{.red}
// 测试:软引用指向的对象是否将会在内存紧张的情况下被回收
public class SoftReferrenceTest
{
private static class User
{
private int id;
private String name;
public User(int id, String name)
{
this.id = id;
this.name = name;
}
protected void finalize() throws Throwable
{
System.out.println(id + ": 对象被回收...");
}
}
public static void main(String[] args) throws InterruptedException
{
// 注意: 软引用指向的对象当前是没有强引用指向的
SoftReference<User> object = new SoftReference<>(new User(1,"冬坂五百里"));
// 强引用指向的对象
User user = new User(2,"斧乃木余接");
// 主动调用垃圾回收
System.gc();
// ① 查看对象是否被回收: 内存充足的情况没有被回收
System.out.println(object.get());
// ② 采用大对象占用堆空间, 逼迫垃圾回收器开始回收弱引用对象
byte[] bytes = new byte[1024 * 1024 * 7 - 1024 * 600];
// 测试结果有点意外,即使在老年代已经被占用 99% 的情况下依然没有被回收
// 但是在发生溢出之后就被回收掉了,作为对比,那个强引用指向的对象至始至终没有被回收
System.out.println(object.get());
}
}
弱引用(Weak Referrence)
定义:用于描述那些非必须的对象,仅使用一次的对象
特点:[弱引用指向的对象在下次垃圾回收器开始回收时就会直接被回收掉]{.red}
细节:
[垃圾回收的线程优先级非常低,所以弱引用指向的对象也可以存活很长时间(可用于实现缓存)]{.red}
:::info
你可以尝试把下面代码中的输出和休眠代码交换顺序,你会发现弱引用指向的对象依然存活
:::
弱引用不需要通过标记算法确定是否回收,因为无论是否存活都会被回收,相比于软引用效率更高
集合框架中的工具类 WeakHashMap 就是采用弱引用实现的
public static void main(String[] args) throws InterruptedException
{
// 注意: 弱引用指向的对象当前是没有强引用指向的
WeakReference<StringBuilder> str = new WeakReference<>(new StringBuilder("Hello, World"));
// 主动调用垃圾回收
System.gc();
// 确保垃圾回收线程能够执行
Thread.sleep(3000);
// 输出
System.out.println(str.get());
}
虚引用(Phantom Referrence)
定义:用于在垃圾回收对象时得到相应的通知(需要借助引用队列)
- 引用队列(Referrence Queue):所有被标记需要回收的对象都会被添加进入引用队列
- 弱引用和软引用都可以使用引用队列用来获取对象被销毁的通知,是可选的
- 虚引用必须借助引用队列来获取对象被销毁的通知
+++ 为什么虚引用必须要使用引用队列而弱引用和软引用不需要呢?
① 因为虚引用无法获取对象的任何信息,还不借助引用队列,那就彻底没有价值了
② 软引用和弱引用可以获取对象实例来确认对象是否已经被回收
+++
特点:
- [虚引用指向的对象也会在下次垃圾回收器开始回收时就被回收]{.red}
- [虚引用指向的对象是无法通过虚引用来获得的]{.red}
细节:你可以认为这种引用基本没有什么卵用
public class PhantomReferrenceTest
{
public static void main(String[] args) throws InterruptedException
{
// 必须传入引用队列,最后需要借助引用队列来获取销毁对象的通知
ReferenceQueue<StringBuilder> queue = new ReferenceQueue<>();
PhantomReference<StringBuilder> str = new PhantomReference<>(new StringBuilder("Hello, World"), queue);
// 使用虚引用获得实例对象: 获取不到
System.out.println(str.get());
// 主动调用垃圾回收
System.gc();
// 引用队列中取出元素: 如果弱引用对象没有被销毁, 线程将会被阻塞, 如果被销毁将会返回对象
Reference<StringBuilder> msg = (Reference<StringBuilder>) queue.remove();
// 测试虚引用指向的对象是否被回收
System.out.println(msg == null ? "对象没有被回收..." : "对象被回收...");
}
}终结器引用(Final Referrence)
- 定义:没有强引用指向的对象虚拟机会为其默认添加的引用就是终结器引用
- 特点:终结器引用的对象将会被添加的 F-Queue 队列中等待被回收
细节:
[同一个对象可以被多个不同的类型的引用所指向]{.red}
[只有强引用指向的对象才会导致内存溢出,其余引用类型指向的对象不可能导致溢出]{.red}
因为其余引用类型指向的对象将会在内存发生溢出之前就会被回收掉
内存溢出
什么是内存溢出
- 定义:[内存空间无法再容纳新的对象]{.red}
内存泄漏
:::info
参考博客:
How can I create a memory leak in Java?
:::
什么是内存泄露?
定义:
- [狭义:不再被使用的对象无法被垃圾回收器回收 / 对象使用完毕之后没有释放相应的空间]{.red}
- [广义:对象的生命周期被无限期延长导致垃圾回收器无法回收]{.red}
图示:
举例:
抽象:
造成内存泄露的原因是什么?
:::warning
① 不少博客上的例子是存在一定问题的,你只要亲手实验了就会发现
② 所以以下列举出的原因并不一定是准确的,即使在 StackOverFlow 上关于内存泄漏的情况都是有争议的
:::

原因:
[静态变量:静态变量和类的生命周期一致,使用完成之后没有即时释放就非常容易造成内存泄露]{.red}
单例模式
public class MemoryLeak{
// 难以被回收的静态变量
private static MemoryLeak memoryLeak = null;
private MemoryLeak(){
}
public static MemoryLeak getInstance(){
return memoryLeak == null ? new MemoryLeak() : memoryLeak;
}
}类变量
public class MemoryLeak{
// 声明了静态变量,但是你使用结束后又没有显示的释放空间,就非常容易造成内存溢出
private static MemoryLeak memoryLeak = new MemoryLeak();
}
[集合:不再使用的元素没有被移除就会导致集合长期持有对象的引用,无法被垃圾回收清除,导致内存泄漏]{.red}
无论集合是成员变量还是局部变量都有可能产生这个问题
public class MemoryLeak{
protected void finalize() throws Throwable{
super.finalize();
System.out.println("我被回收了...");
}
public static void main(String[] args) throws InterruptedException{
List<MemoryLeak> list = new LinkedList<>();
// 集合中的第一个对象我不想再使用的了,但是我忘了移除这个元素
// 垃圾回收器就无法对其进行回收
Collections.addAll(list, new MemoryLeak(),
new MemoryLeak(),
new MemoryLeak(),
new MemoryLeak());
// 主动调用垃圾回收机制,对象肯定无法被回收
System.gc();
Thread.sleep(1000000);
}
}[非静态内部类:]{.red}
- [非静态内部类持有外部类的引用,即使外部类对象不再被引用了,内部类对象仍被引用,也无法被回收]{.red}
- IDEA 通常会建议你将非静态内部类变成内部类
public class MemoryLeak{
// 即使外部类使用完毕,但是由于内部类还在使用,就会导致外部类无法被垃圾回收
private InnerClass innerClass = new InnerClass();
private class InnerClass{
}
}[未关闭的流或者连接:数据库连接,网络连接,IO 流]{.red}
- StackOverFlow 上有部分人认为没有关闭的流或者连接不算做内存泄漏
- 但是我觉得既然连接用完了,没有关闭的连接肯定是要占用资源的
总结:[所有内存泄漏的原因归根究底都是你的代码写的有问题,所以要注意自己的代码质量]{.red}
并行与并发
:::danger
① 如果你已经学习过操作系统中的并行与并发的话,请最好忘记这两个概念在操作系统中的含义
② 垃圾回收机制中的并行与并发和操作系统中的定义完全不同,不要用操作系统中的并行与并发去看待垃圾回收中的
③ 如果你实在无法忘记,我也会尽可能阐释这两者之间的联系和区别
④ 此后提到的所有并行和并发都是指的垃圾回收层面的
最好的参考资料:《垃圾回收算法手册》P257、P286 两页非常清楚地说明了垃圾回收层面的并行和并发
:::
操作系统角度(可以跳过)
前提:[每个计算核在某个时刻仅能够执行一个进程或者线程]{.blue}
操作系统层面:并行与并发
并发(Concurrent):
定义:[多个进程或者线程交替执行,在某个时间段内可以认为同时执行]{.red}
特点:[单核处理器仅能够实现并发]{.red}
- 单核处理器只具有单个计算核,所以只可以在 [同一时刻]{.red} 执行一个进程或者线程
- 但是单核处理器可以在 [某个时间段内]{.red} 交替进程或者线程执行,看起来像是同时执行(伪并行)
并行(Parallel):
定义:[多个进程同时执行]{.red}
特点:[仅有多核处理器能够实现并行]{.red}
- 多核处理器具有多个计算核,所以可以在 [同一时刻]{.red} 同时执行多个进程
细节:
- [并行和并发不是相互矛盾的概念,两者和串行是矛盾的概念]{.red}
- 单核处理器:并发意味着可以交替线程执行,串行就只能按照顺序执行
- 多核处理器:并行意味着可以同时执行多个线程,串行就只能够每次使用单个计算核按照顺序执行
- [并行和并发既然不是矛盾的概念,意味着并行中允许实现并发]{.red}
- 多核处理器同时执行多个进程或者线程,但是进程或者线程的数量超过了计算核的数量
- 那么每个处理器就需要交替执行不同的进程或者线程,也就实现了进程或者线程的并发性
- [并行和并发不是相互矛盾的概念,两者和串行是矛盾的概念]{.red}

垃圾回收角度(重要)
:::danger
理解垃圾回收的并行与并发是非常关键的,因为具体的垃圾回收器就是从这两个方面着手改进的
:::
串行:
- 定义:[GC 线程执行完成后才能够轮到用户线程执行]{.red}
- 代表性垃圾回收器:Serial、Serial Old
并行:
- 定义:[垃圾回收器启用多条 GC 线程同时执行,默认对用户线程使用 STW 机制]{.red}
- 只有多条 GC 线程同时执行:不符合操作系统中的并行概念,但是在垃圾回收中就是并行
- [并行中默认对用户线程使用 STW 机制,因为不采用 STW 机制就是并发了]{.red}
- 优点:提高了单次垃圾回收的效率,降低了单次垃圾回收造成的 STW 时延
- 代表性垃圾回收器:Parallel Scavenge、ParNew、Parallel Old
- 定义:[垃圾回收器启用多条 GC 线程同时执行,默认对用户线程使用 STW 机制]{.red}
并发:
- 定义:[用户线程和 GC 线程同时执行,不对用户线程使用 STW 机制]{.red}
- [多条用户线程和多条 GC 线程同时执行]{.red}
- 符合操作系统的并行概念,但在垃圾回收层面视为并发
- GC 线程数量超过自己拥有的处理器数量就会出现操作系统中的并发现象,需要交替执行
- 用户线程数量超过自己拥有的处理器数量就会出现操作系统中的并发现象,也需要交替执行
- [两者并发执行不会相互干扰,即不会出现 GC 线程抢占用户线程执行的情况,能够继续保持并行]{.orange}
- [不对用户线程使用 STW 机制不代表并发垃圾回收器没有延迟]{.red}
- [垃圾回收器的执行是分阶段,部分阶段可以并发,部分阶段依然只能够并行]{.orange}
- [所以非即时垃圾回收器都是会有 STW 造成的时延的,不可能消除只能够避免]{.orange}
- [多条用户线程和多条 GC 线程同时执行]{.red}
- 总结:
- [垃圾回收层面是并发包含并行,操作系统层面是并行包含并发]{.blue}
- 在操作系统角度看来,垃圾回收器的并行与并发都可以是并行,线程都在同时执行
- 代表性垃圾回收器:CMS、G1
- 定义:[用户线程和 GC 线程同时执行,不对用户线程使用 STW 机制]{.red}
STW
什么是 STW
- 名称:Stop the World
- 定义:[垃圾回收器开始工作时,会暂停用户线程执行的情况]{.red}
- 用户线程被暂停就会让用户明显感觉到“卡顿”,也就是时延
- 垃圾回收器工作的时间越长,用户感觉“卡顿”的时间就久
- 特点:
- [只有基于可达性分析的标记型算法才会出现 STW ,基于引用计数法的所有延伸算法是没有这个问题的]{.red}
- [Java 采用的就是可达性分析算法,所以 Java 虚拟机中的所有垃圾回收器都是无法避免 STW 带来的时延的]{.red}
:::info
① 先简单了解下可达性分析,它是一种标记存活对象的方式,用于确定哪些是可以回收的对象
② 详细了解:可达性分析
:::
为什么需要 STW
- 核心:[确保可达性分析过程的一致性]{.red}
- 什么叫可达性分析的一致性
- 如果在可达性分析的过程中,用户线程和 GC 线程并发执行
- 那么在分析的过程中对象间的引用不断随着用户线程的执行而变化
- 最终导致 GC 线程无法精准识别哪些对象是垃圾,哪些对象是存活的
- 那么此前提到的并发式垃圾回收器是如何做到不采用 STW 呢?
- 并发式垃圾回收器并不是完全并发,只是在某些阶段是并发的,其余阶段依然并行,依然需要采用 STW
- 并发式垃圾回收器在并发的阶段会采用某些手段来确保可达性分析的一致性
- 增量更新(CMS 垃圾回收器采用的方式)
- 原始快照(G1 垃圾回收器采用的方式)
- 什么叫可达性分析的一致性
如何降低 STW 带来的延迟影响呢?
- 增量算法:
- 定义:将 GC 线程的回收过程分成几个阶段执行
- 优点:[减少单次 STW 造成的时延影响,实际上总的时延是增加了的]{.red}
- 缺点:[会造成程序吞吐量的下降]{.green}
- 允许用户线程和 GC 线程并发执行
- 优点:
- [有效降低 STW 造成的时延]{.red}
- [吞吐量下降的幅度相对较小]{.red}
- 缺点:[需要采用手段避免可达性分析的不一致性]{.green}
- 优点:
:::info
增量算法和增量更新是两个不一样的东西
:::
主动发起垃圾回收方法
System.gc()
:::warning
① 默认情况下,虚拟机都是自行决定什么时候执行垃圾回收的
② 但是我们也可以调用相应的方法使得虚拟机执行垃圾回收
:::
System.gc() / Runtime.getRuntime.gc()
作用:[建议或者提醒 虚拟机执行垃圾回收]{.red}
- 意味着调用该方法之后虚拟机不一定立刻执行垃圾回收
- 原因是虚拟机给垃圾回收线程(Finalizer 线程)设置的优先级非常低,不一定能够立即执行
细节:
调用
System.gc()
会直接触发 [Full GC]{.red} 对整个堆空间和方法区都进行回收调用
System.runFinalization()
会强制要求虚拟机立刻执行垃圾回收Runtime.getRuntime.gc()
和System.gc()
没有任何区别(从源码中可以看出)// System 类中的 gc 方法
public static void gc() {
Runtime.getRuntime().gc();
}[调用
System.gc()
适用于使用堆外内存的时候,垃圾回收器难以直接对堆外内存进行回收,所以手动回收]{.red}:::info
正因为方法区在 JDK 8 之后采用元空间实现,所以方法区变得更加难以收集了,所以 ZGC 干脆不支持收集方法区了
:::
命令:
- [-XX:+DisableExplicitGC 禁止手动触发垃圾回收]{.blue}
- 给虚拟机配置该参数之后,调用
System.gc()
是不会生效的
测试(测试结果可能会让你惊讶,也有可能不会)
没有强制执行的垃圾回收的情况下,由于垃圾回收线程优先级较低,
所以你可能看不到相应的输出语句,但也有可能看得到
强制执行垃圾回收的情况下,你是一定能够看到输出语句的
// 测试: System.gc() 是否能够立即执行
public class SystemGCTest
{
public static void main(String[] args) throws InterruptedException
{
// 注:这个对象没有被任何变量引用,所以是会被回收的
new SystemGCTest();
// 建议虚拟机执行垃圾回收
System.gc();
// 强制执行垃圾回收
System.runFinalization();
}
// 对象被销毁之前虚拟机会调用该方法
protected void finalize() throws Throwable
{
super.finalize();
System.out.println("对象将要被销毁...");
}
}例子
finalize()
前提:
finalize()
方法是 Object 类中自带的没有任何实现的方法protected void finalize() throws Throwable{
// 空的方法体
}finalize()
方法必须被子类重写才会有作用,否则没有任何用
作用:[对象被垃圾回收器回收之前自动调用的方法,使得开发者在对象消亡前 自定义逻辑]{.red}
可以在对象消亡前释放连接,关闭文件等资源关闭操作
[可以将对象从死亡的边缘拉回来一次:复活对象]{.green}
:::info
至于对象复活的方式将在稍后提到
:::
对象状态
- [可触及态:对象仍被变量引用的情况]{.red}
- [可复活态:对象没有被变量所引用,但是 finalize 方法没有被调用过]{.red}
- [不可触及态:对象已经被垃圾回收器回收完成了,不可能再复活了]{.red}
:::info
由于对象可能在 finalize 方法中被复活,所以对象具有三种状态
:::
细节:
finalize()
方法在 JDK 9 中被废弃了+++ 为什么要在之后 JDK 版本中废弃这个方法呢?
① finalize 方法中编写的逻辑太差会严重影响垃圾回收的性能(试了下递归,但是没有报错,很奇怪)
② finalize 方法的执行依赖于垃圾回收发生的时间,里面执行的逻辑是没有任何保障的
③ 总结:所以在编写程序的时候尽可能不要再去使用这个方法
+++
[
finalize()
方法仅会对象被调用一次]{.red}
对象“复活”
过程:
对象被垃圾回收的条件就是没有变量引用对象
那么我们只要在对象死亡前让变量重新引用该对象,对象就不会消亡了
但是此前提到
finalize()
方法只会被执行一次,所以对象也只可以复活一次
细节:
- 对象重写该方法的方法没有被执行,那么该对象将会被GC线程添加到F-Queue队列中等待执行
finalize()
- 对象如果在执行
finalize()
的过程中被“复活”,那么将会被移出 F-Queue 队列
- 对象重写该方法的方法没有被执行,那么该对象将会被GC线程添加到F-Queue队列中等待执行
// 测试: 对象“复活”
public class SaveObject
{
// 类变量才可以作为 GC Roots, 实例变量是不可以作为 GC Roots 的
private static SaveObject object;
protected void finalize() throws Throwable
{
// 重写 finalize 方法
System.out.println("开始执行垃圾回收...");
// 让对象重新被 GC Roots 引用
object = this;
System.out.println("对象复活...");
}
public static void main(String[] args) throws InterruptedException
{
// GC Roots 指向堆中的对象
object = new SaveObject();
// 取消 GC Roots 到对象的引用
object = null;
// 开始执行垃圾回收
System.gc();
// 暂停主线程: Finalizer 优先级非常低, 有可能主线程执行结束了, 它还没有执行
Thread.sleep(3000);
// 测试对象是否死亡
System.out.println(object == null ? "对象死亡..." : "对象没有死亡...");
// 可以继续进行后续的测试: 即对象第二次死亡还可以复活吗?
}
}