在Java对象里,有强弱软虚四种引用,它们都和垃圾回收流程密切相关,在项目里,我们可以通过合理地使用不同类型的引用来优化代码的内存使用性能。
指向通过new得到的内存空间的引用叫强引用。比如有String a = newString(“123”);,其中的a就是一个强引用,它指向了一块内容是123的堆空间。
平时我们用的最多的引用就是强引用,以至于很多人还不知道有其他类型引用的存在,下面我们来说下弱软虚这三种平时不常见(但在关键时刻不可替代)的用途。
软引用和弱引用的用法
软引用(SoftReference)的含义是,如果一个对象只具有软引用,而当前虚拟机堆内存空间足够,那么垃圾回收器就不会回收它,反之就会回收这些软引用指向的对象。
弱引用(WeakReference)与软引用的区别在于,垃圾回收器一旦发现某块内存上只有弱引用(一定请注意只有弱引用,没强引用),不管当前内存空间是否足够,那么都会回收这块内存。
通过下面的ReferenceDemo.java,我们来看下软引用和弱引用的用法,并对比一下它们的差别。
1 import java.lang.ref.SoftReference;
2 import java.lang.ref.WeakReference;
3 public class ReferenceDemo {
4 public static void main(String[] args) {
5 // 强引用
6 String str=new String("abc");
7 SoftReference<String> softRef=new SoftReference<String>(str); // 软引用
8 str = null; // 去掉强引用
9 System.gc(); // 垃圾回收器进行回收
10 System.out.println(softRef.get());
11 // 强引用
12 String abc = new String("123");
13 WeakReference<String> weakRef=new WeakReference<String>(abc); // 弱引用
14 abc = null; // 去掉强引用
15 System.gc(); // 垃圾回收器进行回收
16 System.out.println(weakRef.get());
17 }
18 }
在第8行里,我们定义了SoftReference<String>类型的软引用softRef,用来指向第7行通过new创建的空间,在第14行,我们是通过弱引用weakRef指向第13行创建的空间。
接下来我们通过下表来观察下具体针对内存空间的操作。
行号 | 针对内存的操作以及输出结果 |
6 | 在堆空间里分配一块空间(假设首地址是1000),在其中写入String类型的abc,并用str这个强引用指向这块空间。 |
7 | 用softRef这个软引用指向1000号内存,这时1000号内存上有一个强引用str,一个软引用softRef |
8 | 把1000号内存上的强引用str撤去,此时该块内容上就只有一个软引用softRef |
9 | 通过System.gc(),启动垃圾回收动作 |
10 | 通过softRef.get()输出软引用所指向的值,此时1000号内存上没有强引用,只有一个软引用。但由于此时内存空间足够,所以1000号内存上虽然只有一个软引用,但第9行的垃圾回收代码不会回收1000号的内存,所以这里输出结果是123。 |
12 | 在堆空间里分配一块空间(假设首地址是2000),在其中写入String类型的123,并用abc这个强引用指向这块空间。 |
13 | 用weakRef这个弱引用指向2000号内存,这时2000号内存上有一个强引用abc,一个软引用weakRef |
14 | 把2000号内存上的强引用abc撤去,此时该块内容上就只有一个弱引用weakRef |
15 | 通过System.gc(),启动垃圾回收动作 |
16 | 通过weakRef.get()输出软引用所指向的值,此时2000号内存上没有强引用,只有一个弱引用,所以第15行的垃圾回收代码会回收2000号的内存,所以这里输出结果是null。 |
软引用的使用场景
比如在一个博客管理系统里,为了提升访问性能,在用户在点击博文时,如果这篇博文没有缓存到内存中,则需要做缓存动作,这样其它用户在点击同样这篇文章时,就能直接从内存里装载,而不用走数据库,这样能降低响应时间。
我们可以通过数据库级别的缓存在做到这点,这里也可以通过软引用来实现,具体的实现步骤如下。
(1)可以通过定义Content类来封装博文的内容,其中可以包括文章ID、文章内容、作者、发表时间和引用图片等相关信息。
(2)可以定义一个类型为HashMap<String, SoftReference<Content>>的对象类保存缓存内容,其中键是String类型,表示文章ID,值是指向Content的软引用。
(3)当用户点击某个ID的文章时,根据ID到第二步定义的HashMap里去找,如果找到,而且所对应的SoftReference<Content>值内容不是null,则直接从这里拿数据并做展示动作,这样不用走数据库,可以提升性能。
(4)如果用户点击的某篇文章的ID在HashMap中找不到,或者虽然找到,但对应的值内容是空,那么就从数据库去找,找到后显示这篇文章,同时再把它插入到HashMap中,这里要注意的是,显示后需要撤销掉这个Content类型对象上的强引用,保证它上面只有一个软引用。
下面来分析用软引用有什么好处?假设我们用1G的空间缓存了10000篇文章,这10000篇文章所占的内存空间上只有软引用。如果内存空间足够,可以通过缓存来提升性能。如果内存空间不够,我们可以依次释放这10000篇文章所占的1G内存,释放后不会影响业务流程,最多就是降低些性能。
对比一下,如果这里不用软应用,而是用强引用来缓存,由于不知道文章何时将被点击,我们还无法得知什么时候可以撤销这些文章对象上的强引用,或者即使引入了一套缓存淘汰流程,但这是额外的工作,就没有刚才使用“软引用“那样方便了。
通过WeakHashMap来了解弱引用的使用场景
WeakHashMap和HashMap很相似,可以存储键值对类型的对象,但从它的名称上可以看出,其中的引用是弱引用。通过下面的WeakHashMapDemo.java,我们来看一下它的用法。
1 import java.util.HashMap;
2 import java.util.Iterator;
3 import java.util.Map;
4 import java.util.WeakHashMap;
5 public class WeakHashMapDemo {
6 public static void main(String[] args) throws Exception {
7 String a = new String("a");
8 String b = new String("b");
9 Map weakmap = new WeakHashMap();
10 Map map = new HashMap();
11 map.put(a, "aaa");
12 map.put(b, "bbb");
13 weakmap.put(a, "aaa");
14 weakmap.put(b, "bbb");
15 map.remove(a);
16 a=null;
17 b=null;
18 System.gc();
19 Iterator i = map.entrySet().iterator();
20 while (i.hasNext()) {
21 Map.Entry en = (Map.Entry)i.next(); System.out.println("map:"+en.getKey()+":"+en.getValue());
22 }
23 Iterator j = weakmap.entrySet().iterator();
24 while (j.hasNext()) {
25 Map.Entry en = (Map.Entry)j.next();System.out.println("weakmap:"+en.getKey()+":"+en.getValue());
26 }
27 }
28 }
通过下表,我们来详细说明关键代码的含义。
表 WeakHashMapDemo里针对关键代码的说明
行号 | 针对内存的操作以及输出结果 |
7 | 在堆空间里分配一块空间(假设首地址是1000),在其中写入String类型的a,并用a这个强引用指向这块空间。 |
8 | 在堆空间里分配一块空间(假设首地址是2000),在其中写入String类型的b,并用b这个强引用指向这块空间。 |
11,12 | 在HashMap里了插入两个键值对,其中键分别是a和b引用,这样1000号和2000号内存上就分别多加了一个强引用了(有两个强引用了)。 |
13,14 | 在WeakHashMap里了插入两个键值对,其中键分别是a和b引用,这样1000号和2000号内存上就分别多加了一个弱引用了(有两个强引用,和一个弱引用)。 |
15 | 从HashMap里移出键是a引用的键值对,这时1000号内存上有一个String类型的强引用和一个弱引用。 |
16 | 撤销掉1000号内存上的a这个强引用,此时1000号内存上只有一个弱引用了。 |
17 | 撤销掉2000号内存上的b这个强引用,此时2000号内存上有一个HashMap指向的强引用和一个WeakHashMap指向的弱引用。 |
18 | 通过System.gc()回收内存 |
19~22 | 遍历并打印HashMap里的对象,这里争议不大,在11和12行放入了a和b这两个强引用的键,在第15行移出a,所以会打印map:b:bbb。 |
23~25 | 遍历并打印WeakHashMap里的对象,这里的输出是weakmap:b:bbb。 虽然我们没有从WeakHashMap里移除a这个引用,但之前a所对应的1000号内存上的强引用全都已经被移除,只有一个弱引用,所以在第18行时,1000号内存里的内存已经被回收,所以WeakHashMap里也看不到a了,只能看到b。 |
根据上文和这里的描述,我们知道如果当一个对象上只有弱引用时,这个对象会在下次垃圾回收时被回收,下面我们给出一个弱引用的使用场景。
比如在某个电商网站项目里,我们会用Coupan这个类来保存优惠券信息,在其中我们可以定义优惠券的打折程度,有效日期和所作用的商品范围等信息。当我们从数据库里得到所有的优惠券信息后,会用一个List<Coupan>类型的coupanList对象来存储所有优惠券。
而且,我们想要用一种数据结构来保存一个优惠券对象以及它所关联的所有用户,这时我们可以用WeakHashMap<Coupan, <List<WeakReference <User>>>类型的weakCoupanHM对象。其中它的键是Coupan类型,值是指向List<User>用户列表的弱引用。
大家可以想象下,如果有100个优惠券,那么它们会存储于List<Coupan>类型的coupanList,同时,WeakHashMap<Coupan, <List<WeakReference <User>>>类型的weakCoupanHM对象会以键的形式存储这100个优惠券。而且,如果有1万个用户,那么我们可以用List<User>类型的userList对象来保存它们,假设coupan1这张优惠券对应着100个用户,那么我们一定会通过如下的代码存入这种键值对关系,weakCoupanHM.put(coupan1,weakUserList);,其中weakUserList里以弱引用的方式保存coupan1所对应的100个用户。
这样的话,一旦当优惠券或用户发生变更,它们的对应关系就能自动地更新,具体表现如下。
1 当某个优惠券(假设对应于coupan2对象)失效时,我们可以从coupanList里去除该对象,coupan2上就没有强引用了,只有weakCoupanHM对该对象还有个弱引用,这样coupan2对象能在下次垃圾回收时被回收,从而weakCoupanHM里就看不到了。
2 假设某个优惠券coupan3用弱引用的方式指向于100个用户,当某个用户(假设user1)注销账号时,它会被从List<User>类型的userList对象中被移除。这时该对象上只有weakCoupanHM里的值(也就是<List<WeakReference <User>>)这个弱引用,该对象同样能在下次垃圾回收时被回收,这样coupan3的关联用户就会自动地更新为99个。
如果不用弱引用,而是用常规的HashMap<Coupan,List<User>>来保存对应关系的话,那么一旦出现优惠券或用户的变更的话,那么我们就不得不手动地更新这个表示对应关系的HashMap对象了,这样,代码就会变得复杂,而且我们很有可能因疏忽而忘记在某个位置添加更新代码。相比之下,弱引用给我们带来的“自动更新“就能给我们带来很大的便利。
虚引用以及它的使用场景
虚引用的英文叫法是PhantomReference,关于虚引用,请大家记住它如下的两个特点。
第一,虚引用必须和引用队列 (ReferenceQueue)一起使用。
第二,我们始终无法通过虚引用得到它所指向的值。
通过下面的PhantomReferenceDemo.java,我们来体会下上述两个特点。
1 import java.lang.ref.PhantomReference;
2 import java.lang.ref.ReferenceQueue;
3 public class PhantomReferenceDemo {
4 public static void main(String[] args) {
5 String obj = new String("123"); //强引用
6 ReferenceQueue<String> refQueue = new ReferenceQueue<String>(); //引用队列
7 PhantomReference<String> phantom = new PhantomReference<String>(obj,refQueue);//定义虚引用
8 //不管在什么情况下,虚引用始终返回null
9 System.out.println(phantom.get());
10 }
11 }
在第5行里,我们开辟了一块内存空间,假设首地址是1000,在其中存放了123,并用obj这个强引用指向它。在第6行,我们定义了一个引用队列,在第7行我们定义了一个虚引用,并让它指向obj这个强引用所指向的1000号内存。这里请注意,定义虚引用时,必须要和refQueue这个引用队列一起使用。
在第9行,我们想通过了虚引用来访问1000号内存,这里的结果很奇特,虽然我们1000号内存上还存在一个强引用obj,所以这块内存一定不会被垃圾回收器回收掉,但我们拿到的是null值,从这里我们可以看到,通过虚引用,我们拿到的始终是null值。
既然我们无法通过虚引用有效地得到它所对应的内存空间的值,那么它有什么作用?从上述第7行的代码里我们能看到,在定义虚引用时,我们是会把它放入一个引用队列refQueue。一旦当我们用好虚引用所对应的对象后,而且在垃圾回收器回收这个对象前我们需要做些事情(相当于执行析构函数),这时我们就能从refQueue队列里取出这个虚引用,并执行它的析构动作。
这种场景不多见,但我们还是通过下面的PhantomReferenceCleaner.java来观察下这种用法。
1 //省略必要的import语句
2 public class PhantomReferenceCleaner {
3 public static void main(String[] args) {
4 String abc = new String("123");//强引用
5 ReferenceQueue<String> referenceQueue = new ReferenceQueue<String>();//定义引用队列
6 //定义一个虚引用,并放入引用队列
7 PhantomReference<String> ref = new PhantomReference<String>(abc,referenceQueue);
8 abc = null; //清空强应用
9 System.gc();//启动垃圾回收
10 //从引用队列中获取待回收的对象
11 Object obj = referenceQueue.poll();
12 if (obj != null) {
13 Field rereferentVal = null;
14 try {
15 /通过反射,得到待回收对象里的值
16 rereferentVal = Reference.class
17 .getDeclaredField("referent");
18 rereferentVal.setAccessible(true);
19 System.out.println("Before GC Clear:" + rereferentVal.get(obj).toString());
20 //可以在这里执行其它回收前的动作
21 } catch (Exception e) {
22 e.printStackTrace();
23 }
24 }
25 }
26 }
在第4行里,我们在内存里创建了一块空间,假设地址是1000,并在其中存放了内容123,并用abc这个强引用指向它。在第5行里,我们定义了一个引用队列,在第7行里,我们定义了一个虚引用ref,并用它同样指向了1000号内存块。
在第8行里,我们消除了1000号内存上的强引用,这时由于1000号内存上没有了强引用,所以在第9行里启动了垃圾回收时,这块内存会被回收。
我们在第7行在创建虚引用的同时,把虚引用所指向的String对象放入了引用队列referenceQueue,在第11行,我们从引用队列里拿到了之前放入的对象obj。从第16到19行里,我们通过反射机制,拿到了obj对象的值,从输出上来看,obj里包含着之前1000号内存里的值,也就是123。
也就是说,我们可以用虚引用指向某个强引用指向的对象,这里给的案例中,我们是用虚引用指向String类型的对象,在其它场合,我们也可以指向表示员工信息的Employee类型的对象emp。
由于在定义虚引用时,我们一定会把这个虚引用放入到引用队列,所以当这个强引用(假设这里指向emp对象)所指向的对象被回收后,通过虚引用,我们依旧可以从引用队列里得到这个emp对象。
(重点来了)假设我们在销毁emp对象之前,需要销毁其中的诸如工资等敏感信息,那么我们可以在Employee类里封装一个clear方法,在其中定义一些销毁动作。这样的话,我们可以在emp对象被销毁后,通过poll方法从引用队列里得到这个指向emp的虚引用,并通过类似emp.clear()的调用,完成emp对象里销毁的动作。
上述针对虚引用的描述有些复杂,这里来总结一下它的用法:通过虚引用,我们可以在对象被回收后从引用队列里得到它的引用,并能在合适的代码位置通过这个引用执行针对该对象的析构操作。
这其实和finalize方法很相似,但finalize是在对象被回收前执行,如果在其中写了错误的代码有可能导致对象无法正确地被回收,而通过虚引用,我们能在对象被回收的前提下执行析构操作,而且这个操作不会影响到对象的回收。
由于虚引用的使用场合并不多,所以能清晰地说出这块知识点的人并不多。如果大家能在面试中有条理地说出如下的要点,那么面试官会想,你连这种偏僻(但有用)的知识点都知道,那么你一定“非常精通Java Core中的细节”。
要点1,我们始终无法通过虚引用得到它所指向的值,因为通过虚引用,我们一定只能拿到null值得。
要点2,通过虚引用,我们可以在对象被回收后,通过引用队列调用该对象的析构方法。
要点3,如果不做任何动作,在引用队列referenceQueue里存放的对象的内存是无法被回收的,所以在放入后,一定得通过poll方法把虚引用从引用队列中取出。
要点4,也是最重要的,你可以说你理解虚应用,但如果没有十足的把握,你别说在项目里用过。因为一方面项目里很少有机会用到,另一方面,即使用到了,这个复杂程度估计也不是工作年限在3年之内的程序员能说清楚的。所以在面试时,面试官没指望大家对此有实践经验,但如果大家在理论方面说得很好,而把实际案例说砸了,这样反而会画蛇千足。
往期回顾
Java虚拟机的垃圾收集机制_hsm_computer的博客-CSDN博客通过new我们能在堆区里给指定对象分配内存空间,但Java里没有像C++那样提供类似于delete或free的释放内存空间的方法,虚拟机会在特定的时间点启动垃圾回收机制(GC机制)来回收已经不被使用的堆内存。这就会给一些程序员(尤其是年限在三年以内的初级程序员)造成一种误解,他们往往会认为,既然虚拟机的垃圾回收机制能自动回收,那么对此程序员不需要(也没必要)做什么事。恰恰相反,我们得深入了解堆的结构和垃圾回收流程,并在此基础上提升我们代码的内存使用效率,否则的话,代码运行速度慢还是小事,因内存用尽而https://blog.csdn.net/sxeric/article/details/121845907Java虚拟机(JVM)面试题大全_hsm_computer的博客-CSDN博客_jvm面试题Java内存区域说一下 JVM 的主要组成部分及其作用?JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。.https://blog.csdn.net/sxeric/article/details/121760433
这是我的公众号,其中包含了大量面试文章,同时我自己出了多本Python和Java方面的书籍,会定期在公众号里发书的电子版。请大家关注下我的公众号,谢谢了。