Java中的引用
java中的引用
在Java中,引用是一种神奇的东西,通过引用我们可以完成很多事情。习以为常的我们往往忽略了一些本质的东西。我们浅显的以为引用无非就是用于对象调用的,真的是这样吗?你真正了解了Java中的引用了吗?让我们透过现象看本质,去了解一些更深的东西。在讨论引用之前,我们先聊聊对象这种东西。
对象——一个神奇的物种(我无所不能)
看了这张图也许大家都对对象这玩意儿有了很深刻的印象了,以前一直问对象是什么,对象长什么样,好吧,我告诉大家,对象就长这样(哈哈哈哈。。。。此处省略一万字)。为什么说对象是一个很神奇的物种,就我自己的理解,给出如下解释:
封装的完美性:对象是一个实实在在的个体(在堆中有自己的空间),就如同我们一个完整的人一样,都在这大千世界占据着一个空间。一个对象的生成和一个人诞生很相似,都要经过一个异常复杂的过程。人需要十月怀胎,而一个对象从Java虚拟机接到命令(new : 喂,生一个对象出来)到创建一个对象出来需要经历很大周折,有的需要“千万年”,有的需要“几亿年”,有的可能直接挂掉(想想人家CPU的时间周期,我们的1秒就是人家的百亿年了。。。)。怎么样,对象的创建是不是很不容易,所以创建对象一定要慎重(反射技术就能提前对对象进行剖腹产,很多框架都使用了这种技术,其中Spring中的依赖注入(DI)就是对这种技术的深刻运用。。。)。好了,扯皮了半天,知道了对象是咋来的,该说说为啥说封住的完美了,其实还是类比我们人一样,对象头就好比人的头一样,人要靠五官观四方,听八方,对象也需要他存储的一些比如哈希码,指向类元素的指针等来定位一个对象等等,实例数据区就如同我们的身体四肢,大家可以脑补我们可以干什么。就是由于这种近乎偏执严谨完美的封装,才能让对象在Java世界里徜徉,无所不能。
关于对象无所不能的特性,我就不多赘述了,随着学习的深入,大家会慢慢发现,没有对象干不了的事,或许我该这样表达,我们下意识的都会想着用对象去干些什么,也许这就是Java设计的初衷——万物皆对象。
好了,扯了这么半天,也该说说引用是啥玩意了,在具体的说引用之前,还是放两张图,暖暖肠胃。
下面是访问对象的两种方式,也是指针最直观的运用。
+ 使用句柄访问
使用句柄访问,Java堆中会划分出一块内存来作为句柄池,而reference中存储的就是句柄的地址,句柄包含了对象实例数据与类型数据各自的具体地址信息。
- 优势:reference中存储的是稳定的句柄地址,在对象被移动时(比如Java虚拟机在执行垃圾回收时就经常移动对象)时只会改变实例数据指针,而reference本身不需要修改。
- 劣势:时间太慢,相比于直接指针访问,多了一次定位开销。
- 直接指针访问
使用直接指针访问:reference直接指向Java堆中的实例数据,reference中存储的直接是对象的地址。和第一种对比,优劣可自行观之。
这里也简单说一下为什么会有这两种定位方式:Java程序需要通过栈上的reference来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位,访问堆中对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。
引用是啥玩意儿
在Java学习的基础阶段,我们对引用的概念只停留在初级阶段:也就是引用代表一个对象,或者引用中存放着对象在堆内存中的地址,通过引用可以找到这个对象,并操作对象上的实例数据。这种理解本没有错,不过只是停留在比较浅显的层次。其实,从更深的角度去理解,引用在Java中有着举足轻重的地位。
以前我们在学习C, C++的时候,对于开辟一段内存是一件颇为麻烦的事情,我们动态申请了一段内存,就需要在适当的位置把他free或者delete掉,否则就会出现很多溢出等问题,这就需要程序员更多的去关注内存问题,小心的检查每一份开辟的内存是否关闭,在哪个地方关闭,想想就很痛苦。Java是面向对象的语言,对象的产生就是内存的开辟,这就意味着内存的申请是频繁发生的,在Java中,一个new关键字就代表了一块内存的申请。我们平时new一个对象出来很舒服,而且也不用关心这个对象的内存在什么时候释放,只要尽管用就行了,那我们是否想过,why?
其实这与Java虚拟机的垃圾回收机制有关,垃圾回收机制会帮我们自动的清理一些无用的对象,保存有用的对象,关于垃圾回收机制是很么,垃圾回收算法有哪些,在这里将不多赘述,有兴趣的可以等待我的后续更新。言归正传,说了这么半天,其实大家也应该猜到了,引用与对象的回收有着非常密切的关系,可以说,正是有了引用这玩意儿,垃圾回收才能如此给力(在这里小小的提一下:判断对象是否存活的两种算法——引用计数算法和可达性分析算法)。OK了,说了这么半天引用的厉害之处,也该好好说说什么是引用了。
在JDK1.2之前,Java中引用的传统定义如下:如果Reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。(估计很多人的理解也就到这了……)在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为了四种,分别是:强引用(Strong Reference), 软引用(SoftReference), 弱引用(WeakReference), 虚引用(PhantomReference), 这四种引用强度依次逐渐减弱。下面分别举例介绍这四种引用:
强引用(StrongReference): 强引用是最常见的一种引用,在Java中百分之九十九的都是常引用,通常用new关键字new出来的对象都指向一个强引用。例如Object object = new Object(),object就是new Object()这个对象的强引用,我们知道在Java中,对象是可以被多个引用指向的,只要有一个强引用还在,垃圾收集器永远不会回收掉被引用的对象。
软引用(SoftReference): 软引用是用来描述一些还有用但非必须的对象,强度上弱于强引用,在java.lang.ref包下的SoftReference类维护者软引用。它的作用是告诉垃圾回收器,程序中哪些对象是不那么重要的,在内存空间充足时可以被保留,而在内存空间不足时将会被暂时回收。软引用非常适合创建缓存,在系统内存不足时,缓存将会被释放。
如下就是Java软引用的一个简单示例
//person强引用
Person person = new Person();
System.out.println(person);
//将person这个强引用包装成弱引用
Reference<Person> reference = new SoftReference<Person>(person);
//强引用为空,只剩下弱引用
person = null;
System.gc();
//使用get()方法来获取软引用所指向的对象
System.out.println(reference.get());
//运行结果:
com.reference.Person@7852e922
com.reference.Person@7852e922
- 弱引用(WeakReference): 弱引用也是用来描述非必须对象的,强度上弱于软引用,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。在java.lang.ref包下的WeakReference类维护着弱引用。弱引用的作用在于解决强引用所带来的对象之间在存活时间上的耦合关系,最常见的用途在于集合类中,特别是哈希表中(典型的是HashMap)。哈希表的接口允许使用任何Java对象作为键来使用。当一个键值对被放入到哈希表中之后,哈希表对象本身就有了对这些键和值对象的引用。如果这种引用是强引用的话,那么只要哈希表对象本身还存活,其中所包含的键和值对象是不会被回收的。如果某个存活时间很长的哈希表中包含的键值对很多,最终就有可能消耗掉JVM中全部的内存。对于这种情况就使用弱引用来引用这些对象,这样哈希表中的键和值对象都能被垃圾回收。
Integer i = new Integer(1);
HashMap<Integer, Person> w = new HashMap<>();
//将i这个强引用包装成弱引用
WeakReference<Integer> in = new WeakReference<>(i);
i = null;
//将包装后的引用放入HashMap中
i = in.get();
w.put(i, new Person("xx", 12));
System.out.println("Before gc:" + in.get());
//开启垃圾回收
System.gc();
System.out.println("After gc:" + in.get());
结果:Before gc:1
After gc:null
//使用Java中WeakHashMap就可以避免上面所说的情况,它会自动将键值包装为弱引用类型
WeakHashMap<Integer, Person> weak = new WeakHashMap<>();
- 虚引用(PhantomReference): 虚引用也成幽灵引用或幻影引用,是最弱的一种引用关系。一个对象是否有虚引用存在,完全不会对其生命周期构成影响,也无法通过一个虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在java.lang.ref包下的PhantomReference维护着虚引用。
其实虚引用是一个很偏的知识,与Java的对象终止化机制有关。大家可以去了解,在Java中有一个finalize方法,设计初衷就是一个对象在真正被回收前,执行一些清理的工作(就如同C++中的析构函数)。但是垃圾回收的运行时间是不用固定的,所以清理工作也不是提前预知的。而虚引用就可以解决这个问题,在创建一个虚引用的时候必须指定一个引用队列。当一个对象的finalize方法被调用了之后,这个对象的虚引用就会被加入到队列中,通过检查该队列中的内容就可以知道一个对象是不是准备要被回收了。
//缓冲区代码实现展示
public class PhantomBuffer {
private byte[] data = new byte[0];
private ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>();
private PhantomReference<byte[]> ref = new PhantomReference<byte[]>(data, queue);
public byte[] get(int size) {
if (size <= 0) {
throw new IllegalArgumentException("Wrong buffer size");
}
if (data.length < size) {
data = null;
System.gc(); //强制运行垃圾回收器
try {
queue.remove(); //该方法会阻塞直到队列非空
ref.clear(); //幽灵引用不会自动清空,要手动运行
ref = null;
data = new byte[size];
ref = new PhantomReference<byte[]>(data, queue);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return data;
}
}
解释:每次申请新的缓冲区时,都要确保之前缓冲区的字节数组已经被成功回收。引用队列中的remove()方法会阻塞直到新的虚引用被加入到队列中。
- 引用队列(ReferenceQueue): 在有些情况下,程序会需要在一个对象的可达到性发生变化的时候得到通知。比如某个对象的强引用都已经不存在了,只剩下软引用或是弱引用。但是还需要对引用本身做一些其他的处理。典型的情景是在哈希表中。引用对象是作为WeakHashMap中的键对象的,当其引用的实际对象被垃圾回收之后,就需要把该键值对从哈希表中删除。有了引用队列(ReferenceQueue),就可以方便的获取到这些弱引用对象,将它们从表中删除。在软引用和弱引用对象被添加到队列之前,其对实际对象的引用会被自动清空。通过引用队列的poll/remove方法就可以分别以非阻塞和阻塞的方式获取队列中的引用对象。
OK,讲到这,Java中的引用也就结束了,这篇文章主要就讲了Java中的引用,在其他的语言中也有引用的概念,比如C++中。但我没有对它们进行比较,因为个人觉得没有比较的意义,Java中引用和C++中的引用是两种不同的概念。如果大家实在想了解两者的区别,可以点击下面的第一个链接,看看人家写的,我就懒得写了。。。。