Java内存管理与四种引用类型

    当Java虚拟机启动并运行某个程序之后,它所能使用的内存总量的上限通常是固定的。随着程序的不断运行,虚拟机的内存中可用的空闲空间越来越少,垃圾越来越多,这时就需要运行垃圾回收器来回收内存中的垃圾区域。Java虚拟机中的垃圾回收器是运行在一个独立的线程中的,它会根据当前虚拟机中的内存状态,决定在什么时候进行垃圾回收工作。

    在Java程序中可以通过System.gc方法来建议垃圾回收器立即进行回收工作,注意,这里说的是“建议”,在这种情况下,垃圾回收器也可能选择不运行。垃圾回收器无法回收处于活动状态的对象所占用的内存,当垃圾回收器无法找到可用的空闲内存时,创建新对象的操作会抛出java.lang.OutOfMemoryError错误,导致虚拟机退出。

    在程序的运行过程中,对于同一个对象,可能存在多个指向它的引用。如果不再有引用指向一个对象,那么这个对象会成为垃圾回收的候选目标。Java语言中存在四种引用类型:

强引用:

Object obj = new Object()

    强引用是默认的引用类型,对于垃圾回收器来说,强引用的存在会阻止一个对象被回收。一个对象只要存在一个(或多个)强引用指向它,那么这个对象所处的内存区域就不会被回收。

    java.lang.ref包中包括其它三种类型的引用:SoftReference(软引用),WeakReference(弱引用),PhantomAllocator(幽灵引用/虚引用)。

    这三个类都继承自:java.lang.ref.Reference:

package java.lang.ref;

import sun.misc.Cleaner;
public abstract class Reference<T> {
    private T referent;         /* Treated specially by GC */
    ReferenceQueue<? super T> queue;
    Reference next;
    transient private Reference<T> discovered;  /* used by VM */
    static private class Lock { };
    private static Lock lock = new Lock();
    private static Reference pending = null;
    /**
     * pending对象我并没有找到它在哪里进行的初始化,包括其子类中也没有找到,感觉上
     * 这大概是用于向引用队列中添加对象的线程,还请大神指教!
     */
    private static class  extends Thread {
        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }

        public void run() {
            for (;;) {
                Reference r;
                synchronized (lock) {
                    if (pending != null) {
                        r = pending;
                        Reference rn = r.next;
                        pending = (rn == r) ? null : rn;
                        r.next = r;
                    } else {
                        try {
                            lock.wait();
                        } catch (InterruptedException x) { }
                        continue;
                    }
                }

                // Fast path for cleaners
                if (r instanceof Cleaner) {
                    ((Cleaner)r).clean();
                    continue;
                }

                ReferenceQueue q = r.queue;
                if (q != ReferenceQueue.NULL) q.enqueue(r);
            }
        }
    }

    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        /* If there were a special system-only priority greater than
         * MAX_PRIORITY, it would be used here
         */
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();
    }
    
    /**
    * 返回当前的引用
    */
    public T get() {
        return this.referent;
    }

    /**
    * 清除当前的引用
    */
    public void clear() {
        this.referent = null;
    }

    /**
    * 返回当前引用是否被添加进了引用队列
    */
    public boolean isEnqueued() {
        /* In terms of the internal states, this predicate actually tests
           whether the instance is either Pending or Enqueued */
        synchronized (this) {
            return (this.queue != ReferenceQueue.NULL) && (this.next != null);
        }
    }

    /**
    * 将当前引用添加入引用队列
    */
    public boolean enqueue() {
        return this.queue.enqueue(this);
    }

    Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

}

下面来看具体的三个引用,首先定义一个用于测试的类(Project.java):

package com.memory;

public class Project {
    String name = "hello world";
    public String toString() {
        return name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    protected void finalize() throws Throwable {
        System.out.println("finalize方法被调用");
        super.finalize();
    }
}

软引用(java.lang.ref.SoftReference):

package java.lang.ref;
public class SoftReference<T> extends Reference<T> {
    /**
    * 时间戳,由gc初始化并对其进行更新
    */
    static private long clock;
     /**
     * Timestamp updated by each invocation of the get method.  The VM may use
     * this field when selecting soft references to be cleared, but it is not
     * required to do so.
     */
    private long timestamp;

    public SoftReference(T referent) {
        super(referent);
        this.timestamp = clock;
    }

    public SoftReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
        this.timestamp = clock;
    }

    public T get() {
        T o = super.get();
        if (o != null && this.timestamp != clock)
            this.timestamp = clock;
        return o;
    }
}

    软引用使用方式如下:

Project p = new Project();
SoftReference<Object> sref = new SoftReference<Object>(p);
p = null;
System.out.println(sref.get());
System.gc();
System.out.println(sref.get());

    通过创建一个Reference子类SoftReference类的对象sref,就可以在变量p所引用的对象上添加一个软引用。在创建SoftReference类的对象时,要把所指向的对象作为构造方法的参数传递进去。需要注意的是,在创建了新的软引用之后,要显式的把之前创建的对象上的强引用清除。

    运行该段程序,显示结果如下:

hello world
hello world

    可以看到在调用了gc之后,软引用也没有被清除。这是因为在JVM内存足够的情况下,软引用不会被gc回收,只有在内存不足时,gc才会回收软引用对象所占用的内存空间。gc会保证在抛出OutOfMemoryError错误之前,回收掉所有软引用可达的对象。

弱引用(java.lang.ref.WeakReference):

package java.lang.ref;
public class WeakReference<T> extends Reference<T> {
    public WeakReference(T referent) {
        super(referent);
    }
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

弱引用的使用方法和软引用一样:

Project p  =  new Project();
WeakReference<Project> wref = new WeakReference<Project>(p);
p = null;
System.out.println(wref.get());
System.gc();
System.out.println(wref.get());

    运行该段程序,显示结果为:

hello world
null
finalize方法被调用

   可以看到,与软引用不同的是,只要gc运行了垃圾回收的操作,弱引用可达的对象就会被回收(之后再讨论finalize方法)。

幽灵引用/虚引用(java.lang.ref.PhantomReference):

package java.lang.ref;
public class PhantomReference<T> extends Reference<T> {
    public T get() {
        return null;
    }
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

可以看到,源码中的get方法强行的返回了一个null,也就是说幽灵引用是无法通过get方法获得引用的对象的。

幽灵引用用法如下:

        ReferenceQueue<Project> queue = new ReferenceQueue<Project>();        
        Project p = new Project();
        PhantomReference<Project> pref = new PhantomReference<Project>(p, queue);
        p = null;
        System.gc();
        System.out.println("first gc:");
        Thread.currentThread().sleep(3000);
        System.out.println("queue.poll() = " + queue.poll());
        
        System.gc();
        System.out.println("second gc:");
        Thread.currentThread().sleep(3000);
        
        Object o = queue.poll();
        Field referent = Reference.class.getDeclaredField("referent");
        referent.setAccessible(true);
        Object result = referent.get(o);
        System.out.println("gc will collect:"+ result.getClass() + "@"+ result.hashCode() + "@" + result);

运行该段代码显示如下:

first gc:
finalize方法被调用
queue.poll() = null
second gc:
gc will collect:class com.memory.Project@2117796172@hello world

    Project.java中的finalize方法类似于C++中的析构函数,弱引用(包括软引用)在进行gc操作时,将对象添加到引用队列和调用finalize方法并没有一个必然的先后顺序;而幽灵引用则是先调用finalize方法,再将其添加进引用队列。

    gc是在对象没有引用的情况下才调用其finalize方法,尽管如此,在finalize方法的实现也可能为当前对象添加新的引用。因此在finalize方法运行完成之后,gc会重新检查该对象的引用。如果发现新的引用,那么该对象将被复活。对象被复活后,如果再一次的没有引用指向该对象,对象会直接变为不可达状态,之后由gc进行回收。也就是说,一个对象的finalize方法只会被调用一次。

    如果希望在一个对象的内存被回收之前进行某些清理工作,那么相对于使用finalize方法来说,使用幽灵引用能避免出现对象复活的问题。幽灵引用本身只作为一个通知机制存在,必须存在其他指向此对象的引用,否则从引用队列中获取幽灵引用后,无法获取其指向的对象,也就无法对这个对象进行操作;或者可以使用Java反射机制去获取幽灵引用指向的对象。


你可能感兴趣的:(内存管理,引用类型,垃圾回收器)