类文件结构
一、无关性的基石
Java虚拟机的目标不只是运行Java语言,JVM通过字节码做为媒介能够运行其他语言编译后生成的Class文件。
例如使用 Java语言编写的.java文件经过 javac编译器的编译后,生成了对应的.class文件
使用JRuby语言编写的.rb文件经过jrubyc编译器编译后,生成了对应的.class文件,这两者都能在Jvm上运行
二、Class文件的结构
任何一个class文件唯一对应了一个类或接口
相反,一个任何一个类或者接口不一定对应有class文件(动态代理,只在运行时生成类)
Class文件是一组以字节为基础单位的二进制流,且文件中没有多余的分隔符,文件中存储的几乎都是程序运行时需要的数据
2.1、魔数和Class文件的版本
这部分数据为Claa文件前8个字节
1~4字节:Class文件开始的四个字节称为魔数,唯一作用是标识该Class文件是不是能被JVM接收的文件 (做文件身份识别用),魔数取值为 :0xCAFEBABE
5~8字节:紧接着的四个字节存储了该Class文件的版本号,12字节为次版本,34字节为主版本。需要注 意的是,高版本的JDK可以兼容低版本的Class文件,但低版本的JDK不能运行高版本的Class文 件。
JVM在验证Class文件时会校验Class文件版本,高于JDK版本的文件将被拒绝执行
2.2、常量池
常量池的入口使用u2数据来描述常量池中常量的个数,计数从1开始(0位做为不引用时指向)
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)
字面量类似Java中的常量例如:文本字符串、final修饰的常量值登
整个常量池主要包含以下几类常量
- 被模块导出或者开放的包
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 方法的句柄和方法类型
- 动态调用点和动态常量
常量池中每一项常量都对应者一张表,且种类型的常量对应的表结构不同
JDK13中一共有17种不同类型的常量
17种常量类型
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Login_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型的符号引用 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 标识方法的句柄 |
CONSTANT_MethodType_info | 16 | 标识方法类型 |
CONSTANT_Dynamic_info | 17 | 表示一共动态计算常量 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
CONSTANT_Module_info | 19 | 表示一个模块 |
CONSTANT_Package_info | 20 | 表示一个模块中开放或者导出的包 |
细节:因为CONSTANT_Utf8_info型常量的最大长度为u2的最大长度65535,所以Java程序中如果一共变量名超过64KB,将不能被编译
JDK中提供有用于分析Class文件的工具:Javap
使用方法:javap -verbose TestClass.java
2.3、访问标志
常量池结束后紧接着使用 2个字节(16位来表示Class文件的访问标志,每个位标识是或否)
暂时只使用了16位中的9位作为标志位,其余7位一律为0
9个标志位
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final(只有类可以设置) |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义,invokespecial指令的语义在JDK1.0.2发生过改变,JDK1.0.2之后编译的Class文件默认为真 |
ACC_INTERFACE | 0x0200 | 标识是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,抽象类和接口为真,其他类型为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码生成 |
ACC_ANNOTATION | 0x2000 | 标识是否为注解 |
ACC_ENUM | 0x4000 | 标注是否为枚举 |
ACC_MODULE | 0x8000 | 标识是否为模块 |
2.4、类索引、父类索引、接口索引集合
类索引和父类索引:因为一个类只能继承一个类,所以都使用固定长度u2(两个字节)来标识类的全限定名和父类的全限定名。
索引过程
- 索引值都指向常量池中一个CONSTANT_Class_info
- CONSTANT_Class_info中的索引值指向CONSTANT_Utf8_info
- 从而确定类的全限定名和父类的全限定名
2.5、字段表集合
字段表用于描述接口或者类中声明的变量(实例级变量和类变量)
-
字段的作用域(private、protect、public)
-
是实例变量还是类变量(static修饰符)
-
可变性(final)
-
并发可见性(volatile修饰符,是否强制从主内存中读写)
-
可否被序列化(transient修饰符)
-
字段数据类型(基本数据类型、对象、数组)
字段表中不会列出父类或者父接口继承来的字段,但可能会出现原本不在代码中的字段,比如内部类生成的Class文件中就会有外部类的字段
全限定名
org/fenixsoft/clazz/TestClass 类全名中的.替换成/即可
简单名称
例如方法func的简单名称就为 func,不带修饰符等描述
字段描述符
基本数据类型和void都有专门的大写字母对应,引用数据类型则在全限定名前加上字幕L
标识字符 | 含义 | 标识字符 | 含义 |
---|---|---|---|
B | 基本数据类型byte | J | 基本数据类型J |
C | 基本数据类型char | S | 基本数据类型short |
D | 基本数据类型double | Z | 基本数据类型boolean |
F | 基本数据类型float | V | 特殊类型void |
I | 基本数据类型int | L | 对象类型,如Ljava/lang/Object |
如果是数组类型,则在类型前+’[’ ,一个’[’ 表明是一维数组,以此类推
方法描述符
只描述方法参数+返回参数
例如方法 void fun()的描述符就为 ()V
方法String toString() 的描述符就为()Ljava/lang/String
2.6、方法表集合
方法表与字段表极其相似,方法表用来描述类中方法信息
同样,方法表中不对父类继承而来的方法进行描述(如果对父类方法复写,则存在)
依次包括
- 访问标志(private、protect、public、default)
- 名称索引(对应到常量池中的名称)
- 描述符索引(对应到常量池中)
- 属性表集合
方法的代码经过编译成字节码之后会存放在属性表中的CODE属性中
编译器通常会为类生成类构造器() 和实例构造器 () 方法
重载:同一个类中,方法名称(简单名称)相同,且具有不同的特征签名
特征签名:Java代码范围的特征签名包括(方法名称、参数数量、参数类型、参数顺序)
字节码的特征签名包括(方法名称、参数数量、参数类型、参数顺序、检查异常表、返回值)
所以对Class文件来说,方法的返回值不同也是一种重载
2.7、属性表集合
Class文件、字段表、方法表都可以携带自己的属性表集合
在JavaSE 12中,属性表集合中预定义的属性类型包括29种,自定义的编译器向其中添加与预定义不重名的属性也是合法的
下面介绍常用的几种属性
Code属性
使用位置:方法表
含义:Java方法体中的代码编译后生成的字节码指令
并不是所有方法表都具有Code属性,比如抽象类或者接口中的抽象方法
Code属性表常用的属性
-
max_stack 表示操作数栈深度的最大值,JVM在执行方法时,不会超过其最大值,JVM为方法分配栈帧中的操作栈深度时也是根据这项属性来分配
-
max_locals表示方法中局部变量表需要的存储空间,即多少个变量槽(并不是代码中有多少个变量就分配多少变量槽,而是根据同时生存的最大变量数来分配–变量槽可以复用)
-
code_length用来存储生成的字节码Code中字节码的长度,字节码指令使用一个字节码来表示操作类型,一个字节可以表示256种不同指令,目前位置Java中规范了约200条指令,可以根据虚拟机字节码指令表查询
code_length虽然使用u4来表示,实际上只允许使用u2,即一个方法中生成的字节码指令不能超过65535条指令,超过后编译器将拒绝编译
可能会出现的情况:JSP编译器会将JSP的内容和信息全部放到一个方法中,这种情况就可能引起代码过长编译器拒绝编译
this关键字的实现
注意:使用javap对class文件解析后可以看到,即使没有参数的方法,其参数个数仍为1。
这其实是因为Java在每个方法中都可以通过this访问方法的主体,而this的实现方式就是在编译器生成字节码时,对在方法将this作为普通参数来访问,因此实例方法的局部变量槽的第一个位置其实默认放的是调用方法对象的实例
Exception属性
描述方法抛出的异常(throws抛出)
LineNumberTable
描述java源码和字节码行号之间对应关系
并不是运行时不要信息,可以通过在javac中添加参数 -g:none 或 -g:lines 来取消生成。
取消后,当抛出异常时将不会打印行号,并且无法根据源码来设置断点
三、字节码指令简介
JVM与普通指令集架构不同的是,JVM采用面向操作数栈,而不是面向寄存器
一条指令:一个字节的操作码+0~N个参数
因为使用一个字节来表示操作码,所以JVM中操作码个数不能超过256个
3.1、字节码与数据类型
大多数操作码其实已经表达了操作数的数据类型
例如iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令用于加载float类型的数据
操作码助记符上都有特殊字符表示操作数类型,例如:l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference
因为操作码使用一个字节来表示所带来的限制,并不是没做类型都有所有对应的操作符。有一些类型的数据在操作时会被转化成可支持的类型。比如对于byte、short、char、boolean来说,编译器会在编译器或者运行期将其转换成对int的操作。
实际上大部分对boolean、short、char、short的操作都是使用int来进行操作的
3.2、加载存储指令
加载存储指令用于将数据在栈帧中的局部变量表和操作数栈之间传输
- 将局部变量加载到操作栈:iload、lload…
- 将数值从操作数栈存储到局部变量表:istore,lstore…
- 将常量加载到操作数栈:bipush、sipush…
3.3、运算指令
算术指令用于对操作数栈上的两个值进行某种特定的运算,并将结果保存到操作数栈顶
- 加法指令:iadd,ladd,fadd,dadd
- 减法指令:isub,lsub,fsub,dsub
- 乘法指令:imul,lmul,fmul,dmul
- 除法指令:idiv,ldiv,fdiv,ddiv
- 求余指令:irem,lrem,frem,drem
- 取反指令:ineg,lneg,fneg,dneg
- 位移指令:ishl,ishr,iushr,lshl,lshr,lushr
- 按位或指令:ior,lor
- 按位与指令:iand,land
- 按位异或指令:ixor,lxor
- 局部变量自增指令:iinc
- 比较指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp
整数运算不会抛出异常,浮点数运算中如果除数为0会抛出异常
3.4、类型转化指令
将两种不同类型进行转换
小范围转大范围被称为 宽化转换 ,这种转换属于安全转换,这种转换由JVM直接支持,不需要指令
大范围转小范围被称为 窄化转换,这种转换需要对应的转换指令才行,转换过程中可能会出现精度丢失,上下限溢出等问题,但虚拟机不会抛出异常。
3.5、对象创建与访问指令
在Java中,虽然类实例和数组都是对象,当JVM对类实例和数组的创建和操作上使用了不同的字节码指令
- 创建类实例的指令:new
- 创建数组的指令:newarray、anewarrary、multianewarray
- 访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令:getfield、putfield、getstatic、putstatic
- 把数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、dastore、aastore
- 取数组长度的指令:arraylength
- 检查类实例类型的指令:instanceof、checkcast
3.6、操作数栈管理指令
和操作普通数据结构中的堆栈一样,Java虚拟机提供了直接操作操作数栈的指令
- 将操作数栈顶一个或两个元素出栈:pop、pop2
- 复制栈顶一个或两个元素并将复制值或双份复制值重新压入栈顶:dup、dup2
- 将栈最顶端的两个数值呼唤:swap
3.7、控制转移指令
控制转移指令将Java虚拟机有条件或无条件的从指定位置指令的下一条指令开始执行
3.8、方法调用和返回指令
- invokevirtual指令:用于调用对象的实例方法,根据对象的实际类型进行分派
- invokeinterface指令:用于调用接口方法,在运行时搜索实现了这个接口的对象,找出适合的方法进行调用
- invokespecial指令:用于调用需要特殊处理的实例方法,例如:实例初始化方法、私有方法、父类方法
- invokestatic指令:用于调用类的静态方法
- invokedynamic指令:运行时动态解析出调用点限定符所引用的方法
前四条指令的执行逻辑都是用JVM指定的,最后一条指令的逻辑分配由用户设定的引导方法决定
3.9、异常处理指令
JVM中处理异常不是由字节码指令来实现的,而是由异常表实现的
3.10、同步指令
Java虚拟机支持方法级的同步和方法内部一段指令序列的同步,两种同步都是用了管程(锁)来实现。
方法级同步
方法级同步是隐式的,不需要通过字节码来实现,虚拟机会在查看该方法的方法常量池中ACC_SYNCHRONIZED访问标 志是否声明该方法为同步方法,如果该方法被声明成同步方法。
当同步方法被调用时,虚拟机会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果被设置,则执行线程需要先 持有管程才能执行方法。
在方法执行期间,执行线程持有管程,其他线程无法再获取管程。
执行线程正常执行结束后释放管程。当执行线程在方法内部出现异常时且无法解决时,管程会在异常被抛出时外部时释 放。
指令序列级同步
指令集序列的同步通常是由Java语言中的synchronized语句块来表示,Java虚拟机的指令集中有monitorenter和 monitorexit来实现。需要编译器在编译时对应生成字节码序列(monitorenter和monitorexit两个指令必须成对出现)
总结
Class文件是Java虚拟机执行引擎的入口,也是Java技术体系的基础支柱之一。了解Class文件的结构对后面进一步了解虚拟机执行引擎有很重要的意义