上节学习回顾
在上一节当中,主要以自己的工作环境简单地介绍了一下自身的一些调优或者说是故障处理经验。所谓百变不离其宗,这个宗就是我们解决问题的思路了。
本节学习重点
在前面几章,我们宏观地了解了虚拟机的一些运行机制,那么从这一章节开始,我们将更加深入虚拟机的深处去了解其运行细节了。例如本章节的学习重点是类文件的结构,也就是虚拟机的数据入口。既然是数据入口,肯定得要符合虚拟机的数据定义规范才能给虚拟机处理,否则它压根就不认识你。
概述
在学习之前,先抛出一个比较常见的问题: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语言结构体的伪结构来存储数据,这种伪结构中占有两种数据类型:无符号数和表。无符号数就是基本的数据类型,以u1、u2、u4、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文件的次版本号(第5、6个字节)和主版本号(第7、8个字节)了。从十六进制规范看是0032.0000,转化为十进制后就是50.00。JDK时从1.0~1.1版本使用了45.0~45.3,从JDK1.1后每个大版本发布主版本号向上加1,也就是46.00表示JDK1.2、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+),对以后分析源码性能或者关键字特性等都非常容易上手。