JDK 1.4开始添加了新的I/O类,引入了一种基于通道和缓冲区执行I/O的新方式,就像Java堆上的内存支持I/O缓冲区一样,NIO添加了对直接ByteBuffer的支持,ByteBuffer受本机内存而不是Java堆的支持,直接ByteBuffer可以直接传递到本机操作系统库函数,以执行I/O,这种情况虽然提高了Java程序在I/O的执行效率,但是会对本机内存进行直接的内存开销。ByteBuffer直接操作和非直接操作的区别如下:
对于在何处存储直接 ByteBuffer 数据,很容易产生混淆。应用程序仍然在 Java 堆上使用一个对象来编排 I/O 操作,但持有该数据的缓冲区将保存在本机内存中,Java 堆对象仅包含对本机堆缓冲区的引用。非直接 ByteBuffer 将其数据保存在 Java 堆上的 byte[] 数组中。直接ByteBuffer对象会自动清理本机缓冲区,但这个过程只能作为Java堆GC的一部分执行,它不会自动影响施加在本机上的压力。GC仅在Java堆被填满,以至于无法为堆分配请求提供服务的时候,或者在Java应用程序中显示请求它发生。
6)线程:
应用程序中的每个线程都需要内存来存储器堆栈(用于在调用函数时持有局部变量并维护状态的内存区域)。每个 Java 线程都需要堆栈空间来运行。根据实现的不同,Java 线程可以分为本机线程和 Java 堆栈。除了堆栈空间,每个线程还需要为线程本地存储(thread-local storage)和内部数据结构提供一些本机内存。尽管每个线程使用的内存量非常小,但对于拥有数百个线程的应用程序来说,线程堆栈的总内存使用量可能非常大。如果运行的应用程序的线程数量比可用于处理它们的处理器数量多,效率通常很低,并且可能导致糟糕的性能和更高的内存占用。
ii.本机内存耗尽:
Java运行时善于以不同的方式来处理Java堆空间的耗尽和本机堆空间的耗尽,但是这两种情形具有类似症状,当Java堆空间耗尽的时候,Java应用程序很难正常运行,因为Java应用程序必须通过分配对象来完成工作,只要Java堆被填满,就会出现糟糕的GC性能,并且抛出OutOfMemoryError。相反,一旦 Java 运行时开始运行并且应用程序处于稳定状态,它可以在本机堆完全耗尽之后继续正常运行,不一定会发生奇怪的行为,因为需要分配本机内存的操作比需要分配 Java 堆的操作少得多。尽管需要本机内存的操作因 JVM 实现不同而异,但也有一些操作很常见:启动线程、加载类以及执行某种类型的网络和文件 I/O。本机内存不足行为与 Java 堆内存不足行为也不太一样,因为无法对本机堆分配进行控制,尽管所有 Java 堆分配都在 Java 内存管理系统控制之下,但任何本机代码(无论其位于 JVM、Java 类库还是应用程序代码中)都可能执行本机内存分配,而且会失败。尝试进行分配的代码然后会处理这种情况,无论设计人员的意图是什么:它可能通过 JNI 接口抛出一个 OutOfMemoryError,在屏幕上输出一条消息,发生无提示失败并在稍后再试一次,或者执行其他操作。
iii.例子:
这篇文章一致都在讲概念,这里既然提到了ByteBuffer,先提供一个简单的例子演示该类的使用:
——[$]使用NIO读取txt文件——
package org.susan.java.io;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class ExplicitChannelRead {
public static void main(String args[]){
FileInputStream fileInputStream;
FileChannel fileChannel;
long fileSize;
ByteBuffer byteBuffer;
try{
fileInputStream = new FileInputStream("D://read.txt");
fileChannel = fileInputStream.getChannel();
fileSize = fileChannel.size();
byteBuffer = ByteBuffer.allocate((int)fileSize);
fileChannel.read(byteBuffer);
byteBuffer.rewind();
for( int i = 0; i < fileSize; i++ )
System.out.print((char)byteBuffer.get());
fileChannel.close();
fileInputStream.close();
}catch(IOException ex){
ex.printStackTrace();
}
}
}
在读取文件的路径放上该txt文件里面写入:Hello World,上边这段代码就是使用NIO的方式读取文件系统上的文件,这段程序的输入就为:
Hello World
——[$]获取ByteBuffer上的字节转换为Byte数组——
package org.susan.java.io;
import java.nio.ByteBuffer;
public class ByteBufferToByteArray {
public static void main(String args[]) throws Exception{
// 从byte数组创建ByteBuffer
byte[] bytes = new byte[10];
ByteBuffer buffer = ByteBuffer.wrap(bytes);
// 在position和limit,也就是ByteBuffer缓冲区的首尾之间读取字节
bytes = new byte[buffer.remaining()];
buffer.get(bytes, 0, bytes.length);
// 读取所有ByteBuffer内的字节
buffer.clear();
bytes = new byte[buffer.capacity()];
buffer.get(bytes, 0, bytes.length);
}
}
上边代码就是从ByteBuffer到byte数组的转换过程,有了这个过程在开发过程中可能更加方便,ByteBuffer的详细讲解我保留到IO部分,这里仅仅是涉及到了一些,所以提供两段实例代码。
iv.共享内存:
在Java语言里面,没有共享内存的概念,但是在某些引用中,共享内存却很受用,例如Java语言的分布式系统,存着大量的Java分布式共享对象,很多时候需要查询这些对象的状态,以查看系统是否运行正常或者了解这些对象目前的一些统计数据和状态。如果使用的是网络通信的方式,显然会增加应用的额外开销,也增加了不必要的应用编程,如果是共享内存方式,则可以直接通过共享内存查看到所需要的对象的数据和统计数据,从而减少一些不必要的麻烦。
1)共享内存特点:
- 可以被多个进程打开访问
- 读写操作的进程在执行读写操作的时候其他进程不能进行写操作
- 多个进程可以交替对某一个共享内存执行写操作
- 一个进程执行了内存写操作过后,不影响其他进程对该内存的访问,同时其他进程对更新后的内存具有可见性
- 在进程执行写操作时如果异常退出,对其他进程的写操作禁止自动解除
- 相对共享文件,数据访问的方便性和效率
2)出现情况:
- 独占的写操作,相应有独占的写操作等待队列。独占的写操作本身不会发生数据的一致性问题;
- 共享的写操作,相应有共享的写操作等待队列。共享的写操作则要注意防止发生数据的一致性问题;
- 独占的读操作,相应有共享的读操作等待队列;
- 共享的读操作,相应有共享的读操作等待队列;
3)Java中共享内存的实现:
JDK 1.4里面的MappedByteBuffer为开发人员在Java中实现共享内存提供了良好的方法,该缓冲区实际上是一个磁盘文件的内存映象,二者的变化会保持同步,即内存数据发生变化过后会立即反应到磁盘文件中,这样会有效地保证共享内存的实现,将共享文件和磁盘文件简历联系的是文件通道类:FileChannel,该类的加入是JDK为了统一外围设备的访问方法,并且加强了多线程对同一文件进行存取的安全性,这里可以使用它来建立共享内存用,它建立了共享内存和磁盘文件之间的一个通道。打开一个文件可使用RandomAccessFile类的getChannel方法,该方法直接返回一个文件通道,该文件通道由于对应的文件设为随机存取,一方面可以进行读写两种操作,另外一个方面使用它不会破坏映象文件的内容。这里,如果使用FileOutputStream和FileInputStream则不能理想地实现共享内存的要求,因为这两个类同时实现自由读写很困难。
下边代码段实现了上边提及的共享内存功能
// 获得一个只读的随机存取文件对象
RandomAccessFile RAFile = new RandomAccessFile(filename,"r");
// 获得相应的文件通道
FileChannel fc = RAFile.getChannel();
// 取得文件的实际大小
int size = (int)fc.size();
// 获得共享内存缓冲区,该共享内存只读
MappedByteBuffer mapBuf = fc.map(FileChannel.MAP_RO,0,size);
// 获得一个可读写的随机存取文件对象
RAFile = new RandomAccessFile(filename,"rw");
// 获得相应的文件通道
fc = RAFile.getChannel();
// 取得文件的实际大小,以便映像到共享内存
size = (int)fc.size();
// 获得共享内存缓冲区,该共享内存可读写
mapBuf = fc.map(FileChannel.MAP_RW,0,size);
// 获取头部消息:存取权限
mode = mapBuf.getInt();
如果多个应用映象使用同一文件名的共享内存,则意味着这多个应用共享了同一内存数据,这些应用对于文件可以具有同等存取权限,一个应用对数据的刷新会更新到多个应用中。为了防止多个应用同时对共享内存进行写操作,可以在该共享内存的头部信息加入写操作标记,该共享文件的头部基本信息至少有:
共享文件的头部信息是私有信息,多个应用可以对同一个共享内存执行写操作,执行写操作和结束写操作的时候,可以使用如下方法:
public boolean startWrite()
{
if(mode == 0) // 这里mode代表共享内存的存取模式,为0代表可写
{
mode = 1; // 意味着别的应用不可写
mapBuf.flip();
mapBuf.putInt(mode); //写入共享内存的头部信息
return true;
}
else{
return false; //表明已经有应用在写该共享内存了,本应用不能够针对共享内存再做写操作
}
}
public boolean stopWrite()
{
mode = 0; // 释放写权限
mapBuf.flip();
mapBuf.putInt(mode); //写入共享内存头部信息
return true;
}
【*:上边提供了对共享内存执行写操作过程的两个方法,这两个方法其实理解起来很简单,真正需要思考的是一个针对存取模式的设置,其实这种机制和最前面提到的内存的锁模式有点类似,一旦当mode(存取模式)设置称为可写的时候,startWrite才能返回true,不仅仅如此,某个应用程序在向共享内存写入数据的时候还会修改其存取模式,因为如果不修改的话就会导致其他应用同样针对该内存是可写的,这样就使得共享内存的实现变得混乱,而在停止写操作stopWrite的时候,需要将mode设置称为1,也就是上边注释段提到的释放写权限。】
关于锁的知识这里简单做个补充【*:上边代码的这种模式可以理解为一种简单的锁模式】:一般情况下,计算机编程中会经常遇到锁模式,在整个锁模式过程中可以将锁分为两类(这里只是辅助理解,不是严格的锁分类)——共享锁和排他锁(也称为独占锁),锁的定位是定位于针对所有与计算机有关的资源比如内存、文件、存储空间等,针对这些资源都可能出现锁模式。在上边堆和栈一节讲到了Java对象锁,其实不仅仅是对象,只要是计算机中会出现写入和读取共同操作的资源,都有可能出现锁模式。
共享锁——当应用程序获得了资源的共享锁的时候,那么应用程序就可以直接访问该资源,资源的共享锁可以被多个应用程序拿到,在Java里面线程之间有时候也存在对象的共享锁,但是有一个很明显的特征,也就是内存共享锁只能读取数据,不能够写入数据,不论是什么资源,当应用程序仅仅只能拿到该资源的共享锁的时候,是不能够针对该资源进行写操作的。
独占锁——当应用程序获得了资源的独占锁的时候,应用程序访问该资源在共享锁上边多了一个权限就是写权限,针对资源本身而言,一个资源只有一把独占锁,也就是说一个资源只能同时被一个应用或者一个执行代码程序允许写操作,Java线程中的对象写操作也是这个道理,若某个应用拿到了独占锁的时候,不仅仅可以读取资源里面的数据,而且可以向该资源进行数据写操作。
数据一致性——当资源同时被应用进行读写访问的时候,有可能会出现数据一致性问题,比如A应用拿到了资源R1的独占锁,B应用拿到了资源R1的共享锁,A在针对R1进行写操作,而两个应用的操作——A的写操作和B的读操作出现了一个时间差,s1的时候B读取了R1的资源,s2的时候A写入了数据修改了R1的资源,s3的时候B又进行了第二次读,而两次读取相隔时间比较短暂而且初衷没有考虑到A在B的读取过程修改了资源,这种情况下针对锁模式就需要考虑到数据一致性问题。独占锁的排他性在这里的意思是该锁只能被一个应用获取,获取过程只能由这个应用写入数据到资源内部,除非它释放该锁,否则其他拿不到锁的应用是无法对资源进行写入操作的。
按照上边的思路去理解代码里面实现共享内存的过程就更加容易理解了。
如果执行写操作的应用异常中止,那么映像文件的共享内存将不再能执行写操作。为了在应用异常中止后,写操作禁止标志自动消除,必须让运行的应用获知退出的应用。在多线程应用中,可以用同步方法获得这样的效果,但是在多进程中,同步是不起作用的。方法可以采用的多种技巧,这里只是描述一可能的实现:采用文件锁的方式。写共享内存应用在获得对一个共享内存写权限的时候,除了判断头部信息的写权限标志外,还要判断一个临时的锁文件是否可以得到,如果可以得到,则即使头部信息的写权限标志为1(上述),也可以启动写权限,其实这已经表明写权限获得的应用已经异常退出,这段代码如下:
// 打开一个临时文件,注意统一共享内存,该文件名必须相同,可以在共享文件名后边添加“.lock”后缀
RandomAccessFile files = new RandomAccessFile("memory.lock","rw");
// 获取文件通道
FileChannel lockFileChannel = files.getChannel();
// 获取文件的独占锁,该方法不产生任何阻塞直接返回
FileLock fileLock = lockFileChannel.tryLock();
// 如果为空表示已经有应用占有了
if( fileLock == null ){
// ...不可写
}else{
// ...可以执行写操作
}
4)共享内存的应用:
在Java中,共享内存一般有两种应用:
[1]永久对象配置——在java服务器应用中,用户可能会在运行过程中配置一些参数,而这些参数需要永久 有效,当服务器应用重新启动后,这些配置参数仍然可以对应用起作用。这就可以用到该文 中的共享内存。该共享内存中保存了服务器的运行参数和一些对象运行特性。可以在应用启动时读入以启用以前配置的参数。
[2]查询共享数据——一个应用(例 sys.java)是系统的服务进程,其系统的运行状态记录在共享内存中,其中运行状态可能是不断变化的。为了随时了解系统的运行状态,启动另一个应用(例 mon.java),该应用查询该共享内存,汇报系统的运行状态。
v.小节:
提供本机内存以及共享内存的知识,主要是为了让读者能够更顺利地理解JVM内部内存模型的物理原理,包括JVM如何和操作系统在内存这个级别进行交互,理解了这些内容就让读者对Java内存模型的认识会更加深入,而且不容易遗忘。其实Java的内存模型远不及我们想象中那么简单,而且其结构极端复杂,看过《Inside JVM》的朋友应该就知道,结合JVM指令集去写点小代码测试.class文件的里层结构也不失为一种好玩的学习方法。
4.防止内存泄漏
Java中会有内存泄漏,听起来似乎是很不正常的,因为Java提供了垃圾回收器针对内存进行自动回收,但是Java还是会出现内存泄漏的。
i.什么是Java中的内存泄漏:
在Java语言中,内存泄漏就是存在一些被分配的对象,这些对象有两个特点:这些对象可达,即在对象内存的有向图中存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象了。如果对象满足这两个条件,该对象就可以判定为Java中的内存泄漏,这些对象不会被GC回收,然而它却占用内存,这就是Java语言中的内存泄漏。Java中的内存泄漏和C++中的内存泄漏还存在一定的区别,在C++里面,内存泄漏的范围更大一些,有些对象被分配了内存空间,但是却不可达,由于C++中没有GC,这些内存将会永远收不回来,在Java中这些不可达对象则是被GC负责回收的,因此程序员不需要考虑这一部分的内存泄漏。二者的图如下:
因此按照上边的分析,Java语言中也是存在内存泄漏的,但是其内存泄漏范围比C++要小很多,因为Java里面有个特殊程序回收所有的不可达对象:垃圾回收器。对于程序员来说,GC基本是透明的,不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低,JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。
举个例子:
——[$]内存泄漏的例子——
package org.susan.java.collection;
import java.util.Vector;
public class VectorMemoryLeak {
public static void main(String args[]){
Vector<String> vector = new Vector<String>();
for( int i = 0; i < 1000; i++ ){
String tempString = new String();
vector.add(tempString);
tempString = null;
}
}
}
从上边这个例子可以看到,循环申请了String对象,并且将申请的对象放入了一个Vector中,如果仅仅是释放对象本身,因为Vector仍然引用了该对象,所以这个对象对CG来说是不可回收的,因此如果对象加入到Vector后,还必须从Vector删除才能够回收,最简单的方式是将Vector引用设置成null。实际上这些对象已经没有用了,但是还是被代码里面的引用引用到了,这种情况GC拿它就没有了任何办法,这样就可以导致了内存泄漏。
【*:Java语言因为提供了垃圾回收器,照理说是不会出现内存泄漏的,Java里面导致内存泄漏的主要原因就是,先前申请了内存空间而忘记了释放。如果程序中存在对无用对象的引用,这些对象就会驻留在内存中消耗内存,因为无法让GC判断这些对象是否可达。如果存在对象的引用,这个对象就被定义为“有效的活动状态”,同时不会被释放,要确定对象所占内存被回收,必须要确认该对象不再被使用。典型的做法就是把对象数据成员设置成为null或者中集合中移除,当局部变量不需要的情况则不需要显示声明为null。】
ii.常见的Java内存泄漏
1)全局集合:
在大型应用程序中存在各种各样的全局数据仓库是很普遍的,比如一个JNDI树或者一个Session table(会话表),在这些情况下,必须注意管理存储库的大小,必须有某种机制从存储库中移除不再需要的数据。
[$]解决:
[1]常用的解决方法是周期运作清除作业,该作业会验证仓库中的数据然后清楚一切不需要的数据
[2]另外一种方式是反向链接计数,集合负责统计集合中每个入口的反向链接数据,这要求反向链接告诉集合合适会退出入口,当反向链接数目为零的时候,该元素就可以移除了。
2)缓存:
缓存一种用来快速查找已经执行过的操作结果的数据结构。因此,如果一个操作执行需要比较多的资源并会多次被使用,通常做法是把常用的输入数据的操作结果进行缓存,以便在下次调用该操作时使用缓存的数据。缓存通常都是以动态方式实现的,如果缓存设置不正确而大量使用缓存的话则会出现内存溢出的后果,因此需要将所使用的内存容量与检索数据的速度加以平衡。
[$]解决:
[1]常用的解决途径是使用java.lang.ref.SoftReference类坚持将对象放入缓存,这个方法可以保证当虚拟机用完内存或者需要更多堆的时候,可以释放这些对象的引用。
3)类加载器:
Java类装载器的使用为内存泄漏提供了许多可乘之机。一般来说类装载器都具有复杂结构,因为类装载器不仅仅是只与"常规"对象引用有关,同时也和对象内部的引用有关。比如数据变量,方法和各种类。这意味着只要存在对数据变量,方法,各种类和对象的类装载器,那么类装载器将驻留在JVM中。既然类装载器可以同很多的类关联,同时也可以和静态数据变量关联,那么相当多的内存就可能发生泄漏。
iii.Java引用【摘录自前边的《Java引用总结》】:
Java中的对象引用主要有以下几种类型:
1)强可及对象(strongly reachable):
可以通过强引用访问的对象,一般来说,我们平时写代码的方式都是使用的强引用对象,比如下边的代码段:
StringBuilder builder= new StringBuilder();
上边代码部分引用obj这个引用将引用内存堆中的一个对象,这种情况下,只要obj的引用存在,垃圾回收器就永远不会释放该对象的存储空间。这种对象我们又成为强引用(Strong references),这种强引用方式就是Java语言的原生的Java引用,我们几乎每天编程的时候都用到。上边代码JVM存储了一个StringBuilder类型的对象的强引用在变量builder呢。强引用和GC的交互是这样的,如果一个对象通过强引用可达或者通过强引用链可达的话这种对象就成为强可及对象,这种情况下的对象垃圾回收器不予理睬。如果我们开发过程不需要垃圾回器回收该对象,就直接将该对象赋为强引用,也是普通的编程方法。
2)软可及对象(softly reachable):
不通过强引用访问的对象,即不是强可及对象,但是可以通过软引用访问的对象就成为软可及对象,软可及对象就需要使用类SoftReference(java.lang.ref.SoftReference)。此种类型的引用主要用于内存比较敏感的高速缓存,而且此种引用还是具有较强的引用功能,当内存不够的时候GC会回收这类内存,因此如果内存充足的时候,这种引用通常不会被回收的。不仅仅如此,这种引用对象在JVM里面保证在抛出OutOfMemory异常之前,设置成为null。通俗地讲,这种类型的引用保证在JVM内存不足的时候全部被清除,但是有个关键在于:垃圾收集器在运行时是否释放软可及对象是不确定的,而且使用垃圾回收算法并不能保证一次性寻找到所有的软可及对象。当垃圾回收器每次运行的时候都可以随意释放不是强可及对象占用的内存,如果垃圾回收器找到了软可及对象过后,可能会进行以下操作:
- 将SoftReference对象的referent域设置成为null,从而使该对象不再引用heap对象。
- SoftReference引用过的内存堆上的对象一律被生命为finalizable。
- 当内存堆上的对象finalize()方法被运行而且该对象占用的内存被释放,SoftReference对象就会被添加到它的ReferenceQueue,前提条件是ReferenceQueue本身是存在的。
既然Java里面存在这样的对象,那么我们在编写代码的时候如何创建这样的对象呢?创建步骤如下:
先创建一个对象,并使用普通引用方式【强引用】,然后再创建一个SoftReference来引用该对象,最后将普通引用设置为null,通过这样的方式,这个对象就仅仅保留了一个SoftReference引用,同时这种情况我们所创建的对象就是SoftReference对象。一般情况下,我们可以使用该引用来完成Cache功能,就是前边说的用于高速缓存,保证最大限度使用内存而不会引起内存泄漏的情况。下边的代码段:
public static void main(String args[])
{
//创建一个强可及对象
A a = new A();
//创建这个对象的软引用SoftReference
SoftReference sr = new SoftReference(a);
//将强引用设置为空,以遍垃圾回收器回收强引用
a = null;
//下次使用该对象的操作
if( sr != null ){
a = (A)sr.get();
}else{
//这种情况就是由于内存过低,已经将软引用释放了,因此需要重新装载一次
a = new A();
sr = new SoftReference(a);
}
}
软引用技术使得Java系统可以更好地管理内存,保持系统稳定,防止内存泄漏,避免系统崩溃,因此在处理一些内存占用大而且生命周期长使用不频繁的对象可以使用该技术。
3)弱可及对象(weakly reachable):
不是强可及对象同样也不是软可及对象,仅仅通过弱引用WeakReference(java.lang.ref.WeakReference)访问的对象,这种对象的用途在于规范化映射(canonicalized mapping),对于生存周期相对比较长而且重新创建的时候开销少的对象,弱引用也比较有用,和软引用对象不同的是,垃圾回收器如果碰到了弱可及对象,将释放WeakReference对象的内存,但是垃圾回收器需要运行很多次才能够找到弱可及对象。弱引用对象在使用的时候,可以配合ReferenceQueue类使用,如果弱引用被回收,JVM就会把这个弱引用加入到相关的引用队列中去。最简单的弱引用方法如以下代码:
WeakReference weakWidget = new WeakReference(classA);
在上边代码里面,当我们使用weakWidget.get()来获取classA的时候,由于弱引用本身是无法阻止垃圾回收的,所以我们也许会拿到一个null为返回。【*:这里提供一个小技巧,如果我们希望取得某个对象的信息,但是又不影响该对象的垃圾回收过程,我们就可以使用WeakReference来记住该对象,一般我们在开发调试器和优化器的时候使用这个是很好的一个手段。】
如果上边的代码部分,我们通过weakWidget.get()返回的是null就证明该对象已经被垃圾回收器回收了,而这种情况下弱引用对象就失去了使用价值,GC就会定义为需要进行清除工作。这种情况下弱引用无法引用任何对象,所以在JVM里面就成为了一个死引用,这就是为什么我们有时候需要通过ReferenceQueue类来配合使用的原因,使用了ReferenceQueue过后,就使得我们更加容易监视该引用的对象,如果我们通过一ReferenceQueue类来构造一个弱引用,当弱引用的对象已经被回收的时候,系统将自动使用对象引用队列来代替对象引用,而且我们可以通过ReferenceQueue类的运行来决定是否真正要从垃圾回收器里面将该死引用(Dead Reference)清除。
弱引用代码段:
//创建普通引用对象
MyObject object = new MyObject();
//创建一个引用队列
ReferenceQueue rq = new ReferenceQueue();
//使用引用队列创建MyObject的弱引用
WeakReference wr = new WeakReference(object,rq);
这里提供两个实在的场景来描述弱引用的相关用法:
[1]你想给对象附加一些信息,于是你用一个 Hashtable 把对象和附加信息关联起来。你不停的把对象和附加信息放入 Hashtable 中,但是当对象用完的时候,你不得不把对象再从 Hashtable 中移除,否则它占用的内存变不会释放。万一你忘记了,那么没有从 Hashtable 中移除的对象也可以算作是内存泄漏。理想的状况应该是当对象用完时,Hashtable 中的对象会自动被垃圾收集器回收,不然你就是在做垃圾回收的工作。
[2]你想实现一个图片缓存,因为加载图片的开销比较大。你将图片对象的引用放入这个缓存,以便以后能够重新使用这个对象。但是你必须决定缓存中的哪些图片不再需要了,从而将引用从缓存中移除。不管你使用什么管理缓存的算法,你实际上都在处理垃圾收集的工作,更简单的办法(除非你有特殊的需求,这也应该是最好的办法)是让垃圾收集器来处理,由它来决定回收哪个对象。
当Java回收器遇到了弱引用的时候有可能会执行以下操作:
- 将WeakReference对象的referent域设置成为null,从而使该对象不再引用heap对象。
- WeakReference引用过的内存堆上的对象一律被生命为finalizable。
- 当内存堆上的对象finalize()方法被运行而且该对象占用的内存被释放,WeakReference对象就会被添加到它的ReferenceQueue,前提条件是ReferenceQueue本身是存在的。
4)清除:
当引用对象的referent域设置为null,并且引用类在内存堆中引用的对象声明为可结束的时候,该对象就可以清除,清除不做过多的讲述
5)虚可及对象(phantomly reachable):
不是强可及对象,也不是软可及对象,同样不是弱可及对象,之所以把虚可及对象放到最后来讲,主要也是因为它的特殊性,有时候我们又称之为“幽灵对象”,已经结束的,可以通过虚引用来访问该对象。我们使用类PhantomReference(java.lang.ref.PhantomReference)来访问,这个类只能用于跟踪被引用对象进行的收集,同样的,可以用于执行per-mortern清除操作。PhantomReference必须与ReferenceQueue类一起使用。需要使用ReferenceQueue是因为它能够充当通知机制,当垃圾收集器确定了某个对象是虚可及对象的时候,PhantomReference对象就被放在了它的ReferenceQueue上,这就是一个通知,表明PhantomReference引用的对象已经结束,可以收集了,一般情况下我们刚好在对象内存在回收之前采取该行为。这种引用不同于弱引用和软引用,这种方式通过get()获取到的对象总是返回null,仅仅当这些对象在ReferenceQueue队列里面的时候,我们可以知道它所引用的哪些对对象是死引用(Dead Reference)。而这种引用和弱引用的区别在于:
弱引用(WeakReference)是在对象不可达的时候尽快进入ReferenceQueue队列的,在finalization方法执行和垃圾回收之前是确实会发生的,理论上这类对象是不正确的对象,但是WeakReference对象可以继续保持Dead状态,
虚引用(PhantomReference)是在对象确实已经从物理内存中移除过后才进入的ReferenceQueue队列,而且get()方法会一直返回null
当垃圾回收器遇到了虚引用的时候将有可能执行以下操作:
- PhantomReference引用过的heap对象声明为finalizable;
- 虚引用在堆对象释放之前就添加到了它的ReferenceQueue里面,这种情况使得我们可以在堆对象被回收之前采取操作【*:再次提醒,PhantomReference对象必须经过关联的ReferenceQueue来创建,就是说必须和ReferenceQueue类配合操作】
看似没有用处的虚引用,有什么用途呢?
- 首先,我们可以通过虚引用知道对象究竟什么时候真正从内存里面移除的,而且这也是唯一的途径。
- 虚引用避过了finalize()方法,因为对于此方法的执行而言,虚引用真正引用到的对象是异常对象,若在该方法内要使用对象只能重建。一般情况垃圾回收器会轮询两次,一次标记为finalization,第二次进行真实的回收,而往往标记工作不能实时进行,或者垃圾回收其会等待一个对象去标记finalization。这种情况很有可能引起MemoryOut,而使用虚引用这种情况就会完全避免。因为虚引用在引用对象的过程不会去使得这个对象由Dead复活,而且这种对象是可以在回收周期进行回收的。
在JVM内部,虚引用比起使用finalize()方法更加安全一点而且更加有效。而finaliaze()方法回收在虚拟机里面实现起来相对简单,而且也可以处理大部分工作,所以我们仍然使用这种方式来进行对象回收的扫尾操作,但是有了虚引用过后我们可以选择是否手动操作该对象使得程序更加高效完美。
iv.防止内存泄漏[来自IBM开发中心]:
1)使用软引用阻止泄漏:
[1]在Java语言中有一种形式的内存泄漏称为对象游离(Object Loitering):
——[$]对象游离——
// 注意,这段代码属于概念说明代码,实际应用中不要模仿
public class LeakyChecksum{
private byte[] byteArray;
public synchronized int getFileCheckSum(String filename)
{
int len = getFileSize(filename);
if( byteArray == null || byteArray.length < len )
byteArray = new byte[len];
readFileContents(filename,byteArray);
// 计算该文件的值然后返回该对象
}
}
上边的代码是类LeakyChecksum用来说明对象游离的概念,里面有一个getFileChecksum()方法用来计算文件内容校验和,getFileCheckSum方法将文件内容读取到缓冲区中计算校验和,更加直观的实现就是简单地将缓冲区作为getFileChecksum中的本地变量分配,但是上边这个版本比这种版本更加“聪明”,不是将缓冲区缓冲在实例中字段中减少内存churn。该“优化”通常不带来预期的好处,对象分配比很多人期望的更加便宜。(还要注意,将缓冲区从本地变量提升到实例变量,使得类若不带有附加的同步,就不再是线程安全的了。直观的实现不需要将 getFileChecksum() 声明为 synchronized,并且会在同时调用时提供更好的可伸缩性。)
这个类存在很多的问题,但是我们着重来看内存泄漏。缓存缓冲区的决定很可能是根据这样的假设得出的,即该类将在一个程序中被调用许多次,因此它应该更加有效,以重用缓冲区而不是重新分配它。但是结果是,缓冲区永远不会被释放,因为它对程序来说总是可及的(除非LeakyChecksum对象被垃圾收集了)。更坏的是,它可以增长,却不可以缩小,所以 LeakyChecksum 将永久保持一个与所处理的最大文件一样大小的缓冲区。退一万步说,这也会给垃圾收集器带来压力,并且要求更频繁的收集;为计算未来的校验和而保持一个大型缓冲区并不是可用内存的最有效利用。LeakyChecksum 中问题的原因是,缓冲区对于 getFileChecksum() 操作来说逻辑上是本地的,但是它的生命周期已经被人为延长了,因为将它提升到了实例字段。因此,该类必须自己管理缓冲区的生命周期,而不是让 JVM 来管理。
这里可以提供一种策略就是使用Java里面的软引用:
弱引用如何可以给应用程序提供当对象被程序使用时另一种到达该对象的方法,但是不会延长对象的生命周期。Reference 的另一个子类——软引用——可满足一个不同却相关的目的。其中弱引用允许应用程序创建不妨碍垃圾收集的引用,软引用允许应用程序通过将一些对象指定为 “expendable” 而利用垃圾收集器的帮助。尽管垃圾收集器在找出哪些内存在由应用程序使用哪些没在使用方面做得很好,但是确定可用内存的最适当使用还是取决于应用程序。如果应用程序做出了不好的决定,使得对象被保持,那么性能会受到影响,因为垃圾收集器必须更加辛勤地工作,以防止应用程序消耗掉所有内存。高速缓存是一种常见的性能优化,允许应用程序重用以前的计算结果,而不是重新进行计算。高速缓存是 CPU 利用和内存使用之间的一种折衷,这种折衷理想的平衡状态取决于有多少内存可用。若高速缓存太少,则所要求的性能优势无法达到;若太多,则性能会受到影响,因为太多的内存被用于高速缓存上,导致其他用途没有足够的可用内存。因为垃圾收集器比应用程序更适合决定内存需求,所以应该利用垃圾收集器在做这些决定方面的帮助,这就是件引用所要做的。如果一个对象惟一剩下的引用是弱引用或软引用,那么该对象是软可及的(softly reachable)。垃圾收集器并不像其收集弱可及的对象一样尽量地收集软可及的对象,相反,它只在真正 “需要” 内存时才收集软可及的对象。软引用对于垃圾收集器来说是这样一种方式,即 “只要内存不太紧张,我就会保留该对象。但是如果内存变得真正紧张了,我就会去收集并处理这个对象。” 垃圾收集器在可以抛出OutOfMemoryError 之前需要清除所有的软引用。通过使用一个软引用来管理高速缓存的缓冲区,可以解决 LeakyChecksum中的问题,如上边代码所示。现在,只要不是特别需要内存,缓冲区就会被保留,但是在需要时,也可被垃圾收集器回收:
——[$]使用软引用修复上边代码段——
public class CachingChecksum
{
private SoftReference<byte[]> bufferRef;
public synchronized int getFileChecksum(String filename)
{
int len = getFileSize(filename);
byte[] byteArray = bufferRef.get();
if( byteArray == null || byteArray.length < len )
{
byteArray = new byte[len];
bufferRef.set(byteArray);
}
readFileContents(filename,byteArray);
}
}
一种廉价缓存:
CachingChecksum使用一个软引用来缓存单个对象,并让 JVM 处理从缓存中取走对象时的细节。类似地,软引用也经常用于 GUI 应用程序中,用于缓存位图图形。是否可使用软引用的关键在于,应用程序是否可从大量缓存的数据恢复。如果需要缓存不止一个对象,您可以使用一个 Map,但是可以选择如何使用软引用。您可以将缓存作为 Map<K, SoftReference<V>> 或SoftReference<Map<K,V>> 管理。后一种选项通常更好一些,因为它给垃圾收集器带来的工作更少,并且允许在特别需要内存时以较少的工作回收整个缓存。弱引用有时会错误地用于取代软引用,用于构建缓存,但是这会导致差的缓存性能。在实践中,弱引用将在对象变得弱可及之后被很快地清除掉——通常是在缓存的对象再次用到之前——因为小的垃圾收集运行得很频繁。对于在性能上非常依赖高速缓存的应用程序来说,软引用是一个不管用的手段,它确实不能取代能够提供灵活终止期、复制和事务型高速缓存的复杂的高速缓存框架。但是作为一种 “廉价(cheap and dirty)” 的高速缓存机制,它对于降低价格是很有吸引力的。正如弱引用一样,软引用也可创建为具有一个相关的引用队列,引用在被垃圾收集器清除时进入队列。引用队列对于软引用来说,没有对弱引用那么有用,但是它们可以用于发出管理警报,说明应用程序开始缺少内存。
2)垃圾回收对引用的处理:
弱引用和软引用都扩展了抽象的 Reference 类虚引用(phantom references),引用对象被垃圾收集器特殊地看待。垃圾收集器在跟踪堆期间遇到一个 Reference 时,不会标记或跟踪该引用对象,而是在已知活跃的 Reference 对象的队列上放置一个 Reference。在跟踪之后,垃圾收集器就识别软可及的对象——这些对象上除了软引用外,没有任何强引用。垃圾收集器然后根据当前收集所回收的内存总量和其他策略考虑因素,判断软引用此时是否需要被清除。将被清除的软引用如果具有相应的引用队列,就会进入队列。其余的软可及对象(没有清除的对象)然后被看作一个根集(root set),堆跟踪继续使用这些新的根,以便通过活跃的软引用而可及的对象能够被标记。处理软引用之后,弱可及对象的集合被识别 —— 这样的对象上不存在强引用或软引用。这些对象被清除和加入队列。所有 Reference 类型在加入队列之前被清除,所以处理事后检查(post-mortem)清除的线程永远不会具有 referent 对象的访问权,而只具有Reference 对象的访问权。因此,当 References 与引用队列一起使用时,通常需要细分适当的引用类型,并将它直接用于您的设计中(与 WeakHashMap 一样,它的 Map.Entry 扩展了 WeakReference)或者存储对需要清除的实体的引用。
3)使用弱引用堵住内存泄漏:
[1]全局Map造成的内存泄漏:
无意识对象保留最常见的原因是使用 Map 将元数据与临时对象(transient object)相关联。假定一个对象具有中等生命周期,比分配它的那个方法调用的生命周期长,但是比应用程序的生命周期短,如客户机的套接字连接。需要将一些元数据与这个套接字关联,如生成连接的用户的标识。在创建 Socket 时是不知道这些信息的,并且不能将数据添加到 Socket 对象上,因为不能控制 Socket 类或者它的子类。这时,典型的方法就是在一个全局 Map 中存储这些信息:
public class SocketManager{
private Map<Socket,User> m = new HashMap<Socket,User>();
public void setUser(Socket s,User u)
{
m.put(s,u);
}
public User getUser(Socket s){
return m.get(s);
}
public void removeUser(Socket s){
m.remove(s);
}
}
SocketManager socketManager;
//...
socketManager.setUser(socket,user);
这种方法的问题是元数据的生命周期需要与套接字的生命周期挂钩,但是除非准确地知道什么时候程序不再需要这个套接字,并记住从 Map 中删除相应的映射,否则,Socket 和 User 对象将会永远留在 Map 中,远远超过响应了请求和关闭套接字的时间。这会阻止 Socket 和User 对象被垃圾收集,即使应用程序不会再使用它们。这些对象留下来不受控制,很容易造成程序在长时间运行后内存爆满。除了最简单的情况,在几乎所有情况下找出什么时候 Socket 不再被程序使用是一件很烦人和容易出错的任务,需要人工对内存进行管理。
[2]弱引用内存泄漏代码:
程序有内存泄漏的第一个迹象通常是它抛出一个 OutOfMemoryError,或者因为频繁的垃圾收集而表现出糟糕的性能。幸运的是,垃圾收集可以提供能够用来诊断内存泄漏的大量信息。如果以 -verbose:gc 或者 -Xloggc 选项调用 JVM,那么每次 GC 运行时在控制台上或者日志文件中会打印出一个诊断信息,包括它所花费的时间、当前堆使用情况以及恢复了多少内存。记录 GC 使用情况并不具有干扰性,因此如果需要分析内存问题或者调优垃圾收集器,在生产环境中默认启用 GC 日志是值得的。有工具可以利用 GC 日志输出并以图形方式将它显示出来,JTune 就是这样的一种工具。观察 GC 之后堆大小的图,可以看到程序内存使用的趋势。对于大多数程序来说,可以将内存使用分为两部分:baseline 使用和 current load 使用。对于服务器应用程序,baseline 使用就是应用程序在没有任何负荷、但是已经准备好接受请求时的内存使用,current load 使用是在处理请求过程中使用的、但是在请求处理完成后会释放的内存。只要负荷大体上是恒定的,应用程序通常会很快达到一个稳定的内存使用水平。如果在应用程序已经完成了其初始化并且负荷没有增加的情况下,内存使用持续增加,那么程序就可能在处理前面的请求时保留了生成的对象。
public class MapLeaker{
public ExecuteService exec = Executors.newFixedThreadPool(5);
public Map<Task,TaskStatus> taskStatus
= Collections.synchronizedMap(new HashMap<Task,TaskStatus>());
private Random random = new Random();
private enum TaskStatus { NOT_STARTED, STARTED, FINISHED };
private class Task implements Runnable{
private int[] numbers = new int[random.nextInt(200)];
public void run()
{
int[] temp = new int[random.nextInt(10000)];
taskStatus.put(this,TaskStatus.STARTED);
doSomework();
taskStatus.put(this,TaskStatus.FINISHED);
}
}
public Task newTask()
{
Task t = new Task();
taskStatus.put(t,TaskStatus.NOT_STARTED);
exec.execute(t);
return t;
}
}
[3]使用弱引用堵住内存泄漏:
SocketManager 的问题是 Socket-User 映射的生命周期应当与 Socket 的生命周期相匹配,但是语言没有提供任何容易的方法实施这项规则。这使得程序不得不使用人工内存管理的老技术。幸运的是,从 JDK 1.2 开始,垃圾收集器提供了一种声明这种对象生命周期依赖性的方法,这样垃圾收集器就可以帮助我们防止这种内存泄漏——利用弱引用。弱引用是对一个对象(称为 referent)的引用的持有者。使用弱引用后,可以维持对 referent 的引用,而不会阻止它被垃圾收集。当垃圾收集器跟踪堆的时候,如果对一个对象的引用只有弱引用,那么这个 referent 就会成为垃圾收集的候选对象,就像没有任何剩余的引用一样,而且所有剩余的弱引用都被清除。(只有弱引用的对象称为弱可及(weakly reachable))WeakReference 的 referent 是在构造时设置的,在没有被清除之前,可以用 get() 获取它的值。如果弱引用被清除了(不管是 referent 已经被垃圾收集了,还是有人调用了 WeakReference.clear()),get() 会返回 null。相应地,在使用其结果之前,应当总是检查get() 是否返回一个非 null 值,因为 referent 最终总是会被垃圾收集的。用一个普通的(强)引用拷贝一个对象引用时,限制 referent 的生命周期至少与被拷贝的引用的生命周期一样长。如果不小心,那么它可能就与程序的生命周期一样——如果将一个对象放入一个全局集合中的话。另一方面,在创建对一个对象的弱引用时,完全没有扩展 referent 的生命周期,只是在对象仍然存活的时候,保持另一种到达它的方法。弱引用对于构造弱集合最有用,如那些在应用程序的其余部分使用对象期间存储关于这些对象的元数据的集合——这就是 SocketManager 类所要做的工作。因为这是弱引用最常见的用法,WeakHashMap 也被添加到 JDK 1.2 的类库中,它对键(而不是对值)使用弱引用。如果在一个普通 HashMap 中用一个对象作为键,那么这个对象在映射从 Map 中删除之前不能被回收,WeakHashMap 使您可以用一个对象作为 Map 键,同时不会阻止这个对象被垃圾收集。下边的代码给出了 WeakHashMap 的 get() 方法的一种可能实现,它展示了弱引用的使用:
public class WeakHashMap<K,V> implements Map<K,V>
{
private static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V>
{
private V value;
private final int hash;
private Entry<K,V> next;
// ...
}
public V get(Object key)
{
int hash = getHash(key);
Entry<K,V> e = getChain(hash);
while(e != null)
{
k eKey = e.get();
if( e.hash == hash && (key == eKey || key.equals(eKey)))
return e.value;
e = e.next;
}
return null;
}
}
调用 WeakReference.get() 时,它返回一个对 referent 的强引用(如果它仍然存活的话),因此不需要担心映射在 while 循环体中消失,因为强引用会防止它被垃圾收集。WeakHashMap 的实现展示了弱引用的一种常见用法——一些内部对象扩展 WeakReference。其原因在下面一节讨论引用队列时会得到解释。在向 WeakHashMap 中添加映射时,请记住映射可能会在以后“脱离”,因为键被垃圾收集了。在这种情况下,get() 返回 null,这使得测试 get() 的返回值是否为 null 变得比平时更重要了。
[4]使用WeakHashMap堵住泄漏
在 SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就行了,如下边代码所示。(如果 SocketManager 需要线程安全,那么可以用 Collections.synchronizedMap() 包装 WeakHashMap)。当映射的生命周期必须与键的生命周期联系在一起时,可以使用这种方法。不过,应当小心不滥用这种技术,大多数时候还是应当使用普通的 HashMap 作为 Map 的实现。
public class SocketManager{
private Map<Socket,User> m = new WeakHashMap<Socket,User>();
public void setUser(Socket s, User s)
{
m.put(s,u);
}
public User getUser(Socket s)
{
return m.get(s);
}
}
引用队列:
WeakHashMap 用弱引用承载映射键,这使得应用程序不再使用键对象时它们可以被垃圾收集,get() 实现可以根据 WeakReference.get() 是否返回 null 来区分死的映射和活的映射。但是这只是防止 Map 的内存消耗在应用程序的生命周期中不断增加所需要做的工作的一半,还需要做一些工作以便在键对象被收集后从 Map 中删除死项。否则,Map 会充满对应于死键的项。虽然这对于应用程序是不可见的,但是它仍然会造成应用程序耗尽内存,因为即使键被收集了,Map.Entry 和值对象也不会被收集。可以通过周期性地扫描 Map,对每一个弱引用调用 get(),并在 get() 返回 null 时删除那个映射而消除死映射。但是如果 Map 有许多活的项,那么这种方法的效率很低。如果有一种方法可以在弱引用的 referent 被垃圾收集时发出通知就好了,这就是引用队列的作用。引用队列是垃圾收集器向应用程序返回关于对象生命周期的信息的主要方法。弱引用有两个构造函数:一个只取 referent 作为参数,另一个还取引用队列作为参数。如果用关联的引用队列创建弱引用,在 referent 成为 GC 候选对象时,这个引用对象(不是referent)就在引用清除后加入 到引用队列中。之后,应用程序从引用队列提取引用并了解到它的 referent 已被收集,因此可以进行相应的清理活动,如去掉已不在弱集合中的对象的项。(引用队列提供了与 BlockingQueue 同样的出列模式 ——polled、timed blocking 和 untimed blocking。)WeakHashMap 有一个名为 expungeStaleEntries() 的私有方法,大多数 Map 操作中会调用它,它去掉引用队列中所有失效的引用,并删除关联的映射。
4)关于Java中引用思考:
先观察一个列表:
级别 |
回收时间 |
用途 |
生存时间 |
强引用 |
从来不会被回收 |
对象的一般状态 |
JVM停止运行时终止 |
软引用 |
在内存不足时 |
在客户端移除对象引用过后,除非再次激活,否则就放在内存敏感的缓存中 |
内存不足时终止 |
弱引用 |
在垃圾回收时,也就是客户端已经移除了强引用,但是这种情况下内存还是客户端引用可达的 |
阻止自动删除不需要用的对象 |
GC运行后终止 |
虚引用[幽灵引用] |
对象死亡之前,就是进行finalize()方法调用附近 |
特殊的清除过程 |
不定,当finalize()函数运行过后再回收,有可能之前就已经被回收了。 |
可以这样理解: SoftReference:假定垃圾回收器确定在某一时间点某个对象是软可到达对象。这时,它可以选择自动清除针对该对象的所有软引用,以及通过强引用链,从其可以到达该对象的针对任何其他软可到达对象的所有软引用。在同一时间或晚些时候,它会将那些已经向引用队列注册的新清除的软引用加入队列。 软可到达对象的所有软引用都要保证在虚拟机抛出 OutOfMemoryError 之前已经被清除。否则,清除软引用的时间或者清除不同对象的一组此类引用的顺序将不受任何约束。然而,虚拟机实现不鼓励清除最近访问或使用过的软引用。 此类的直接实例可用于实现简单缓存;该类或其派生的子类还可用于更大型的数据结构,以实现更复杂的缓存。只要软引用的指示对象是强可到达对象,即正在实际使用的对象,就不会清除软引用。例如,通过保持最近使用的项的强指示对象,并由垃圾回收器决定是否放弃剩余的项,复杂的缓存可以防止放弃最近使用的项。一般来说,WeakReference我们用来防止内存泄漏,保证内存对象被VM回收。
WeakReference:弱引用对象,它们并不禁止其指示对象变得可终结,并被终结,然后被回收。弱引用最常用于实现规范化的映射。假定垃圾回收器确定在某一时间点上某个对象是弱可到达对象。这时,它将自动清除针对此对象的所有弱引用,以及通过强引用链和软引用,可以从其到达该对象的针对任何其他弱可到达对象的所有弱引用。同时它将声明所有以前的弱可到达对象为可终结的。在同一时间或晚些时候,它将那些已经向引用队列注册的新清除的弱引用加入队列。 SoftReference多用作来实现cache机制,保证cache的有效性。
PhantomReference:虚引用对象,在回收器确定其指示对象可另外回收之后,被加入队列。虚引用最常见的用法是以某种可能比使用 Java 终结机制更灵活的方式来指派 pre-mortem 清除操作。如果垃圾回收器确定在某一特定时间点上虚引用的指示对象是虚可到达对象,那么在那时或者在以后的某一时间,它会将该引用加入队列。为了确保可回收的对象仍然保持原状,虚引用的指示对象不能被检索:虚引用的 get 方法总是返回 null。与软引用和弱引用不同,虚引用在加入队列时并没有通过垃圾回收器自动清除。通过虚引用可到达的对象将仍然保持原状,直到所有这类引用都被清除,或者它们都变得不可到达。
以下是不确定概念
【*:Java引用的深入部分一直都是讨论得比较多的话题,上边大部分为摘录整理,这里再谈谈我个人的一些看法。从整个JVM框架结构来看,Java的引用和垃圾回收器形成了针对Java内存堆的一个对象的“闭包管理集”,其中在基本代码里面常用的就是强引用,强引用主要使用目的是就是编程的正常逻辑,这是所有的开发人员最容易理解的,而弱引用和软引用的作用是比较耐人寻味的。按照引用强弱,其排序可以为:强引用——软引用——弱引用——虚引用,为什么这样写呢,实际上针对垃圾回收器而言,强引用是它绝对不会随便去动的区域,因为在内存堆里面的对象,只有当前对象不是强引用的时候,该对象才会进入垃圾回收器的目标区域。
软引用又可以理解为“内存应急引用”,也就是说它和GC是完整地配合操作的,为了防止内存泄漏,当GC在回收过程出现内存不足的时候,软引用会被优先回收,从垃圾回收算法上讲,软引用在设计的时候是很容易被垃圾回收器发现的。为什么软引用是处理告诉缓存的优先选择的,主要有两个原因:第一,它对内存非常敏感,从抽象意义上讲,我们甚至可以任何它和内存的变化紧紧绑定到一起操作的,因为内存一旦不足的时候,它会优先向垃圾回收器报警以提示内存不足;第二,它会尽量保证系统在OutOfMemoryError之前将对象直接设置成为不可达,以保证不会出现内存溢出的情况;所以使用软引用来处理Java引用里面的高速缓存是很不错的选择。其实软引用不仅仅和内存敏感,实际上和垃圾回收器的交互也是敏感的,这点可以这样理解,因为当内存不足的时候,软引用会报警,而这种报警会提示垃圾回收器针对目前的一些内存进行清除操作,而在有软引用存在的内存堆里面,垃圾回收器会第一时间反应,否则就会MemoryOut了。按照我们正常的思维来考虑,垃圾回收器针对我们调用System.gc()的时候,是不会轻易理睬的,因为仅仅是收到了来自强引用层代码的请求,至于它是否回收还得看JVM内部环境的条件是否满足,但是如果是软引用的方式去申请垃圾回收器会优先反应,只是我们在开发过程不能控制软引用对垃圾回收器发送垃圾回收申请,而JVM规范里面也指出了软引用不会轻易发送申请到垃圾回收器。这里还需要解释的一点的是软引用发送申请不是说软引用像我们调用System.gc()这样直接申请垃圾回收,而是说软引用会设置对象引用为null,而垃圾回收器针对该引用的这种做法也会优先响应,我们可以理解为是软引用对象在向垃圾回收器发送申请。反应快并不代表垃圾回收器会实时反应,还是会在寻找软引用引用到的对象的时候遵循一定的回收规则,反应快在这里的解释是相对强引用设置对象为null,当软引用设置对象为null的时候,该对象的被收集的优先级比较高。
弱引用是一种比软引用相对复杂的引用,其实弱引用和软引用都是Java程序可以控制的,也就是说可以通过代码直接使得引用针对弱可及对象以及软可及对象是可引用的,软引用和弱引用引用的对象实际上通过一定的代码操作是可重新激活的,只是一般不会做这样的操作,这样的用法违背了最初的设计。弱引用和软引用在垃圾回收器的目标范围有一点点不同的就是,使用垃圾回收算法是很难找到弱引用的,也就是说弱引用用来监控垃圾回收的整个流程也是一种很好的选择,它不会影响垃圾回收的正常流程,这样就可以规范化整个对象从设置为null了过后的一个生命周期的代码监控。而且因为弱引用是否存在对垃圾回收整个流程都不会造成影响,可以这样认为,垃圾回收器找得到弱引用,该引用的对象就会被回收,如果找不到弱引用,一旦等到GC完成了垃圾回收过后,弱引用引用的对象占用的内存也会自动释放,这就是软引用在垃圾回收过后的自动终止。
最后谈谈虚引用,虚引用应该是JVM里面最厉害的一种引用,它的厉害在于它可以在对象的内存从物理内存中清除掉了过后再引用该对象,也就是说当虚引用引用到对象的时候,这个对象实际已经从物理内存堆中清除掉了,如果我们不用手动对对象死亡或者濒临死亡进行处理的话,JVM会默认调用finalize函数,但是虚引用存在于该函数附近的生命周期内,所以可以手动对对象的这个范围的周期进行监控。它之所以称为“幽灵引用”就是因为该对象的物理内存已经不存在的,我个人觉得JVM保存了一个对象状态的镜像索引,而这个镜像索引里面包含了对象在这个生命周期需要的所有内容,这里的所需要就是这个生命周期内需要的对象数据内容,也就是对象死亡和濒临死亡之前finalize函数附近,至于强引用所需要的其他对象附加内容是不需要在这个镜像里面包含的,所以即使物理内存不存在,还是可以通过虚引用监控到该对象的,只是这种情况是否可以让对象重新激活为强引用我就不敢说了。因为虚引用在引用对象的过程不会去使得这个对象由Dead复活,而且这种对象是可以在回收周期进行回收的。】