Java虚拟机之类加载与字节码技术(上)

1. 类文件结构

JVM规范的类文件结构:
在这里插入图片描述
字节码对应名称:
在这里插入图片描述

1.1 魔数

Class文件的前四个字节称为魔数,用来确定这个文件是否是能被虚拟机接收的Class文件。值为:ca fe ba be

例如:
0000000 ca fe ba be 00 00 00 34 00 32 0a 00 06 00 15 09

1.2 版本

第五、六个字节为小版本号;
第七、八个字节为主版本号。

对应:
在这里插入图片描述
例如:
0000000 ca fe ba be 00 00 00 34 00 32 0a 00 06 00 15 09

1.3 常量池

第九和第十个字节为常量池的项数的16进制数,但是从1开始计数的而非从0开始。
例如:0000000 ca fe ba be 00 00 00 34 00 32 0a 00 06 00 15 09

常量池值对应类型表:value是10进制,需要将类文件由16进制转为10进制。
在这里插入图片描述

1.4 访问标识

例如:0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

  1. 00 21 找下图中的 20 + 1 :表示一个公共类;
  2. 00 05 去常量池中找第五项,本类的全限定类名;
  3. 00 06 去常量池中找第六项,父类的全限定类名;
  4. 00 00 接口的数量为0。

在这里插入图片描述

1.5 成员变量

用于标识接口或类内部中声明的变量,不包括方法内部声明的变量。
包括:修饰符、实例变量或类变量、可变性、并发可见性、字段名称等。
例如:0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
其中 00 00 表示没有成员变量。

访问标识符对应:
在这里插入图片描述
类型对应:
在这里插入图片描述

1.6 方法信息

方法也包括:访问标识、名称索引、描述索引、属性表索引。
例如:0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
其中 00 02 表示本类中有两个方法。

在这里插入图片描述

1.7 附加属性

各个属性表之间不要求有严格顺序,不重复名称即可,可以向编译器中写入自己的属性信息,JVM会忽视掉不认识的属性。

2. 字节码指令

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(操作码)已经跟随其后的零至多个代表此操作所需参数(操作数)而构成。
Java的字节码由于限制了Java虚拟机操作码的长度为一个字节(0~255),又由于Class文件格式放弃了编译后代码的操作数和长度对齐,也就是JVM处理超过一个字节的数据时,需要在运行时重建出具体数据的结构。放弃了操作数长度对齐,可以省略很多填充和间隔符号,尽可能的获得短小精干的编译代码。

2.1 JavaP工具

JavaP用来反编译Class文件。

代码如下:

/**
 * @Description JavaP反编译测试
 * @date 2022/3/27 9:15
 */
public class AgaCom {
    public static void main(String[] args) {
        System.out.println("JavaP");
    }
}

使用JavaP反编译:
在这里插入图片描述在这里插入图片描述

2.2 运行流程

初始运行代码:

/**
 * @Description 字节码运行流程
 * @date 2022/3/27 9:30
 */
public class ShowRunStep {
    public static void main(String[] args) {
        int a = 10;
        int b = Short.MAX_VALUE + 1;
        int c = a + b;
        System.out.println(c);
    }
}

使用JavaP反编译之后:

Classfile /D:/NewJava/JavaFoundation/java-jvm/target/classes/jm/java/bytecode/ShowRunStep.class
  Last modified 2022-3-27; size 629 bytes
  MD5 checksum 49deee0c8b09c3878c12cc33b430f985
  Compiled from "ShowRunStep.java"
public class jm.java.bytecode.ShowRunStep
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#25         // java/lang/Object."<init>":()V
   #2 = Class              #26            // java/lang/Short
   #3 = Integer            32768
   #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
   #6 = Class              #31            // jm/java/bytecode/ShowRunStep
   #7 = Class              #32            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Ljm/java/bytecode/ShowRunStep;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               a
  #20 = Utf8               I
  #21 = Utf8               b
  #22 = Utf8               c
  #23 = Utf8               SourceFile
  #24 = Utf8               ShowRunStep.java
  #25 = NameAndType        #8:#9          // "<init>":()V
  #26 = Utf8               java/lang/Short
  #27 = Class              #33            // java/lang/System
  #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
  #29 = Class              #36            // java/io/PrintStream
  #30 = NameAndType        #37:#38        // println:(I)V
  #31 = Utf8               jm/java/bytecode/ShowRunStep
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/System
  #34 = Utf8               out
  #35 = Utf8               Ljava/io/PrintStream;
  #36 = Utf8               java/io/PrintStream
  #37 = Utf8               println
  #38 = Utf8               (I)V
{
  public jm.java.bytecode.ShowRunStep();
    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 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ljm/java/bytecode/ShowRunStep;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: ldc           #3                  // int 32768
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 9: 0
        line 10: 3
        line 11: 6
        line 12: 10
        line 13: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            3      15     1     a   I
            6      12     2     b   I
           10       8     3     c   I
}
SourceFile: "ShowRunStep.java"
2.2.1 将Class文件的常量池放入到运行中常量池

比较小的数会根字节码的指令存放在一起,但是数字范围超过整数最大值就会存储在常量池中。
例如:# 3 = 32768

在这里插入图片描述

2.2.2 方法字节码载入方法区

在这里插入图片描述

2.2.3 启动主线程,分配栈帧内存

局部变量表的大小和操作数栈的深度对应上方反编译文件中main方法下的:stack=2, locals=4,来存储数据和字节码指令。

在这里插入图片描述

2.2.4 执行引擎开始执行字节码
  1. bipush 10 :将一个byte压入操作数栈。基于操作数栈的大小,如果长度不够会补齐,长度超过会分多次压入。
    在这里插入图片描述
  2. istore 1 将操作数栈顶数据弹出,存入局部变量表 solt 1。
    在这里插入图片描述
    在这里插入图片描述
  3. ldc #3 从常量池加载 #3 数据到操作数栈。超过整数的值是在编译期间就计算好了。
    在这里插入图片描述
  4. isotre 2弹出操作数栈顶数据存入 slot 2
    在这里插入图片描述
    在这里插入图片描述
  5. 执行iload 1 iload 2 把局部变量slot 1 和 slot 2 的值读取到操作数栈中。
    在这里插入图片描述
  6. iadd 弹出操作数栈的变量,执行相加,并且把结果存入操作数栈
    在这里插入图片描述
  7. isotre 3 把操作数栈顶数据弹出放到 slot 3
  8. getstatic #4 ,把堆中对象的引用放到操作数栈中。
    在这里插入图片描述
    在这里插入图片描述
  9. iload 3把slot 3 中的值读入到操作数栈中。在这里插入图片描述
  10. invokevirtual #5找到常量池总的 #5 ;定位到方法;给方法分配新的栈帧;传递参数,执行新栈帧中的字节码。在这里插入图片描述
    执行完毕,弹出栈帧;清除 main 操作数栈内容
    在这里插入图片描述
  11. return 完成main方法调用,弹出main栈帧,程序结束。

2.3 字节码命令分析

2.3.1 条件判断

byte,short,char都会按照int比较,因为操作数栈是4字节。
比较成立,会使用goto来跳转到指定行号的字节码。

在这里插入图片描述
源代码:

/**
 * @Description 判断语句字节码
 * @date 2022/3/27 16:50
 */
public class ShowCheat {
    public static void main(String[] args) {
        int a = 0;
        if (a == 0){
            a = 20;
        }else{
            a = 10;
        }
    }
}

字节码核心部分:

         0: iconst_0
         1: istore_1
         2: iload_1
         3: ifne          12
         6: bipush        20
         8: istore_1
         9: goto          15
        12: bipush        10
        14: istore_1
        15: return

如上述字节码,第三行ifne判断如果成立就跳转到12行,将10加入操作数栈中,在执行完赋值之后就goto到15行返回,结束方法的调用。不成立就执行第六行,赋值完毕返回,结束方法调用。

2.3.2 循环控制指令

代码:

/**
 * @Description 字节码循环控制指令
 * @date 2022/3/27 16:58
 */
public class CircControl {
    public static void main(String[] args) {
        int a = 0;
        while (a < 10){
            a++;
        }
    }
}

字节码核心:

      	 0: iconst_0
         1: istore_1
         2: iload_1
         3: bipush        10
         5: if_icmpge     14
         8: iinc          1, 1
        11: goto          2
        14: return

显然循环控制指令并没有超出条件判断的范畴中,如上方字节码的运行过程是:比较变量a与10的大小,如果大于等于直接跳转到14行结束运行,如果不符合条件就执行++命令,然后通过goto再跳回第二行,重新运行。

2.4 构造方法及方法调用

2.4.1 <cinit>()V

代码:

/**
 * @Description 构造方法——cinit
 * @date 2022/3/27 17:08
 */
public class Cinit {
    static int i = 10;

    static {
        i = 20;
    }

    static {
        i = 30;
    }
}

字节码核心:

 		 0: bipush        10
         2: putstatic     #2                  // Field i:I
         5: bipush        20
         7: putstatic     #2                  // Field i:I
        10: bipush        30
        12: putstatic     #2                  // Field i:I
        15: return

编译器会按从上自下的顺序,搜集所有的staic静态代码块和静态成员变量的赋值的代码,合并为<cinit>()V

2.4.2 <init>()V
/**
 * @Description 构造方法——init
 * @date 2022/3/27 17:12
 */
public class Init {
    private String a = "s1";

    {
        b = 20;
    }

    private int b = 10;

    {
        a = "s2";
    }

    public Init(String a, int b) {
        this.a = a;
        this.b = b;
    }

    public static void main(String[] args) {
        Init d = new Init("s3", 30);
        System.out.println(d.a);
        System.out.println(d.b);
    }
    
}
 public jm.java.bytecode.Init(java.lang.String, int);
    descriptor: (Ljava/lang/String;I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String s1
         7: putfield      #3                  // Field a:Ljava/lang/String;
        10: aload_0
        11: bipush        20
        13: putfield      #4                  // Field b:I
        16: aload_0
        17: bipush        10
        19: putfield      #4                  // Field b:I
        22: aload_0
        23: ldc           #5                  // String s2
        25: putfield      #3                  // Field a:Ljava/lang/String;
        28: aload_0
        29: aload_1
        30: putfield      #3                  // Field a:Ljava/lang/String;
        33: aload_0
        34: iload_2
        35: putfield      #4                  // Field b:I
        38: return
      LineNumberTable:
        line 20: 0
        line 8: 4
        line 11: 10
        line 14: 16
        line 17: 22
        line 21: 28
        line 22: 33
        line 23: 38
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      39     0  this   Ljm/java/bytecode/Init;
            0      39     1     a   Ljava/lang/String;
            0      39     2     b   I

编译器也会按照从上至下的顺序,将所有的代码块和成员变量赋值的代码放到一块形成新的构造方法,但原始构造方法的代码放在最后。

2.4.3 方法调用

源代码:

/**
 * @Description 字节码——方法调用
 * @date 2022/3/27 17:22
 */
public class GetMethod {
    public GetMethod() { }
    
    private void test1() { }
    
    private final void test2() { }
    
    public void test3() { }
    
    public static void test4() { }
    
    public static void main(String[] args) {
        GetMethod d = new GetMethod();
        d.test1();
        d.test2();
        d.test3();
        d.test4();
        GetMethod.test4();
    }
}

字节码核心:

	0: new #2 // class cn/itcast/jvm/t3/bytecode/Demo3_9
	3: dup
	4: invokespecial #3 // Method "<init>":()V
	7: astore_1
	8: aload_1
	9: invokespecial #4 // Method test1:()V
	12: aload_1
	13: invokespecial #5 // Method test2:()V
	16: aload_1
	17: invokevirtual #6 // Method test3:()V
	20: aload_1
	21: pop
	22: invokestatic #7 // Method test4:()V
	25: invokestatic #7 // Method test4:()V
	28: return
  1. new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈。
  2. dup 是赋值操作数栈栈顶的内容。
  3. 普通成员方法是由 invokespecial 调用,属于动态绑定,即支持多态。
  4. 最终方法(final),私有方法(private),构造方法都是由invokevirtual指令来调用,属于静态绑定。
  5. 如果使用【对应引用】调用静态方法,会执行pop指令,把【对象引用】弹出。
  6. 通过super调用父类方法,也会执行invokespecial
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值