概述
计算机只认0和1,我们的程序需要被编译器翻译成0和1构成的二进制格式才能被计算机识别执行。
而虚拟机的出现,使得把我们写的程序编译成二进制本地机器码已经不是唯一的选择,越来越多的语言使用了与操作系统、机器指令均无关的格式作为编译后的存储格式。
字节码(byte code)是构成平台无关性的基石,这是各种不同平台都统一支持的程序存储格式。
Java虚拟机不与java语言绑定,也不和其它任意语言绑定,它只与“Class文件”这种特定的二进制文件格式关联。
图片
Class类文件的结构
Class是一组以字节为基础单位的二进制流。其中Class的各个数据项严格按照顺序紧凑地排列在文件之中,使得整个Class文件存储的内容都是程序运行所需要的必要数据。
伪结构的两种数据类型
-
无符号数
无符号数属于基本数据类型,u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节。
可以用来描述数字、索引引用、数量值、或者UTF-8编码格式的字符串。 -
表
表示一种符合类型数据,有无符号数或者其它表组成。表的命名以"_info"结尾。
Class本质上可以看作是一张表。
魔数与Class文件的版本
每个Class文件的头4个字节称为魔数,用于确定这个Class文件是否能被虚拟机接受。事实上,很多文件格式也用魔数进行身份识别,比如GIF、JPEG的头文件也存在魔数。
Class的魔数值称为:0xCAFEBABE,颇有“咖啡宝贝”的之嫌。
常量池
常量池可以比喻为Class文件的资源仓库,通常是占用Class文件最大的数据项目之一。常量池中的每一项常量都是一个表。
常量池有21项常量。和一般的习惯不一样,这个常量池的容量计数是从1开始的,原因在于设计者考虑到可能会需要表达“不引用任何一个常量池项目”的含义,所以留出了第0个常量。
备注:对于Class的其他的集合类型,和一般的习惯一样,是从0开始。
字面量
比如文本字符串,被声明为final的常量值等。
符号引用
属于编译原理的概念,包括以下的常量类型:
- 被模块导出或者开放的包
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 方法句柄和方法类型
- 动态调用点和动态常量
备注:Java代码在进行Javac编译的时候,没有像C和C++那样进行“连接”,而是会在虚拟机加载Class文件的时候进行动态连接。就是说Class文件并不保存方法、字段在内存中的布局信息,而是需要虚拟机在运行期间对其进行转换才能得到真正的内存地址。
常量池的项目类型
- CONSTANT_Class_info:代表一个类或者接口的符号引用。
- CONSTANT_Utf8_info:UTF-8编码的字符串
- CONSTANT_Interger_info:整型字面量
- CONSTANT_Float_info:浮点型字面量
- CONSTANT_Long_info:长整型字面量
- CONSTANT_Double_info:双精度浮点字面量
- CONSTANT_Class_info:类或接口的符号引用
- CONSTANT_String_info:字符串类型字面量
- CONSTANT_Fieldref_info:字段的符号引用
- CONSTANT_Methodref_info:类中方法的符号引用
- CONSTANT_InteraceMehodref_info:接口中方法的符号引用
- CONSTANT_NameAndType_info:字段或方法的部分符号引用
- CONSTANT_MethodHandle_info:方法句柄
- CONSTANT_MethodType_info:方法类型
- CONSTANT_Dynamic_info:动态计算常量
- CONSTANT_Invoke_info:动态方法调用点
- CONSTANT_Module_info:一个模块
- CONSTANT_Package_info:一个模块中开发或者导出的包
这十七个常量类型中有着完全独立的数据结构,两两之间没有共性和联系。
备注:由于Class中的方法、字段都需要引用CONSTANT_Utf8_info型的常量来描述,所以CONSTANT_Utf8_info型的常量的最大长度就是Java中方法、字段名的最大长度。而这CONSTANT_Utf8_info的定义的最长长度是个u2类型,u2类型能表达的最大值是65535。这就是Java无法编译超过64KB大小的英文字符变量或方法名的原因。
实战部分:查看hellowolrd程序的Class常量池
这里需要用到用于分析Class文件字节码的工具:javap。
package com.lsj.lib.vm;
/**
* File description
*
* @author linshujie
* @date 2021/7/21
*/
public class Helloworld {
public static void main(String[] args) {
System.out.println("hello world!");
}
}
到Hellowolrd.java的目标文件中执行命令
javac Helloworld.java
会生成Helloworld.class。接着执行
javap -verbose Helloworld
生成以下信息
警告: 二进制文件Helloworld包含com.lsj.lib.vm.Helloworld
Classfile /D:/code/Android/temp/TestApplication/lib/src/main/java/com/lsj/lib/vm/Helloworld.class
Last modified 2021-7-21; size 441 bytes
MD5 checksum 74ac2c771acc90351ef5a260b34d1703
Compiled from "Helloworld.java"
public class com.lsj.lib.vm.Helloworld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // hello world!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // com/lsj/lib/vm/Helloworld
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Helloworld.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 hello world!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 com/lsj/lib/vm/Helloworld
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public com.lsj.lib.vm.Helloworld();
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 9: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
}
SourceFile: "Helloworld.java"
上面我们可以看到一些"I" “V” “” "LineNumberTable"等常量,这部分常量实际上都是编译器自动生成的,会被字段表、方法表、属性表所引用。而这些常量会用来描述一些不方便用“固定字节”进行表达的内容,比如方法的返回值、参数类型是什么、有几个参数等。
访问标志
常量池后面,我们能看到的便是访问标志,用来标识一些类或接口的信息。例如:这个Class是个类还是接口;是否定义为public类型;是否是abstract类型等。
其中,我们上面例子中:
flags: ACC_PUBLIC, ACC_STATIC
ACC_PUBLIC代表是public类型,
ACC_STATIC代表是静态类型。