“一次编写,到处运行(Write Once, Run Anywhere)”,因为有虚拟机的机制。
“同一份输入,不同的输出”,我们只需要生成一份字节码文件,然后同一份.class字节码文件在不同的操作系统中,由不同的虚拟机生成对应机器码。虚拟机和字节码是Java的两个最底层的原理。
最简单的编译运行流程,实际情况比这个复杂的多
万物皆可HelloWorld,字节码也不例外。
字节码文件是二进制文件,只不过我们一般打开都是16进制形式的。众所周知在二进制中,8位为一个字节,4位二进制数表示一个16进制数,也就是说两个16进制数表示一个字节,所以上图中一个字符占了半个字节长度。在下文中,u1表示占用一个字节长度,u2为两个字节长度,以此类推。
统一的CAFE BABE,其作用与文件名后缀.java、.png等是一样的作用,标示一种类型的文件,不同的文件以二进制的形式打开都能看到对应的魔数。虚拟机在加载字节码文件时会首先检查魔数。
CAFE BABE的来由:http://mishadoff.com/blog/java-magic-part-2-0xcafebabe/
第二部分是由副版本号与主版本号组成的版本号,在1.2中的HelloWorld.class中的版本号[0000 003a]能看出来我电脑中用的JDK版本是316 + 101 = 58,由于Java版本是从45开始的,所以算下来是Java14,用java -version 确认下:
一个大版本下还会有多个小版本,根据字节码文件留出来的长度可知,一个大版本下最多可以有[0xffff] + 1 = 16^4 = 65536个小版本。
由于每个新版本都会有新特性,所以老版本的虚拟机不能够兼容新版本的字节码文件,所以进行类加载的时候也会检查版本号是否兼容,假如不兼容会抛出java.lang.UnsupportedClassVersionError
的错误。
常量池算是字节码中最复杂的数据结构,常量池中会存放一些字符串常量与较大的整数,比较小的与常用的整数则是内嵌在字节码指令中,如iconst_1表示整数1入栈。
常量池的结构由常量池大小与其内容组成,常量池大小的字节码长度为两个字节,所有从这里也可以得知一个类文件中最多只能有65536个常量。
假设常量池大小为N,其中有效索引为1~N-1,0为保留索引,其中常量池则最多有N-1项,为何是最多呢?因为Long与Double的类型的常量占用两个索引位置,所以实际常量项会比N少。
一个常量项的数据结构分为类型tag 与 内容,如下:
目前Java有14种常量类型,这里不展开细讲,有兴趣的同学可以自行了解:
constant type | tag |
---|---|
CONSTANT_Utf8_info | 1 |
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 |
类访问标记用来标识一个类是否为final、abstract等,大小为两个字节,用一个位标记一种类型,目前只用了8个位
其中ACC_SUPER已经弃用,但是为了兼容旧版本的字节码文件,所以还占着这个位置。
这三个部分是用以确认一个类的继承关系,索引指向常量池中的项。例如1.2中,对应的类索引为[0x00015] = 21 索引对应的常量也是一个索引,指向的是一个字符串常量——真正的类名:
超类索引与接口表索引也是类似,这里不多赘述。
类中定义的静态与非静态字段都会存储在这个集合中,不包括方法中定义的字段。
字段表的结构与常量池相似,由长度与内容组成,如下所示:
每个字段项结构如下所示,由四部分组成:
描述符 | 类型 |
---|---|
B | byte 类型 |
C | char 类型 |
D | double 类型 |
F | float 类型 |
I | int 类型 |
J | long 类型 |
S | short 类型 |
Z | bool 类型 |
L ClassName ; | 引用类型,“L” + 对象类型的全限定名 + “;” |
[ | 一维数组 |
其中引用类型描述符比较特殊,如String类型表示为:“Ljava/lang/String;” ,由于long类型的L被引用类型占了,所以long类型用了J。
字段表后面的就是方法表,类中定义的方法都会存放在这里,其结构与字段表结构相似:
每个方法项的结构也与字段长得一摸一样,如下:
表示一个方法所需的参数与返回值,格式为“(参数1类型 参数2类型 参数3类型 …)返回值类型”。例如方法Object foo(int i, double d, Thread t)
的方法描述符为:(IDLjava/lang/Thread;)Ljava/lang/Object;
属性表不只是在顶层的class文件中出现,从上文中可以得知,字段表与方法表中也有对应的属性表。属性表的结构如下:
各种不同属性有不同的结构,具体属性要具体分析,由于篇幅有限这里只介绍最重要的ConstantValue属性与Code属性。
该属性只会出现在字段的属性表中,其结构如下:
attribute_name_index 是指向常量池中值为 “ConstantValue” 的常量项,ConstantValue 属性的 attribute_length 值恒定为 2,constantvalue_index 指向常量池中具体的常量值索引,根据变量的类型不同 constantvalue_index 指向不同的常量项。
方法中最重要的就是方法字节码,其实都包含在Code属性中,Code的属性结构如下所示:
Code 属性表的字段含义如下: