代码编译的结果从本地机器码转为字节码,是储存格式发展的一小步,倒是编程语言的一大步。——《深刻理解Java虚拟机》编程
计算机只认识0和1.因此咱们写的编程语言只有转义成二进制本地机器码才能让机器认识。然而随着虚拟机的发展,包括Java在内的不少语言,都选择了一种和操做系统、机器指令集无关的中立储存格式来储存编译后的数据。数组
咱们都知道Java经典标语,“一次编译,处处运行”。实现这一目标,每一个平台上定制的虚拟机,须要读取统一的数据。这种数据不依赖于任何一种平台,甚至不关心是由哪一种语言编译来的,只要统一了格式,虚拟机就能正确的使用它。这种统一的格式就是——字节码(Class文件)。安全
Class文件中储存了Java虚拟机指令集和符号表以及若干其余辅助和结构化约束。处于安全考虑,Class文件中使用了许多强制性的语法和结构化约束。bash
下面来看下本文的硬菜,Class文件的结构。虽然说大佬书中是以JDK1.4为版本讲述的,可是它所包含的指令、属性是Class文件中最重要最基础的。后续不一样的版本都是对它的加强。架构
任何一个Class文件都对应着惟一一个类或者接口的定义信息,可是反过来讲,类和接口并不必定都得定义在文件里(譬如类和接口也能够经过类加载器直接生成)。编程语言
Class文件是以一组以8位字节为基础单位的二进制流,这个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中储存的内容几乎是程序运行的必要数据,没有空格存在。编辑器
Class有两种数据类型(虽然用十六进制编辑器打开,看上去都是十六进制字符):无符号数和表。无符号数能够用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。表是由多个无符号数或者其余表做为数据项构形成的符合数据类型,全部表都习惯性地以“info_”结尾。表用于描述层次关系的复合结构数据,整个Class文件实质上就是一张表。ui
其中相似于紧挨着的constant_pool_count、constant_pool 这样的数据可视为一个总体(一个表),前面记录后者数据的数量。this
看class文件结构那张表,第一个就是u4 magic。这是一个占了4个字节的魔数,它的惟一做用就是肯定这个文件是否为一个Class文件。它就是一个标志,告诉虚拟机本身是Class文件,这样作更加安全,四个字节储存的值是固定的,十六进制下为“0xCAFEBABE”,咖啡宝贝。编码
接下来分别是两个字节的minor(次版本)和两个字节的major(主版本)。分别储存着此Class文件时何种版本的编译器编译的,例如50.3,50就是主版本3就是次版本。在运行时能够向下兼容,好比51版本虚拟机能够运行50.3版本的class文件,可是反过来就不行了。
紧接着 constant_pool_count、constant_pool就是常量池部分。常量池能够理解为Class文件的资源仓库,它是Class文件结构中与其余项目关联最多的数据类型,也是占用Class文件最大的数据项目之一。
首先两字节的constant_pool_count是统计后面constant_pool的常量数量的。注意后面的数量是从1开始,例如constant_pool_count储存的数字是22,那么constant_pool中就储存了21个数据项。这么设计是为了让“第0个位置”储存写特殊的数据。Class文件只有这一部分计数是从1开始的,其余部分仍是从0开始。
常量池中主要储存两大类常量:字面量和符号引用。字面量好理解就是注入字符串、final修饰的常量值等等。符号引用主要包含一下三个常量:
Class文件中不会储存各个方法、字段的最终内存分布,只有在执行到特定的代码时才会知道真正的内存入口(某信息的地址)。在JDK1.4中,常量池可包含的常量项以下(之后的版本会对内容进行扩充):
最麻烦的这些类型分别有本身的结构,不过共同的特色是第一个字节都储存着tag,即告诉虚拟机本身那种常量项。从这部份内容能够看出不少东西,好比说一个变量名称最大时两个字节,即64KB英文字符大小,固然按常理来讲不会出现这样变态的变量名吧。
在常量池结束以后,紧接着两个字节表明访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息。
访问标志使用或来计算,好比一个类被ACC_PIBLIC(0x0001)、ACC_SUPER(0x0020)所修饰,那么计算为0x0001|0x0020 = 0x0021,该值就是被访问标志储存的值。Java中有专门计算关键字的包。
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引是一组u2类型的数据的集合。Class文件中有这三项来肯定继承关系。除了Object类之外,全部的夫索引都不是0。若是结构计数器的大小是0,那么后面那部分就没有数据。
字段表用于描述接口或者类中声明的变量。字段包括类级变量和实例级(对象级)变量,但不包括方法内部的局部变量。如下是字段表结构和字段表的第一个属性访问标志。
access_flags 的计算方式和前面类或者接口的访问表示相同。后面紧跟着两个属性是name_index 和 descriptor_index,分别表明着简易名称和方法描述符。
字段表集合中不会列出从超类或者父接口中继承下来的字段,可是能够列出原本Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性自动添加的字段。另外在Java中,同一个类不能出现简易名称相同的字段名,例如int name,后面紧跟着String name。可是在字节码层面,简易名称能够相同,后面的描述不一样就行了。
方法表的结构和字段表的机构基本相似。
与字段表集合相对应,若是父类方法在子类中没有被重写,方法表集合中就不会出现父类的方法信息。在Java语言中,要重载一个方法,除了要与原方法具备相同的简单名称以外,还要求拥有一个与原方法不一样的特征签名。特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是由于返回值不会包含在特征签名中,因此仅仅是返回值不一样,不是重载。
在Class文件、字段表、方法表都携带本身的属性表集合。属性表的数据项目相对于其余部分比较宽松一点,可是内容也有不少。下面来看一下比较重要的。
Java类的程序方法体中的代码通过编译后储存在Code属性中,可是接口和抽象类中的方法就不存在Code属性中。
max_locals表明了局部变量表所须要的储存空间,其中最小单位是Slot。其中Slot能够复用,当代码执行超出一个局部变量的做用域时,这个局部变量所占的Slot能够被其余局部变量所使用,极大节省了空间。
code_length和code值储存的时Java源代码编译后生成的字节码指令。因为每一个code只占了一个字节,因此能表示的指令数只有256个。code_length的长度虽然时四个字节,可是因为虚拟机的规定只能使用两个字节,因此最大只能编译65535条指令,通常来讲也是够用了,可是在编译复杂的JSP的时候要注意,某些编译器会把JSP内容和页面输出的信息归并于一个方法中,就可能致使编译失败。
值得一提的是,Javac在编译方法的时候,参数即便你没有填,agrs_size也多是1,这是因为隐式传进去了this,固然static修饰的方法参数就是0(不填写的状况下)。
曾经使用try-catch的时候,注意到finlly不会改变局部变量的值,觉得是try已经return了,return以后才去执行的finlly中的数据,其实否则。例以下面这段代码。
public int inc(){
int x;
try{
x=1;
return x;
}catch(Exception e){
x=2;
return x;
}finally{
x=3;
}
}
复制代码
这段代码永远不会输出x=3,执行顺序是这样的(以不会抛出异常为例):首先执行x=1,此时局部变量等于1.而后读到return指令,而后将x的值赋给一个空间,这个空间是return时返回的值,咱们暂且将这块空间起个名字,叫作returnX,而后代码进入finally,注意此时,还在这个inc()方法的做用域中。而后将x赋值等于3,最后执行return指令,返回刚才那块returnX空间的值给调用者。离开inc()做用域,此时x那块Slot能够被复用。
字节码指令不会超过256个,通常来讲一个指令后面会跟着参数,这很天然,就像咱们写方法时须要加入参数(没有参数也是种参数)。可是因为Java虚拟机采用面向操做数栈而不是寄存器(编译语言)的架构,因此大多数状况下只包含一个操做码。
因为字节码数量有限,因此不少指令会被强制统一。好比处理boolean、byte、short和char类型的数组时,也会转化为对应的int类型的字节码指令来处理。
字节码操做的时候可能会致使溢出,例如两个很大的正整数相加,结果可能会称为一个负数。当一个操做产生溢出时,将会使用有符号的无穷大来表示,若是某个操做结果没有明确的数字定义的话,将会使用NaN值来表示。全部使用NaN值做为操做数的算术操做,结果会返回NaN。
Java虚拟机能够支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都时使用管程(Monitor)来支持的。能够看做Synchronized此时拿的锁就是Monitor。