导航:
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/黑马旅游/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码-CSDN博客
目录
一、概念准备
1.1 GC Roots
1.2 可达性分析算法
1.3 非可达对象被回收过程中的两次标记
1.4 finalize()方法
二、垃圾回收算法
2.1 标记清除算法(Mark-Sweep)
2.2 标记复制算法(Copying)
2.3 标记整理算法(Mark-Compact)
2.4 小结:三种垃圾回收对比
三、分代收集理论
一、概念准备
1.1 GC Roots
GC Roots:即GC根节点集合,是一组必须活跃的引用,这些对象一定是存活的,不需要被垃圾回收器回收。GC即Garbage Collection,译为垃圾回收。
可作为GC Roots的对象:
- 本地方法栈中引用的对象:即由本地方法中的局部变量所引用的对象。本地方法栈用于管理本地方法的调用,这些本地方法底层是用C语言写的,被编译为基于本机硬件和操作系统的程序,它们引用的对象不会被回收,所以是GC Root。
- 虚拟机栈中引用的对象:虚拟机栈又称Java方法栈,即由 Java 方法中的局部变量所引用的对象。虚拟机栈中的局部变量、参数以及返回值等都是直接引用对象的,他们是明确的、在程序运行期间被访问到的对象,所以它们也是GC Root。
- 方法区中常量、静态变量引用的对象:常量不可变、静态变量先于对象产生,被所有该类的对象共享,所以它们都可认为是GC Root。final修饰的类不可被继承、方法不可被重写、变量不可变。静态变量是类变量,优先于对象产生。常量、静态变量都存放在JVM方法区的类常量池中,类常量池在类加载阶段从Java字节码文件中解析。
- 所有被同步锁持有的对象:在 Java 虚拟机中,线程持有同步锁时,该对象就是被线程所引用的,即使在程序的其他部分没有对该对象进行引用,它也不会被回收。
- 所有线程对象:线程对象是程序执行的基本单位之一,他们被创建后,会一直存在直到它们被显式地销毁或者程序运行结束。
- 所有跨代引用对象:年轻代和老年代之间相互引用的对象成为跨带引用对象。
- JVM内部的引用:如基本数据类型对应的Class对象,常驻的异常对象(如空指针异常、参数不合法异常,他们在JVM启动过程中就被加载,因为它们太常用了),以及应用程序类类加载器;
回顾JVM内存模型:
对JVM内存模型(方法区、堆、栈等元素) 不熟悉,可以参考下文:
什么是JVM的内存模型?详细阐述Java中局部变量、常量、类名等信息在JVM中的存储位置-CSDN博客
1.2 可达性分析算法
以所有GC Roots为起始点,根据引用关系向下搜索,将所有与GC Roots直接或间接有引用关系的对象在对象头的Mark Word里标记为可达对象,即不需要回收的有引用关系对象。搜索过程所走过的路径称为“引用链” 。
例如下面对象1,2,3,4都在GC Roots的引用链上,所以它们都是可达对象,不会被回收。而对象5,6,7因为没有跟任何一个GC Root直接或间接产生调用关系,所以它们是非可达对象,需要被回收。
1.3 非可达对象被回收过程中的两次标记
非可达对象被回收需要两次标记:
- 第一次标记后筛选非可达对象:
- 非可达对象第一次被可达性分析算法标记后,会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,也就是是否有机会自救。
- 假如对象没有覆盖或者已被JVM调用过finalize()方法,也就是说不想自救或已自救过,那么此对象需要被回收;
- 假如对象覆盖并没被JVM调用过finalize()方法,该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法,完成自救后它们就不会被回收。
- 第二次标记F-Queue里的未自救对象:
- 稍后,收集器将对F-Queue中的对象进行第二次小规模的标记。
- 如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this)赋值给某个引用类型的类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的F-Queue。如果对象这时候还没有逃脱,那它就真的要被回收了。
1.4 finalize()方法
Java中,任何对象都直接或间接是Object类的子类,finalize()是Object类中的一个方法,所以可以说,Java中任何类都有一个finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会。
需要注意的是,任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。
另外,finalize()方法的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方明确声明为不推荐使用的语法。
finalize()方法所在位置:
二、垃圾回收算法
2.1 标记清除算法(Mark-Sweep)
步骤:
- 标记、清除:当堆中有效内存空间被耗尽时,会STW(stop the world,暂停其他所有工作线程),然后先标记,再清除。
- 标记:可达性分析法,从GC Roots开始遍历,找到可达对象,并在对象头中进行标记。
- 清除:堆内存内从头到尾进行线性遍历,“清除”非可达对象。注意清除并不是真的置空,垃圾还在原来的位置。实际是把垃圾对象的地址维护在空闲列表,对象实例化的申请内存阶段会通过空闲列表找到合适大小的空闲内存分配给新对象。
对象头: Hotspot 虚拟机中每个对象都有一个对象头(Object Header),包含Mark Word(标记字段) 和 Class Pointer(类型指针)。
- Mark Word(标记字段):存储哈希码、GC分代年龄、锁信息、GC标记(标志位,标记可达对象或垃圾对象)等。锁信息包括:
- 锁标志位:64位的二进制数,通过末尾能判断锁状态。01未锁定、01可偏向、00轻量级锁、10重量级锁、11垃圾回收标记
- 偏向锁线程ID、时间戳等;
- 轻量级锁的指针:指向锁记录的指针
- 重量级锁的指针:指向Monitor锁的指针
- 类型指针:指向它的类元数据的指针,用于找到对象所在的类。
优缺点和使用场景:
- 优点:简单
- 缺点:
- 效率不高:需要可达性遍历和线性遍历,效率差。
- STW导致用户体验差:GC时需要暂停其他所有工作线程,用户体验差。
- 有内存碎片,要维护空闲列表:回收垃圾对象后没有整理,导致堆中出现一块块不连续的内存碎片。
- 适用场景:适合小型应用程序,内存空间不大的情况。应用程序越大越不适用这种回收算法。
2.2 标记复制算法(Copying)
在开始阶段,将内存空间分为两块,每次只使用一块。
步骤:
- 标记:在进行垃圾回收时,先可达性分析法标记可达对象。
- 复制:然后将可达对象复制到没有被使用的那个内存块中;
- 清除:最后清除旧内存块中的所有对象。、
- 后续再按同样的流程来回复制和清除。
优缺点和使用场景:
- 优点:
- 垃圾多时效率高:只需可达性遍历,效率很高。
- 无内存碎片:因为有移动操作,所以内存规整。
- 缺点:
- 内存利用率低,浪费内存:始终有一半以上的空闲内存。
- 需要调整引用地址:可达对象移动后,内存地址发生了变化,需要调整所有引用,指向移动后的地址。
- 垃圾少时效率相对差,但还是比其他算法强:如果可达对象比较多,垃圾对象比较少,那么复制算法的效率就会比较低。只为了一点垃圾而移动所有对象未免有些小题大做。所以垃圾对象多的情况下,复制算法比较适合。
- 适用场景:
- 适合垃圾对象多,可达对象少的情况,这样复制耗时短。
- 非常适合新生代的垃圾回收,因为新生代要频繁地把可达对象从伊甸园区移动到幸存区,而且是新生代满了适合再Minor GC,垃圾对象占比高,所以回收性价比非常高,一次通常可以回收70-90%的内存空间,现在的商业虚拟机都是用这种GC算法回收新生代。
2.3 标记整理算法(Mark-Compact)
步骤:
- 标记:首先可达性分析法标记可达对象;
- 整理:然后将可达对象按顺序整理到内存的一端;
- 清除:最后清理边界外的垃圾对象。
标记整理算法相当于内存碎片优化版的标记清楚算法,不用维护空闲列表。
优缺点和使用场景:
- 优点:
- 无内存碎片:内存规整。
- 内存利用率最高:内存既规整又不用浪费一般空间。
- 缺点:
- 效率最低:效率比其他两种算法都低
- 需要调整引用地址:可达对象移动后,内存地址发生了变化,需要调整所有引用,指向移动后的地址。
- STW导致用户体验差:移动时需要暂停其他所有工作线程,用户体验差。
2.4 小结:三种垃圾回收对比
标记清除算法 | 标记复制算法 | 标记整理算法 | |
---|---|---|---|
速度 | 中等 | 最快 | 最慢 |
时间开销 | mark阶段与存活对象的数量成正比,sweep阶段与整堆大小成正比 | 与存活对象大小成正比 | mark阶段与存活对象的数量成正比,compact阶段与整堆大小成正比,与存活对象的大小成正比 |
空间开销 | 少(但会堆积碎片) | 通常需要存活对象的2倍大小(不堆积碎片) | 少(不堆积碎片) |
移动对象 | 否 | 是 | 是 |
三、分代收集理论
垃圾回收器都不会只选择一种算法,JVM根据对象存活周期的不同,将内存划分为几块。一般是把堆分为新生代和老年代,根据年代的特点来选择最佳的收集算法。
分代收集算法:将堆分为新生代、老年代不同生命周期的对象放在不同的代,采用不同的收集算法,以提高回收效率。
HotSpot 中大部分垃圾回收器都采用分代回收的思想,即新生代用一种垃圾回收算法,老年代用一种垃圾回收算法。
以JDK8为例,JDK8默认回收器是Parallel+Parallel Old,新生代用Parallel回收器,老年代用Parallel Old回收器。
回收过程:
- 首先,任何新对象都分配到 eden 空间。两个幸存者空间开始时都是空的。
- 当 eden 空间填满时,将触发一个Minor GC(年轻代的垃圾回收,也称为Young GC),删除所有未引用的对象,大对象(需要大量连续内存空间的Java对象,如那种很长的字符串)直接进入老年代。
- 所有被引用的对象作为存活对象,将移动到第一个幸存者空间S0,并标记年龄为1,即经历过一次Minor GC。之后每经过一次Minor GC,年龄+1。GC分代年龄存储在对象头的Mark Word里。
- 当 eden 空间再次被填满时,会执行第二次Minor GC,将Eden和S0区中所有垃圾对象清除,并将存活对象复制到S1并年龄加1,此时S0变为空。
- 如此反复在S0和S1之间切换几次之后,还存活的年龄等于15的对象(JDK8默认15,JDK9默认7,-XX:InitialTenuringThreshold=7)在下一次Minor GC时将放到老年代中。
- 当老年代满了时会触发Major GC(也称为Full GC),Major GC 清理整个堆 – 包括年轻代和老年代。