字节码/Class文件/ClassFile 均为同一概念
当Java源代码成功编译成字节码后,如果想在不同的平台上面运行,则无须再次编译这个优势不再那么吸引人了。Python、PHP、Perl、Ruby、Lisp等有强大的解释器。跨平台似乎已经快成为一门语言必选的特性。
Java 虚拟机不和包括 Java 在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联。无论使用何种语言进行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行。可以说,统一而强大的Class文件结构,就是Java虚拟机的基石、桥梁。
想要让一个Java程序正确地运行在JVM中,Java源码就必须要被编译为符合JVM规范的字节码。
词法解析
、语法解析
、语义解析
以及生成字节码
。oracle的JDK软件包括两部分内容:
Java源代码的编译结果是字节码,那么肯定需要有一种编译器能够将Java源码编译为字节码,承担这个重要责任的就是配置在path环境变量中的javac编译器。javac是一种能够将Java源码编译为字节码的前端编译器
。
Hotspot JVM并没有强制要求前端编译器只能使用javac来编译字节码,其实只要编译结果符合JVM规范都可以被JVM所识别即可。
在Java的前端编译器领域,除了Javac之外,还有一种被大家经常用到的前端编译器,那就是内置在Eclipse中的ECJ(EclipseCompiler for Java)编译器。和Javac的全量式编译不同,ECJ是一种增量式编译器。在Eclipse中,当开发人员编写完代码后,使用“ctrl+S”快捷键时,ECJ编译器所采取的编译方案是把未编译部分的源码逐行进行编译,而非每次都全量编译。
因此ECJ的编译效率会比javac更加迅速和高效,当然编译质量和javac相比大致还是一样的。ECJ不仅是Eclipse的默认内置前端编译器,在Tomcat中同样也是使用ECJ编译器来编译jsp文件。由于ECJ编译器是采用GPLv2的开源协议进行源代码公开,所以,大家可以登录eclipse官网下载ECJ编译器的源码进行二次开发。
默认情况下,Inteli IDEA 使用 javac 编译器。
前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节移交给HotSpot的JIT编译器负责。
字节码文件/Class文件,是JVM的基石!!!
源代码经过编译器编译之后便会生成一个字节码文件,字节码是一种二进制的类文件,它的内容是JVM的指令,而不像C、C++经由编译器直接生成机器码。
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)
以及跟随其后的零至多个代表此操作所需参数的操作数(operand)
所构成。
虚拟机中许多指令并不包含操作数,只有一个操作码。
比如:
0 aload_0
1 invokespecial #1 <Father.<init> : ()V>
4 aload_0 //只有操作码
5 bipush 30 //操作码+操作数
7 putfield #2 <Son.x : I>
10 aload_0
11 invokevirtual #3 <Son.print : ()V>
14 aload_0
15 bipush 40
17 putfield #2 <Son.x : I>
20 return
任何一个Class 文件都对应着唯一一个类或接口的定义信息,但反过来说,Class 文件实际上它并不一定以磁盘文件的形式存在。
Class 文件是一组以8位字节为基础单位的二进制流。
Class 的结构不像 XML 等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
无符号数
和表
。 u1、u2、u4、u8
来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节
的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。Class文件的结构并不是一成不变的,随着Java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但是其基本结构和框架是非常稳定的。
Class文件的总体结构如下:
下图为官方给出的结构组成
结构字段解释
使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。
紧接着魔数的 4 个字节存储的是 Class文件的版本号,也是4长个字节。
其中前2个字节所代表的含义就是编译的副版本号minor version
而后2个字节就是编译的主版本号major_version
它们共同构成了Class文件的格式版本号。譬如某个 Class文件的主版本号为 M,副版本号为 m,那么这个Class文件的格式版本号就确定为 M.m。
不同版本的Java编译器编译的Class文件对应的版本是不一样的
高版本的Java虚拟机可以执行由低版本编译器生成的Class文件
低版本的Java虚拟机不能执行由高版本编译器生成的Class文件
。否则JVM会抛出java.lang.UnsupportedclassVersionError异常。常量池是整个class文件的基石
。数量是不固定的
,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant pool count)
。从1而不是0开始
的。容量计数器(constant_pool count)
加若干个连续的数据项(constant pool)
的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合。字面量和符号引用
,这部分内容将在类加载后
进入方法区的运行时常量池
中存放第1个字节作为类型标记,用于确定该项的格式
,这个字节称为tag byte (标记字节、标签字节)。String str = "ohyes";
final int num = 10;
类和接口的全限定名
:com/CSDN/test/Demo这个就是类的全限定名,仅仅是把包名的".“替换成”/",为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。字段/方法的名称
:简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子中的类的add()方法和num字段的简单名称分别是add和num。字段/方法的描述符
:描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,详见下表:举例说明
String [] arr = new String[10];
System.out.println(arr);
// 输出结果应该为:[Ljava/lang/String;@xxxxxxx
// [是数组,L是对象类型的引用,java/lang/String是指向的全限定名,@xxxxxxx就是对象具体的编号了
long [][] arr = new long[10][10];
System.out.println(arr);
// 输出结果应该为:[[J@yyyyyyy
// [[是数组,J是对象类型的引用,基本数据类型不需要限定名,@yyyyyyy就是对象具体的编号了
int abc(int[] x, int y)
// 描述符为:([II)I
// ()表示方法,里面两个参数,一个[I,一个I,返回类型为I
补充说明:
- 虚拟机在加载Class文件时才会进行动态链接,也就是说,Class文件中
不会保存
各个方法和字段的最终内存布局信息
- 因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时, 需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段
将其替换为直接引用
,并翻译到具体的内存地址中.- 这里说明下
符号引用和直接引用的区别与关联
:
符号引用
:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。直接引用
:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
在常量池后,紧跟着访问标记。该标记使用两个字节
表示一用于识别一些类或者接口层次的访问信息,包括:这个 Class是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final等。
各种访问标记如下所示:
需要注意的是,这里的两个字节,代表的是上表中各标志对应标志值的和
如:
补充说明:
ACC_INTERFACE标志
:带有ACC_INTERFACE标志的Class文件表示的是接口而不是类,反之则表示的是类而不是接口。- 如果一个Class文件被设置了 ACC_INTERFACE 标志,那么同时也得设置ACC_ABSTRACT 标志。同时它不能再设置 ACC_FINAL、ACC_SUPER 或 ACC_ENUM 标志。
- 如果没有设置ACC_INTERFACE标志,那么这个Class文件可以具有上表中除 ACC_ANNOTATION外的其他所有标志。当然,ACC_FINAL和ACC_ABSTRACT这类互斥的标志除外。这两个标志不得同时设置。
ACC SUPER标志
:用于确定类或接口里面的invokespecial指令使用的是哪一种执行语义。针对Java虚拟机指令集的编译器都应当设置这个标志
。对于Java SE 8及后绿版本来说,无论Class文件中这个标志的实际值是什么,也不管Class文件的版本号是多少,Java虚拟机都认为每个Class文件均设置了ACC_SUPER标志。- ACC_SUPER标志是为了向后兼容由旧Java编译器所编译的代码而设计的。目前的 ACC_SUPER标志在由JDK 1.0.2之前的编译器所生成的access_flags中是没有确定含义的,如果设置了该标志,那么oracle的Java虚拟机实现会将其忽略。
ACC_SYNTHETIC标志
:意味着该类或接口是由编译器生成的,而不是由源代码生成的。ACC_ANNOTATION标志
:注解类型必须设置ACC_ANNOTATION标志。如果设置了 ACC_ANNOTATION标志,那么也必须设置ACC_INTERFACE标志。ACC_ENUM标志
:表明该类或其父类为举类型。
这三项数据来确定这个类的继承关系
因为接口索引是个数组,所以必定会有字节来表示数组长度
类级变量以及实例级变量
,但是不包括方法内部、代码块内部声明的局部变量。字段的标识符、访问修饰符(public、private或protected)、是类变量还是实例变量(static)、是否是常量(final修饰符)
等。注意事项:
- 字段表集合中不会列出从父类或者实现的接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段。譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。
作用域(public、private、protected修饰符)
是实例变量还是类变量(static修饰符)
可变性(final)
并发可见性(volatile修饰符,是否强制从主内存读写)
可否序列化(transient修饰符)
字段数据类型(基本数据类型、对象、数组)
字段名称
字段表作为一个表,同样有他自己的结构:
作用域修饰符(public、private、protected)、static修饰符、final修饰符、volatile修饰符等等。因此,其可像类的访问标志那样,使用一些标志来标记字段。
需要注意的是,这里的两个字节,代表的是上表中各标志对应标志值的和
如:这两个字节对应的是0X0021,则意味着是 PUBLIC+SUPER 。
字段的访问标志有如下这些:
add()方法和num字段的简单名称分别是add和num。
这里可以参照上方的符号引用来理解
methods:指向常量池索引集合,它完整描述了每个方法的签名。
使用注意事项:
methods表中的每个成员都必须是一个method_Info结构,用于表示当前类或接口中某个方法的完整描述。如果某个method_Info结构的access flags项既没有设置 ACC_NATIVE 标志也没有设置ACC_ABSTRACT标志,那么该结构中也应包含实现这个方法所用的Java虚拟机指令
方法表的结构实际跟字段表是一样的
和字段表一样,方发表也有访问标志,部分和字段访问标志一样,部分则不同。
需要注意的是,这里的两个字节,代表的是上表中各标志对应标志值的和
如:这两个字节对应的是0X0021,则意味着是 PUBLIC+SUPER 。
方法的访问标志有如下这些:
add()方法和num字段的简单名称分别是add和num。
这里可以参照上方的符号引用来理解
int abc(int[] x, int y)
// 方法名就是abc
// 描述符为:([II)I
在Java虚拟机(JVM)的ClassFile结构中,字段的属性表和方法的属性表分别描述了字段和方法的附加信息。这些属性表可以包含各种额外的数据,例如注释、调试信息、性能优化信息等。
字段的属性表是在ClassFile的字段结构中包含的一系列属性。每个字段结构都可以包含零个或多个属性。这些属性用于提供与字段相关的额外信息。常见的字段属性有:
ConstantValue:这个属性用于表示常量字段的初始值。对于static final
字段,如果它是一个基本类型或String类型的常量,它的初始值会在这个属性中指定。
Synthetic:标记一个字段为编译器生成,而不是在源代码中显式定义。
Deprecated:标记一个字段为不推荐使用。
方法的属性表是在ClassFile的方法结构中包含的一系列属性。每个方法结构同样可以包含零个或多个属性。这些属性用于提供与方法相关的附加信息。常见的方法属性有:
Code:这个属性包含了方法的字节码,以及其他与方法实现相关的信息,如局部变量表和异常处理表。
Exceptions:列出方法抛出的异常类型。
LineNumberTable:这个属性用于调试目的,映射方法的字节码指令到源代码的行号。
LocalVariableTable:这个属性也是用于调试目的,描述方法的局部变量及其作用域。
Synthetic:标记一个方法为编译器生成,而不是在源代码中显式定义。
Deprecated:标记一个方法为不推荐使用。
RuntimeVisibleAnnotations 和 RuntimeInvisibleAnnotations:用于表示方法的注解。
字段的属性表和方法的属性表在Java应用中有重要作用:
提供元数据:属性表包含了大量的元数据,使得Java编译器、虚拟机和开发工具能够理解和操作类的结构。
支持调试和错误诊断:调试信息(如行号表和局部变量表)使得开发者可以在调试器中查看源代码对应的位置和变量状态,从而更容易定位和修复问题。
性能优化:某些属性,如Synthetic,帮助编译器和虚拟机进行性能优化,因为它们能够识别编译器生成的代码。
注解处理:属性表中的注解信息可以在运行时通过反射机制读取,从而支持框架和库的注解处理功能。
总的来说,字段和方法的属性表在Java ClassFile结构中提供了灵活和扩展性,允许在字节码级别上附加各种有用的信息,以支持不同的编译器和运行时需求。