本文已收录 【修炼内功】跃迁之路
学习C语言的时候,需要在不同的目标操作系统上(或者使用交叉编译环境),(使用正确的CPU指令集)编译成对应操作系统可运行的执行文件,才可以在相应的系统上运行,如果使用操作系统差异性的库或者接口,还需要针对不同的系统做不同的处理(宏)
Java的出现也正是为了解决"平台无关性","Write Once, Run Anywhere"的口号也充分表达了软件开发人员对冲破平台接线的渴求
"与平台无关"的最终实现还是要在操作系统的应用层上,这便是JVM的存在,不同的平台有不同的JVM,而所有的JVM都可以载入与平台无关的字节码,从而实现程序的"一次编写,到处运行"
JVM并非只为Java设计,而字节码也并非只有Java才可以编译得到,早在Java发展之初,设计者便将Java规范拆分为Java语言规范及Java虚拟机规范,同时也承诺,对JVM做适当的扩展,以便更好地支持其他语言运行于JVM之上,而这一切的基础便是Class文件(字节码文件),Class文件中存放了JVM可以理解运行的字节码命令
In the future, we will consider bounded extensions to th Java virtual machine to provide better support for other languages
JVM并不关心Class的来源是何种语言,在JVM发展到1.7~1.8的时候,设计者通过JSR-292基本兑现了以上承诺
本篇不会去详细地介绍如何去解析Class文件,目的是为了了解Class文件的结构,Class文件中都包含哪些内容
Class文件可以由JVM加载并执行,其中记录了类信息、变量信息、方法信息、字节码指令等等,虽然JVM加载Class之后(在JIT之前)进行的是解释执行,但Class文件并不是文本文件,而是被严格定义的二进制流文件
接下来,均会以这段代码为示例进行分析
import java.io.Serializable;
public class ClassStruct implements Serializable {
private static final String HELLO = "hello";
private String name;
public ClassStruct(String name) {
this.name = name;
}
public void print() {
System.out.println(HELLO + ": " + name);
}
public static void main(String[] args) {
ClassStruct classStruct = new ClassStruct("ManerFan");
classStruct.print();
}
}
使用$ javac ClassStruct.java
进行编译,编译后的文件可以使用$ javap -p -v ClassStruct
查看Class文件的内容(见文章末尾)
魔数
很多文件存储都会使用魔数来进行身份识别,比如图片文件,即使将图片文件改为不正确的后缀,绝大多数图片预览器也会正确解析
同样Class文件也不例外,使用二进制模式打开Class文件,会发现所有Class文件的前四个字节均为OxCAFEBABE
,这个魔术在Java还被称为"Oak"语言的时候就已经确定下来了
版本号
紧接着魔术的四个字节(接下来不再对照二进制进行查看,而是直接查看javap帮我们解析出来的结果,见文章末尾)存储的是Class文件的版本号,前两个字节为次版本号(minor version),后两个字节为主版本号(major version)
Java版本号从45开始,高版本的JDK可以向下兼容低版本的Class文件,但无法向上兼容高版本,即使文件格式并未发生变化
常量池
紧接着主次版本号之后的是常量池入口
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)
字面量:如文本字符串、被声明为final的常量值等
符号引用:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述(Descriptor)、方法的名称和描述符
Java代码在进行编译时,并不像C或C++那样有"连接"这一步骤,而是在虚拟机加载Class文件时进行动态连接,Class文件中不会保存各方法和字段的内存布局,在虚拟机运行时,需要从常量池中获得对应的符号引用,再在类创建或运行时解析并翻译到具体的内存地址中,才能被虚拟机使用
访问标志
访问标志用于识别一些类或接口层次的访问信息
访问标志用于识别这个Class是类还是接口;是否定义为public;是否为abstract类型;是否声明为final;等等,具体标志含义如下
标志 | 名称 |
---|---|
ACC_PUBLIC | 是否为public类型 |
ACC_FINAL | 是否被声明为final |
ACC_SUPER | 是否允许使用invokespecial字节码指令 |
ACC_INTERFACE | 是否为接口 |
ACC_ABSTRACT | 是否为abstract |
ACC_SYNTHETIC | 标识这个类并非由用户代码生成 |
ACC_ANNOTATION | 标识这是一个注解 |
ACC_ENUM | 标识这是一个枚举 |
类索引、父类索引、接口索引集合
Class文件中由类索引(this_class)、父类索引(super_class)及接口索引集合(interfaces)三项数据确定这个类的继承关系
父类索引只有一个(对应extends语句),而接口索引则是一个集合(对应implements语句)
字段表集合
字段表(field_info)用于描述类或者接口中声明的变量
字段(field)包括了类级变量(如static)及实例级变量,但不包括在方法内部声明的变量
字段包含的信息有:作用域(public、private、protected)、类级还是实例级(static)、可变性(final)、并发可见性(volatile)、可否序列化(transient)、数据类型、字段名等
描述符
这里简单解释一下描述符(descriptor)
描述符用来描述字段数据类型、方法参数列表和返回值,根据描述符规则,基本数据类型及代表无返回值的void类型都用一个大写字符表示,对象类型则用字符L
加对象全限定名来表示
标识字符 | 含义 |
---|---|
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
Z | boolean |
V | void |
L | 对象类型,如 Ljava/lang/Object; |
对于数组,每一个维度使用一个前置的[
来描述,如java.lang.String[][]
将被记录为[[java/lang/String;
,int[]
将被记录为[I
描述方法时,按照先参数列表,后返回值的顺序描述,参数列表放在()
内,如void inc()
描述符为()V
,方法java.lang.String toString()
描述符为()Ljava/lang/String;
,方法int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex
的描述符为([CII[CIII)I
方法表集合
Class文件中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes),但是方法内部的代码并不在方法表中,而是经过编译器编译成字节码指令后,存放在属性表集合中一个名为"Code"的属性中
属性表集合
在Class文件、字段表、方法表中都可以携带自己的属性表集合,用于描述某些场景专有的信息,Java虚拟机规范中预定义了9种虚拟机实现应当能识别的属性
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键自定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 原文件名称 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
关于属性表,会在之后的文章中穿插介绍
附:Class文件
Classfile ~/articles/【修炼内功】跃迁之路/JVM/[JVM] 类文件结构/src/ClassStruct.class
Last modified 2019-6-2; size 829 bytes
MD5 checksum 9f7454acd0455837a33ff8e03edffdb3
Compiled from "ClassStruct.java"
public class ClassStruct implements java.io.Serializable
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #14.#31 // java/lang/Object."":()V
#2 = Fieldref #6.#32 // ClassStruct.name:Ljava/lang/String;
#3 = Fieldref #33.#34 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Class #35 // java/lang/StringBuilder
#5 = Methodref #4.#31 // java/lang/StringBuilder."":()V
#6 = Class #36 // ClassStruct
#7 = String #37 // hello:
#8 = Methodref #4.#38 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#9 = Methodref #4.#39 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#10 = Methodref #40.#41 // java/io/PrintStream.println:(Ljava/lang/String;)V
#11 = String #42 // ManerFan
#12 = Methodref #6.#43 // ClassStruct."":(Ljava/lang/String;)V
#13 = Methodref #6.#44 // ClassStruct.print:()V
#14 = Class #45 // java/lang/Object
#15 = Class #46 // java/io/Serializable
#16 = Utf8 HELLO
#17 = Utf8 Ljava/lang/String;
#18 = Utf8 ConstantValue
#19 = String #47 // hello
#20 = Utf8 name
#21 = Utf8
#22 = Utf8 (Ljava/lang/String;)V
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 print
#26 = Utf8 ()V
#27 = Utf8 main
#28 = Utf8 ([Ljava/lang/String;)V
#29 = Utf8 SourceFile
#30 = Utf8 ClassStruct.java
#31 = NameAndType #21:#26 // "":()V
#32 = NameAndType #20:#17 // name:Ljava/lang/String;
#33 = Class #48 // java/lang/System
#34 = NameAndType #49:#50 // out:Ljava/io/PrintStream;
#35 = Utf8 java/lang/StringBuilder
#36 = Utf8 ClassStruct
#37 = Utf8 hello:
#38 = NameAndType #51:#52 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#39 = NameAndType #53:#54 // toString:()Ljava/lang/String;
#40 = Class #55 // java/io/PrintStream
#41 = NameAndType #56:#22 // println:(Ljava/lang/String;)V
#42 = Utf8 ManerFan
#43 = NameAndType #21:#22 // "":(Ljava/lang/String;)V
#44 = NameAndType #25:#26 // print:()V
#45 = Utf8 java/lang/Object
#46 = Utf8 java/io/Serializable
#47 = Utf8 hello
#48 = Utf8 java/lang/System
#49 = Utf8 out
#50 = Utf8 Ljava/io/PrintStream;
#51 = Utf8 append
#52 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#53 = Utf8 toString
#54 = Utf8 ()Ljava/lang/String;
#55 = Utf8 java/io/PrintStream
#56 = Utf8 println
{
private static final java.lang.String HELLO;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
ConstantValue: String hello
private java.lang.String name;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE
public ClassStruct(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: aload_1
6: putfield #2 // Field name:Ljava/lang/String;
9: return
LineNumberTable:
line 7: 0
line 8: 4
line 9: 9
public void print();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #4 // class java/lang/StringBuilder
6: dup
7: invokespecial #5 // Method java/lang/StringBuilder."":()V
10: ldc #7 // String hello:
12: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_0
16: getfield #2 // Field name:Ljava/lang/String;
19: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
28: return
LineNumberTable:
line 12: 0
line 13: 28
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #6 // class ClassStruct
3: dup
4: ldc #11 // String ManerFan
6: invokespecial #12 // Method "":(Ljava/lang/String;)V
9: astore_1
10: aload_1
11: invokevirtual #13 // Method print:()V
14: return
LineNumberTable:
line 16: 0
line 17: 10
line 18: 14
}
SourceFile: "ClassStruct.java"
参考:
深入理解Java虚拟机