字节码文件的跨平台性
Java语言:跨平台的语言(Write Once,Run Anywhere)
- 当Java源代码编译成字节码后,如果想在不同平台上运行,则无须再次编译
- 这上优势不再那么吸引人,Python,PHP,Ruby,Lisp等有强大的解释器
- 跨平台似乎已经成为一门语言必选的特性
Java虚拟机:跨语言的平台
- Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制格式文件关联,无论使用何种语言进行软件开发,只要能将源文件编译成正确的Class文件,那么这种语言就可以在Java虚拟机上执行,统一而强大的Class文件结构,是Java虚拟机的基石和桥梁。
- 所有的JVM全部遵守Java虚拟机规范,字节码文件可以在各种JVM上执行
前端编译器
- 负责将符合Java语法规范的Java代码编译成符合JVM规范的字节码
- javac是最常用的前端编译器
- javac编译器将Java源码编译为一个有效的字节码文件过程经历4步,分别是词法分析、语法分析、语义解析以及生成字节码
Oracle JDK软件包括两部分内容
- 一部分将Java源代码编译成Java虚拟机的指令集的编译器
- 另一部分是用于实现Java虚拟机的运行时环境
public class IntegerTest {
public static void main(String[] args) {
Integer x = 5;
int y = 5;
System.out.println(x == y);
Integer i1 = 5;
Integer i2 = 5;
System.out.println(i1 == i2);
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);
}
}
//执行结果
true
true
false
//查看字节码
0 iconst_5
1 invokestatic #2 <java/lang/Integer.valueOf> //给Integer x进行赋值
4 astore_1
5 iconst_5
6 istore_2
7 getstatic #3 <java/lang/System.out>
10 aload_1
11 invokevirtual #4 <java/lang/Integer.intValue> //对Integer x进行拆箱操作
14 iload_2
15 if_icmpne 22 (+7)
18 iconst_1
19 goto 23 (+4)
22 iconst_0
23 invokevirtual #5 <java/io/PrintStream.println>
26 iconst_5
27 invokestatic #2 <java/lang/Integer.valueOf>
30 astore_3
31 iconst_5
32 invokestatic #2 <java/lang/Integer.valueOf>
35 astore 4
37 getstatic #3 <java/lang/System.out>
40 aload_3
41 aload 4
43 if_acmpne 50 (+7)
46 iconst_1
47 goto 51 (+4)
50 iconst_0
51 invokevirtual #5 <java/io/PrintStream.println>
54 sipush 128
57 invokestatic #2 <java/lang/Integer.valueOf>
60 astore 5
62 sipush 128
65 invokestatic #2 <java/lang/Integer.valueOf>
68 astore 6
70 getstatic #3 <java/lang/System.out>
73 aload 5
75 aload 6
77 if_acmpne 84 (+7)
80 iconst_1
81 goto 85 (+4)
84 iconst_0
85 invokevirtual #5 <java/io/PrintStream.println>
88 return
/**
* 成员变量赋值过程:①默认初始化②显式初始化③构造器初始化④有了对象后,可以通过对象.属性的方式对成员变量赋值
*/
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;
}
public void print() {
System.out.println("Son.x = " + x);
}
}
public class SonTest {
public static void main(String[] args) {
Father f = new Son();
System.out.println(f.x);
}
}
//执行结果
Son.x = 0
Son.x = 30
20
Class文件
- 字节码文件里是什么?
- 源代码经过编译器编译生成一个字节码文件,字节码是一种二进制的类文件,它的内容是JVM指令,不像C,C++经由编译器直接生成机器码
- 什么是字节码指令(byte code)
- Java虚拟机的指令由一个字节长度,代表某种特定操作含义的操作码以及跟随其后的零至多个代表此操作所需参数的操作数所构成,虚拟机中指令并不包含操作数,只有一个操作码
- 如何解读供虚拟机解释执行的二进制字节码
- ①通过NotePad++中安装一个HEX-Editor插件,或使用BinaryViewer
- ②使用javap指令:jdk自带反解析工具
C:\Users\Administrator\IdeaProjects\jvm\target\classes\com\chapter09>javap -v -p IntegerTest.class
- ③使用IDEA插件:jclasslib或jclasslib bytecode viewer客户端
Class文件结构
//class文件结构相应信息都使用下面代码编译为字节码进行
public class Demo {
private int num = 1;
public int add() {
num += 2;
return num;
}
}
- class类的本质
任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在,Class文件是一组以字节为基础单位的二进制流 - class文件格式
- Class的结构不像XML等描述语言,由于没有分隔符,所以其中的数据项,无论是字节顺序还是数量,都被严格限定,哪个字节代表什么含义,长度多少,先后顺序如何,都不允许改变
- Class文件格式采用一种类似于C语言结构体的方式进行数据存储,包含两种数据类型:无符号数和表
- 无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1字节,2字节,4字节和8字节无符号数,无符号数可以用来描述数字、索引引用、数量值或者按UTF8编码构成的字符串值
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以_info结尾,表用于描述有层次关系的复合结构数据,整个Class文件本质上就是一张表,由于表没有固定长度,所以通常会在其前面 加上个数说明
- class文件结构描述
- Class文件的结构并不是一成不变的,随着Java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但是其基本结构和框架是非常稳定的
- 魔数
- Class文件版本
- 常量池
- 访问标志
- 类索引、父类索引、接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
魔数(Magic Number)
- 每个Class文件开头的4个字节的无符号整数称为魔数
- 它的唯一作用是确定文件是否为一个能被虚拟机接受的有效合法的Class文件,即魔数是Class文件的标识符
- 魔数值固定为0xCAFEBABE,不会改变
- 如果一个Class文件不以oxCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出以下错误:
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 1885430635 in class file Demo
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(Unknown Source)
at java.security.SecureClassLoader.defineClass(Unknown Source)
at java.net.URLClassLoader.defineClass(Unknown Source)
at java.net.URLClassLoader.access$100(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.launcher.LauncherHelper.checkAndLoadMain(Unknown Source)
- 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地更改
Class文件版本号
- 紧接着魔数的4个字节存储的是Class文件的版本号,同样4个字节,第5、6字节所代表的含义就是编译的副版本号minor_version,第7、8字节就是编译的主版本号major_version
- 它们共同构成了class文件的格式版本号,譬如某个Class文件的主版本号为M,副版本号为m,那么这个Class文件的格式版本号就确定为M.m
- 版本号和Java编译器的对应关系统如下
- Java版本号从45开始,JDK1.1之后的每个JDK大版本发布主版本号向上加1
- 不同版本的Java编译器的Class文件对应的版本是不一样的,目前,高版本的Java虚拟机可以执行由低版本编译器生成的Class文件,但是低版本的Java虚拟机不能执行高版本编译器生成的Class文件,否则JVM会抛出java.lang.UnsupportedClassVersionError异常
- 在实际应用中,由于开发环境和生产环境的不同,可能导致该问题的发生,因此,需要在开发时,注意开发编译JDK版本和生产环境是否一致
- 虚拟机JDK版本为1.k(k >= 2)时,对应的class文件格式版本号的范围为45 - (44 + k.0)
常量池
-
常量池是Class文件中最为丰富的区域之一,常量池对于Class文件中的字段和方法解析也有着至关重要的作用
-
随着Java虚拟机的不断发展,常量内容日渐丰富,常量池是整个Class文件的基石
-
在版本号之后,是常量池的数量,以及若干个常量池表项
-
常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_pool_count),与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的
-
由上表可见,Class文件使用了一个前置的容量计数器(constant_pool_count)加若十个连续的数据项(constant_pool)的形式来描述常量池内容,我们把一系列连续常量池数据称为常量池集合
-
常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分加载后进入方法区的运行时常量池中存放
常量池计数器
- 由于常量池的数量不固定,时长时短,需要两个字节表示常量池容量计数值
- 常量池容量计数值:从1开始,表示常量池中有多少项常量,即constant_pool_count = 1表示常量池中有0个常量项
其值0x0016,掐指一算,是22
注:实际上只有21项常量,索引范围1-21,常量池是从1开始的
常量池
- constant_poo是一种表结构,以1-constant_pool_count - 1索引,表明后面有多少个常量项
- 常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)
- 它包含了class文件结构及子结构中引用的所有字符串常量、类或接口名、字段名和其他常量,常量池中的每一项具备相同的特征,第1个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte(标记字节、标签字节)
字面量和符号引用
- 全限定名
- com/chapter09/Demo这个是类的全限定名,仅把包名的".“替换为”/“,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个”;"表示全限定名结束
- 简单名称
- 简单名称是没有类型和类修饰的方法或者字段名称,上面例子中的类的add()方法和num字段的简单名称分别为add和num
- 描述符
- 描述符的作用用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值,根据描述符规则,基本数据类型(byte,char,double,float,int,long,short,boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型用字符L加对象的全限定名来表示
注:虚拟机在加载Class文件时才会进行动态链接,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的,当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中
- 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用与虚拟机实现内存布局无关,引用的目标并不一定已经加载到了内存中
- 直接引用:可以直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那说明引用的目标必定已经在于内存之中了。
注:常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项之一
常量池中为何要包含这么多内容
Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态链接,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换无法得到真正的内存地址,也就无法直接被虚拟机使用,当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。