我们都知道在java世界中,对象的引用有四种方式。当然,工作中,我们一般都只使用一种,也就是强引用。因为我们一般设置运行时的内存足够大,只要及时的释放对象,GC自动回收不再使用的对象内存,一般情况下,是不会内存溢出的。
一般我们是怎么释放对象的呢?就是直接将对象设置为null。那么我们先看下如下这个示例:
// -Xmx200m -Xms200m -XX:+PrintGCDetails
@Test
public void test() {
byte[] key = new byte[100 * 1024 * 1024]; // 100M大小的key
Object val = new Object();
Map map = new HashMap();
// 将key和value设置到map中
map.put(key, val);
// 主动触发第一次GC
System.gc();
System.out.println("---------------");
// 好玩的地方在这里,我们主动将key设置为null,后续再主动触发一次GC
key = null;
System.gc();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
执行以上代码时,我们需要先设置下JVM参数,笔者设置如下
-Xmx200m -Xms200m -XX:+PrintGCDetails
不看执行结果之前,我们先来猜一下两次GC的结果,第一次GC的话,被设置的100M的key肯定是不会被回收的,因为强引用还在;
那么第二次呢?我们主动将key设置为null之后呢,我们会觉得GC会将这100M的key内存回收掉。
来看下实际结果:
[GC (System.gc()) [PSYoungGen: 11382K->1704K(59904K)] 113782K->104112K(196608K), 0.0022167 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 1704K->0K(59904K)] [ParOldGen: 102408K->103975K(136704K)] 104112K->103975K(196608K), [Metaspace: 5245K->5245K(1056768K)], 0.0117054 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
---------------
[GC (System.gc()) [PSYoungGen: 1034K->32K(59904K)] 105009K->104007K(196608K), 0.0012404 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 32K->0K(59904K)] [ParOldGen: 103975K->103525K(136704K)] 104007K->103525K(196608K), [Metaspace: 5248K->5248K(1056768K)], 0.0102300 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
Heap
PSYoungGen total 59904K, used 4137K [0x00000000fbd80000, 0x0000000100000000, 0x0000000100000000)
eden space 51712K, 8% used [0x00000000fbd80000,0x00000000fc18a420,0x00000000ff000000)
from space 8192K, 0% used [0x00000000ff800000,0x00000000ff800000,0x0000000100000000)
to space 8192K, 0% used [0x00000000ff000000,0x00000000ff000000,0x00000000ff800000)
ParOldGen total 136704K, used 103525K [0x00000000f3800000, 0x00000000fbd80000, 0x00000000fbd80000)
object space 136704K, 75% used [0x00000000f3800000,0x00000000f9d197d8,0x00000000fbd80000)
Metaspace used 5511K, capacity 5696K, committed 5888K, reserved 1056768K
class space used 632K, capacity 659K, committed 768K, reserved 1048576K
ParOldGen依旧是75%的占用,那100M的key没有被回收掉。
为什么呢?因为HashMap对着100M对象的强引用依然在。
注意:笔者之前会陷入一个误区,本例中key只是对这100M内存对象的一个强引用而已,后续将key=null,只是将key不再指向这100M对象而已,但是这个大对象还是存在的。
那么该如何回收这100M内存呢?很简单,将Map对其的引用也删除,如下所示:
// 原来的方式
key = null;
// 现在的方式
map.remove(key);
key = null;
这样,结果就很明朗了,GC结果如下:
[GC (System.gc()) [PSYoungGen: 11382K->1688K(59904K)] 113782K->104096K(196608K), 0.0035644 secs] [Times: user=0.05 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 1688K->0K(59904K)] [ParOldGen: 102408K->103975K(136704K)] 104096K->103975K(196608K), [Metaspace: 5249K->5249K(1056768K)], 0.0112903 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
---------------
[GC (System.gc()) [PSYoungGen: 1034K->32K(59904K)] 105009K->104007K(196608K), 0.0012414 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 32K->0K(59904K)] [ParOldGen: 103975K->1574K(136704K)] 104007K->1574K(196608K), [Metaspace: 5250K->5250K(1056768K)], 0.0082791 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 59904K, used 3102K [0x00000000fbd80000, 0x0000000100000000, 0x0000000100000000)
eden space 51712K, 6% used [0x00000000fbd80000,0x00000000fc087b28,0x00000000ff000000)
from space 8192K, 0% used [0x00000000ff800000,0x00000000ff800000,0x0000000100000000)
to space 8192K, 0% used [0x00000000ff000000,0x00000000ff000000,0x00000000ff800000)
ParOldGen total 136704K, used 1574K [0x00000000f3800000, 0x00000000fbd80000, 0x00000000fbd80000)
object space 136704K, 1% used [0x00000000f3800000,0x00000000f3989ab8,0x00000000fbd80000)
Metaspace used 5511K, capacity 5696K, committed 5888K, reserved 1056768K
class space used 632K, capacity 659K, committed 768K, reserved 1048576K
ParOldGen 只有1% used,彻底删除了100M大对象的内存
从这个示例中,我们看到了GC回收的困难之处,只有还有一个强引用没被删除,对象就不会被回收。
实际上除了强引用,还有其他几种对象引用方式,在不同的引用方式下,GC对其处理方式也是不同的。
1.强引用强引用,是我们使用最多的一个引用方式,方式很简单,就是将一个引用直接执行一个对象。
byte[] key = new byte[100 * 1024 * 1024];
Object val = new Object();
这里的key和val都是对byte[]和Object对象的强引用。
如果一个对象具有强引用,那么在内存回收时,GC是绝不会回收这块内存的,当内存不足时,其会抛出OOM的异常,也不会回收这块内存。
2.软引用相对强引用而言,软引用主要用来引用一些有用但不是必须的对象。软引用所关联的对象,只有当内存不足时,才会考虑将这些被软引用所引用的对象回收掉。
2.1 软引用示例//100M的缓存数据
byte[] cacheData = new byte[100 * 1024 * 1024];
//将缓存数据用软引用持有
SoftReference cacheRef = new SoftReference(cacheData);
这里的100M的数组对象,存在两个引用,一个是cacheData的强引用,另一个是cacheRef的软引用。
当然,我们也可以直接创建一个唯一的软引用对象,如下所示:
SoftReference cacheRef = new SoftReference(new byte[100 * 1024 * 1024]);
2.2 软引用GC示例
// -Xmx200m -Xms200m
@Test
public void test2() throws Exception {
//100M的缓存数据
byte[] cacheData = new byte[100 * 1024 * 1024];
//将缓存数据用软引用持有
SoftReference cacheRef = new SoftReference(cacheData);
//将缓存数据的强引用去除
cacheData = null;
System.out.println("第一次GC前" + cacheData);
System.out.println("第一次GC前" + cacheRef.get());
//进行一次GC后查看对象的回收情况
System.gc();
//等待GC
Thread.sleep(500);
System.out.println("第一次GC后" + cacheData);
System.out.println("第一次GC后" + cacheRef.get());
//在分配一个120M的对象,看看缓存对象的回收情况
byte[] newData = new byte[120 * 1024 * 1024];
System.out.println("分配后" + cacheData);
System.out.println("分配后" + cacheRef.get());
}
// res:
第一次GC前null
第一次GC前[B@6996db8
第一次GC后null
第一次GC后[B@6996db8
分配后null
分配后null
进行这个测试的先决条件是,我们需要对最大最小内存先进行设置,设置为200M
我们直接对结果进行分析:
1)第一次GC后,byte[]还存在一个cacheRef的弱引用,此时200M的内存只占用了100M+,内存还是够用的,所以第一次GC时,byte[]仍旧在,还是可以通过cacheRef.get()获取到数组
2)在重新分配一个120M的byte[]时,此时200M的内存已经不够用了,所以,GC会主动触发一次,会将cacheRef这个弱引用所关联的byte[]内存清除掉,此时再通过cacheRef.get()就获取不到数组信息了
2.2.1 重设内存条件,再次弱引用测试
刚才设置是200M的堆内存,现在我们设置成500M的堆内存,重新测试该代码,会发现结果有所不同
第一次GC前null
第一次GC前[B@6996db8
第一次GC后null
第一次GC后[B@6996db8
分配后null
分配后[B@6996db8
结果的不同之处在于:在分配120M的数组时,cacheRef所关联的100M的byte[]数据依旧没有被清除
总结:该结果直接认证了我们对弱引用的介绍:软引用所关联的对象,只有当内存不足时,才会考虑将这些被软引用所引用的对象回收掉。
2.3 软引用适用场景针对软引用这种内存充足时就不清除,只有当内存不足时才进行回收的对象引用方式,就特别适合做数据的缓存。
3.弱引用相对于软引用而言,它对对象的引用强度更弱。在只有弱引用来引用的对象,在GC时,无论内存是否够用,都会将该对象回收。
3.1 弱引用示例//100M的缓存数据
byte[] cacheData = new byte[100 * 1024 * 1024];
//将缓存数据用弱引用持有
WeakReference cacheRef = new WeakReference(cacheData);
这里的100M的数组对象,存在两个引用,一个是cacheData的强引用,另一个是cacheRef的弱引用。
当然,我们也可以直接创建一个唯一的弱引用对象,如下所示:
WeakReference cacheRef = new WeakReference(new byte[100 * 1024 * 1024]);
3.2 弱引用GC示例
// -Xmx200m -Xms200m
@Test
public void test3() throws Exception {
//100M的缓存数据
byte[] cacheData = new byte[100 * 1024 * 1024];
//将缓存数据用弱引用持有
WeakReference cacheRef = new WeakReference(cacheData);
System.out.println("第一次GC前" + cacheData);
System.out.println("第一次GC前" + cacheRef.get());
//将缓存数据的强引用去除
cacheData = null;
System.gc();
//等待GC
Thread.sleep(500);
System.out.println("第一次GC后" + cacheData);
System.out.println("第一次GC后" + cacheRef.get());
}
// res
第一次GC前[B@6996db8
第一次GC前[B@6996db8
第一次GC后null
第一次GC后null
结果很明确,无论内存够不够,只要发生了GC,弱引用所引用的对象都会被回收
3.3 弱引用适用场景弱引用这种有什么适用场景呢?回到我们在前言中的那个示例,我们来对其改造下
// -Xmx200m -Xms200m
@Test
public void test() {
byte[] key = new byte[100 * 1024 * 1024];
Object val = new Object();
Map map = new HashMap();
// 在这里改造下,使用WeakReference作为map的key
WeakReference weakRef = new WeakReference(key);
map.put(weakRef, val);
System.gc();
System.out.println("---------------");
key = null;
System.gc();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// res
[GC (System.gc()) [PSYoungGen: 11382K->1704K(59904K)] 113782K->104112K(196608K), 0.0052081 secs] [Times: user=0.00 sys=0.03, real=0.02 secs]
[Full GC (System.gc()) [PSYoungGen: 1704K->0K(59904K)] [ParOldGen: 102408K->103975K(136704K)] 104112K->103975K(196608K), [Metaspace: 5245K->5245K(1056768K)], 0.0113374 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
---------------
[GC (System.gc()) [PSYoungGen: 1034K->32K(59904K)] 105009K->104007K(196608K), 0.0016759 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
[Full GC (System.gc()) [PSYoungGen: 32K->0K(59904K)] [ParOldGen: 103975K->1541K(136704K)] 104007K->1541K(196608K), [Metaspace: 5246K->5246K(1056768K)], 0.0079988 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 59904K, used 3102K [0x00000000fbd80000, 0x0000000100000000, 0x0000000100000000)
eden space 51712K, 6% used [0x00000000fbd80000,0x00000000fc087b28,0x00000000ff000000)
from space 8192K, 0% used [0x00000000ff800000,0x00000000ff800000,0x0000000100000000)
to space 8192K, 0% used [0x00000000ff000000,0x00000000ff000000,0x00000000ff800000)
ParOldGen total 136704K, used 1541K [0x00000000f3800000, 0x00000000fbd80000, 0x00000000fbd80000)
object space 136704K, 1% used [0x00000000f3800000,0x00000000f3981448,0x00000000fbd80000)
Metaspace used 5512K, capacity 5696K, committed 5888K, reserved 1056768K
class space used 632K, capacity 659K, committed 768K, reserved 1048576K
相对于前言中的测试结果,这里我们看到,ParOldGen只有1% used,那100M的byte[]被清除掉了。
总结:针对Map的这种对key的依赖,如果我们想在key的强依赖被删除时,就释放其所对应的内存时,那么我们就可以使用WeakReference来包装这个byte[],这样当发生GC时,这个byte[]就被内存回收掉。
实际这个也就是WeakHashMap做的事情,具体源码笔者不再分析。
除了这个还有哪些呢?可以参考笔者另一篇博客:关于ThreadLocal的。
4.虚引用虚引用是最后一种引用方式,也是最弱的一种引用方式。
我们无法通过虚引用来获取其所引用的对象信息。如果一个对象仅有虚引用指向它,那么它就和没有任何引用一样,在任何时候它都有可能被GC回收掉。
4.1 虚引用作用既然这么弱,那么虚引用还有什么作用呢?
它的主要作用就是:当GC准备回收一个对象时,如果发现它还有虚引用,就会在回收对象内存前,把这个虚引用加入到与之关联的引用队列中。程序通过判断引用队列是否已经加入了虚引用,来了解与之关联的对象是否要被垃圾回收。
4.2 虚引用使用示例static class TestClass {
private String name;
public TestClass(String name) {
this.name = name;
}
@Override
public String toString() {
return "TestClass - " + name;
}
}
// -Xmx10m -Xms10m -XX:+PrintGCDetails
@Test
public void test() {
final List TEST_DATA = new LinkedList();
final ReferenceQueue QUEUE = new ReferenceQueue();
TestClass obj = new TestClass("Test");
final PhantomReference phantomReference = new PhantomReference(obj, QUEUE);
// 该线程不断读取这个虚引用,并不断往列表里插入数据,以促使系统早点进行GC
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
TEST_DATA.add(new byte[1024 * 1024]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
System.out.println(phantomReference.get());
}
}
}).start();
// 这个线程不断读取引用队列,当弱引用指向的对象被回收时,该引用就会被加入到引用队列中
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
Reference
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?