本文已收录 【修炼内功】跃迁之路
学习C语言的时候,须要在不一样的目标操做系统上(或者使用交叉编译环境),(使用正确的CPU指令集)编译成对应操做系统可运行的执行文件,才能够在相应的系统上运行,若是使用操做系统差别性的库或者接口,还须要针对不一样的系统作不一样的处理(宏)java
Java的出现也正是为了解决"平台无关性","Write Once, Run Anywhere"的口号也充分表达了软件开发人员对冲破平台接线的渴求segmentfault
"与平台无关"的最终实现仍是要在操做系统的应用层上,这即是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基本兑现了以上承诺app
本篇不会去详细地介绍如何去解析Class文件,目的是为了了解Class文件的结构,Class文件中都包含哪些内容ide
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文件的内容(见文章末尾)ui
不少文件存储都会使用魔数来进行身份识别,好比图片文件,即便将图片文件改成不正确的后缀,绝大多数图片预览器也会正确解析this
一样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虚拟机