如何判断对象可被回收

垃圾回收算法

判断垃圾可被回收:引用计数法

当前对象被一个变量所引用,则该对象引用计数加一
当对象的引用计数为 0 时 -> 应该回收

缺陷:这种情况会导致内存泄漏

image-20250613001104438

判断垃圾可被回收:可达性分析算法

这是 JVM 判断对象是否为垃圾的算法

一些根对象(GC Root):一定不会被回收的对象
JVM 进行垃圾回收时,会先去堆中将所有对象扫描一遍,看是否能够沿着 GC Root 为起点的引用链找到该对象:如果找不到 -> 表示可以回收;如果找得到 -> 不回收

阅读扩展:对象的存活与毁灭(待优化)

要经历两次标记:

  • 第一次标记:是否在 GC Root 的引用链上(是否可达)

(在对象被判断为不可达之后不会立即回收,还会有第二次标记)将第一次标记为需要回收的对象放入一个 F-Queue 队列中,然后让一个优先级较低的线程来调用队列中要回收的对象的 finalize()

  • 第二次标记(对象的自救)

通过判断是否重写了 finalize() / 是否已经被调用过 finalize(),来决定是否还要调用该方法
-> 只有未被调用过 finalize() 的对象和重写了 finalize() 的对象才会调用该方法;否则第二次标记时还是会存在于 F-Queue 中

自救的方法:在调用该对象的 finalize() 时,在重写的该方法中将对象赋值给某个变量
-> 这样一来,在第二次标记时就会发现该对象存在于引用链上
-> 会从要回收的名单中剔除,即完成了自救

注意:每个对象的 finalize() 只能被执行一次

查看 GC Root 对象

可使用可视化工具查看

Types of References in Java (Java 引用类型)

(目前记得有点乱,待整理)

介绍

  1. Strong References (强引用)

当所有 GC Roots 对象都不通过强引用引用该对象,该对象才能被垃圾回收

  1. Soft Reference (软引用)
  • 当只有软引用引用该对象,并且在垃圾回收后内存仍不足时,才会再次触发垃圾回收,回收软引用对象
  • 可以配合引用队列来释放软引用本身
  1. Weak References (弱引用)
  • 当只有软引用引用该对象时,在垃圾回收时无论内存是否充足,都会回收弱引用对象
  • 可以配合引用队列来释放弱引用本身
  1. Phantom References (虚引用)

必须配合引用队列使用,主要配合 ByteBuffer 使用,当被引用对象被回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存

  1. Final Reference (终结器引用)

无需手动编码,但其内部配合引用队列,在垃圾回收时终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize(),第二次 GC 时才能回收被引用对象

强引用

平时 new 的都是强引用,只要能够沿着 GC Root 的引用链找到它,就不会被回收

没有 GC Root 直接/间接引用该对象了 -> 该对象会被回收

软引用

当垃圾回收且内存不足时 -> 该对象会被回收(前提是没有被某个强引用直接引用)

弱引用

当垃圾回收时,即使内存足够 -> 也会被回收掉(前提是没有被某个强引用直接引用)

软引用 & 弱引用

image-20250613001204955

软引用和弱引用可以配合“引用队列”来使用,也可以不配合

“软引用”和“弱引用”本身也是对象,当它们引用的对象被回收后,它们就可以放到“引用队列”中去

image-20250613001218575

软引用/弱引用的应用

案例:
很多图片资源,如果使用强引用引用这些图片资源 -> 容易发生内存溢出
改进:我们希望在内存紧张时,将这些资源占有的内存释放掉,等未来要使用时再读取一遍 -> 要使用软/弱引用

改进前:
List –> byte[]
(在 List 中直接强引用 byte[])

image-20250613001230638

改进后(通过软/弱引用):
List ==> SoftReference/WeakReference –> byte[]
(在 List 中强引用一个软引用/若引用对象,软引用对象再引用一个 byte[])

image-20250613001242153

软引用/弱引用的对象只要没有被根对象强引用 -> 内存紧张触发垃圾回收时,它会被回收

软引用/弱引用结合引用队列

软引用引用的对象在内存紧张时被回收,我们希望将软引用对象本身也回收掉,因为软应用对象本身也是占用内存的

如何清理一个无用的软引用对象本身?
-> 需要使用引用队列来完成

如何关联软引用对象和引用队列?
-> 在创建软引用对象时,将引用队列作为参数传入

image-20250613001300123

虚引用

用来跟踪对象被垃圾回收的活动

当虚引用对象创建时,就会关联一个引用队列
-> 可以再虚引用引用的对象被回收之前采取一定的措施
-> 虚引用的典型用法(释放直接内存):在虚引用引用的对象被垃圾回收时,虚引用对象自己就会被放进引用队列,从而可以间接地用一个线程来调用虚引用对象的方法,调用 Unsafe.freeMemory() 来释放直接内存

虚引用 & 终结器引用

和软引用、弱引用不同,“虚引用”和“终结器引用”必须配合引用队列使用

同样地,虚引用、终结器引用本身都是对象

垃圾销毁的时机

finalize():

  1. 如果希望对象被垃圾回收时执行一段代码 -> 重写 finalize()
  2. finalize() 可以完成对象的自救,判断一个对象是否可以被回收,需要两次标记,第一次是可达性分析标记,第二次就是分析这个要回收的对象是否调用过 finalize() / 是否重写了 finalize():如果未调用过 / 重写了 -> JVM 会调用该对象的 finalize(),在该方法中,我们可以两件事:
    (1) 销毁资源
    (2) 对象自救(让某个强引用引用该对象)

一个对象的 finalize() 只会被调用一次,而且 finalize() 被调用不意味着 GC 会立即回收该对象 -> 有可能调用 finalize() 后,该对象又不需要被回收了,然而到了真正要被回收的时候,因为前面调用过一次而不会调用 finalize(),从而产生问题

类加载的时机

1
2
3
static {
// 静态代码块执行
}

Garbage Collection Algorithms (垃圾回收算法)

Garbage Collection Algorithms (垃圾回收算法)

三种算法

  1. 标记-清除

优点:速度快
缺点:容易产生内存碎片,可用内存的连续空间不大

(TODO:补充图片)

  1. 标记-整理

第一阶段也是清理,区别在于第二阶段——整理

在整理过程中会让可用的内存紧凑起来,这样可用内存就比较连续

优点:没有内存碎片
缺点:时间长,耗费性能

(TODO:补充图片)

  1. 标记-复制

新生代使用标记复制算法时,每次将 Eden 或者 from 中存活的对象放入 to,并寿命加一个,并交换 from 和 to。如果 to 没有足够的空间存放存活的对象,这些对象就会通过分配担保机制直接进入老年代,这是安全的(因为研究表明新生代中 98% 的对象都熬不过第一轮回收)

优点:没有内存碎片
缺点:占用两份内存

JVM 中这三种算法是结合着使用,不会只使用一种

标记清除算法和标记整理算法的本质区别

  1. 是否设计对象的移动(是否需要 STW)
  2. 内存碎片

Generational garbage collection (分代垃圾回收机制)

Generational garbage collection (分代垃圾回收机制)

介绍

JVM 会结合上述三种算法协同工作,具体的实现叫做分代的垃圾回收机制

将堆内存划分为两大块:新生代(Young Generation)和老年代(Old Generation)

新生代又划分为:伊甸园(Eden)、幸存区 From、幸存区 To

Java 中有些对象长时间使用的放在老年代,有些用的时间短的放在新生代

根据 Java 对象生命周期的变化存放在不同的位置,并采取不同的垃圾回收策略

老年代中很久触发一次垃圾回收,而新生代垃圾回收的频率高一些

(TODO:补充图片)

如何工作的

Eden 中对象进行一次 GC 后存活下来的对象,寿命加一

Minor GC 垃圾回收流程

开始创建的对象都会直接被存放到 Young Generation 的 Eden 中,当 Eden 的内存快满时 -> 触发一次 GC 垃圾回收。新生代的垃圾回收一般称为 Minor GC

然后根据可达性分析算法,根据 GC Root 引用链去找哪些对象是没有的,哪些对象是有用的。对经过一次 GC 后存活下来的对象,使用复制算法放入到 Young Generation 的 To 中,并寿命加一;伊甸园中其他对象被直接清理掉。

交换 From 和 To 的指针(交换位置)

然后就可以继续向 Eden 中存放对象

第二次触发 Minor GC 时,会在 Eden 和 From 中根据可达性分析算法寻找需要被回收的垃圾,然后将存活下来的存放到 To 中,再交换 From 和 To 的位置

经历了一定次数的垃圾回收后,仍然在 From 中存活的对象会被放入 Old Generation(因为显然是有价值的)

具体的晋升阈值取决于垃圾回收器

老年代使用的内存达到一定阈值时,就会触发一次 Full GC(STW 时间更长)

Minor GC 会引发 STW,暂停其他用户线程,等垃圾回收结束后用户线程才恢复运行

如果 Full GC 后内存仍然不够用,会抛出 OutOfMemory 异常

(TODO:补充图片)

GC 相关 VM 参数

image-20250613002832437

GC 分析

  1. 读取垃圾回收的日志

image-20250613002926070

Young Generation 的 From 中的对象不一定是数量达到阈值才会被放入 Old Generation,当 Young Generation 中的内存紧张时,也可能会被放入到 Old Generation

  1. 大对象直接晋升到 Old Generation

整个大到放不下 Young Generation -> 会直接被放入老年代

  1. Java 中某一个线程内发生了内存溢出异常,并不会导致整个 Java 进程的结束

Garbage Collection Tuning (垃圾回收调优)

(TODO:补充)