强引用,弱引用,软引用,虚引用它们有什么区别?你知道吗?
讲解到了这里,各位同学应该对强软弱虚四种引用有一些初步的了解了,那么我们来简单的总结一下吧!强引用:就是不同的引用,平常创建对象的方式就是强引用,被强引用指向的对象不能被垃圾回收器回收。软引用:通过创建软引用类对象来实现,内存足够时允许停留在内存中,内存不够时就将其从内存中清除给其他对象腾出空间,可以作为缓存来使用。软引用:比强引用弱,就算有引用指向它,只要发生GC垃圾回收过程,软引用对象就会被清
目录
1. 先简单了解JVM内存模型
(注:如果已经了解JVM内存模型的同学可以直接看下面)
在讲解强弱软虚引用四种引用之前,我们先来回顾一下 JVM 虚拟机的内存模型,简单了解一下对象在 JVM 中的存放原理,也是为了让不了解Java虚拟机的同学在看此篇文章的时候不那么迷惑,下面开始正题。
Java 虚拟机运行在内存中,当虚拟机拿到了自己可支配的内存之后,会将内存分为五个部分,分别是 栈(JVM栈),堆,方法区(JDK1.8之后改名元空间),程序计数器,本地方法栈。
如下图所示
JVM栈:运行我们的程序,如 main 方法;
堆:存放对象,创建的对象都存放在堆中;
方法区(元空间):存放类加载器,静态变量静态方法,全部变量;
这里举个例子,如下代码所示,我定义一个 main 函数,打印一句话,那么该程序就会开辟一个新的JVM栈,当我们再定义另一个 main 方法时,就会再开辟一个新的JVM栈,JVM栈是每个线程私有的,但它们会共享堆中的对象和元空间中的全局静态变量。
2. 强引用类型解析
2.1 强引用理论解释
Java 中,我们通常都是通过 Object o = new Object() 的方式来创建一个对象,这个 new Object() 对象就存放在堆中,我们的 o 就存放在各自程序的JVM栈中是私有的,o 中保存了堆中对象的内存地址,如下图所示
当我们的程序想要操作对象的时候,就会通过 o 中保存的内存地址去堆中寻找该对象,然后对该对象进行操作。强引用对自然非常强,只要堆中的对象有变量指向它,它就不会被GC回收,只有没有人任何对象引用它的时候,它才会被GC垃圾回收器回收。
2.2 强引用代码演示
如下代码展示。写了注释,所以就不再赘述了
public class Test {
// 重写 Test 类中的 finalize() 方法
@Override
public void finalize() throws Throwable{
// 打印一句话作为标记,证明该方法被调用过
System.out.println("finalize方法执行");
}
public static void main(String[] args) throws Exception {
// 创建类对象 t
Test t = new Test();
System.out.println(t+"第一次获取对象");
t = null;
// 开启垃圾回收GC
System.gc();
// 因为GC垃圾回收是另外的垃圾回收线程,所以我们让主线程先睡两秒,避免造成误差
Thread.sleep(2000);
// 经过GC之后再次获取t对象
System.out.println(t+"第二次获取对象");
}
}
关于 finalize 方法我还是要说明一下。
finalize 方法是定义在 Object 类中的方法,每个对象都可以调用该方法,当对象被回收的时候,就会执行 finalize 方法;因为Java虚拟机的GC垃圾回收是在一定条件下才运行的,我们这里只把 t 对象赋值为 null ,不一定会启动 GC垃圾回收过程,所以我们通过 System.gc() 主动启动垃圾回收线程,我也在注释中也说明了,GC垃圾回收有它自己的线程,所以我们调用 sleep 方法让 main 方法的进行先睡一会,让GC完成之后再去重新获取。
运行之后如下图所示,在控制台中我们也可以看到,finalize 方法被执行打印出来了,说明对象被回收之后,会执行自身的 finalize 方法。
2.3 强引用的使用场景?
强引用的使用场景是我们在 Java 开发的时候最为常用的,正常的 new 对象的方式产生的对象引用方式都是强引用,它最大的特点就是只要有强引用指着,垃圾回收器就不能回收,哪怕产生OOM(内存溢出)都不会回收。
3. 软引用类型解析
3.1 软引用理论解释
首先我需要给大家明确一点,软引用本身其实是一个类,名为 "SoftReference",可以添加泛型。我们创建出该类的对象,那么该类的对象引用类型就是软引用。
创建软引用对象的方式为 SoftReference<?> m = new SoftReference<?>(new ?)
软引用内存图如下所示,m 对象在JVM程序栈中,软引用对象和软引用对象内部的数组对象都包含在堆中,m 对象与软引用对象之间是正常的强引用,软引用对象与内部的数组对象它们两个之间则是弱引用,在图中也表示为虚线,没有强引用那么强。
3.2 软引用与强引用的区别?
刚才说到了,软引用没有强引用强,是如何体现的呢?
假设我们 Java 虚拟机的堆内存为20M,现在我定义了一个软引用的字节数组,大小为10M,接下来我还要创建一个大小为12M的数组,此时我们来看,10M+12M>20M,已经要内存溢出,所以按道理来说我们想要创建的数组是不可能创建成功的,但是由于我们先前创建的字节数组为软引用,那么此时堆就会把这个10M的字节数组从堆中清除,清除之后就有足够的内存空间容纳新创建的数组了;
而假设我们要创建一个大小为5M的数组,此时 10+5<20,还没有超过内存大小,不会内存溢出,那么此时堆就不会把这个10M的字节数组清除,而是让它继续留在堆中。
3.3 软引用代码展示
如下展示软引用对象被清除的代码,注释我都写的很清楚,应该不需要做过多的解释说明了。
public static void main(String[] args) throws InterruptedException {
// 创建一个软引用对象 m,并在m软引用对象在定义一个 10M 的字节数组
SoftReference<byte[]> m = new SoftReference<>(new byte[1024 * 1024 * 10]);
// 通过get方法获取一下软引用对象中的字节数组对象
System.out.println(m.get()+"---"+"第一次获取软引用字节数组对象");
// 调用 GC,启动垃圾回收
System.gc();
// 让 main 线程睡 0.5秒
Thread.sleep(500);
// GC之后再获取软引用的字节数组对象,看是否能获取到
System.out.println(m.get()+"---"+"GC之后第二次获取软引用字节数组对象");
// 再创建一个大小为 12M 的字节数组
byte[] b = new byte[1024 * 1024 * 12];
// 获取新创建的数组,看是否被创建成功,看能否获取成功
System.out.println(b+"---"+"获取新创建的字节数组对象b");
// 此时再次获取先前的软引用对象 m,看是否还存在
System.out.println(m.get()+"---"+"创建字节数组b之后再来获取软引用字节数组对象");
}
在运行 main 函数之前,我们需要先对JVM栈做一个配置,如下图,点击该类配置项,
点击之后弹出如下界面,这里我们配置一下 VM 虚拟机的参数项,这里配置一下JVM内存大小为20M,每个人的程序由于你可能在当前模块定义了其他的类或者程序,或者堆中已经存放了其他对象,导致结果可能不太相同是正常现象,可以试着变换一下内存参数大小值或数组大小,这里我就是将内存设置为了20M,在调试运行程序之前也试过其他数值大小,但没有得出正确的结果,所以结果不一样你可以改变一下大小,多试几次,换个数值试一下即可,不一定是程序的问题。只要能让内存溢出的情况产生即可。
配置完成后如下所示,点击应用确认,然后关闭窗口即可运行 main 函数
然后我们运行上述 main 方法,在控制台中得到如下结果,可以看到,在创建强引用硬数组之后,我们并没有手动将弱引用的数组对象赋值为 null,但是在第三次获取软引用字节数组的时候,它却变成了 null,这就是弱引用的特性,内存足够时,允许它留在堆中,对内存不够用的时候,就把它从堆内存中清除
测试过软引用对象的字节数组被清除的情况之后,我们再来测试一下不被清除的情况,很简单,把要创建强引用的数组大小变小一点即可,这里我改为5M,即[1024 * 1024 * 5],其他代码都不用动
重新运行 main 函数,在控制台得到了不一样的结果如下所示,可以看到,此时创建了一个小的数组,没有内存溢出的情况下,第三次获取软引用的数组时,可以获取得到
3.4 软引用的使用场景?
通过上面弱引用的特性,我们其实可以大概能了解它的一个使用场景,有什么东西我们可以满足内存足够时让它存在,内存不足时让它离开呢?
当然是缓存啦!!!
各位同学想一下,如果一张图片非常的大,或者其他资源,加载需要时间比较久,我们就可以把它定义为弱引用,提前加载到内存中,在需要的时候直接访问,当内存不够的时候,再把它从内存中清除,需要的时候在读取到内存中。这就是软引用的其中一个使用场景。
4. 弱引用类型解析
4.1 弱引用理论解释
弱引用与软引用类似,都是一个了类,Java中叫 "WeakReference",该类创建的对象的引用方式便是弱引用,创建对象的方法也是与软引用相同,这里就不举了了,下面演示代码的时候也可以看到。
弱引用与强引用在遇到GC垃圾回收时的情况恰恰相反,强引用的对象不管哪次GC垃圾回收时,都不会被清除,而弱引用对象只要遇到GC,都会被回收;
4.2 弱引用代码演示
代码如下所示,注释都已经说明,不需要做过多解释
public class WeakReferenceTest {
public static void main(String[] args) throws InterruptedException {
// 通过弱引用创建一个字符串对象
WeakReference<String> m = new WeakReference<>(new String("我是弱引用"));
// 打印弱引用对象的字符串对象
System.out.println(m.get()+"---"+"GC之前第一次获取弱引用字符串对象");
// 手动开启GC
System.gc();
// 让 main 线程睡1秒,防止GC未结束,main函数先运行完导致结果异常
Thread.sleep(1000);
// GC之后再来获取弱引用对象,看能否获取到
System.out.println(m.get()+"---"+"GC之后第二次获取弱引用字符串组对象");
}
}
然后我们运行上述 main 方法,在控制台中得到如下结果
4.3 弱引用的使用场景
弱引最重要的一点应用就是在 ThreadLocal 中,使用弱引用避免了 ThreadLocal 造成内存泄露问题,这里不做大篇幅的讲解,想了解的可以看我的另一篇文章。
5. 虚引用类型解析
5.1 虚引用构造器展示
虚引用同样也是一个类,它的构造方法需要我们传递两个参数,如下图所示
第一个参数是虚引用对象,第二个参数需要我们传递一个队列。
5.2 虚引用与弱引用的比较
虚引用与弱引用相似的是:被虚引用指向的对象仍旧会被GC垃圾回收器回收。
虚引用与弱引用不同的是:虚引用内部的对象我们永远无法通过 get() 方法获取到其中的值,上面我们可以看到强软弱三种引用都可以获取到其对象,虚引用则不可以。
虚引用在代码中使用很少,这里就不作代码演示了,面试中问的也不是特别多,重点记住上面三种引用。
5.2 虚引用的使用场景?
那么既然获取不到虚引用的对象,要它有什么用呢?
虚引用最主要的一个场景就是管理堆外内存。如下图所示
Java 虚拟机是运行在操作系统的内存之中的,当我们要处理一份数据的时候,
第一步:操作系统先读取到自己的内存中;
第二步:然后再拷贝到我们的JVM内存中;
第三步:JVM处理完毕之后,再拷贝回操作系统内存中;
第四步:再由操作系统返回至外界;
中间经过了操作系统这一媒介,效率可想而知,是比较低的,所以 Java 就提供了虚引用,它可以直接得到并直接操作操作系统的内存,提高了效率。
但是随着虚引用可以管理堆外内存,一个新的问题产生。
我的虚引用对象存放在JVM的堆中,虚引用所指向的对象则是在操作系统的内存中。假如说我的虚引用在JVM内存中已经赋值为空,虚引用对象与操作系统中的对象断开了联系。虚引用之前所指向的操作系统内存中的对象是不能被JVM垃圾回收器回收的,因为虚引用指向的对象并没有存放在JVM内存的堆中,而是直接存放在操作系统的内存中,所以JVM是无法将其回收的,一旦长时间如此,操作系统的内存终究会出现大量没有引用的对象而且无法被清除,造成内存泄露。
为了避免内存泄漏相框的发生,就引入了队列这一属性,它最大的作用就是当虚引用的对象被回收的时候,就会给队列发一个信号,说明一下虚引用的对象引用失效了,此时JVM内部的GC垃圾回收就会对队列做判断,如果满足一定的条件,那么JVM的垃圾回收器就会把JVM内存中的垃圾对象堆外操作系统中的垃圾对象一并回收,就不会造成内存泄露的情况了。
(这里补充一点,Java的GC垃圾回收是由C++语言编写的,可以直接操控计算机底层,不仅可以清除JVM内存的垃圾,也可以清除总操作系统内存的垃圾)
6. 小小总结
讲解到了这里,各位同学应该对强软弱虚四种引用有一些初步的了解了,那么我们来简单的总结一下吧!
强引用:就是普通的引用,平常创建对象的方式就是强引用,被强引用指向的对象不能被垃圾回收器回收;
软引用:通过创建软引用类对象来实现,内存足够时允许停留在内存中,内存不够时就将其从内存中清除给其它对象腾出空间,可以作为缓存来使用;
弱引用:比强引用弱很多,就算有引用指向它,只要发生GC垃圾回收过程,软引用对象就会被清除;
虚引用:比弱引用还要若,通常用作管理对外内存;
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)