java虚拟机系列(五)- 类文件的结构
一、无关性基石
1.1 平台无关性
“与平台无关”的理想最终实现在操作系统层面上:Sun公司以及其它虚拟机提供商发布了许多运行在不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码(Class字节码文件),从而实现程序的“一次编写,到处运行”。
1.2 语言无关性
java发展之初,设计者就考虑并实现让其他语言运行在java虚拟机上的可能,所以发布文档的时候,可以拆分为《java语言规范》和《java虚拟机规范》,至今已有不少语言编写的程序运行在java虚拟机上,如JRuby、Groovy等。
实现java语言无关性的基础仍然是虚拟机和字节码存储格式,java虚拟机不和包括java语言在内的任何其它语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联。即在编译器编译后可以把程序代码编译成Class文件的任何语言,如下图所示。
二、类文件结构
2.1 魔数与class文件版本
每个类文件的头4个字节称为魔数(Magic number),它的唯一作用是确认这个文件是否为一个能被虚拟机接受的Class文件,class文件魔数的值为:0xCAFEBASE;紧接着魔数之后的4个字节存储的是class文件的版本号:第5、6个字节是次版本号,第7、8个字节是主版本号。
java的主版本号是从45开始的。高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。class文件结构如下图所示。
2.2 常量池
紧接着主次版本号之后的是常量池入口,class文件结构中,与其它项目关联最多的数据类型,也是class文件空间最大的数据项目之一,同时它还是在class文件中第一次出现的表类型数据项目。
常量池的容量计数值是从1而不是从0开始的。class文件结构中只有常量池的的容量计数是从1开始,其它的集合类型都是从0开始。
常量池中主要存在两大常量:字面量和符号引用。
字面量
字面量比较接近java语言层面的常量概念,主要是指:
- 文本字符串
- 声明为final的常量值等
符号引用
符号引用则属于编译原理方面的概念,主要包含下面三类常量:
- 类和接口的权限定名(唯一能定义到类和接口的名称如类和接口的定义路径:com/example/demo/javasebase/jvmtest/FinalTest)
- 字段的名称和描述符
- 方法的名称和描述符
java代码在进行javac编译为Class文件的时候,Class文件中不会保存各个方法、字段的最终内存布局,因为这些方法、字段的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法被虚拟机使用。
当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时解析、翻译到具体的内存地址中。
常量池中每一项常量都是一个表,截止jdk1.7共有14个种表结构。
java代码如下:
package com.example.demo.javasebase.jvmtest;
/**
* Create by likaihai 2019/5/16
*/
public class FinalTest
{
public final static int TEST_INT = 45;
public final static String str2 = "第一个字符串";
public static void main(String[] args)
{
String str = "你好啊";
}
}
class反编译后如下:
D:\java\myproject2\demo\target\classes\com\example\demo\javasebase\jvmtest>javap -v -l -c -s FinalTest.class
Classfile /D:/java/myproject2/demo/target/classes/com/example/demo/javasebase/jvmtest/FinalTest.class
Last modified 2019-5-16; size 600 bytes
MD5 checksum d01da6ae50c93d38b4c16f413b2bace8
Compiled from "FinalTest.java"
public class com.example.demo.javasebase.jvmtest.FinalTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#26 // java/lang/Object."<init>":()V
#2 = String #27 // 你好啊
#3 = Class #28 // com/example/demo/javasebase/jvmtest/FinalTest
#4 = Class #29 // java/lang/Object
#5 = Utf8 TEST_INT
#6 = Utf8 I
#7 = Utf8 ConstantValue
#8 = Integer 45
#9 = Utf8 str2
#10 = Utf8 Ljava/lang/String;
#11 = String #30 // 第一个字符串
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcom/example/demo/javasebase/jvmtest/FinalTest;
#19 = Utf8 main
#20 = Utf8 ([Ljava/lang/String;)V
#21 = Utf8 args
#22 = Utf8 [Ljava/lang/String;
#23 = Utf8 str
#24 = Utf8 SourceFile
#25 = Utf8 FinalTest.java
#26 = NameAndType #12:#13 // "<init>":()V
#27 = Utf8 你好啊
#28 = Utf8 com/example/demo/javasebase/jvmtest/FinalTest
#29 = Utf8 java/lang/Object
#30 = Utf8 第一个字符串
{
public static final int TEST_INT;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 45
public static final java.lang.String str2;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String 第一个字符串
public com.example.demo.javasebase.jvmtest.FinalTest();
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 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/demo/javasebase/jvmtest/FinalTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String 你好啊
2: astore_1
3: return
LineNumberTable:
line 13: 0
line 14: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
3 1 1 str Ljava/lang/String;
}
SourceFile: "FinalTest.java"
上文中的Constant pool: 代表的是常量池中存储的内容。
可以看出如 #8的45、#27的“第一个字符串”、#30的“你好啊”均为字面量。
#1、#2、#3、#4、#11、#26等为字段或方法的描述符;#9和#19则为字段或方法名称。这些都属于符号引用。
2.3 访问标志
在常量池结束之后,紧接着两个字节代表着访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否声明为final等。
2.4 类索引、父类索引和接口索引集合
类索引、父类索引和接口索引都按顺序排列在访问标志之后。
类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的集合,Class文件由这三项数据来确定这个类的继承关系。类索引用来确定这个类的全限定名,父类索引用来确定这个类的父类的全限定名。
由于java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外所有的java类都有父类,因为除了java.lang.Object,所有java类的父类索引都不为0。接口索引集合用来描述这些类实现了哪些接口。
2.5 字段表集合
字段表用来描述接口或者类中声明的变量。字段包括类型变量和实例型变量,但不包括在方法内部声明的局部变量。主要包括:字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称等。
2.6 方法表集合
方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。
2.7 属性表集合
Class文件中、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。