class类文件的结构
class文件结构组成
- class文件是以一组8位字节为基础的二进制流。
- class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。
- 表是有多个无符号数或者其他表构成的符合数据类型。
魔数与class文件的版本
- 每个class文件的头四个字节称为魔数,他的唯一的作用是确定这个文件是否为一个能被虚拟机接受的class文件。
- 紧接着魔数的4个字节存储的是class文件的版本号:5和6个字节是次版本号,7和8字节是主版本号,虚拟机拒绝执行超过其版本号的class文件。
常量池
- 紧接着主次版本号之后是常量池的入口,常量池可以理解为class文件中的资源仓库。
- 常量池中主要存放两大类常量:字面量和符号引用。
- 符号引用主要包括了下面3类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符.
- 之所以常量池是最繁琐的数据,是因为14种常量类型各自均有自己的结构.(待添加图)
- class文件中方法、字段都需要引用CONSTANT_UTF8_INFO型常量来描述名称,所以CONSTAT_UTF8_INFO常量的最大长度也就是java中方法、字段名的最大长度。这里最大长度就是length的最大值,现在u2类型能够表达的最大值是65535,所以java程序中如果定义了超过64KB英文字符的变量或方法名,将无法编译。
访问标志
- 在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息。包括这个class是类还是接口。是否定义为public,是否定义为abstract类型,如果是类,是否被声明为final等。
类索引、父类索引、接口索引集合
- 类索引、父类索引都是一个u2类型的数据,二接口索引集合是一组u2类型的数据集合。
- class文件中由这三项数据来确定这个类的继承关系。
- 类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类全限定名,由于java语言不允许多继承,所以父类索引只有一个,除了java.lang.object之外,所有的java的类都有父类。因此除了java.lang.object,所有java类的父类的索引都不为0.
- 接口索引集合就用描述这个类实现了那些接口,这些被实现的接口按照implements语句厚的接口顺序从左到右排列在接口索引集合中。
字段表集合
- 字段表用于描述接口或者类中声明的变量,字段包括类级变量和实例变量,但是不包括在方法内声明的局部变量。
- 字段表依次包括:访问标志、名称索引、描述符索引、属性表集合。
- 对于数组类型,每个维度使用一个前置的"["字符来表示,二位数组用"[["来表示。
- 用于描述方法时,按照参数在前,返回值在后的顺序描述。
- 字段表不会列出超类父类接口中继承而来的字段,但是有可能列出原本java代码中不存在的字段
方法表集合
- class文件存储格式对方法的表述鱼对字段的描述几乎采用了完全一致的方式,方法表的结构与字段表一样,依次包括访问标志、名称索引、描述符、索引属性表集合。
- 方法里面的java代码,经过编译器编译或者字节码指令后,有效在方法属性表集合中名为"code"的属性里面,属性表作为class文件各种最具扩展啥小的一种数据项目。
class属性
code属性
- java程序方法体中的代码经过javac编译器处理后,最终变为字节码指令存储在code属性中。
- 在字节码指令之后是这个方法的显示异常处理表集合,异常处理表对code属性来说并不是必须存在的。
exceptions属性
- exceptions属性的作用是列举出方法中可能抛出的收查异常,也就是方法上在throws关键字后面列举的异常。
lineNumberTable属性
- lineNumberTable属性用于描述java源码行号与字节码行号之间的对应关系。
localVariableTable属性
- localVariableTable属性用于描述栈帧中局部变量表中的变量鱼java源码中定义的变量之间的关系。
souurceFile属性
- souurceFile属性用于记录生成这个class文件的源码文件名称。
constantValue属性
-
constantValue属性的作用通知虚拟机自动为静态变量赋值
-
虚拟机对静态变量和实例变量的赋值方式和时刻是不同的,对于非static类型的变量的赋值是在实例构造器方法中进行的,对于类变量,则有两种选择:在类构造器 方法中或者使用constantValue属性。
i.如果同时使用final和static休市的一个变量并且这个变量是基本数据类型或者是java.lang.string,则生产constantValue属性来进行初始化。 ii.如果这个变量没有被final修饰或者并非基本类型及字符串,则将会选择在方法中进行初始化。
innerClass属性
- innerClass属性用于记录内部类或者宿主类之间的关联。
deprecated及synthetic属性
- deprecated及synthetic两个属性都是属于标志类型的属性,只有存在和不存在的区别,没有属性值的概念。
- deprecated属性用于表示某个类、字段或者方法已经被程序不推荐使用。
- synthetic属性代表此字段或者方法并不是java代码产生的,而是编译器自行添加的。
stackMapTable属性
- 位于code属性的属性表中,会在虚拟机类加载字节码验证阶段被检查验证器使用。
signature属性
- signature属性回味他记录泛型的签名信息,之所以要专门使用这一个属性去记录泛型类型,是因为java语言泛型采用的是擦除发实现的伪泛型。
bootStrapMethod属性
- 这个属性用于保存invokedynamic执行的引导方法限定符。
字节码指令简介
- java虚拟机的指令由一个字节长度的,代表某种特定含义的数字(称为操作码)以及跟随其后零个或多个代表此操作所需要参数称为操作数而构成。
- 字节码治指令,java虚拟机限制操作码的长度为一个字节,这意味着指令集的操作码总数不可能超过256条。
- 由于class文件格式放弃了编译后代码的操作数长度对齐,这就意味着虚拟机处理那些超过一个字段数据时,不得不在运行字节中重构初具体数据结构,这种操作在某种程度上导致解释字节码时损失了一些性能,放弃了操作数长度对齐,就意味着可以省略很多填充和间隔符号,获得短小精干的编译代码。
字节码与数据类型
- 由于java虚拟机的操作码长度只有一个字节,所以指令集是射击场非完全独立的,有一些单独的指令可以将一些不支持的类型转为支持的类型。
- 大部分指令没有支持数据类型,byte、char、和short、甚至没有支持boolean类型,编译器会在编译期或者运行期将byte和short类型的数据带符号扩展为int类型数据。
加载和存储指令
- 加载和存储指令用于将数据在栈帧中的局部变量表和操作栈之间来回传输。
- 将一个局部变量加载到操作数栈:iload、iload、lload
- 将一个数值从操作数栈存储到局部变量表:istore、istore
- 将一个常量加载到操作数栈:bipush、sipush、idc
- 扩充局部变量表的访问索引的指令:wide
运算指令
- 运算算术指令用于两个操作数栈上的值进行某种运算,并且把结果重新存入操作数栈。
- 加法指令:iadd、ladd、fadd、dadd
- 减法指令:isub、lsub、ssub、dsub
- 乘法指令: imul、lmul、fmul、dmul
- 除法指令: idiv、ldiv、fdiv、ddiv
- 求余指令:irem、lrem、frem、drem
- 取反指令:ineg、lneg、fneg、dneg
- 位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
类型转换指令
- 类型转换指令可以将两种不同的数据类型进行转换。 2.java虚拟机直接支持以下数据类型的宽化类型转换,int到long、float、double、float到double、long到float、double。
- 窄化类型转换时,必须显示的使用转换指令来完成,这些转换指令包括,i2b、i2c、i2s、l2i、f2i、窄化类型转换可能会导致转换结果产生不同的正负号,以及精度丢失。
对象创建于访问指令
- 虽然类实例和数组都是对象,但是java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。、
- 创建类实例的指令:new、
- 创建数组的指令Z:newarray、anewarray、multianewarray。
- 访问类字段和实例字段:getfield、putfield、getstatic、putstatic。
- 把数组元素架子啊到操作数栈的指令:baload、caload、saload。
- 把一个操作数栈的值存储到数组元素中的指令:bastore、castore。
- 取数组的长度:arraylength
- 检查类实例类型的指令:instanceof、cheakcast。
操作数栈管理指令
- java虚拟机提供了一些用于直接操作操作数栈的指令。
- 将操作数栈的栈顶一个或者两个元素出栈:pop、pop2
- 复制栈顶一个或者两个数值将复制值重新亚茹栈顶:dup、dup2、dup_x1
- 将栈顶两个元素进行互换:swap。
控制转移指令
- 控制转移指令可以让java虚拟机有条件或者无条件的修改PC寄存器的值。
- 条件分支:ifeq、iflt、ifle、ifge、ifne、ifnell、ifnonull、
- 复合条件分支:tableswitch、lookeupswitch。
- 无条件分支:goto、goto_w、jsr、hsr_w、ret
方法调用和返回指令
-
5条方法调用指令:
i. invokevirtual:指令用于调用对象的实例方法,根据对象的实际类型进行分派虚方法分派。
ii. invokeinterface:指令用于调用接口方法,他会运行时搜索一这实现了这个接口的方法对象,找出合适的方法进行调用。
iii. invokespecial:指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
iiii. invokestatic:指令用于调用类方法(static方法)
V. invokedynamic:指令用于运行时动态解析出调用点限定符所引用的方法、并且执行了该方法前面4条调用指令分派逻辑固化在虚拟机内部,而invokedynamic指令分派逻辑是由用户所设置的引导方法决定的。
-
方法调用指令与数据类型无关,返回指令根据返回值的类型区分的ireturn、lreturn。
异常处理指令
- 在java中显式跑出的异常的操作都是由athrow指令来实现的。
同步指令
- java虚拟机支持方法级别的同步和方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的。
- 方法的同步是隐式的,无需通过字节码指令来控制,他在实现方法调用和返回操作中,虚拟机可以从方法常量池acc_syschornoized访问标志得知每个方法是否声明为同步方法。
- 如果是同步方法,执行线程就要要求先成功持有monitor,然后才能执行方法,执行线程有了monitor,其他任何线程都无法在获取同一个monitor。
- 同步一段指令集序列通常是由java语言中的synchoronized语言来表示的,java虚拟机的指令集有monitorenter和momitorexit两条指令来支持sychronized关键字的含义,
我是菜鸟,希望大家多多留言讨论~谢谢!
我的笔记是看完深入理解java虚拟机的体会,是本好书,推荐给大家.