5、Class文件结构及字节码的执行过程

字节码助记码解释地址:https://cloud.tencent.com/developer/article/1333540
1、Class类文件结构
Class 文件是一组以 8 位字节为基础单位的二进制流。
5、Class文件结构及字节码的执行过程
2、字节码查看工具
(1)Sublime:查看16进制的编辑器
5、Class文件结构及字节码的执行过程

(2)javap:是jdk自带的反解析工具,它的作用是将 .class 字节码文件解析成可读的文件格式。javap -v person.class
(3)jclasslib:jclasslib 是一个图形化的工具,能够更加直观的查看字节码中的内容。
有Idea 的插件,你可以从 plugins 中搜索到它。
jclasslib 的下载地址:https://github.com/ingokegel/jclasslib
5、Class文件结构及字节码的执行过程

3、Class文件格式

5、Class文件结构及字节码的执行过程
4、Class 文件格式详解
魔术 和class文件的版本 ->常量池 -> 访问标志->类索引、父类索引与接口索引集合->字段表集合->方法表集合->属性表集合
(1)、魔数与 Class 文件的版本
每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。使用魔数而不是扩展 名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。文件格式的制定者可以*地选择魔数值,只要这个魔数值还没有被广泛 采用过同时又不会引起混淆即可。( ) 紧接着魔数的 4 个字节存储的是 Class 文件的版本号:第 5 和第 6 个字节是次版本号(MinorVersion),第 7 和第 8 个字节是主版本号(Major Version)。 Java 的版本号是从 45 开始的,JDK 1.1 之后的每个 JDK 大版本发布主版本号向上加 1 高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版 本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件。 代表 JDK1.8(16 进制的 34,换成 10 进制就是 52)
(2)常量池
常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的数据,代表常量池容量计数值(constant_pool_count)。 与 Java 中语言习惯不一样的是,这个容量计数是从 1 而不是 0 开始的
5、Class文件结构及字节码的执行过程
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等。 而符号引用则属于编译原理方面的概念,包括了下面三类常量: 类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符
(3)访问标志
用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被 声明为 final 等
(4)类索引、父类索引与接口索引集合
这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。
(5)字段表集合
描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量
(6)方法表集合
描述了方法的定义,但是方法里的 Java 代码,经过编译器编译成字节码指令后,存放在属性表集合中的方法属性表集合中一个名为“Code”的属性里面。 与字段表集合相类似的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编
译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”
(7)属性表集合
存储 Class 文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在 Code 属性表中。

5、字节码指令
字节码指令属于方法表中的内容。
方法表是一个表结构,表中每个成员必须是method__info数据结构,用于表示当前类或者接口的某个方法的完整描述。
(1)加载和存储指令
用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括如下内容。
1)将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。
2)将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n >。
3)将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。 扩充局部变量表的访问索引的指令:wide。
(2)运算或算术指令
用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶
1)加法指令:iadd、ladd、fadd、dadd。
2)减法指令:isub、lsub、fsub、dsub。
3)乘法指令:imul、lmul、fmul、dmul 等等
(3)类型转换指令
可以将两种不同的数值类型进行相互转换,Java 虚拟机直接支持以下数值类型的宽化类型转换(即小范围类型向大范围类型的安全转换):
1)int 类型到 long、float 或者 double 类型。
2)long 类型到 float、double 类型。
3)float 类型到 double 类型。
处理窄化类型转换(Narrowing Numeric Conversions)时,必须显式地使用转换指令来完成,这些转换指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。
(4)创建类实例的指令:new
(5)创建数组的指令:newarray、anewarray、multianewarray。
(6)访问字段指令:getfield、putfield、getstatic、putstatic。
(7)数组存取相关指令
1)把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
2)将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
3)取数组长度的指令:arraylength。
(8)检查类实例类型的指令:instanceof、checkcast。
(9)操作数栈管理指令
1)将操作数栈的栈顶一个或两个元素出栈:pop、 pop2。
2)复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。
3)将栈最顶端的两个数值互换:swap。
(10)控制转移指令
控制转移指令可以让 Java 虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控 制转移指令就是在有条件或无条件地修改 PC 寄存器的值。控制转移指令如下。
1)条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq 和 if_acmpne。
2)复合条件分支:tableswitch、lookupswitch。
3)无条件分支:goto、goto_w、jsr、jsr_w、ret。
(11)方法调用指令
1)invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式。
2)invokeinterface 指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
3)invokespecial 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
4)invokestatic 指令用于调用类方法(static 方法)。
5)invokedynamic 指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面 4 条调用指令的分派逻辑都固化在 Java 虚拟机内部,而 invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的。
方法调用指令与数据类型无关。
(12)方法返回指令
根据返回值的类型区分的,包括 ireturn(当返回值是 boolean、byte、char、short 和 int 类型时使用)、lreturn、freturn、dreturn 和 areturn,另外还有 一条 return 指令供声明为 void 的方法、实例初始化方法以及类和接口的类初始化方法使用
(13)异常处理指令
在 Java 程序中显式抛出异常的操作(throw 语句)都由 athrow 指令来实现
(14)同步指令
有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义

6、字节码指令——异常处理
每个时刻正在执行的当前方法就是虚拟机栈顶的栈桢。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。 当一个方法执行完,要返回,那么有两种情况,一种是正常,另外一种是异常。
完成出口(返回地址): 正常返回:(调用程序计数器中的地址作为返回) 三步曲: 恢复上层方法的局部变量表和操作数栈、 把返回值(如果有的话)压入调用者栈帧的操作数栈中、 调整程序计数器的值以指向方法调用指令后面的一条指令、 异常的话:(通过异常处理表<非栈帧中的>来确定)
(1)异常机制
5、Class文件结构及字节码的执行过程
Error 和 RuntimeException 是非检查型异常(Unchecked Exception),也就是 不需要 catch 语句去捕获的异常;而其他异常,则需要程序员手动去处理。

(2)异常表
5、Class文件结构及字节码的执行过程
5、Class文件结构及字节码的执行过程
在 synchronized 生成的字节码中,其实包含两条 monitorexit 指令,是为了保证所有的异常条件,都能够退出。 可以看到,编译后的字节码,带有一个叫 Exception table 的异常表,里面的每一行数据,都是一个异常处理器:

  • from 指定字节码索引的开始位置
  • to 指定字节码索引的结束位置
  • target 异常处理的起始位置
  • type 异常类型
    也就是说,只要在 from 和 to 之间发生了异常,就会跳转到 target 所指定的位置。

(3)Finally
通常在做一些文件读取的时候,都会在 finally 代码块中关闭流,以避免内存的溢出。
5、Class文件结构及字节码的执行过程
5、Class文件结构及字节码的执行过程

上面的代码,捕获了一个 FileNotFoundException 异常,然后在 finally 中捕获了 IOException 异常。当我们分析字节码的时候,却发现了一个有意思的地 方:IOException 足足出现了三次。
Java 编译器使用了一种比较傻的方式来组织 finally 的字节码,它分别在 try、catch 的正常执行路径上,复制一份 finally 代码,追加在正常执行逻辑的 后面;同时,再复制一份到其他异常执行逻辑的出口处。

7、字节码指令——装箱拆箱
Java 中有 8 种基本类型,但鉴于 Java 面向对象的特点,它们同样有着对应的 8 个包装类型,比如 int 和 Integer,包装类型的值可以为 null(基本类 型没有 null 值,而数据库的表中普遍存在 null 值。 所以实体类中所有属性均应采用封装类型),很多时候,它们都能够相互赋值。
![!](https://img-blog.csdnimg.cn/20200823003214258.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dsY2hpbmExMjM=,size_16,color_FFFFFF,t_70#pic_center)
5、Class文件结构及字节码的执行过程
通过观察字节码,我们发现:
1、在进行乘法运算的时候,调用了 Integer.intValue 方法来获取基本类型的值。
2、赋值操作使用的是 Integer.valueOf 方法。
3、在方法返回的时候,再次使用了 Integer.valueOf 方法对结果进行了包装。
这就是 Java 中的自动装箱拆箱的底层实现。

IntegerCache
5、Class文件结构及字节码的执行过程
这个 IntegerCache,缓存了 low 和 high 之间的 Integer 对象
5、Class文件结构及字节码的执行过程
一般情况下,缓存是的-128 到 127 之间的值,但是可以通过 -XX:AutoBoxCacheMax 来修改上限。

8、字节码指令——数组
数组是 JVM 内置的一种对象类型,这个对象同样是继承的 Object 类。
5、Class文件结构及字节码的执行过程
5、Class文件结构及字节码的执行过程
(1)数组创建
可以看到,新建数组的代码,被编译成了 newarray 指令
5、Class文件结构及字节码的执行过程
数组里的初始内容,被顺序编译成了一系列指令放入:

  • sipush 将一个短整型常量值推送至栈顶;
  • iastore 将栈顶 int 型数值存入指定数组的指定索引位置。
    具体操作:
    1、 iconst_0,常量 0,入操作数栈
    2、 sipush 将一个常量 1111 加载到操作数栈
    3、 将栈顶 int 型数值存入数组的 0 索引位置
    为了支持多种类型,从操作数栈存储到数组,有更多的指令:bastore、castore、sastore、iastore、lastore、fastore、dastore、aastore。
    (2)数组访问
    5、Class文件结构及字节码的执行过程
    数组元素的访问,是通过第 28 ~ 30 行代码来实现的:
  • aload_1 将第二个引用类型本地变量推送至栈顶,这里是生成的数组;
  • iconst_2 将 int 型 2 推送至栈顶;
  • iaload 将 int 型数组指定索引的值推送至栈顶。

9、字节码指令——foreach
无论是 Java 的数组,还是 List,都可以使用 foreach 语句进行遍历,虽然在语言层面它们的表现形式是一致的,但实际实现的方法并不同。
5、Class文件结构及字节码的执行过程
数组:它将代码解释成了传统的变量方式,即 for(int i;i<length;i++) 的形式。
List 的它实际是把 list 对象进行迭代并遍历的,在循环中,使用了 Iterator.next() 方法。
使用 jd-gui 等反编译工具,可以看到实际生成的代码
5、Class文件结构及字节码的执行过程
10、字节码指令——注解
5、Class文件结构及字节码的执行过程
5、Class文件结构及字节码的执行过程
5、Class文件结构及字节码的执行过程
无论是类的注解,还是方法注解,都是由一个叫做 RuntimeInvisibleAnnotations 的结构来存储的。
而参数的存储,是由 RuntimeInvisibleParameterAnotations 来保证的。

11、JVM即时编译器JIT
热点探测:(1)方法调用计数器(2)回边计数器
5、Class文件结构及字节码的执行过程