1 JVM的“无关性”
Java具有平台无关性,也就是任何操作系统都能运行Java代码。之所以能实现这一点,是因为Java运行在虚拟机之上,不同的操作系统都拥有各自的Java虚拟机,因此Java能实现“一次编写,处处运行”。
而JVM不仅具有平台无关性,还具有语言无关性。
平台无关性是指不同操作系统都有各自的JVM,而语言无关性是指Java虚拟机能运行除Java以外的代码!
(1)平台无关性:任何操作系统都能运行 Java 代码
(2)语言无关性: JVM 能运行除 Java 以外的其他代码
这听起来非常惊人,但JVM对能运行的语言是有严格要求的。首先来了解下Java代码的运行过程。
Java源代码首先需要使用Javac编译器编译成.class文件,然后启动JVM执行.class文件,从而程序开始运行。
也就是JVM只认识.class文件,它并不管何种语言生成了.class文件,只要.class文件符合JVM的规范就能运行。
因此目前已经有Scala、JRuby、Jython等语言能够在JVM上运行。它们有各自的语法规则,不过它们的编译器都能将各自的源码编译成符合JVM规范的class文件,从而能够借助JVM运行它们。
Java 语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的, 因此字节码命令所能提供的语义描述能力肯定会比 Java 语言本身更加强大。 因此,有一些 Java 语言本身无法有效支持的语言特性,不代表字节码本身无法有效支持。
2 Class文件结构
class文件是二进制文件,它的内容具有严格的规范,文件中没有任何空格,全是连续的0/1。class文件中的所有内容被分为两种类型:无符号数 和 表。
(1)无符号数:它表示Class文件中的值,这些值没有任何类型,但有不同的长度。根据这些值长度的不同分为:u1、u2、u4、u8,分别代表1字节的无符号数、2字节的无符号数、4字节的无符号数、8字节的无符号数。
(2)表:由多个无符号数或者其他表作为数据项构成的符合数据类型。class文件中所有数据(即无符号数)要么单独存在,要么由多个无符号数组成二维表。即class文件中的数据要么是单个值,要么是二维表。
2.1 class文件的组织结构
魔数
本文件的版本信息
常量池
访问标志
类索引、父类索引、接口索引集合
字段表集合
方法表集合
属性表集合
2.1.1 Class文件的构成1:魔数
class文件的头4个字节称为魔数,用来表示这个class文件的类型。
魔数的作用就相当于文件后缀名,只不过后缀名容易被修改,不安全,因此在class文件中标示文件类型比较合适。
class文件的魔数是用16进制表示的“CAFEBABE”,非常具有浪漫主义色彩!
2.1.2 Class文件的构成2:版本信息
紧接着魔数的4个字节是版本号。5-6 字节表示次版本号,7-8 字节表示主版本号,它表示当前class文件中使用的是哪个版本的JDK。
在高版本的JVM上能够运行低版本的class文件,但在低版本的JVM上无法运行高版本的class文件,即使该class文件中没有用到任何高版本JDK的特性也无法运行,虚拟机也必需拒绝执行超过其版本号的 Class 文件。
2.1.3 Class文件的构成3:常量池
2.1.3.1 常量池的定义
紧接着版本号之后的就是常量池。常量池中存放两种类型的常量:
(1)字面值常量
字面值常量即我们在程序中定义的字符串、被final修饰的值。
(2)符号引用
符号引用就是我们定义的各种名字:类和接口的全限定名、字段的名字和描述符、方法的名字和描述符
2.1.3.2 常量池的特点
(1)常量池长度不固定
常量池的大小是不固定的,因此常量池开头放置一个u2类型的无符号数,用来存储当前常量池的容量。JVM根据这个值就知道常量池的头尾来。
注: 这个值是从1开始的,若为5表示池中有4个常量。
(2)常量池中的常量由表来表示
常量池的每一项常量都是一个表,表开始的第一位是一个 u1 类型的标志位(tag),代表当前这个常量属于哪种常量类型。
常量池开头有个常量池容量计数器,接下来就全是一个个常量了,只不过常量都是由一张张二维表构成,除了记录常量的值以外,还记录当前常量的相关信息。
常量池是class文件的资源仓库
常量池是与本class中其它部分关联最多的部分
常量池是class文件中空间占用最大的部分之一
2.1.3.3 常量池中常量的类型
刚才介绍了,常量池中的常量大体上分为:字面值常量 和 符号引用。在此基础上,根据常量的数据类型不同,又可以被细分为14种常量类型。这14种常量类型都有各自的二维表示结构。每种常量类型的头1个字节都是tag,用于表示当前常量属于14种类型中的哪一个。
类型 | tag | 描述 |
---|---|---|
CONSTANT_utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_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_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
以CONSTANT_Class_info常量(此类型的常量代表一个类或者接口的符号引用)为例,它的二维表示结构如下:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
tag是标志位,用于区分常量类型,表示当前常量的类型(当前常量为CONSTANT_Class_info,因此tag的值应为7,表示一个类或接口的全限定名);
name_index 是一个索引值,它指向常量池中一个 CONSTANT_Utf8_info 类型常量,此常量代表这个类(或接口)的全限定名,这里 name_index 值若为 0x0002,也即是指向了常量池中的第二项常量。表示这个类或接口全限定名的位置。它的值表示指向常量池的第几个常量。它会指向一个CONSTANT_Utf8_info类型的常量,CONSTANT_Utf8_info的二维表结构如下:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
CONSTANT_Utf8_info表示字符串常量;
tag表示当前常量的类型,这里应该是1;length表示这个字符串的长度;bytes为这个字符串的内容(采用缩略的UTF8编码)
问:为什么Java中定义的类、变量名字必须小于64K?
类、接口、变量等名字都属于符号引用,它们都存储在常量池中。而不管哪种符号引用,它们的名字都由CONSTANT_Utf8_info类型的常量表示,这种类型的常量使用u2存储字符串的长度。由于2字节最多能表示65535个数,因此这些名字的最大长度最多只能是64K。
问:什么是UTF-8编码?什么是缩略UTF-8编码?
前者每个字符使用3个字节表示,而后者把128个ASKII码用1字节表示,某些字符用2字节表示,某些字符用3字节表示。
2.1.4 Class文件的构成4:访问标志
在常量池之后是2字节的访问标志。访问标志是用来表示这个class文件是类还是接口、是否被public修饰、是否被abstract修饰、是否被final修饰等。
由于这些标志都由是/否表示,因此可以用0/1表示。
访问标志为2字节,可以表示16位标志,但JVM目前只定义了8种,未定义的直接写0.
2.1.5 Class文件的构成5:类索引、父类索引、接口索引集合
类索引、父类索引、接口索引集合是用来表示当前class文件所表示类的名字、父类名字、接口们的名字。
它们按照顺序依次排列,类索引和父类索引各自使用一个u2类型的无符号常量,这个常量指向CONSTANT_Class_info类型的常量,该常量的bytes字段记录了本类、父类的全限定名。
由于一个类的接口可能有好多个,因此需要用一个集合来表示接口索引,它在类索引和父类索引之后。这个集合头两个字节表示接口索引集合的长度,接下来就是接口的名字索引。
类索引和父类索引都是一个 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合,Class 文件中由这三项数据来确定类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。
由于 Java 不允许多重继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。一个类可能实现了多个接口,因此用接口索引集合来描述。这个集合第一项为 u2 类型的数据,表示索引表的容量,接下来就是接口的名字索引。
类索引和父类索引用两个 u2 类型的索引值表示,它们各自指向一个类型为 CONSTANT_Class_info 的类描述符常量,通过该常量总的索引值可以找到定义在 CONSTANT_Utf8_info 类型的常量中的全限定名字符串。
2.1.6 Class文件的构成6:字段表的集合
2.1.6.1 什么是字段表集合?
字段表集合用于存储本类所涉及到的成员变量,包括实例变量和类变量,但不包括方法中的局部变量。
每一个字段表只表示一个成员变量,本类中所有的成员变量构成了字段表集合。
2.1.6.2 字段表结构的定义
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | access_flags | 1 | 字段的访问标志,与类稍有不同 |
u2 | name_index | 1 | 字段名字的索引 |
u2 | descriptor_index | 1 | 描述符,用于描述字段的数据类型。 基本数据类型用大写字母表示; 对象类型用“L 对象类型的全限定名”表示。 |
u2 | attributes_count | 1 | 属性表集合的长度 |
u2 | attributes | attributes_count | 属性表集合,用于存放属性的额外信息,如属性的值。 |
access_flags:字段的访问标志。在Java中,每个成员变量都有一系列的修饰符,和上述class文件的访问标志的作用一样,只不过成员变量的访问标志与类的访问标志稍有区别。
name_index:本字段名字的索引。指向一个CONSTANT_Class_info类型的常量,这里面存储了本字段的名字等信息。
descriptor_index:描述符。用于描述本字段在Java中的数据类型等信息(下面详细介绍)。
attributes_count:属性表集合的长度。
attributes:属性表集合。到descriptor_index为止是字段表的固定信息,光有上述信息可能无法完整地描述一个字段,因此用属性表集合来存放额外的信息,比如一个字段的值(下面会详细介绍)。
2.1.6.3 什么是描述符?
成员变量(包括静态成员变量和实例变量)和 方法都有各自的描述符。
对于字段而言,描述符用于描述字段的数据类型;
对于方法而言,描述符用于描述字段的数据类型、参数列表、返回值。
在描述符中,基本数据类型用大写字母表示,对象类型用“L对象类型的全限定名”表示,数组用“[数组类型的全限定名”表示。
描述方法时,将参数根据上述规则放在()中,()右侧按照上述方法放置返回值。而且,参数之间无需任何符号。
2.1.6.4 字段表集合的注意点
字段表集合中不会出现从父类(或接口)中继承而来的字段,但有可能出现原本 Java 代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
一个class文件的字段表集合中不能出现从父类/接口继承而来的字段;
一个class文件的字段表集合中可能会出现程序没有定义的字段
如编译器会自动地在内部类的class文件的字段表集合中添加外部类对象的成员变量,供内部类访问外部类。
Java中只要两个字段名字相同就无法通过编译。但在JVM规范中,允许两个字段的名字相同但描述符不同的情况,并且认为它们是两个不同的字段。
2.1.7 Class文件的构成7:方法表的集合
在class文件中,所有的方法以二维表的形式存储,每张表来表示一个函数,一个类中的所有方法构成方法表的集合。
方法表的结构和字段表的结构一致,只不过访问标志和属性表集合的可选项有所不同。
volatile 关键字 和 transient 关键字不能修饰方法,所以方法表的访问标志中没有 ACC_VOLATILE 和 ACC_TRANSIENT 标志。
方法表的属性表集合中有一张Code属性表,用于存储当前方法经编译器编译过后的字节码指令。
方法表集合的注意点
如果本class没有重写父类的方法,那么本class文件的方法表集合中是不会出现父类/父接口的方法表;
本class的方法表集合可能出现程序猿没有定义的方法
编译器在编译时会在class文件的方法表集合中加入类构造器和实例构造器。
重载一个方法需要有相同的简单名称和不同的特征签名。JVM的特征签名和Java的特征签名有所不同:
Java特征签名:方法参数在常量池中的字段符号引用的集合
JVM特征签名:方法参数+返回值
2.1.8 Class文件的构成8:属性表的集合
每个属性对应一张属性表,属性表的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
评论加载中