逅弈 欢迎转载,注明原创出处即可,谢谢!
java能够实现“Write Once,Run Anywhere”的目标,跟JVM和字节码文件密不可分。而字节码文件是平台无关的,java代码或者其他语言的代码(如Groovy,JRuby,Scala等)由该语言对应的编译器编译成Class文件,再由JVM进行解释执行。
本文将深入理解并分析Class类文件的结构,以期望能揭示出JVM是如何根据Class文件翻译解释出类的实例,属性,方法,访问标识等等信息的。
Class文件是一组以8位的字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件中,中间没有添加任何分隔符。当遇到需要占用8位字节以上空间的数据项时,会按照高位在前的方式分割成若干个8位字节进行存储。
Class文件中只有两种数据类型:无符号数和表。无符号数属于基本的数据类型:u1,u2,u4,u8分别代表1个字节,2个字节,4个字节,8个字节,无符号数可以用来描述数字、索引引用、数量值、字符串值(UTF-8编码)。表由多个无符号数或者其他表组成,所有的表都以_info结尾。
整个Class文件就是一张表,包括以下数据项:魔数,次版本号,主版本号,常量池容量计数器,常量池,访问标志,类索引,父类索引,接口计数器,接口索引,字段个数,字段表集合,方法个数,方法表集合,属性个数,属性表集合。
以下是Class文件的表结构:
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u4 | magic | 1 | 魔数,第1-4字节,值为CAFEBABE,用以确定一个文件是为一个能被虚拟机接受的Class文件 |
u2 | minor_version | 1 | 次版本号,第5-6字节 |
u2 | major_version | 1 | 主版本号,第7-8字节 |
u2 | constant_pool_count | 1 | 常量池容量计数器 |
cp_info | constant_pool | constant_pool_count-1 | 常量池,主要有字面量和符号引用,数量等于constant_pool_count-1,因为常量在常量池中的索引从1开始,0被用来表示该常量无引用 |
u2 | access_flags | 1 | 访问标志 |
u2 | this_class | 1 | 类索引,值为类的全限定名 |
u2 | super_class | 1 | 父类索引,值为父类的全限定名,由于不允许有多重继承,所以父类索引只有一个 |
u2 | interfaces_count | 1 | 接口计数器 |
u2 | interfaces | interfaces_count | 该类实现的接口的索引,可以有多个接口 |
u2 | fields_count | 1 | 字段个数 |
field_info | fields | fields_count | 字段表集合 |
u2 | methods_count | 1 | 方法个数 |
method_info | methods | methods_count | 方法表集合 |
u2 | attributes_count | 1 | 属性个数 |
attribute_info | attributes | attributes_count | 属性表集合 |
以下通过一个简单的类来了解java代码编译成Class文件后,class文件中的内容
1.定义一个简单的类
/**
* Test Class
* 1. compile the TestClass.java into TestClass.class
* 2. use javap -verbose TestClass > TestClass.txt
* 3. analyze the class
* @author gris.wang
* @since 2018/1/22
**/
public class TestClass {
private static int id;
private String name;
public static void showId(){
System.out.println("id="+id);
}
}
2.TestClass.class的文件内容如下:
CAFE BABE 0000 0034 0033 0A00 0C00 1B09
001C 001D 0700 1E0A 0003 001B 0800 1F0A
0003 0020 0900 0B00 210A 0003 0022 0A00
0300 230A 0024 0025 0700 2607 0027 0100
0269 6401 0001 4901 0004 6E61 6D65 0100
124C 6A61 7661 2F6C 616E 672F 5374 7269
6E67 3B01 0006 3C69 6E69 743E 0100 0328
2956 0100 0443 6F64 6501 000F 4C69 6E65
4E75 6D62 6572 5461 626C 6501 0012 4C6F
6361 6C56 6172 6961 626C 6554 6162 6C65
0100 0474 6869 7301 001A 4C63 6F6D 2F6C
656D 656D 6F2F 6A76 6D2F 5465 7374 436C
6173 733B 0100 0673 686F 7749 6401 000A
536F 7572 6365 4669 6C65 0100 0E54 6573
7443 6C61 7373 2E6A 6176 610C 0011 0012
0700 280C 0029 002A 0100 176A 6176 612F
6C61 6E67 2F53 7472 696E 6742 7569 6C64
6572 0100 0369 643D 0C00 2B00 2C0C 000D
000E 0C00 2B00 2D0C 002E 002F 0700 300C
0031 0032 0100 1863 6F6D 2F6C 656D 656D
6F2F 6A76 6D2F 5465 7374 436C 6173 7301
0010 6A61 7661 2F6C 616E 672F 4F62 6A65
6374 0100 106A 6176 612F 6C61 6E67 2F53
7973 7465 6D01 0003 6F75 7401 0015 4C6A
6176 612F 696F 2F50 7269 6E74 5374 7265
616D 3B01 0006 6170 7065 6E64 0100 2D28
4C6A 6176 612F 6C61 6E67 2F53 7472 696E
673B 294C 6A61 7661 2F6C 616E 672F 5374
7269 6E67 4275 696C 6465 723B 0100 1C28
4929 4C6A 6176 612F 6C61 6E67 2F53 7472
696E 6742 7569 6C64 6572 3B01 0008 746F
5374 7269 6E67 0100 1428 294C 6A61 7661
2F6C 616E 672F 5374 7269 6E67 3B01 0013
6A61 7661 2F69 6F2F 5072 696E 7453 7472
6561 6D01 0007 7072 696E 746C 6E01 0015
284C 6A61 7661 2F6C 616E 672F 5374 7269
6E67 3B29 5600 2100 0B00 0C00 0000 0200
0A00 0D00 0E00 0000 0200 0F00 1000 0000
0200 0100 1100 1200 0100 1300 0000 2F00
0100 0100 0000 052A B700 01B1 0000 0002
0014 0000 0006 0001 0000 000B 0015 0000
000C 0001 0000 0005 0016 0017 0000 0009
0018 0012 0001 0013 0000 0038 0003 0000
0000 001C B200 02BB 0003 59B7 0004 1205
B600 06B2 0007 B600 08B6 0009 B600 0AB1
0000 0001 0014 0000 000A 0002 0000 0012
001B 0013 0001 0019 0000 0002 001A
从上可以发现一些简单的信息:
- 第1-4字节:CAFE BABE,表示这是一个合法的Class文件
- 第5-6字节:0000,表示次版本号为0
- 第7-8字节:0034,表示主版本号为52,笔者安装的JDK版本为:
java version "1.8.0_152"
Java(TM) SE Runtime Environment (build 1.8.0_152-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.152-b16, mixed mode)
- 第9-10字节:0033,表示该类中共有51-1=50个常量
- 第11字节:0A,这是第一个常量,常量tag为0A(tag标志值为十进制中的10),查表得知该常量为CONSTANT_Methodref_info类型,而CONSTANT_Methodref_info类型的常量规定,该常量tag后面紧跟着的是两个u2类型的常量:
- 第一个常量指向声明方法的类描述符CONSTANT_Class_info的索引,该常量为第12-13字节:000C,表示指向第12(0x0C)个常量,通过查找常量池得知第12个常量为:java/lang/Object
- 第二个常量指向名称及类型描述符CONSTANT_NameAndType的索引,该常量为第14-15字节:001B,表示指向第27(0x1B)个常量,而CONSTANT_NameAndType类型的常量tag后面也紧跟着两个u2类型的常量:
- 第一个常量指向字段或方法的名称,是一个CONSTANT_Utf8_info类型的常量,通过查找常量池得知该常量指向的是第17个常量,值为:<init>
- 第二个常量指向字段或方法的描述符,也是一个CONSTANT_Utf8_info类型的常量,通过查找常量池得知该常量指向的是第18个常量,值为:()V
- 后面的常量也依次根据查表的方式获得具体的值
3.通过javap对编译好的TestClass.class文件进行分析
javap -verbose TestClass > TestClass.txt
4.生成的TestClass.txt的内容如下:
Classfile ~/workspace/lememo/out/production/classes/com/lememo/jvm/TestClass.class
Last modified 2018-1-22; size 750 bytes
MD5 checksum b90886860e2bf374d0a26c33f08e41e5
Compiled from "TestClass.java"
public class com.lememo.jvm.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #12.#27 // java/lang/Object."<init>":()V
#2 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #30 // java/lang/StringBuilder
#4 = Methodref #3.#27 // java/lang/StringBuilder."<init>":()V
#5 = String #31 // id=
#6 = Methodref #3.#32 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = Fieldref #11.#33 // com/lememo/jvm/TestClass.id:I
#8 = Methodref #3.#34 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#9 = Methodref #3.#35 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#10 = Methodref #36.#37 // java/io/PrintStream.println:(Ljava/lang/String;)V
#11 = Class #38 // com/lememo/jvm/TestClass
#12 = Class #39 // java/lang/Object
#13 = Utf8 id
#14 = Utf8 I
#15 = Utf8 name
#16 = Utf8 Ljava/lang/String;
#17 = Utf8 <init>
#18 = Utf8 ()V
#19 = Utf8 Code
#20 = Utf8 LineNumberTable
#21 = Utf8 LocalVariableTable
#22 = Utf8 this
#23 = Utf8 Lcom/lememo/jvm/TestClass;
#24 = Utf8 showId
#25 = Utf8 SourceFile
#26 = Utf8 TestClass.java
#27 = NameAndType #17:#18 // "<init>":()V
#28 = Class #40 // java/lang/System
#29 = NameAndType #41:#42 // out:Ljava/io/PrintStream;
#30 = Utf8 java/lang/StringBuilder
#31 = Utf8 id=
#32 = NameAndType #43:#44 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#33 = NameAndType #13:#14 // id:I
#34 = NameAndType #43:#45 // append:(I)Ljava/lang/StringBuilder;
#35 = NameAndType #46:#47 // toString:()Ljava/lang/String;
#36 = Class #48 // java/io/PrintStream
#37 = NameAndType #49:#50 // println:(Ljava/lang/String;)V
#38 = Utf8 com/lememo/jvm/TestClass
#39 = Utf8 java/lang/Object
#40 = Utf8 java/lang/System
#41 = Utf8 out
#42 = Utf8 Ljava/io/PrintStream;
#43 = Utf8 append
#44 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#45 = Utf8 (I)Ljava/lang/StringBuilder;
#46 = Utf8 toString
#47 = Utf8 ()Ljava/lang/String;
#48 = Utf8 java/io/PrintStream
#49 = Utf8 println
#50 = Utf8 (Ljava/lang/String;)V
{
public com.lememo.jvm.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/lememo/jvm/TestClass;
public static void showId();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: ldc #5 // String id=
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: getstatic #7 // Field id:I
18: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
21: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
LineNumberTable:
line 18: 0
line 19: 27
}
SourceFile: "TestClass.java"
下面对Class文件中的各数据进行分析
魔数与版本号
- 魔数占4个字节,值为CAFEBABE,共32bit(4个二进制数可以表示一个十六进制数,则32bit二进制可以用8个十六进制数表示)
- 魔数唯一的作用是确定这个文件是否为一个能被虚拟机接受的Class文件
- 使用魔数而不是扩展名进行识别,主要是出于安全方面的考虑,因为扩展名可以随意更改
- 次版本号占2个字节,主版本号占2个字节
- java版本号从45开始,所以主版本号最小为0x002D
- 高版本的JDK能向下兼容低版本的Class文件,但不能运行更高版本的Class文件
- 因为主、次版本号各占2个字节,所以最大可以为65535()
常量池
- 可以将常量池理解为Class文件中的资源仓库
- 常量池中的常量的个数不是固定的,所以在常量池前面有一个u2类型的数据,表示常量池的容量constant_pool_count,但是由于常量的计数索引是从1开始的,因此常量的个数=constant_pool_count-1
- 常量池中存放着两类常量:字面量和符号引用,字面量类似于字符串、数值等常量,符号引用包括三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 每一个常量可以理解为一个tag-value对,tag用来表示该常量的类型,并且每一个tag都是一个u1类型的无符号数,value表示该常量的具体的行为
- JDK1.7中共有14种不同类型的常量
- CONSTANT_Utf8_info类型的常量主要用来表示字符串,而该类型的常量的length是一个u2类型的无符号数,最大长度为65535,所以如果一个类的方法名或者字段名的长度超过64KB,将会无法编译
访问标志
- 访问标志占2个字节
- 这个标志用于标识一些类或者接口层次的访问信息
- access_flags中一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位都为0
- 把8种标志位的值进行或运算,运算结果就是access_flags的值
以下是具体的标志位和含义:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否为final类型,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语意,JDK1.0.2之后类的这个标志都必须为真 |
ACC_INTERFACE | 0x0200 | 是否为一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,接口或抽象类该值为真,其他为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
类索引,父类索引,接口索引集合
- 类索引和父类索引都是一个u2类型的数据,除了java.lang.Object类,所有类的父类索引都不为空,并且有且只有一个父类索引
- 类索引和父类索引都指向一个CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info常量中的索引值可以找到在CONSTANT_Utf8_info常量中定义的全限定名字符串
- 接口索引集合是一组u2类型的数据的集合,用来标识该类实现了哪些接口
字段表集合
- 字段表用以描述类或者接口中定义的变量
- 字段包括类变量和实例变量,但不包括方法内部的局部变量
- 一个字段可以有以下信息:
- 变量的作用域,是public,private还是protected的
- 属于类变量还是实例变量,即是否有static修饰符
- 是否是不可变变量,即是否有final修饰符
- 是否是并发可见的,即是否有valatile修饰符
- 是否可以被序列化,即是否有transient修饰符
- 变量的数据类型,是基本类型、对象还是数组
- 变量的名称
字段表的结构如下:
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u2 | access_flags | 1 | 字段修饰符,与类的access_flags非常相似 |
u2 | name_index | 1 | 字段的简单名称,是没有修饰符修饰的字段名称,是对常量池的引用 |
u2 | descriptor_index | 1 | 字段的描述符,用以描述字段的数据类型。基本数据类型以及无返回值void类型都用一个大写字符表示,对象类型用字符L加对象的全限定名表示,一维数组用[表示,二维数组用[[表示 |
u2 | attributes_count | 1 | 字段的属性表计数器 |
attribute_info | attributes | attributes_count | 属性表集合,用于保存字段的一些额外的信息 |
方法表集合
- 方法表用以描述类或者接口中定义的方法
方法表的结构如下:
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u2 | access_flags | 1 | 方法修饰符,与类的access_flags非常相似,不包括volatile,transient修饰符 |
u2 | name_index | 1 | 方法的简单名称,是指没有类型和参数修饰的方法名称,是对常量池的引用 |
u2 | descriptor_index | 1 | 方法的描述符,用以描述方法的参数列表,包括数量,类型以及顺序 |
u2 | attributes_count | 1 | 方法的属性表计数器 |
attribute_info | attributes | attributes_count | 属性表集合,用于保存方法的一些额外的信息,比如方法里的java代码将会保存在方法属性表集合中一个名为“Code”的属性里面 |
属性表集合
- 在Class文件,字段表,方法表中都携带了自己的属性表集合,用以描述某些场景特有的信息
- 属性表中的顺序不再是严格要求不变的,只要不予已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息
- java虚拟机会忽略掉它不认识的属性
- 《java虚拟机规范(第2版)》中预定义了9项虚拟机能识别的属性
- 《java虚拟机规范(Java SE 7)》中预定义的属性增加到21项
以下列举一些常见的虚拟机规范预定义的属性:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 纪录源文件名称 |