合理使用软引用和弱引用,提升JVM内存使用性能

时间:2024-01-23 13:09:33

    在项目运行时,OOM异常是比较处理的,因为从日志看出的发生异常的代码点可能仅仅是最后一根稻草,从中可能未必能发现OOM的原因,而且OOM未必是固定重现的。

    上医治未病,与其等OOM问题发生时再通过看日志等手段判断问题,还不如在开发时就注意内存的使用性能,在本人的其它博客里,提到过如何在代码里提升内存使用性能的操作要点,在这篇博文里,就从强弱软引用这个角度来给出些使用技巧。

 

1 内存对象上如果有强引用,该对象就无法被回收(回收的条件)

    不论是轻量级回收还是Full GC,我们都无法回避这样一个问题:Java虚拟机如何判断一个对象可以被回收?

    标准非常简单,当某个对象上没有强引用时,该对象就可以被回收。不过,在绝大多数的场景里,当某对象上的最后一个    强引用被撤去后,该对象不会被立即回收,而是会在下次启动垃圾回收机制时被回收。

在JDK的早期版本里,是用“引用计数法”来判断对象上是否有强引用,具体来讲,当一个对象上有一个强引用时,把该对象的引用计数值加1,反之则减1。

          1      String a = new String(“123”); //包含123内容的对象上的引用数加1

          2      a = null; //引用数减1

    这里请大家区分“引用”和“值”的差别。比如通过上述代码的第1行,我们会在堆空间里分配一块空间,假设内存首地址是1000,在其中存放了123这个内容,而且通过一个引用a指向这块空间,这时,1000号内存的引用数是1。

    在第2行里,我们并不是把1000号内存里的值设置成null(初学者往往会有这样错误的理解),而是把a这个引用指向null。这时,虽然1000号内存的值没变,但没有引用指向它了,它的引用计数值就变0了。 在这种情况下,下次垃圾回收机制启动时,1000号内存就会被回收。

    引用计数法的好处是简单,但缺点是无法回收循环引用的对象,比如a引用指向b,b指向c,c再指向a,在这种情况下,哪怕它们游离于主程序之外了(程序不再用到它们了),abc三个引用的计数值都是1,这样它们就始终无法被回收。

    正是因为有这样的原因,在后继的JDK版本里,引入了“根搜索算法”(Tracing Collector)。

 

 

    在这个算法里,将从一个根节点(GC ROOT)开始,寻找它所对应的引用节点,找到这个节点后,继续寻找该节点的引用节点,以此类推。这样当所有的引用节点都搜索完毕后,剩下的就是没有被引用的节点,也就是可以回收的节点。比如在上图里,从根节点里能找到a,b,c和d这四个节点,而u1和u2这两个节点属于不可达,也就是可以被回收。

    具体来讲,可作为GC Root的对象有如下四个。第一,虚拟机栈中引用的对象。第二,方法区中静态属性引用的对象。第三, 方法区中常量引用的对象。第四,本地方法栈中引用的对象。

     一般来说,指向通过new得到的内存空间的引用叫强引用。比如有String a = newString(“123”);,其中的a就是一个强引用,它指向了一块内容是123的堆空间。

 

2 软引用和弱引用的用法

    软引用(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行创建的空间。接下来我们通过下表来观察下具体针对内存空间的操作。

    表 ReferenceDemo里针对内存空间的操作归纳表

行号

针对内存的操作以及输出结果

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。

 

3 软引用的使用场景

    比如在一个博客管理系统里,为了提升访问性能,在用户在点击博文时,如果这篇博文没有缓存到内存中,则需要做缓存动作,这样其它用户在点击同样这篇文章时,就能直接从内存里装载,而不用走数据库,这样能降低响应时间。

    我们可以通过数据库级别的缓存在做到这点,这里也可以通过软引用来实现,具体的实现步骤如下。

    第一,可以通过定义Content类来封装博文的内容,其中可以包括文章ID、文章内容、作者、发表时间和引用图片等相关信息。

    第二,可以定义一个类型为HashMap<String, SoftReference<Content>>的对象类保存缓存内容,其中键是String类型,表示文章ID,值是指向Content的软引用。

    第三,当用户点击某个ID的文章时,根据ID到第二步定义的HashMap里去找,如果找到,而且所对应的SoftReference<Content>值内容不是null,则直接从这里拿数据并做展示动作,这样不用走数据库,可以提升性能。

    第四,如果用户点击的某个文章的ID在HashMap里找不到,或者虽然找到,但对应的值内容是空,那么就从数据库去找,找到后显示这个文章,同时再把它插入到HashMap里,这里请注意,显示后需要撤销掉这个Content类型对象上的强引用,保证它上面只有一个软引用。

    来分析下用软引用有什么好处?假设我们用1个G的空间缓存了10000篇文章,这10000篇文章所占的内存空间上只有软引用。如果内存空间足够,那么我们可以通过缓存来提升性能,但万一内存空间不够,我们可以依次释放这10000篇文章所占的1G内存,释放后不会影响业务流程,最多就是降低些性能。

    对比一下,如果我们这里不用软应用,而是用强引用来缓存,由于不知道文章何时将被点击,我们还无法得知什么时候可以撤销这些文章对象上的强引用,或者即使我们引入了一套缓存淘汰流程,但这就是额外的工作了,这就没刚才使用“软引用“那样方便了。

 

4 通过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对象了,这样,代码就会变得复杂,而且我们很有可能因疏忽而忘记在某个位置添加更新代码。相比之下,弱引用给我们带来的“自动更新“就能给我们带来很大的便利。