源码随想 String -> SoftReference

2021年7月27日 15:38:14

今天实习时看 String的源码,发现其中的一个构造方法

public String(byte bytes[], int offset, int length, String charsetName)
    throws UnsupportedEncodingException {
    if (charsetName == null)
        throw new NullPointerException("charsetName");
    checkBounds(bytes, offset, length);
    this.value = StringCoding.decode(charsetName, bytes, offset, length);
}

StringCoding.decode()方法处点入之后,发现其实现如下:

static char[] decode(String charsetName, byte[] ba, int off, int len)
    throws UnsupportedEncodingException
{
    StringDecoder sd = deref(decoder);
    String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
    if ((sd == null) || !(csn.equals(sd.requestedCharsetName())
                          || csn.equals(sd.charsetName()))) {
        sd = null;
        try {
            Charset cs = lookupCharset(csn);
            if (cs != null)
                sd = new StringDecoder(cs, csn);
        } catch (IllegalCharsetNameException x) {}
        if (sd == null)
            throw new UnsupportedEncodingException(csn);
        set(decoder, sd);
    }
    return sd.decode(ba, off, len);
}

deref()方法处点入,发现其实现如下:

private static <t> T deref(ThreadLocal<softreference<t>> tl) {
    SoftReference<t> sr = tl.get();
    if (sr == null)
        return null;
    return sr.get();
}

没想到,这个 decoder居然是一个存放在 ThreadLocal里的 软引用!!!

关于 tl.get(),也就是 ThreadLocal :: get(),虽然还没看过这部分代码,但是在马士兵的课上和一些面经上面都强调过它的重要性,在这里一看到,忽然有一种他乡遇故知的感受。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

不止如此,在 sr.get()方法中,有两个很奇怪的量 clocktimestamp

public T get() {
    T o = super.get();
    if (o != null && this.timestamp != clock)
        this.timestamp = clock;
    return o;
}

进入到 SoftReference.java类中之后,有如下定义:

    /**
     * Timestamp clock, updated by the garbage collector
     */
    static private long clock;

    /**
     * Timestamp updated by each invocation of the get method.  The VM may use
     * this field when selecting soft references to be cleared, but it is not
     * required to do so.
     */
    private long timestamp;

也就是说,clock会在每次进行垃圾收集的时候更新;timestamp会在每次方法调用的时候更新。JVM通过对 timestamp字段的监测来判断是否需要清楚弱引用。

那么到底什么时候真正进行软引用的清理呢?

首先,软引用中有一个全局变量 clock,在每一次垃圾回收的时候,它的值都会更新(in millis)。

同时,每一个软引用都有一个 timestamp变量,当前软引用每次被访问的时候都会更新(包括 初始化 和 调用get()方法)

当一次新的垃圾回收开始时,是否要回收一个软饮用对象依据如下:

  1. timestamp的年龄多大了
  2. 剩下的堆空间还有多少

具体的计算方式如下:

  • free_heap为堆中的空余空间大小(以 MB为单位)
  • interval是上一次 GC和 现在timestamp的时间差,
  • ms_per_mb是一个常量,保持堆中空余的每MB空间中的 软引用 的毫秒值
interval <= free_heap * ms_per_mb

举个例子:

如果当前的某个软引用 A,它的 timestamp值为 2000ms,上一次 GC的 clock时间是 5000ms,ms_per_mb常量的大小是 1000,空闲的堆空间为 1MB,那么,计算可得

5000 - 2000 <= 1 * 1000 是假的,返回 false

所以我们需要清理掉这个软引用。

那么如果假设堆空间比较大,比如是 4MB,那么计算可得

5000 - 2000 <= 4 * 1000 是真的

所以我们不需要清理这个软引用


需要注意到的是:

一个软引用在一次访问之后,至少会存活一次 GC。

因为:

interval的值是我们通过 clocktimestamp计算出来的,而 clock是上一次的 FGC时间,既然有上一次的 GC,说明它肯定至少得活过一次 GC呀。

此外,每当一个引用在某次垃圾回收之后被访问到了,那么 timestamp的值会被设置成和 clock一样,然后 interval就变成了 0,软引用就肯定不会被回收了。

想想也是的呀,针对经常在用的对象,我们肯定是要保留的呀。</softreference