-
2019-03-26 15:48:03
Java对象的引用
一、概念,什么是Java对象的引用?
每种编程语言都有自己的数据处理方式。有些时候,程序员必须注意将要处理的数据是什么类型。你是直接操纵元素,还是用某种基于特殊语法的间接表示(例如C/C++里的指针)来操作对象。所有这些在 Java 里都得到了简化,一切都被视为对象。因此,我们可采用一种统一的语法。尽管将一切都“看作”对象,但操纵的标识符实际是指向一个对象的“引用”(reference)。 对Java对象的引用,是描述的定义。
二、Java对象引用的目的
Java中提供这四种引用类型主要有两个目的:
第一是 可以让程序员通过代码的方式决定某些对象的生命周期
第二是 有利于JVM进行垃圾回收三、四中Java对象的引用
Java对象的引用包括:强引用,软引用,弱引用,虚引用
强引用:是指创建一个对象并把这个对象赋给一个引用变量。
软引用:如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;
如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被 程序使用。软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。
SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。
弱引用:WeakReference弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
虚引用:虚引用(PhantomReference) 虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收 。要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
更多相关内容 -
java对象引用及对象赋值-java对象克隆
2018-08-09 09:54:08一、Java对象及其引用        初学Java,总是会自觉或不...假如说你想复制一个简单变量。很简单:
int apples = 5; int pears = apples;
不仅仅是int类型,其它七种原始数据类型(boolean,char,byte,short,float,double.long)同样适用于该类情况。
但是如果你复制的是一个对象,情况就有些复杂了。
假设说我是一个beginner,我会这样写:
class Student { private int number; public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } } public class Test { public static void main(String args[]) { Student stu1 = new Student(); stu1.setNumber(12345); Student stu2 = stu1; System.out.println("学生1:" + stu1.getNumber()); System.out.println("学生2:" + stu2.getNumber()); } }
结果:
学生1:12345
学生2:12345这里我们自定义了一个学生类,该类只有一个number字段。
我们新建了一个学生实例,然后将该值赋值给stu2实例。(Student stu2 = stu1;)
再看看打印结果,作为一个新手,拍了拍胸腹,对象复制不过如此,
难道真的是这样吗?
我们试着改变stu2实例的number字段,再打印结果看看:
stu2.setNumber(54321); System.out.println("学生1:" + stu1.getNumber()); System.out.println("学生2:" + stu2.getNumber());
结果:
学生1:54321
学生2:54321
这就怪了,为什么改变学生2的学号,学生1的学号也发生了变化呢?
原因出在(stu2 = stu1) 这一句。该语句的作用是将stu1的引用赋值给stu2,
这样,stu1和stu2指向内存堆中同一个对象。如图:
那么,怎样才能达到复制一个对象呢?
是否记得万类之王Object。它有11个方法,有两个protected的方法,其中一个为clone方法。
在Java中所有的类都是缺省的继承自Java语言包中的Object类的,查看它的源码,你可以把你的JDK目录下的src.zip复制到其他地方然后解压,里面就是所有的源码。发现里面有一个访问限定符为protected的方法clone():
/* Creates and returns a copy of this object. The precise meaning of "copy" may depend on the class of the object. The general intent is that, for any object x, the expression: 1) x.clone() != x will be true 2) x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements. 3) x.clone().equals(x) will be true, this is not an absolute requirement. */ protected native Object clone() throws CloneNotSupportedException;
仔细一看,它还是一个native方法,大家都知道native方法是非Java语言实现的代码,供Java程序调用的,因为Java程序是运行在JVM虚拟机上面的,要想访问到比较底层的与操作系统相关的就没办法了,只能由靠近操作系统的语言来实现。
- 第一次声明保证克隆对象将有单独的内存地址分配。
- 第二次声明表明,原始和克隆的对象应该具有相同的类类型,但它不是强制性的。
- 第三声明表明,原始和克隆的对象应该是平等的equals()方法使用,但它不是强制性的。
因为每个类直接或间接的父类都是Object,因此它们都含有clone()方法,但是因为该方法是protected,所以都不能在类外进行访问。
要想对一个对象进行复制,就需要对clone方法覆盖。
为什么要克隆?
大家先思考一个问题,为什么需要克隆对象?直接new一个对象不行吗?
答案是:克隆的对象可能包含一些已经修改过的属性,而new出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠clone方法了。那么我把这个对象的临时属性一个一个的赋值给我新new的对象不也行嘛?可以是可以,但是一来麻烦不说,二来,大家通过上面的源码都发现了clone是一个native方法,就是快啊,在底层实现的。
提个醒,我们常见的Object a=new Object();Object b;b=a;这种形式的代码复制的是引用,即对象在内存中的地址,a和b对象仍然指向了同一个对象。
而通过clone方法赋值的对象跟原来的对象时同时独立存在的。
如何实现克隆
先介绍一下两种不同的克隆方法,浅克隆(ShallowClone)和深克隆(DeepClone)。
在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制,下面将对两者进行详细介绍。
一般步骤是(浅克隆):
1. 被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常), 该接口为标记接口(不含任何方法)
2. 覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象。(native为本地方法)
下面对上面那个方法进行改造:
class Student implements Cloneable{ private int number; public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } @Override public Object clone() { Student stu = null; try{ stu = (Student)super.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return stu; } } public class Test { public static void main(String args[]) { Student stu1 = new Student(); stu1.setNumber(12345); Student stu2 = (Student)stu1.clone(); System.out.println("学生1:" + stu1.getNumber()); System.out.println("学生2:" + stu2.getNumber()); stu2.setNumber(54321); System.out.println("学生1:" + stu1.getNumber()); System.out.println("学生2:" + stu2.getNumber()); } }
结果:
学生1:12345
学生2:12345
学生1:12345
学生2:54321
如果你还不相信这两个对象不是同一个对象,那么你可以看看这一句:
System.out.println(stu1 == stu2); // false
上面的复制被称为浅克隆。
还有一种稍微复杂的深度复制:
我们在学生类里再加一个Address类。
class Address { private String add; public String getAdd() { return add; } public void setAdd(String add) { this.add = add; } } class Student implements Cloneable{ private int number; private Address addr; public Address getAddr() { return addr; } public void setAddr(Address addr) { this.addr = addr; } public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } @Override public Object clone() { Student stu = null; try{ stu = (Student)super.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return stu; } } public class Test { public static void main(String args[]) { Address addr = new Address(); addr.setAdd("杭州市"); Student stu1 = new Student(); stu1.setNumber(123); stu1.setAddr(addr); Student stu2 = (Student)stu1.clone(); System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd()); System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd()); } }
结果:
学生1:123,地址:杭州市
学生2:123,地址:杭州市
乍一看没什么问题,真的是这样吗?
我们在main方法中试着改变addr实例的地址。
addr.setAdd("西湖区"); System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd()); System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
结果:
学生1:123,地址:杭州市 学生2:123,地址:杭州市 学生1:123,地址:西湖区 学生2:123,地址:西湖区
这就奇怪了,怎么两个学生的地址都改变了?
原因是浅复制只是复制了addr变量的引用,并没有真正的开辟另一块空间,将值复制后再将引用返回给新对象。
所以,为了达到真正的复制对象,而不是纯粹引用复制。我们需要将Address类可复制化,并且修改clone方法,完整代码如下:
package abc; class Address implements Cloneable { private String add; public String getAdd() { return add; } public void setAdd(String add) { this.add = add; } @Override public Object clone() { Address addr = null; try{ addr = (Address)super.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return addr; } } class Student implements Cloneable{ private int number; private Address addr; public Address getAddr() { return addr; } public void setAddr(Address addr) { this.addr = addr; } public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } @Override public Object clone() { Student stu = null; try{ stu = (Student)super.clone(); //浅复制 }catch(CloneNotSupportedException e) { e.printStackTrace(); } stu.addr = (Address)addr.clone(); //深度复制 return stu; } } public class Test { public static void main(String args[]) { Address addr = new Address(); addr.setAdd("杭州市"); Student stu1 = new Student(); stu1.setNumber(123); stu1.setAddr(addr); Student stu2 = (Student)stu1.clone(); System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd()); System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd()); addr.setAdd("西湖区"); System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd()); System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd()); } }
结果:
学生1:123,地址:杭州市 学生2:123,地址:杭州市 学生1:123,地址:西湖区 学生2:123,地址:杭州市
这样结果就符合我们的想法了。
最后我们可以看看API里其中一个实现了clone方法的类:
java.util.Date:
/** * Return a copy of this object. */ public Object clone() { Date d = null; try { d = (Date)super.clone(); if (cdate != null) { d.cdate = (BaseCalendar.Date) cdate.clone(); } } catch (CloneNotSupportedException e) {} // Won't happen return d; }
该类其实也属于深度复制。
参考文档:Java如何复制对象
浅克隆和深克隆
1、浅克隆
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。
简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。
2、深克隆
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。
简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。
在Java语言中,如果需要实现深克隆,可以通过覆盖Object类的clone()方法实现,也可以通过序列化(Serialization)等方式来实现。
(如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。)
序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。
扩展Java语言提供的Cloneable接口和Serializable接口的代码非常简单,它们都是空接口, 这种空接口也称为标识接口,标识接口中没有任何方法的定义, 其作用是告诉JRE这些接口的实现类是否具有某个功能, 如是否支持克隆、是否支持序列化等。
解决多层克隆问题
如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。
public class Outer implements Serializable{ private static final long serialVersionUID = 369285298572941L; //最好是显式声明ID public Inner inner; //Discription:[深度复制方法,需要对象及对象所有的对象属性都实现序列化] public Outer myclone() { Outer outer = null; try { // 将该对象序列化成流,因为写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。所以利用这个特性可以实现对象的深拷贝 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(this); // 将流序列化成对象 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); outer = (Outer) ois.readObject(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return outer; } }
Inner也必须实现Serializable,否则无法序列化:
public class Inner implements Serializable{ private static final long serialVersionUID = 872390113109L; //最好是显式声明ID public String name = ""; public Inner(String name) { this.name = name; } @Override public String toString() { return "Inner的name值为:" + name; } }
这样也能使两个对象在内存空间内完全独立存在,互不影响对方的值。
总结
实现对象克隆有两种方式:
1). 实现Cloneable接口并重写Object类中的clone()方法;
2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。
注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object类的clone方法克隆对象。让问题在编译的时候暴露出来总是优于把问题留到运行时。
-
java对象引用修改值的问题
2018-07-12 07:08:09改变students[i]可以改变s是不是意味着是引用的数组元素地址 2.如果1成立,那为什么students[i]=null,不能是s也未null,从而在打印引起空指针异常 ``` java package com.zhuxl.jdk.sourcecode.java.util.hashmap;... -
Java引用对象
2018-12-11 10:09:03在写了15年C/C++之后,我于1999年开始写Java。借助指针切换(pointer handoffs)等编码实践或者Purify等工具,我认为自己对C风格的内存管理已经得心应手了,甚至已经不记得上次发生内存泄露是什么时候了。所以起初我...简介
在写了15年C/C++之后,我于1999年开始写Java。借助指针切换(pointer handoffs)等编码实践或者Purify等工具,我认为自己对C风格的内存管理已经得心应手了,甚至已经不记得上次发生内存泄露是什么时候了。所以起初我接触到Java的自动内存管理时有些不屑,但很快就爱上它了。在我不需要再管理内存后我才意识到之前耗费了多少精力。
接着我就遇到了第一个
OutOfMemoryError
。当时我就坐在那面对着控制台,没有堆栈,因为堆栈也需要内存。调试这个错误很困难,因为常用的工具都不能用了,甚至malloc logger
都没有,而且1999年的时候Java的调试器还很原始。我不记得当时是什么原因导致的错误了,但我肯定当时没有用引用对象解决它。引用对象是在一年后我写服务端数据库缓存,尝试用软引用来限制缓存大小时才进入我的“工具箱”的。结果证明它们在这种场景下用处不大,我下面会解释原因。但当引用类型才进入我的“工具箱”后,我发现了很多其他用途,并且对JVM也有了更好的理解。
Java堆和对象生命周期
对于刚接触Java的C++程序员来说,栈和队之间的关系很难理解。在C++中,对象可以通过
new
操作在堆上创建,也可以通过“自动”分配在栈上创建。下面这种在C++中是合法的,会在栈上创建一个新的Integer
对象,但对于Java编译器来说这有语法错误。1
Integer foo = Integer(1);
不同于C++,Java的所有对象都在堆保存,要求用
new
操作来创建对象。局部变量仍然储存在栈中,但它们持有这个对象的指针而不是这个对象本身(更让C++程序员困惑的是这些指针被叫做“引用”)。下面这个Java方法,有一个Integer
变量引用一个从String
解析而来的值:1 2 3 4
public static void foo(String bar) { Integer baz = new Integer(bar); }
下图显示了这个方法相应的堆和栈之间的关系。栈被分割为栈帧,用于保存调用树中各个方法的参数和局部变量。这些变量指向对象–这个例子中的参数
bar
和局部变量baz
–指23向存在于堆中的变量。现在仔细看看
foo()
的第一行,创建了一个Integer
对象。这种情况下,JVM会先试图去为这个对象找足够的堆空间–在32位JVM上大约12 bytes,如果可以分配出空间,就调用Integer
的构造函数,Integer
的构造函数会解析传入的String然后初始化这个新创建的对象。最后,JVM在变量baz
中保存一个指向该对象的指针。这是理想的道路,还有一些不那么美好的道路,其中我们关心的是当
new
操作不能为这个对象找到12 bytes的情况。在这种情况下,JVM会在放弃并抛出OutOfMemoryError
之前调用垃圾回收器尝试腾出空间。垃圾回收
虽然Java给了你
new
操作来在堆上分配对象,但是没有给你对应的delete
操作来移除它们。当方法foo()
返回,变量baz
离开了作用域,但是它指向的对象依然存在于堆中。如果只是这样的话,那所有的程序都会很快耗尽内存。Java提供了垃圾回收器来清理那些不再被引用的对象。垃圾回收器会在程序尝试创建一个新对象但堆没有足够的空间时工作。回收器在堆上寻找那些不再被程序使用的对象并回收它们的空间时,请求创建对象的线程会暂停。如果回收器无法腾出足够的空间,并且JVM无法扩展堆,
new
操作就会失败并抛出OutOfMemoryError
,通常接下来你的应用会停止。标记-清除
其中一个关于垃圾回收器的误区是,很多人认为JVM为每个对象保存了一个引用计数,回收器只会回收那些引用计数为0的对象。事实上,JVM使用被称为“标记-清除”的技术。标记-清除算法的思路很简单:所有不能被程序访问到的对象都是垃圾,都可以被收集。
标记-清除算法有以下阶段:
阶段一:标记
垃圾回收器从“root”引用开始,标记所有可以到达的对象。
阶段二:清除
在第一阶段没有被标记的都是不可到达的,也就是垃圾。如果垃圾对象定义了
finalizer
,它会被加到finalization
队列(后文详细讨论)。否则,它占用的空间就可以被重新分配使用(具体的情况视GC的实现而定,有很多种实现)。阶段三:压缩(可选)
一些回收器有第三步——压缩。在这一步,GC会移动对象使回收的对象留下的空闲空间合并,这可以防止堆变得碎片化,避免大块相邻内存分配的失败。
例如,Hotspot JVM,在新生代使用会压缩的回收器,而在老年代使用非压缩的回收器(至少在1.6和1.7的“server” JVM是这样)。想了解更多信息,可以看本文后面的参考文献。
那么什么是“roots”呢?在一个简单的Java应用中,它们是方法参数和局部变量(保存在栈中)、当前执行的表达式操作的对象(也保存在栈中)、静态类成员变量。
对于使用自己classloader的程序,例如应用服务器,情况复杂一些:只有被
system classloader
(JVM启动时使用这个loader)加载的类包含root引用。那些被应用创建的classloader一旦没有其他引用也会被回收。这是应用服务器可以热部署的原因:它们为每个部署的应用创建独立的classloader,当应用下线或重新部署时释放classloader引用。理解root引用很重要,因为这定义了“强引用”,即如果可以从root沿着引用链到达某个对象,那么这个对象就被“强引用”了,则不会被回收。
回到
foo()
方法,参数bar
和局部变量baz
使用当方法执行时才是强引用,一旦方法结束,它们都超出了作用域,被他们引用的对象就可以回收。另一种可能是,foo()
返回一个它创建的Integer引用,这意味着这个对象会被调用foo()
的那个方法保持强引用。看下面这个例子:
1 2
LinkedList foo = new LinkedList(); foo.add(new Integer(123));
变量
foo
是一个指向LinkedList对象的root引用,列表中有0个或多个元素,都指向其对象。当我们调用add()
时,向列表中添加了一个指向值为123的Integer实例的元素,这是一个强引用,意味着这个Integer实例不会被回收。一旦foo
超出了作用域,这个LinkedList和它里面的一切都可以被回收,当前前提是没有其他强引用指向它了。你也许想知道循环引用会发生什么,即对象A包含一个对象B的引用,同时对象B也包含对象A的引用。答案是标记-清除回收器并不傻,如果A和B都无法由强引用链到达,那么它们都可以被回收。
Finalizers
C++允许对象定义析构方法,当对象离开作用域或者被明确删除时,它的析构函数会被调用来清理它使用的资源,对大多数对象来说即释放通过
new
或malloc
分配的内存。在Java中,垃圾回收器会为你处理内存清理,所以不需要明确的析构函数来做这些。然而,内存并不是唯一可能需要被清理的资源。例如
FileOutputStream
,当创建这个对象的实例时,会从操作系统分配一个文件操作符(文件句柄),如果你在关闭流之前让它的所有引用都离开作用域了,这个文件操作符会发生什么呢?答案是流有finalizer
,这个方法会在垃圾回收器回收对象前被JVM调用。这个例子中的FileOutputStream
在finalizer
方法中会关闭流,这样就会将文件操作符返回给操作系统,同时也会刷新缓存,确保所有数据被正确地写到磁盘。任何对象都可以有
finalizer
,你只需要定义finalize()
方法即可:1 2 3 4
protected void finalize() throws Throwable { // 在这里释放你的对象 }
finalizers
看上去是一个由你自己清理的简单方式,但实际上有严重的限制。首先,你永远也不要依赖它做重要的事,因为对象的finalizers
可能不会被调用,应用可能在对象被回收之前就结束了。finalizers
还有一些更微妙的问题,我会在虚引用时讨论。对象的生命周期(无引用对象)
总结起来,对象的一生可以用下面的图总结:被创建、被使用、可回收、最终被回收。阴影部分表示对象是“强可达”的时期,这是与引用对象规定的可达性比较而言很重要的时期。
进入引用对象的世界
JDK 1.2引入了
java.lang.ref
包,对象的生命周期增加了3种阶段:软可达、弱可达、虚可达。这些阶段只用来可否被回收,换言之,那些不是强引用的对象,必须是其中一种引用对象的被引用者:-
软可达
对象是SoftReference
的被引用者,并且没有强引用指向它。垃圾回收器会尽可能地保留它,但会在抛出OutOfMemoryError
之前回收它。 -
弱可达
对象是WeakReference
的被引用者,并且没有强引用指向它。垃圾回收器可以在任何时间回收它,不会试图去保留它。通常这个对象会在Major GC
被回收,可能在Minor GC
中存活。 -
虚可达
对象是PhantomReference
的被引用者,它已经被选择要回收并且finalizer
(如果有)已经运行了。这里的“可达”有点用词不当,在这个时候你已经没有办法访问到原始的对象了。
如你所想,把这三种新的可选状态加到对象生命周期图中会变得很复杂。尽管文档指出了一个逻辑上从强可达到软可达、弱可达、虚可达的回收过程,但实际过程取决于你的程序创建了哪种引用对象。如果你创建了一个
WeakReference
而不是一个SoftReference
,那么对象回收的过程是直接从强可达到弱可达最后被回收的。还有一点需要清楚的是,不是所有的对象都需要与引用对象关联,事实上,只有极少部分对象需要。引用对象是一个间接层:你通过引用对象去访问它的被引用者,你肯定不希望你的代码中充斥着这些间接层。事实上大部分程序只会使用引用对象去访问很少一部分它创建的对象。
引用和被引用者
引用对象是在你程序代码和一些称为被引用者的对象之间的中间层。每个引用对象都是围绕它的被引用者创建,并且被引用者是不能修改的。
引用对象提供了
get()
方法来获取被引用者的强引用。垃圾回收器可能在某些情况下回收被引用者,一旦回收了,get()
会返回null
。正确使用引用,你需要类似下面这样的代码:1 2 3 4 5 6 7 8 9 10 11 12
SoftReference<List<Foo>> ref = new SoftReference<List<Foo>>(new LinkedList<Foo>()); // 代码其他地方创建了`Foo`,你想把它添加到列表中 List<Foo> list = ref.get(); if (list != null) { list.add(foo); } else { // 列表已经被回收了,做一些恰当的事 }
换言之:
- 你必须总是检查看被引用者是否是null。
垃圾回收器可能在任何时间回收被引用者,如果你无所顾忌地使用,很快就会收获NullPointerException
。 - 当你想使用被引用者时,你必须持有一个它的强引用。
再次强调, 垃圾回收器可能在任何时间回收被引用者,甚至是在单个表达式之间。上面的例子如果我不定义list
变量,我而是简单地调用ref.get().add(foo)
,被引用者可能在检查是否为null和实际使用之间被回收。牢记垃圾回收器是在它自己的线程运行的,它不关心你的代码在干什么。 - 你必须持有引用类型的强引用。
如果你创建了一个引用对象,但超出了它的作用域,那么这个引用对象自己也会被回收。这是显然的,但很容易被忘记,特别是在用引用队列(qv)追踪引用对象的时候。
同样要记住的是软引用、弱引用、虚引用只有在没有其他强引用指向被引用者时才有意义,它们让你可以在对象通常会成为垃圾回收器的食物时候获得该对象。这可能看起来很奇怪,如果不再持有强引用了,为什么我还关心这个对象呢?原因视特殊的引用类型而定。
软引用
我们先从软引用开始来回答这个问题。如果一个对象是
SoftReference
的被引用者,并且它没有强引用,那么垃圾回收器可以回收但尽量不去回收它。因此,只要JVM有足够的内存,软引用对象就会在垃圾回收中存活,甚至经历好几轮垃圾回收依然存活。JDK文档说软引用适用于内存敏感的缓存:每个缓存对象都通过
SoftReference
访问,如果JVM觉得需要内存,它就会清除一些或者所有引用并回收对应的被引用者。如果JVM不需要内存,被引用者就会留在堆中,并且可以被程序代码访问到。在这种方案下,被引用者在使用时是强引用的,其他情况是软引用的,如果软引用被清除了,你需要刷新缓存。想作为这种角色使用,被缓存的对象需要比较大,如每个几kB。比如说,你想实现一个文件服务相同的文件会被定期检索,或者有一些大的图片对象需要缓存时会有用。但如果你的对象很小,你只有在需要定义大量对象时情况才会不同,引用对象还会增加整个程序的负担。
内存限制型缓存被认为是有害的
我的观点是,可用内存绝对是最差的管理缓存的方式。如果你的堆很小,你不时需要重新加载对象,无论它们是不是被活跃地使用,你也无法知道这个,因为缓存会静默地处理它们。大的堆更糟:你会持有对象远大于它的正常寿命,当每次垃圾回收时会使你的应用变慢,因为需要检查这些对象。如果这些对象没有被访问,这一部分堆有可能被交换出去,回收过程中可能有大量页错误。
底线:如果你要用缓存,详细它会如何被使用,选一个适合的缓存策略(LRU、timed LRU),在选择基于内存的策略前仔细考虑。软引用用于断路器
用软引用为内存分配提供断路器是更好的选择:在你的代码和它分配的内存之间使用软引用,你就可以避免可怕的
OutOfMemoryError
。这个技巧可以正常运作是因为在应用里内存的分配是趋于局部的:从数据库中读取行、从一个文件中处理数据。例如,如果你写过很多JDBC的代码,你可能会有类似下面这样的方法以某种方式处理查询的结果并且确保
ResultSet
被正确地关闭。这只有一个小缺陷:如果查询返回了一百万行,你没有可用的内存去保存它们时会发生什么?1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
public static List<List<Object>> processResults(ResultSet rslt) throws SQLException { try { List<List<Object>> results = new LinkedList<List<Object>>(); ResultSetMetaData meta = rslt.getMetaData(); int colCount = meta.getColumnCount(); while (rslt.next()) { List<Object> row = new ArrayList<Object>(colCount); for (int ii = 1 ; ii <= colCount ; ii++) row.add(rslt.getObject(ii)); results.add(row); } return results; } finally { closeQuietly(rslt); } }
答案当然是会得到
OutOfMemoryError
。这是使用断路器的绝佳地方:如果在处理查询时JVM要耗尽内存了,那就释放所有已经使用的那些内存,抛出一个应用特殊的异常。你可能很奇怪,这种情况下这次查询将被忽略,为什么不直接让内存耗尽的错误来做这件事呢?原因是并不仅仅只有你的应用被内存耗尽影响。如果你在一个应用服务器上运行,你的内存使用可能干掉其他应用。即使是在一个独有的环境,断路器也能提升你的应用的健壮性,因为它能限制问题,让你有机会恢复并继续运行。
要创建一个断路器,首先你需要做的是把结果的列表包装在
SoftReference
中(你在前面已经见过这个代码了):1 2
SoftReference<List<List<Object>>> ref = new SoftReference<List<List<Object>>>(new LinkedList<List<Object>>());
然后,你遍历结果,在你需要更新这个列表时为它创建强引用:
1 2 3 4 5 6 7 8 9 10 11 12 13
while (rslt.next()) { rowCount++; // store the row data List<List<Object>> results = ref.get(); if (results == null) throw new TooManyResultsException(rowCount); else results.add(row); results = null; }
这可以满足要求是因为这个方法几乎所有的内存分配都发生在2个地方:调用
next()
时和代码把行里的数据存放到它自己的列表中时。第一种情况当你调用next()
时会发生很多事情:ResultSet
一般会在包含多行的一大块二进制数据中检索,然后当你调用getObject()
,它会取出一部分数据把它转成Java对象。当这些昂贵的操作发生时,这个list只有来自
SoftReference
的引用,如果内存耗尽,引用会被清除,list会变成垃圾。这意味着这个方法可能抛出异常,但抛出异常的影响是有限的,也许调用方能以一点数量限制重新进行查询。一旦昂贵的操作完成,你可以没有影响地拿到list的强引用。注意到我用
LinkedList
保存结果而不是ArrayList
,LinkedList
增长时只会增加少量字节,不太可能引起OutOfMemoryError
,而如果ArrayList
需要增加容量,它需要创建一个新数组,对于大列表来说,这可能意味着数MB的内存分配。还注意到我在添加新元素后把
results
变量设置为null
,这是少数几种这样做是合理的情形之一。尽管在循环的最后变量超出了作用域,但垃圾回收器可能并不知道(因为JVM没有理由去清除变量在调用栈中的位置)。因此如果我不清除这个变量的话,它会在随后的循环中成为隐藏的强引用。软引用不是万能的
软引用可以预防很多内存耗尽的情况,但不能预防所有。问题在于:为了真正地使用软引用,你需要创建一个被引用者的强引用,即为了向
results
中添加一行,我们需要持有实际列表的引用。我们持有强引用的时候就会面临发生内存耗尽错误的风险。使用断路器的目标是把一些无用的东西的时间窗口减到最小:你持有对象强引用的时间,更重要的是在这段时间中分配内存的总量。在我们的例子中,我们限制强引用去添加一行到
results
中,我们使用LinkedList
而不是ArrayList
因为前者扩容时增长更小。我想重申的是,如果我一个变量持有强引用,但这个变量很快超出了作用域,语言细则没有说JVM需要清除超出作用域的变量,如果是像写的这样,Oracle/OpenJDK JVM都没有这样做,如果我不明确地清除
results
变量,在遍历期间会保持强引用,阻止软引用做它的工作。最后,仔细考虑那些隐藏的强引用。例如,你可能会想在使用DOM构造XML文档时加入断路器。在DOM中,每个节点都持有它父节点的引用,从而导致持有了树中每个其他节点的引用。如果你用递归去创建文档,你的栈中可能塞满了个别节点的引用。
弱引用
弱引用,正如它名字显示,是一个当垃圾回收器来敲门时不会反抗的引用对象。如果被引用者没有强引用或软引用而只有弱引用,那它就可以被回收。所以弱引用有什么用呢?有2个主要用途:关联没有内在联系的对象,或者通过
canonicalizing map
减少重复。ObjectOutputStream
的问题第一个例子,我准备聚焦不使用弱引用的对象序列化。
ObjectOutputStream
以及它的伙伴ObjectInputStream
提供了任意Java对象与字节流之间相互转换的方式。根据对象模型的观点,流和用这些流写的对象之间是没有联系的。流不是由这些被写的对象组成的,也不是它们的聚集。但是当你看这些流的说明时,你会看到事实上是有联系的:为了维持对象的唯一性,输出流会和每个被写的对象关联一个唯一的标识符,随后的写对象的请求被替换为写这个标识符。这个特征对于流序列号对象的能力来说绝对是很重要的,如果没有这个特征,自我引用的对象会变成一个无限的字节流。
要实现这个特征,流需要持有每个写到流中的对象的强引用。对于决定在socket通信时用对象流作为消息协议的程序员来说,有这么一个问题:消息被设计为短暂的,但流会在内存中持有它们,不久之后,程序会耗尽内存(除非程序员知道在每次通信后调用
reset()
)。这种非与生俱来的联系惊人的普遍。它们会在程序员为了使用对象而需要去维持必不可少的上下文时出现。有时这些联系被运行环境默默管理,例如servlet
Session
对象;有时这些联系需要被程序员明确地管理,例如对象流;还有些时候,这种联系只有当生产环境的服务抛出内存耗尽的错误时才会被发现,比如埋藏在程序代码深处的静态Map
。弱引用提供了一种维持这种联系的同时还能让垃圾回收器做它的工作的方式,弱引用只有在同时还有强引用时才保持有效。回到对象流的例子,如果你用流来通信,一旦消息被写完就可以被回收了。另一方面,当流用来RMI访问一个生命周期很长的数据结构时,它能保持它一致。
不幸的是,尽管对象流通信协议在JDK 1.2时被更新了。虚引用也是这样被加入的,但JDK的开发者并没有选择把二者结合到一起,所以记得调用
reset()
。用
Canonicalizing Maps
消除重复数据尽管存在对象流这种情况,但我不认为有很多你应该关联两个没有内在关系的对象的情行。我所看到的一些例子,例如Swing监听器,它们会自我清理,看起来更像是黑客,而不是有效的设计选择。
当我最初写这篇文章的时候,大约是在2007年,我提出了
canonicalizing map
作为String.intern()
的替代物,是在假设被存入常量池的字符串永远不会被清理的前提下。后来我得知这种担心是毫无根据的。更重要的是,从JDK 8开始,OpenJDK已经完全去掉了永久代。因此,没有必要害怕intern()
,但是canonicalizing map
对于字符串以外的对象仍然有用。在我看来,弱引用的最佳用途是实现
canonicalizing map
,这是一种确保同时只存在一个值对象实例的办法。String.intern()
是这种map的典型例子:当你把一个字符串存入常量池时,JVM会将它添加到一个特殊的map中,这个map也用于保存字符串文本。这样做的原因不是像一些人认为的那样为了更快地进行比较。这是为了最大限度地减少重复的非文字字符串(如从文件或消息队列中读取的字符串)占用的内存量。简单的
canonicalizing map
通过使用相同的对象作为key和value来工作:你用任意实例传给map,如果map中已经有一个值,你就返回它。如果map中没有值,则存储传入的实例(并返回它)。当然,这仅适用于可用作map的key的对象。如果我们不担心内存泄漏,下面可能是我们实现String.intern()
的方式:1 2 3 4 5 6 7 8 9
private Map<String,String> _map = new HashMap<String,String>(); public synchronized String intern(String str) { if (_map.containsKey(str)) return _map.get(str); _map.put(str, str); return str; }
如果你只有少量字符串要放入常量池,例如也许在处理一个文件的简单方法中,这个实现没什么问题。然而,假设你正在编写一个长期运行的应用程序,该应用程序必须处理来自多个来源的输入,其中包含范围广泛的字符串,但仍有高度的重复。例如,一台处理上传的邮政地址数据文件的服务:
New York
将会有很多条目,Temperanceville VA
的条目就不多了。你会想要消除前者的重复,但是不想保留后者超过必要的时间。这就是弱引用的
canonicalizing map
有所帮助的地方:只有程序中的一些代码正在使用它,它才允许你创建一个规范的实例。最后一个强引用消失后,这个规范的字符串将被回收。如果稍后再次出现该字符串,它将成为新的规范的实例。为了改进我们的“规范化工具”,我们可以用
WeakHashMap
替换HashMap
:1 2 3 4 5 6 7 8 9 10 11 12 13
private Map<String,WeakReference<String>> _map = new WeakHashMap<String,WeakReference<String>>(); public synchronized String intern(String str) { WeakReference<String> ref = _map.get(str); String s2 = (ref != null) ? ref.get() : null; if (s2 != null) return s2; _map.put(str, new WeakReference(str)); return str; }
首先要注意的是,虽然map的key是字符串,但它的值是
WeakReference<String>
。这是因为WeakHashMap
对其key使用弱引用,但对其value持有强引用。因为我们的key和value是相同的,所以entry永远不会被回收。通过包装条目,我们让GC回收它。其次,注意返回字符串的过程:首先我们检索弱引用,如果它存在,那么我们检索引用对象。但是我们也必须检查那个对象。存在引用仍在map中但已经被清除了的可能。只有当引用对象不为空时,我们才返回它;否则,我们认为传入的字符串是新的规范的版本。
第三,请注意我对
intern()
方法用了synchronized
。canonicalizing map
最有可能的用途是在多线程环境中,例如应用服务,WeakHashMap
没有内部同步。这个例子中的同步实际上相当幼稚,intern()
方法可能成为争论的焦点。在现实世界的实现中,我可能会使用ConcurrentHashMap
,但是对于教程来说,这种幼稚的方法更有效。最后,
WeakHashMap
的文档关于条目何时从map中移除有些模糊。它指出,“WeakHashMap
的行为可能就像一个未知线程正在无声地删除条目。”实际上没有其他线程。相反,每当map被访问时,它就会被清理。为了跟踪哪些条目不再有效,它使用了引用队列。引用队列
虽然判断一个引用是不是null可以让你知道它的引用对象是不是已经被回收,但是这样做并不是很高效;如果你有很多引用,你的程序会花大部分时间寻找那些已经被清除的引用。
更好的解决方案是引用队列:你在构建时将引用与队列相关联,并且该引用将在被清除后放入队列中。要发现哪些引用已被清除,你需要从队列拉取。这可以通过后台线程来完成,但是在创建新引用时从队列拉取通常更简单(
WeakHashMap
就是这样做的)。引用队列最常与虚引用一起使用,在后面会描述,但是可以与任何引用类型一起使用。下面的代码是一个弱引用的例子:它创建了一组缓冲区,通过
WeakReference
访问,并且在每次创建后查看哪些引用已经被清除。如果运行此代码,你会看到create
消息的长时间出现,当垃圾回收器运行时偶尔会出现一些clear
消息。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
public static void main(String[] argv) throws Exception { Set<WeakReference<byte[]>> refs = new HashSet<WeakReference<byte[]>>(); ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>(); for (int ii = 0 ; ii < 1000 ; ii++) { WeakReference<byte[]> ref = new WeakReference<byte[]>(new byte[1000000], queue); System.err.println(ii + ": created " + ref); refs.add(ref); Reference<? extends byte[]> r2; while ((r2 = queue.poll()) != null) { System.err.println("cleared " + r2); refs.remove(r2); } } }
一如既往,关于这个代码有一些值得注意的事情。首先,虽然我们创建的是
WeakReference
实例,但是队列会给我们返回Reference
。这提醒你,一旦它们入队,你使用的是什么类型的引用就不再重要了,被引用者已经被清除。第二,我们必须对引用对象本身进行强引用。引用对象知道队列,但是队列在引用进入队列前不知道引用。如果我们没有维护对引用对象的强引用,它本身就会被回收,并且永远不会被添加到队列中。在这个例子中,我使用了一个
Set
,一旦引用被清除,就删除它们(将它们留在Set
中是内存泄漏)。虚引用
虚引用不同于软引用和弱引用,它们不用于访问它们的被引用者。相反,他们的唯一目的是当它们的被引用者已经被回收时通知你。虽然这看起来毫无意义,但它实际上允许你比
finalizers
更灵活地执行资源清理。Finalizers
的问题这篇文章中我更详细地讨论了
finalizers
。简而言之,你应该依靠try/catch/finally
清理资源,而不是finalizers
或虚引用。在对象生命周期的描述中,我提到
finalizers
有一些微妙的问题,使得它们不适合清理非内存资源。还有一些非微妙的问题,为了完整起见,我将在这里讨论。-
finalizer
可能永远不会被调用如果你的程序从未用完可用内存,那么垃圾回收器不会运行,你的
finalizer
也不会运行。对于长时间运行的应用程序(例如服务)来说,通常不会出现这个问题,但是短时间运行的程序可能会在没有运行垃圾收集的情况下完成。虽然有一种方法可以告诉JVM在程序退出之前运行finalizers
,但这是不可靠的,可能会与其他shutdown hooks
冲突。 -
Finalizers
可能创建一个对象的其他强引用例如,通过将对象添加到集合中。这基本上复活了这个对象,但是,就像
Stephen King's Pet Sematary
一样,返回的对象“不太正确”。尤其是,当对象再次符合回收条件时,它的finalizer
不会运行。也许你会使用这种复活技巧是有原因的,但是我无法想象,而且在代码上看起来会非常模糊。
现在这些都已经过时了,我相信
finalizers
的真正问题是它们在垃圾回收器首次识别要回收的对象的时间和实际回收其内存的时间之间引入了间隙,因为finalization
发生在它自己的线程上,独立于垃圾回收器的线程。JVM保证在返回OutMemoryError
之前执行一次full collection
,但是如果所有符合回收条件的对象都有finalizers
,则回收将不起作用:这些对象保留在内存中等待finalization
。假设一个标准JVM只有一个线程来处理所有对象的finalization
,一些长时间运行的finalization
,你就可以看到问题可能会出现。以下程序演示了这种行为:每个对象都有一个
finalizer
休眠半秒钟。不会有很长时间,除非你有成千上万的对象要清理。每个对象在创建后都会立即超出作用域,但是在某个时候你会耗尽内存(如果你想运行这个例子,我建议使用-Xmx64m
来使错误快速发生;在我的开发机器上,有3Gb堆,实际上需要几分钟才能失败)。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
public class SlowFinalizer { public static void main(String[] argv) throws Exception { while (true) { Object foo = new SlowFinalizer(); } } // some member variables to take up space -- approx 200 bytes double a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z; // and the finalizer, which does nothing by take time protected void finalize() throws Throwable { try { Thread.sleep(500L); } catch (InterruptedException ignored) {} super.finalize(); } }
虚引用知晓之事
当对象不再被使用时虚引用允许应用程序知晓,这样应用程序就可以清理对象的非内存资源。然而,与
finalizers
不同的是,当应用程序知道到这一点时,对象本身已经被收集了。此外,与
finalizers
不同,清理由应用程序而不是垃圾回收器来调度。您可以将一个或多个线程专用于清理,如果对象数量需要,可以增加线程数量。另一种方法——通常更简单——是使用对象工厂,并在创建新实例之前清理所有回收的实例。理解虚引用的关键点是,你不能使用引用来访问对象:
get()
总是返回null,即使对象仍然是强可达的。这意味着引用对象持有的不能是要清理的资源的唯一引用。相反,你必须维持对这些资源的至少一个其他强引用,并使用引用队列来通知被引用者已被回收。与其他引用类型一样,您的程序也必须拥有对引用对象本身的强引用,否则它将被回收,资源将内存泄露。用虚引用实现连接池
数据库连接是任何应用程序中最宝贵的资源之一:它们需要时间来建立,并且数据库服务器严格限制它们将接受的同时打开的连接的数量。尽管如此,程序员对它们非常粗心,有时会为每个查询打开一个新的连接,或者忘记关闭它,或者不在
finally
块中关闭它。大多数应用服务部署使用连接池,而不是允许应用直接连接数据库:连接池维护一组打开的连接(通常是固定的),并根据需要将它们交给程序。用于生产环境的连接池提供了几种防止连接泄漏的方法,包括超时(识别运行时间过长的查询)和恢复被垃圾回收器回收的连接。
下面这个连接池旨在演示虚引用,不能用于生产环境。Java有几个可用于生产环境的连接池,如
Apache Commons DBCP
和C3P0
。后一个特性是虚引用的一个很好的例子。为了使它工作,连接池提供的
Connection
对象只是实际数据库连接的包装,可以在不丢失数据库连接的情况下回收它们,因为连接池保持对实际连接的强引用。连接池将虚引用与“包装成的”连接相关联,如果引用最终出现在引用队列中,则会将实际连接返回给连接池。连接池中最不有趣的部分是
PooledConnection
,如下所示。正如我说过的,它是一个包装,委派对实际连接的调用。不同的是我使用了反射代理来实现。JDBC接口随着Java的每一个版本而发展,其方式既不向前也不向后兼容;如果我使用了具体的实现,除非你使用了与我相同的JDK版本,否则你将无法编译。反射代理解决了这个问题,也使代码变得更短。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
public class PooledConnection implements InvocationHandler { private ConnectionPool _pool; private Connection _cxt; public PooledConnection(ConnectionPool pool, Connection cxt) { _pool = pool; _cxt = cxt; } private Connection getConnection() { try { if ((_cxt == null) || _cxt.isClosed()) throw new RuntimeException("Connection is closed"); } catch (SQLException ex) { throw new RuntimeException("unable to determine if underlying connection is open", ex); } return _cxt; } public static Connection newInstance(ConnectionPool pool, Connection cxt) { return (Connection)Proxy.newProxyInstance( PooledConnection.class.getClassLoader(), new Class[] { Connection.class }, new PooledConnection(pool, cxt)); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // if calling close() or isClosed(), invoke our implementation // otherwise, invoke the passed method on the delegate } private void close() throws SQLException { if (_cxt != null) { _pool.releaseConnection(_cxt); _cxt = null; } } private boolean isClosed() throws SQLException { return (_cxt == null) || (_cxt.isClosed()); } }
需要注意的最重要的一点是,
PooledConnection
同时引用了底层数据库连接和连接池。后者用于那些确实记得关闭连接的应用程序:我们希望立即告知连接池,以便底层连接可以立即被重用。getConnection()
方法也值得一提:它的存在是为了捕捉那些在显式关闭连接后试图使用该连接的应用程序。如果连接已经交给另一个消费者,这可能是一件非常糟糕的事情。因此close()
显式清除引用,getConnection()
会检查该引用,并在连接不再有效时抛出异常。invocation handler
用于所有委托调用。现在让我们将注意力转向连接池本身,从它用来管理连接的对象开始。
1 2 3 4 5 6
private Queue<Connection> _pool = new LinkedList<Connection>(); private ReferenceQueue<Object> _refQueue = new ReferenceQueue<Object>(); private IdentityHashMap<Object,Connection> _ref2Cxt = new IdentityHashMap<Object,Connection>(); private IdentityHashMap<Connection,Object> _cxt2Ref = new IdentityHashMap<Connection,Object>();
当连接池被构造并存储在
_pool
中时,可用连接被初始化。我们使用引用队列_refQueue
来标识已回收的连接。最后,我们有连接和引用之间的双向映射,在将连接返回到连接池时使用。正如我之前说过的,实际的数据库连接将在提交给应用程序代码之前被包装在
PooledConnection
中。这发生在wrapConnection()
函数中,也是我们创建虚引用和连接-引用映射的地方。1 2 3 4 5 6 7 8 9
private synchronized Connection wrapConnection(Connection cxt) { Connection wrapped = PooledConnection.newInstance(this, cxt); PhantomReference<Connection> ref = new PhantomReference<Connection>(wrapped, _refQueue); _cxt2Ref.put(cxt, ref); _ref2Cxt.put(ref, cxt); System.err.println("Acquired connection " + cxt ); return wrapped; }
与
wrapConnection
对应的是releaseConnection()
,该函数有两种变体。当应用程序代码显式关闭连接时,PooledConnection
调用第一个,这是“快乐的道路”,它将连接放回连接池中供以后使用。它还会清除连接和引用之间的映射,因为它们不再需要。请注意,此方法具有默认(包)同步:它由PooledConnection
调用,因此不能是私有的,但通常不可访问。1 2 3 4 5 6 7
synchronized void releaseConnection(Connection cxt) { Object ref = _cxt2Ref.remove(cxt); _ref2Cxt.remove(ref); _pool.offer(cxt); System.err.println("Released connection " + cxt); }
另一个变体使用虚引用来调用,这是“可悲的道路”,当应用程序不记得关闭连接时才会调用。在这种情况下,我们得到的只是虚引用,我们需要使用映射来检索实际连接(然后使用第一个变体将其返回到连接池中)。
1 2 3 4 5 6
private synchronized void releaseConnection(Reference<?> ref) { Connection cxt = _ref2Cxt.remove(ref); if (cxt != null) releaseConnection(cxt); }
有一种边缘情况:如果引用在应用程序调用
close()
之后进入队列,会发生什么?这种情况不太可能发生:当我们清除映射时,虚引用应该已经有资格被回收,这样它就不会进入队列。然而,我们必须考虑这种情况,这导致上面的空检查:如果映射已经被移除,那么连接已经被显式返回,我们不需要做任何事情。好了,您已经看到了底层代码,现在是时候让应用程序调用唯一的方法了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
public Connection getConnection() throws SQLException { while (true) { synchronized (this) { if (_pool.size() > 0) return wrapConnection(_pool.remove()); } tryWaitingForGarbageCollector(); } }
getConnection()
的最佳路径是在_pool
中有可用的连接。在这种情况下,一个连接被移除、包装并返回给调用者。不信的情况是没有任何连接,在这种情况下,调用者希望我们阻塞直到有一个连接可用。这可以通过两种方式发生:要么应用程序关闭连接并返回到_pool
中,要么垃圾回收器找到一个已被放弃的连接,并将其关联的虚引用加入队列。为什么我使用
synchronized(this)
而不是显式锁?简而言之,这个实现是作为教学辅助工具,我想用最少的样板来强调同步点。在生产环境使用的连接池中,我实际上会避免显式同步,而是依赖并行数据结构,如ArrayBlockingQueue
和ConcurrentHashMap
。在走这条路之前,我想谈谈同步。显然,对内部数据结构的所有访问都必须同步,因为多个线程可能会尝试同时获取或返回连接。只要
_pool
中有连接,同步代码就能快速执行,竞争的可能性就很低。然而,如果我们必须循环直到连接变得可用,我们希望最大限度地减少同步的时间:我们不希望在请求连接的调用者和返回连接的另一个调用者之间造成死锁。因此,在检查连接时,使用显式同步块。那么,如果我们调用
getConnection()
,并且池是空的,会发生什么呢?这是我们检查引用队列以找到被废弃的连接的时机。1 2 3 4 5 6 7 8 9 10 11 12 13 14
private void tryWaitingForGarbageCollector() { try { Reference<?> ref = _refQueue.remove(100); if (ref != null) releaseConnection(ref); } catch (InterruptedException ignored) { // we have to catch this exception, but it provides no information here // a production-quality pool might use it as part of an orderly shutdown } }
这个函数强调了另一组相互冲突的目标:如果引用队列中没有任何引用,我们不想浪费时间,但是我们也不想在一个循环中重复检查
_pool
和_refQueue
。所以我在轮询队列时使用了一个短暂的超时时间:如果没有准备好,它会给另一个线程返回连接的机会。当然,这也带来了一个公平性问题:当一个线程正在等待引用队列时,另一个线程可能会返回一个被第三个线程立即占用的连接。理论上,等待线程可能会永远等待。在现实世界中,由于不太需要数据库连接,这种情况不太可能发生。虚引用带来的问题
前面我提到到
finalizers
不能保证被调用。虚引用也是这样,原因相同:如果回收器不运行,不可达的对象不会被回收,对这些对象的引用也不会进入队列。考虑一个程序只在循环中调用getConnection()
,让返回的连接超出作用域,如果它没有做任何其他事情来让垃圾回收器运行,那么它会很快耗尽连接池然后阻塞,等待永远无法恢复的连接。当然,有办法解决这个问题。最简单的方法之一是在
tryWaitingForGarbageCollector()
中调用System.gc()
。尽管围绕这种方法有一些争议,但这是促使JVM回到理想状态的有效方式。这是一种既适用于finalizers
也适用于虚引用的技术。这并不意味着你应该忽略虚引用,只使用
finalizer
。例如,在连接池的情况下,你可能希望显式关闭该连接池并关闭所有底层连接。你可以用finalizer
来完成,但是需要和虚引用一样多的工作。在这种情况下,通过引用获得的可控因素(相对于任意终结线程)使它们成为更好的选择。最后一些思考:有时候你只需要更多内存
虽然引用对象是管理内存消耗的非常有用的工具,但有时它们是不够的,有时又是过度的。例如,假设你正在构建一些大型对象,其中包含从数据库中读取的数据。虽然你可以使用软引用作为读取的断路器,并使用弱引用将数据规范化,但最终您的程序需要一定量的内存来运行。如果你不能给它足够的内存来实际完成任何工作,那么不管你的错误恢复能力有多强都无济于事。
应对
OutOfMemoryError
时你首先应该搞清楚它为什么会发生。可能你有内存泄露,可能仅仅是你内存的设置太低了。开发过程中,你应该指定大的堆内存大小——1G或更多——关注程序到底用了多少内存(这种情况jconsole是一个有用的工具)。大多数应用会在模拟的负载下达到一个稳定的状态,这将指引你的生产环境堆配置。如果你的内存使用随时间增长,那很可能你在对象不再使用后仍持有强引用,引用类型可能会有用,但更可能的是有bug需要修复。
底线是你需要理解你的应用。如果没有重复,
canonicalizing map
对你没有帮助。如果你希望定期执行数百万行查询,软引用是没有用的。但是在可以使用引用对象的情况下,它们会是你的救命恩人。其他信息
你可以下载这篇文章中的示例代码:
CircuitBreakerDemo通过模拟数据库的结果集引出内存驱动的断路器。
WeakCanonicalizingMap 用WeakHashMap
创建了典范字符串。这个demo 可能更有趣: 它用极端的长度来触发垃圾回收(注意:在大的堆内存下运行可能不凑效,试试-Xmx100m
).
SlowFinalizer展示了如何在垃圾回收器运行的情况下耗尽内存。
ConnectionPool 和 PooledConnection 实现了一个简单的连接池。ConnectionPoolDemo 通过内存型的HSQLDB数据库来运用这个连接池(这里 是构建这个和其他例子的Maven POM)。
“string canonicalizer” 类可以在这下到SourceForge, licensed为Apache 2.0.
Sun有许多关于调整他们JVM的内存管理的文章。这篇 文章是一篇精彩的介绍,并提供了其他文档的链接。
Brian Goetz在IBM developerWorks网站上有一个极好的专栏,叫做”Java Theory and Practice”。几年前他写了关于软引用 和 弱引用 的专栏. 这些文章对一些我看过的议题影响很深,例如使用WeakHashMap
来用不同生命时期关联对象。我有一个微信公众号,经常会分享一些Java技术相关的干货;如果你喜欢我的分享,可以用微信搜索“Java团长”或者“javatuanzhang”关注。
-
-
浅谈java对象引用及对象赋值
2017-01-05 15:11:46一、Java对象及其引用 初学Java,总是会自觉或不自觉地把Java和C++相比较。在学习Java类与对象章节的时候,发现教科书和许多参考书把对象和对象的引用混为一谈。可是,如果分不清对象与对象引用, 那实在没法很...一、Java对象及其引用
初学Java,总是会自觉或不自觉地把Java和C++相比较。在学习Java类与对象章节的时候,发现教科书和许多参考书把对象和对象的引用混为一谈。可是,如果分不清对象与对象引用, 那实在没法很好地理解下面的面向对象技术。把自己的一点认识写下来,或许能让初学Java的朋友们少走一点弯路。
为便于说明,我们先定义一个简单的类:
class Vehicle {
int passengers;
int fuelcap;
int mpg;
}
有了这个模板,就可以用它来创建对象:
Vehicle veh1 = new Vehicle();
通常把这条语句的动作称之为创建一个对象,其实,它包含了四个动作。
1)右边的“new Vehicle”,是以Vehicle类为模板,在堆空间里创建一个Vehicle类对象(也简称为Vehicle对象)。
2)末尾的()意味着,在对象创建后,立即调用Vehicle类的构造函数,对刚生成的对象进行初始化。构造函数是肯定有的。如果你没写,Java会给你补上一个默认的构造函数。
3)左边的“Vehicle veh 1”创建了一个Vehicle类引用变量。所谓Vehicle类引用,就是以后可以用来指向Vehicle对象的对象引用。
4)“=”操作符使对象引用指向刚创建的那个Vehicle对象。
我们可以把这条语句拆成两部分:
Vehicle veh1;
veh1 = new Vehicle();
效果是一样的。这样写,就比较清楚了,有两个实体:一是对象引用变量,一是对象本身。
在堆空间里创建的实体,与在数据段以及栈空间里创建的实体不同。尽管它们也是确确实实存在的实体,但是,我们看不见,也摸不着。不仅如此,我们仔细研究一下第二句,找找刚创建的对象叫什么名字?有人说,它叫“Vehicle”。不对,“Vehicle”是类(对象的创建模板)的名字。
一个Vehicle类可以据此创建出无数个对象,这些对象不可能全叫“Vehicle”。
对象连名都没有,没法直接访问它。我们只能通过对象引用来间接访问对象。
为了形象地说明对象、引用及它们之间的关系,可以做一个或许不很妥当的比喻。对象好比是一只很大的气球,大到我们抓不住它。引用变量是一根绳, 可以用来系汽球。
如果只执行了第一条语句,还没执行第二条,此时创建的引用变量veh1还没指向任何一个对象,它的值是null。引用变量可以指向某个对象,或者为null。
它是一根绳,一根还没有系上任何一个汽球的绳。执行了第二句后,一只新汽球做出来了,并被系在veh1这根绳上。我们抓住这根绳,就等于抓住了那只汽球。
再来一句:
Vehicle veh2;
就又做了一根绳,还没系上汽球。如果再加一句:
veh2 = veh1;
系上了。这里,发生了复制行为。但是,要说明的是,对象本身并没有被复制,被复制的只是对象引用。结果是,veh2也指向了veh1所指向的对象。两根绳系的是同一只汽球。
如果用下句再创建一个对象:
veh2 = new Vehicle();
则引用变量veh2改指向第二个对象。
从以上叙述再推演下去,我们可以获得以下结论:
(1)一个对象引用可以指向0个或1个对象(一根绳子可以不系汽球,也可以系一个汽球);
(2)一个对象可以有N个引用指向它(可以有N条绳子系住一个汽球)。
如果再来下面语句:
veh1 = veh2;
按上面的推断,veh1也指向了第二个对象。这个没问题。问题是第一个对象呢?没有一条绳子系住它,它飞了。多数书里说,它被Java的垃圾回收机制回收了。这不确切。正确地说,它已成为垃圾回收机制的处理对象。至于什么时候真正被回收,那要看垃圾回收机制的心情了。
由此看来,下面的语句应该不合法吧?至少是没用的吧?
new Vehicle();
不对。它是合法的,而且可用的。譬如,如果我们仅仅为了打印而生成一个对象,就不需要用引用变量来系住它。最常见的就是打印字符串:
System.out.println(“I am Java!”);
字符串对象“I am Java!”在打印后即被丢弃。有人把这种对象称之为临时对象。对象与引用的关系将持续到对象回收。
二、另一个角度分析Java对象和引用以及与其密切相关的参数传递
先看下面的程序:
StringBuffer s;
s = new StringBuffer("Hello World!");
第一个语句仅为引用(reference)分配了空间,而第二个语句则通过调用类(StringBuffer)的构造函数StringBuffer(String str)为类生成了一个实例(或称为对象)。这两个操作被完成后,对象的内容则可通过s进行访问——在Java里都是通过引用来操纵对象的。
Java对象和引用的关系可以说是互相关联,却又彼此独立。彼此独立主要表现在:引用是可以改变的,它可以指向别的对象,譬如上面的s,你可以给它另外的对象,如:
s = new StringBuffer("Java");这样一来,s就和它指向的第一个对象脱离关系。
从存储空间上来说,对象和引用也是独立的,它们存储在不同的地方,对象一般存储在堆中,而引用存储在速度更快的堆栈中。
引用可以指向不同的对象,对象也可以被多个引用操纵,如:
StringBuffer s1 = s;
这条语句使得s1和s指向同一个对象。既然两个引用指向同一个对象,那么不管使用哪个引用操纵对象,对象的内容都发生改变,并且只有一份,通过s1和s得到的内容自然也一样,(String除外,因为String始终不变,String s1=”AAAA”; String s=s1,操作s,s1由于始终不变,所以为s另外开辟了空间来存储s,)如下面的程序:
StringBuffer s;
s = new StringBuffer("Java");
StringBuffer s1 = s;
s1.append(" World");
System.out.println("s1=" + s1.toString());//打印结果为:s1=Java World
System.out.println("s=" + s.toString());//打印结果为:s=Java World
上面的程序表明,s1和s打印出来的内容是一样的,这样的结果看起来让人非常疑惑,但是仔细想想,s1和s只是两个引用,它们只是操纵杆而已,它们指向同一个对象,操纵的也是同一个对象,通过它们得到的是同一个对象的内容。这就像汽车的刹车和油门,它们操纵的都是车速,假如汽车开始的速度是80,然后你踩了一次油门,汽车加速了,假如车速升到了120,然后你踩一下刹车,此时车速是从120开始下降的,假如下降到60,再踩一次油门,车速则从60开始上升,而不是从第一次踩油门后的120开始。也就是说车速同时受油门和刹车影响,它们的影响是累积起来的,而不是各自独立(除非刹车和油门不在一辆车上)。所以,在上面的程序中,不管使用s1还是s操纵对象,它们对对象的影响也是累积起来的(更多的引用同理)。
只有理解了对象和引用的关系,才能理解参数传递。
一般面试题中都会考Java传参的问题,并且它的标准答案是Java只有一种参数传递方式:那就是按值传递,即Java中传递任何东西都是传值。如果传入方法的是基本类型的东西,你就得到此基本类型的一份拷贝。如果是传递引用,就得到引用的拷贝。
一般来说,对于基本类型的传递,我们很容易理解,而对于对象,总让人感觉是按引用传递,看下面的程序:
public class ObjectRef { //基本类型的参数传递 public static void testBasicType(int m) { System.out.println("m=" + m);//m=50 m = 100; System.out.println("m=" + m);//m=100 } //参数为对象,不改变引用的值 public static void add(StringBuffer s) { s.append("_add"); } //参数为对象,改变引用的值 public static void changeRef(StringBuffer s) { s = new StringBuffer("Java"); } public static void main(String[] args) { int i = 50; testBasicType(i); System.out.println(i);//i=50 StringBuffer sMain = new StringBuffer("init"); System.out.println("sMain=" + sMain.toString());//sMain=init add(sMain); System.out.println("sMain=" + sMain.toString());//sMain=init_add changeRef(sMain); System.out.println("sMain=" + sMain.toString());//sMain=init_add } }
以上程序的允许结果显示出,testBasicType方法的参数是基本类型,尽管参数m的值发生改变,但并不影响i。
add方法的参数是一个对象,当把sMain传给参数s时,s得到的是sMain的拷贝,所以s和sMain指向同一个对象,因此,使用s操作影响的其实就是sMain指向的对象,故调用add方法后,sMain指向的对象的内容发生了改变。
在changeRef方法中,参数也是对象,当把sMain传给参数s时,s得到的是sMain的拷贝,但与add方法不同的是,在方法体内改变了s指向的对象(也就是s指向了别的对象,牵着气球的绳子换气球了),给s重新赋值后,s与sMain已经毫无关联,它和sMain指向了不同的对象,所以不管对s做什么操作,都不会影响sMain指向的对象,故调用changeRef方法前后sMain指向的对象内容并未发生改变。
对于add方法的调用结果,可能很多人会有这种感觉:这不明明是按引用传递吗?对于这种问题,还是套用Bruce Eckel的话:这依赖于你如何看待引用,最终你会明白,这个争论并没那么重要。真正重要的是,你要理解,传引用使得(调用者的)对象的修改变得不可预期。
public class Test{ public int i,j; public void test_m(Test a){ Test b = new Test(); b.i = 1; b.j = 2; a = b; } public void test_m1(Test a){ a.i = 1; a.j = 2; } public static void main(String argv[]){ Test t = new Test(); t.i = 5; t.j = 6; System.out.println( "t.i = "+ t.i + " t.j= " + t.j); //5,6 t.test_m(t); // 5,6,a和t都指向了一个对象,而在test_m中s又指向了另一个对象,所以对象t不变!!! System.out.println( "t.i = "+ t.i + " t.j= " + t.j); t.test_m1(t); System.out.println( "t.i = "+ t.i + " t.j= " + t.j); //1,2 } }
答案只有一个:Java里都是按值传递参数。而实际上,我们要明白,当参数是对象时,传引用会发生什么状况(就像上面的add方法)。
如下表达式:
A a1 = new A();
它代表A是类,a1是引用,a1不是对象,new A()才是对象,a1引用指向new A()这个对象。在JAVA里,“=”不能被看成是一个赋值语句,它不是在把一个对象赋给另外一个对象,它的执行过程实质上是将右边对象的地址传给了左边的引用,使得左边的引用指向了右边的对象。JAVA表面上看起来没有指针,但它的引用其实质就是一个指针,引用里面存放的并不是对象,而是该对象的地址,使得该引用指向了对象。在JAVA里,“=”语句不应该被翻译成赋值语句,因为它所执行的确实不是一个赋值的过程,而是一个传地址的过程,被译成赋值语句会造成很多误解,译得不准确。
再如:A a2;
它代表A是类,a2是引用,a2不是对象,a2所指向的对象为空null;再如:a2 = a1;
它代表,a2是引用,a1也是引用,a1所指向的对象的地址传给了a2(传址),使得a2和a1指向了同一对象。综上所述,可以简单的记为,在初始化时,“=”语句左边的是引用,右边new出来的是对象。在后面的左右都是引用的“=”语句时,左右的引用同时指向了右边引用所指向的对象。再所谓实例,其实就是对象的同义词。
如果需要赋值,就需要类实现Cloneable接口,实现clone()方法。
- class D implements Cloneable{//实现Cloneable接口
- String sex;
- D(String sex){
- this.sex=sex;
- }
- @Override
- protected Object clone() throws CloneNotSupportedException {
- // 实现clone方法
- return super.clone();
- }
- }
class D implements Cloneable{//实现Cloneable接口 String sex; D(String sex){ this.sex=sex; } @Override protected Object clone() throws CloneNotSupportedException { // 实现clone方法 return super.clone(); } }
赋值的时候:
如果类中的变量不是主类型,而是对象,也需要调用该对象的clone()方法
下面是一个完整的例子:- public class Test2 {
- public static void main(String[] args) throws CloneNotSupportedException {
- // TODO Auto-generated method stub
- D d=new D("男");
- C c=new C("张三","20",d);
- C new_c=(C) c.clone();//调用clone方法来赋值
- new_c.name="李四";
- d.sex="女";//d
- System.out.println(c.d.sex);
- System.out.println(c.name);
- }
- }
- class C implements Cloneable{
- String name;
- String age;
- D d;
- C(String name,String age,D d) throws CloneNotSupportedException{
- this.name=name;
- this.age=age;
- this.d=(D) d.clone();//调用clone方法来赋值,这样即便外部的d发生变化,c里的也不会变
- }
- @Override
- protected Object clone() throws CloneNotSupportedException {
- // TODO Auto-generated method stub
- return super.clone();
- }
- }
- class D implements Cloneable{//实现Cloneable接口
- String sex;
- D(String sex){
- this.sex=sex;
- }
- @Override
- protected Object clone() throws CloneNotSupportedException {
- // 实现clone方法
- return super.clone();
- }
- }
public class Test2 { public static void main(String[] args) throws CloneNotSupportedException { // TODO Auto-generated method stub D d=new D("男"); C c=new C("张三","20",d); C new_c=(C) c.clone();//调用clone方法来赋值 new_c.name="李四"; d.sex="女";//d System.out.println(c.d.sex); System.out.println(c.name); } } class C implements Cloneable{ String name; String age; D d; C(String name,String age,D d) throws CloneNotSupportedException{ this.name=name; this.age=age; this.d=(D) d.clone();//调用clone方法来赋值,这样即便外部的d发生变化,c里的也不会变 } @Override protected Object clone() throws CloneNotSupportedException { // TODO Auto-generated method stub return super.clone(); } } class D implements Cloneable{//实现Cloneable接口 String sex; D(String sex){ this.sex=sex; } @Override protected Object clone() throws CloneNotSupportedException { // 实现clone方法 return super.clone(); } }
三、Java强引用、 软引用、 弱引用、虚引用
既然讨论到了java的引用问题,自然就会想到java的引用分为几种情况,下面详细说一下Java的四种引用。
1、对象的强、软、弱和虚引用
在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及(reachable)状态,程序才能使用它。从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
图1为对象应用类层次
1)强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
2)软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。3) 弱引用(WeakReference)
弱引用与软引用的区别在于:弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。4)虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
2、对象可及性的判断
在很多时候,一个对象并不是从根集直接引用的,而是一个对象被其他对象引用,甚至同时被几个对象所引用,从而构成一个以根集为顶的树形结构。如图2所示
在这个树形的引用链中,箭头的方向代表了引用的方向,所指向的对象是被引用对象。由图可以看出,从根集到一个对象可以由很多条路径。比如到达对象5的路径就有①-⑤,③-⑦两条路径。由此带来了一个问题,那就是某个对象的可达性如何判断:
单条引用路径可达性判断:在这条路径中,最弱的一个引用决定对象的可达性。
多条引用路径可达性判断:几条路径中,最强的一条的引用决定对象的可达性。
比如,我们假设图2中引用①和③为强引用,⑤为软引用,⑦为弱引用,对于对象5按照这两个判断原则,路径①-⑤取最弱的引用⑤,因此该路径对对象5的引用为软引用。同样,③-⑦为弱引用。在这两条路径之间取最强的引用,于是对象5是一个软可达对象。
3、使用软引用构建敏感数据的缓存
3.1 为什么需要使用软引用
首先,我们看一个雇员信息查询系统的实例。我们将使用一个Java语言实现的雇员信息查询系统查询存储在磁盘文件或者数据库中的雇员人事档案信息。作为一个用户,我们完全有可能需要回头去查看几分钟甚至几秒钟前查看过的雇员档案信息(同样,我们在浏览WEB页面的时候也经常会使用“后退”按钮)。这时我们通常会有两种程序实现方式:一种是把过去查看过的雇员信息保存在内存中,每一个存储了雇员档案信息的Java对象的生命周期贯穿整个应用程序始终;另一种是当用户开始查看其他雇员的档案信息的时候,把存储了当前所查看的雇员档案信息的Java对象结束引用,使得垃圾收集线程可以回收其所占用的内存空间,当用户再次需要浏览该雇员的档案信息的时候,重新构建该雇员的信息。很显然,第一种实现方法将造成大量的内存浪费,而第二种实现的缺陷在于即使垃圾收集线程还没有进行垃圾收集,包含雇员档案信息的对象仍然完好地保存在内存中,应用程序也要重新构建一个对象。我们知道,访问磁盘文件、访问网络资源、查询数据库等操作都是影响应用程序执行性能的重要因素,如果能重新获取那些尚未被回收的Java对象的引用,必将减少不必要的访问,大大提高程序的运行速度。
3.2 如果使用软引用
SoftReference的特点是它的一个实例保存对一个Java对象的软引用,该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。另外,一旦垃圾线程回收该Java对象之后,get()方法将返回null。看下面代码:
MyObject aRef = new MyObject();
SoftReference aSoftRef = new SoftReference( aRef );
此时,对于这个MyObject对象,有两个引用路径,一个是来自SoftReference对象的软引用,一个来自变量aRef的强引用,所以这个MyObject对象是强可及对象。
随即,我们可以结束aRef对这个MyObject实例的强引用:
aRef = null ;
此后,这个MyObject对象成为了软可达对象。如果垃圾收集线程进行内存垃圾收集,并不会因为有一个SoftReference对该对象的引用而始终保留该对象。Java虚拟机的垃圾收集线程对软可达对象和其他一般Java对象进行了区别对待:软可及对象的清理是由垃圾收集线程根据其特定算法按照内存需求决定的。也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软可及对象,而且虚拟机会尽可能优先回收长时间闲置不用的软可达对象,对那些刚刚构建的或刚刚使用过的软可达对象会被虚拟机尽可能保留。在回收这些对象之前,我们可以通过:
MyObject anotherRef =(MyObject) aSoftRef .get()
重新获得对该实例的强引用。而回收之后,调用get()方法就只能得到null了。
3.3 使用ReferenceQueue清除失去了软引用对象的SoftReference
作为一个Java对象,SoftReference对象除了具有保存软引用的特殊性之外,也具有Java对象的一般性。所以,当软可及对象被回收之后,虽然这个SoftReference对象的get()方法返回null,但这个SoftReference对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量SoftReference对象带来的内存泄漏。在java.lang.ref包里还提供了ReferenceQueue。如果在创建SoftReference对象的时候,使用了一个ReferenceQueue对象作为参数提供给SoftReference的构造方法,如:
ReferenceQueue queue = new ReferenceQueue();
SoftReference ref = new SoftReference( aMyObject , queue );
那么当这个SoftReference所软引用的aMyOhject被垃圾收集器回收的同时,ref所强引用的SoftReference对象被列入ReferenceQueue。也就是说,ReferenceQueue中保存的对象是Reference对象,而且是已经失去了它所软引用的对象的Reference对象。另外从ReferenceQueue这个名字也可以看出,它是一个队列,当我们调用它的poll()方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个Reference对象。
在任何时候,我们都可以调用ReferenceQueue的poll()方法来检查是否有它所关心的非强可达对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。利用这个方法,我们可以检查哪个SoftReference所软引用的对象已经被回收。于是我们可以把这些失去所软引用的对象的SoftReference对象清除掉。常用的方式为:
SoftReference ref = null ;
while ((ref = (SoftReference)q .poll()) != null ) {
// 清除 ref
}
理解了ReferenceQueue的工作机制之后,我们就可以开始构造一个Java对象的高速缓存器了。
3.4通过软可及对象重获方法实现Java对象的高速缓存
利用Java2平台垃圾收集机制的特性以及前述的垃圾对象重获方法,我们通过一个雇员信息查询系统的小例子来说明如何构建一种高速缓存器来避免重复构建同一个对象带来的性能损失。我们将一个雇员的档案信息定义为一个Employee类:
public class Employee {
private String id ; // 雇员的标识号码
private String name ; // 雇员姓名
private String department ; // 该雇员所在部门
private String Phone ; // 该雇员联系电话
private int salary ; // 该雇员薪资
private String origin ; // 该雇员信息的来源
// 构造方法
public Employee(String id) {
this . id = id;
getDataFromlnfoCenter();
}
// 到数据库中取得雇员信息
private void getDataFromlnfoCenter() {
// 和数据库建立连接井查询该雇员的信息,将查询结果赋值
// 给 name, department, plone, salary等变量
// 同时将 origin赋值为 "From DataBase"
}
}
这个Employee类的构造方法中我们可以预见,如果每次需要查询一个雇员的信息。哪怕是几秒中之前刚刚查询过的,都要重新构建一个实例,这是需要消耗很多时间的。下面是一个对Employee对象进行缓存的缓存器的定义:import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.util.Hashtable; public class EmployeeCache { private static EmployeeCache cache ; // 一个 Cache实例 private Hashtable<String, EmployeeRef> employeeRefs ; // 用于 Chche内容的存储 private ReferenceQueue<Employee> q ; // 垃圾 Reference的队列 // 继承 SoftReference,使得每一个实例都具有可识别的标识, // 并且该标识与其在 HashMap内的 key相同。 private class EmployeeRef extends SoftReference<Employee> { private String _key = "" ; public EmployeeRef(Employee em, ReferenceQueue<Employee> q) { super (em, q); _key = em.getID(); } } // 构建一个缓存器实例 private EmployeeCache() { employeeRefs = new Hashtable<String,EmployeeRef>(); q = new ReferenceQueue<Employee>(); } // 取得缓存器实例 public static synchronized EmployeeCache getInstance() { if(cache == null){ cache = new EmployeeCache(); } return cache ; } // 以软引用的方式对一个 Employee对象的实例进行引用并保存该引用 private void cacheEmployee(Employee em) { cleanCache(); // 清除垃圾引用 EmployeeRef ref = new EmployeeRef(em, q ); employeeRefs .put(em.getID(), ref); } // 依据所指定的 ID号,重新获取相应 Employee对象的实例 public Employee getEmployee(String ID) { Employee em = null ; // 缓存中是否有该 Employee实例的软引用,如果有,从软引用中取得。 if(employeeRefs .containsKey(ID)) { EmployeeRef ref = (EmployeeRef) employeeRefs .get(ID); em = (Employee) ref.get(); } // 如果没有软引用,或者从软引用中得到的实例是 null,重新构建一个实例, // 并保存对这个新建实例的软引用 if (em == null ) { em = new Employee(ID); System. out .println( "Retrieve From EmployeeInfoCenter. ID=" + ID); this .cacheEmployee(em); } return em; } // 清除那些所软引用的 Employee对象已经被回收的 EmployeeRef对象 private void cleanCache() { EmployeeRef ref = null ; while ((ref = (EmployeeRef) q .poll()) != null ) { employeeRefs .remove(ref. _key ); } } }
-
java对象与对象引用变量
2018-07-12 14:47:54Java对象及其引用 先搞清楚什么是堆,什么是栈。 Java开辟了两类存储区域,对比二者的特点 存储区域 存储内容 优点 缺点 回收 栈 基本类型的变量和对象的引用变量 存取速度比堆要快,仅次于... -
Java——值引用和对象引用
2019-09-12 11:47:49如果你答对了,也不要高兴,你可能只是感觉上对了,我之前一直以为对象引用和C语言中的指针是一回事,其实不是,他是引用的复制,接下来我们看看他在内存中是怎么存储的吧: 总之你可以这么理解: = 抽象上来... -
Java中对象的赋值与引用
2017-07-28 19:52:34Java中对象的赋值与引用详解 -
JAVA对象引用和值引用
2009-12-02 22:45:00以前就知道JAVA对象分对象引用和值引用,并且还知道8种基础数据类型,即引用时是值引用的数据类型,比如int,short,long,byte,float,double,char,boolean,其它都是对象引用。可是其它的对象引用我一直都以为跟c里面... -
Java中的引用类型的对象存放在哪里
2020-06-19 12:04:22根据上下文来确定。 根据上下文来确定。 根据上下文来确定。 比如 void func() ...对于方法中的局部变量的引用时存放在java运行时数据区的栈中,对于实例变量则是存放在java运行时数据区的堆中。 ... -
Java对象的四种引用
2021-09-09 16:00:03也就是说,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。垃圾回收器一旦发现这些无用对象,就会对其进行回收。但是,在某些情况下,我们会希望有些对象不需要被立即回收,或者说从全局的角度来说没有... -
Java对象,对象引用,参数传递
2018-07-01 19:45:46在Java中,万物皆对象! 比如定义一个学生类 public class Student { private int id; private String name; private int age; public Student() { // TODO Auto-generated constructor stub su... -
一看就懂的Java对象四种引用方式总结
2020-07-06 16:50:50这应该是一道很常见的面试题,但是有些小伙伴也不一定能很好的说清楚Java对象四种引用方式,这边文章总结Java四种引用方式,希望可以帮到有缘人,哈哈。 强引用StrongReference 这种方式是平时工作中应用最多的一种... -
Java对象引用处理机制
2013-11-13 11:31:17Java的引用别名机制(原文为Aliasing,别名,即Java中的多态)意味着多个引用变量可以定位到同一个实际物理对象,而这些引用变量可以是不同的类型. 下面的代码中,S类继承P类, pp 和 ss 分别是P类型 和 S类 -
java如何输出一个对象的引用名
2016-12-09 21:43:11Object abc = new object(); 想要输出上面语句的“abc”这个名字 方法一: 在Object类中加入一个String类型的属性 例如:class Object(){ ...如此,在创建abc这个引用名的时候同时new object("abc")即可 -
Java对象和引用变量
2016-08-31 00:45:30对于引用变量的深层含义,未必在初学的时候就能深刻理解, 所以理解好下面这两句话的真正含义非常重要Case cc=new Case();... 基本类型的变量和对象的引用变量 存取速度比堆要快,仅次于寄存器,栈 -
java引用拷贝,对象浅拷贝,对象深拷贝
2019-10-29 16:05:43JAVA对象拷贝分为两种方式,一种是引用拷贝,一种是对象拷贝 引用拷贝:和对象拷贝的不同之处在于,引用拷贝只会生成一个新的对象引用地址,但两个地址其最终指向的还是同一个对象; 对象拷贝:这种方式会重新生成... -
java中对象和引用对象的区别
2019-09-15 15:49:09什么是对象,什么是对象引用 对象,就是类的一个实例化,把一个抽象不好理解的类举出一个实体来,例如人类是一个类,会吃喝拉撒,实例化出一个小明这个具体的人。 对象引用,就是得给这个人取个名字来指代他,跟c++... -
java之循环引用
2018-12-13 14:49:26在学习java内存模型及垃圾回收时提到了引用计数法无法解决循环引用的问题,心里一直在思考怎么才是循环引用。 netty中的循环引用的例子。 例如:NioServerSocketChannel类中有内部类NioServerSocketChannelConfig。... -
Java 对象序列化 JSON时,数据出现引用结构 $ref
2019-02-01 15:44:31对Java对象序列化操作时,发现JSON(使用阿里巴巴的FastJSON组件)数据中,包含“$ref”结构数据; 这是为了避免触发 StackOverflowError 错误而做的处理。 第一种解决方法是,关闭FastJSON的引用检测。(不建议... -
Java 中对象的引用的四种级别
2016-07-14 10:44:28Java 中对象的引用的四种级别 -
Java中对象的几种引用
2013-11-14 22:34:26随时随地阅读更多技术实战干货,获取项目源码、学习资料,请关注源代码社区...这是Java最常见的引用方式,创建一个对象,并把它赋给一个引用变量,程序通过该变量来操作实际的对象,当一个对象被其他引用变量引用... -
Java对象的生命周期与垃圾回收以及四种引用
2021-03-06 22:31:10创建对象的方式用new语句创建...调用对象的clone()方法使用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法。还有其他一些隐式创建对象的方法:对于java命令中的每个命令行参数,Java虚拟机都会... -
对象的引用和清除_Java语言程
2021-03-17 15:54:13对象的引用和清除_Java语言程4.3.3 对象的引用和清除在创建了类的对象后,就可以使用对象。即对象使用的原则是“先创建后使用”。使用对象的方法是:通过运算符“.”访问对象的各个成员变量和成员方法,进行各种... -
java中对象,变量,方法,变量的数据和对象的引用的存放位置
2018-10-11 10:13:32java中,对象,还有所有的new出来的对象都是存放在堆里。 方法存放在方法体中。 基本类型的变量的数据和对象的引用存放在栈中 ... -
关于Java对象作为参数传递是传值还是传引用的问题
2016-09-06 22:37:20前言 在Java中,当对象作为参数传递时,究竟传递的是对象的值,还是对象的引用,这是一个饱受争议的话题。若传的是值,那么函数接收的只是实参的一个副本,函数对形参的操作并不会对实参产生影响;若传的是引用,... -
java中对象的引用(强引用、软引用、弱引用、虚引用)
2015-04-08 14:31:03在JDK1.2以前的版本中,当一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及状态,程序才能使用它。这 就像在日常生活中,从商店购买了某样物品后,如果有用,就一直 -
Java 对象、引用和指针
2016-05-13 21:02:02在前面PersonTest.java代码中,有这样一行代码:Person p = new Person(); 这行代码创建了一个Person实例,也被称为Person对象,这个Person对象被赋给p变量。这行代码中实际产生了两个东西:一个是p变量,一个是... -
吃人的那些 Java 名词:对象、引用、堆、栈
2019-09-05 15:57:09作为一个有着 8 年 Java 编程经验的 IT 老兵,说起来很惭愧,我被 Java 当中的四五个名词一直困扰着:**对象、引用、堆、栈、堆栈**(栈可同堆栈,因此是四个名词,也是五个名词)。每次我看到这几个名词,都隐隐...