Bootstrap

Class文件结构和字节码指令集

Class文件结构和字节码指令集

概述

字节码文件的跨平台性

Java 语言:跨平台的语言(write once, run anywhere)
  • 当Java源代码成功编译成字节码之后,如果想在不同的平台上运行,不需要再次编译。
  • 这个优势不是很吸引人了,因为Python、PHP、Perl、Ruby、Lisp等语言都有强大的解释器
  • 跨平台已经几乎快成为一门语言的必选特性
Java 虚拟机:跨语言的平台
  • Java虚拟机不与包含Java在内的任何语言进行绑定,它只和 “Class” 文件这种特定的二进制文件格式关联。无论使用何种语言进行软件开发, 只要能将源文件编译为正确的 Class 文件,那么这种语言就可以在 Java 虚拟机上执行,可以说,统一而强大的 Class 文件结构,就是 Java 虚拟机的基石、桥梁。

image-20220605210251730

JVM规范

所有的 JVM 全部遵守 Java 虚拟机规范,也就是说所有的 JVM 环境都是一样的, 这样一来字节码文件可以在各种 JVM 上进行。

想要让一个 Java 程序正确地运行在 JVM 中,Java 源码就是必须要被编译为符合 JVM 规范的字节码。

前端编译器的主要任务就是负责将符合 Java 语法规范的 Java 代码转换为符合 JVM 规范的字节码文件。

javac 是一种能够将 Java 源码编译为字节码的前端编译器。

javac 编译器在将 Java 源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法分析、语法分析、语义分析以及生成字节码。

image-20220605210516146

Oracle 的 JDK 软件包括两部分内容:

  • 一部分是将 Java 源代码编译成 Java 虚拟机的指令集的编译器
  • 另一部分是用于实现 Java 虚拟机的运行时环境

Java的前端编译器

image-20220605210704208

前端编译器 VS 后端编译器

Java 源代码的编译结果是字节码,那么肯定需要有一种编译器能够将 Java 源码编译为字节码,承担这个重要责任的就是配置在 path 环境变量中的 javac 编译器。javac 是一种能够将 Java 源码编译为字节码的前端编译器

HotSpot VM 并没有强制要求前端编译器只能使用 javac 来编译字节码,其实只要编译结果符合 JVM 规范都可以被 JVM 所识别即可。在 Java 的前端编译器领域,除了 javac 之外,还有一种被大家经常用到的前端编译器,那就是内置在 Eclipse 中的 ECJ (Eclipse Compiler for Java)编译器。和 javac 的全量式编译不同,ECJ 是一种增量式编译器。

  • 在 Eclipse 中,当开发人员编写完代码后,使用"Ctrl + S"快捷键时,ECJ 编译器所采取的编译方案是把未编译部分的源码逐行进行编译,而非每次都全量编译。因此 ECJ 的编译效率会比 javac 更加迅速和高效,当然编译质量和 javac 相比大致还是一样的。
  • ECJ 不仅是 Eclipse 的默认内置前端编译器,在 Tomcat 中同样也是使用 ECJ 编译器来编译 jsp 文件。由于 ECJ 编译器是采用 GPLv2 的开源协议进行源代码公开,所以,大家可以登录 Eclipse 官网下载 ECJ 编译器的源码进行二次开发。
  • 默认情况下,IntelliJ IDEA 使用 javac 编译器(还可以自己设置为 AspectJ 编译器 ajc)

前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节移交给 HotSpot 的 JIT 编译器负责。

透过字节码指令看代码细节

  1. BAT面试题目

① 类文件结构有几个部分?
② 知道字节码吗?字节码都有哪些? Integer x = 5; int y = 5;比较×==y都经过哪些步骤?

  1. 代码举例
public class IntegerTest {
   

    public static void main(String[] args) {
   
        Integer x = 5;
        int y = 5;
        System.out.println(x == y); // 

        Integer i1 = 10;
        Integer i2 = 10;
        System.out.println(i1  ==i2);  // true

        Integer i3 = 128;
        Integer i4 = 128;
        System.out.println(i3 == i4); // false
    }
}

Integer的valueOf方法,IntegerCache是一个静态内部类,用于创建-128~127范围内的Integer数组。

    public static Integer valueOf(int i) {
   
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

下面是IntegerCache的静态内部类

image-20220604194054116

image-20220604193655858

image-20220604195245925

实例二:使用Sting来看

public class StringTest {
   
    public static void main(String[] args) {
   

        String str = new String("hello") + new String("world");

        String str2 = "helloword";

        System.out.println(str2 == str);  // 输出:false

    }
}

image-20220604200442633

实例三:

class Father {
   

    int x = 10;



    public Father() {
   

        this.print();

        x = 20;

    }



    public void print() {
   

        System.out.println("Father.x = " + x);

    }

}



class Son extends Father {
   

    int x = 30;



    public Son() {
   

        this.print();

        x = 40;

    }


    @Override
    public void print() {
   

        System.out.println("Son.x = " + x);

    }

}

public class _03_SonTest {
   

    public static void main(String[] args) {
   

        Father f = new Father();

        System.out.println(f.x);

    }

}

查看Father类的字节码文件

image-20220604202516291

查看son类的字节码文件

image-20220604214221708

Class文件结构

  • 官方文档位置

类文件结构的官方文件位置:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

  • Class 类的本质

任何一个 Class 文件都对应着唯一一个类或接口的定义信息,但反过来说,Class 文件实际上它并不一定以磁盘文件形式存在。Class 文件是一组以8位字节为基础单位的二进制流

  • Class 文件格式

Class 的结构不像 XML 等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。

Class 文件格式采用一种类似于 C 语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数

  • 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。
  • (表相当于Java的数组)是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以"_info"结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明(因为没有分隔符)。

image-20220605211616136

表用于描述有层次关系的复合结构的数据

image-20220605211652318

整个 Class 文件本质上就是一张表

image-20220605211722803

Class文件结构

image-20220605212650918

也就回答了上面的面试题目

image-20220605212735855

类型 名称 说明 长度 数量
u4 magic 魔数,识别Class文件格式 4个字节 1
u2 minor_version 副版本号(小版本) 2个字节 1
u2 major_version 主版本号(大版本) 2个字节 1
u2 constant_pool_count 常量池计数器 2个字节 1
cp_info constant_pool 常量池表 n个字节 constant_pool_count-1
u2 access_flags 访问标识 2个字节 1
u2 this_class 类索引 2个字节 1
u2 super_class 父类索引 2个字节 1
u2 interfaces_count 接口计数器 2个字节 1
u2 interfaces 接口索引集合 2个字节 interfaces_count
u2 fields_count 字段计数器 2个字节 1
field_info fields 字段表 n个字节 fields_count
u2 methods_count 方法计数器 2个字节 1
method_info methods 方法表 n个字节 methods_count
u2 attributes_count 属性计数器 2个字节 1
attribute_info attributes 属性表 n个字节 attributes_count

查看Demo的字节码解读(了解)

image-20220605205104194

加载与存储指令

加载(load):就是将局部变量压栈到操作数栈中,局部变量可能来自于局部变量表(存储指令),也可能来自于常量池。
存储(store):保存到栈帧的局部变量表
这下面的i、l、d、a标识的类型分别是int(4个字节),float(4个字节),double(8个字节),a是引用类型(4个字节)(其实像byte、short、char、boolean也是用的int一个槽位4个字节来表示的

  • 含有load、push、const就是把局部变量压入到操作数栈当中。
  • 含有store的指令就是把从操作数栈中取出存到局部变量表中。
  1. 作用

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递。

  1. 常用指令

    1. 【局部变量压栈指令】将一个局部变量加载到操作数栈: xload、xload_<n>(其中x为i、1、f、d、a,n为0到3)
    2. 【常量入栈指令】将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
    3. 【出栈装入局部变量表指令】将一个数值从操作数栈存储到局部变量表:xstore、xstore_<n>(其中x为i、1、f、d、a,n为0到3) ; xastore(其中x为i、l、f、d、a、b、c、s)(其实x就是表示类型,n就是表示存储的位置)
    4. 扩充局部变量表的访问索引的指令:wide。

    上面所列举的指令助记符(也就是字节码指令)中,有一部分是以尖括号结尾的(例如iload_<n>)。这些指令助记符实际上代表了一组指令(例如 iload_<n>代表了iload_0、iload_1、iload_2和iload_3这几个指令)。这几组指令都是某个带有一个操作数的通用指令(例如 iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中

    除此之外,它们的语义与原生的通用指令完全一致(例如 iload_0的语义与操作数为0时的 iload 指令语义完全一致)。在尖括号之间的字母指定了指令隐含操作数的数据类型,<n>代表非负的整数,<i>代表是int类型数据,<l>代表long类型,<f>代表float类型,<d>代表double类型。

注意:iload_0和iload 0其实是一个意思,但是iload_0只占用一个字节(只有操作码,操作数包含在操作码中了),二iload 0占用三个字节(操作码一个字节+操作数两个字节);还有像short、byte、char、boolean底层也是int的指令,这样做的目的是减少指令的条数,因为指令条数总共只有2^8,两百多条,后面jdk更新的时候,可能会又出现新的指令

image-20220626173411938

image-20220626173437864

复习:再谈操作数栈与局部变量表

操作码后面的#4 #5这些就叫做操作数

image-20220626173640381

字节码执行过程

    //1.局部变量压栈指令Z
    public void load(int num, Object obj,long count,boolean flag,short[] arr) {
   
        System.out.println(num);
        System.out.println(obj);
        System.out.println(count);
        System.out.println(flag);
        System.out.println(arr);
    }

image-20220626173832384

操作数栈(Operand Stacks)
我们知道,Java字节码是Java虚拟机所使用的指令集。因此,它与Java虚拟机基于栈的计算模型是密不可分的。
在解释执行的过程中,每当为Java方法分配栈帧时,Java虚拟机往往需要开辟一块额外的空间作为操作数栈,用来存放计算的操作数以及返回的结果。
具体来说便是:执行每条指令之前,Java虚拟机要求该指令的操作数已经被压入操作数栈中。在执行指令时,Java虚拟机会将该指令所需要的操作数弹出,并且将指令的结果重新压入栈中。

示例

//3.出栈装入局部变量表指令
public void store(int k, double d) {
   
    int m = k + 2;
    long l = 12;
    String str = "atguigu";
    float f = 10.0F;
    d = 10;
}

image-20220626182222133

解释上面的字节码指令,iload_1的意思就是将局部变量表中下标为1的变量的值放入到操作数栈中,iconst_2的意思是将局部变量表中的下标为2的变量的值压入操作数栈中,iadd是弹出操作数栈中的两个变量,然后将它们求和,istore4存储到局部变量表索引为4的位置(m),将k+2的值加入到操作数栈中。ldc #15就是到常量池中取出索引为15的变量的值(atguigu),放入到操作数栈中。astore 7就是弹出操作数栈中的栈顶的元素存储到局部变量下标为7的位置,ldc #16的意思是从局部变量表中取出下标为16的变量的值(10.0)放入到操作数栈中,fstore 8 就是从操作数栈中弹出栈顶的元素然后将其存到局部变量表下标为8的位置ldc2_w #17的意思是将常量池中下标为17的变量(10.0)的值放入到操作数栈中,dstore_2的意思是从栈顶弹出10.0然后加入到局部变量表中索引为2的位置,return然后终止方法的执行。

 0 iload_1
 1 iconst_2
 2 iadd
 3 istore 4
 5 ldc2_w #13 <12>
 8 lstore 5
10 ldc #15 <atguigu>
12 astore 7
14 ldc #16 <10.0>
16 fstore 8
18 ldc2_w #17 <10.0>
21 dstore_2
22 return

i++和++i字节码详细分析

不涉及其他运算的情况下

如果不涉及到其他的运算的情况下,i++和++i的字节码是一样的。

3 iinc 1 by 1字节码指令的意思是直接将局部变量表中的索引为1的变量的值+1

image-20220612142237862

    //关于(前)++和(后)++
    public void method6(){
   
        int i = 10;
//        i++;
        ++i;
    }

后加加的代码以及字节码

0 bipush 10
2 istore_1
3 iinc 1 by 
;