美文网首页
[翻译]弱引用,软引用和虚引用对GC的影响

[翻译]弱引用,软引用和虚引用对GC的影响

作者: 徐士林 | 来源:发表于2018-03-04 23:18 被阅读385次

    原文

    程序中的非强引用会造成一系列问题,这些问题都会影响GC的延迟和吞吐量。尽管在很多情况下,使用非强引用有助于避免不必要的OutOfMemoryError,但是大量使用非强引用会显著的影响GC的方式并且会影响程序的性能。

    Why should I care?

    当使用弱引用的时候,应该记住软引用被GC的方式。每当GC发现一个对象是弱可达时,即对该对象剩下的最后一个引用时弱引用时,该对象会被放到相对应的ReferenceQueue,并且对finaliza线程可见。然后可以轮询ReferenceQueue,并且执行相应的清理活动。. A typical example for such cleanup would be the removal of the now missing key from the cache.

    这里的技巧是,在这个时间上(注:对象被加入到ReferenceQueue),仍然可以为对象创建新的强引用,所以在它最终完成并回收之前,GC必须再次检查确实可以这样进行回收。
    弱引用比你想象的更加常用。许多缓存的解决方案都是使用弱引用来实现,所以即使你的代码没有明确的使用弱引用,弱引用也很可能隐式的出现在你的代码中。

    当使用软引用的时候,应该记住软引用没有虚引用被回收的那么频繁。如果没有显示的指明被回收的时间点,那么软引用被回收的时机取决去JVM本身的实现。通常JVM将软引用的回收作为内存耗尽之前的最后一次努力,即内存耗尽之前,JVM是不会回收软引用的。这意味着程序可能出现GC更频繁或者暂停时间更长的情况。

    当使用虚引用时,必须手动进行内存的管理,以便标记符合垃圾回收条件的引用。程序员可能自认为对javadoc的肤浅认识可以让他们安全的使用需引用,然而这是非常危险的。

    为了确保申明的对象保持状态,虚引用指向的对象不会被检测到:虚引用的get方法总是返回null。

    Surprisingly,many developers skip the very next paragraph in the same javadoc

    不同于软引用和弱引用,当虚引用进入队列后,GC并不能自动的清除它。通过虚引用访问的对象一直保持可达的状态,除非引用被清除或者对象自己无法访问。

    也就是说我们必须手动调用clear()方法,否则可能会出现OOM的错误。The reason why the Phantom references are there in the first place is that this is the only way to find out when an object has actually become unreachable via the usual means虚引用与软或弱引用不同,不能复活虚引用对象。

    Give me some examples

    我们开始看下面这个程序。程序分配了很多的对象,而且这些对象在minor GC期间都被成功回收。我们使用-Xmx24M -XX:NewSize=16M -XX:MaxTenuringThreshold=1参数运行程序

    package ru.gvsmirnov.perv.labs.gc;
    
    import java.lang.ref.WeakReference;
    import java.util.Arrays;
    
    public class WeakReferences {
    
        // This example shows how having weak references pointing to objects
        // May result in more frequent Full GC pauses
        //
        // There are two modes (controlled by weak.refs)
        //
        //  1. A lot of objects are created
        //  2. A lot of objects are created, and weak references are created
        //     for them. These references are held in a buffer until it's full
        //
        // The allocations made in both cases need to be exactly the same,
        // so in (1) weak references will be also created, but all of them
        // will be pointing to the same object
    
    
        // 1. Run with: -verbose:gc -Xmx24m -XX:NewSize=16m
        //              -XX:MaxTenuringThreshold=1 -XX:-UseAdaptiveSizePolicy
        //
        //    Observe that there are mostly young GCs
        //
        // 2. Run with: -Dweak.refs=true -verbose:gc -Xmx24m -XX:NewSize=16m
        //              -XX:MaxTenuringThreshold=1 -XX:-UseAdaptiveSizePolicy
        //
        //    Observe that there are mostly full GCs
        //
        // 3. Run with: -Dweak.refs=true -verbose:gc -Xmx64m -XX:NewSize=32m
        //              -XX:MaxTenuringThreshold=1 -XX:-UseAdaptiveSizePolicy
        //
        //    Observe that there are mostly young GCs
    
        private static final int OBJECT_SIZE           = Integer.getInteger("obj.size", 192);
        private static final int BUFFER_SIZE           = Integer.getInteger("buf.size", 64 * 1024);
        private static final boolean WEAK_REFS_FOR_ALL = Boolean.getBoolean("weak.refs");
    
        private static Object makeObject() {
            return new byte[OBJECT_SIZE];
        }
    
        public static volatile Object sink;
    
        public static void main(String[] args) throws InterruptedException {
    
            System.out.printf("Buffer size: %d; Object size: %d; Weak refs for all: %s%n", BUFFER_SIZE, OBJECT_SIZE, WEAK_REFS_FOR_ALL);
    
            final Object substitute = makeObject(); // We want to create it in both scenarios so the footprint matches
            final Object[] refs = new Object[BUFFER_SIZE];
    
            System.gc(); // Clean up young gen
    
            for (int index = 0;;) {
                Object object = makeObject();
                sink = object; // Prevent Escape Analysis from optimizing the allocation away
    
                if (!WEAK_REFS_FOR_ALL) {
                    object = substitute;
                }
    
                refs[index++] = new WeakReference<>(object);
    
                if (index == BUFFER_SIZE) {
                    Arrays.fill(refs, null);
                    index = 0;
                }
            }
        }
    }
    

    程序运行之后可以看到如下的GC日志

    2.330: [GC (Allocation Failure)  20933K->8229K(22528K), 0.0033848 secs]
    2.335: [GC (Allocation Failure)  20517K->7813K(22528K), 0.0022426 secs]
    2.339: [GC (Allocation Failure)  20101K->7429K(22528K), 0.0010920 secs]
    2.341: [GC (Allocation Failure)  19717K->9157K(22528K), 0.0056285 secs]
    2.348: [GC (Allocation Failure)  21445K->8997K(22528K), 0.0041313 secs]
    2.354: [GC (Allocation Failure)  21285K->8581K(22528K), 0.0033737 secs]
    2.359: [GC (Allocation Failure)  20869K->8197K(22528K), 0.0023407 secs]
    2.362: [GC (Allocation Failure)  20485K->7845K(22528K), 0.0011553 secs]
    2.365: [GC (Allocation Failure)  20133K->9501K(22528K), 0.0060705 secs]
    2.371: [Full GC (Ergonomics)  9501K->2987K(22528K), 0.0171452 secs]
    

    程序很少出现Full GC,但是如果程序程序为这些创建的对象赋予弱引用(-Dweak.refs=true),则情况会发生很大转变。将对象赋予弱引用的原因可能有很多,首先可能会使用这些对象作为weak hash map的键。无论如何,这里使用弱引用可能会导致Full GC

    2.059: [Full GC (Ergonomics)  20365K->19611K(22528K), 0.0654090 secs]
    2.125: [Full GC (Ergonomics)  20365K->19711K(22528K), 0.0707499 secs]
    2.196: [Full GC (Ergonomics)  20365K->19798K(22528K), 0.0717052 secs]
    2.268: [Full GC (Ergonomics)  20365K->19873K(22528K), 0.0686290 secs]
    2.337: [Full GC (Ergonomics)  20365K->19939K(22528K), 0.0702009 secs]
    2.407: [Full GC (Ergonomics)  20365K->19995K(22528K), 0.0694095 secs]
    

    可以看到这里出现了很多Full GC,并且GC的时间增长了一个数量级。这是一个明显的过早晋升的例子。问题的根源仍是弱引用。在我们添加所引用之前,程序创建的对象在被提升到老年代之前就死亡了。一个简单的解决方案是指定-Xmx64m -XX:NewSize=32m以便增大young generation。

    2.328: [GC (Allocation Failure)  38940K->13596K(61440K), 0.0012818 secs]
    2.332: [GC (Allocation Failure)  38172K->14812K(61440K), 0.0060333 secs]
    2.341: [GC (Allocation Failure)  39388K->13948K(61440K), 0.0029427 secs]
    2.347: [GC (Allocation Failure)  38524K->15228K(61440K), 0.0101199 secs]
    2.361: [GC (Allocation Failure)  39804K->14428K(61440K), 0.0040940 secs]
    2.368: [GC (Allocation Failure)  39004K->13532K(61440K), 0.0012451 secs]
    

    对象再一次在minor GC时被回收。软引用的对象一直在JVM面临OOM之前都不会被回收。将弱引用换成软引用将面临更多的Full GC

    下一个情况中使用了软引用,可能时更糟糕的情况。

    package ru.gvsmirnov.perv.labs.gc;
    
    import java.lang.ref.SoftReference;
    import java.util.Arrays;
    
    public class SoftReferences {
    
        // This example shows how having soft references pointing to objects
        // May result in more frequent Full GC pauses
        //
        // There are two modes (controlled by soft.refs)
        //
        //  1. A lot of objects are created
        //  2. A lot of objects are created, and soft references are created
        //     for them. These references are held in a buffer until it's full
        //
        // The allocations made in both cases need to be exactly the same,
        // so in (1) soft references will be also created, but all of them
        // will be pointing to the same object
    
    
        // 1. Run with: -verbose:gc -Xmx24m -XX:NewSize=16m
        //              -XX:MaxTenuringThreshold=1 -XX:-UseAdaptiveSizePolicy
        //
        //    Observe that there are mostly young GCs
        //
        // 2. Run with: -Dsoft.refs=true -verbose:gc -Xmx24m -XX:NewSize=16m
        //              -XX:MaxTenuringThreshold=1 -XX:-UseAdaptiveSizePolicy
        //
        //    Observe that there are lots of full GCs
        //
        // 3. Run with: -Dsoft.refs=true -verbose:gc -Xmx64m -XX:NewSize=32m
        //              -XX:MaxTenuringThreshold=1 -XX:-UseAdaptiveSizePolicy
        //
        //    Observe that there are still many full GCs
    
        private static final int OBJECT_SIZE           = Integer.getInteger("obj.size", 192);
        private static final int BUFFER_SIZE           = Integer.getInteger("buf.size", 64 * 1024);
        private static final boolean SOFT_REFS_FOR_ALL = Boolean.getBoolean("soft.refs");
    
        private static Object makeObject() {
            return new byte[OBJECT_SIZE];
        }
    
        public static volatile Object sink;
    
        public static void main(String[] args) throws InterruptedException {
    
            System.out.printf("Buffer size: %d; Object size: %d; Soft refs for all: %s%n", BUFFER_SIZE, OBJECT_SIZE, SOFT_REFS_FOR_ALL);
    
            final Object substitute = makeObject(); // We want to create it in both scenarios so the footprint matches
            final Object[] refs = new Object[BUFFER_SIZE];
    
            System.gc(); // Clean up young gen
    
            for (int index = 0;;) {
                Object object = makeObject();
                sink = object; // Prevent Escape Analysis from optimizing the allocation away
    
                if (!SOFT_REFS_FOR_ALL) {
                    object = substitute;
                }
    
                refs[index++] = new SoftReference<>(object);
    
                if (index == BUFFER_SIZE) {
                    Arrays.fill(refs, null);
                    index = 0;
                }
            }
        }
    }
    
    2.162: [Full GC (Ergonomics)  31561K->12865K(61440K), 0.0181392 secs]
    2.184: [GC (Allocation Failure)  37441K->17585K(61440K), 0.0024479 secs]
    2.189: [GC (Allocation Failure)  42161K->27033K(61440K), 0.0061485 secs]
    2.195: [Full GC (Ergonomics)  27033K->14385K(61440K), 0.0228773 secs]
    2.221: [GC (Allocation Failure)  38961K->20633K(61440K), 0.0030729 secs]
    2.227: [GC (Allocation Failure)  45209K->31609K(61440K), 0.0069772 secs]
    2.234: [Full GC (Ergonomics)  31609K->15905K(61440K), 0.0257689 secs]
    

    第三个程序使用了虚引用,使用之前同样的参数会给出相似于弱引用的的日志输出。实际上,Full GC的次数可能更少,因为本部分开始部分描述的finalization不同。

    4.180: [Full GC (Ergonomics)  57343K->57087K(61440K), 0.0879851 secs]
    4.269: [Full GC (Ergonomics)  57089K->57088K(61440K), 0.0973912 secs]
    4.366: [Full GC (Ergonomics)  57091K->57089K(61440K), 0.0948099 secs]
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    

    使用虚引用的时候必须谨慎,必须始终手动清理虚引用的对象。如果不这样做可能会出现OOM

    Could my JVMs be affected?

    作为一个一般性的建议,启用-XX:+PrintReferenceCGC 选项以查看不同引用对垃圾回收的影响。如果我们把这个参数加到WeakReference的例子中,我们可以看到下面的输出

    2.173: [Full GC (Ergonomics) 2.234: [SoftReference, 0 refs, 0.0000151 secs]2.234: [WeakReference, 2648 refs, 0.0001714 secs]2.234: [FinalReference, 1 refs, 0.0000037 secs]2.234: [PhantomReference, 0 refs, 0 refs, 0.0000039 secs]2.234: [JNI Weak Reference, 0.0000027 secs][PSYoungGen: 9216K->8676K(10752K)] [ParOldGen: 12115K->12115K(12288K)] 21331K->20792K(23040K), [Metaspace: 3725K->3725K(1056768K)], 0.0766685 secs] [Times: user=0.49 sys=0.01, real=0.08 secs] 
    2.250: [Full GC (Ergonomics) 2.307: [SoftReference, 0 refs, 0.0000173 secs]2.307: [WeakReference, 2298 refs, 0.0001535 secs]2.307: [FinalReference, 3 refs, 0.0000043 secs]2.307: [PhantomReference, 0 refs, 0 refs, 0.0000042 secs]2.307: [JNI Weak Reference, 0.0000029 secs][PSYoungGen: 9215K->8747K(10752K)] [ParOldGen: 12115K->12115K(12288K)] 21331K->20863K(23040K), [Metaspace: 3725K->3725K(1056768K)], 0.0734832 secs] [Times: user=0.52 sys=0.01, real=0.07 secs] 
    2.323: [Full GC (Ergonomics) 2.383: [SoftReference, 0 refs, 0.0000161 secs]2.383: [WeakReference, 1981 refs, 0.0001292 secs]2.383: [FinalReference, 16 refs, 0.0000049 secs]2.383: [PhantomReference, 0 refs, 0 refs, 0.0000040 secs]2.383: [JNI Weak Reference, 0.0000027 secs][PSYoungGen: 9216K->8809K(10752K)] [ParOldGen: 12115K->12115K(12288K)] 21331K->20925K(23040K), [Metaspace: 3725K->3725K(1056768K)], 0.0738414 secs] [Times: user=0.52 sys=0.01, real=0.08 secs]
    

    一如既往,只有在确定GC对应用程序的吞吐量和延迟有影响的时候才应该分析此信息。这种情况下,你可以检查日志的这些部分。通常情况,每一个GC周期内清除的引用是非常少的,很可能根本没有清除引用。如果情况并非如此,并且程序花费相当长的时间来清理引用,那么就需要进一步调查问题所在。

    What is the solution?

    如果程序实际上正在遭受软引用、弱引用、虚引用的错误、滥用或过度使用,解决这个问题可能需要更改程序的内在逻辑。因为要涉及到具体的程序,所以这里很难提供通用的解决方案。但是我们也可以提供一些思路

    • 弱引用:如果问题是由使用大量内存池触发的,则提升内存池的大小可能会解决这个问题。如示例部分,增加堆和年轻代的大小可以缓解问题。
    • 虚引用:确保已经清除了引用。确实存在一些情况,清理线程无法跟上队列的填充速度,或者干脆无法清理队列,这样会给GC造成很大压力,也可能会有OOM的风险。
    • 软引用:当软引用是问题的根源时,缓解这个问题的唯一方法就是更改程序的内部逻辑。

    相关文章

      网友评论

          本文标题:[翻译]弱引用,软引用和虚引用对GC的影响

          本文链接:https://www.haomeiwen.com/subject/embqfftx.html