咱们平时在DOS界面中每每须要运行先运行javac命令,这个命令的直接结果就是产生相应的class文件,而后基于这个class文件才能够真正运行程序获得结果。天然。这是Java虚拟机的功劳,那么是否是Java虚拟机只能编译.java的源文件呢?答案是否认的。时至今日,Java虚拟机已经实现了语言无关性的特色。而实现语言无关性的基础是虚拟机和字节码的存储格式,Java虚拟机已经不和包括Java语言在内的任何语言绑定。它只与“class”文件这种特定的二进制文件相关联。在class文件中包含了Java虚拟机指令集和符号表以及若干辅助信息。能够很容易想到Java(本质上不是Java语言自己的平台无关性,而是其底层的Java虚拟机的平台无关性使然。)的跨平台,由于任何一门功能性语言均可以表示为能被Java虚拟机接受的有效的class文件。好比,除了Java虚拟机能够将Java源文件直接编译为class文件外,使用JRuby等其余语言的编译器同样能够把程序代码编译成class文件,因而可知,Java虚拟机并不关心class文件是由何种语言编译来的。java
Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在class文件中,中间没有任何分隔符,这使得class文件中存储的内容几乎是所有程序运行的程序。Java虚拟机规范规定,Class文件格式采用相似C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表。数组
无符号数属于基本数据类型,主要能够用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,大小使用u一、u二、u四、u8分别表示1字节、2字节、4字节和8字节。安全
表是由多个无符号数或者其余表做为数据项构成的复合数据类型,全部的表都习惯以“_info”结尾。那么表是干吗的呢?表主要用于描述有层次关系的复合结构的数据,好比方法、字段。须要注意的是class文件是没有分隔符的,因此每一个的二进制数据类型都是严格定义的。具体的顺序定义以下:数据结构
在class文件中,主要分为魔数、Class文件的版本号、常量池、访问标志、类索引(还包括父类索引和接口索引集合)、字段表集合、方法表集合、属性表集合。并发
头4个字节是魔数,魔数的惟一做用在于肯定这个Class文件是不是Java虚拟机接受的Class文件。如gif和jpeg等在文件头中都存在魔术,使用魔术而不是使用扩展名是基于安全性考虑的——扩展名能够随意被改变。Class文件的魔术值为“0xCAFEBABE”(咖啡宝贝?)。布局
紧接着魔数的4个字节是Class文件版本号:版本号又分为次版本号和主版本号。其中前两个字节用于表示次版本号,后两个字节用于表示主版本号。这个的版本号是随着jdk版本的不一样而表示不一样的版本范围的。若是Class文件的版本号超过虚拟机版本,将被拒绝执行。性能
常量池能够简单理解为class文件的资源从库,这种数据类型是Class文件结构中与其余项目关联最多的数据类型,也是占用Class文件空间最大的项目之一。在常量池中主要存放字面量和符号引用。字面量比较接近Java语言层面的常量概念,好比文本字符串、声明为final的常量值等(百度百科的解释是字面量是用双引用号引住的一系列字符)。符号引用则主要包括三类常量:编码
符号引用与直接引用的关联spa
符号引用是一组符号,用来描述所引用的目标,符号是以任何形式存在的字面量。对于符号引用Java虚拟机并无严格的限制。规定只须要使用的时候可以无歧义定位到目标就能够。常量池存在于Class文件中,而Class文件是必须首先经过Java虚拟机的类加载机制加载到内存中(确切的说是方法区这个内存区域,回顾一下,方法区存放的主要是对象的实例,这个Class文件是虚拟机对外接受访问的接口)。符号引用属于常量池中的内容,那么是否是说符号引用的目标已经加载到内存中了呢?答案是否认的,由于符号引用与虚拟机的内存布局无关,符号引用的目标并不必定已经加载到内存中了。翻译
直接引用能够是直接指向引用目标的指针、相对偏移量或者是一个可以间接定位到目标的句柄。直接引用是和虚拟机的内存布局有关的,同一个符号引用在不一样的虚拟机上翻译的直接引用通常是不一样的。若是有了直接引用,那么引用的目标一定是存在内存中的。
在常量池中每一项常量都是一个表,在jdk1.7中共有14中常量类型,因此常量池的项目就对应14张表,这14张表的每种类型都不同。可是有一个共同特色:表开始的第一位都是一个u1类型的标志位,表明这个常量属于哪一种类型。
须要注意的是,在Class文件中,方法、字段都须要引用CONSTANT-Utf8_info类型的常量,因此这种类型的常量的长度有必定的限制,也就是Java中方法、字段的最大长度。在CONSTANT-Utf8_info中,其length的值u2,说明Java虚拟机只能编译最大大约64KB的变量或者方法名。超过的话将不会进行编译。
常量池以后的数据结构是访问标志(access_flags),这个标志主要用于识别一些类或者接口层次的访问信息,主要包括:这个Class是类仍是接口、是否认义public、是否认义abstract类型;若是是类的话是否被声明为final等。具体的标志访问以下:
这个数据项主要用于肯定这个类的继承关系。
其中类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据。在Java中因为不容许多继承,因此父类索引是惟一的,可是一个类能够实现多个接口,因此获得的接口索引是一个集合,表示这个类实现了哪些接口。
字段表用于描述接口或者类中声明的变量。
字段包括类级变量和实例级变量,可是不包括方法内部声明的局部变量(这些变量是存储在Java虚拟机栈中的局部变量表中的)。天然,描述一个字段的信息包括:字段的做用域(public、protected、private)、实例变量与否(static)、可变性(final)、并发可见性(volatile)、能否被序列化(transient)、字段数据类型(基本数据类型、对象、数组)、字段名称。字段的信息也被存放在一张表中,其字段表包括三种类型:
上面出现了简单名称,上文中出现了全限定名,以及这里出现的描述符,三者有什么区别呢?其中全限定名称比较好理解,就是类的完整路径信息。而简单名称则是指没有类型和参数修饰的方法或者字段名称,好比一个方法以下:
public void inc(int a,int b){ System.out.println(a+b); }
那么这个方法的简单名称就是inc。
相对于以上二者,描述符相对复杂一些。描述符的主要的做用是描述字段的数据类型、方法的参数列表和返回值。其中咱们熟悉的void,在Class文件中用V表示。下面是完整的描述符标志的含义:
对于数组类型,每一维度使用一个前置的“[”字符描述,若是是二维数组,那么就有两个“[”符号。好比“java.lang.String[][]”会被记录成“[[Ljava.lang.String;”
对于方法,则是按照县参数列表后返回值的顺序进行描述的。好比方法int inc(int a,int[] b,char[][] c,int d)的描述符是“(I[I[[CI)I”。
JVM中堆方法表的描述与字段表是一致的,包括了:访问标志、名称索引、描述符索引、属性表集合。方法表的结构与字段表是一致的,区别在于访问标志的不一样。在方法中不能用volatile和transient关键字修饰,因此这两个标志不能用在方法表中。在方法中添加了字段不能使用的访问标志,好比方法可使用synchronized、native、strictfp、abstract关键字修饰,因此在方法表中就增长了相应的访问标志。
要注意的是,若是父类方法没有在子类中重写,那么在方法中不会自动出现来自父类的方法信息。一样的,有可能添加编译器自动增长的方法,好比方法。
前面的Class文件、字段表和方法表均可以携带本身的属性信息,这个信息用属性表进行描述,用于描述某些场景专有的信息。在属性表中没有相似Class文件的数据项目类型和顺序的严格要求,只要新的属性不与现有的属性名重复,任何人均可以向属性表中写入本身定义的属性信息。
Code属性
Java程序方法体中的代码通过javac编译最终编译成的字节码指令就保存在Code属性中。可是并不是全部的方法表都必须存在这个属性。Code属性是Class文件中最重要的一个属性,若是把一个Java程序中的信息分为代码(Code)和元数据(Metadata,包括类、字段、方法定义及其其余信息)两部分,那么在整个Class文件中,Code属性用于描述代码,全部其余的数据项目都用于描述元数据。
Exceptions属性
这个属性的做用是列举出方法中可能抛出的受查异常(Checked Exception),也就是描述throws 后的列举的异常
LineNumberTable属性
主要用于描述Java源代码行号与字节码行号之间的对应关系。这个属性也不是必须的。若是没有这个属性,对程序的直接影响就是当抛出异常的时候没法显示对应的行号;而且在调试的时候没法经过设置断点的方法是调试程序。
LocalVariableTable属性
用于描述栈帧中局部变量表中的变量与Java源码中定义的变量的之间的关系。也不属于必须的属性。若是没有这个属性,产生的直接影响就是当别人引用这个方法的时候,全部的参数名称都会丢失,IDE将会使用诸如args0、args1之类的参数进行显示。天然,当调试程序的时候,显示的参数名称是不可知的。
SourceFile属性
用于记录这个Class文件的源码文件名称。若是不使用这个属性,那么当抛出异常的时候,堆栈中将不会显示出错代码所属的文件名。
ConstantValue属性
做用是通知虚拟机自动为静态变量赋值。要注意的是,只有被static关键字修饰的额变量才可使用这个属性(类变量)。对于非类变量,初始化是在方法中进行的;对于类变量能够选择两种方式进行变量的初始化:一是在类构造器方法中使用;二是是ConstantValue属性。目前Sun Hotspot的选择原则是:若是一个变量同时使用static和final关键字修饰,而且这个变量是基本数据类型或者java.lang.String类型的话,就使用ConstantValue属性进行初始化。若是没有被final修饰或者并不是是基本数据类型,那么将会选择使用方法进行初始化。
InnerClass属性
这个属性主要用于记录内部类与宿主类之间的关联关系。
Deprecated以及Synthetic属性
这两个属性都属于标志类型的布尔属性,只存在有没有的区别。
Deprecated属性用于表示某个类、字段或者方法,已经被程序做者定为再也不推荐使用,能够经过注解@deprecated实现
Synthetic属性表明此字段并非由Java源码产生的,而是经过编译器自行添加的。
StackMapTable属性
该属性的目的在于代替之前比较消耗性能的基于数据流分析的类型推导验证器。
Signature属性
这个属性是专门用来记录泛型类型的,由于在Java语言采用的是擦除法实现的泛型,在字节码(Code属性)中,泛型信息编译以后会被擦除。擦除法的优势是可以节省泛型所占的内存空间,缺点是在运行期间没法经过反射获得泛型信息,而Signature属性则弥补了这一缺陷。如今的Java反射API已经可以获得泛型信息,功劳就在于这个属性。
BootstrapMethods属性
这个属性用于保存invokedynamic指令引用的引导方法限定符。该指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。
参考 一、周志明,深刻理解Java虚拟机:JVM高级特性与最佳实践,机械工业出版社