本文已收录【修炼内功】跃迁之路
学习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 | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
关于属性表,会在之后的文章中穿插介绍
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."<init>":()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."<init>":()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."<init>":(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 <init> #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 // "<init>":()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 // "<init>":(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."<init>":()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."<init>":()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 "<init>":(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虚拟机