性能文章>从一个Java数组的小测验浅看字节码指令>

从一个Java数组的小测验浅看字节码指令原创

https://a.perfma.net/img/2382850
9月前
524505

前两天在查看订阅的Oracle Java Magazine邮件时,发现了一个有趣的Java Quiz(小测验),该Quiz的网址请右转Java Magazine,这里我简单的贴一下题目:

This quiz looks at evaluation order, the assignment operator, and array access. Given the following code

int[] a = {3,2,1,0,4};
int[] b = {2,4,2,2,1};
System.out.print(a[(a=b)[2]]); // line n1
System.out.print(a[(a=b)[2]]); // line n2

A. Compilation fails at line n1.

B. The java.lang.ArrayIndexOutOfBoundsException is thrown at line n2.

C. 11

D. 12

E. 22

这个题目挺有意思的,因为我们平日里不会用到这种写法,所以我们也有可能不能第一眼就得出答案.

正确答案是D,这道题目的作者也巴拉巴拉讲了一堆来说明原因(因为太懒了就不逐句给大家翻译了,这里就表述下大概意思,想看原文请点上面的传送门),他们将整个表达式a[(a=b)[2]]分解为两部分:

1.  a[<expr>]
2.  (a=b)[2]

表达式的第一部分表示print语句中整个表达式的外部部分,它访问由引用变量a标识的数组元素,下标由表达式的值<expr>标识,而表达式的第二部分确定<expr>的值.

根据**Java Language Specification section 15.10.4 **“Run-time evaluation of array access expressions,”(即Java语言规范第15.10.4节“数组访问表达式的运行时求值”)中的规定:

At run time, evaluation of an array access expression behaves as follows:

  • First, the array reference expression is evaluated. If this evaluation completes abruptly, then the array access completes abruptly for the same reason and the index expression is not evaluated.
  • Otherwise, the index expression is evaluated. If this evaluation completes abruptly, then the array access completes abruptly for the same reason.
  • Otherwise, if the value of the array reference expression is null, then a NullPointerException is thrown.
  • Otherwise, the value of the array reference expression indeed refers to an array. If the value of the index expression is less than zero, or greater than or equal to the array’s length, then an ArrayIndexOutOfBoundsException is thrown.
  • Otherwise, the result of the array access is the variable of type T, within the array, selected by the value of the index expression.

规定中的大致意思呢就是说数组引用表达式在求值索引表达式之前先求值,即我们先确定表达式1中a的值,例如在line n1中,我们先确定表达式1中a指向数组{3,2,1,0,4};然后我们再求索引表达式<expr>的值,即表达式2中的值,此时发生了赋值操作a = b改变了a的指向,a的指向变成了{2,4,2,2,1},此时的a[2]也就变成了{2,4,2,2,1}中的下标为2的值,即expr为a[2]即2. 此时回到整体,我们输出的是表达式1中a确定的值的第expr(2)下标的元素,即{3,2,1,0,4}中的1,(至于Java是怎么实现这个规定的我们下面再探究).

而到了line n2行的时候,因为此时a已经指向了{2,4,2,2,1},所以我们在计算表达式1的时候a就已经是{2,4,2,2,1}了,再计算表达式2得到expr的值为2,最后输出2.


两位作者对原理以及规定作了一番解释,但是依然让人感到困惑. Java是如何实现的这种规定?表达式1中a的值又是如何做到不变的呢?这恐怕还是需要我们自己去探究一哈了.

先写一个相同的java类quiz1:

public class quiz1 {
    public static void main(String[] args) {
        int[] a = {3,2,1,0,4};
        int[] b = {2,4,2,2,1};
        System.out.print(a[(a=b)[2]]); // line n1
        System.out.print(a[(a=b)[2]]); // line n2
    }
}

使用javac -g quiz1.java来进行编译,然后对编译好后的class文件使用javap -c -v -l quiz1.class来查看字节码(因为字节码指令较多所以此处只粘贴main方法):

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=3, args_size=1
         0: iconst_5
         1: newarray       int
         3: dup
         4: iconst_0
         5: iconst_3
         6: iastore
         7: dup
         8: iconst_1
         9: iconst_2
        10: iastore
        11: dup
        12: iconst_2
        13: iconst_1
        14: iastore
        15: dup
        16: iconst_3
        17: iconst_0
        18: iastore
        19: dup
        20: iconst_4
        21: iconst_4
        22: iastore
        23: astore_1
        24: iconst_5
        25: newarray       int
        27: dup
        28: iconst_0
        29: iconst_2
        30: iastore
        31: dup
        32: iconst_1
        33: iconst_4
        34: iastore
        35: dup
        36: iconst_2
        37: iconst_2
        38: iastore
        39: dup
        40: iconst_3
        41: iconst_2
        42: iastore
        43: dup
        44: iconst_4
        45: iconst_1
        46: iastore
        47: astore_2
        48: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        51: aload_1
        52: aload_2
        53: dup
        54: astore_1
        55: iconst_2
        56: iaload
        57: iaload
        58: invokevirtual #3                  // Method java/io/PrintStream.print:(I)V
        61: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        64: aload_1
        65: aload_2
        66: dup
        67: astore_1
        68: iconst_2
        69: iaload
        70: iaload
        71: invokevirtual #3                  // Method java/io/PrintStream.print:(I)V
        74: return
      LineNumberTable:
        line 9: 0
        line 10: 24
        line 11: 48
        line 12: 61
        line 13: 74
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      75     0  args   [Ljava/lang/String;
           24      51     1     a   [I
           48      27     2     b   [I

是不是看上去挺让人头大的,别担心,让我们来逐步讲解:

  • 首先我们查看字节码指令会发现,0-23行与24-47行的指令是高度相似的. 简单的说这两块就是分别newarray了数组a和b(1: newarray int25: newarray int),并给他们的下标0,1,2,3,4进行了赋值(例如4: iconst_05: iconst_36: iastore就是给a的下标0位置赋值为3).
    需要注意的是两块的最后一句指令astore_1astore_2,这两句指令分别把a、b存入了局部变量表(Local Variable)的下标为1和2的地方,我们看字节码输出即可看到:

     LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      75     0  args   [Ljava/lang/String;
               24      51     1     a   [I
               48      27     2     b   [I
    

    此处大量用到dup指令对操作数栈(Operand Stack)栈顶元素进行复制是因为newarray创建的obj在被压入栈后,会由于后续的一些操作如4: iconst_05: iconst_36: iastore而导致obj出栈,而出栈后无法再对该array对象进行操作,所以要使用dup指令复制一份obj压入栈顶来提供后续对array的操作,此处不是我们探究的重点,一笔带过即可.

  • 我们继续往下走,走到第51、52行的指令:

    51: aload_1    //将第本地变量中的下标为1的变量推送至栈顶(引用类型,即数组a)
    52: aload_2    //将第本地变量中的下标为2的变量推送至栈顶(引用类型,即数组b)
    

    如图所示:1.png

  • go on

    53: dup		//复制栈顶元素b并压入栈中
    54: astore_1    //将栈顶元素保存到局部变量表下标1的位置
    

    如图所示:2.png

  • go on

    55: iconst_2	//将2压入栈顶
    56: iaload      //将指定数组中特定位置的变量加载到栈上
    

    如图所示:3.png

  • go on

    57: iaload     //同理 再输出a[2](a为操作数栈上的a)
    

    如图所示:4.png

此时一连串的字节码指令看下来,我们才发现line n1能输出1和Java实现数组规范的真正原因在于字节码指令的控制,在于虚拟机栈与栈帧的逻辑。

同理,我们继续看64-70行的字节码指令,就会发现因为局部变量表中a、b都已经是指向{2,4,2,2,1}了,所以

64: aload_1 65: aload_2压入栈中的值都是{2,4,2,2,1},所以运行到最后输出的就是2了.

Finally,最后整体输出的结果就是12.

当然我还发现了一个有意思的地方,如果我们使用IDEA查看quiz1.class会发现,line n2的输出直接就变成了b[b[2]],看来Java还有更多的优化在里面:

public class quiz1 {
    public quiz1() {
    }

    public static void main(String[] args) {
        int[] a = new int[]{3, 2, 1, 0, 4};
        int[] b = new int[]{2, 4, 2, 2, 1};
        System.out.print(a[b[2]]);
        System.out.print(b[b[2]]);
    }
}

这里浅谈一下字节码指令啦,其实字节码指令挺有意思的,如果后续有时间会继续写关于字节码的文章(挖坑,以前的坑一堆没填的呢).

PS:更多的Quiz可以访问传送门,里面有很多关于Java的小测验很有意思,很多都可以拿出来单独写一篇文章,我愿称之为你真的了解Java吗系列,大家感兴趣可以去翻一翻.

分类:
标签:
请先登录,再评论

暂无回复,快来写下第一个回复吧~

为你推荐

字符串字面量长度是有限制的
前言 偶然在一次单元测试中写了一个非常长的字符串字面量。 正文 在一次单元测试中,我写了一个很长的字符串字面量,大概10万个字符左右,编译时,编译器给出了异常告警 `java: constant
多次字符串相加一定要用StringBuilder而不用-吗?
今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144): ``` package test; public c
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和
「每日五分钟,玩转 JVM」:久识你名,初居我心
聊聊 JVMJVM,一个熟悉又陌生的名词,从认识Java的第一天起,我们就会听到这个名字,在参加工作的前一两年,面试的时候还会经常被问到JDK,JRE,JVM这三者的区别。JVM可以说和我们是老朋友了
据说99.99%的人都会答错的类加载的问题
概述首先还是把问题抛给大家,这个问题也是我厂同学在做一个性能分析产品的时候碰到的一个问题。 同一个类加载器对象是否可以加载同一个类文件多次并且得到多个Class对象而都可以被java层使用吗请仔细注意
Java多线程——并发测试
编写并发程序时候,可以采取和串行程序相同的编程方式。唯一的难点在于,并发程序存在不确定性,这种不确定性会令程序出错的地方远比串行程序多,出现的方式也没有固定规则。那么如何在测试中,尽可能的暴露出这些问
Java多线程知识小抄集(一)
本文主要整理笔者遇到的Java多线程的相关知识点,适合速记,故命名为“小抄集”。本文没有特别重点,每一项针对一个多线程知识做一个概要性总结,也有一些会带一点例子,习题方便理解和记忆。 1.interr