上节学习回顾java
在上一节当中,主要以本身的工做环境简单地介绍了一下自身的一些调优或者说是故障处理经验。所谓百变不离其宗,这个宗就是咱们解决问题的思路了。编程
本节学习重点数组
在前面几章,咱们宏观地了解了虚拟机的一些运行机制,那么从这一章节开始,咱们将更加深刻虚拟机的深处去了解其运行细节了。例如本章节的学习重点是类文件的结构,也就是虚拟机的数据入口。既然是数据入口,确定得要符合虚拟机的数据定义规范才能给虚拟机处理,不然它压根就不认识你。架构
概述并发
在学习以前,先抛出一个比较常见的问题:C语言与Java的运行效率如何?其实这个问题随着技术的发展愈来愈很差回答,先看看下图:函数
若是单单看C语言和Java语言的一个运行流程,我会毫无疑问的举起手脚投C语言运行效率比Java的运行效率高,但随着技术的进步和发展(后面章节会学习到的技术),我只能说Java的运行速度跟其它的高级语言相比只会愈来愈近,而且某些状况不输给C语言。固然,这一章节讨论的不是跟其它语言比效率,是先给你和我一个比较宏观的角度去理解类文件的位置。Java语言最大的优点就是一次编译处处运行,不像C语言文件在不一样的操做系统会有兼容性问题。但凡事有收获就确定有付出的,世界上没有那么完美的事情,Java这跨平台的优点也却倒是劣势。由于多了一层“虚拟机系统”这件“温暖的棉袄”才得以让Java能够处处跑,也确实有人用C语言是裸奔而Java是裹着棉袄奔跑来形容二者的运行效率。C语言编译后是机器语言文件能够直接执行,而Java语言文件编译后是类文件。类文件还须要在虚拟机运行时(解释+编译)转换成机器语言才能执行。若是咱们直接去查看机器语言文件,里面除了0就是1,这就是计算机惟一认识的两个字。由于类文件也称字节文件,就是以一个字节(8bit)为单位组成的文件, 用文本打开同样是全是0和1的二进制样式,但类文件的二进制规则和机器语言的二进制规则又有所不一样。例如类文件开头的前32位(4字节)是定义类文件的标识,前32位字节若是Java虚拟机不认识,那就不是类文件了。同理,若是计算机硬件不认识这个二进制文件的排版规则,那就是这个不是机器语言。而这一章节主要学习的就是类文件是如何组成的?又有哪些规则?其实说白了,类文件也是一种语言文件,只不过面对的不是咱们这些普罗大众的应用开发者,而是面向于那些基于Java虚拟机的语言设计者和开发者看的而已。工具
无关性的基石性能
“一次编写,处处运行(Write Once,Run Anywhere)”是Java诞生的主要目的,这是多硬件和多操做系统时代发展的必然选择,我想就算今天没有JVM的诞生也会有其它跨平台虚拟机取而代之。虚拟机充当了兼容不一样平台的“中间件”,不一样平台都会有一个对应的虚拟机,解放了字节码文件(类文件)对不一样平台的兼容性(例如C语言的兼容性问题),统一了对字节码的规范。在设计者周全的考虑下,把Java的规范拆分红了Java语言规范《The Java Language Specification》及Java虚拟机规范《The Java Virtual Machine Specification》。也就是不仅仅只有Java语言能运行在Java虚拟机上,其它遵循Java虚拟机规范的语言同样能够运行在Java虚拟机上,好比JRuby、Groovy、Scala以及Clojure等语言。这些语言只要经过编译成符合Java虚拟机规范的字节码文件(.class)就能运行在Java虚拟机上。因为各语言实现规范的方式不一致,因此会出现语言之间的一些特性会有所不一样,但它们最终都是经过字节码的命令组成的。学习
Class类文件的结构优化
Class文件是一组以8位字节为基础单位的二进制流,因此咱们有时候也称之为字节文件。各个数据项是字节按照类文件组成规范严格按顺序紧凑地排列在Class文件之中,中间是没有任何分隔符的,因此你们把Class文件打开来看就像看机器码同样一堆十六进制字符,以下图所示:
按照Java虚拟机规范所说,Class文件格式采用一种相似了C语言结构体的伪结构来存储数据,这种伪结构中占有两种数据类型:无符号数和表。无符号数就是基本的数据类型,以u一、u二、u四、u8来分表表明1个字节、2个字节、4个字节和8个字节的无符号数。而表由多个无符号数或者其它表做为数据项构成的负荷结构数据, 全部表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,它由如下数据项构成:
类型 |
名称 |
数量 |
u4 |
magic |
1 |
u2 |
minor_version |
1 |
u2 |
major_version |
1 |
u2 |
constant_pool_count |
1 |
cp_info |
constant_pool |
constant_pool_count |
u2 |
access_flags |
1 |
u2 |
this_class |
1 |
u2 |
super_class |
1 |
u2 |
interfaces_count |
1 |
u2 |
interfaces |
interfaces_count |
u2 |
fileds_count |
1 |
filed_info |
fileds |
fileds_count |
u2 |
methods_count |
1 |
method_info |
methods |
methods_count |
u2 |
atributes_account |
1 |
atribute_info |
atributes |
atributes_account |
Class文件格式表说明
以上表格的排版结构就是整个Class文件格式的“表面结构”,也就是第一层次的结构。为何说是“表面结构”,是由于上面也介绍过Class文件表的可扩展性的问题,每一层的表里面还能够隐藏着另外一个层次的表,以此类推。为了让人更好理解,能够去想象一下XML的组织架构。XML看似简单,但一样因为节点的扩展性问题,除了同一层次的横向扩展,还能够无限垂直扩展形成深度复杂的数据架构。二者道理是同样的,不过Class的结构又不像XML等描述语言那样,先前也提到过,因为类文件结构没有任何分隔符,因此在以上表格描述的数据项中不管是顺心仍是数量,甚至于数据存储的字节序这样的细节都是被严格限定的,例如哪一个字节表明什么含义,长度多少,前后顺序如何等都不容许改变。而XML的同一层次的节点能够改变顺序且不影响器数据的表达。这些就像我先前说的,XML是给普罗大众看的,而Class文件是给虚拟机看的。因此这些所谓的分隔符等人性化标志就不须要了,省得浪费空间了。
类文件结构之:魔数与文件版本
请看如下截图的红色框部分,前4个字节称为魔数(Magic Number),它是惟一肯定这个文件为Class文件的标识说明,许多的文件都用相似的魔数进行身份识别标识,例如GIF或JPEG等文件都存在魔数。从0XCAFEBABE(咖啡宝贝?)这个魔数大概能够看出,为何Java的Logo是一杯咖啡了。近接其后的就是Class文件的次版本号(第五、6个字节)和主版本号(第七、8个字节)了。从十六进制规范看是0032.0000,转化为十进制后就是50.00。JDK时从1.0~1.1版本使用了45.0~45.3,从JDK1.1后每一个大版本发布主版本号向上加1,也就是46.00表示JDK1.二、47.00表示JDK1.3以此类推,那么上文的50.00表示JDK1.6了。
类文件结构之:常量池
从Class文件的第一层结构能够看到,magic、minor_version、major_version以后的就是constant_pool_count以及constant_pool,也就是常量池数量以及常量池,常量池数量为u2类型,也就是占用两个字节,从以上的类文件能够看到偏移量为0X00000008日后的两个字节就是常量池数量的值:0X0016,也就是22个常量,不过java规定常量池的索引值从1开始,第0项常量空出来是有特殊考虑,这样作的目的在于知足后面某些指向常量池的索引值的数据在特定状况下须要表达“不引用任何一个常量池项目”的含义。也就是说实际的常量有21个。由于“常量[0]”表示“不引用任何常量”,那么就从常量[1]开始,而第一个常量就是紧跟随常量数量的标识后继续延伸,也就是类文件偏移量0X0000000A,开始翻译类文件有哪些常量前,先介绍一下常量池的项目类型,毕竟一个常量池包含了多种类型,知道各类类型的表示方式才知道常量表示什么,这种结构就有点相似我在“Class类文件结构”介绍的“表中表”常量池的各类项目类型就是第二层的表,以下所示:
类型 |
标志 |
描述 |
CONSTANT_Utf8_info |
1 |
UTF-8编码字符串 |
CONSTANT_Integer_info |
3 |
整型字面量 |
CONSTANT_Float_info |
4 |
浮点型字面量 |
CONSTANT_Long_info |
5 |
长整型字面量 |
CONSTANT_Double_info |
6 |
双精度浮点型字面量 |
CONSTANT_Class_info |
7 |
类或接口的符号引用 |
CONSTANT_String_info |
8 |
字符串类型字面量 |
CONSTANT_Fieldref_info |
9 |
字段符号的应用 |
CONSTANT_Methodref_info |
10 |
类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info |
11 |
接口中方法的符号引用 |
CONSTANT_NameAndType_info |
12 |
字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info |
15 |
标识方法句柄 |
CONSTANT_MethodType_info |
16 |
标识方法类型 |
CONSTANT_InvokeDtnamic_info |
18 |
表示一个动态方法调用点 |
常量池项目类型说明
以上介绍的每一个常量项目类型都是以“_info”结尾,看来也都是表中表了。接着继续跟踪上文介绍的21个常量中的地1个常量,在类文件偏移量0X0000000A上的十六进制是07,再结合常量类型表的各类类型标识能够对比出来标识7是CONSTANT_Class_info(每一个常量表的第一个字节都是常量类型标识),接下来在看看CONSTANT_Class_info的表结构:
类型 |
名称 |
数量 |
u1 |
tag |
1 |
u2 |
name_index |
1 |
CONSTANT_Class_info型常量结构
从CONSTANT_Class_info表结构看出由u1+u2一共三个字节组成,tag表就是刚才所说的常量标识,而接下来的两个字节就是name_index的表示,name_index是一个索引值,既然是索引值,确定是指向其它地方去了。再看看它指向谁了。继续下移类文件偏移量到0X0000000B来,name_index值为0X0002,也就是指向了常量池中的第二项目(即常量[2]),从这一点能够看出,常量项目的各类项目结构中会存在索引指向,而索引值就表明常量池中第几个常量的意思了。那行,接下来继续看第二个常量,由于CONSTANT_Class_info是刚才介绍的第一个常量,那么第二个常量就是从类文件偏移量0X0000000D处开始了,此处的值为0X01,再对照常量项目表的标识能够获得此常量是一个CONSTANT_Utf8_info的类型常量,也就是一个字符串了。那行,继续介绍CONSTANT_Utf8_info常量类型的结构:
类型 |
名称 |
数量 |
u1 |
tag |
1 |
u2 |
length |
1 |
u1 |
bytes |
length |
CONSTANT_Utf8_info型常量结构
从CONSTANT_Utf8_info能够看到,第一个类型(tag)为常量标识,这都知道了。第二个类型(length)该字符串长度,也就是这个字符串有多长,那么第三个类型表示该字符串的各个字节了,从u1也能够看出,具体数量为多少个字节了,也就是多少个byte呢,那得靠第二个类型字段length决定了,因此他的数量也写着length。继续跟踪类文件偏移量看看length到底多少,看类文件偏移量0X0000000E,length是u2类型,占用两个字节,那么值为0X001D,十进制也就是29了,那说明这个CONSTANT_Utf8_info类型的字符串长度为29了,那继续把当前文件偏移量移动个29字节,字节值以下:
这串值是使用UTF-8缩略编码表示的,UTF-8缩略编码与普通UTF-8的区别是:从‘\u0001’到‘\u007f’之间的字符(至关于1~127的ASCII码)的缩略编码使用一个字节表示,从‘\u0080’到‘\u07ff’之间的全部字节的缩略编码用两个字节表示,从‘\u0800’到‘\uffff’之间的全部字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示。缩略编码说白了就是为了节省类文件空间吗。若是按照UTF-8缩略编码去编译以上29个字节,那么转换获得的字符串也就是“org/fenixsoft/clazz/TestClass”,再结合第一个常量(CONSTANT_Class_info)就很明白了,这是一个完整的类名了。好了,若是再这么解释下去我本身都晕了,咋们仍是靠工具(javap)去看看这个类文件到底怎么回事吧:
$javap –verbose TestClass Compiled from "TestClass.java" public class org.fenixsoft.clazz.TestClass extends java.lang.Object SourceFile: "TestClass.java" minor version: 0 major version: 50 Constant pool: const #1 = class #2; // org/fenixsoft/clazz/TestClass const #2 = Asciz org/fenixsoft/clazz/TestClass; const #3 = class #4; // java/lang/Object const #4 = Asciz java/lang/Object; const #5 = Asciz m; const #6 = Asciz I; const #7 = Asciz <init>; const #8 = Asciz ()V; const #9 = Asciz Code; const #10 = Method #3.#11; // java/lang/Object."<init>":()V const #11 = NameAndType #7:#8;// "<init>":()V const #12 = Asciz LineNumberTable; const #13 = Asciz LocalVariableTable; const #14 = Asciz this; const #15 = Asciz Lorg/fenixsoft/clazz/TestClass;; const #16 = Asciz inc; const #17 = Asciz ()I; const #18 = Field #1.#19; // org/fenixsoft/clazz/TestClass.m:I const #19 = NameAndType #5:#6;// m:I const #20 = Asciz SourceFile; const #21 = Asciz TestClass.java; { public org.fenixsoft.clazz.TestClass(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #10; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lorg/fenixsoft/clazz/TestClass; public int inc(); Code: Stack=2, Locals=1, Args_size=1 0: aload_0 1: getfield #18; //Field m:I 4: iconst_1 5: iadd 6: ireturn LineNumberTable: line 9: 0 LocalVariableTable: Start Length Slot Name Signature 0 7 0 this Lorg/fenixsoft/clazz/TestClass; }
从以上javap工具对类文件的解析,能够看到Constant pool的全部内容,从内容中也能够看到一共有21个常量,而第一个常量就是class类型的,指向了第二个常量,而第二个常量就是字符串“org/fenixsoft/clazz/TestClass”。
类文件结构之:访问标志
继续按照Class第一次结构层次顺序学习,接下来学习就是访问标志(access_flags),这些标志经常使用于类或者接口层次的访问信息。也很好理解,既然上面介绍了类名,接下来要说明的确定是类或者接口的访问权限了,例如咱们写类的第一个标识字段有public、default、protected、private,Class除了以上几个权限标志外,还有final的修饰等,具体标志值请看下表:
标志名称 |
标志值 |
含义 |
ACC_PUBLIC |
0X0001 |
是否为public类型 |
ACC_FINAL |
0X0010 |
是否被声明为final,只有类能够设置 |
ACC_SUPER |
0X0020 |
是否容许使用invokespecial字节码指令的新语意,invokespecial指令的语意在JDK1.0.2发生过改变,为了区别这条指令使用哪一种语意,JDK1.0.2以后编译 |
ACC_INTERFACE |
0X0200 |
标志这是一个接口 |
ACC_ABSTRACT |
0X0400 |
是否为abstract类型,对于接口或者抽象类来讲,此标志值为真,其余类为假 |
ACC_SYNTHETIC |
0X1000 |
标志这个类并不是由用户代码产生的 |
ACC_ANNOTATION |
0X2000 |
标志这是一个注解 |
ACC_ENUM |
0X4000 |
标志这是一个枚举 |
类访问标志
从Class文件格式表说明能够看出,访问标志是u2类型,也就是占用两个字节,咱们继续经过类二进制文件继续查看TestClass显示的是什么。把以上全部常量跳过以后来到访问标志的偏移地址以下所示:
0X0021也就是0X0001|0X0020的值,说明TestClass的ACC_PUBLIC和ACC_SUPER为真。
类文件结构之:类索引、父类索引与接口索引集合
继续从Class文件结构的第一层出发,学习完访问标志,接下来的三个就是类索引、父类索引与接口索引,而类索引和父类索引都是一个u2类型标识,而接口是一组u2类型的组合。咱们都知道,类引用(this)和父类引用(super)都只有一个,而接口能够继承多个,因此接口索引是一组组合也不难理解。继续观察类二进制编码状况:
由于是索引值,因此指向的都是常量池的内容,先看看类索引值(0X0001)指向了常量池的第一个常量,也就是类org/fenixsoft/clazz/TestClass。在看父类索引值(0X0003)指向了常量池的第三个变量,也就是java/lang/Object。而接口索引的数量值(0X0000)为0,就是没有接口引用。
类文件结构之:字段表集合
按照类文件结构的第一层顺序看,其实跟咱们平时写类的顺序是一致的,定义类路径、名称、继承关系,接下来定义的就是类属性字段了。因此接下来要学习的就是类结构中的字段表集合标识(不包含局部变量)。从类文件结构表看到,类字段是由一个u2类型的fields_count以及field_info类型的fields组成的,每一个字段(field)都是field_info的结构,具体有多少个field那就由field_acount决定了,先来看看field_info结构如何:
类型 |
名称 |
数量 |
u2 |
access_flags |
1 |
u2 |
name_index |
1 |
u2 |
descriptor_index |
1 |
u2 |
attribute_count |
1 |
attribute_info |
attributes |
attribute_count |
字段表结构
我相信access_flags已经很眼熟了,就是相似于Class的访问标识,字段一样有访问标识,如做用域(private、prodected、 public)、static修饰符、可变性final、并发可见性volatile、能否被序列化transient、基本数据类型(基本类型、对象、数组)还有字段名称。这些修饰信息都是布尔值,一样相似于Class访问标识的组成方式。下面来看看这些访问标识的标志值:
标志名称 |
标志值 |
含义 |
ACC_PUBLIC |
0X0001 |
字段是否public |
ACC_PRIVATE |
0X0002 |
字段是否private |
ACC_PROTECTED |
0X0004 |
字段是否protected |
ACC_STATIC |
0X0008 |
字段是否static |
ACC_FINAL |
0X0010 |
字段是否final |
ACC_VOLATILE |
0X0040 |
字段是否volatile |
ACC_TRANSIENT |
0X0080 |
字段是否transient |
ACC_SYNTHETIC |
0X0100 |
字段是否由编译器自动产生的 |
ACC_ENUM |
0X0400 |
字段是否enum |
字段访问标志
继续探索field_info结构的第二个字段(name_index),经过以上学习经验大概能够得知以index结尾的而由没有特殊说明的都是指向常量池的。而这个name_index就是字段的“简单名称”,例如private int age,age就是这个“简单名称”。而descriptor_index一样是指向常量池的一个字段“描述符”,仍是以age字段为例子,这个int就是对age的一个描述符,对于描述符,JVM还有有一套规范的:
标识字符 |
含义 |
B |
基本类型byte |
C |
基本类型char |
D |
基本类型double |
F |
基本类型float |
I |
基本类型int |
J |
基本类型long |
S |
基本类型short |
Z |
基本类型boolean |
V |
特殊类型void |
L |
对象类型。如Ljava/lang/Object |
描述符标识字符含义
除了这里讲解的“简单名称”和“描述符”外,还有以上类名(如org/fenixsoft/clazz/TestClass)称为“全限定名”。这些字符名称还有要加以区分。
此外,字段表结构还有属性(attribute_count和attributes),本文例子TestClass是没有用到这个属性的,而属性的结构格式后续节点还有介绍,这里暂时不详细写了。经过以上字段结构的属性,再结合TestClass字节文件回顾一下以上的学习内容。下来看看TestClass有多少个字段,看TestClass字节码表示字段集合的地址位置:
0X0001表示的是field_count,代表TestClass类有一个字段,接下来的字节码就是表示第一个字段结构的开始了,0X0002表示的是access_flags了,从字段访问标志表能够看出该字段为ACC_PRIVATE标识,表明私有类型。在来看第二个属性name_index,值为0X0005,表明指向常量池的第五个值,结合以上常量池看就是“m”了,再来看看descriptor_index值0X0006,常量池的第六个常量是I,由于这个字段的属性数量为0X0000,因此表明没有属性,因此这个字段就到此结束。不难看出,TestClass的一个字段定义为:“private int m;”。
在这里仍是有必要介绍一下对象类型的描述“L”,若是是一个有开发经验的Java开发人员的话,我相信“LJava/…”这类字符串看到也很多了,这就是JVM规范定义的对象类型了。L为对象类型,那么具体是什么类型,还得看跟在L后面的全限定名,这个名称就是具体的对象名称了,名称后以“;”表明描述结束。数组同理,“[”符号表示数组类型,那具体是什么数组类型,还得结合数组符号后面的对象标示符+全限定名了。例如“[Ljava/lang/String;”表示的是字符串数组,若是是二维数组,那就是两个“[”符号了,如“[[Ljava/lang/String;”。若是用描述符描述方法时用“()”表示方法,方面有什么参数,就填入具体的参数类型,返回类型紧跟括号后面,如描述方法“viod main(String[] args)”的描述符为“([Ljava/lang/String;)V”,又或者描述方法“int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex)”的描述符为“([CII[CIII)I”,不难看出,括号表明方法,第一个参数为char数组类型则为[C,第二和第三个参数为int类型则为II,第四个又是char数组则未[C,最后三个都是int类则未III。有一个特别须要注意的是,基本类型和对象类型的描述确保,基本类型是一个大写字母,而对象类型则须要“;”结束防止多个全限定名之间产生混淆,再如void test(String a,int b)的方法描述符为(Ljava/lang/String;I)V。
类文件结构之:方法表集合
若是理解了上一节“字段表集合”学习内容的话,那么这一节的方法表集合很是好理解了,由于表元素都是同样的,以下:
类型 |
名称 |
数量 |
u2 |
access_flags |
1 |
u2 |
name_index |
1 |
u2 |
descriptor_index |
1 |
u2 |
attribute_count |
1 |
attribute_info |
attributes |
attribute_count |
方法表结构
至于字段的访问标志和方法的访问标志多少仍是有些出入的,以下所示:
标志名称 |
标志值 |
含义 |
ACC_PUBLIC |
0X0001 |
方法是否public |
ACC_PRIVATE |
0X0002 |
方法是否private |
ACC_PROTECTED |
0X0004 |
方法是否protected |
ACC_STATIC |
0X0008 |
方法是否static |
ACC_FINAL |
0X0010 |
方法是否final |
ACC_SYNCHRONIZED |
0X0020 |
方法是否synchronized |
ACC_BRIDGE |
0X0040 |
方法是否由编译器产生的桥接方法 |
ACC_VARARGS |
0X0080 |
方法是否接受不定参数 |
ACC_NATIVE |
0X0100 |
方法是否为native |
ACC_ABSTRACT |
0X0400 |
方法是否为abstract |
ACC_STRICTFP |
0X0800 |
方法是否为strictfp |
ACC_SYNTHETIC |
0X1000 |
防范是否由编译器自动产生 |
方法访问标志
经过方法表结构结合TestClass类文件继续追中该字节码的奥秘,请看一下方法表结构的字节码:
方法表结构入口在以上的0X0002值上,第一个u2类型的值表明方法的数量,经过字节码看到TestClass类有两个方法。这里有个疑问,我记得写TestClass的时候只有一个inc方法啊,如今怎么会出现两个呢,继续观察。看看第一个方法的访问标志为0X0001,参考访问标志标识表就知道这是一个public修饰的方法,方法名索引指向了常量池的第0X0007个常量,结合常量池就知道方法名为“<init>”,描述符的索引值为0X0008,也就是“()V”,attributeCount值为0X0001,代表这个方法有一个属性,这个属性须要特别提到的就是,方法体的代码都是放在属性上的,因此这个属性几时init方法的实现代码。稍后一节再详细谈论属性表结构。这个init方法是编译器自动添加的,是类文件构造函数的入口。因此也就明白为何会有两个方法了。
类文件结构之:属性表集合
终于来到了类文件表层结构的最后一个环节“属性表结构”了,在前面的讲解中已经出现屡次,在Class文件、字段表、方法表均可以携带本身的属性表集合。可是,这个属性表结构的丰富程度比以上的表结构大得多,下面一一学习吧。
与Class文件中其余的数据项目要求严格的顺序、长度和内容不一样,属性表集合的限制稍微宽松了一些,再也不要求各个属性表具备严格顺序,而且只要不与已有属性名重复,任何人实现的编译器均可以想属性表中写入本身定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。也就是说,不一样的属性都各类有本身一套结构规则,例如上文说到的Code属性。最新的《Java虚拟机规范(Java SE 7)》版中,属性项已经增长到21项了。因此,属性结构已经有21种,若是须要学习,能够阅读本书的6.3.7章节。本学习文字只对部分属性进行学习和理解。
属性名称 |
使用位置 |
含义 |
Code |
方法表 |
Java代码编译成的字节码指令 |
ConstantValue |
字段表 |
final关键字定义的常量值 |
Deprecated |
类、方法表、字段表 |
被声明为deprecated的方法和字段 |
Exceptions |
方法表 |
方法抛出的异常 |
EnclosingMethod |
类文件 |
仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClasses |
类文件 |
内部类列表 |
LineNumberTable |
Code属性 |
Java源码的行号与字节码指令的对应关系 |
LocalVariableTable |
Code属性 |
方法的局部变量描述 |
StackMapTable |
Code属性 |
JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操做数栈所须要的类型是否匹配 |
Signature |
类、方法表、字段表 |
JDK1.5中新增的属性,这个属性用于支持泛型状况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名若是包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。因为Java的泛型采用擦除法实现,在为了不类型信息被擦除后致使签名混乱,须要这个属性记录泛型中的相关信息 |
SourceFile |
类文件 |
记录源文件名称 |
SourceDebugExtension |
类文件 |
JDK1.6中新增的属性,SourceDebugExtension属性用于存储额外的调试信息。譬如在进行JSP文件调试时,没法经过Java堆栈来定位JSP文件的行号,JSR-45规范为这些非Java语言编写,却须要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension属性就能够用于存储这个标准所新加入的调试信息 |
Synthetic |
类、方法表、字段表 |
标识方法或字段为编译器自动生成的 |
LocalVariableTypeTable |
类 |
JDK1.5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法以后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations |
类、方法表、字段表 |
JDK1.5新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations属性用于注明哪些注解是运行时(实际上运行时就是进行反射调用)可见的 |
RuntimeInvisibleAnnotations |
类、方法表、字段表 |
JDK1.5新增的属性,与RuntimeVisibleAnnotations属性做用恰好相反,用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotations |
方法表 |
JDK1.5新增的属性,做用与RuntimeVisibleAnnotations属性相似,只不过做用对象为方法参数 |
RuntimeInvisibleParameterAnnotations |
方法表 |
JDK1.5新增的属性,做用与RuntimeInvisibleAnnotations属性相似,只不过做用对象为方法参数 |
AnnotationDefault |
方法表 |
JDK1.5新增的属性,用于记录注解类元素的默认值 |
BootstrapMethods |
类文件 |
JDK1.7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符 |
虚拟机规范定义的属性
以上为虚拟机规范(1.7以前)定义的属性,对于每一个属性,它的名称须要从常量池引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则彻底自定义的,只须要经过一个u4的长度属性去说明属性值所占用的位数便可。一个符号规则的属性表应该知足如下定义结构:
类型 |
名称 |
数量 |
u2 |
attribute_name_index |
1 |
u4 |
attribute_length |
1 |
u1 |
info |
attribute_length |
属性表结构
属性表集合之:Code属性
Java程序方法体中的代码通过Javac编译处理后,最终变为字节码指令存储在Code属性中,但并不是全部方法表都有Code属性,例如抽象类或接口。下面来看看Code属性表结构:
类型 |
名称 |
数量 |
u2 |
attribute_name_index |
1 |
u4 |
attribute_length |
1 |
u2 |
max_stack |
1 |
u2 |
max_locals |
1 |
u4 |
code_length |
1 |
u1 |
code |
code_length |
u2 |
exception_table_length |
1 |
exception_info |
exception_table |
exception_table_length |
u2 |
attribute_count |
|
attribute_info |
attributes |
attribute_count |
Code属性表结构
attribute_name_index是一项指向常量池的索引,上文也介绍过全部属性的的表结构必有attribute_name_index字段。而attribute_name_index所指向的utf8常量值表明这属性的类型(可结合参考虚拟机规范定义的属性)。例如Code属性表的attribute_name_inde指向常量池的utf8字符串就是“Code”。attribute_length标识属性的总长度,意味着这个属性一共有占多少字节。因为attribute_name_index和attribute_length一共占用了6个字节,那么属性的真实长度是整个属性表长度-6。max_stack表明了操做数栈深度的最大值。max_locals表明了局部变量所表示的存储空间(单位是Slot),一个Slot占用32个字节,若是是double或long这种64位的数据类型则须要两个Slot来存放。占用Slot的变量包括方法参数、局部变量、异常变量等,Javac编译器会根据变量的做用域来分配Slot,每一个Slot在整个线程周期能够重复使用,而后根据变量数和做用域计算出max_locals的大小。code_length和code是用来存储Java源程序编译后产生的字节码指令,code_length表明字节码长度,既然叫字节码,每一个指令确定占用一个字节长度,因此一个字节取值范围为0~255,那么字节码指令确定不会超过255个指令,事实上目前Java虚拟机规范定义了其中约200条编码值对应指令的含义,若是往后超过的话,扩展到双字节的时候,有可能更名为双字节码了,呵呵。由于code_length是一个u4类型,因此理论上每一个方法的字节长度不能超过2^23-1,可是虚拟机规范中明确限定了一个方法不能超过65535条字节码指令,即实际只用到了u2的长度。有一点须要注意的是,Java虚拟机的字节码有一个特殊情形,就是某些指令(字节码)后面会带有参数,因此全部code的字节码不必定全是指令,有多是指令后的参数,下面继续对TestClass的字节码进行分析:
以上标蓝的字节码就是第一个方法的Code属性表结构字节码,0X0009表明属性名指向常量池的值(attribute_name_index),也就是常量池的第9个常量“Code”,由于这是一个Code属性的属性表;而后的0X0000002F就是这个Code属性表的属性值的长度;0X0001说明这个堆栈的深度为1;0X0001说明该堆栈总共有一个Slot局部存储;0X00000005意味着该方法一共有5个字节的字节码长度。那具体这5个字节码分表表明什么指令或参数呢?继续参量Java虚拟机字节码指令表学习:
1)第一个字节码2A:对应指令为aload_0,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到栈顶。
2)第二个字节码B7:对应指令为invokespecial,这条指令的做用是以栈顶的reference类型的数据指向的对象做为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指想常量池中的一个CONSTANT_Methodref_info类型的常量,即此方法的方法符号引用。
3)读取invokespecial的u2类型参数:0X000A指向常量池对应的常量为实例构造函数“<init>”方法的符号引用。第10个常量池是一个Method类型的常量,以下所示:
const #10 = Method #3.#11; // java/lang/Object."<init>":()V const #11 = NameAndType #7:#8;// "<init>":()V
它一样是由其余常量组成的,组成的值为“java/lang/Object.”<init>”()V”,意思是调用Object对象的init方法,这个方法描述符是没法无返回类型“()V”。
4)读入B1,对应的指令为return,含义是返回此方法,而且返回值为void。这条指令执行后,当前方法结束。再来看一下这个方法的反编译出来的指令描述:
public org.fenixsoft.clazz.TestClass(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #10; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lorg/fenixsoft/clazz/TestClass;
从以上截图看到一个陌生的词语“Args_size”,意思很明显是参数个数,既然构造函数是无参的,为何还会有一个呢?并且默认的构造函数是没有局部变量的,又为何会有一个局部变量呢?再往下看有一个本地变量表(LocalVariableTable),肉眼能够看出里面存放了一个变量,一个类型为TestClass的this变量。有Java编程经验的人都知道,任何对象实例均可经过this获取对象的属性。实际上是Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,而后在虚拟机调用实例方法时自动传入此参数而已。所以在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量也会预留出第一个Solt位来存放对象实例引用,方法参数值从1开始计算。这个处理对实例方法有效,对静态方法就无效了。
另外,虽然在本文例子中exception_table_length为0,但仍是有必要介绍一下exception_info的表结构:
类型 |
名称 |
数量 |
u2 |
start_pc |
1 |
u2 |
end_pc |
1 |
u2 |
handle_pc |
1 |
u2 |
catch_type |
1 |
以上字段的意思是若是当字节码在第start_pc行到end_pc行之间(不含第end_pc行)出现了类型为catch_type或其子类异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当catch_type的值为0时,表明任意异常状况都须要转向到handler_pc处进行处理。在以上的TestClass例子中没有异常捕获,那么就我来重写一下inc方法添加try-catch-finally代码学习吧。
源码以下:
package org.fenixsoft.clazz; public class TestClass { public int inc() { int x; try{ x = 1; return x; }catch(Exception e){ x = 2; return x; }finally{ x = 3; } } }
字节码以下:
Code: Stack=1, Locals=5, Args_size=1 0: iconst_1 //常量值1进栈 1: istore_1 //将栈顶int型数值(1)出栈并存入第二个局部变量(Locals[1]=1) 2: iload_1 //将第二个int型局部变量(1)进栈 3: istore 4 //将栈顶int型数值(1)出栈存入第五个局部变量(Locals[4]=1) 5: iconst_3 //常量值3进栈(把3入栈) 6: istore_1 //将栈顶int型数值(3)存入第二个局部变量(Locals[1]=3) 7: iload 4 //将第五个int型局部变量(1)进栈 9: ireturn //返回方法的int元素(返回栈顶元素1) 10: astore_2 //把当前栈顶元素存入到第三个局部变量(Locals[2]=X) 11: iconst_2 //常量值2进栈 12: istore_1 //把栈顶int型数值(2)出栈并存到第二个局部变量(Locals[1]=2) 13: iload_1 //把第二个局部变量(2)入栈 14: istore 4 //把栈顶int型数值2出栈并存入第五个局部变量(Locals[4]=2) 16: iconst_3 //常量值3进栈(把3入栈) 17: istore_1 //将栈顶int型数值(3)存入第二个局部变量(Locals[1]=3) 18: iload 4 //将第五个int型局部变量(2)进栈 20: ireturn //返回方法的int元素(返回栈顶元素2) 21: astore_3 //把当前栈顶元素存入到第四个局部变量(Locals[3]=Y) 22: iconst_3 //常量值3进栈(把3入栈) 23: istore_1 //将栈顶int型数值(3)存入第二个局部变量(Locals[1]=3) 24: aload_3 //把第四个局部变量(Y)入栈 25: athrow //抛出异常 Exception table: from to target type 0 5 10 Class java/lang/Exception //第0到第5行若是抛出Exception异常则跳转到第10行 0 5 21 any //第0到第5行若是抛出任何异常则跳转到第21行 10 16 21 any //第10到第16行若是抛出任何异常则跳转到第21行
以上为改造后的TestClass的JVM指令码,结合Code和Exception table两个区域代码看,意思大概是0至5行若是发生Exception异常则进入21行,若是第0至5行发生任何异常(除刚才定义的Exception)则跳转至21行,若是第10至16行发生任何异常则跳转至21行。看出,21行开始都是finally的处理逻辑。但不是全部的finally处理逻辑都跳转到21行来,而是根据Exception table表的定义来跳转,若是没有异常,finally的逻辑已经定义在各自的指令区域,如5至6行,16至17行。因此字节码处理逻辑并不是好像源码逻辑那样经过跳转实现的,因此可能会存在字节码的处理逻辑跟源码的感受会天差地别的(若是编译器优化级别够较高的话)。
总结
学习到这基本上能够算是弄清楚了整个Class文件的表层结构各表的含义,剩下的都是些属性级别的学习(参考以上“虚拟机规范定义的属性”表),学习方式都是同样的,若是遇到了不懂的属性表,可经过书本自行查询并解析。若是学习过汇编指令的同窗,面对这些字节码指令都很是容易上手,就算没有学习过汇编指令也没关系,对照着字节码指令含义表一样也能够看出个大概,若是熟悉了指令(200+),对之后分析源码性能或者关键字特性等都很是容易上手。