初学者或初级程序员在面试时如果能证明自己具有分析内存用量和内存调优的能力, 这相当有利, 因为这是针对 5 年左右相关经验的高级程序员的要求而对于高级程序员来说, 如果能在面试时让面试官感觉你确实做过内存调优的工作, 那么面试官很有可能不问 Java Core 部分的其它问题了, 毕竟虚拟机调优是 Java Core 部分非常资深的知识点
在 Java 对象里, 有强弱软虚四种引用, 它们都和垃圾回收流程密切相关, 在项目里, 我们可以通过合理地使用不同类型的引用来优化代码的内存使用性能
指向通过 new 得到的内存空间的引用叫强引用比如有 String a = newString(123);, 其中的 a 就是一个强引用, 它指向了一块内容是 123 的堆空间
平时我们用的最多的引用就是强引用, 以至于很多人还不知道有其他类型引用的存在, 下面我们来说下弱软虚这三种平时不常见 (但在关键时刻不可替代) 的用途
1 软引用和弱引用的用法
软引用 (SoftReference) 的含义是, 如果一个对象只具有软引用, 而当前虚拟机堆内存空间足够, 那么垃圾回收器就不会回收它, 反之就会回收这些软引用指向的对象
弱引用 (WeakReference) 与软引用的区别在于, 垃圾回收器一旦发现某块内存上只有弱引用(一定请注意只有弱引用, 没强引用), 不管当前内存空间是否足够, 那么都会回收这块内存
通过下面的 ReferenceDemo.java, 我们来看下软引用和弱引用的用法, 并对比一下它们的差别
- import java.lang.ref.SoftReference;
- import java.lang.ref.WeakReference;
- public class ReferenceDemo {
- public static void main(String[] args) {
- // 强引用
- String str=new String("abc");
- SoftReference<String> softRef=new SoftReference<String>(str); // 软引用
- str = null; // 去掉强引用
- System.gc(); // 垃圾回收器进行回收
- System.out.println(softRef.get());
- // 强引用
- String abc = new String("123");
- WeakReference<String> weakRef=new WeakReference<String>(abc); // 弱引用
- abc = null; // 去掉强引用
- System.gc(); // 垃圾回收器进行回收
- System.out.println(weakRef.get());
- }
- }
在第 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。 |
2 软引用的使用场景
比如在一个博客管理系统里, 为了提升访问性能, 在用户在点击博文时, 如果这篇博文没有缓存到内存中, 则需要做缓存动作, 这样其它用户在点击同样这篇文章时, 就能直接从内存里装载, 而不用走数据库, 这样能降低响应时间
我们可以通过数据库级别的缓存在做到这点, 这里也可以通过软引用来实现, 具体的实现步骤如下
第一, 可以通过定义 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 内存, 释放后不会影响业务流程, 最多就是降低些性能
对比一下, 如果我们这里不用软应用, 而是用强引用来缓存, 由于不知道文章何时将被点击, 我们还无法得知什么时候可以撤销这些文章对象上的强引用, 或者即使我们引入了一套缓存淘汰流程, 但这就是额外的工作了, 这就没刚才使用软引用那样方便了
3 通过 WeakHashMap 来了解弱引用的使用场景
WeakHashMap 和 HashMap 很相似, 可以存储键值对类型的对象, 但我们可以从它的名字上看出, 其中的引用是弱引用通过下面的 WeakHashMapDemo.java, 我们来看下它的用法
- import java.util.HashMap;
- import java.util.Iterator;
- import java.util.Map;
- import java.util.WeakHashMap;
- public class WeakHashMapDemo {
- public static void main(String[] args) throws Exception {
- String a = new String("a");
- String b = new String("b");
- Map weakmap = new WeakHashMap();
- Map map = new HashMap();
- map.put(a, "aaa");
- map.put(b, "bbb");
- weakmap.put(a, "aaa");
- weakmap.put(b, "bbb");
- map.remove(a);
- a = null;
- b = null;
- System.gc();
- Iterator i = map.entrySet().iterator();
- while (i.hasNext()) {
- Map.Entry en = (Map.Entry) i.next();
- System.out.println("map:" + en.getKey() + ":" + en.getValue());
- }
- Iterator j = weakmap.entrySet().iterator();
- while (j.hasNext()) {
- Map.Entry en = (Map.Entry) j.next();
- System.out.println("weakmap:" + en.getKey() + ":" + en.getValue());
- }
- }
- }
通过下表, 我们来详细说明关键代码的含义
行号 | 针对内存的操作以及输出结果 |
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 对象了, 这样, 代码就会变得复杂, 而且我们很有可能因疏忽而忘记在某个位置添加更新代码相比之下, 弱引用给我们带来的自动更新就能给我们带来很大的便利
4 不能投机取巧, 但面试确实有技巧
笔者写本文的意思, 不是让大家投机取巧, 事实上, 如果大家只知道这些知识, 而不知道其他虚拟机 (或 Java Core) 相关的知识点, 面试通过的可能性很低
但话说回来, 如果大家在平时开发时积累了很多经验, 但不会总结, 在面试时也无法很好地展示各种能力, 这样也是非常可惜的
根据本人在培训学校的经验, 首先通过可能掌握各种 Java 技能, 在这个基础上再讲述上述软引用和弱引用的技能, 这些候选人得到的反馈是, 至少在 Java Core 方面比较精通
在本文的其它博客里, 也列了相关面试技巧, 欢迎大家看其它的文章
来源: https://www.cnblogs.com/JavaArchitect/p/8685993.html