目录
1. 先简单了解JVM内存模型
2. 强引用类型解析
2.1 强引用理论解释
2.2 强引用代码演示
3. 软引用类型解析
3.1 软引用理论解释
3.2 软引用与强引用的区别?
3.3 软引用代码展示
3.4 软引用的使用场景?
4. 弱引用类型解析
4.1 弱引用理论解释
4.2 弱引用代码演示
5. 虚引用类型解析
5.1 虚引用构造器展示
5.2 虚引用与弱引用的比较
5.2 虚引用的使用场景?
6. 小小总结
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 方法。
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 方法,在控制台中得到如下结果
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垃圾回收过程,软引用对象就会被清除。
虚引用:比弱引用还要若,通常用作管理对外内存。