1. 前言
Java中的引用有强引用
、软引用
、弱引用
、虚引用
四种引用类型。任何一种引用最终都会被垃圾回收机制(GC)回收,也就是说,当一个对象永久的失去引用后,就会变成垃圾,等待系统垃圾回收机制进行回收。
先来了解一下GC是怎样工作的:
- 垃圾回收机制只会回收堆内存中的对象,不会回收物理资源;
- 垃圾回收机制是由系统控制的,每隔固定的时间就执行一次释放操作,当一个对象永久的失去引用后,会由可达状态变为可恢复状态;
- 系统在进行垃圾回收之前,会调用finalize()方法,这个方法可能会使对象被重新引用,变为可达状态,此时垃圾回收就会取消;
- 当调用了所有的finalize()方法后,垃圾回收还是没有取消,该对象就会由可恢复状态转变为不可达状态,最终被系统作为垃圾回收掉该对象所占用的资源。
上面的过程中提到了三种状态,下面我们先来解释一下:
- 可达性:当一个对象被创建后(即被new出来之后),只要有一个以上的引用变量引用它,就是可达状态;
- 可恢复性:当一个对象失去所有引用后,就进入了可恢复状态;
- 不可达性:也就是当调用所有finalize()方法之后。处于不可达的状态,就会等待系统随时回收掉其所占有的资源。
2. 四种引用类型
2.1 强引用
-
定义:强引用一般就是指被new出来的对象,如:(Object obj = new Object()),这是Java中最普遍的。
只要强引用存在,垃圾回收器就不会回收被引用的对象
。 -
测试
1)对finalize()方法进行重写,目的是为了方便查看结果;
@Slf4j public class Student { @Override protected void finalize() throws Throwable { log.info("调用finalize()方法"); super.finalize(); } }
2)编写测试类
@Slf4j public class Test { public static void main(String[] args) throws IOException { Student student = new Student(); System.gc(); log.info(student.toString()); System.in.read();//阻塞main线程,给垃圾回收线程时间执行 } } /** * [main] INFO com.glw.thread.referenceType.强引用.Test - com.glw.thread.referenceType.强引用.Student@46fbb2c1 */
从以上的结果我们看到Student对象并没有被回收。那么如何使强引用类型被回收呢???答案是只要将对象设为null即可。
3)测试强引用类型被垃圾回收器回收
public class Test { public static void main(String[] args) throws IOException { Student student = new Student(); student = null; System.gc(); System.out.println("student:" + student); System.in.read();//阻塞main线程,给垃圾回收线程时间执行 } } /** * student:null * [Finalizer] INFO com.glw.thread.referenceType.强引用.Student - 调用finalize()方法 */
2.2 软引用
-
定义:软引用用来描述一些非必需但仍有用的对象。
在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统就会回收软引用对象
(即内存够用就保留,不够用就丢弃)。如果回收了软引用对象仍然没有足够的内存,才会抛出内存溢出异常
。这种技术常常被用来实现缓存技术。在JDK1.2之后,用java.lang.ref.SoftReference类来表示软引用。 -
测试
那么强引用和软引用有什么区别呢,我们以具体的代码体现,如下:
在运行代码之前,我们需要先修改一下配置参数,将虚拟机的最大内存设为10M。
public class Test10M { public static void main(String[] args) { strongReference(); } private static void strongReference(){ byte[] bytes = new byte[1024*1024*5];//创建5M的字节数组,此时虚拟机的最大内存为10M System.out.println(bytes); } } /** * [B@1be6f5c3 */
此时我们创建了5M的字节数组,没有超过虚拟机内存,证明此时程序正常运行。那么当我们创建10M大小的字节数据呢???
public class Test10M { public static void main(String[] args) { strongReference(); } private static void strongReference(){ byte[] bytes = new byte[1024*1024*10];//创建5M的字节数组,此时虚拟机的最大内存为10M System.out.println(bytes); } } /** * Exception in thread "main" java.lang.OutOfMemoryError: Java heap space * at com.glw.thread.referenceType.强引用.Test10M.strongReference(Test10M.java:15) * at com.glw.thread.referenceType.强引用.Test10M.main(Test10M.java:11) */
那么此时就会报错“
OutOfMemoryError
”,即内存不足错误
,可以证明强引用类型即使内存不够也不会被回收,同时会报出内存不足的错误,那么我们来看看软引用类型会是什么情况?public class Test10M { public static void main(String[] args) { softReference(); } private static void softReference(){ SoftReference<byte[]> softReference = new SoftReference<>(new byte[1024 * 1024 * 5]); System.out.println("softReference:"+softReference.get()); System.gc(); try { //进行睡眠让垃圾回收有时间执行 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("softReference:"+softReference.get()); byte[] bytes = new byte[1024 * 1024 * 5]; System.out.println("softReference:"+softReference.get()); } } /** * softReference:[B@1be6f5c3 * softReference:[B@1be6f5c3 * softReference:null */
此时我们的虚拟机的最大内存仍然为10M,此时我们创建了5M大小的软引用类型的对象,对于此时来说,内存还是足够的,但当我们执行到下面创建了一个5M大小的强引用类型的对象,我们知道,此时的内存是不够的,强引用类型的对象我们在上面验证过是不会被回收的,那么,最后的结果就是软引用类型的对象被回收,我们就能得到最后软引用对象的结果为空。同时也验证了上面的理论,当内存足够用时,软引用是不会被回收的,但是当内存不够时,软引用对象就会被内存回收。
2.3 弱引用
-
定义:弱引用的引用强度比软引用更弱一些,
无论内存是否足够,只要JVM开始进行垃圾回收,那些被弱引用关联的对象都会被回收
。即“只要发生GC,一定被回收”。 -
测试:
ThreadLocal和WeakHashMap内部都是使用了弱引用,用来保证那些不被用到的key值,在垃圾回收的时候可以被回收掉。下面我们以ThreadLocal为例来解析内部的使用:
ThreadLocal的主要原理是找到当前线程的map;Map的结构为Map(key,value),key为ThreadLocal本身value设的数据;key使用弱引用,解决内存泄露,使用remove,解决value内存泄露;
内存泄露(memory leak):内存够用,但里面有一个对象总是不能被回收(例如:强引用),位置占用。
内存溢出(out of memory):大量的内存泄露会导致内存溢出。
ThreadLocal<Person> tl = new ThreadLocal<>(); tl.set(new Person()); //此时的new Person会被Map读到 tl.remove(); //remove主要为了移除value,解决value内存泄露问题
在上面的代码中解释了set方法的原理,以及remove的作用,在这里我们能看出
ThreadLocal是一个容器
,可以用来new出一个对象,但是是线程的私有容器
,只能被一个线程读到。那么我们来看看set的主要实现:public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //t.threadLocals,拿取自己的key,value if (map != null) map.set(this, value); //this指的是当前的ThreadLocal对象 else createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
public class Test { static ThreadLocal<Person> tl = new ThreadLocal<>(); public static void main(String[] args) { new Thread(()->{ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } tl.set(new Person("zhangsan")); System.out.println(tl.get()); // tl.remove(); //把tl记录从map中删掉,(线程池中一定要remove掉,否则会导致内存泄露问题) }).start(); new Thread(()->{ try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(tl.get()); }).start(); } } /** * Person(name=zhangsan) * null */
此例子验证了ThreadLocal是线程的私有容器,只能被一个线程读到。
上面我们说到Entry使用的是弱引用,那么为什么Ehtry使用的是弱引用呢???
若是使用强引用,即使t1=null,使key的引用依然指向ThreadLocal对象,所以会有内存泄露,而使用弱引用不会。但还是有内存泄漏存在,ThreadLocal被回收,key的值变成null,则导致整个value再也无法被访问到,因此依然存在内存泄露。这就是为什么在线程池中使用完需要remove的原因。
2.4 虚引用
-
定义:虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,随时可能会被回收。JDK 1.2 之后,使用
java.lang.ref.PhantomReference
表示虚引用,这个类只有构造器和一个 get() 方法,且 get() 方法仅返回一个 null 值。
我们无法通过虚引用获得对象,必须要和ReferenceQueue引用队列
一起使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 -
测试
public class PhantomReferenceTest { public static void main(String[] args) { ReferenceQueue<String> queue = new ReferenceQueue<String>(); PhantomReference<String> pr = new PhantomReference<String>(new String("yamiyami"), queue); System.out.println(pr.get()); } } /** * null */