JVM 垃圾收集(GC)器——垃圾判断

本文围绕JVM垃圾回收展开,介绍了需回收的内存区域,主要是堆区和方法区。阐述了Java中四种引用类型,重点讲解堆区垃圾回收判断的引用计数法(JVM不使用)和可达性分析算法(JVM使用),还说明了方法区废弃常量和无用类的垃圾回收判断。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
在这里插入图片描述

哪些内存需要回收

JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。

其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性(可以认为编译期间就可以确定),方法结束或者线程结束时,内存自然就被回收,因此无需过多考虑回收的问题。

Java堆区和方法区有着很大的不确定性,只有处于运行期间我们才能知道程序会创建哪些,会创建多少对象,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。垃圾收集器在对堆区和方法区进行回收前,首先要确定这些区域的对象哪些可以被回收,哪些暂时还不能回收。

谈谈引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。
在JDK1.2之后,Java对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。

1、强引用
  在程序代码中普遍存在的,类似 Object obj = new Object() 这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象

2、软引用
  用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。

3、弱引用
  也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。比如 threadlocal

4、虚引用
  也叫幽灵引用或幻影引用(名字真会取,很魔幻的样子),是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的作用是能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

注:无论引用计数算法还是可达性分析算法都是基于强引用而言的。

在这里插入图片描述

一、垃圾回收判断—堆区

Java堆中存放着几乎所有的对象实例,垃圾收集器在堆进行垃圾回收前,首先要判断这些对象那些还存活,那些已经“死去”。判断对象是否已“死”有如下几种算法:

1、引用计数法(JVM不使用该方法)

堆中每个对象实例都有一个引用计数器,当一个对象被创建时,就将该对象实例分配给一个引用计数器变量,该变量计数设置为1每当有地方引用该对象(任何其它变量被赋值为这个对象的引用)时,引用计数器加1(a> = b,则b引用的对象实例的计数器+1),当引用失效时(对象实例的某个引用超过了生命周期或者被设置为一个新值),对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

缺点:无法解决循环引用的问题,引用计数永远不可能为0。

2、可达性分析算法(JVM使用该方法)

可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图。通过一系列称为“GC
Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象,或者说一个对象到GC Roots间没有任何引用链相连则为可回收对象

在这里插入图片描述
对象Object5 —Object7之间虽然彼此还有联系,但是它们到 GC Roots 是不可达的,因此它们会被判定为可回收对象。

(1)GC Roots对象有哪些

① 虚拟机栈中引用的对象

(如各个线程被调用的方法堆栈中使用的参数、局部变量、临时变量等)

public class StackLocalParameter {

  public StackLocalParameter(String name) {}

  public static void testGC() {
    StackLocalParameter s = new StackLocalParameter("localParameter");
    s = null;
  }
}

此时的s,即为GC Root,当s置空时,localParameter对象也断掉了与GC Root的引用链,将被回收。

② 方法区中类静态属性引用的对象

(如Java类的引用类型静态变量)

public class MethodAreaStaicProperties {

  public static MethodAreaStaicProperties m;

  public MethodAreaStaicProperties(String name) {}

  public static void testGC(){
    MethodAreaStaicProperties s = new MethodAreaStaicProperties("properties");
    s.m = new MethodAreaStaicProperties("parameter");
    s = null;
  }
}

此时的s,即为GC Root,s置为null,经过GC后,s所指向的properties对象由于无法与GC Root建立关系被回收。而m作为类的静态属性,也属于GC Root,parameter 对象依然与GC root建立着连接,所以此时parameter对象并不会被回收。

③ 方法区中常量引用的对象

public class MethodAreaStaicProperties {

  public static final MethodAreaStaicProperties m = MethodAreaStaicProperties("final");

  public MethodAreaStaicProperties(String name) {}

  public static void testGC() {
    MethodAreaStaicProperties s = new MethodAreaStaicProperties("staticProperties");
    s = null;
  }
}

m即为方法区中的常量引用,也为GC Root,s置为null后,final对象也不会因没有与GC Root建立联系而被回收。

④ 本地方法栈中引用的对象
在这里插入图片描述

任何native接口都会使用某种本地方法栈,实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。

⑤ Java虚拟机内部的引用

如基本数据类型对应的Class对象,一些常驻的异常对象(如NullPointException、OutOfMemoryError),系统加载器等

⑥ 所有被同步锁(synchronized 关键字)持有的对象

⑦ 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

(2)“非死不可”吗

即使在可达性分析算法中不可达的对象,也并非"非死不可"的,这时候他们暂时处在"缓刑"阶段。要宣告一个对象的真正死亡,至少要经历两次标记过程:如果对象在进行可达性分析之后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。

当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过,虚拟机会将这两种情况都视为"没有必要执行",此时的对象才是真正"死"的对象

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(这里所说的执行指的是虚拟机会触发finalize()方法)。

finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象在finalize()中成功拯救自己(只需要重新与引用链上的任何一个对象建立起关联关系即可(譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量)),那在第二次标记时它将会被移除出"即将回收"的集合;如果对象这时候还是没有逃脱,那基本上它就是真的被回收了。

程序中可以通过覆盖finapze()来一场"惊心动魄"的自我拯救过程,但是这只有一次机会

自我拯救示例:

public class Test {
	public static Test test = null;
	public void isAlive() {
		System.out.println("I am alive :)");
	}
	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		System.out.println("finalize method executed!");
		//重新与引用链上的任何一个对象建立起关联关系,避免被回收
		Test.test = this;
	}
	public static void main(String[] args)throws Exception {
		test = new Test();
		//对象第一次成功拯救自己
		test = null;
		System.gc();
		//因为finapze方法优先级很低,所以暂停0.5秒以等待它
		Thread.sleep(500);
		if (test != null) {
			test.isAlive();
		}else {
			System.out.println("no,I am dead :(");
		}
		
		// 下面代码与上面完全一致,但是此次自救失败:任何一个对象的finalize()方法都只会被系统自动调用一次
		test = null;
		//此时该对象的finalize()方法不会被再次调用了,因此要被回收
		System.gc();
		Thread.sleep(500);
		if (test != null) {
			test.isAlive();
		}else {
			System.out.println("no,I am dead :(");
		}
	}
}

运行结果为:

finapze mehtod executed! 
yes, i am still apve :)
no, i am dead :(

从上面代码示例我们发现,finalize方法确实被JVM触发,并且对象在被收集前成功逃脱。
但是从结果上我们发现,两个完全一样的代码片段,结果是一次逃脱成功,一次失败。

这是因为,任何一个对象的finalize()方法都只会被系统自动调用一次,如果相同的对象在逃脱一次后又面临一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败。

二、垃圾回收判断—方法区

1、方法区(永久代)的垃圾回收的内容

废弃常量无用类

2、方法区(永久代)垃圾回收的判断

1、废弃常量

对于废弃常量,可通过引用的可达性来判断。以常量池中字面量(直接量)的回收为例,假如一个字符串"Java"已经进入了常量池中,但是当前系统没有任何一个String对象引用常量池中的"Java"常量,也没有其他地方引用这个字面量,如果此时发生GC并且有必要的话,这个"Java"常量会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

2、无用类

判定一个类是否是"无用类"则相对复杂很多。类需要同时满足下面三个条件才会被算是"无用的类":

1.该类的所有实例都已经被回收(即在Java堆中不存在任何该类的实例)
2.加载该类的ClassLoader已被回收
3.该类对应的Class对象没有任何其他地方被引用,无法在任何地方通过反射访问该类的方法

JVM可以对同时满足上述3个条件的无用类进行回收,也仅仅是“可以”而不是必然。在大量使用反射、动态代理等场景都需要JVM具备类卸载的功能来防止永久代的溢出。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值