JAVA不可不知的强软弱虚四种引用

Posted 等不到的口琴

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA不可不知的强软弱虚四种引用相关的知识,希望对你有一定的参考价值。

情景引入

ThreadLocal 在什么情况下可能发生内存泄漏?如果想清楚这个问题的来龙去脉,看源码是必不可少的,看了源码之后你发现,实际 ThreadLocal 中实际用到 static class Entry extends WeakReference> {} ,谜底实际就是使用了弱引用 WeakReference。本文主要总结概括java中强软弱虚四种引用的特点以及区别,并且进行代码验证。

强引用

强引用就是我们经常用到的方式:Object o = new Object()。这非常常见, 垃圾回收时,强引用的变量是不会被回收,即使内存溢出也不会回收,只有设置 o=null,jvm 通过根可达性分析,发现没有 GC root 到达对象,确定为垃圾对象后,垃圾回收器才会清理堆中的对象,释放内存。 当继续申请内存分配,就会OOM。

先创建一个类,重写finalize方法:

public class M {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize");
    }
}

再写一个垃圾回收的测试类:

public class NormalReference {
    public static void main(String[] args) throws IOException {
        M m = new M();
        m = null;//只有没有引用时,M的对象m才会被回收
        System.gc(); //DisableExplicitGC
        System.in.read();//阻塞线程的目的是:因为GC是运行在其他线程中的,不阻塞很可能还没开始回收,线程就已经结束了。
    }
}

输出结果:

finalize

软引用

软引用只有在内存不够的时候才会回收, 用来描述一些还有用但并非必须的对象,JDK从 1.2 开始加入了 Reference ,SoftReference 是其中一个分类,它的作用是通过 GC root 到达对象 a,如果对象仅仅有 SoftReference ,对象 a 将会在JVM OOM之前被 jvm gc 释放掉,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
首先设置虚拟机参数:-Xmx20M
测试类:

public class SoftReferenceDemo {
    public static void main(String[] args) {
        SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*10]);
        System.out.println(m.get());
        System.gc();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(m.get());

        //再分配一个数组,heap将装不下,这时候系统会垃圾回收,先回收一次,如果不够,会把软引用干掉
        byte[] b = new byte[1024*1024*15];
        System.out.println(m.get());
    }

}

控制台输出:

[B@10f87f48
[B@10f87f48
null

同时也可以通过 jvisualvm 查看 jvm 堆的使用,可以看到堆在要溢出的时候就会回收掉,空闲的内存很大的时候,你主动执行 执行垃圾回收,内存是不会回收的。

弱引用

只要调用了垃圾回收(System.gc( ))就会被回收。

应用场景:只要强引用消失则应该被回收,一般用在容器里,典型应用ThreadLock,看下WeakHashMap、AQSunlock源码(Tomcat缓存用的是弱应用)

弱应用测试案例:

public class WeakReferenceDemo {
    public static void main(String[] args) {
        WeakReference<M> m = new WeakReference<>(new M());
        System.out.println(m.get());
        System.gc();
        System.out.println(m.get());
        ThreadLocal<M> tl = new ThreadLocal<>();
        tl.set(new M());
        tl.remove();//必须ThreadLocal用完必须remove,否则还是有内存泄漏
    }
}

弱引用举例子程序

public class ThreadLocal2 {
    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());
        }).start();
    }
    static class Person {
        String name = "zhangsan";
    }
}

例子图示:

img

虚引用

虚引用,主要用于堆外内存。一般是搞JVM的人用,所以基本没用。
1、堆外内存。
NIO里面有一个Buffer,叫DirectByteBuffer(直接内存,不被jvm虚拟机管理),也叫堆外内存,被操作系统管理,如果这个引用被置为空值,则没法回收,用虚引用时,检测Queue,如果被回收了则去清理堆外内存。java可以自己回收堆外内存,用的是Unsave中的freeMemory。

2、会关联一个队列,当虚引用被回收的时候回接收到关联队列里,也就是给你一个通知,被回收了,弱引用里面的值是可以get到的,虚引用根本get不到。

3、垃圾回收器一过来,直接就被回收了。

验证程序(先设置堆内存为20M,VM options :-Xms20M -Xmx20M,生产环境中堆空间最大最小设置成一样)

public class T04_PhantomReference {
    private static final List<Object> LIST = new LinkedList<>();
    private static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();
    public static void main(String[] args) {
        PhantomReference<M> phantomReference = new PhantomReference<>(new M(), QUEUE);
        new Thread(() -> {
            while (true) {
                LIST.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(() -> {
            while (true) {
                Reference<? extends M> poll = QUEUE.poll();
                if (poll != null) {
                    System.out.println("--- 虚引用对象被jvm回收了 ---- " + poll);
                }
            }
        }).start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

关于ThreadLocal

ThreadLocal 在我们实际开发中用的还是比较多的。那它到底是个什么东东呢? 线程本地变量,我们知道 局部变量 (方法内定义的变量)和 成员变量 (类的属性)。

有的时候呢,我们希望一个变量的生命周期可以贯穿整个线程的一个任务运行周期(线程池中的线程可以分配执行不同的任务),在各个方法调用的时候我们可以拿到这个预先设置的变量,这就是 ThreadLocal 的作用。

比如我们想要拿到当前请求的 HttpServletRequest,然后在当前各个方法都可以获取到,SpringBoot 已经帮我们封装好了,RequestContextFilter 在每个请求过来之后,都会通过 RequestContextHolder 设置线程本地变量,原理就是操作 ThreadLocal

ThreadLocal 只是针对当前线程中的调用,跨线程调用是不行的,所以 Jdk 通过 InheritableThreadLocal 继承 ThreadLocal 来实现。

以上是关于JAVA不可不知的强软弱虚四种引用的主要内容,如果未能解决你的问题,请参考以下文章

JVM,垃圾回收面试题

Java:对象的强软弱和虚引用的区别

强软弱虚引用,只有体会过了,才能记住

提升--11---java的四种引用:强软弱虚

你不可不知的Java引用类型总结篇

Java中强软弱虚引用