软引用
好,首先我们来看一下这里,给大家写了一个testSoftReference。
/**
* 软引用特性:内存不足的时候,才会得到释放
* 内存马上要报OOM的时候,才会进行释放
*/
public void testSoftReference(){
User user = new User(1, "Andy");
//user对象保存在软引用里面
SoftReference<User> userSoft = new SoftReference<User>(user);
user = null;//干掉强引用
System.out.println(userSoft.get());//取user对象
System.gc();//调用gc回收内存
System.out.println("After gc");
System.out.println(userSoft.get());
//向堆中填充数据,导致OOM
List<byte[]> list = new LinkedList<>();
try{
for (int i=0; i<100; i++){
System.out.println("for====="+userSoft.get());
list.add(new byte[1024*1024*1]);//添加1M(兆)的byte数组
}
}catch (Throwable e){
System.out.println("Exception====="+userSoft.get());
}
}
我们来测试一下这个软引用,这里我先造了一个对象,那么这个对象呢,非常非常简单。
有它的 id、name,还有一个构造方法。
好,那么通过这个构造方法,我们生产了一个 User 对象:id为1,name为Andy 。
同时在这里我们去new 一个SoftReference。那么new出来以后呢,
这个强引用我就暂时把它给干掉,我就不要了。
好了,我们的 User 对象是保存在这个软引用里面的。
那么现在我去取,那么正常来说呢,它是可以取得到的。
那么软引用它本身的一个特性,是在内存不足的时候,才会得到一个释放。
所以正常来说呢,它相对于我们的强引用来说,软引用它会有在内存马上要报OOM的时候,它会进行一个释放,不像强引用。
强引用它只要引用类没有断开,它就不会释放。而我们的软引用呢,它的层次稍微低一点点。
好,那么在这里的打印正常会得到一个 User 对象。然后我就调用了一次 gc。
那么 gc 来回收我的内存,当它回收完以后,我再去打印再去打印这个对象。那么正常来说呢,它是可以得到的,得到这个对象的。好,当然我把下面这些代码我先注释起来,先注释起来。
//向堆中填充数据,导致OOM
// List<byte[]> list = new LinkedList<>();
// try{
// for (int i=0; i<100; i++){
// System.out.println("for====="+userSoft.get());
// list.add(new byte[1024*1024*1]);//添加1M(兆)的byte数组
// }
// }catch (Throwable e){
// System.out.println("Exception====="+userSoft.get());
// }
好,我们运行,先看一下下面这个结果。
[GC (Allocation Failure) 2037K->752K(9728K), 0.0012723 secs][GC (Allocation Failure) 2800K->1010K(9728K), 0.0013523 secs]
[GC (Allocation Failure) 3058K->1243K(9728K), 0.0008724 secs][GC (Allocation Failure) 3291K->1482K(9728K), 0.0010526 secs]
com.yyh.example.myapplication.User@15327b79
[GC (System.gc()) 1721K->1530K(9728K), 0.0031037 secs]
[Full GC (System.gc()) 1530K->1184K(9728K), 0.0120377 secs]
After gc
com.yyh.example.myapplication.User@15327b79
Process finished with exit code 0
好,那么通过下面这个结果,通过这个结果可以反映出一个问题。也就是说我这个对象它并不会受到 gc 回收的影响。
好,希望大家把这一点给记住了。那么我们的软引用不会受到 gc 回收的影响,它是正常运行的。
那么它什么情况下会回收软引用呢?
好,接下来我把注释打开,也就向堆里面。填充大量的数据,这里我建一个list,然后我循环一百次,然后我就每次向这个list 里面呢,添加一兆大小的一个 bete 数组。然后每次我都会去,在循环中进行一个打印。打印的过程中,如果发生了内存溢出的问题,那么我就直接就抛一个异常了。
//向堆中填充数据,导致OOM
List<byte[]> list = new LinkedList<>();
try{
for (int i=0; i<100; i++){
System.out.println("for====="+userSoft.get());
list.add(new byte[1024*1024*1]);//添加1M(兆)的byte数组
}
}catch (Throwable e){
System.out.println("Exception====="+userSoft.get());
}
当然这个程序在执行过程中,我们现在这样子测试,是看不到效果的,我们可以看一看,他会给我打印一百个出来,对吧!
当然大家,在你们的机器上可能会看不到我这个效果。输出结果:
[GC (Allocation Failure) 2037K->728K(9728K), 0.0084932 secs][GC (Allocation Failure) 2776K->1010K(9728K), 0.0165672 secs]
[GC (Allocation Failure) 3058K->1250K(9728K), 0.0018261 secs][GC (Allocation Failure) 3298K->1433K(9728K), 0.0363407 secs]
com.yyh.example.myapplication.User@15327b79
[GC (System.gc()) 1673K->1457K(9728K), 0.0119827 secs]
[Full GC (System.gc()) 1457K->1079K(9728K), 0.0136774 secs]
After gc
com.yyh.example.myapplication.User@15327b79
for=====com.yyh.example.myapplication.User@15327b79
for=====com.yyh.example.myapplication.User@15327b79
for=====com.yyh.example.myapplication.User@15327b79
for=====com.yyh.example.myapplication.User@15327b79
for=====com.yyh.example.myapplication.User@15327b79
for=====com.yyh.example.myapplication.User@15327b79
for=====com.yyh.example.myapplication.User@15327b79
[GC (Allocation Failure) -- 7263K->7263K(9728K), 0.0013182 secs]
[Full GC (Ergonomics) 7263K->7224K(9728K), 0.0206816 secs]
[GC (Allocation Failure) -- 7224K->7224K(9728K), 0.0148380 secs]
[Full GC (Allocation Failure) 7224K->7167K(9728K), 0.0444590 secs]
for=====null
[Full GC (Ergonomics) 8231K->8192K(9728K), 0.0133925 secs]
[Full GC (Allocation Failure) 8192K->8192K(9728K), 0.0274480 secs]
Exception=====null
Process finished with exit code 0
那么这个地方我们需要去调整一下虚拟机的配置。大家可以选择这个 Edit Configurations,我们选到这个里面。
在这里虚拟机正常来说后面这一排参数是没有的。那么我在这里,把对内存和对内存的限制,我把它设置成了十兆。
同时呢 gc 的信息打印,把它添加进来了,所以大家可以把这一个配置加进来以后,再正常来执行这一个代码。
修改虚拟机配置:
-ea -Xms10m -Xmx10m -XX:+PrintGC
-Xms10m -Xmx10m 表示堆内存和堆内存的限制设置为10M
-XX:+PrintGC 表示添加GC信息的打印
那我们根据刚刚的执行结果,我们就可以看得出来,在for 循环的过程中,那么前期呢,这个堆内存只有十兆大小。
那我每次开 一兆 对吧,那么它就开了几个以后呢。前期没有发生任何问题。
好,当内存比较紧张的时候,那么这个时候我们的 Full GC 等, 这个垃圾回收器,就开始回收内存了。
好,回收内存的过程中,当我们内存不足的时候,大家可以看到这里最后一次循环的时候,也就是说实际上是在抛异常之前的最后一次循环。那么因为我们打印的位置是在,这个出问题之前对吧?在出问题之前,所以最后一次呢,他在这里是取了一个为空的情况。
也就是说,当我们添加一次,添加到内存溢出的时候,那么下一次循环。这个地方是已经接近内存溢出的时候,他会去释放掉这一个空间。释放掉空间以后,然后出现内存溢出了,接下来就开始打印出我们的异常信息。
刚刚这个过程,相信大家应该是比较容易明白的,对吧?也就是说呢,最后一次到这个位置打印的时候。
内存已经非常紧张了,但是还没有发生异常。内存比较紧张的时候,那么我们看这个结果。
当 gc 来回收内存,比较紧张的时候,那么它就已经开始释放掉我们的 SoftReference 里面的对象了啦。
这个对象释放完以后,那么后面呢,他还是继续在回收内存。那么直到这个异常抛出来,那当然这个对象肯定也已经早就不存在了。
所以这个就是我们对于软引用的一个测试。那么经过这个测试,我们可以得到一个结论:我们的软引用它是在内存不足的时候进行一个回收的,是内存不足的时候进行回收。
弱引用
当然接下来我们来看一看弱引用,那么当然大家看到这个总结是 gc 到来的时候就进行,一个回收了。
/**
* 弱引用
*/
public void testWeakReference(){
User user = new User(1, "Andy");
WeakReference<User> userWeakReference = new WeakReference<User>(user);
user = null;
System.out.println(userWeakReference.get());//取user对象
System.gc();//调用gc回收内存
System.out.println("After gc");
System.out.println(userWeakReference.get());
}
我们看看弱引用是怎么回收的,还是同样的我造一个对象,然后我把这个对象放到我们的弱引用里面,然后把这个原来的强引用把它干掉了。干掉以后,第一是我的打印。那么这里肯定是有数据的。接下来我把 gc 叫过来了,那么 gc 来了之后呢,我就再打印一次。
我们看看这个打印结果。
com.yyh.example.myapplication.User@15327b79
After gc
null
在这个打印结果里面,我们会发现 gc 没来之前,这个对象是存在的。gc 来了之后它就不存在了。
所以通过这个案例,我们也可以得到一个结论:弱引用是在 gc 到来的时候进行一个回收的。
那么这个是我们平时用的软引用和弱引用。
那么软引用呢,其实在我们的开发中,一般是用在做缓存。你比方说。
我们一个程序用来处理用户提供的图片。如果说我要把所有的图片读到内存,那么这么做的话,内存的消耗消耗它是非常大的。
虽然说我打开图片很快,但是内存空间使用巨大,一些使用较少的图片呢,它会浪费掉我的内存空间,这是第一种情况。
那么还有一种情况呢,如果说我每次打开图片都从磁盘中进行一个读取。
那么这样的话,我们虽然说内存占用比较少,但一些经常使用的图片,每次打开都要访问磁盘,那么它的代价是非常非常大的。
所以这个时候我们就可以用软引用来构建一块内存。而和软引用构建的内存,它的特点是内存不足时,就会把你这一块内存给进行一个回收了。
我们使用弱引用,往往它都是用来。用在一些内存资源紧张,不能说是内存紧张,应该说是需要去使用。
但是呢这些对象并不是一个必须的东西。也就是说这些对象回收的时候,对我们 app 并不会造成影响。
像这一类的对象,我们就可以考虑用弱引用来进行一个管理的,你比方说我们有时候在处理一些内存泄露问题的时候,
像一个 activity 的内存泄露,就会经常去使用弱引用这种方式。那么我相信有很多同学也已经开始用弱引用的方式去处理。
处理一些不是非常非常重要。但是呢,我们还是在程序中需要用到的一些引用的情况的。那么比较典型的就是 activity 内存泄露这一块的。
虚引用
好,那么我们刚刚讲解的软引用和弱引用。那么还有大家可能平时经常会听到一种叫做虚引用。
那么虚引用呢,在实际开发中,它其实还有一个名字也可以叫做幽灵引用。但是实际上我们并没什么太多用处。
如果你不做虚拟机级别的开发,我们平时 app 开发中没有太多用处。他主要是帮我们可以得到一个 gc 回收的通知,那怎么得到呢?我们还是回到代码中来看这个通知是怎么拿到的。
/**
* 虚引用
* @throws Exception
*/
public void testPhantomReference()throws Exception{
//虚引用:功能,不会影响到对象的生命周期的,
//但是能让程序员知道该对象什么时候被 回收了
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();
Object phantomObject = new Object();
PhantomReference phantomReference
= new PhantomReference(phantomObject,referenceQueue);
phantomObject = null;
System.out.println("phantomObject:"+phantomObject);//null
System.out.println("phantomReference:"+referenceQueue.poll());//null
System.gc();//调用gc回收内存
Thread.sleep(2000);
System.out.println("referenceQueue:"+referenceQueue.poll());
}
在这里我先造了一个引用队列,大家注意虚引用是不会单独使用的。我们在构造虚引用的时候呢,需要把我们的这个对象,这就是一个普通的对象。我这里就没有去用 User 对象了,我就造了一个虚引用的对象和一个引用队列关联起来。
那么这个虚引用呢,其实它是存放到这个引用队列里面的,但是我们在使用的过程中。
这里我写了两个打印,一个是这个对象,这个对象我把它设为空,输出肯定是等于空的。
那么我就从这个队列里面去弹一个出来,从队列里面去拿一个。那么正常来说,如果是我们普通的引用,放到我们引用队列是拿出来以后是有数据的,但是虚引用放进去以后。
你去拿出来的时候呢,它是不会有问题的,它是不会有数据的,它会等于一个空。但是呢我们 gc 来了以后,大家注意。
只要 gc 一来,那么这个队列中,他就会对这个虚引用回收的信息把它绑定进去。
phantomObject:null
phantomReference:null
referenceQueue:java.lang.ref.PhantomReference@15327b79
所以我们这里可以看得到,前两次打印为空! 第三次打印,大家会发现这个 虚引用 的对象,他的 hashcode 的已经有了,对不对?那么他的 hashcode 已经有了,就证明这个对象存在。但是事实上这个对象你是永远不可能从这个队列中,取出来进行任何使用的。
所以它唯一的功能,仅仅是 gc 在回收某个对象的时候,我们可以得到这个对象被回收的一个通知消息。
这样的话我就可以监听到 gc 回收,你说定位的这个对象是什么时间呢,进行一个回收的?
这就是我们对于:强引用、软引用、弱引用以及虚引用java 中的这四种引用的一个说明。
所以我们平时在面试的过程中呢,如果有提到这些问题的时候,那么大家面试的过程中,你未必能够写代码。
所以我们只要能够正常的讲出这四种引用它的回收时机以及它的作用。也就ok 了!好,ok 那么本节内容呢就到这里了。