前言:
在前几篇文章中:
我们大概介绍了JVM、字节码、类加载器和JVM运行时数据区的概念,现在让我们进入JVM的重要部分—.class文件的结构。所以本篇文章的主题主要包含以下2个部分:
1.Java语言的平台无关性和JVM的语言无关性
2.字节码.class文件的结构
1.Java语言的平台无关性和JVM的语言无关性
Java语言的平台无关性
《深入理解Java虚拟机-第二版》中第6章开头就写到:
代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。
为什么这么说呢?作者下面也解释的很明白。因为在虚拟机出现之前,程序要想正确运行在计算机上,首先要将代码编译成二进制本地机器码,而这个过程是和电脑的操作系统OS、CPU指令集强相关的,所以可能代码只能在某种特定的平台下运行,而换一个平台或操作系统就无法正确运行了。随着虚拟机的出现,直接将程序编译成机器码,已经不再是唯一的选择了。越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。Java就是这样一种语言。“一次编写,到处运行”于是成立Java的宣传口号。
正是虚拟机和字节码(ByteCode)构成了平台无关性的基石,从而实现“一次编写,到处运行”
Java虚拟机将.java文件编译成字节码,而.class字节码文件经过JVM转化为当前平台下的机器码后再进行程序执行。这样,程序猿就无需重复编写代码来适应不同平台了,而是一套代码处处运行,至于字节码怎样转化成对应平台下的机器码,那就是Java虚拟机的事情了。
JVM的语言无关性
Java语言通过JVM虚拟机和字节码(ByteCode)实现了平台无关性,那么语言无关性又是什么意思?其实,在Java虚拟机设计之初,作者非常前瞻性的说过:
"In the future,we will consider bounded extensions to the Java virtual machine to provide better support for other languages" 在未来,我们会对java虚拟机进行适当的拓展,以便更好的支持其他语言运行于JVM之上。
时至今日,商业机构和开源机构以及在Java语言之外发展出一大批在Java虚拟机之上运行的语言,如Groovy,JRuby,Jython,Scala等等。这些语言通过各自的编译器编译成为.class文件,从而可以被JVM所执行。
所以,由于Java虚拟机设计之初的定位,以及字节码(ByteCode)的存在,使得JVM可以执行不同语言下的字节码.class文件,从而构成了语言无关性的基础。或许在未来,语言无关性的优势会赶超Java平台无关性的优势。。。
2.字节码.class文件的结构
根据Java虚拟机的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有2种数据类型:无符号数和表。
当然无论是无符号数还是表,Class文件都是以8位(8bit),一个字节为单位存储的,各个数据项目紧密无间隔排列的二进制流。当数据项长度超过8位时,按照高位在前(Big Endian)的方式分隔成若干个8位字节存储。
无符号数用u表示后面跟1、2、4、8代表1个字节、2个字节、4个字节、8个字节。无符号数用来描述数字、索引引用、数量值、字符串值。
表则是由无符号数或者其他表作为数据复合而成的数据类型,所有表都习惯以_info结尾。
整个Class文件实质上就是一张表,其中的数据项由各个子表和无符号数构成。Class文件的格式如下:
此处需要注意的是,由于class文件没有任何分隔符号,所有.class文件中所有的数据项(表或无符号数)都是按照图表中的顺序依次排列好的,所以我们可以在.class文件中依照字节的顺序来查看对应数据项的详细信息。
这里我们以一个.class文件为例,看看其具体的字节信息,源码如下:
package JustCoding.Practise;
public class ConstantPool {
private static String a = "Class";
public int VERSION = 100;
private static void test1(String s){
String b = "Method ";
String c = b + s;
System.out.println("合并后的字符串:"+c);
}
public static void main(String[] args){
test1(a);
}
}
用vim打开其.class文件查看其16进制文件如下:
魔数magic
如上图所示,是ConstantPool.class文件的16进制表示,前4个字节为ca fe ba be,这个即为表6-1中的magic,magic译为“魔数”,在.class文件的头4个字节,它的唯一作用是确定这个文件是否是能够被虚拟机识别的class文件,其值是固定的为0xCAFEBABE(咖啡宝贝),这个也是Java语言中一段有意思的“黑”历史了,哈哈🙃
版本声明major_version、minor_version
紧挨着魔数后的第5、6两个字节存储的是minor_version,即Java的次版本号,第7、8两个字节是major_version主版本号。可以看见这里此版本号为0x0000,主版本号为0x0034。
每个Java版本都有对应的主、次版本号可以查询。
例子中的0x0034对应10进制的52,表示JDK的主版本号为1.8。
常量池计数项constant_pool_count
版本声明后,是一个2个字节的无符号数u2用于标志常量池容量,此处0x0040,等于10进制下的64,表明常量池中有63项常量。
(此处有个小设计,容量计数是从1开始而不是从0开始,故64-1=63)
常量池表constant_pool
接着就到了常量池表cp_info。此处常量池表就是之前文章Java虚拟机—堆、栈、运行时数据区中提到的,方法区中的运行时常量池。
5.1运行时常量池
运行时常量池(Runtime Constant Pool)是.class文件中每一个类或接口的常量池表(constant pool table)的运行时表示形式,属于方法区的一部分。每一个运行时常量池都在Java虚拟机的方法区中分配,在加载类和接口道虚拟机后,就创建对应的运行时常量池。常量池的作用是:
存放编译器生成的各种字面量和符号引用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时解析、翻译到具体的内存地址之中。
字面量(Literal),通俗理解就是Java中的常量,如文本字符串、声明为final的常量值等。
符号引用(Symbolic References)则是属于编译原理中的概念,包括了下面三类常量:
1.类和接口的全限定名
2.字段的名称和描述符
3.方法的名称和描述符
常量池可以理解为class文件中的资源仓库,它是class文件结构中与其他项目关联最多的数据类型,也是占用class空间最大的一个数据项。
因为常量池中常量的数量不是固定的,所以需要2字节的无符号u2(constant_pool_count)代表常量池容量计数值,(此处有个小设计,容量计数是从1开始而不是从0开始)。
常量池中的每一项常量都是一个表。每个常量项表中第一位是一个u1类型的标志位,用于标志常量的类型,具体各个常量表如下图所示(目前有14种类型的常量,表中只列了11项):
到此,我们来看一下用javap -v ConstantPool.class反编译一下.class文件来看看字节码的组成情况:
Classfile xxx/.../ConstantPool.class
Last modified 2018年9月20日; size 1063 bytes
MD5 checksum 024d748f4dc1776164f6c3e8e19cf95b
Compiled from "ConstantPool.java"
public class JustCoding.Practise.ConstantPool
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #14 // JustCoding/Practise/ConstantPool
super_class: #15 // java/lang/Object
interfaces: 0, fields: 2, methods: 4, attributes: 1
Constant pool:
#1 = Methodref #15.#39 // java/lang/Object."<init>":()V
#2 = Fieldref #14.#40 // JustCoding/Practise/ConstantPool.VERSION:I
#3 = String #41 // Method
#4 = Class #42 // java/lang/StringBuilder
#5 = Methodref #4.#39 // java/lang/StringBuilder."<init>":()V
#6 = Methodref #4.#43 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = Methodref #4.#44 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#8 = Fieldref #45.#46 // java/lang/System.out:Ljava/io/PrintStream;
#9 = String #47 // 合并后的字符串:
#10 = Methodref #48.#49 // java/io/PrintStream.println:(Ljava/lang/String;)V
#11 = Fieldref #14.#50 // JustCoding/Practise/ConstantPool.a:Ljava/lang/String;
#12 = Methodref #14.#51 // JustCoding/Practise/ConstantPool.test1:(Ljava/lang/String;)V
#13 = String #52 // Class
#14 = Class #53 // JustCoding/Practise/ConstantPool
#15 = Class #54 // java/lang/Object
#16 = Utf8 a
#17 = Utf8 Ljava/lang/String;
#18 = Utf8 VERSION
#19 = Utf8 I
#20 = Utf8 <init>
#21 = Utf8 ()V
#22 = Utf8 Code
#23 = Utf8 LineNumberTable
#24 = Utf8 LocalVariableTable
#25 = Utf8 this
#26 = Utf8 LJustCoding/Practise/ConstantPool;
#27 = Utf8 test1
#28 = Utf8 (Ljava/lang/String;)V
#29 = Utf8 s
#30 = Utf8 b
#31 = Utf8 c
#32 = Utf8 main
#33 = Utf8 ([Ljava/lang/String;)V
#34 = Utf8 args
#35 = Utf8 [Ljava/lang/String;
#36 = Utf8 <clinit>
#37 = Utf8 SourceFile
#38 = Utf8 ConstantPool.java
#39 = NameAndType #20:#21 // "<init>":()V
#40 = NameAndType #18:#19 // VERSION:I
#41 = Utf8 Method
#42 = Utf8 java/lang/StringBuilder
#43 = NameAndType #55:#56 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#44 = NameAndType #57:#58 // toString:()Ljava/lang/String;
#45 = Class #59 // java/lang/System
#46 = NameAndType #60:#61 // out:Ljava/io/PrintStream;
#47 = Utf8 合并后的字符串:
#48 = Class #62 // java/io/PrintStream
#49 = NameAndType #63:#28 // println:(Ljava/lang/String;)V
#50 = NameAndType #16:#17 // a:Ljava/lang/String;
#51 = NameAndType #27:#28 // test1:(Ljava/lang/String;)V
#52 = Utf8 Class
#53 = Utf8 JustCoding/Practise/ConstantPool
#54 = Utf8 java/lang/Object
#55 = Utf8 append
#56 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#57 = Utf8 toString
#58 = Utf8 ()Ljava/lang/String;
#59 = Utf8 java/lang/System
#60 = Utf8 out
#61 = Utf8 Ljava/io/PrintStream;
#62 = Utf8 java/io/PrintStream
#63 = Utf8 println
{
public int VERSION;
descriptor: I
flags: (0x0001) ACC_PUBLIC
public JustCoding.Practise.ConstantPool();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 100
7: putfield #2 // Field VERSION:I
10: return
LineNumberTable:
line 3: 0
line 7: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this LJustCoding/Practise/ConstantPool;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: getstatic #11 // Field a:Ljava/lang/String;
3: invokestatic #12 // Method test1:(Ljava/lang/String;)V
6: return
LineNumberTable:
line 15: 0
line 16: 6
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 args [Ljava/lang/String;
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #13 // String Class
2: putstatic #11 // Field a:Ljava/lang/String;
5: return
LineNumberTable:
line 5: 0
}
SourceFile: "ConstantPool.java"
可以看到,minor version: 0 major version: 52;Constant pool共有63项。和我们之前看16进制码时是一一对应的。
访问标志access_flags
在常量池表后面的两个字节代表访问标志,用于标志类或接口层次的访问信息。如:这个Class文件是类还是接口?是否是public?是否为抽象的abstract?是否为final的等。
类索引this_class、父类索引super_class和接口索引集合intefaces
类索引用于确定此类的全限定名称:JustCoding/Practise/ConstantPool,父类索引super_class用于确定这个类父类的全限定名:java/lang/Object。接口索引集合intefaces用来描述这个类实现了哪些接口。
类索引this_class、父类索引super_class都是一个u2类型的数据,接口索引集合包含一个u2类型的接口计数项intefaces_count和若干个u2类型的数据集合。
字段表集合fields_count+field_info
字段表集合用于描述接口或类中声明的变量。字段filed包括类变量、实例变量,但不包括方法内部声明的局部变量。字段表结构如下:
字段表集合中第一项是access_flags,需要注意的是,这里的access_flags和之前类中的access_flags类似,是一个u2类型的数据,表示字段访问标记,可以设置9个标记位用于标记字段是否为:public,private,protected,static,final,volatile,transient,enum,是否由编译器自动产生。
然后是name_index和descriptor_index,他们分别代表字段的简单名称和字段OR方法的描述符。方法表集合用于存储此类或接口中包含的方法,表结构和字段表类似。简单名称是指没有类型修饰、没有参数修饰的字段OR方法名称。简单名称很好理解,在例子中有:a ,VERSION ,test1。描述符descriptor则稍微麻烦点,描述符的作用是用来描述字段的数据类型、方法参数列表和返回值。例子中描述符有:I , ()V,([Ljava/lang/String;)V这几个。
最后是属性表集合attributes_count+attribute_info,用于记录一些属性。
方法表集合methods_count+method_info
和字段表集合类似,此处需要注意的是,通过访问标志accessflags、名称索引nameindex、描述符索引descriptorindex来定义了方法,方法的实际代码存放在属性表attribute_info中的“Code”属性中。
属性表集合attributes_count+attribute_info
属性表在前面已经出现了多次,在class文件、字段表、方法表中都可以包含自己的属性表集合用于描述自己特定的属性。class文件中其他的数据项对顺序、长度和内容要求十分严格,而对属性表则相对宽松,不再要求属性表具有严格顺序,且只要不和已有属性重名,即可向属性表中写入自己定义的属性。JVM运行时会忽略掉它所不认识的属性。
Java虚拟机规范(Java8)中预定义了23种属性,按照不同分类,大致可分为3类:
熟悉了这些属性,学习了class文件结构,这时再用javap -v xxx.class命令来反编译一下字节码,看上去就清晰多了。所以下一篇文章我们就来对着反编译后的.class文件来继续学习和讲解JVM字节码指令,毕竟JVM指令才是整个JVM的核心。