JVM错误?缓存对象字段值导致 ArrayIndexOutOfBoundsException

Posted

技术标签:

【中文标题】JVM错误?缓存对象字段值导致 ArrayIndexOutOfBoundsException【英文标题】:JVM bug? Cached Object field value cause ArrayIndexOutOfBoundsException 【发布时间】:2013-06-19 18:57:08 【问题描述】:

这有点奇怪,但代码比文字更能说话,所以看看测试看看我在做什么。在我当前的设置(Windows 64 位上的 Java 7 更新 21)中,此测试因 ArrayIndexOutOfBoundsException 而失败,但是用注释代码替换测试方法代码,它可以工作。我想知道 Java 规范中是否有任何部分可以解释原因。

在我看来,正如“michael nesterenko”所建议的那样,数组字段的值在调用方法之前被缓存在堆栈中,并且在调用返回时不会更新。我不知道它是 JVM 错误还是记录在案的“优化”。不涉及多线程或“魔法”。

public class TestAIOOB 
    private String[] array = new String[0];
    private int grow(final String txt) 
        final int index = array.length;
        array = Arrays.copyOf(array, index + 1);
        array[index] = txt;
        return index;
    
    @Test
    public void testGrow() 
        //final int index = grow("test");
        //System.out.println(array[index]);
        System.out.println(array[grow("test")]);
    

【问题讨论】:

只是一个猜测,当您从数组中调用grow 时,它已经在堆栈中,因此链接不会更新,但是如果您之前调用grow 然后使用索引,则会加载指向数组的链接在它更新之后,它就可以工作了。只是一个猜测。也许查看字节码可能会有所帮助 我不会称它为“缓存在堆栈中”。我认为这是一个Java(语言)问题。您正在引用一个名称,该名称在之前使用/应用与其关联的已解析值进行解析。因此,如果在两者之间重新分配名称,则您的值是错误的。 这是一个更简单的测试用例:public class TestAIOOB static Object[] array; static int reassign() array = new Object[] new Object() ; return 0; public static void main(String[] args) System.out.println(array[reassign()]); 。这抛出(就像它应该的那样),你问为什么。 值得一提的是,它不仅仅是Java。数组引用是在 javascript (example)、C# (example) 和 C (example) 中的 grow 调用之前评估的,可能还有其他的,但我没有尝试过。 (您可以在此处在线运行最后两个中的任何一个:compileonline.com) 【参考方案1】:

这由Java Language Specification 很好地定义:要评估x[y],首先评估x,然后评估y。在您的情况下,x 评估为具有零元素的String[]。然后,y 修改一个成员变量,并计算为0。尝试访问已返回数组的第 0 个元素失败。成员 array 更改的事实与数组查找无关,因为我们正在查看 String[] 在评估时引用的 array

【讨论】:

链接中的第一个要点。很棒。【参考方案2】:

这种行为是 JLS 规定的。根据15.13.1,“使用以下过程评估数组访问表达式:首先,评估数组引用表达式。如果此评估突然完成,那么数组访问会出于同样的原因而突然完成,并且不会评估索引表达式。否则,将评估索引表达式。[...]"。

【讨论】:

【参考方案3】:

使用javap -c TestAIOOB比较编译后的Java代码

未注释的代码:

public void testGrow();
  Code:
   0:   getstatic       #6; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   aload_0
   4:   getfield        #3; //Field array:[Ljava/lang/String;
   7:   aload_0
   8:   ldc     #7; //String test
   10:  invokespecial   #8; //Method grow:(Ljava/lang/String;)I
   13:  aaload
   14:  invokevirtual   #9; //Method java/io/PrintStream.println:(Ljava/lang/St
ing;)V
   17:  return

注释代码:

public void testGrow();
  Code:
   0:   aload_0
   1:   ldc     #6; //String test
   3:   invokespecial   #7; //Method grow:(Ljava/lang/String;)I
   6:   istore_1
   7:   getstatic       #8; //Field java/lang/System.out:Ljava/io/PrintStream;
   10:  aload_0
   11:  getfield        #3; //Field array:[Ljava/lang/String;
   14:  iload_1
   15:  aaload
   16:  invokevirtual   #9; //Method java/io/PrintStream.println:(Ljava/lang/Str
ing;)V
   19:  return

第一个getfield 发生在调用grow 之前,第二个发生在之后。

【讨论】:

这解释了行为,但没有解释为什么会这样编译。 JLS 里面一定有什么东西……

以上是关于JVM错误?缓存对象字段值导致 ArrayIndexOutOfBoundsException的主要内容,如果未能解决你的问题,请参考以下文章

因在缓存对象中增加字段,导致Redis出现反序列化失败的问题

因在缓存对象中增加字段,而导致Redis中取出缓存转化成Java对象时出现反序列化失败的问题

从缓存中读取查询导致在对象未定义上找不到字段 <field>

表单提交后,在输入中显示新值而不是缓存值

http缓存

5 一文看完flink的内存管理