Java虚拟机的平台无关性
程序员文章站
2022-07-13 14:10:48
...
我们知道,java 语言一次编写,到处运行,那它是如何做到的了?早在java语言设计之初,设计者就考虑到了这方面的事,他们将java规范拆分为java语言规范和java虚拟机规范,而虚拟机只和class文件进行绑定,所以不管是java语言,还是c语言,只要有编译器把*.java或*.c文件编译成*.class文件即可,然后class文件运行在 java虚拟机上就可以运行了.
那下面说下 class 的文件结构
任何一个class文件都对应唯一的一个类或接口,但是类或接口不一定要定义在 class 文件中. Class 文件是一组以 8 字节为基础单位的二进制流,各个数据项严格按照顺序紧密排列,中间没有任何分割符。如果要存储超过 8 字节的数据的话,那么就按照高字节在前,低字节在后进行存储。
Class 文件结构类似于 C 语言中的结构体,它只包含两种数据类型,无符号数和表,无符号数u1、u2、u4、u8 分别代表1字节、2字节、4字节和8字节,表是复合数据类型,由无符号数和表组成。所以从本质上来看,class 文件本质上是一张表。
无符号数多用于描述索引引用、数值、UTF-8编码的字符串,而表多以 "_info" 结尾,用于描述带有层级关系的数据。
有小伙伴会问如何存储数组类型了?
class 文件对于相同数据类型结构的存储,会在前面放一个长度计数器,后跟该数据类型(ps: 这个跟对象头很像哦)
下面正式介绍 class 文件格式.
使用 javap -verbose xxx.class
xhdeMacBook-Pro:fadp xh$ javap -verbose /Users/xh/workspace/jvm-read/target/classes/com/hanlin/fadp/StringGC.class
Classfile /Users/xh/workspace/jvm-read/target/classes/com/hanlin/fadp/StringGC.class
Last modified 2020-2-5; size 720 bytes
MD5 checksum 8c8a2d157d5cb8fe7ea8543bef59ed02
Compiled from "StringGC.java"
public class com.hanlin.fadp.StringGC
minor version: 0
major version: 49
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#27 // java/lang/Object."<init>":()V
#2 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #30 // abcccds
#4 = Methodref #31.#32 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Long 10000l
#7 = Methodref #33.#34 // java/lang/Thread.sleep:(J)V
#8 = Class #35 // com/hanlin/fadp/StringGC
#9 = Class #36 // java/lang/Object
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/hanlin/fadp/StringGC;
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 i
#20 = Utf8 I
#21 = Utf8 args
#22 = Utf8 [Ljava/lang/String;
#23 = Utf8 Exceptions
#24 = Class #37 // java/lang/InterruptedException
#25 = Utf8 SourceFile
#26 = Utf8 StringGC.java
#27 = NameAndType #10:#11 // "<init>":()V
#28 = Class #38 // java/lang/System
#29 = NameAndType #39:#40 // out:Ljava/io/PrintStream;
#30 = Utf8 abcccds
#31 = Class #41 // java/io/PrintStream
#32 = NameAndType #42:#43 // println:(Ljava/lang/String;)V
#33 = Class #44 // java/lang/Thread
#34 = NameAndType #45:#46 // sleep:(J)V
#35 = Utf8 com/hanlin/fadp/StringGC
#36 = Utf8 java/lang/Object
#37 = Utf8 java/lang/InterruptedException
#38 = Utf8 java/lang/System
#39 = Utf8 out
#40 = Utf8 Ljava/io/PrintStream;
#41 = Utf8 java/io/PrintStream
#42 = Utf8 println
#43 = Utf8 (Ljava/lang/String;)V
#44 = Utf8 java/lang/Thread
#45 = Utf8 sleep
#46 = Utf8 (J)V
{
public com.hanlin.fadp.StringGC();
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/hanlin/fadp/StringGC;
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 28
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #3 // String abcccds
13: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: ldc2_w #5 // long 10000l
19: invokestatic #7 // Method java/lang/Thread.sleep:(J)V
22: iinc 1, 1
25: goto 2
28: return
LineNumberTable:
line 6: 0
line 7: 8
line 8: 16
line 6: 22
line 11: 28
LocalVariableTable:
Start Length Slot Name Signature
2 26 1 i I
0 29 0 args [Ljava/lang/String;
Exceptions:
throws java.lang.InterruptedException
}
SourceFile: "StringGC.java"
下面是用文本计数器打开 class 文件的截图:
从该文件中我们是不是没有发现魔数 CAFEBABE 这4字节了?但是我在 class 源文件中确实看到了。
先不管这个了,继续往下看是两个版本号,minor version 和 major version.
minor version 小版本(u2)
major version 大版本(u2)
接下来是常量池
常量池肯定存放多个元素,必然是 len(u2) + 元素 这种形式. 值得注意的是,常量池的下标是从 1 开始的.
002f -> 47,我们用 javap 查看到,一共 46 个元素,下标从 1 开始的正好核上了. 那空出来的 0 号位有啥用了?不是越紧促越好吗?设计者将 0 号位用于实现指向常量池数据但需要表达不指向任何一个常量池项目的含义.
常量池主要存放字面量和符号引用,字面量例如:文本字符串,被 fianl 修饰的常量,基本数据类型的值.
符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符.
常量池中每一项都是一个表,jdk1.7 之前共有 12 个结构.
0a -> 10 Constant_Methodref_info 类中方法的符号引用.
tag
index1 指向声明方法的类或接口信息 Constant_Class_info
index2 指向名称及类型描述符索引项 Constant_NameAndType_info
08 -> 8 Constant_String_Info 表示 String 类型的常量
tag
index 指向字符串字面量的索引
Constant_Utf8_info 表示 Utf8 编码的字符串(基石).
tag
length
bytes
Constant_Fieldref_info 类中的字段
tag
index1 指向声明该字段的类或接口描述符 Constant_Class_info
index2 指向字段描述符的索引项 Constant_NameAndType_info
Constant_Class_info
tag
index 指向全限定名称的索引
Constant_Integer_info、Constant_Double_info、Constant_Float_info、Constant_Long_info 都是 tag + bytes
Constant_NameAndType_info
tag
index 指向该字段名称或方法的索引项
index 指向该字段名称或方法描述的索引项
Constant_InterfaceMethod_info
tag
index 指向声明方法的接口描述符 Constant_Class_info 的索引项
index 指向名称及类型描述符 Constant_NameAndType_info 的索引项
以一个为例进行分析:
Constant_Methodref_Info 有两个索引项,一个执行声明该字段或方法的表,另一个指向该字段或方法的名称及类型描述表.
我们重点看下名称及类型描述表,根据名称我们就可以得出这个表必然有两个索引项,一个是指向名称,一个是指向描述。关于方法和字段的描述后面会详细讲。
jdk1.7 新增的表(后面统一说明)
Constant_MethodHandle_info 方法句柄
Constant_MethodType_info 方法的描述
Constant_InvokeDynamic_info
接着后面是访问标志
在常量池结束后,紧接着是两个字节的访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是
接口,是否定义为 public 类型,是否定义为 abstract 类型,如果是的话,是否被声明为 final 等.
access_flags 中一共有 16 个标志位可以使用,当前只定义了其中 8 个,没有使用到的标志位一律为 0.
例如:ACC_PUBLIC、ACC_SUPER、ACC_FINAL、ACC_INTERFACE 等.
类索引 & 父类索引 & 接口索引
类索引(this_class) 和父类索引(super_class) 都是一个 u2 类型的数据,而接口索引集合(interfaces) 是一组 u2 类型的数据的集合,
Class 文件中由这三项数据来确定这个类的继承关系. 类索引确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名. Java 是不允许
多重继承,所以父类索引只有一个. 接口索引集合用来描述这个类实现了那些接口.
类索引和父类索引指向一个类型为 Constant_Class_Info 的类描述符常量,通过 Constant_Class_Info 类型的常量中的索引值可以找到定义
在 Constant_Utf8_info 类型的常量中的全限定名字符串.
对于接口索引集合,入口的第一项是 u2 类型的数据为接口计数器(最大为 65535,所以在看 JDK Proxy 的时候,会发现它有限制接口数组的长度
为 65535 哦,原因就在这里).
那下面说下 class 的文件结构
任何一个class文件都对应唯一的一个类或接口,但是类或接口不一定要定义在 class 文件中. Class 文件是一组以 8 字节为基础单位的二进制流,各个数据项严格按照顺序紧密排列,中间没有任何分割符。如果要存储超过 8 字节的数据的话,那么就按照高字节在前,低字节在后进行存储。
Class 文件结构类似于 C 语言中的结构体,它只包含两种数据类型,无符号数和表,无符号数u1、u2、u4、u8 分别代表1字节、2字节、4字节和8字节,表是复合数据类型,由无符号数和表组成。所以从本质上来看,class 文件本质上是一张表。
无符号数多用于描述索引引用、数值、UTF-8编码的字符串,而表多以 "_info" 结尾,用于描述带有层级关系的数据。
有小伙伴会问如何存储数组类型了?
class 文件对于相同数据类型结构的存储,会在前面放一个长度计数器,后跟该数据类型(ps: 这个跟对象头很像哦)
下面正式介绍 class 文件格式.
使用 javap -verbose xxx.class
xhdeMacBook-Pro:fadp xh$ javap -verbose /Users/xh/workspace/jvm-read/target/classes/com/hanlin/fadp/StringGC.class
Classfile /Users/xh/workspace/jvm-read/target/classes/com/hanlin/fadp/StringGC.class
Last modified 2020-2-5; size 720 bytes
MD5 checksum 8c8a2d157d5cb8fe7ea8543bef59ed02
Compiled from "StringGC.java"
public class com.hanlin.fadp.StringGC
minor version: 0
major version: 49
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#27 // java/lang/Object."<init>":()V
#2 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #30 // abcccds
#4 = Methodref #31.#32 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Long 10000l
#7 = Methodref #33.#34 // java/lang/Thread.sleep:(J)V
#8 = Class #35 // com/hanlin/fadp/StringGC
#9 = Class #36 // java/lang/Object
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/hanlin/fadp/StringGC;
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 i
#20 = Utf8 I
#21 = Utf8 args
#22 = Utf8 [Ljava/lang/String;
#23 = Utf8 Exceptions
#24 = Class #37 // java/lang/InterruptedException
#25 = Utf8 SourceFile
#26 = Utf8 StringGC.java
#27 = NameAndType #10:#11 // "<init>":()V
#28 = Class #38 // java/lang/System
#29 = NameAndType #39:#40 // out:Ljava/io/PrintStream;
#30 = Utf8 abcccds
#31 = Class #41 // java/io/PrintStream
#32 = NameAndType #42:#43 // println:(Ljava/lang/String;)V
#33 = Class #44 // java/lang/Thread
#34 = NameAndType #45:#46 // sleep:(J)V
#35 = Utf8 com/hanlin/fadp/StringGC
#36 = Utf8 java/lang/Object
#37 = Utf8 java/lang/InterruptedException
#38 = Utf8 java/lang/System
#39 = Utf8 out
#40 = Utf8 Ljava/io/PrintStream;
#41 = Utf8 java/io/PrintStream
#42 = Utf8 println
#43 = Utf8 (Ljava/lang/String;)V
#44 = Utf8 java/lang/Thread
#45 = Utf8 sleep
#46 = Utf8 (J)V
{
public com.hanlin.fadp.StringGC();
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/hanlin/fadp/StringGC;
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 28
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #3 // String abcccds
13: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: ldc2_w #5 // long 10000l
19: invokestatic #7 // Method java/lang/Thread.sleep:(J)V
22: iinc 1, 1
25: goto 2
28: return
LineNumberTable:
line 6: 0
line 7: 8
line 8: 16
line 6: 22
line 11: 28
LocalVariableTable:
Start Length Slot Name Signature
2 26 1 i I
0 29 0 args [Ljava/lang/String;
Exceptions:
throws java.lang.InterruptedException
}
SourceFile: "StringGC.java"
下面是用文本计数器打开 class 文件的截图:
从该文件中我们是不是没有发现魔数 CAFEBABE 这4字节了?但是我在 class 源文件中确实看到了。
先不管这个了,继续往下看是两个版本号,minor version 和 major version.
minor version 小版本(u2)
major version 大版本(u2)
接下来是常量池
常量池肯定存放多个元素,必然是 len(u2) + 元素 这种形式. 值得注意的是,常量池的下标是从 1 开始的.
002f -> 47,我们用 javap 查看到,一共 46 个元素,下标从 1 开始的正好核上了. 那空出来的 0 号位有啥用了?不是越紧促越好吗?设计者将 0 号位用于实现指向常量池数据但需要表达不指向任何一个常量池项目的含义.
常量池主要存放字面量和符号引用,字面量例如:文本字符串,被 fianl 修饰的常量,基本数据类型的值.
符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符.
常量池中每一项都是一个表,jdk1.7 之前共有 12 个结构.
0a -> 10 Constant_Methodref_info 类中方法的符号引用.
tag
index1 指向声明方法的类或接口信息 Constant_Class_info
index2 指向名称及类型描述符索引项 Constant_NameAndType_info
08 -> 8 Constant_String_Info 表示 String 类型的常量
tag
index 指向字符串字面量的索引
Constant_Utf8_info 表示 Utf8 编码的字符串(基石).
tag
length
bytes
Constant_Fieldref_info 类中的字段
tag
index1 指向声明该字段的类或接口描述符 Constant_Class_info
index2 指向字段描述符的索引项 Constant_NameAndType_info
Constant_Class_info
tag
index 指向全限定名称的索引
Constant_Integer_info、Constant_Double_info、Constant_Float_info、Constant_Long_info 都是 tag + bytes
Constant_NameAndType_info
tag
index 指向该字段名称或方法的索引项
index 指向该字段名称或方法描述的索引项
Constant_InterfaceMethod_info
tag
index 指向声明方法的接口描述符 Constant_Class_info 的索引项
index 指向名称及类型描述符 Constant_NameAndType_info 的索引项
以一个为例进行分析:
Constant_Methodref_Info 有两个索引项,一个执行声明该字段或方法的表,另一个指向该字段或方法的名称及类型描述表.
我们重点看下名称及类型描述表,根据名称我们就可以得出这个表必然有两个索引项,一个是指向名称,一个是指向描述。关于方法和字段的描述后面会详细讲。
jdk1.7 新增的表(后面统一说明)
Constant_MethodHandle_info 方法句柄
Constant_MethodType_info 方法的描述
Constant_InvokeDynamic_info
接着后面是访问标志
在常量池结束后,紧接着是两个字节的访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是
接口,是否定义为 public 类型,是否定义为 abstract 类型,如果是的话,是否被声明为 final 等.
access_flags 中一共有 16 个标志位可以使用,当前只定义了其中 8 个,没有使用到的标志位一律为 0.
例如:ACC_PUBLIC、ACC_SUPER、ACC_FINAL、ACC_INTERFACE 等.
类索引 & 父类索引 & 接口索引
类索引(this_class) 和父类索引(super_class) 都是一个 u2 类型的数据,而接口索引集合(interfaces) 是一组 u2 类型的数据的集合,
Class 文件中由这三项数据来确定这个类的继承关系. 类索引确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名. Java 是不允许
多重继承,所以父类索引只有一个. 接口索引集合用来描述这个类实现了那些接口.
类索引和父类索引指向一个类型为 Constant_Class_Info 的类描述符常量,通过 Constant_Class_Info 类型的常量中的索引值可以找到定义
在 Constant_Utf8_info 类型的常量中的全限定名字符串.
对于接口索引集合,入口的第一项是 u2 类型的数据为接口计数器(最大为 65535,所以在看 JDK Proxy 的时候,会发现它有限制接口数组的长度
为 65535 哦,原因就在这里).
推荐阅读
-
Java虚拟机是怎么new的对象?
-
重写(OverWrite)在JVM中的实现 JVM字节码多态虚拟机栈Java
-
java -verbose参数来观察Java的运行信息 Java虚拟机
-
[二]Java虚拟机 jvm内存结构 运行时数据内存 class文件与jvm内存结构的映射 jvm数据类型 虚拟机栈 方法区 堆 含义
-
了解Java多线程的可见性与有序性
-
PHP创建word文档的方法(平台无关),_PHP教程
-
荐 【探究JVM四】Java方法执行的线程内存模型——虚拟机栈 字节码指令追踪,万字长文深入探究内部结构
-
Java开源生鲜电商平台-RBAC系统权限的设计与架构(源码可下载)
-
BAE百度云平台的mysql数据库的施用(Java)
-
[转]Java 平台中的增补字符