您的位置:首页 > 编程语言 > Java开发

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

2014-10-21 20:52 441 查看
当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反射机制去获取幽灵引用指向的对象。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: