JAVA 字节码文件分析

​​​​Java不只是一种编程语言,还是一个完整的操作平台。Java之所以可以跨平台,这离不开JVM虚拟机

JVM是一个软件,在不同的平台上,JVM有不同的版本。Java在编译之后会生成一种.class文件,这种文件成为字节码文件。JVM虚拟机就是将Java编译后的.class文件翻译成特定平台下的机器码,然后运行。也就是说,在不同平台上装上平台对应的JVM虚拟机后,就可以将Java字节码文件转换,然后运行我们的Java程序。

值得注意的是,Java编译后的结果是生成字节码,而不是机器码。字节码是不可以直接运行的,必须通过JVM再次翻译成机器码才可以运行。即使是将Java程序打包成可执行文件,也仍然需要JVM的支持才可以运行。

跨平台的是Java程序,而不是JVM。JVM是用C/C++开发的,不能平台,不同的平台下JVM的版本是不同的。

字节码文件,有什么用?

  1. JVM虚拟机的特点:一处编译,多处运行。

  2. 多处运行,靠的是.class 字节码文件。

  3. JVM本身,并不是跨平台的。Java之所以跨平台,是因为JVM本身不夸平台。

  4. 二进制的文件,显然不是给人看的。是给机器看的。

  5. 从根源了解了之后,返回到语言层次 好多都会豁然开朗。

Java语言规范补充:

JVM虚拟机规范(相对底层的)Java,Groovy,kotlin,Scala。 编译后都是Class文件,所以就都能在JVM虚拟机上运行。

 

字节码文件解读

一个Java类,然后进行编译成字节码文件

package com.dawa.jvm.bytecode; 
public class MyTest1 { 
    private int a = 1; 
    public int getA() { return a; } 
    public void setA(int a) { this.a = a; } 
}

javap 编译后的结果:

➜  main javap com.dawa.jvm.bytecode.MyTest1   
Compiled from "MyTest1.java"
public class com.dawa.jvm.bytecode.MyTest1 {
  public com.dawa.jvm.bytecode.MyTest1();
  public int getA();
  public void setA(int);
}

Java -c 编译后的结果:

➜  main javap -c com.dawa.jvm.bytecode.MyTest1 
Compiled from "MyTest1.java"
public class com.dawa.jvm.bytecode.MyTest1 {
  public com.dawa.jvm.bytecode.MyTest1();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: aload_0
       5: iconst_1
       6: putfield      #2                  // Field a:I
       9: return

  public int getA();
    Code:
       0: aload_0
       1: getfield      #2                  // Field a:I
       4: ireturn

  public void setA(int);
    Code:
       0: aload_0
       1: iload_1
       2: putfield      #2                  // Field a:I
       5: return
}

javap -verbose编译后的结果

 ➜   main javap -verbose com.dawa.jvm.bytecode.MyTest1 
Classfile /Users/shangyifeng/work/workspace/jvm_leature/build/classes/java/main/com/dawa/jvm/bytecode/MyTest1.class
  Last modified 2020-2-14; size 489 bytes
  MD5 checksum 952635139a8b5b42f0142d033929d8c2
  Compiled from "MyTest1.java"
public class com.dawa.jvm.bytecode.MyTest1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."":()V
   #2 = Fieldref           #3.#21         // com/dawa/jvm/bytecode/MyTest1.a:I
   #3 = Class              #22            // com/dawa/jvm/bytecode/MyTest1
   #4 = Class              #23            // java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/dawa/jvm/bytecode/MyTest1;
  #14 = Utf8               getA
  #15 = Utf8               ()I
  #16 = Utf8               setA
  #17 = Utf8               (I)V
  #18 = Utf8               SourceFile
  #19 = Utf8               MyTest1.java
  #20 = NameAndType        #7:#8          // "":()V
  #21 = NameAndType        #5:#6          // a:I
  #22 = Utf8               com/dawa/jvm/bytecode/MyTest1
  #23 = Utf8               java/lang/Object
{
  public com.dawa.jvm.bytecode.MyTest1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 3: 0
        line 4: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/dawa/jvm/bytecode/MyTest1;

  public int getA();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field a:I
         4: ireturn
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/dawa/jvm/bytecode/MyTest1;

  public void setA(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #2                  // Field a:I
         5: return
      LineNumberTable:
        line 11: 0
        line 12: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/dawa/jvm/bytecode/MyTest1;
            0       6     1     a   I
}
SourceFile: "MyTest1.java"

字节码整体结构分解

  • 整体结构

     JAVA 字节码文件分析_第1张图片

  1. 魔数
  2. 版本号
  3. 常量池数-1
  4. 常量池数组(常量池中的每个常量的具体信息)
  5. 当前类的访问控制权限(private,public , pro , 等几种 当前类的标识符)
  6. 当前类的名字
  7. 父类的名字
  8. 当前类的接口信息
  9. 当前类的成员变量的信息
  10. 当前类的方法的信息
  11. 当前类附加的属性

上面一张图说明了字节码所有的事情

JAVA 字节码文件分析_第2张图片

JAVA 字节码文件分析_第3张图片

  • Class字节码中有两种数据类型
  1. 字节数据直接量:这是基本的数据类型。共细分为u1、u2、u4、u8四种,分表代表连续的1个字节、2个字节、4个字节、8个字节组成的整体数据。
  2. 表(数组):表是由多个基本数据或其他表,按照既定顺序组成的大的数据集合。表是由结构的,它的结构体现在:组成表的成分所在的位置和顺序都是已经严格定义好的。

常量池深入分析

借助工具:Hex_fiend(mac) 查看16进制的文件

用工具打开的二进制文件:16进制

4字节展示格式

JAVA 字节码文件分析_第4张图片

单字节展示格式

JAVA 字节码文件分析_第5张图片

使用javap -verbose 命令分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法、类中的方法信息、类变量与成员变量等信息。

  • 魔数:所有的.class字节码文件的前4个字节都是魔数,魔术值为固定值:0xCAFEBABE
  • 版本号:魔术之后的4个字节为jdk版本信息,前两个表示此版本号,后两个表示主版本号,这里的00 00 00 34,换成十进制,表示此版本为0,主版本号为52,该文件的版本号为 1.8.0
  • 常量池:紧接着主版本号之后的就是常量池,一个java类中定义的很多信息 都是常量池来维护和描述的,可以将常量池看作class文件的资源仓库,比如说java类中定义的方法与变量信息,都是存储在常量池中,常量池中主要存储两类常量:字面量与符号引用,字面量如文本字符串,java中声明为final的常量值等。而符号引用如类和接口的全局限定名,字段的名称和描述符、方法的名称和描述符等。常量池:里面的值不一定都是常量。变量也是放在常量池的。
  • 常量池的总体结构:java类所对应的常量池主要由常量池数量常量池数组这两部分 共同构成。常量池数量紧跟在主版本号后面,占据2个字节,常量池数组紧跟在常量池数量之后,常量池数组与一般的数组不同,常量池数组中不同的元素的类型、结构都是不同的,长度当然也就不同;但是 每一种元素的第一个数据都是u1类型,该字节是标志位,占据1个字节。jvm在解析常量池时,会根据这个u1类型来获取元素的具体类型。
  • 在jvm规范中,每个变量/字段都有描述信息,描述信息主要的作用描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符来表示,对象类型则使用字符L加对象的全限定名称来表示。为了压缩字节码文件的体积,对于基本数据类型,jvm都只使用一个大写 字母来表述如:B -byte   C - char.   D -double.   F - float.   I - int   J - long、S - short 、Z - boolean、V - void、L - 对象类型,如 Ljava/lang/String;对于数组类型来说,int[] 记录为[I,String[][] 记录为[[Ljava/lang/String
  • 用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组()之内,如方法 String getDetail(int id,String name)的描述为(I,Ljava/lang/String)Ljava/lang/String
  • 下面我们解析一下字节码文件
  1.  16进制 00 18 就是24 ,代表常量池数组长度24,  但是 常量池数组中元素个数 = 常量池数 -1 (其中0暂时不使用),目的是满足某些常量池索引值的数据在特定情况下需要表达“不引用任何一个常量池”的含义:根本愿意在于 索引为0也是一个常量,只不过它不位于常量表中,对应的就是null,所以 常量池的索引从1开始。 JAVA 字节码文件分析_第6张图片 反编译后常量池相关数据 JAVA 字节码文件分析_第7张图片 字节码文件
  2. 那么常量池数组到哪里结束,是由常量池数组里的内容长度来决定的,需要一个一个常量解析才知道具体的长度多少
  3. 0A 是第一个常量的标志位 占一个字节 16进制是10 ,对应去找到常量池数据类型对应的值为10 的是CONSTANT_Methodref_info ,数据类型里面包含两部分,并且都是U2 类型的,表示分别占据两个字节JAVA 字节码文件分析_第8张图片
  4. 00 04 00 14 分别对应 16进制的 4 和 20 索引位置(指向声明方法的类的描述符);可以对应反编译结果的图进行查看到,由于会默认生成一个无参的构造方法;所以是 java/lang/object:()V 
  5. 那么继续 第二个常量 09 16进制表示 9  是fieldref ,也是有两个 两个字节的,那就是 00 03 00 15 ,对应找 十进制 3 和21     
  6.  3是 当前的类信息,21 对应的是 5 、6 的索引值 ;5是 a, 6是 i; 
  7.  那么继续第三个常量 07 对应的是 class_info ,后面占据两个字节 ;00 16 对应十六进制 22 

        

        

Access_Flag 访问标志

  1. 访问标志信息包括该class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被声明成final。通过上面的源代码,我们知道该文件是类并且是publicJAVA 字节码文件分析_第9张图片
  2. 0x0021:是0x0020 和 0x0001的并集,表示 acc_public 与acc_super

         

当前类的名字

  1. 包含两个字节 是 00 03 表示的是类的索引 ,指的是常量池中的 3 索引位置 

        

父类的名字

  1. 也是两个字节 00 04 表示的是父类索引 ,指的是常量池中4 的索引位置

       

当前类的接口信息

  1. 也是占用两个字节 00 00 说明该类并没有实现接口,因此接口名不占位 

      

当前类的成员变量信息     

      JAVA 字节码文件分析_第10张图片 

       

  1. 字段个数 00 01 表示 1个成员变量
  2. 字段表:用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包含方法内部声明的局部变量。
  3. 字段表结构:描述一个字段包含的信息有:字段的作用域(public private protected)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)、字段数据类型    JAVA 字节码文件分析_第11张图片 JAVA 字节码文件分析_第12张图片
  4. 00 02 表示  字段的作用域(Access_Flag) 就是私有变量 (acc_private)
  5. 00 05 名字的索引 是5 去常量池中寻找 就是 a,00 06 描述符(数据类型) 对应常量池中 6的索引 就是 I
  6. 属性数量 0000 为0  

当前类的方法信息

        

          

  1. 对应两个字节 00 03 表示 有3个方法
  2. 方法表:与字段表信息一致                                                        JAVA 字节码文件分析_第13张图片JAVA 字节码文件分析_第14张图片
  3. 00 01 表示 public的方法 00 07 00 08 表示去常量池 名称索引和描述符索引  
  4. 继续找 两个字节 atrributes_count  00 01 表示 有一个属性 
  5. 属性表:                                                                                               
  6. 再继续找 两个字节的 属性名 00 09 对应常量池索引 是code, code表示方法的执行代码。
  7. 再找四个字节的 属性长度 00 00 00 38 ;长度为56

Code结构

  •  code attribute作用是保存该方法的结构,对应字节码                    JAVA 字节码文件分析_第15张图片

  • attribute_length表示 attribute所包含的字节数,不包含 attribute_name_index和 attribute_length字段

  • max_stack表示这个方法运行的任何时刻所能达到的操作数盏的最大深度

  • max_locals 表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量

  • code_length表示该方法所包含的字节码的字节数以及具体的指令码

  • 具体字节码即是该方法被调用时,虚拟机所执行的字节码

  • exception_table,这里存放的是处理异常的信息

  • 每个exception_table表项有start_pc,end_pc,handler_pc,catch_type组成

复杂的字节码文件分析#

源文件:

image-20200216063833438

java -p 字节码:

JAVA 字节码文件分析_第16张图片

JAVA 字节码文件分析_第17张图片

JAVA 字节码文件分析_第18张图片

JAVA 字节码文件分析_第19张图片

JAVA 字节码文件分析_第20张图片

先补充synchronize关键词的字节码讲解#

  1. 如果把方法设置为private,方法在字节码中就没有了。如图

    (并不是不见了。是因为只是用javap 命令的话,是不显示私有变量的参数的)

    要使用javap -p 这个参数。

    JAVA 字节码文件分析_第21张图片

加了synchronized之后的字节码文件比较:只多了一个访问修饰符flag

JAVA 字节码文件分析_第22张图片

为什么只有一个访问修饰符呢?我们平时说的同步关键字和同步块,功能都是怎么实现的呢?对于moniterenter 和 moniterexit 这两个指令怎么执行的?是如何通过上述指令完成对锁的锁定和释放呢?

  • monitorenter的作用:进入到对象的监视器当中:

    JAVA 字节码文件分析_第23张图片

JAVA 字节码文件分析_第24张图片

  • monitorexit的作用:退出对象的监视器

    JAVA 字节码文件分析_第25张图片

  • synchronized可以用到的地方:实例方法上 或者 静态方法上,或者在方法内修饰锁对象。

  • 在方法名字上,用的是synchronized关键字。如果是在方法名字上,则只是多了一个标记符

  • 在方法里面,用的是synchronized方法,修饰锁对象,如果修饰在对象上,则字节码有其他体现

  • 如果在静态方法上,则代表给当前方法所在的Class类的对象进行上锁

  • 修饰实例方法的时候,synchronize修饰的是this对象。字节码中只在访问修饰符里面体现。(如果用在方法里面,就能够在字节码中看到具体的二进制代码的实现了。如下图所示,常量池中 13 和3 对应的两个关键词。)其中 13 是正常退出的操作。19 是异常退出的操作。确保锁在出异常的时候能够正常退出。

JAVA 字节码文件分析_第26张图片uploading.4e448015.gif转存失败重新上传取消JAVA 字节码文件分析_第27张图片

该方法对应的字节码如下

JAVA 字节码文件分析_第28张图片

需要了解的:可重入锁,单线程执行

JAVA 字节码文件分析_第29张图片

  • (自己乱写的一些概念。别人可能看不懂。对上述概念的自我翻译。)当第一个线程使用锁对象的时候,状态有0变为1,在访问锁的同事,可以再次访问这个锁对象中的其他synchronize标记方法。当第二个线程尝试去获取这个锁的时候,是进不去的。因为这个对象就进入等待状态。在等待的时候处于类的自旋状态。老版本的synchronize是比较重量级的,所以之前要求能不用就不用,但是现在已经相对优化了。

  • 如果另外一个线程已经拥有了这个线程锁对象的标记,在状态由1变为0之前,另外一个线程会一直处于等待的阻塞状态。直到那边线程释放,这边再次尝试获得拥有权。

深入分析上述的复杂程序的字节码#

字节码如下所示,当然不会像之前那么详细,是对整体有一个认知。

JAVA 字节码文件分析_第30张图片

当然,还是要通过结构表来进行查询操作:

JAVA 字节码文件分析_第31张图片

JClasslib对应的信息:

JAVA 字节码文件分析_第32张图片

70个常量池,70-1 = 69个常量

  • 在字节码中,能看到字节装箱的操作:

从第9个字段定义中,赋值的是5.53 调用的是L类型的Integer。完成了自动装箱操作

JAVA 字节码文件分析_第33张图片

  • 是类中存在静态变量的时候才会出现的静态初始化块,对静态变量进行初始化

JAVA 字节码文件分析_第34张图片

  • 默认的访问标识符为 0000。 0009 = public static

    JAVA 字节码文件分析_第35张图片

  • 方法:6个

    JAVA 字节码文件分析_第36张图片

  • 初始化变量的赋值,是在构造方法()中执行的 (不包括静态变量)

    JAVA 字节码文件分析_第37张图片

    问题1:如果我自己没有提供无参构造,系统会自动生成构造方法,然后在里面进行初始化。如果我们提供一个无参数构造方法呢?

    测试结果:结果一样。如下图所示

    JAVA 字节码文件分析_第38张图片

    为什么会这样呢?原来,因为在编译完之后,JVM会对指令,进行重新的排序。所以会是这样的现象。

    问题2:如果有两个构造方法呢?那么变量的赋值操作,是在哪里进行执行的呢?

    测试结果:有参构造和无参构造的字节码,完全相同,没有任何的变化。

    说明:JVM是把所有赋值,都给放在所有的构造方法中了。(并且是先赋值,然后再执行构造方法自定义的代码片段。)字节码面前,了无秘密。

    JAVA 字节码文件分析_第39张图片

  • 静态变量和静态代码块的内容都是在cinit()方法中执行的

    不管有多少个代码块,都只会生成一个cinit()方法

    JAVA 字节码文件分析_第40张图片

This关键词和异常表的详解#

源文件代码:

 

Copy

package com.dawa.jvm; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; public class MyTest3 { public void test(){ try { InputStream is = new FileInputStream("test.txt"); ServerSocket serverSocket = new ServerSocket(9999); serverSocket.accept(); } catch (FileNotFoundException ex) { } catch (IOException ex) { } catch (Exception ex) { }finally { System.out.println("finally "); } } }

JAVA 字节码文件分析_第41张图片

对应的字节码(java -p)

JAVA 字节码文件分析_第42张图片

JAVA 字节码文件分析_第43张图片

JAVA 字节码文件分析_第44张图片

JAVA 字节码文件分析_第45张图片

JAVA 字节码文件分析_第46张图片

  • 对于类中的每一个实例方法(非static方法),其在编译后说生成的字节码当中,方法参数的数量总是会比源代码中方法参数的数量多一个(tiis),它位于方法的第一个参数位置处;这样我们就可以在java的实例方法中使用this来访问当前对象的属性以及其他方法

  • 这个操作是在编译期间完成的,即由javac编译器在编译的时候对this的访问转化为对一个普通实例方法参数的访问,接下来起的运行期间,由JVM在调用实例方法时,自动向实例方法传入this参数。所以,在实例方法的局部变量表中,至少会有一个指向对象的局部变量。

    如下所示:无参数的方法的参数个数为1

    JAVA 字节码文件分析_第47张图片

  • 为什么局部变量是4个?

    • 我的答案:“test”,“is”, "serverSokect" , "9999"

    • 正确答案:this / is / serverSocket / ex(执行的时候最多进入一个,所以4个)

      JAVA 字节码文件分析_第48张图片

    • 如果没有抛出异常,则只有3个变量。上面只是说最大本地变量数为4个。

    • 堆栈的执行方式。java调用的机制,都是基于堆栈的。所以有一个概念叫“栈帧”。后续会涉及。 表示最多压入3个元素。image-20200216070002638

  • 关于异常表的内容:

    JAVA 字节码文件分析_第49张图片

    JAVA 字节码文件分析_第50张图片

    • 这里涉及了goto指令。出异常的时候,就是使用goto跳转到指定的异常处理。会有一个默认 any , exceptiontype = 0,代表所有的异常。

      JAVA 字节码文件分析_第51张图片

      JAVA 字节码文件分析_第52张图片

  • 关于goto,我的想法:我觉得下面博客中的这种说的不对。 他没有深入理解JVM。 因为goto语句在字节码文件里面,异常处理的时候用到了。(网上有很多种说goto没有用的观点)

Java字节码对于异常的处理方式:

  1. 统一采用异常表的方式来对异常进行处理的。(下图为异常表)

    JAVA 字节码文件分析_第53张图片

  2. 在JDK1.4.2 之前的版本,并不是使用异常表的方式来对异常进行处理的。而是采用特定的指令。

  3. 当异常处理存在你finally语句块时,现代化的JVM采取的处理方式是:将finally语句块的字节码拼接到每一个catch块后面,换句话说,冲虚中存在多少个catch块,就会在每一个catch块后面重复多少个finally语句块的字节码。

异常处理有两种方式。1.catch 2. throws ,上述的是第一种方式。

如果是throws 抛异常,查看异常的字节码。(方法级别的异常)

注意:

  1. 两种异常方式,异常不是在同一个级别的

  2. 不论是运行时异常,还是其他异常,都是一样的。都能在这里throws

JAVA 字节码文件分析_第54张图片

字节码到底是如何执行的?#

栈帧(stack frame):

  • 栈:先进后出的数据结构

  • 帧:小的单元

  • 栈帧是有一种用于帮助虚拟机执行方法调用与方法执行的数据结构。

  • 栈帧本身是一种数据结构,封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息

  • 符号引用,直接引用 (由动态链接,扩展出来的概念)

补充知识:动态链接,C语言的动态链接库。符号引用,直接引用的概念。入栈和出栈的过程。

JAVA 字节码文件分析_第55张图片

到这里该学学汇编语言了。

SLOT: 32位。 一个int,长度为32,占用1个Slot。Slot是可以复用的。

JAVA 字节码文件分析_第56张图片

对于slot可复用的解释,如上图:如b,c所占用的slot, 可能会被后来的d,e 占用。所以所占用的slot数量不能固定计算出来的。是动态的。

符号引用,如何转换为直接引用

直接引用:可以直接拿到方法或者变量的内存地址。在运行期间是拿不到的。

符号引用,是存在java的常量池中的。

方法调用:通过常量池,将符号引用,转换为直接引用

所以,符号引用的转换的两种方式:

  1. 静态解析。

    有些符号引用是在类加载阶段或者是第一次使用的时候就会转换为直接引用,这些转换叫做静态解析。

  2. 动态解析。

    另外一些符号引用则是在 每次运行期转换为直接引用,这种转换叫做动态链接。这体现为Java的多态性。

    通过伪代码来解释(下图):只有在处于运行期的时候,这些变量才会被动态的识别。这就是多塔ID一种体现,也是多态性的体现。在字节码中所能看到的,字节码的a所对应的都是Animal类的引用。叫做:invokevirtual:动态调用转发。

    JAVA 字节码文件分析_第57张图片

    方法重载与invokevirtual字节码指令的关系**

  1. invokeinterface: 助记符:调用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的哪个对象的特定方法。
  2. invokestatic:调用静态方法
  3. invokespecial:调用自己的私有方法、构造方法()以及父类的方法
  4. invokevirtual:调用虚方法,运行期动态查找的过程。
  5. invokedynamic:动态调用方法。JDK1.7引入的

从代码去理解invokestatic 含义

JAVA 字节码文件分析_第58张图片

 

  • 能被invokestatic和in是invokespecial 描述的方法,在加载的时候,就会转换为直接引用。
  • 静态解析的四种情形(称为非虚方法,在类加载阶段就可以将符号引用转换为直接引用 )
    • 静态方法
    • 父类方法
    • 构造方法
    • 私有方法 (无法被重写,共有方法会被重写)

静态分派机制(方法重载)#

方法重载,是一种静态的行为。

JAVA 字节码文件分析_第59张图片

输出结果是什么?

我的猜想:两个都是Grandpa

实际结果:(bingo)JAVA 字节码文件分析_第60张图片

原因:在编译阶段,并识别不出子类。

解释:涉及到方法的静态分派

  1. Grandpa g1 = new Father()

    以上代码,g1的静态类型是Grandpa,而g1的实际类型(真正指向的类型)是Father

    我们可以得出这样一个结论:变量的静态类型是不会发生变化的,而变量的实际类型则是可以发生变化的(多态的一种体现),实际类型是在运行期方可确定。

  2. 根本原因:方法重载,是一种静态的行为。编译期就可以完全确定。(通过查看字节码就能看出来。字节码中已经确定)。invokevirtual

    JAVA 字节码文件分析_第61张图片

动态分派机制(方法重写)#

JAVA 字节码文件分析_第62张图片

运行结果是什么?

我的猜想:apple, orange,orange

运行结果:(bingo)

JAVA 字节码文件分析_第63张图片

从字节码角度去分析结果的原因。

JAVA 字节码文件分析_第64张图片

new 关键字的的三个作用

  1. 开辟空间
  2. 执行构造方法
  3. 将对应的引用值给返回

为什么字节码汇中:还是调用的Fruit类的test方法?这和看到的输出结果不符合吧?

因为,这是方法的动态分派。

方法的动态分派涉及到一个重要概念:方法接收者。

invokevirtual字节码指令的多态查找流程(方法重写)

  1. 换句话说:查找这个方法是谁调用的

  2. 找到操作树栈顶的第一个元素所指向的对象的实际类型。(找到实例方法接收者)

  3. 如果找到了方法描述符和方法名称都完全相同的方法,且访问权限也校验通过。则直接返回当前实际类型对象的调用。

  4. 如果没有找到,则从子类往父类,从下往上,依次查找。找到,则返回。

  5. 如果一直没找到,则抛出异常。

比较方法重载(overload)和方法重写(overwrite),我们可以得到这样的结论:

方法重载是静态的,是编译器行为。

方法重写是动态的,是运行期行为。

java方法:虚方法表与动态分派机制

案例:

JAVA 字节码文件分析_第65张图片

运行结果是什么?

我的猜测:animal str / animal date (第二个掺杂了蒙的成分。)

运行结果(猜错了):

JAVA 字节码文件分析_第66张图片

第二行是Dog的原因:

根据静态分派和动态分派区分。 (如何区分呢?根据调用者的类型吧,如果调用者调用方法,调用的是当前类的重载的方法,那就是静态分派,如果调用者是重写的方法,则认为是动态分派。)

虚表

  • 针对于方法调用动态分派的过程,虚拟机会在类的方法区建立一个虚方法表的数据结构(virtual method table,vtable)
  • 针对于invokeInterface指令来说,虚拟机会建立一个叫做接口方法表的数据结构(Interface method tabke)
  • 续表,其实就是标记了指定方法的方法入口。

JAVA 字节码文件分析_第67张图片

图中,粉红色的线,代表父类的方法。子类没有对其重写,所以子类的vtable中,直接指导父类的方法索引。从而提升了查找效率,节省了内存空间。前面提到的类加载的连接阶段中将符号引用转换为直接引用,就是这个操作

在初学多态的时候,初学者经常犯这样的错误。

从字节码角度,看看下面这个程序是不行的? 就是 父类不能调用子类的引用对象。

JAVA 字节码文件分析_第68张图片

结论:不行, child调不到子类的test3().

分析:因为方法在编译器会在编译阶段就进行静态静态分派。指向的还是父类的类,父类里面没有test3()方法。

基于栈的指令集与基于寄存器的指令集详细对比#

  1. 编译执行和解释执行的概念,不同于java的编译后,然后再执行的这种概念
  2. CPU,寄存器,机器码由物理机直接执行,效率肯定高。*(涉及到了计算机底层原理知识)

现在JVM在执行Java代码的时候,通常都会将解释执行和编译执行二者结合起来进行。

  1. 所谓的解释执行,就是通过解释器来读取字节码,遇到相应的指令就去执行该指令。
  2. 所谓的编译执行,是通过及时编译器(Just In Time,JIT)将字节码转换为机器码来执行。
  3. 现在JVM会根据代码热点(频率)来生成相应的本地机器码。(两者都有使用)

基于栈的指令集与基于寄存器的指令集之前的关系:

  1. JVM执行指令所采用的方式是基于栈的指令集。
  2. 基于栈的指令集主要的操作有入栈与出栈两种。
  3. 基于栈的指令集的优势在于它可以在不同平台之间移植。而基于寄存器的指令集是与硬件架构密切相关的,无法做到可移植。
  4. 基于栈的指令集的缺点在于完成相同的操作,指令数量通常要比基于寄存器的指令集数量要多;基于栈的指令集是在内存中完成操作的,而基于寄存器的指令集是直接由CPU来执行的,它是在高速缓冲区中进行执行的,速度要快的很多。虽然虚拟机可以采取一些优化手段,但是总体来说,基于栈的指令集的执行速度要慢。

如,要完成一个2-1的操作。

  1. 栈指令集:

JAVA 字节码文件分析_第69张图片

  1. 寄存器,10-1=1.直接执行机器码。

栈指令集的实例剖析#

分析一个计算类

JAVA 字节码文件分析_第70张图片

这个myCalculate()方法对应的二进制. 有22个指令需要执行。

JAVA 字节码文件分析_第71张图片

stack = 2 ;表示这个栈最多容下2个值

locals = 6 ; 表示最大变量数为6

args_size = 1 ; this 参数

对于这22个指令,进行逐行分析

JAVA 字节码文件分析_第72张图片

  1. iconst_1 : bipush :表示将1推到操作数栈当中。

    JAVA 字节码文件分析_第73张图片

  2. istore_1:index of :把索引1的变量,将栈顶弹出来的值赋值到这个索引。

  3. istore 4 : 不是简写了。 局部变量表的索引:4 ,把栈顶的值,放在这个索引对应的变量值的位置里

    此时:JAVA 字节码文件分析_第74张图片

  4. iload_0:从局部变量中加载一个int类型的值 1, 2, 3,4

    iload_1和iload2执行完之后,左边的栈就有值了,为 2 、 1

  5. iadd: Add int,从操作数栈中弹出两个。 value1+ value2 = value。 然后把value再压回栈顶。

    此时这个指令执行完之后,是这样的:

    JAVA 字节码文件分析_第75张图片

  6. isub: 相减 (同 iadd)

  7. imul:相✖️乘 (同iadd)

  8. 全部执行完之后: 就算完了。

    JAVA 字节码文件分析_第76张图片

  9. 结论:JVM是基于操作数栈的实现

从字节码角度审视动态代理运作机制#

Spring的动态代理

  1. 动态代理接口

    JAVA 字节码文件分析_第77张图片

  2. 接口的实现类

    JAVA 字节码文件分析_第78张图片

  3. 动态代理对象的实现 (实现了InvocationHandler)

    JAVA 字节码文件分析_第79张图片

  4. 客户端实现类

    JAVA 字节码文件分析_第80张图片

  5. 执行结果:

    JAVA 字节码文件分析_第81张图片

  6. 结论:对Subject的操作,全部由动态代理对象来代理操作。

  7. 如果打印subject对象的类,是 com.sun.proxy.$Proxy0,在程序运行期动态生成出来的,那么Proxy的父类为:java.lang.reflect.Proxy

JAVA 字节码文件分析_第82张图片

那么:com.sun.proxy.$Proxy0 。动态生成这个类的字节码长什么样?

生成代理类的源码:

JAVA 字节码文件分析_第83张图片

跟入:generateProxyClass()方法,是由JVM实现的。 再跟入这个方法

JAVA 字节码文件分析_第84张图片

JAVA 字节码文件分析_第85张图片

通过设置属性值sun.misc.ProxyGenerator.saveGeneratedFiles将值保存在磁盘上左边目录里面就已经有了

JAVA 字节码文件分析_第86张图片

generateClassFile()生成动态代理类的二进制文件的字节流。

谈谈你对动态代理的理解?(important)

我现在的理解:无非就是在不知道子类是什么的时候,在运行期间,就能够动态的生成Class文件。这就是动态代理。

字节码总结#

对字节码有了整体的认知。

  • java字节码的整体结构(10个)
    • 魔数(CAFFBABE)
    • 版本 (向下兼容,向上不兼容,在这里就能判断)
    • 常量池 (索引从1开始的)
    • 访问标识符
    • 类名
    • 父类名
    • 接口
    • 成员变量
    • 方法(CODE) - init - clinit -method
    • 属性(sourcefile)

关于method里面是否可以不包含任何方法?甚至是init。 是可以的。之前说的关于java的构造方法是必须会有的,那是站在java层次和源码层次去考虑的。字节码规范里面并没有规范必须要有方法。java语言规范和JVM规范并不一是一个东西,不要混为一谈。

  • Class字节码中有两种数据类型

    • 字节数据直接量
    • 表(数组)
  • 字节码指令

  • 异常表

  • 动态代码的原理

你可能感兴趣的:(JVM)