Skip to content

垃圾回收机制

更新: 5/6/2025 字数: 0 字 时长: 0 分钟

如果接触过C/C++的小伙伴应该知道,我们在申请了空间后一定要释放空间,不然就会有内存泄漏的风险,JVM通过自行完成这个操作来省去的程序员申请空间与释放空间的过程。

JVM会在认为一个对象实例不再被用到的时候将他回收掉

那么这里就引入一个新的机制 垃圾回收机制——如何认定与回收对象

对象存活判定算法

上轨线先来看看如何认为一个对象已经不会再被用到/如何认为一个对象已经可以被回收

引用计数法

当我们吹昂见一个对象,首先要创建一个他的引用变量

java
String a="12334";  //这里的a是就是引用变量,表示变量"1234的应用"

这里就需要引入一个观念了:我们操作的实际上都是变量的一个引用

每个引用都会存放一给引用计数器

  1. 当对象在一个地方被使用,则计数器+1
  2. 当离开了引用失效(离开局部变量的作用域(也就是栈帧被推出),或者引用被设置为null)则-1
  3. 当引用的计数为0时就不再被使用了

但是存在一种卡bug的行为

java
String a= new Test();
String b= new Test();
a.inner=b;
b.innser=a;
a=b=null;

public class Test{
	Test inner;
}

此时有两个无法被清除的变量,就是a和b的inner,又因为a和b都被赋值为null了,所以我们也无法再去操作原本的那两个实例

可达性分析算法

可达性分析会认为没有根节点的对象就是可以被回收的,那么什么是根节点呢?

  1. 栈帧中引用到的对象
  2. 本地方法栈引用到的对象
  3. 常量池里的对象
  4. 被加了锁的对象
  5. 虚拟机内部用到的对象

垃圾回收算法

提及垃圾回收就必须得先了解对象是如何存放的了。

在Java内存机制中我们曾提到过,Java的对象实际是存放在堆中的,那么堆究竟是什么样的呢?

堆大致分为新生代,老年代和永久代(后面修改为元空间)三个部分,新生代又分为Eden和Survivor两个部分,Survivro内部含有to和from两个部分,其中Eden,To,From三者大小大致为8:1:1

当对象第一次创建成功后就会被放在新生代的Eden中,等待GC的到来,第一次GC经历后,会剩余的对象放入Survivor的From区,然后交换一次to和From区(To变成From,From变成To)

接着经历第二次GC,再次将Eden中新加入且未被淘汰的对象放入From,然后对To区的对象做一次年龄检测,若年龄>15(每当对象挺过一次GC就会+1年龄),则放入老年代,否则放入From区,然后再次交换From和To区

(注意:GC是针对整个新生代和老年代的,所有新生代和老年代的对象如果满足GC的标准都会被回收,其中对于老年代的GC有Full GC和Major GC)

空间分配担保

当Survivor区的大小装不下Eden传过来的对象时,会直接将Survivor中的对象送到姥姥年代,若老年代也放不下,则来一次Full GC,若Full GC后还是放不下,则抛出OOM

垃圾回收器

目前HotSpot最常见的垃圾收集器是Garbage First收集器,也就是我们常说的G1收集器,他在JDK7走上历史舞台,并于JDK9正式成为默认的方案

我们知道垃圾回收分为Minor GC,Major GC,和Full GC,他们分别为新生代,老年代和整个堆

而G1回收器绕过这些约定,将堆分为2048个Region,每个Region大小为1~32不等,但均为2的n次幂且所有的Region大小相同,在JVM的整个声明周期都不会改变

而分出这些Region的原因就是为了可以让他们自由的决定到底是扮演Eden,Survivor还是老年代,这样就使得Eden,Survivor和老年代不再是完整的物理空间而是到处分布的

  1. 初始标记(暂停用户线程):这一阶段只是标记GC Roots能关联到的对象,并且修改TAMS的值,让下一阶段的用户线程并发运行时,能正确的在可用的Region中分配对象,这个阶段需要停顿线程,但耗时很短,并且是借用进行Minor GC的时候完成的,因此这一阶段没有额外的停顿
  2. 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这个阶段耗时比较长,但可与用户程序并发执行
  3. 最终标记(暂停用户线程):对用户线程做一个短暂的暂停,用于处理并发标记漏掉的对象
  4. 筛选回收(暂停用户线程):负责更显Region的统计数据,根据用户所期望的停顿时间来指定回收计划,可以自由的选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理整个Region的全部空间

元空间(MetaSpace)

在JDK8之前,HotSpot虚拟机的方法区是永久代实现的,但是在JDK8之后,HotSpot就不再使用永久代而是使用全新的元空间,类的元信息被存储在元空间中,元空间没有使用堆内存而是与堆不相连的本地内存区域。因此理论空间(物理内存)多大元空间就会有多大,所以不会出现永久代内存溢出的情况

元空间与永久代

JVM有自己的一套设计规范,里面规定了JVM应该有哪些组成部分(就像电脑会有CPU,主板等),方法区就是JVM设计规范中要求必须要有的东西,它用来存放一个类的所用信息(比如类的版本,字段,方法等)

在JDK8之前,JVM的方法区由永久代实现,他被放在了堆中,而由于种种问题(比如难以优化,容易OOM),而最终被元空间替代,永久代不再存放于堆中

其他的引用类型

我们都知道,如果一个变量是对象类型,那么它实际上存放的是对象的引用类型,但是如果是一个基本类型,那么他存放的就是类型的值了,实际上我们平时代码中使用Object a=new Object() 这样的都是强引用

我们知道,在Java中,如果强引用想要回收,前提必须是强引用所在的方法或者强引用断开才行,不然的话这个引用后面很大可能会再次用到,所以JVM宁愿抛出OOM也不会随意的回收强引用

本站访客数 人次      本站总访问量