引用类型
引用类型
引用与对象
在 JDK1.2 之前,Java中的定义很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表着一个引用。
Java 中的垃圾回收机制在判断是否回收某个对象的时候,都需要依据“引用”这个概念。
在不同垃圾回收算法中,对引用的判断方式有所不同:
引用计数法:
为每个对象添加一个引用计数器,每当有一个引用指向它时,计数器就加1,当引用失效时,计数器就减1,当计数器为0时,则认为该对象可以被回收(目前在Java中已经弃用这种方式了)。
可达性分析算法:
从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。
JDK1.2 之前,一个对象只有“已被引用”和"未被引用"两种状态,这将无法描述某些特殊情况下的对象,比如,当内存充足时需要保留,而内存紧张时才需要被抛弃的一类对象。
四种引用类型
所以在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。
强引用
强引用是最普遍的一种引用,我们写的代码,99.9999%都是强引用:
Object o =new Object();
只要某个对象有强引用与之关联,这个对象永远不会被回收,即使内存不足,JVM宁愿抛出OOM,也不会去回收。
那么什么时候才可以被回收呢?当强引用和对象之间的关联被中断了,就可以被回收了。
我们可以手动把关联给中断了,方法也特别简单:
o= null;
我们可以手动调用GC,看看如果强引用和对象之间的关联被中断了,资源会不会被回收,为了更方便、更清楚的观察到回收的情况,我们需要新写一个类,然后重写finalize方法,下面我们来进行这个实验:
publicclassStudent{
@Override
protectedvoidfinalize()throwsThrowable{
System.out.println("Student 被回收了");
}
}
publicstaticvoidmain(String[] args){
Student student =newStudent();
student =null;
System.gc();
}
运行结果:
Student被回收了
可以很清楚的看到资源被回收了。
当然,在实际开发中,千万不要重写finalize方法
软引用
下面先来看看如何创建一个软引用:
SoftReference studentSoftReference = new SoftReference(newStudent());
软引用就是把对象用SoftReference包裹一下,当我们需要从软引用对象获得包裹的对象,只要get一下就可以了:
SoftReferencestudentSoftReference=newSoftReference(newStudent());
Student student = studentSoftReference.get();
System.out.println(student);
SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*10]);
//m = null;
System.out.println(m.get());
System.gc();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(m.get());
//再分配一个数组,heap将装不下,这时候系统会垃圾回收,先回收一次,如果不够,会把软引用干掉
byte[] b = new byte[1024*1024*15];
System.out.println(m.get());
特点:
当内存不足,会触发JVM的GC,如果GC后,内存还是不足,就会把软引用的包裹的对象给干掉,也就是只有在内存不足,JVM才会回收该对象。
实验:
SoftReference softReference =newSoftReference(newbyte[1024*1024*10]);
System.out.println(softReference.get());
System.gc();
System.out.println(softReference.get());
byte[] bytes =newbyte[1024*1024*10];
System.out.println(softReference.get());
我定义了一个软引用对象,里面包裹了byte[],byte[]占用了10M,然后又创建了10Mbyte[]。
运行程序,需要带上一个参数:
-Xmx20M
代表最大堆内存是20M。
运行结果:
[B@11d7fff
[B@11d7fff
null
可以很清楚的看到手动完成GC后,软引用对象包裹的byte[]还活的好好的,但是当我们创建了一个10M的byte[]后,最大堆内存不够了,所以把软引用对象包裹的byte[]给干掉了,如果不干掉,就会抛出OOM。
软引用到底有什么用呢?比较适合用作缓存,当内存足够,可以正常的拿到缓存,当内存不够,就会先干掉缓存,不至于马上抛出OOM。
弱引用
弱引用的使用和软引用类似,只是关键字变成了WeakReference:
WeakReference weakReference =newWeakReference(newbyte[1024*1024*10]);
System.out.println(weakReference.get());
弱引用的特点是不管内存是否足够,只要发生GC,都会被回收:
WeakReference weakReference =newWeakReference(newbyte[1]);
System.out.println(weakReference.get());
System.gc();
System.out.println(weakReference.get());运行结果:
[B@11d7fff
null
可以很清楚的看到明明内存还很充足,但是触发了GC,资源还是被回收了。
弱引用在很多地方都有用到,比如ThreadLocal、WeakHashMap。
虚引用
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
publicclassPhantomReference\<T\>extendsReference\<T\>{
/**
* Returns this reference object's referent. Because the referent of a
* phantom reference is always inaccessible, this method always returns
* <code>null</code>.
* @return <code>null</code>
*/
publicTget() { return null; }
publicPhantomReference(Treferent,ReferenceQueue q) {
super(referent, q);
}
}
虚引用又被称为幻影引用,我们来看看它的使用:
ReferenceQueue queue =newReferenceQueue();
PhantomReference reference =newPhantomReference(newbyte[1], queue);
System.out.println(reference.get());
虚引用的使用和上面说的软引用、弱引用的区别还是挺大的,我们先不管ReferenceQueue 是个什么鬼,直接来运行:
null
竟然打印出了null,我们来看看get方法的源码:
publicTget() {
return null;
}
这是几个意思,竟然直接返回了null。
这就是虚引用特点之一了:无法通过虚引用来获取对一个对象的真实引用。
那虚引用存在的意义是什么呢?这就要回到我们上面的代码了,我们把代码复制下,以免大家再次往上翻:
ReferenceQueue queue = new ReferenceQueue();
PhantomReference reference = new PhantomReference(newbyte[1], queue);
System.out.println(reference.get());
创建虚引用对象,我们除了把包裹的对象传了进去,还传了一个ReferenceQueue,从名字就可以看出它是一个队列。
虚引用的特点之二就是 虚引用必须与ReferenceQueue一起使用,当GC准备回收一个对象,如果发现它还有虚引用,就会在回收之前,把这个虚引用加入到与之关联的ReferenceQueue中。
我们来用代码实践下吧:
ReferenceQueue queue = new ReferenceQueue();
List bytes = new ArrayList<>();
PhantomReference reference = new PhantomReference(newStudent(),queue);
newThread(()->{
for(int i =0; i <100;i++ ) {
bytes.add(newbyte[1024*1024]);
}
}).start();
newThread(()->{
while(true) {
Reference poll = queue.poll();
if(poll !=null) {
System.out.println("虚引用被回收了:"+ poll);
}
}
}).start();
Scanner scanner =newScanner(System.in);
scanner.hasNext();
运行结果:
Student被回收了
虚引用被回收了:java.lang.ref.PhantomReference@1ade6f1
我们简单的分析下代码:
第一个线程往集合里面塞数据,随着数据越来越多,肯定会发生GC。
第二个线程死循环,从queue里面拿数据,如果拿出来的数据不是null,就打印出来。
从运行结果可以看到:当发生GC,虚引用就会被回收,并且会把回收的通知放到ReferenceQueue中。
虚引用有什么用呢?在NIO中,就运用了虚引用管理堆外内存。