Java四种引用类型
在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用。
- 强引用:强引用就是代码中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还在,垃圾回收器就不会回收掉被引用的对象。
- 软引用:用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列金回收范围之中并进行第二次回收。如果这次回收还是没有足够内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。软引用可以结合ReferenceQueue来使用,当由于系统内存不足,导致软引用的对象被回收了,JVM会把这个软引用加入到与之相关联的ReferenceQueue中。
- 弱引用:也用来描述费必需对想,强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用管理的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。弱引用可以结合ReferenceQueue来使用,当由于系统触发gc,导致软引用的对象被回收了,JVM会把这个弱引用加入到与之相关联的ReferenceQueue中,不过由于垃圾收集器线程的优先级很低,所以弱引用不一定会被很快回收。
- 虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能来这个对象被收集器回收时收到一个系统通知。与软引用和弱引用不同的是,虚引用必须有一个与之关联的ReferenceQueue。
SoftReference
SoftReference<String> sr = new SoftReference<String>(new String("hello"),queue);
System.out.println(sr.get());
System.gc();
System.out.println(sr.get());
System.out.println(sr.isEnqueued());
WeakReference
ReferenceQueue<String> queue = new ReferenceQueue<String>();
WeakReference<String> wr = new WeakReference<String>(new String("hello"),queue);
System.out.println(wr.get());
System.gc();
System.out.println(wr.get());
System.out.println(wr.isEnqueued());
PhantomReference
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String>pr = new PhantomReference<String>(new String("hello"), queue);
System.out.println(pr.get());
FinalReference
类的修饰有很多,比如final,abstract,public等,如果某个类用final修饰,我们就说这个类是final类,上面列的都是语法层面我们可以显式指定的,在JVM里其实还会给类标记一些其他符号,比如finalizer,表示这个类是一个finalizer类(为了和java.lang.ref.Fianlizer类区分,下文在提到的finalizer类时会简称为f类),GC在处理这种类的对象时要做一些特殊的处理,如在这个对象被回收之前会调用它的finalize方法。
如何判断一个类是不是一个f类
在讲这个问题之前,我们先来看下java.lang.Object里的一个方法
protected void finalize() throws Throwable { }
在Object类里定义了一个名为finalize的空方法,这意味着Java里的所有类都会继承这个方法,甚至可以覆写该方法,并且根据方法覆写原则,如果子类覆盖此方法,方法访问权限至少protected级别的,这样其子类就算没有覆写此方法也会继承此方法。
而判断当前类是否是f类的标准并不仅仅是当前类是否含有一个参数为空,返回值为void的finalize方法,还要求finalize方法必须非空,因此Object类虽然含有一个finalize方法,但它并不是f类,Object的对象在被GC回收时其实并不会调用它的finalize方法。
需要注意的是,类在加载过程中其实就已经被标记为是否为f类了。(jvm在类加载的时候会遍历当前类的所有方法,包括父类的方法,只要有一个参数为空且返回void的非空finalize方法就认为这个类是一个f类。)
f类的对象何时传到Finalizer.register方法
对象的创建其实是被拆分成多个步骤的,比如A a=new A(2)这样一条语句对应的字节码如下:
0: new #1 // class A
3: dup
4: iconst_2
5: invokespecial #11 // Method “”:(I)V
先执行new分配好对象空间,然后再执行invokespecial调用构造函数,JVM里其实可以让用户在这两个时机中选择一个,将当前对象传递给Finalizer.register方法来注册到Finalizer对象链里,这个选择取决于是否设置了RegisterFinalizersAtInit这个vm参数,默认值为true,也就是在构造函数返回之前调用Finalizer.register方法,如果通过-XX:-RegisterFinalizersAtInit关闭了该参数,那将在对象空间分配好之后将这个对象注册进去。
另外需要提醒的是,当我们通过clone的方式复制一个对象时,如果当前类是一个f类,那么在clone完成时将调用Finalizer.register方法进行注册。
private static class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g, null, "Finalizer", 0, false);
}
public void run() {
// in case of recursive call to run()
if (running)
return;
// Finalizer thread starts before System.initializeSystemClass
// is called. Wait until JavaLangAccess is available
while (VM.initLevel() == 0) {
// delay until VM completes initialization
try {
VM.awaitInitLevel(1);
} catch (InterruptedException x) {
// ignore and continue
}
}
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();//从引用队列(referenceQueue)中取出
f.runFinalizer(jla);
} catch (InterruptedException x) {
// ignore and continue
}
}
}
}
f对象的finalize方法会执行多次吗
如果我们在f对象的finalize方法里重新将当前对象赋值,变成可达对象,当这个f对象再次变成不可达时还会执行finalize方法吗?答案是否定的,因为在执行完第一次finalize方法后,这个f对象已经和之前的Finalizer对象剥离了,也就是下次GC的时候不会再发现Finalizer对象指向该f对象了,自然也就不会调用这个f对象的finalize方法了。
Finalizer对象何时被放到ReferenceQueue里
除了这里接下来要介绍的环节之外,整个过程大家应该都比较清楚了。
当GC发生时,GC算法会判断f类对象是不是只被Finalizer类引用(f类对象被Finalizer对象引用,然后放到Finalizer对象链里),如果这个类仅仅被Finalizer对象引用,说明这个对象在不久的将来会被回收,现在可以执行它的finalize方法了,于是会将这个Finalizer对象放到Finalizer类的ReferenceQueue里,但是这个f类对象其实并没有被回收,因为Finalizer这个类还对它们保持引用,在GC完成之前,JVM会调用ReferenceQueue中lock对象的notify方法(当ReferenceQueue为空时,FinalizerThread线程会调用ReferenceQueue的lock对象的wait方法直到被JVM唤醒),此时就会执行上面FinalizeThread线程里看到的其他逻辑了。Finalizer导致的内存泄露
这里举一个简单的例子,我们使用挺广的Socket通信,SocksSocketImpl的父类其实就实现了finalize方法:
/**
- Cleans up if the user forgets to close it.
*/
protected void finalize() throws IOException {
close();
}
其实这么做的主要目的是万一用户忘记关闭Socket,那么在这个对象被回收时能主动关闭Socket来释放一些系统资源,但是如果用户真的忘记关闭,那这些socket对象可能因为FinalizeThread迟迟没有执行这些socket对象的finalize方法,而导致内存泄露,这种问题我们碰到过多次,需要特别注意的是对于已经没有地方引用的这些f对象,并不会在最近的那一次gc里马上回收掉,而是会延迟到下一个或者下几个gc时才被回收,因为执行finalize方法的动作无法在gc过程中执行,万一finalize方法执行很长呢,所以只能在这个gc周期里将这个垃圾对象重新标活,直到执行完finalize方法从queue里删除,这样下次gc的时候就真的是漂浮垃圾了会被回收,因此给大家的一个建议是千万不要在运行期不断创建f对象,不然会很悲剧。
Finalizer的客观评价
上面的过程基本对Finalizer的实现细节进行了完整剖析,Java里我们看到有构造函数,但是并没有看到析构函数一说,Finalizer其实是实现了析构函数的概念,我们在对象被回收前可以执行一些“收拾性”的逻辑,应该说是一个特殊场景的补充,但是这种概念的实现给f对象生命周期以及GC等带来了一些影响:
f对象因为Finalizer的引用而变成了一个临时的强引用,即使没有其他的强引用,还是无法立即被回收;
f对象至少经历两次GC才能被回收,因为只有在FinalizerThread执行完了f对象的finalize方法的情况下才有可能被下次GC回收,而有可能期间已经经历过多次GC了,但是一直还没执行f对象的finalize方法;
CPU资源比较稀缺的情况下FinalizerThread线程有可能因为优先级比较低而延迟执行f对象的finalize方法;
因为f对象的finalize方法迟迟没有执行,有可能会导致大部分f对象进入到old分代,此时容易引发old分代的GC,甚至Full GC,GC暂停时间明显变长;
f对象的finalize方法被调用后,这个对象其实还并没有被回收,虽然可能在不久的将来会被回收。
Java中的“隐式”强引用
java中有些强引用并非简单可以分辨出的,比如上边Finalizer中对对象临时的引用。这些引用在编码过程中非常容易造成oom,下面就来总结下,java中还有哪些常见的此类的“隐式”引用。
-
f类对象的引用
-
内部类对外部类的引用(哪些内部类会对外部类持有引用)
- 成员内部类:外部类可以访问内部类所有成员变量,内部类可以访问外部类所有成员变量;内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。比如上面的例子,如果成员内部类Inner用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。
public class Outter {
private String outterStr="outterPrivate";
public void accessInner(){
System.out.println(new Inner().innerStr);
}
public class Inner{
private String innerStr="innerPrivate";
public void accessOutter(){
System.out.println(outterStr);
}
}
public static void main(String[] args){
Outter o=new Outter();
Inner i=o.new Inner();
o.accessInner();
i.accessOutter();
System.out.println("debugger");
}
}
反编译Outter$Inner.class,可以看到inner通过Outter.access$0访问外部类字段
public void accessOutter()
{
System.out.println(Outter.access$0(this.this$0));
}
查看字节码,发现内部类持有了外部类的引用:
final Outter this$0;
descriptor: LOutter;
flags: (0x1010) ACC_FINAL, ACC_SYNTHETIC
- 局部内部类:局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。局部内部类就像是方法里面的一个局部变量一样,不能有public、protected、private以及static修饰符,只能访问局部final变量。代码如下:
public class Outter {
private String outterStr="outterPrivate";
public void accessLocalInner(){
class LocalInner{
private String innerStr="innerPrivate";
public void accessOutter(){
System.out.println(outterStr);
}
}
LocalInner inner=new LocalInner();
System.out.println(inner.innerStr);
inner.accessOutter();
}
public static void main(String[] args){
Outter o=new Outter();
o.accessLocalInner();
System.out.println("debugger");
}
}
内部类持有了外部类的引用
- 匿名内部类:会持有外部类的引用(如果只是在函数的参数那里传入匿名的类,那不会持有外部类的引用)
public class Outter {
private String outterStr="outterPrivate";
public interface Ainterface{
public void sayHaha();
}
public interface AinterfaceB{
public void sayHahaB();
}
// public void accessAnonymousInner(Ainterface a){
// new Thread(new Runnable() {
// @Override
// public void run() {
// System.out.println("a thread has start");
// }
// }).start();
// new Runnable() {
// @Override
// public void run() {
// System.out.println("a thread has run");
// }
// }.run();
// a.sayHaha();
// new AinterfaceB(){
//
// @Override
// public void sayHahaB() {
// // TODO Auto-generated method stub
// System.out.println("HahaB");
// }
//
// }.sayHahaB();
//
// }
//
public void methodA(){
new Ainterface(){
@Override
public void sayHaha() {
// TODO Auto-generated method stub
System.out.println("haha");
}
}.sayHaha();
}
public static void main(String[] args){
Outter o=new Outter();
o.methodA();
/* o.accessAnonymousInner(new Ainterface(){
@Override
public void sayHaha() {
// TODO Auto-generated method stub
System.out.println("haha");
}
});*/
System.out.println("debugger");
}
}
- 静态内部类: 静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。
public class Outter {
private static String staticStr="static";
static class Inner {
public Inner() {
System.out.println(staticStr);
}
}
public static void main(String[] args){
Outter o=new Outter();
Inner i=new Inner();
System.out.println("debugger");
}
}
内部类不会持有外部类的引用