Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。Java语言编译程序将Java源文件编译成字节码。Java虚拟机接收字节码,将其解释为具体平台上的机器指令,而后执行。
如果将Java虚拟机看做一个黑盒,那么从上一句话我们可以得到Java虚拟机的初印象:
在进入Java虚拟机的内部之前,先来了解输入即编译成字节码的class文件。
字节码文件
在MacOS机器上安装Java 1.7, Java环境信息如:
$ java -version
java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)
先常见一个HelloWorld.java文件,内容如:
public class HelloWorld {
private String helloStr = "Hello World!";
public String sayHello(String name) {
return helloStr + name + "!";
}
public static void main(String [] args) {
HelloWorld helloWorld = new HelloWorld();
System.out.println(helloWorld.sayHello("Wendll"));
}
}
使用命令javac HelloWorld.java
来编译源文件并生成字节码文件HelloWorld.class
。字节码文件是一种二进制的类文件,按照JVM规范规定的class文件的结构应为:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
这里u2,u4代表2 字节,4字节, 而cp_info, field_info, method_info, attribute_info代表其他结构体。得知class文件是一个这样的结构体,我们便可以使用任意语言的文件读取程序去解析这个字节码文件。这里我用Java的DataInputStream来读取这个文件的一部分内容,如:
FileInputStream inputStream = new FileInputStream("HelloWorld.class");
DataInputStream stream = new DataInputStream(inputStream);
System.out.println(String.format("%X",stream.readInt())); // magic: CAFEBABE
System.out.println(String.format("%X",stream.readUnsignedShort())); //minor_version: 0
System.out.println(stream.readUnsignedShort()); //major_version: 51
System.out.println(stream.readUnsignedShort()); //constant_pool_count: 53
常量池(constant_pool)
常量池是一个变长的数组,其结构体非常简单
cp_info {
u1 tag;
u1 info[];
}
从tag可以得到此cp_info的类型,cp_info的类型是约定好的,目前有14种,如CONSTANT_Utf8是1, CONSTANT_Integer是3, CONSTANT_String是8, CONSTANT_Class是7,CONSTANT_Fieldref是9, CONSTANT_Methodref是10等等。具体参考Constant pool tags
这些tag的不同意味着后续的info数组的长度是不一样的, 如CONSTANT_Utf8_info用来表示字符串常量:
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
再如CONSTANT_Class用来表示一个类或接口,其对应的是info数组的长度是2, 因为其对应的结构体是CONSTANT_Class_info:
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
CONSTANT_Long与对应的info数组的长度是8,其对应的结构体是:
CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
而对于CONSTANT_Methodref,其对应的info数组长度是4,其对应的结构体是:
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
这里的class_index的值是指向常量池数组的某一个元素,这个元素必须是CONSTANT_Class_info结构,也注意这里的要求必须指向的是class而不是interface。
疑问:如何从字节码文件中区分是class还是interface呢?从CONSTANT_Class_info结构是无法区分的。通过access_flags可以区分。access_flags
name_and_type_index也指向常量池数组的某一个元素,这个元素必须是CONSTANT_NameAndType_info结构,此结构中包含名字和描述符:
-
如果name是以 ‘<’ 开头,则其为
方法,代表着实例初始化方法,返回值为void。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值,如可以表示为`" ":()V`。 -
基本类型都有对应的大写字符表示,如C表示Char, D表示Double, Z表示Boolean, B表示Byte。
-
用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号”()”之内。如方法void inc()的描述符为”()V”,方法java.lang.String.toString()的描述符为”()Ljava/lang/String;”。
-
对于数组类型,每一个维度将使用一个前置的”[“字符来描述,如一个定义的”java.lang.String[][]”类型的二维数组,将被记录为:”[[Ljava/lang/String;”,一个整型数组”int[]”将被记录为”[I”
-
对象类型则用字符L加对象的全限定名来表示,如Ljava/lang/Object
access_flags
可以设置为ACC_PUBLIC,ACC_FINAL, ACC_SUPER, ACC_INTERFACE, ACC_ABSTRACT, ACC_SYNTHETIC, ACC_ANNOTATION, ACC_ENUM。
如设置了ACC_INTERFACE其为接口,如果没有设置ACC_INTERFACE,其为class。
怎么区分是不是抽象类(abstract class)呢?
this_class
this_class是一个索引,指向常量池数组中的一个元素,这个元素必须是CONSTANT_Class_info结构。
super_class
super_class可以为0或一个索引,指向常量池中为CONSTANT_Class_info结构的一个元素。
interfaces, fields与methods
interfaces数组只容纳那些直接出现在类声明的implements子句或者接口声明的extends子句中的父接口。
fields数组只列出在文件中由类或者接口声明了的字段才,不列出从超类或者父接口继承而来的字段。但fields列表可能会包含在对应的Java源文件中没有叙述的字段,这是因为Java编译器可以会在编译时向类或者接口添加字段。fileds数组中的每一个元素的结构如下:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
methods数组中列出该类或者接口中所声明的方法的描述。其描述结构与fields的描述结构一样,只是access_flags的值不一样,其可以设置为如ACC_ABSTRACT,ACC_PUBLIC,ACC_PRIVATE,ACC_PROTECTED,ACC_STATIC,ACC_FINAL,ACC_SYNCHRONIZED等。
attributes
在Class文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。attribute_info的结构如下:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
虚拟机预定义的属性有如:
-
Code,Java代码编译成的字节码指令
-
ConstantValue,final关键字定义的常量池
-
Exceptions,方法抛出的异常
-
LineNumberTable, Java源码的行号与字节码指令的对应关系
-
LocalVariableTable, 方法的局部便狼描述
-
Signature, 用于支持泛型情况下的方法签名
拿Code属性举例,其结构定义为Code_attribute,格式如:
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Code_attribute中也可以包含attribute_info,如LineNumberTable,LocalVariableTable, LocalVariableTypeTable, StackMapTable。
字节码反汇编(javap)
javap是JDK自带的反汇编器,可以查看java编译器为我们生成的字节码。通过它,我们可以对照源代码和字节码,从而了解很多编译器内部的工作。
常用命令选项:
- -c 输出类中各方法的未解析的代码,即构成 Java 字节码的指令。
- -verbose 输出堆栈大小、各方法的 locals 及 args 数,以及class文件的编译版本
使用命令javap -c -v HelloWorld
即可查看HelloWorld.class文件的字节码相关信息。
可以查看到的基本信息如:
Last modified Feb 11, 2017; size 964 bytes
MD5 checksum d2ae1ea5de75d19acbe8a0c18d851298
Compiled from "HelloWorld.java"
public class HelloWorld
SourceFile: "HelloWorld.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
常量池如:
Constant pool:
#1 = Methodref #15.#35 // java/lang/Object."<init>":()V
#2 = String #36 // Hello World!
#3 = Fieldref #9.#37 // HelloWorld.helloStr:Ljava/lang/String;
#4 = Class #38 // java/lang/StringBuilder
#5 = Methodref #4.#35 // java/lang/StringBuilder."<init>":()V
#6 = Methodref #4.#39 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = String #40 // !
#8 = Methodref #4.#41 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Class #42 // HelloWorld
#10 = Methodref #9.#35 // HelloWorld."<init>":()V
#11 = Fieldref #43.#44 // java/lang/System.out:Ljava/io/PrintStream;
#12 = String #45 // Wendll
#13 = Methodref #9.#46 // HelloWorld.sayHello:(Ljava/lang/String;)Ljava/lang/String;
#14 = Methodref #47.#48 // java/io/PrintStream.println:(Ljava/lang/String;)V
#15 = Class #49 // java/lang/Object
#16 = Utf8 helloStr
#17 = Utf8 Ljava/lang/String;
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 LHelloWorld;
#25 = Utf8 sayHello
#26 = Utf8 (Ljava/lang/String;)Ljava/lang/String;
#27 = Utf8 name
#28 = Utf8 main
#29 = Utf8 ([Ljava/lang/String;)V
#30 = Utf8 args
#31 = Utf8 [Ljava/lang/String;
#32 = Utf8 helloWorld
#33 = Utf8 SourceFile
#34 = Utf8 HelloWorld.java
#35 = NameAndType #18:#19 // "<init>":()V
#36 = Utf8 Hello World!
#37 = NameAndType #16:#17 // helloStr:Ljava/lang/String;
#38 = Utf8 java/lang/StringBuilder
#39 = NameAndType #50:#51 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#40 = Utf8 !
#41 = NameAndType #52:#53 // toString:()Ljava/lang/String;
#42 = Utf8 HelloWorld
#43 = Class #54 // java/lang/System
#44 = NameAndType #55:#56 // out:Ljava/io/PrintStream;
#45 = Utf8 Wendll
#46 = NameAndType #25:#26 // sayHello:(Ljava/lang/String;)Ljava/lang/String;
#47 = Class #57 // java/io/PrintStream
#48 = NameAndType #58:#59 // println:(Ljava/lang/String;)V
#49 = Utf8 java/lang/Object
#50 = Utf8 append
#51 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#52 = Utf8 toString
#53 = Utf8 ()Ljava/lang/String;
#54 = Utf8 java/lang/System
#55 = Utf8 out
#56 = Utf8 Ljava/io/PrintStream;
#57 = Utf8 java/io/PrintStream
#58 = Utf8 println
#59 = Utf8 (Ljava/lang/String;)V
Code属性如:
{
public HelloWorld();
flags: 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: ldc #2 // String Hello World!
7: putfield #3 // Field helloStr:Ljava/lang/String;
10: return
LineNumberTable:
line 1: 0
line 3: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this LHelloWorld;
public java.lang.String sayHello(java.lang.String);
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: new #4 // class java/lang/StringBuilder
3: dup
4: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
7: aload_0
8: getfield #3 // Field helloStr:Ljava/lang/String;
11: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: aload_1
15: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: ldc #7 // String !
20: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
26: areturn
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 27 0 this LHelloWorld;
0 27 1 name Ljava/lang/String;
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #9 // class HelloWorld
3: dup
4: invokespecial #10 // Method "<init>":()V
7: astore_1
8: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_1
12: ldc #12 // String Wendll
14: invokevirtual #13 // Method sayHello:(Ljava/lang/String;)Ljava/lang/String;
17: invokevirtual #14 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: return
LineNumberTable:
line 10: 0
line 11: 8
line 12: 20
LocalVariableTable:
Start Length Slot Name Signature
0 21 0 args [Ljava/lang/String;
8 13 1 helloWorld LHelloWorld;
}
Code属性中的代码是Java指令,是基于栈的体系结构,JVM就是解析这些指令,把它翻译成相应的二进制机器码来执行。Java总共有200多条指令,不过很多都是重复的。其中一个原因可能是基于网络是Java一个非常重要的特性,而且Java在设计之初就认为字节码是要在网络中传输的,为了减少网络传输流量,字节码就要尽量设计精简、紧凑。因而Java增加了很多重复指令,比如尽量减少操作数,因而我们会发现Java的很多指令都是没有操作数的;并且指令中的操作数基本上都是当无法将值放到栈中的数据,比如局部变量的索引号和常量池中的索引号。