欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

浅谈Java引用类型

程序员文章站 2022-06-10 13:42:45
...

浅谈Java引用类型

本篇主要介绍了Java的几种引用类型,引用是垃圾回收的核心问题。


1. 可达性分析

可达性分析(Reachability Analysis)是垃圾回收的依据,用来判定对象是否存活,所谓存活即在栈上有没有引用指向堆上的对象。其主要算法为从GC ROOT开始作深度搜索,搜索过得路径为引用链,当一个对象从GC ROOT没有任何引用链与之关联,那么这个对象即符合了垃圾回收的标准,会在下一次GC中被回收。
Java中可以作为GC ROOT的对象包括:

栈帧中的本地变量表中引用的对象;

方法区中类的静态变量引用的对象;

方法区中常量引用的对象;

本地方法栈中Native方法引用的对象;

2 引用类型

狭义地理解引用就可以认为引用存储着另一块内存的起始地址,这种定义下对象只有两种状态:被引用和未被引用。然而Java从JDK1.2之后就提供了更灵活地对引用的定义,这些定义的引用类型,可以使得对象的生命周期不再那么绝对。下面按照引用强度从高到低依次介绍Java现存的4中引用类型。

2.1 强引用 (Strong Reference)

强引用就是最常见的直接引用,通过Object obj = new Object()引用,或者将引用指向另一个对象,都可以实现强引用。只要强引用存在,被引用的对象永远不会被垃圾回收。若要解除某个引用和对象的强引用,只需要显式地将null赋给该引用,之后在下一次GC时,原先被引用的在堆上的对象就会被回收。

public class Foo {

    public static void main(String[] args) {
        Foo foo = new Foo();
        foo = new Foo();
        System.gc();
    }

    public Foo() {
        System.out.println("Foo=" + this.hashCode() + " has created.");
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("Foo=" + this.hashCode() + " is about to be garbage collected.");
        super.finalize();
    }
}

显式执行System.gc()会触发垃圾回收,引用foo在被重新指向新的Foo对象后符合垃圾回收条件,会被回收掉。在回收前,由于我们重写了finalize()方法,将打印出该对象的信息。
浅谈Java引用类型

另外,JDK本身也会利用显式地将引用置为null来处理不再被引用的对象,例如JDK提供的集合框架,在remove()方法时就会采用这种方法:

/*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // Let gc do its work
    }

代码片段取自于ArrayList.remove()方法,最终执行的私有方法fastRemove()中就在底层数组中把需要移除的元素显式地标记为了null,并且利用GC去回收之前的元素对象。这种强引用关系,在任何情况下除非显式地解除,JVM不会有任何机制加以管理,即便在内存不足时,即将抛出OutOfMemoryError时也不会。

2.2 软引用 (Soft Reference)

软引用用来表示一些有用但是非必要的对象。Java1.2之后,提供了软引用的实现,SoftReference类。根据JavaDoc的定义,为了响应内存的需求,软引用对象会被垃圾回收器感知并清除,软引用通常用来实现内存敏感型的缓存。如果一个对象被确定为时软可达,垃圾回收器会保证在JVM抛出OutOfMemoryError之前清理所有这个对象的软引用。简单讲,软引用对象只会在内存不足时被回收,这样就实现了缓存,并且并不会因为大量的软引用缓存导致OOM,因为OOM之前会进行二次回收,如果内存还是不足才会真正抛出OOM。当引用类型Reference的对象被回收后,如果在构造引用类型的时候传入一个ReferenceQueue队列实例,则被回收的引用类型将被放入该队列。

/**
 * VM args: -Xmx20m -Xms20m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
 * 
 * @author Ken
 *
 */
public class SoftReferenceExample {

    public static void main(String[] args) throws InterruptedException {

        // Reference queue
        ReferenceQueue<Object> queue = new ReferenceQueue<>();

        // SoftReference to strong reference
        SoftReference<Foo> reference = new SoftReference<>(new Foo(), queue);

        // ExplicitGC
        System.gc();

        // Before OOM
        System.out.println("Before OOM: softReference=" + reference);
        System.out.println("Before OOM: referent=" + reference.get());
        System.out.println("Before OOM: refereceQueue.poll=" + queue.poll());

        // OOM happened
        try {
            OOM();
        } catch (Throwable e) {
            System.err.println("OutOfMemoryError is caught so that GC is guranteed to be executed.");
        }

        // After OOM
        System.out.println("After OOM: softReference=" + reference);
        System.out.println("After OOM: referent=" + reference.get());
        System.out.println("After OOM: referenceQueue.poll=" + queue.poll());

    }

    private static void OOM() {
        List<Object> list = new ArrayList<Object>();
        while (true) {
            list.add(new Object());
        }
    }
}

示例代码中,首先实例化一个软引用对象,并将其底层对象指向一个新建的Foo实例,对于Foo对象,我们重写了其finalize()方法,意在让我们能感知其在何时被垃圾回。然后调用系统GC方法,显式地执行一次GC。最后模拟一次OOM,观察现象。

浅谈Java引用类型

从结果可以看到,首先Foo对象被创建了,其hashcode=366712642, 随后在进行了一次显式的GC操作后,软引用对象及其引用的对象都没有被回收,因此引用队列中也无法获取元素。接着执行了OOM()方法,在真正抛出OOM之前,Foo对象的finalize()被调用了,意味着这个对象将被垃圾回收。随后,我们可以看到,软引用对象本身还维持着引用,不影响输出,但是其指向的Foo对象由于OOM的发生,被回收了,从引用队列中获取头元素,也可以得到刚才被回收的对象其关联的软引用对象,这也印证了,被软引用引用的对象,只有在OOM发生前才会被回收。

2.3 弱引用 (Weak Reference)

弱引用与软引用类似,但是它的强度比软引用更弱一些,也就是说它被回收的条件更为宽松,只要GC发生,弱引用必被回收。Java1.2之后,提供了弱引用的实现,WeakReference类。

/**
 * VM args: -Xmx20m -Xms20m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
 * 
 * @author Ken
 *
 */
public class WeakReferenceExample {

    public static void main(String[] args) throws InterruptedException {
        // Reference queue
        ReferenceQueue<Foo> queue = new ReferenceQueue<>();

        // WeakReference to strong reference
        WeakReference<Foo> reference = new WeakReference<>(new Foo(), queue);

        // Before GC
        System.out.println("Before GC: weakReference=" + reference);
        System.out.println("Before GC: referent=" + reference.get());
        System.out.println("Before GC: refereceQueue.poll=" + queue.poll());

        // ExplicitGC
        System.gc();

        // After GC
        System.out.println("After GC: weakReference=" + reference);
        System.out.println("After GC: referent=" + reference.get());
        System.out.println("After GC: referenceQueue.poll=" + queue.poll());

    }
}

示例代码基本与2.2中的相同,唯一不同的是,我们不再强制触发OOM,只执行GC。观察现象发现,即使没有产生OOM,任意的GC都会回收弱引用指向的对象。

浅谈Java引用类型

同样地,当WeakReference所指向的对象被回收后,弱引用也会被加入引用队列。

2.4 虚引用 (Phantom Reference)

虚引用不改变对象的生命周期,垃圾回收对于引用指向的对象不会因为虚引用的存在而产生不一样的行为,只要对象本身符合垃圾回收条件,该对象即会被回收,并且虚引用和弱引用、软引用一样会被加入引用队列,因此虚引用只用于追踪对象是否被回收,可以通过检验队列中是否有虚引用来判断与之关联的对象是否已经回收。

/**
 * 
 * @author Ken
 *
 */
public class PhantomReferenceExample {

    public static void main(String[] args) throws InterruptedException {
        Object foo = new Object();
        System.out.println("Object=" + foo.hashCode() + " has been created.");
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        PhantomReference<Object> reference = new PhantomReference<>(foo, queue);

        foo = null;
        Thread.sleep(3000);
        System.gc();
        Thread.sleep(3000);

        // After GC
        System.out.println("After GC: softReference=" + reference);
        System.out.println("After GC: referent=" + reference.get());
        System.out.println("After GC: referenceQueue.poll=" + queue.poll());
    }
}

代码中看到,当foo被显式设为null即符合了垃圾回收条件,foo最终会被回收,并且引用将被加入队列。

浅谈Java引用类型

3. TheadLocal中的内存泄漏问题

谈到WeakReference就不得不谈谈其在ThreadLocal中的应用。限于篇幅,这一节不会详细介绍ThreadLocal的原理。概括地讲,ThreadLocal是线程独立的容器,ThreadLocal中存放的数据,可以贯穿整个线程的生命周期,其实现依靠了Thread类中的成员变量ThreadLocal.ThreadLocalMap threadLocals,因此每一个Thread实例维护一个ThreadLocalMap实例,实现了线程的隔离。最后最关键地,我们可以看到ThreadLocalMap是一个类似于HashMap的数据结构,底层也是数组,数组桶中的元素为ThreadLocalMap.Entry,这个Entry继承WeakReference<ThreadLocal>, 这些看清楚了,ThreadLocal本身不存储数据,它只是作为key去从映射表中获取value,而这个key是弱引用。下图很好地展示了,线程及其ThreadLocal对象之间的引用关系。
浅谈Java引用类型

3.1 内存泄漏原因

上文讲到了,ThreadLocal对象作为弱引用key存放在ThreadLocalMap中,那么当外部没有强引用指向这个ThreadLocal对象,加上ThreadLocalMap持有的是弱引用,那么这个对象会在下一次GC中被回收。产生的现象,Map出现key=null的Entry,而此时的value是访问不到的,因为ThreadLocal已经没有强引用了,所以value就成了残留对象,并且是Entry持有的强引用,无法被回收造成内存泄漏。强引用链如下:

Thread引用–>Thread对象–>ThreadLocalMap对象–>Entry对象–>value对象

此时就产生了所谓的StaleEntry,事实上,JDK在实现的时候已经考虑了这种场景,ThreadLocalMap中有的私有方法replaceStaleEntry()以及expungeStaleEntry()就是用来处理StaleEntry,而这些私有方法,在ThreadLocal中的set(),get(),remove()中都会最终执行来清除线程ThreadLocalMap中的残留数据。

然而事情不会那么简单,当我们碰到以下两种场景,内存泄漏仍然会存在:

ThreadLocal对象为静态使之与线程同生命周期, ThreadLocal无法被回收。
无法保证会清除线程ThreadLocalMap的方法一定会被执行。

接下来的问题,如果我们不使用WeakReference会怎么样?
由于ThreadLocal对象被ThreadLocalMap持有,如果ThreadLocal是强引用实现,那么由于ThreadLocalMap的生命周期与线程相同,会造成ThreadLocal本身也会产生内存泄漏;反之,如果用弱引用实现,那么起码可以保证ThreadLocal不会产生内存泄漏,最坏的情况是value产生泄漏。而value有机会在下一次的清除操作中回收。

因此总结下内存泄漏的根因,根本上与弱引用有关,但不是弱引用直接造成的,而是由于ThreadLocal在用完的时候没有显式地移除该ThreadLocal对应的Entry。

3.2 ThreadLocal使用的正确姿势

每次使用完ThreadLocal后,手动调用remove()方法,清除数据,不但能够避免内存泄漏,更重要的是,当线程本身是线程池看护的,如果不是每次调用remove(),ThreadLocal无法被回收,因为线程并没有被销毁,它只是被池化了,如果再加上糟糕的编程习惯,极有可能造成业务逻辑的混乱。

4. 总结

本文从可达性分析入手介绍了GC的基本准侧,进而展开了对Java引用类型的简单介绍,最后给出了弱引用在JDK中的应用场景,并总结了其使用中可能遇到的内存泄漏问题,给出了最佳实践。


以上

© 著作权归作者所有

引用

[1] 深入理解Java虚拟机 JVM高级特性与最佳实践, 周志明著,chapter 3.2

[2] ThreadLocal内部机制 https://www.jianshu.com/p/0ce314da0248

[3] 深入分析 ThreadLocal 内存泄漏问题 http://www.importnew.com/22039.html