Java虚拟机会为我们管理内存,当内存不足时,通过垃圾回收算法来释放不可达的内存。作为Java程序员我们似乎不需要关注这些。
但是在工作中我们可能会遇到内存充足的情况下,也会出现OutOfMemoryError
。
笔者就多次遇到过这种情况,有一次是加载内容过多引起的,通过
Xmx2g
,在分配了2G内存的情况下出现了内存溢出的问题,最后定位到了是压测的时候压测导出Excel接口没有分页导致一次性读取整张表的数据到内存,同时查询数据量过多时间过长,上一个还没导出成功下一个导出请求又来了。
解决方案是控制一次导出的数据量。在此期间执行过Full GC,但是并不会回收加载到内存中的待导出数据,因为它们都是强引用。
本文的切入点是引用类型,下面开始进入主题。Java中有四种引用类型:强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference)和虚引用(Phantom Reference)。
强度由强到弱。
软引用和弱引用哪个更强呢?是不是容易混淆。可以通过单词来记忆,Strong的反义词是Weak,Strong很强的话,Weak就很弱了,而Soft就介于它们之间。
我们通常遇到的引用类型都是强引用,比如通过new
关键词实例化的对象:
Object obj = new Object();
如果没有出现obj = null
,那么在它的作用域范围内,GC是不会进行回收的。当然可能你知道这个对象已经没用了,但是GC不知道。
因此很多书籍推荐显示的执行obj = null
将该引用置空,这样它原先的那块内存就是不可达的了。
为了演示软引用,我们定义一个类,复写了它的finalize
方法,打印一些日志,以使我们知悉。
public class MyObject {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("MyObject's finalize called.");
}
@Override
public String toString() {
return "MyObject@" + Integer.toHexString(hashCode());
}
}
构造软引用代码如下:
MyObject obj = new MyObject();//强引用
ReferenceQueue<MyObject> softQueue = new ReferenceQueue<>();//创建引用队列,如果对象被回收则进入引用队列
SoftReference<MyObject> softRef = new SoftReference<>(obj,softQueue);//创建软引用
new CheckRefQueueThread(softQueue).start();
obj = null;//删除强引用
第1行构造了一个强引用对象,第3行构造了一个软引用对象。
也就是说,现在有两个引用指向了MyObject
对象(obj
和softRef
中的referent
,可以结合下图)。
注意软引用对象和软引用的对象之间的区别,软引用对象指该软引用本身,而软引用的对象指的是软引用中
referent
对象,也就是该软引用持有的对象,在本例中就是MyObject
对象实例。下文中的弱引用的对象以及虚引用的对象都是这个意思。
上图中还有一个ReferenceQueue
,这个后文会分析。
此时,MyObject
实例还不具备被回收的条件(因为还有强引用指向它)。
第5行释放了该强引用,此时,该对象不存在强引用,但存在软引用(称为软可达(softly reachable ))。
此时,我们还是能使用MyObject
对象,通过softRef.get()
。
一个被软引用持有的对象不会被JVM很快回收,只有当堆快要溢出时(内存不足时),才会回收软引用的对象。也就是说,只要有足够的内存,软引用的对象就能在内存中存活相当长的一段时间,该对象还可以继续被程序使用。 软引用一般用来实现内存敏感的缓存。
下面给出完整的例子:
private static void testSoftReference() {
MyObject obj = new MyObject();//强引用
ReferenceQueue<MyObject> softQueue = new ReferenceQueue<>();//创建引用队列,如果对象被回收则进入引用队列
SoftReference<MyObject> softRef = new SoftReference<>(obj,softQueue);//创建软引用
new CheckRefQueueThread(softQueue).start();
obj = null;//删除强引用
System.gc();
System.out.println("After GC:soft Get=" + softRef.get());
System.out.println("分配大块内存");//分配大块内存,强迫GC
byte[] b = new byte[4 *1024 * 804];//防止出现OOM,这个值需要微调一下
System.out.println("After new byte[]:Soft Get=" + softRef.get());
}
上面代码中的CheckRefQueueThread
:
private static class CheckRefQueueThread extends Thread {
private ReferenceQueue<MyObject> queue;
private CheckRefQueueThread(ReferenceQueue<MyObject> queue) {
this.queue = queue;
}
@Override
public void run() {
Reference ref = null;
try {
ref = queue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ref != null) {
System.out.println("Object for " + ref.getClass().getSimpleName() + " is " + ref.get());
}
}
}
执行前,先限定堆大小为5M: Xmx5M
,执行结果为:
After GC:soft Get=MyObject@723279cf
分配大块内存
MyObject's finalize called.
Object for SoftReference is null
After new byte[]:Soft Get=null
(不同的电脑执行结果可能不同,需要微调byte[] b = new byte[4 *1024 * 804];
里面的数值,过大的话会出现OOM)
我们来分析一下上面的执行结果,首先构造出MyObject
对象,然后构造该对象的软引用softRef
,并注册到引用队列,
当MyObject
强引用对象被回收时,软引用会被加入到引用队列。
设置obj=null
来删除强引用,此时MyObject
对象变成软可达,然后显式调用GC,通过软引用的get()
(此时我们只能通过软引用的该方法来访问它了,之前的obj
已经置为null
了)方法,还是可以取得MyObject
对象实例的强引用,发现对象并未被回收。这说明GC在内存充足的情况下,并不会回收软可达对象。
然后请求一块大的堆空间,使得内存不足,从而迫使新一轮的GC。在这次GC后,软引用的get()
方法也无法返回MyObject
对象实例,说明,它已经被回收,此时该软引用会加入到注册的引用队列(通过构造函数注册)。
在新线程中将该软引用取出,同时调用get
方法验证此时却是无法获取MyObject
实例了。
构造和测试弱引用的代码如下:
MyObject obj = new MyObject();
ReferenceQueue<MyObject> weakQueue = new ReferenceQueue<>();
WeakReference<MyObject> weakRef = new WeakReference<>(obj,weakQueue);
new CheckRefQueueThread(weakQueue).start();
obj = null;
System.out.println("Before GC:Weak Get=" + weakRef.get());
System.gc();
System.out.println("After GC:Weak Get=" + weakRef.get());
可以看出,和软引用的构造方法类似,只是名称不同。
第5行删除强引用,此时不存在强引用和软引用,存在弱引用指向它,称为弱可达(weakly reachable )。
在系统GC时,只要发现该对象仅是弱可达的,不管内存是否充足,都会对对象进行回收。但是,由于GC线程优先级较低,可能不会那么快的发现,可能也会存在较长时间。
上面代码的执行结果如下:
Before GC:Weak Get=MyObject@723279cf
MyObject's finalize called.
After GC:Weak Get=null
Object for WeakReference is null
从结果来看,在GC前,可以通过weakRef.get()
取得对应的强引用。但是只要进行垃圾回收,并且发现弱引用的对象(这里是MyObject
)是弱可达,便会立即被回收,并且weakRef
会加入到引用队列中。
此时,再次通过get
方法获取对象会失败。
弱引用的典型应用就是WeakHashMap
,下面我们一起简单探讨下它的使用方法及原理。
它使用弱引用作为内部数据的存储方案。
Map map;
map = new WeakHashMap<>();//new HashMap<>();
for (int i = 0; i < 10000;i++) {
Integer ii = new Integer(i);//注意这个ii,它每次循环都指向一个Integer对象,下次循环后之前指向的对象就没有强引用指向它了,类似于ii = null;而在作用域之外,也是类似的
map.put(ii,new byte[i]);
}
使用-Xmx5M
限定最大可用堆后,执行WeakHashMap
的代码正常运行,使用HashMap
的代码抛出java.lang.OutOfMemoryError: Java heap space
。
WeakHashMap
是如何工作的呢,在其源码中对Entry
的定义如下:
/**
* 继承了WeakReference, 使用referent封装了key
*/
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
/**
* Creates new entry.
*/
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue);//以key为referent构造了key的弱引用
this.value = value;
this.hash = hash;
this.next = next;
}
...
}
在上面的代码中似乎看不到K key
了,其实它变成了WeakReference
中的referent
了,理解这一点很重要。
在WeakHashMap
的各项操作中,如get()
、put()
函数,都直接或间接调用
了expungeStaleEntries()
方法(方法名意味清理不新鲜的项),以清理持有弱引用的key
的表项。
private void expungeStaleEntries() {
//queue中都是弱可达对象的弱引用,表示这些对象可以移除了
//注意此处使用的是poll()方法,不会阻塞,如果队列中有值直接返回队顶元素;否则返回null
//若是remove()方法,在队列中无值的情况下会阻塞
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);//找到这个项的索引
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {//找到了
if (prev == e)//说明是链表头节点
table[i] = next;
else
prev.next = next;//让pre指向它的next,就没有引用指向它了
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC ,key已经不新鲜了,value也没什么用了
size--;
break;
}
prev = p;
p = next;
}
}
}
}
简单了解下WeakHashMap
的工作原理后,可以知道,如果存放WeakHashMap
中的
key
都存在强引用,那么WeakHashMap
就会退化为HashMap
:
Map<Integer, byte[]> map;
map = new WeakHashMap<>();//new HashMap<>();
List<Integer> list = Lists.newArrayList();
for (int i = 0; i < 10000; i++) {
Integer ii = new Integer(i);
list.add(ii);//强引用key
map.put(ii, new byte[i]);
}
上面的代码也会抛出内存不足异常。如果希望在系统中通过WeakHashMap
自动清理数据,
就尽量不要在代码的其他地方强引用WeakHashMap
的key
,否则这些key
就不会被回收。
虚引用是所有引用中最弱的一个。它是finalize()
方法的一个更加灵活的代替版本。
MyObject obj = new MyObject();
ReferenceQueue<MyObject> phantomQueue = new ReferenceQueue<>();
PhantomReference<MyObject> phantomRef = new PhantomReference<>(obj,phantomQueue);
System.out.println("Phantom Get: " + phantomRef.get());
new CheckRefQueueThread(phantomQueue).start();
obj = null;
Thread.sleep(1000);
int i = 1;
while (true) {
System.out.println("第" + i++ + "次gc");
System.gc();
Thread.sleep(1000);
}
本例中需要修改下检查引用线程的代码,在run()
方法体最后加入:
ref.clear();//虚引用需要手动调用clear方法
System.exit(0);
软引用和弱引用其实可以在构造时将引用队列置为null
,但是虚引用不同,没有注册引用队列的虚引用是没有意义的。
如果一个对象只存在虚引用指向它,那它就是虚可达(phantom reachable)。如果垃圾收集器发现虚引用的对象是虚可达的,
那么该虚引用对象会被加到引用队列。
上例中执行结果如下:
Phantom Get: null
第1次gc
MyObject's finalize called.
第2次gc
Object for PhantomReference is null
在虚引用中调用get()
方法总是返回null
,一个对象只有虚引用指向它,几乎和没有引用指向它是一样的。
在第一次GC时,系统找到了垃圾对象,并调用其finalize()
方法回收内存,但是没有立即加入回收队列。第二次GC时,该对象真正被GC清除,此时,加入虚引用队列。
当JVM真正回收MyObject
时,将虚引用放入引用队列。一旦从虚引用队列中取得该虚引用,表明虚引用的对象已经被回收。此时可以做一些清理工作,清理啥呢,清理GC无法清理的资源,比如文件句柄。
比如,在FileInputStream
中重写了finalize
方法:
protected void finalize() throws IOException {
if ((fd != null) && (fd != FileDescriptor.in)) {
close();
}
}
最后会调用close()
方法,所以如果你忘记调用的话,在GC回收FileInputStream
对象时会很贴心的帮你关闭文件句柄,
但是如果一直没有GC的话,那么应该关闭的文件就会一直被打开,浪费系统资源。
虽然虚引用可以用于进行清理工作,但是一般情况下还是建议直接使用try-with-resource
语法及时释放不要的资源。