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);git
// 得到一个可读写的随机存取文件对象
RAFile = new RandomAccessFile(filename,"rw");程序员
// 得到相应的文件通道
fc = RAFile.getChannel();算法
// 取得文件的实际大小,以便映像到共享内存
size = (int)fc.size();编程
// 得到共享内存缓冲区,该共享内存可读写
mapBuf = fc.map(FileChannel.MAP_RW,0,size);bootstrap
// 获取头部消息:存取权限 数组
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复活,并且这种对象是能够在回收周期进行回收的。
】