引言 Java虚拟机会为我们管理内存,当内存不足时,通过垃圾回收算法来释放不可达的内存。作为Java程序员我们似乎不需要关注这些。
但是在工作中我们可能会遇到内存充足的情况下,也会出现OutOfMemoryError
。
笔者就多次遇到过这种情况,有一次是加载内容过多引起的,通过本文的切入点是引用类型,下面开始进入主题。Java中有四种引用类型:强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference)和虚引用(Phantom Reference)。Xmx2g
,在分配了2G内存的情况下出现了内存溢出的问题,最后定位到了是压测的时候压测导出Excel接口没有分页导致一次性读取整张表的数据到内存,同时查询数据量过多时间过长,上一个还没导出成功下一个导出请求又来了。
解决方案是控制一次导出的数据量。在此期间执行过Full GC,但是并不会回收加载到内存中的待导出数据,因为它们都是强引用。
强度由强到弱。
软引用和弱引用哪个更强呢?是不是容易混淆。可以通过单词来记忆,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 softQueue = new ReferenceQueue<>();
//创建引用队列,如果对象被回收则进入引用队列
SoftReference 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 softQueue = new ReferenceQueue<>();
//创建引用队列,如果对象被回收则进入引用队列SoftReference 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 queue;
private CheckRefQueueThread(ReferenceQueue 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
对象实例,说明,它已经被回收,此时该软引用会加入到注册的引用队列(通过构造函数注册)。【一文弄懂Java中的四种引用类型】在新线程中将该软引用取出,同时调用
get
方法验证此时却是无法获取MyObject
实例了。弱引用 构造和测试弱引用的代码如下:
MyObject obj = new MyObject();
ReferenceQueue weakQueue = new ReferenceQueue<>();
WeakReference 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
,下面我们一起简单探讨下它的使用方法及原理。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 extends WeakReference
在上面的代码中似乎看不到
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 e = (Entry) x;
int i = indexFor(e.hash, table.length);
//找到这个项的索引Entry prev = table[i];
Entry p = prev;
while (p != null) {
Entry 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 = https://www.it610.com/article/null;
// Help GC,key已经不新鲜了,value也没什么用了
size--;
break;
}
prev = p;
p = next;
}
}
}
}
简单了解下
WeakHashMap
的工作原理后,可以知道,如果存放WeakHashMap
中的key
都存在强引用,那么WeakHashMap
就会退化为HashMap
:Map map;
map = new WeakHashMap<>();
//new HashMap<>();
List 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 phantomQueue = new ReferenceQueue<>();
PhantomReference 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
语法及时释放不要的资源。推荐阅读
- Java|Java基础——数组
- 人工智能|干货!人体姿态估计与运动预测
- java简介|Java是什么(Java能用来干什么?)
- Java|规范的打印日志
- Linux|109 个实用 shell 脚本
- 程序员|【高级Java架构师系统学习】毕业一年萌新的Java大厂面经,最新整理
- Spring注解驱动第十讲--@Autowired使用
- SqlServer|sql server的UPDLOCK、HOLDLOCK试验
- jvm|【JVM】JVM08(java内存模型解析[JMM])
- 技术|为参加2021年蓝桥杯Java软件开发大学B组细心整理常见基础知识、搜索和常用算法解析例题(持续更新...)