java字节码解读

2020-11-29  本文已影响0人  _火山_

什么是字节码文件

字节码文件就是以字节(1字节=8位)为最小存储单位的文件;
我们平常开发的java代码,其存储文件都是以.java为后缀的;
而这些以.java为后缀的文件是给开发者看的,机器无法识别这些.java文件,所以在交由机器执行前,我们需要将这些.java文件通过编译器编译成.class文件,而这些编译后的.class文件就是我们常说的字节码文件;
然后jvm在加载类或接口信息时通过查找.class文件将其读入内存并构造klassinstance对象。

如何获取字节码文件

我们怎样才可以得到一个字节码文件呢?

如果我们使用idea开发工具来编写代码的话,可以通过idea的编译功能便可以对.java文件进行编译,得到对应的.class文件;
如果使用的是像vim这样的文本编辑器来编写.java文件,那么我们可以使用jdk提供的工具->javac来对.java文件进行编译,javac命令的用法为:
javac sourcefile
例如:javac /tmp/Test.java
.java文件经过编译后便可得到.class文件,即字节码文件。

字节码文件到底长什么呢?
java字节码文件是一种按十六进制格式存储的文件,所以我们编译后直接使用vim编辑器打开.class文件的话,看到的是一堆乱码;
那这个.class文件的原貌是什么样的呢?
既然它是一种十六进制文件,那么我们可以选择使用十六进制文件编辑器来打开它,看看它的原貌是什么样的:
可以下载一个MadEdit编辑器:https://sourceforge.net/projects/madedit/(windows版本)
使用MadEdit打开的.class文件是下图这样的:

字节码文件原貌

左边红框表示字节码文件的实际内容,可以看到它们确实是十六进制内容;右边的红框就是我们通过vim编辑器打开时看到的乱码。

如果想要更加详细地解读字节码文件,还可以通过jclasslib工具来结构化地展示class文件的信息;
jclasslib下载地址:https://github.com/ingokegel/jclasslib

为什么需要字节码文件

这需要从java语言的平台无关性以及jvm的语言无关性这两大特性说起。
在虚拟机出现之前,程序要想正确运行在计算机上,首先要将代码编译成计算机能够正确识别的二进制本地机器码;
而这个编译过程是和平台的操作系统、CPU指令集密切相关的,也就是说在不同的操作系统、不同的cpu指令集下编译得到的结果不一定是一致的;
所以在没有虚拟机之前,某种平台下编译通过且能够正常运行的二进制机器码,换到另一种平台上,可能就无法运行了,这样就有很大的局限性了。
但是java虚拟机的出现打破了这一僵局,程序代码不再需要直接编译成计算机可识别的本地机器码了,而是先编译成java虚拟机可识别的字节码,然后再由java虚拟机加载并转换为计算机可识别的指令,java虚拟机就相当于一个兼容各种平台的媒介,能够将程序代码编译得到的字节码文件转换为计算机可识别的机器码,字节码文件并不需要直接跟计算机打交道;
这体现了java语言的平台无关性,而这个特性是基于java虚拟机实现的。

再者,java虚拟机技术发展至今已经非常成熟,其不仅可以支持运行java语言编写的程序,还支持其他语言如Groovy、Scala等语言编写的程序,这些语言编写的程序通过各自的编译器编译成为字节码文件后,可以被java虚拟机识别并转换为计算机可执行的机器码;
也就是说不同的语言编写的程序,编译成字节码文件后,就可通过java虚拟机这个平台来运行,这体现了java虚拟机的语言无关性,并不是只有java语言编写的程序才能跑在jvm上。

图片来源于:https://zhuanlan.zhihu.com/p/45003974

java语言的平台无关性以及JVM的语言无关性都是基于java虚拟机实现的,而java虚拟机又只认字节码文件,所以我们编写的程序需要先编译成字节码文件。

字节码文件构成以及术语解读

参考文章:
https://zhuanlan.zhihu.com/p/44657693
https://zhuanlan.zhihu.com/p/44694290
https://zhuanlan.zhihu.com/p/45003974
https://blog.csdn.net/chenzhao2013/article/details/53917239
https://blog.csdn.net/zuoyouzouzou/article/details/91346848(总结得非常详细)

图片来源于:https://blog.csdn.net/zuoyouzouzou/article/details/91346848

阅读字节码文件的前提

得先对java虚拟机的内存划分以及其中的java虚拟机栈和方法区有一定的了解,接下来简要解释一下这三个方面。

java虚拟机的划分如下图所示:


每一个方法调用开始都会伴随着一个栈帧在java虚拟机栈的入栈,每一个方法调用结束都会伴随着一个栈帧在java虚拟机栈的出栈;
也就是说栈帧是方法调用过程使用到的数据结构,它分为四个部分:局部变量表、操作数栈、动态链接和返回地址。

局部变量表
局部变量表用于存储在方法调用过程中创建的局部变量以及方法参数,它是一个数组结构,每一个方法的局部变量表的容量在编译期就会被确定。

因为每一个方法调用都会对应一个栈帧,在开始执行一个方法之前,该方法对应的栈帧就会被创建好并被push到java虚拟机栈中。

如果该方法带参数的话,在开始执行方法之前,参数值会被存放到栈帧的局部变量表中。

操作数栈
操作数栈是栈帧的另一个组成部分,它是一个栈结构,元素进出的顺序是先进后出。
在方法的执行过程中,jvm会将局部变量表中的元素读取到操作数栈中,当执行某个操作时,再从操作数栈栈顶弹出数据进行相关操作;
如现在需要做加法运算:

int a = 1;
int b = 2;
int c = a + b;

局部变量表以及操作数栈的变化过程如下:

常量数值或者对象引用在操作过程中需要加载到操作数栈暂存,然后在进行相关的操作时,需要从操作数栈弹出相应的元素,经过计算后,存储到局部变量表相应的区域中。

动态链接
java虚拟机栈的栈帧数据结构里有一项存储的是当前所执行方法调用的其他方法的符号引用地址,而该符号引用是存储在方法区的运行时常量池中,存储该该方法的符号引用目的是为了在程序运行过程中将该方法(即被本方法调用的方法)的符号引用解析为直接引用,由于这个过程是动态的,所以称这种解析方式为动态链接。

既然有动态链接,那肯定就有与它相对应的符号引用解析方式,这种方式叫做静态解析。

这两种方式有什么不一样的地方呢?

静态解析针对的是在代码编译期间就可以确定的方法,而动态链接呢则相反,在编译期间无法确定被调用方法的具体实现,只能知道会调用这个方法,所以就先通过符号引用来标识;
因为在有方法被重写的场景下,某个方法可能有多种实现(父类实现与子类实现),具体使用哪一个在编译期间确定不了,所以通过动态链接的方式在程序运行期间确定。

返回地址
一个方法被调用结束后,需要返回到被调用的位置,而确定该位置的地址信息就是由栈帧的返回地址区域记录的。
该地址值的确认有两种方式:
1)将调用者线程对应的程序计数器存储的值(即当前方法调用指令的下一条指令的地址)作为返回地址,这种情况对应的是方法正常退出(即正常执行结束后退出);
2)通过异常表确定返回地址,这种情况对应的是异常退出,异常退出是指方法向调用者抛出异常,自己不处理异常;如果方法调用过程发生了异常,但是自己捕获并处理了,这最终还是属于正常退出。

本质上,方法的退出就是当前栈帧出栈。此时,需要恢复上层方法的局部变量、操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。

例如现有如下代码:
private void B() {}
    
public void set(int i)  {
    B();
}

在方法set中调用方法B,其对应的字节码指令如下:
方法set中调用方法B的指令为invokespecial #2 // Method B:()V,对应的地址为指令行号为1,那么【当前方法调用指令的下一条指令的地址】则为4,而不是1了,你试想,如果记录的还是invokespecial指令的地址,那岂不是下次执行指令执行的又将是invokespecial指令,显然这不合理,被调用方法的正常返回地址记录的应是方法调用指令的下一条指令对应的地址。

正常完成的出口和异常完成的出口区别在于:通过异常完成出口退出的不会给它的上层调用者产生任何的返回值。

方法区
首先我们需要明确一个概念,我们一直说的方法区其实是jvm的一个组成部分的规范,而不是具体实现。

随着jvm的发展,在jdk8这个版本上,hotspot对方法区的具体实现做了重要的调整:
在jdk8之前,hotspot对方法区的具体实现为永久代,即perm区;
在jdk8及之后,hotspot对方法区的具体实现为元空间,即metaspace。

永久代与元空间有什么不同呢?
1)在jdk1.7及之前,jvm堆内存=新生代+老生代+永久代,即永久代使用的内存是堆内存的一部分,也就是说永久代的大小其实是受限于jvm的内存大小的,且永久代也受java GC管理;

2)而元空间使用的内存资源则是独立于分配给jvm的内存资源之外的,是物理计算机的本地内存资源,所以理论上来说只要物理机剩余内存有多大,方法区就能有多大;
虽说元空间的大小理论上可以达到剩余物理内存的上限,但实际使用中我们肯定不能任由它如此,需要合理设置元空间的大小。

3)在jdk1.7及之前,hotspot对方法区的实现都是永久代,但是其实jdk1.7的永久代与jdk1.7之前的还是有一定区别:
在hotspot实现中,jdk1.7之前,字符串常量池是放在方法区中的;

而在jdk1.7版本中,hotspot将字符串常量池从方法区中剥离出来,将其放到了堆中;

而到了jdk1.8及之后,hotspot将元空间作为方法区的实现,取代了永久代,但是字符串常量池依然跟jdk1.7一样,还是放在堆中。

运行时常量池呢,则是一直都是放在方法区中的,在jdk1.8之前,是放在方法区的对应实现永久代中,而在jdk1.8及之后,则是放到元空间中了。

运行时常量池
我们应该经常听到运行时常量池这个说法,但是除了运行时常量池之外,其实还有一个概念叫做静态常量池,也就是.java代码编译后得到的.class文件中constant pool表项。

每一个java类都会被编译得到唯一对应的一个.class文件(字节码文件),每个字节码文件都会有一个constant pool表项。

字节码文件常量池(Constant pool)的作用是存储编译生成的各种字面量和符号引用
字面量是指按字面值表示的常量,如字符串、final修饰的基本数据类型值;
而符号引用是指:
1、类和接口的全限定名
即包名.类名或者包名.接口名。
2、字段的名称和描述符
字段的名称就是字面上的意思,指的是变量名,而字段描述符呢指的是字段的类型。
3、方法的名称和描述符
方法名称也是字面上的意思,指的是方法的名字,而方法描述符指的是方法的参数类型+方法的返回值类型。

我们编译java代码得到字节码文件后,程序未启动之前,字节码文件还存在于磁盘中,一般不会发生变化,所以我们可以理解字节码常量池为静态常量池,而在程序启动之后,字节码文件会被加载进jvm内存中,字节码文件的常量池信息自然也会被加载进去,它就在方法区中被构建成我们所说的运行时常量池,运行时常量池就是字节码文件常量池的程序运行时的表现形式,每一个字节码文件都会有一个常量池,所以每加载一个字节码文件,都会在方法区内存中构建一个与之对应的运行时常量池。

开始阅读一个完整的字节码文件

如果想直接阅读字节码文件可以参考文章:https://blog.csdn.net/zuoyouzouzou/article/details/91346848(总结得非常详细)。

本节是针对字节码文件的反编译结果进行解读。

java源代码如下:
Test.java

public class Test {


    public int getValue(int var){
        try {
            var += 10;
            return var;
        } catch (Exception e){
            var += 20;
            return var;
        } finally {
            var += 30;
            return var;
        }
    }


    public static void main(String[] args) {

        Test test = new Test();
        int value = test.getValue(10);
        System.out.println(value);

    }
}

那如何得到字节码文件的反编译结果呢?
通过javap命令来实现,我们先看看javap命令的用法:

通过上图可知,执行javap -c Test.class文件路径就可以得到字节码文件的反编译结果,如果想要得到更加详细的信息,可以加上参数-l和-v

我切换到Test.class文件所在目录,执行javap -cvl Test.class,得到Test.class文件的反编译结果如下:

Last modified 2020-11-29; size 810 bytes
  MD5 checksum 9d589d877b3f558df720e52b172750cb
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#30         // java/lang/Object."<init>":()V
   #2 = Class              #31            // java/lang/Exception
   #3 = Class              #32            // Test
   #4 = Methodref          #3.#30         // Test."<init>":()V
   #5 = Methodref          #3.#33         // Test.getValue:(I)I
   #6 = Fieldref           #34.#35        // java/lang/System.out:Ljava/io/PrintStream;
   #7 = Methodref          #36.#37        // java/io/PrintStream.println:(I)V
   #8 = Class              #38            // java/lang/Object
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               LTest;
  #16 = Utf8               getValue
  #17 = Utf8               (I)I
  #18 = Utf8               e
  #19 = Utf8               Ljava/lang/Exception;
  #20 = Utf8               var
  #21 = Utf8               I
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               test
  #27 = Utf8               value
  #28 = Utf8               SourceFile
  #29 = Utf8               Test.java
  #30 = NameAndType        #9:#10         // "<init>":()V
  #31 = Utf8               java/lang/Exception
  #32 = Utf8               Test
  #33 = NameAndType        #16:#17        // getValue:(I)I
  #34 = Class              #39            // java/lang/System
  #35 = NameAndType        #40:#41        // out:Ljava/io/PrintStream;
  #36 = Class              #42            // java/io/PrintStream
  #37 = NameAndType        #43:#44        // println:(I)V
  #38 = Utf8               java/lang/Object
  #39 = Utf8               java/lang/System
  #40 = Utf8               out
  #41 = Utf8               Ljava/io/PrintStream;
  #42 = Utf8               java/io/PrintStream
  #43 = Utf8               println
  #44 = Utf8               (I)V
{
  public Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LTest;

  public int getValue(int);
    descriptor: (I)I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=5, args_size=2
         0: iinc          1, 10
         3: iload_1
         4: istore_2
         5: iinc          1, 30
         8: iload_1
         9: ireturn
        10: astore_2
        11: iinc          1, 20
        14: iload_1
        15: istore_3
        16: iinc          1, 30
        19: iload_1
        20: ireturn
        21: astore        4
        23: iinc          1, 30
        26: iload_1
        27: ireturn
      Exception table:
         from    to  target type
             0     5    10   Class java/lang/Exception
             0     5    21   any
            10    16    21   any
            21    23    21   any
      LineNumberTable:
        line 6: 0
        line 7: 3
        line 12: 5
        line 13: 8
        line 8: 10
        line 9: 11
        line 10: 14
        line 12: 16
        line 13: 19
        line 12: 21
        line 13: 26
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           11      10     2     e   Ljava/lang/Exception;
            0      28     0  this   LTest;
            0      28     1   var   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #3                  // class Test
         3: dup
         4: invokespecial #4                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: bipush        10
        11: invokevirtual #5                  // Method getValue:(I)I
        14: istore_2
        15: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_2
        19: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
        22: return
      LineNumberTable:
        line 20: 0
        line 21: 8
        line 22: 15
        line 24: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  args   [Ljava/lang/String;
            8      15     1  test   LTest;
           15       8     2 value   I
}
SourceFile: "Test.java"

下面对字节码文件的反编译结果进行解析:
通过oracle的文档可以查阅每个指令助记符的含义:https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-6.html#jvms-6.5.dup

Last modified 2020-11-29; size 810 bytes #指的是编译时间。
MD5 checksum 9d589d877b3f558df720e52b172750cb #指的是字节码文件的md5值,你也可以使用md5sum命令亲自获取字节码文件的md5值验证下。
Compiled from "Test.java" #字节码文件对应的是那个java类文件
public class Test #被编译的类
  minor version: 0 #每个字节码文件都会有minor version和major version这两个版本号,叫做小版本号和大版本号,这个是小版本号
  major version: 49 #大版本号
  flags: ACC_PUBLIC, ACC_SUPER #访问标识,指的是类的访问权限修饰符,ACC_PUBLIC指的是这个类是public修饰符修饰的,而ACC_SUPER表示该类是一个子类,它有父类
Constant pool: #这个就是java代码编译后得到的字节码文件的常量池,这里表明该常量池一共有44项常量数据,从#1~#44,每一项前面的#i表示的是常量表的元素索引,即数组下标,等号右边的每一个常量元素的数据结构类型,共有五种类型:Utf8、Methodref、Fieldref、Class和NameAndType,第三列表示常量池中每个常量项存储的值,如果是#i的话,表示存储的是地址,指的是常量项索引值,否则就是字面量或者方法的描述符等,第四列则表示的是在常量项值为索引值的情况下,根据地址解析得到的实际的值。jvm加载字节码文件时,这个常量池信息同时会被加载到内存,被构造成我们常说的运行时常量池。
   #1 = Methodref          #8.#30         // java/lang/Object."<init>":()V
   #2 = Class              #31            // java/lang/Exception
   #3 = Class              #32            // Test
   #4 = Methodref          #3.#30         // Test."<init>":()V
   #5 = Methodref          #3.#33         // Test.getValue:(I)I
   #6 = Fieldref           #34.#35        // java/lang/System.out:Ljava/io/PrintStream;
   #7 = Methodref          #36.#37        // java/io/PrintStream.println:(I)V
   #8 = Class              #38            // java/lang/Object
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               LTest;
  #16 = Utf8               getValue
  #17 = Utf8               (I)I
  #18 = Utf8               e
  #19 = Utf8               Ljava/lang/Exception;
  #20 = Utf8               var
  #21 = Utf8               I
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               test
  #27 = Utf8               value
  #28 = Utf8               SourceFile
  #29 = Utf8               Test.java
  #30 = NameAndType        #9:#10         // "<init>":()V
  #31 = Utf8               java/lang/Exception
  #32 = Utf8               Test
  #33 = NameAndType        #16:#17        // getValue:(I)I
  #34 = Class              #39            // java/lang/System
  #35 = NameAndType        #40:#41        // out:Ljava/io/PrintStream;
  #36 = Class              #42            // java/io/PrintStream
  #37 = NameAndType        #43:#44        // println:(I)V
  #38 = Utf8               java/lang/Object
  #39 = Utf8               java/lang/System
  #40 = Utf8               out
  #41 = Utf8               Ljava/io/PrintStream;
  #42 = Utf8               java/io/PrintStream
  #43 = Utf8               println
  #44 = Utf8               (I)V
{
  public Test(); #构造方法
    descriptor: ()V #方法的描述符,其形式为方法参数+方法返回值类型,()表示参数为空,V表示返回值为空。
    flags: ACC_PUBLIC #访问标识,标识该方法的访问权限为public
    Code: #方法的属性,这是字节码文件层面的概念,与java类中的属性不是一个概念。
      stack=1, locals=1, args_size=1 #stack表示该方法的java虚拟机栈的最大栈深、locals表示局部变量表的最大容量、args_size表示方法的参数个数,可是刚才不是说方法参数为空吗?这里为什么args_size还等于1,那是因为对于非静态方法而言,其最少有一个隐藏参数,就是this引用。
         0: aload_0 #aload_i的含义可以查阅上面贴出oracle文档,它以a开头,表示加载的是引用类型数据,后面的0表示加载局部变量表的第0个索引处存储的引用。
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V #表示调用方法,后面的#1表示常量池的索引,通过索引即可查找具体调用的是哪个方法。
         4: return #方法返回
      LineNumberTable: #行号表
        line 1: 0 #字节码指令号码与java代码行号的对应关系。
      LocalVariableTable: #局部变量表
        Start  Length  Slot  Name   Signature #元素占用内存的偏移量、元素大小(字节)、元素索引、元素(变量)名、变量类型
            0       5     0  this   LTest;

  public int getValue(int); #方法
    descriptor: (I)I #方法描述符,(I)表示有一个int类型的参数,后面的I表示返回值类型是int类型。
    flags: ACC_PUBLIC #访问标识,标识访问修饰符是public。
    Code: #字节码层面的方法属性
      stack=1, locals=5, args_size=2 #方法栈帧的最大栈深为1、局部变量表的最大容量为5、方法参数为2,一个是this引用,另一个是int类型参数。
         0: iinc          1, 10 #iinc指令的作用是直接对局部变量表的元素进行操作,一般是用于自增或者自减操作中,它是唯一一个可以直接对局部变量表的元素进行计算的指令,后面的1,10表示对局部变量表的索引1处的值加10。
         3: iload_1 #iload_1以i开头,表示加载int类型数据,iload_1表示加载局部变量表索引1处的变量值到操作数栈。
         4: istore_2 #弹出栈顶int类型元素,将其存储到局部变量表的索引2处,对应try代码块的return操作。
         5: iinc          1, 30 #对局部变量表索引1处的值加30。我们通过java代码可以知道,只有finally代码块处才有加30的操作,说明现在执行的是finally代码块。编译器在编译时会将finally代码块拷贝到try代码块和catch代码块的后面进行编译,所以才会实现不管是执行了try代码块或者是执行了catch代码块,最终都总会执行finally代码块,因为经过编译后,try和catch代码块后面都包含了finally代码块的实现逻辑。
         8: iload_1 #加载局部变量表索引1处的int类型数据到操作数栈。
         9: ireturn #弹出栈顶int类型元素,返回
        10: astore_2 #将一个异常对象引用存储到局部变量表的索引2处
        11: iinc          1, 20 #对局部变量表索引1处的值加20
        14: iload_1 #加载局部变量表索引1处的int类型数据到操作数栈
        15: istore_3 #弹出栈顶的int元素,将其存储到局部变量表的索引3处,对应catch代码块的return操作。
        16: iinc          1, 30 #这里由出现了加30的操作,这个是被拷到catch代码块之后的finally代码。
        19: iload_1 #加载局部变量表索引1处的int类型数据到操作数栈
        20: ireturn #弹出栈顶元素返回,对应java代码的finally代码块的return操作。
        21: astore        4 #存放异常对象引用到局部变量表的索引4处,不管有多少个异常,其后都会跟着finally代码,所以finally代码块才会被称为总会执行代码块。
        23: iinc          1, 30 #finally代码块的自增操作,对局部变量表索引1处的int类型值加30。
        26: iload_1 #加载局部变量表1处的int元素到操作数栈
        27: ireturn #弹出栈顶int元素,返回
      Exception table: #异常表
         from    to  target type # from、to表示监控的指令范围,target表示如果发生了异常,那么将会跳到的位置,type表示异常类型。
             0     5    10   Class java/lang/Exception
             0     5    21   any #any表示任意异常。
            10    16    21   any
            21    23    21   any
      LineNumberTable: #行号表
        line 6: 0 #字节码指令号:java代码行号
        line 7: 3
        line 12: 5
        line 13: 8
        line 8: 10
        line 9: 11
        line 10: 14
        line 12: 16
        line 13: 19
        line 12: 21
        line 13: 26
      LocalVariableTable: #局部变量表
        Start  Length  Slot  Name   Signature
           11      10     2     e   Ljava/lang/Exception; #变量类型,其中L表示引用类型,后面跟着的java/lang/Exception表示该引用类型具体是哪种类型。
            0      28     0  this   LTest;
            0      28     1   var   I #int类型的变量。

  public static void main(java.lang.String[]); #main方法
    descriptor: ([Ljava/lang/String;)V #描述符,([Ljava/lang/String;)表述参数是一个String数组,[表示数组,L表示引用类型,java/lang/String;表示具体是String类型的引用,合起来就是String数组;V表示方法的返回类型是void,无返回值。
    flags: ACC_PUBLIC, ACC_STATIC #表示访问标识,ACC_PUBLIC表示public修饰,ACC_STATIC标识static修饰。
    Code: #字节码层面的方法属性
      stack=2, locals=3, args_size=1 #main方法的最大栈深、局部变量表最大容量,参数个数;因为这里main方法是static方法,所以不会有this引用,所以其参数个数与描述符中指定的参数个数一致。
         0: new           #3                  // class Test   new指令创建一个对象, #3是常量池索引,通过该索引可以知道要创建的对象类型是什么。
         3: dup #创建了对象后,对象的内存地址会被压入栈顶,dup指令的作用是赋值栈顶元素,并压入栈顶,即此时栈中会存在两个同样的地址,这样做的目的是因为后续的初始化需要使用一个地址,而给对象引用赋值又需要使用一个地址,所以创建了一个对象后,栈中需要存在两个地址,所以才会做这个复制并入栈的操作。
         4: invokespecial #4                  // Method "<init>":()V #执行invokespecial调用方法,它需要参数,弹出栈顶元素的对象地址,整体的含义是调用该对象(Test)的<init>:()V方法进行初始化,此时栈中还剩一个该对象的地址值;由Test的构造方法可知,在执行new指令调用构造方法时已经对Test类的父类Object类初始化过了,到这里才进行Test类的初始化,从这里可以说明java父类的初始化是在子类之前的,父类初始化之后,才到子类初始化。
         7: astore_1 #弹出栈顶的对象地址,将其存储到局部变量表的索引1处,即将对象地址赋给对象引用。
         8: aload_1 #将局部变量表索引1处存储的该对象的地址加载到操作数栈
         9: bipush        10 #加载int类型值10到操作数栈
        11: invokevirtual #5                  // Method getValue:(I)I #调用方法,此时会构建一个新的栈帧,main方法栈帧的栈顶元素10会被弹出并被放入getValue方法栈帧的局部变量表中,通过上一条指令加载到操作数栈的对象地址也会出栈,这样才知道调用的是哪个对象的方法。
        14: istore_2 #方法调用结束后,会执行该条指令,如果方法正常返回,该条指令地址就是getValue方法的返回地址,它将getValue方法的返回地址存储到局部变量表的索引2处。
        15: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream; #从常量池中获取java/io/PrintStream类的System.out对象引用的值,并放入操作数栈:。
        18: iload_2 #加载局部变量表索引2处的int类型值到操作数栈
        19: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V #弹出操作数栈的int类型值和System.out对象引用,通过invokevirtual调用调用常量池索引#7表示的方法。
        22: return #方法返回,方法调用结束
      LineNumberTable:
        line 20: 0
        line 21: 8
        line 22: 15
        line 24: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  args   [Ljava/lang/String;
            8      15     1  test   LTest;
           15       8     2 value   I
}
SourceFile: "Test.java" #字节码文件层面的类属性,属性名是SourceFile,属性值是Test.java,表示的含义是该字节码文件对应的源文件是Test.java文件。
上一篇下一篇

猜你喜欢

热点阅读