jvm(java虚拟机)是java程序的运行平台,也是java语言实现平台无关性的基石。但实际上,并不仅仅java程序能运行于jvm之上,目前Scala,Kotlin,Groovy等语言也以jvm为运行平台,并且未来可能会出现更多以jvm为运行载体的语言。所以jvm并不仅仅实现了平台无关性,还实现了语言无关性,而这一切的基础就是java字节码,也就是我们编译java源码后得到的.class文件。
字节码(class文件)是一种由java虚拟机规范所定义的特定格式的二进制文件,一个java虚拟机就是一个按照java虚拟机规范的基本标准,能正确读取class文件并执行class文件所定义的操作的程序。除了基本标准外,java虚拟机在内存布局、垃圾收集等细节方面可以有不同的实现,我们目前讨论虚拟机时一般以官方的hotspot为例来讨论。
下面以一段简单的java代码来学习class文件:
public class ClassFileTest {
public static final boolean WRONG = false;
public static int HUNDRED = 100;
private final double PI = 3.14;
private String str = "a";
private int age = 2;
public int simpleMethod() throws RuntimeException {
int a = 1;
int b = 2;
return a + b;
}
}
用jdk提供的javac工具将其编译成class文件,我们用文本编辑器打开.class文件,可以看到是一串二进制文件(由于二进制阅读不便,这里编辑器已经自动转成16进制表示)。既然叫字节码文件(bytecode),那自然是以字节(byte)为最小单位。在16进制中,每两位(8个bit)代表一个字节。
cafe babe 0000 0034 002a 0a00 0a00 2106
4009 1eb8 51eb 851f 0900 0900 2208 0023
0900 0900 2409 0009 0025 0900 0900 2607
0027 0700 2801 0005 5752 4f4e 4701 0001
5a01 000d 436f 6e73 7461 6e74 5661 6c75
6503 0000 0000 0100 0748 554e 4452 4544
0100 0149 0100 0250 4901 0001 4401 0003
7374 7201 0012 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 0100 0361 6765 0100
063c 696e 6974 3e01 0003 2829 5601 0004
436f 6465 0100 0f4c 696e 654e 756d 6265
7254 6162 6c65 0100 0c73 696d 706c 654d
6574 686f 6401 0003 2829 4901 000a 4578
6365 7074 696f 6e73 0700 2901 0008 3c63
6c69 6e69 743e 0100 0a53 6f75 7263 6546
696c 6501 0012 436c 6173 7346 696c 6554
6573 742e 6a61 7661 0c00 1600 170c 0011
0012 0100 0161 0c00 1300 140c 0015 0010
0c00 0f00 1001 000f 612f 436c 6173 7346
696c 6554 6573 7401 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0100 1a6a 6176
612f 6c61 6e67 2f52 756e 7469 6d65 4578
6365 7074 696f 6e00 2100 0900 0a00 0000
0500 1900 0b00 0c00 0100 0d00 0000 0200
0e00 0900 0f00 1000 0000 1200 1100 1200
0100 0d00 0000 0200 0200 0200 1300 1400
0000 0200 1500 1000 0000 0300 0100 1600
1700 0100 1800 0000 3b00 0300 0100 0000
172a b700 012a 1400 02b5 0004 2a12 05b5
0006 2a05 b500 07b1 0000 0001 0019 0000
0012 0004 0000 0003 0004 0009 000b 000b
0011 000d 0001 001a 001b 0002 0018 0000
0028 0002 0003 0000 0008 043c 053d 1b1c
60ac 0000 0001 0019 0000 000e 0003 0000
0010 0002 0011 0004 0012 001c 0000 0004
0001 001d 0008 001e 0017 0001 0018 0000
001e 0001 0000 0000 0006 1064 b300 08b1
0000 0001 0019 0000 0006 0001 0000 0007
0001 001f 0000 0002 0020
根据java虚拟机规范,class文件由无符号数和表组成,一般用u1、u2、u4 分别表示 1个字节、2个字节、4个字节的无符号数,表是由多个无符号数及其他表组成的复合单位,名称一般以_info结尾,整个class文件其实就是一张总的大表。class文件的构成如下所示:
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_info_count | 1 |
cp_info | constant_pool_info | constant_info_count-1 |
u2 | access_flag | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interface_count | 1 |
u2 | interface | interface_count |
u2 | field_count | 1 |
field_info | field | field_count |
u2 | method_count | 1 |
method_info | method | method_count |
u2 | attribute_count | 1 |
attribute_info | attribute_count | attribute_count |
class文件是一种有序紧凑排列的二进制文件,当元素数量不确定时,如字段数量、方法数量,都会在前面预置一个u2无符号数来明示数量,这个数量在编译成class文件时就可以确定下来。
jdk提供了javap工具来查看class文件,这里推荐另一个工具来查看class文件,工具地址:https://github.com/zxh0/classpy。下载完毕后用它打开上面编译完的ClassFileTest.class文件,界面如下:
1. 魔数与版本号
魔数起到标记作用,代表这是一个面向java虚拟机的二进制文件,java虚拟机只会接受以指定魔数开头的二进制文件,查看开头的4个字节,以16进制表示为CAFEBABE(咖啡宝贝),这里不妨联想下java的标志是什么,哈哈。
接下来的4个字节代表版本号,版本号限定了能接受此class文件的虚拟机版本,高版本的虚拟机能执行低版本的class文件,但不能执行比自己版本更高的class文件。举个例子,jdk 1.5版本的虚拟机能执行jdk 1.1 至 1.5版本的class文件,但不能执行1.5版本以上的class文件。可以看到此class文件的版本号是52.0,我们手动将0034改为0044,将版本号升到68,再执行java ClassFileTest用虚拟机(jdk 1.8版本的)加载class文件,将得到java.lang.UnsupportedClassVersionError的报错。
2. 常量池
接下来一部分是常量池,常量池一般是整个class文件中最大的一部分:
由于常量池常量数不确定,所以先用一个u2表示常量数,需要注意的是,常量池的索引是从1开始的,所以常量的数量是常量数-1。ClassFileTest.class的常量数是42,代表有41个常量。千万不要将class文件的常量与java语言中的static final常量等同,class文件的常量池简单的说就是一堆定义此class文件的值的集合,以一个语境来理解,比如“小明是一个20岁的男生”,拆分成常量池就是:小明、20、年龄、男、性别。class文件常量池后的类名、字段、方法、属性表等都会引用常量池。
常量都是表类型的数据,每个常量开头将用u1字节表示tag,指明此常量类型,不同tag的常量的表结构不一样。由于用u1表示tag,所以不扩充的话一共能支持到255种常量类型,目前jdk 1.8定义了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 | 字段和方法的名称及描述符 |
利用前面提到的工具简单看几个常量:
常量索引9: tag为7,代表是CONSTANT_Class_info,下来的u2类型的00 27表示指向常量索引39。继续看索引为39的常量,tag为1代表是CONSTANT_UTF8_info,接下来的15个字节按utf8编码为a/ClassFileTest,所以此CONSTANT_Class_info代表名称为a/ClassFileTest的类名。
常量索引7: tag为9,代表是CONSTANT_Fieldref_info,tag后的u2类型的00 09代表类索引,指向常量索引9,正是我们上面刚看的a/ClassFileTest;再之后的00 25指向索引为37的常量,这是一个tag为12,即CONSTANT_NameAndType_info类型的常量,它的名称索引为21,查看索引为21的常量,是一个UTF8常量,值为age,继续看CONSTANT_NameAndType_info常量的描述符索引为16,看索引为16的常量,也是一个UTF8常量,值为I,这是一种类型描述符规范,代表这是一个int整型。综合起来,这个CONSTANT_Fieldref_info表示这是一个a/ClassFileTest类的名称为age,类型为整型的字段。正好对应源文件中的int age字段。
通过上面两个例子,我们可以看到,常量也有层级之分,基本层级的常量如CONSTANT_UTF8_info、CONSTANT_Integer_info等,复杂层级的常量如CONSTANT_Fieldref_info等,复杂层级的常量会用索引指向基本层级的常量,形成一种链式结构。
3. access_flag
常量池之后的u2表示access flag,代表此class是类、接口、枚举还是注解,是否是public,是否是abstarct,是否是final等信息。由于是u2,所以一共有16个标志位可以用。
4. 类、超类、接口
下来是类、超类和接口,用来描述此class文件所属的类、类的超类、类的接口信息,这3种类型都用索引指向之前的常量池中CONSTANT_Class_info类型的常量。
5. 字段表
再接下来是字段,字段是一种固定结构的表,结构如下:
{
u2 access_flag
u2 name_index
u2 descriptor_index
u2 attribute_count
attribute_info
}
以第一个字段为例,u2标志位表示它是个pubic、static、final类型的字段;name_index指向常量池中索引为11的CONSTANT_UTF8_info类型常量,值为WRONG;descriptor_index指向索引为12的CONSTANT_UTF8_info类型常量,值为Z,在java虚拟机描述符中,代表boolean类型;attribute_count为1,代表有一个属性(这里涉及到后面属性表的知识),继续看后面的属性表,属性名称为ConstantValue,值为0。所以这是一个pubic static final的boolean类型的名称为WRONG,值为0的字段,对应源文件中public static final boolean WRONG = false。
6. 方法表
字段后是类的方法,方法也是一种固定格式的表结构,且其表结构和字段表的结构完全一样。
如果没有定义构造函数,方法表中的第一个方法一般是默认的构造函数,这边我们看下自定义的simpleMethod方法。
u2标志位表示是一个pulic方法;name_index指向常量池中索引为26的CONSTANT_UTF8_info类型常量,值为simpleMethod;descriptor_index指向索引为27的CONSTANT_UTF8_info类型常量,值为()I,表示这是个入参为空,返回为int的方法;attribute_count为2,代表接下来有2个属性表;第一个属性名称为Code,Code属性对应方法的方法体,用虚拟机字节码指令来表示方法的逻辑。第二个属性名称为Exceptions,这个属性用来表示异常信息。
7. 属性表
class文件的最后一部分是属性表,属性表可以理解为对class文件的扩展,与常量表不同,属性表不用tag区分,而是直接用名称区分,这意味着不同虚拟机可以定义自己的扩展属性,虚拟机在遇到自己不能处理的属性时,可以直接跳过。
目前java虚拟机规范8定义的属性有23种,有些属性我们上面已经见过了,像ConstantValue、Code、Exceptions属性,其他的属性就不一一展开了。
8. 总结
以上是对class文件的简要介绍,有些细节地方并未展开,推荐3本学习java虚拟机的书:《Java虚拟机规范》、《深入理解Java虚拟机》、《自己动手写Java虚拟机》,这3本书理解下来的话对虚拟机的基本实现应该是比较熟悉了,作为一个java coder,如果不是专攻虚拟机方向的话应该是完全够用了。