关于java:谈谈java中的引用

本文简略谈一谈java中的各种援用。

java中传统援用定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的援用。

—来自《深刻了解java虚拟机》

这个定义就是指的强援用,这种强援用只能形容两种状况,被援用和未被援用,为了能示意内存短缺就援用着,内存不足就回收的状况,又搞进去三个(其实是四个,还有一个FinalReference,它与finalize办法无关,深刻了解java虚拟机这本书说不举荐用,笔者也没钻研,就不谈了)示意不同强弱水平的援用,这个强弱水平与GC无关。

几种援用的介绍

强援用

这种援用咱们再相熟不过了,比方像下边这样

User user = new User();
// 或者
byte[] user = new User[10];

强援用的特点就是:当有强援用存在时,就算将要产生OOM了也不会被回收。当然须要留神的是当gc root不可达时,就算被强援用也是会被回收的。比方下边这样的:

A a = new A();
B b = new B();
a.b = b;
b.a = a;
a = null
b = null;

这个例子中尽管a对象被b对象的a属性强援用着,b对象被a对象的b属性强援用着,然而通过可达性剖析看,他们是不可达的,所以会在下一次gc时被回收。

上面看一个强援用的测试用例,留神jvm参数中将堆内存管制在10m,并且打印出gc日志

// -Xms20m -Xmx20m -XX:+PrintGC
public class StrongReferenceTest {
    public static void main(String[] args) throws InterruptedException {
        int _10M = 10 * 1024 * 1024;
        byte[] bytes = new byte[_10M];
        System.out.println("gc前:"+ bytes);
        System.gc();
        Thread.sleep(300);
        System.out.println("gc后:"+bytes);
        System.out.println("再调配一个10M模仿堆内存不足,看看之前的bytes会不会被回收");
        byte[] bytes1 = new byte[_10M];
        Thread.sleep(1000);
    }
}

如下是输入后果:

调用System.gc()后,发现做了一次Minor GC, 一次Full GC, Minor GC回收了很多内存,Full GC 则没有回收多少内存,gc后,发现还是能找到bytes这个数组(打印进去了内存地址),所以阐明他没有被回收(要是这种强援用都被回收就没法玩了)

接下来有调配一个10M的数组,显然内存不够了,从gc日志来看,他尝试做了几次gc,然而因为咱们的bytes是强援用,所以没法回收,抛出OOM了。

软援用SoftReference

软援用的特点是当要产生OOM前,他援用的对象或者内存块会在gc时会被回收。

// -Xms20m -Xmx20m -XX:+PrintGC
public class SoftReferenceTest {
    public static void main(String[] args) throws InterruptedException {
        int _10M = 10 * 1024 * 1024;
        byte[] bytes = new byte[_10M];

        SoftReference<byte[]> softReference = new SoftReference<byte[]>(bytes);
        bytes = null; // 这个很要害,把强援用给断开,否则测试会发现一个OOM
        System.out.println("gc前:" + softReference.get());
        Thread.sleep(500);
        System.gc();
        Thread.sleep(500);
        System.out.println("gc后:" + softReference.get());
        Thread.sleep(500);
        System.out.println("再调配一个10M, 模仿堆内存不足");
        byte[] bytes1 = new byte[_10M];
        System.out.println("调配后:" + softReference.get());
    }
}

输入如下

十分神奇,也是两个10M,这次没有产生OOM!留神到红框框这一行,发现回收了10292kb,大略是10M,联合前面的调配后:null, 能够看出SoftReference援用的对象在产生OOM前被回收了。

这里还须要留神输入的第2行和第3行,发现尽管产生了gc,然而那个10M的数组没被回收,这里须要与接下来的WeakReference比照看。

弱援用WeakReference

WeakReference的特点是产生下一次gc时回收被援用的对象,不论内存是否短缺,这里须要留神比照与SoftReference的区别

接下来还是一个测试用例, 只用将上边例子中的SoftReference改为WeakReference即可:

// -Xms20m -Xmx20m -XX:+PrintGC
public class WeakReferenceTest {
    public static void main(String[] args) throws InterruptedException {
        int _10M = 10 * 1024 * 1024;
        byte[] bytes = new byte[_10M];

        WeakReference<byte[]> softReference = new WeakReference<byte[]>(bytes);
        bytes = null; // 这个很要害,把强援用给断开,否则测试会发现一个OOM
        System.out.println("gc前:" + softReference.get());
        Thread.sleep(500);
        System.gc();
        Thread.sleep(500);
        System.out.println("gc后:" + softReference.get());
        Thread.sleep(500);
        System.out.println("再调配一个10M, 模仿堆内存不足");
        byte[] bytes1 = new byte[_10M];
        System.out.println("调配后:" + softReference.get());
    }
}

输出后果:

留神到第一个10M的数组在第一次gc时就被回收了,但其实这时的内存是短缺的。

虚援用PhantomReference

虚援用也称为“幽灵援用”或者“幻影援用”,它是最弱的一种援用关系。一个对象是否有虚援用的存在,齐全不会对其生存工夫形成影响,也无奈通过虚援用来获得一个对象实例。为一个对象设置虚援用关联的惟一目标只是为了能在这个对象被收集器回收时收到一个零碎告诉。

—来自《深刻了解jvm虚拟机》

这个货色笔者看了很久,发现不得要领,做测试也不太好弄,不晓得这种援用到底有啥用。对于应用办法方面,一些博文说是要和ReferenceQueue配合着应用。

如下是一篇看起来不错的文章,有趣味的读者自行钻研吧,笔者不费这个精力了。

《在Java中应用PhantomReference析构资源对象》

应用场景

ThreadLocal中对WeakReference的应用

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

这个次要是保障当你定义的ThreadLocal不被援用时,里边的ThreadLocalMap能被回收。

参考这篇文章《ThreadLocal与WeakReference》,几句话说得还挺分明

WeakHashMap中对WeakReference的应用

与之相干的一段源码是下边这个样子的:

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;

    /**
     * Creates new entry.
     */
    Entry(Object key, V value,
          ReferenceQueue<Object> queue,
          int hash, Entry<K,V> next) {
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }
    // 此处略去很多代码,我也没看
}

在没有hash抵触的状况下,WeakHashMap就相当于保护了一个Entry数组,而Entry的key是WeakFeference援用, 所以能够猜测,如果外边没有对某个key的援用,那么下一次gc时,这个key指向的对象就会被回收。

还是做一个试验验证一下:

class User {
    private byte[] bytes = new byte[5 * 1024 *  1024];
}

public class WeakHashMapTest {
    public static void main(String[] args) throws InterruptedException {
        WeakHashMap<User,User> weakHashMap = new WeakHashMap<User, User>();
        User u2 = new User();
        weakHashMap.put(u2, new User());
        System.out.println("size1="+weakHashMap.size());
        System.gc(); // 1
        Thread.sleep(500);
        System.out.println("size2="+weakHashMap.size());
        System.out.println("---------");
        u2 = null;
        System.gc(); // 2
        Thread.sleep(500);
        System.out.println("size3="+weakHashMap.size());

    }
}

输入如下:

1处gc后发现内存没有5M的变动,因为key被u2援用着;

将u2值为null, key除了被WeakHashMap弱援用着,没别的援用了,所以调用gc后被回收,内存缩小大概5M,size3变为0。5M是key指向的对象占用的内存。

如果再调用一次gc,会发现还会gc掉5M, 这个就是value指向的对象了。

WeakHashMap常被用来做缓存,看到博客里边常有人用tomcat的一个缓存的源码举例,笔者还没看过tomcat源码,这里间接抄一个过去

package org.apache.tomcat.util.collections;

import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

public final class ConcurrentCache<K,V> {

    private final int size;

    private final Map<K,V> eden;

    private final Map<K,V> longterm;

    public ConcurrentCache(int size) {
        this.size = size;
        this.eden = new ConcurrentHashMap<>(size);
        this.longterm = new WeakHashMap<>(size);
    }

    public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            synchronized (longterm) {
                v = this.longterm.get(k);
            }
            if (v != null) {
                this.eden.put(k, v);
            }
        }
        return v;
    }

    public void put(K k, V v) {
        if (this.eden.size() >= size) {
            synchronized (longterm) {
                this.longterm.putAll(this.eden);
            }
            this.eden.clear();
        }
        this.eden.put(k, v);
    }
}

通过这种形式,将罕用的key放到eden强援用里边,不罕用的放到longterm里边,longterm是个WeakHashMap, 没有人援用key下一次gc就能够主动回收掉。做得还是挺奇妙的。

SoftReference的利用-本地缓存

public class Cache {

    public static void main(String[] args) {
        Service service = new Service();
        SoftReference<User> softReference = new SoftReference<User>(null);
        if(softReference.get() != null) {
            System.out.println(softReference.get());
        } else {
            softReference = new SoftReference<User>(service.getUser());
        }
    }
}

将拿到的user用弱援用援用着,每次都softReference查,查到则命中缓存,缩小对service申请。

用SoftReference的益处是, 当内存不足时缓存可能被回收,腾出一些内存给其余更为紧急的用途。

参考

  • java中的Reference类型
  • 软援用、弱援用、虚援用-他们的特点及利用场景
  • ThreadLocal弱援用与内存透露剖析
  • 《深刻了解java虚拟机》

一些思维导图和并发编程学习笔记可参考以下形式支付

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理