JIT 优化和弱引用

7

我有以下的代码片段:

private final List<WeakReference<T>> slaves;

public void updateOrdering() {
  // removes void weak references 
  // and ensures that weak references are not voided 
  // during subsequent sort 
  List<T> unwrapped = unwrap();
  assert unwrapped.size() == this.slaves.size();
  // **** could be reimplemented without using unwrap() ****
  Collections.sort(this.slaves, CMP_IDX_SLV);
  unwrapped = null;// without this, ....
}

方法unwrap()只是创建了一个由slaves中弱引用所引用的T列表,同时消除了在slaves中引用null的弱引用。然后进行排序,其中依赖于slaves的每个成员都引用某个T;否则代码将产生NullPointerException
由于unwrappedslaves中拥有每个T的引用,因此在排序过程中没有GC消除T。最后,unwrapped = null消除了对解包的引用,因此再次释放了GC。似乎效果很好。
现在我的问题是:如果我删除unwrapped = null;,在一些负载下运行许多测试就会导致NullPointerExceptions。我怀疑JIT消除了List<T> unwrapped = unwrap();,因此GC在排序期间应用于slaves中的T。您是否有另一种解释?如果您同意我的看法,那么这是JIT的一个错误吗?
我个人认为不需要unwrapped = null,因为unwrapped一旦updateOrdering()返回,就会从帧中删除。是否有规范说明可优化的内容和不可优化的内容?还是我做错了?我的想法是修改比较器,使其允许弱引用null。您对此有何看法?谢谢建议。
补充信息(1):
首先是Java版本:
java version "1.7.0_45" OpenJDK Runtime Environment (IcedTea 2.4.3) (suse-8.28.3-x86_64) OpenJDK 64-Bit Server VM (build 24.45-b08, mixed mode)
然后有人想看unwrap方法。
private synchronized List<T> unwrap() {
List<T> res = new ArrayList<T>();
T cand;
WeakReference<T> slvRef;
Iterator<WeakReference<T>> iter = this.slaves.iterator();
while (iter.hasNext()) {
    slvRef = iter.next();
    cand = slvRef.get();
    if (cand == null) {
    iter.remove();
    continue;
    }
    assert cand != null;
    res.add(cand);
} // while (iter.hasNext())

return res;
}

请注意,遍历时void引用会被移除。事实上,我已经替换了这个方法。
private synchronized List<T> unwrap() {
List<T> res = new ArrayList<T>();
for (T cand : this) {
    assert cand != null;
    res.add(cand);
}

return res;
}

我使用了自己的迭代器,但功能上应该是相同的。

有人需要堆栈跟踪信息。这里有一部分。

 java.lang.NullPointerException: null
 at WeakSlaveCollection$IdxComparator.compare(WeakSlaveCollection.java:44)
 at WeakSlaveCollection$IdxComparator.compare(WeakSlaveCollection.java:40)
 at java.util.TimSort.countRunAndMakeAscending(TimSort.java:324)
 at java.util.TimSort.sort(TimSort.java:189)
 at java.util.TimSort.sort(TimSort.java:173)
 at java.util.Arrays.sort(Arrays.java:659)
 at java.util.Collections.sort(Collections.java:217)
 at WeakSlaveCollection.updateOrdering(WeakSlaveCollection.java:183)

在比较器中,它指向了带有返回值的那一行。

static class IdxComparator 
    implements Comparator<WeakReference<? extends XSlaveNumber>> {
    public    int compare(WeakReference<? extends XSlaveNumber> slv1, 
              WeakReference<? extends XSlaveNumber> slv2) {
        return slv2.get().index()-slv1.get().index();
    }
} // class IdxComparator 

最后,
 private final static IdxComparator CMP_IDX_SLV = new IdxComparator();

是一个重要的常数。

附加内容(2)

现在观察到,即使在updateOrdering()中存在'unwrapped = null',仍然会发生NPE。

如果没有严格的引用保持,在jit优化后,弱引用可能会被Java运行时删除。源代码似乎一点也不重要。

我以下面的方式解决了这个问题:

public void updateOrdering() {
Collections.sort(this.slaves, CMP_IDX_SLV);
}

没有插入任何修饰,以防止从属对象被垃圾回收,并启用CMP_IDX_SLV中的比较器来处理对null的弱引用。
    public    int compare(WeakReference<? extends XSlaveNumber> slv1, 
              WeakReference<? extends XSlaveNumber> slv2) {
    XSlaveNumber sSlv1 = slv1.get();
    XSlaveNumber sSlv2 = slv2.get();
    if (sSlv1 == null) {
    return sSlv2 == null ? 0 : -1;
    }
    if (sSlv2 == null) {
    return +1;
    }
    assert sSlv1 != null && sSlv2 != null;

    return sSlv2.index()-sSlv1.index();
    }

作为一个副作用,对底层列表List> slaves; 进行排序会将空虚的弱引用放置在列表末尾,以便稍后可以回收。

2
我怀疑它是否会删除unwrap()调用本身,但除非之后从未使用过unwrapped,否则它有资格被收集。无论是空赋值还是不是,它都依赖于垃圾收集器的实现细节。 - kiheru
这只是一个理论,但编译器可能会注意到unwrap()的返回值实际上并没有被使用,并通过不存储它来进行优化(从而完全消除了unwrapped变量)。无论你尝试什么,你都会在与编译器的斗争中失败。你可以做的是对已拆包的列表进行排序,然后重新打包它们。 - Mattias Buelens
1
请展示堆栈跟踪和unwrap方法。问题很可能在您的代码中,而不是JIT中。 - user2357112
1
没有一个合法的JITC会消除方法调用(除了内联)。那将是一个重大的错误。但是,无论是否使用JIT,只要没有对未包装的List的未来引用,它就有资格在任何时候被GCed。由于Java堆栈的工作方式,在非JITC情况下可能不会发生这种情况,但语言定义中没有阻止它的内容。 - Hot Licks
好的,我更新了我的视图:可能是方法调用的返回值赋值有问题,而不是调用本身。 - user2609605
显示剩余6条评论
3个回答

2

从Java 9开始,防止JIT丢弃unwrapped的正确方法是使用Reference.reachabilityFence:

public void updateOrdering() {
  List<T> unwrapped = unwrap();
  Collections.sort(this.slaves, CMP_IDX_SLV);
  Reference.reachabilityFence(unwrapped);
}

reachabilityFence的存在使得在调用之前,unwrapped被视为强引用,防止在sort完成之前收集unwrapped或其元素。(由于reachabilityFence主要作为JIT指令行为,因此它的影响似乎是向后传播的。)如果没有reachabilityFence,即使变量仍在作用域内,一旦JIT可以证明它永远不会再次被访问,unwrapped仍然可能被收集。


2
我检查了您的源代码,当JIT编译与您的"updateOrdering"方法对应的方法并且排序过程中发生GC时,我遇到了NullPointerException。
但是,无论是否使用unwrapped = null,我都会在Collections.sort中遇到NullPointerException。这可能是由于我的示例源代码与您的不同或Java版本差异引起的。如果您告诉我Java版本,我将进行检查。
我使用以下Java版本。
java version "1.7.0_40" Java(TM) SE Runtime Environment (build 1.7.0_40-b43) Java HotSpot(TM) 64-Bit Server VM (build 24.0-b56, mixed mode)
如果您想欺骗JIT编译,请将下面的代码插入到您的源代码中,而不是unwrapped = null(例如)。然后,JIT编译不会消除unwrapped代码。
long value = unwrapped.size() * unwrapped.size();
if(value * value % 3 == 1) {
  //Because value * value % 3 always is 1 or 0, this code can't reach. 
  //Insert into this the source code that use unwrapped array, for example, show unwrapped array.
}

我的考试成绩如下。

  • 如果JIT不针对updateOrdering方法进行优化,则不会发生NullPointerException。
  • 如果JIT优化了我的方法,那么在某些时候会发生NullPointerException。
  • 如果JIT通过插入上述源代码来欺骗JIT编译器来优化我的方法,则不会发生NullPointerException。

因此,我(和你)建议JIT优化消除未封装的代码,然后会发生NullPointerException。

顺便说一句,如果您想显示JIT编译器的优化情况,请使用-XX:+PrintCompilation参数调用Java。
如果您想显示GC情况,请使用-verbose:gc参数。

仅供参考,我的示例源代码如下。

public class WeakSampleMain {
    private static List<WeakReference<Integer>> weakList = new LinkedList<>();
    private static long sum = 0;
    public static void main(String[] args) {
        System.out.println("start");
        int size = 1_000_000;
        for(int i = 0; i < size; i++) {
            Integer value = Integer.valueOf(i);
            weakList.add(new WeakReference<Integer>(value));
        }
        for(int i = 0; i < 10; i++) {
            jitSort();
        }
        GcTask gcTask = new GcTask();
        Thread thread = new Thread(gcTask);
        thread.start();
        for(int i = 0; i < 100000; i++) {
            jitSort();
        }
        thread.interrupt();
        System.out.println(sum);
    }

    public static void jitSort() {
        List<Integer> unwrappedList = unwrapped();
        removeNull();
        Collections.sort(weakList, 
                new Comparator<WeakReference<Integer>>() {

                    @Override
                    public int compare(WeakReference<Integer> o1,
                            WeakReference<Integer> o2) {
                        return Integer.compare(o1.get(), o2.get());
                    }
        }
                );
        for(int i = 0; i < Math.min(weakList.size(), 1000); i++) {
            sum += weakList.get(i).get();
        }
        unwrappedList = null;
//          long value = (sum + unwrappedList.size());
//          if((value * value) % 3 == 2) {
//              for(int i = 0; i < unwrappedList.size(); i++) {
//                  System.out.println(unwrappedList.get(i));
//              }
//          }
    }

    public static List<Integer> unwrapped() {
        ArrayList<Integer> list = new ArrayList<Integer>();
        for(WeakReference<Integer> ref : weakList) {
            Integer i = ref.get();
            if(i != null) {
                list.add(i);
            }
        }
        return list;
    }

    public static void removeNull() {
        Iterator<WeakReference<Integer>> itr = weakList.iterator();
        while(itr.hasNext()) {
            WeakReference<Integer> ref = itr.next();
            if(ref.get() == null) {
                itr.remove();
            }
        }
    }

    public static class GcTask implements Runnable {
        private volatile int result = 0;
        private List<Integer> stockList = new ArrayList<Integer>();
        public void run() {
            while(true) {
                if(Thread.interrupted()) {
                    break;
                }
                int size = 1000000;
                stockList = new ArrayList<Integer>(size);
                for(int i = 0; i < size; i++) {
                    stockList.add(new Integer(i));
                }
                if(System.currentTimeMillis() % 1000 == 0) {
                    System.out.println("size : " + stockList.size());
                }
            }
        }

        public int getResult() {
            return result;
        }
    }
}

你的评论帮了很大忙。从实验中我无法确定解包= null是否可以防止垃圾回收。这可能取决于我的环境的某些方面。这就是为什么我问是否有规范指定了什么被优化和未被优化的原因。你的实验结果不同,可能表明没有规范。好吧......规范只是说弱引用在有强引用时不会被GC清除。但是这被弱化了:在Java源代码中,解包提供了强引用,但如果它被优化掉…… - user2609605
顺便说一下,我在你要求的问题中添加了一些有用的信息。 - user2609605

0

您的问题

如果我删除 unwrapped = null; ,则在某些负载下运行多个测试时会导致 NullPointerException

根据我的理解,我不认为 unwrapped = null; 会有任何影响。
是的,我也读到过,将 objects = null 有时会增加被引用对象被垃圾回收的概率,但我认为它在这里无关紧要,因为一旦方法结束,unwrapped的作用域就结束了,它就有资格进行垃圾回收,在您的函数中,排序Collections.sort(this.slaves, CMP_IDX_SLV); unwrapped = null; 之前完成,因此在添加或删除它们时得到NPE是没有意义的。

我认为你只是碰巧得到了NPE,我相信如果你再次运行测试,你也会得到NPE。

如果您阅读Java文档

弱引用对象不会阻止其引用者被标记为可终结、终结,然后被回收。弱引用最常用于实现规范化映射。假设垃圾收集器在某个时间点确定一个对象是弱可达的。此时,它将原子性地清除所有对该对象的弱引用以及通过一系列强引用和软引用到达该对象的任何其他弱可达对象的所有弱引用。同时,它将声明所有以前弱可达的对象为可终结对象。同时或稍后,它将把那些新清除的已注册到引用队列的弱引用加入队列中。因此,在使用unwrap()构建List时,有可能会使某些对象标记为已终结,在Collection.sort工作时,某些WeakReference被赋值为nullMattias Buelens所述的观点是完全有效的,您总是会在与编译器的斗争中失败。
如果你同意我的观点,那么这是JIT中的一个bug吗?
不,当然不是,我完全不同意你的看法。
我有一个想法,可以修改比较器(comparator),使其允许在null上使用弱引用。你对此有什么看法?
我认为这将解决你的NPE问题之一,但你的要求“删除void弱引用并确保在后续排序期间不会使弱引用失效”没有得到满足。 相反,尝试再次调用unwrap,这将几乎消除NPE的可能性。
List<T> unwrapped = unwrap();
unwrapped = unwrap(); //Again to eliminate the chances for NPE as now we would have 
           //already made strong refrences to all objects which have not been `null`

首先感谢大家提供的好答案。首先,我会提供很多人缺失的信息。 - user2609605
最简单的提供方式:Java版本: Java版本“1.7.0_45” OpenJDK运行环境(IcedTea 2.4.3)(suse-8.28.3-x86_64) OpenJDK 64位服务器虚拟机(构建24.45-b08,混合模式) - user2609605
方法unwrap现在已经使用自己的迭代器进行了更改,但应该与以下内容等效: <code> private synchronized List<T> unwrap() { List<T> res = new ArrayList<T>(); T cand; WeakReference<T> slvRef; Iterator<WeakReference<T>> iter = this.slaves.iterator(); while (iter.hasNext()) { slvRef = iter.next(); cand = slvRef.get(); if (cand == null) { iter.remove(); continue; } res.add(cand); } // while (iter.hasNext()) return res; } </code> - user2609605
好的,这是我对你第一部分的真实评论: “所以当添加或删除它们时,你得到NPE是没有意义的。” 你能更具体一些吗? 你认为NPE不可能发生吗?还是在两种情况下都会发生? - user2609605
好的,现在我已经观察到了与unwrapped==null相关的问题。 - user2609605
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接