(待更新)📝JVM-2:JVM-垃圾回收
如何判断对象可被回收
垃圾回收算法
判断垃圾可被回收:引用计数法
当前对象被一个变量所引用,则该对象引用计数加一
当对象的引用计数为 0 时 -> 应该回收
缺陷:这种情况会导致内存泄漏
判断垃圾可被回收:可达性分析算法
这是 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 引用类型)
(目前记得有点乱,待整理)
介绍
- Strong References (强引用)
当所有 GC Roots 对象都不通过强引用引用该对象,该对象才能被垃圾回收
- Soft Reference (软引用)
- 当只有软引用引用该对象,并且在垃圾回收后内存仍不足时,才会再次触发垃圾回收,回收软引用对象
- 可以配合引用队列来释放软引用本身
- Weak References (弱引用)
- 当只有软引用引用该对象时,在垃圾回收时无论内存是否充足,都会回收弱引用对象
- 可以配合引用队列来释放弱引用本身
- Phantom References (虚引用)
必须配合引用队列使用,主要配合 ByteBuffer 使用,当被引用对象被回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
- Final Reference (终结器引用)
无需手动编码,但其内部配合引用队列,在垃圾回收时终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize(),第二次 GC 时才能回收被引用对象
强引用
平时 new 的都是强引用,只要能够沿着 GC Root 的引用链找到它,就不会被回收
没有 GC Root 直接/间接引用该对象了 -> 该对象会被回收
软引用
当垃圾回收且内存不足时 -> 该对象会被回收(前提是没有被某个强引用直接引用)
弱引用
当垃圾回收时,即使内存足够 -> 也会被回收掉(前提是没有被某个强引用直接引用)
软引用 & 弱引用
软引用和弱引用可以配合“引用队列”来使用,也可以不配合
“软引用”和“弱引用”本身也是对象,当它们引用的对象被回收后,它们就可以放到“引用队列”中去
软引用/弱引用的应用
案例:
很多图片资源,如果使用强引用引用这些图片资源 -> 容易发生内存溢出
改进:我们希望在内存紧张时,将这些资源占有的内存释放掉,等未来要使用时再读取一遍 -> 要使用软/弱引用
改进前:
List –> byte[]
(在 List 中直接强引用 byte[])
改进后(通过软/弱引用):
List ==> SoftReference/WeakReference –> byte[]
(在 List 中强引用一个软引用/若引用对象,软引用对象再引用一个 byte[])
软引用/弱引用的对象只要没有被根对象强引用 -> 内存紧张触发垃圾回收时,它会被回收
软引用/弱引用结合引用队列
软引用引用的对象在内存紧张时被回收,我们希望将软引用对象本身也回收掉,因为软应用对象本身也是占用内存的
如何清理一个无用的软引用对象本身?
-> 需要使用引用队列来完成
如何关联软引用对象和引用队列?
-> 在创建软引用对象时,将引用队列作为参数传入
虚引用
用来跟踪对象被垃圾回收的活动
当虚引用对象创建时,就会关联一个引用队列
-> 可以再虚引用引用的对象被回收之前采取一定的措施
-> 虚引用的典型用法(释放直接内存):在虚引用引用的对象被垃圾回收时,虚引用对象自己就会被放进引用队列,从而可以间接地用一个线程来调用虚引用对象的方法,调用 Unsafe.freeMemory() 来释放直接内存
虚引用 & 终结器引用
和软引用、弱引用不同,“虚引用”和“终结器引用”必须配合引用队列使用
同样地,虚引用、终结器引用本身都是对象
垃圾销毁的时机
finalize():
- 如果希望对象被垃圾回收时执行一段代码 -> 重写 finalize()
- finalize() 可以完成对象的自救,判断一个对象是否可以被回收,需要两次标记,第一次是可达性分析标记,第二次就是分析这个要回收的对象是否调用过 finalize() / 是否重写了 finalize():如果未调用过 / 重写了 -> JVM 会调用该对象的 finalize(),在该方法中,我们可以两件事:
(1) 销毁资源
(2) 对象自救(让某个强引用引用该对象)
一个对象的 finalize() 只会被调用一次,而且 finalize() 被调用不意味着 GC 会立即回收该对象 -> 有可能调用 finalize() 后,该对象又不需要被回收了,然而到了真正要被回收的时候,因为前面调用过一次而不会调用 finalize(),从而产生问题
类加载的时机
1 | static { |
Garbage Collection Algorithms (垃圾回收算法)
Garbage Collection Algorithms (垃圾回收算法)
三种算法
- 标记-清除
优点:速度快
缺点:容易产生内存碎片,可用内存的连续空间不大
(TODO:补充图片)
- 标记-整理
第一阶段也是清理,区别在于第二阶段——整理
在整理过程中会让可用的内存紧凑起来,这样可用内存就比较连续
优点:没有内存碎片
缺点:时间长,耗费性能
(TODO:补充图片)
- 标记-复制
新生代使用标记复制算法时,每次将 Eden 或者 from 中存活的对象放入 to,并寿命加一个,并交换 from 和 to。如果 to 没有足够的空间存放存活的对象,这些对象就会通过分配担保机制直接进入老年代,这是安全的(因为研究表明新生代中 98% 的对象都熬不过第一轮回收)
优点:没有内存碎片
缺点:占用两份内存
JVM 中这三种算法是结合着使用,不会只使用一种
标记清除算法和标记整理算法的本质区别
- 是否设计对象的移动(是否需要 STW)
- 内存碎片
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 参数
GC 分析
- 读取垃圾回收的日志
Young Generation 的 From 中的对象不一定是数量达到阈值才会被放入 Old Generation,当 Young Generation 中的内存紧张时,也可能会被放入到 Old Generation
- 大对象直接晋升到 Old Generation
整个大到放不下 Young Generation -> 会直接被放入老年代
- Java 中某一个线程内发生了内存溢出异常,并不会导致整个 Java 进程的结束
Garbage Collection Tuning (垃圾回收调优)
(TODO:补充)