1.JVM的垃圾回收机制
1.1什么是垃圾回收?
JVM的垃圾回收(Garbage Collection,GC)是一种自动内存管理机制,用于回收程序中不再使用的对象占用的内存空间,避免因对象未被正确释放而导致内存耗尽
1.2垃圾回收的区域
在上篇博文中,我介绍了JVm中运行时数据区的区域划分。其中,虚拟机栈,本地方法栈,程序计数器是线程私有空间,会随着线程的销毁而自动释放。剩下的方法区和堆是所有线程共享的内存空间,不会伴随某一个线程释放,生命周期和应用程序一致,所以需要程序员对方法区和堆中的无用数据进行手动释放,避免内存溢出。
但是在JDK8之后,方法区的实现方式由永久代变成了元空间(上篇博文有详细介绍),这使得方法区中存放的的数据有一定变化。元空间不再在堆中开辟空间而是单独的一块空间,并且把类的静态变量和常量池转移到堆中,方法区中实际上只剩下类元信息。一个应用程序中的类不是无限的,所以一般也不需要回收。
基于上述条件,可以确定垃圾回收的主要区域是堆,主要回收目标是堆中的无用对象
1.3 垃圾判断算法
在Java中,要使用一个对象必定是通过一个引用来访问的,所以可以根据一个对象是否有引用指向它来判断是否有用。
1.3.1引用计数算法
工作原理:
1.初始化:每个对象被创建时,其引用计数被初始化为1,表示有一个引用指向该对象
2.引用增加:当有一个新的引用指向该对象时,引用计数加一
3.引用减少:当某一引用不再指向该对象时,引用计数减一
4.内存回收:当引用计数减少为0时,表示没有任何引用指向该对象,此时可以安全地回收该对象占用的内存空间
缺点:
1.额外的内存空间开销:每个对象都需要为其引用计数单独开辟空间
2.线程安全问题:引用计数的增加和减少涉及到非原子性写操作,在多线程环境下可能会导致引用计数不同步
以上两个问题不是致命缺点,最严重的是接下来的问题
3.循环引用:如果两个或者多个对象相互引用,即使它们煤油其他外部变量来引用,引用计数不会减少为0,导致内存泄露
例如以下代码示例:
class A{
B b2 = new B();
}
class B{
A a2 = new A();
}
public class Test {
public static void main(String[] args) {
A a1 = new A();
B b1 = new B();
}
}
- 在main线程的私有虚拟机栈中,A和B对象别分有一个引用(a1/b1),此时引用计数为1。
- 在A对象内部有指向B对象的引用,在B对象内部有指向A对象的引用,此时引用计数为2
- 当a1和b1引用被回收后,引用计数减为1,但对象内部的引用没有被回收,导致引用计数不为0
这是引用计数算法的致命缺点,所以又引出了可达性分析算法
1.3.2可达性分析算法
工作原理:从一组称为“根对象”(GC Roots)的对象开始,沿着引用链遍历所有可达的对象,未被标记为可达的对象则被认为是不可达的,可以被回收
基本步骤:
1.根搜索(Root Searching):从根对象开始,根对象包括全局变量、活跃线程的栈中的局部变量、JNI引用、方法区中的常量引用等
2.遍历对象图(Object Graph Traversal):从根对象开始,递归地遍历对象之间的引用关系,标记所有可达的对象
3.标记阶段(Marking Phase):在遍历过程中,已访问的对象被标记为“被访问过”,以避免重复访问
4.清除阶段(Sweeping Phase):标记完成后,所有未被标记的对象被视为不可达,回收其占用的内存
优点:
1.解决循环引用问题:可达性分析能够检测和处理循环引用的问题,即使对象之间存在互相引用,只要它们不能从根对象或其他可达对象访问到,就可以安全地回收
2.避免引用计数的性能开销:不需要实时更新引用计数,避免了引用计数算法带来的性能开销
1.4 垃圾回收算法
1.4.1 标记-清除算法
工作原理:
1.标记所有可达对象
2.清除未标记的对象
优点:
1.解决循环引用问题
2.内存利用率高:不需要额外预留空间
缺点:
1.内存碎片:清除后产生内存碎片,可能导致后续内存申请失败。如上图,将垃圾回收后,剩下的可利用空间被分割成BCD和FGH两个区域,假设一个格子表示1kb。此时总可利用内存大小是6kb,但是实际上一次性可申请的最大内存是3kb
2.STW停顿:标记和清除阶段需暂停应用线程,导致延迟。不过随着技术不断发展,STW的停顿时间越来越短了
适用场景:老年代垃圾回收。因为老年代发生垃圾回收的频率较低,不容易产生内存碎片(年代划分等到分代回收算法再介绍)
1.4.2 复制算法
工作原理:将内存分为两块(From和To),存活对象从From复制到To,然后清空From
优点:
1.无内存碎片:对象复制后内存连续
2.高效分配:新内存从To空间顶端顺序分配
3.吞吐量高:适合存活率低的对象(如年轻代)
缺点:
1.内存浪费:需双倍内存空间,利用率仅50%
2.对象复制开销:存活率高时性能急剧下降
适用场景:年轻代回收。因为年轻代的存活率较低,回收率较高
1.4.3 分代回收算法
思想:基于对象存活时间划分内存区域(如年轻代、老年代),对不同代采用不同算法
回收流程:
1.代码中创建(new)一个对象时,就会存储在伊甸区。伊甸区的对象大概率活不过一轮GC
2.活过一轮GC的对象被转移到生存区A。经历GC后存活的对象通过复制算法复制到同样大小的生存区B,只要能在经历GC后存活下来,就会被复制到生存区的另一半,并且每经历一轮GC年龄就会+1
3.如果对象在生存区中经历多轮GC仍然存活,JVM就会认为这种类的生命周期较长,就可能会复制到老年区
优点:
1.针对性优化:年轻代(存活率低)用复制算法,老年代用标记-清除/整理
2.综合性能高:减少全局GC频率,提升吞吐量
缺点:
1.实现复杂:需要使用多种回收算法相互协同