阅读《深入理解java虚拟机》第六章心得。

1.概述

这里要说的是我们通过文本编辑器写好一个“a.java”文件以后,是如何被虚拟机编译为字节码的,以及字节码的格式。不过先说一下关于java特性的问题。一般的语言,都是直接编译为本地机器语言的,所以一段代码为了运行在不同的环境下可能需要编译为不同的格式才能运行,最终的二进制格式必须是平台识别的。java的理念是“写出来的代码可以到处运行”,指的是代码只需要编译一次就可以运行在各个平台上。原理是java虚拟机编译源码为一种中间格式即java字节码,这个中间格式是与平台无关的,然后在各个平台上安装一个相应平台的虚拟机,这个虚拟机再负责把中间格式的字节码编译为相应平台的机器码。java的执行文件是一个字节码级别的war包或者jar包,而没有到机器码级别。编译和执行涉及到了虚拟机的两个核心组件,编译工具和执行引擎。

编译:源程序 -》字节码

执行:字节码 -》机器码, 运行

这里会探究编译过程,总体来讲,就是把我们写的源程序转成一个二进制的.class文件。这个class文件符合一定的规则,可以让虚拟机得到类的具体信息。

2.class文件结构

class文件是纯二进制的,其格式是事先定义好的,所以当虚拟机拿到一个class文件时,它就会按照已经定义好的格式来解析。这一点与xml文件还是有一定区别,class文件的格式相比xml更加严格,xml可以自定义格式,只要拿标签指出类型即可,解析的时候就用类型匹配,但是class文件格式则不然,它是一种的严格的定义,每一位是什么数据就是什么,不能改变顺序,正是因为如此严格的规定,class文件不需要任何的分隔符,结构可谓相当紧凑。

在class文件内部,只有两种类型的数据,无符号数和表。

无符号数可以用u1,u2,u4,u8来表示具体的类型,分别代表了1,2,4,8个字节的长度。

表是一种复合类型,内部可以含有若干个无符号数或者表,但注意这里“若干”并不是集合类型,而是类似c语言的结构体概念。class文件中没有集合的类型,如果要表示这里可能会包含多个A类型的数据,class的实现方式是提前放一个变量来指明后续的数据的个数。

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_flag

1

u2

this_class

1

u2

super_class

1

u2

interfaces_count

1

u2

interfaces

interfaces_count

u2

fields_counts

1

fields_info

fields

fields_counts

u2

methods_count

1

methods_info

methods

methods_count

u2

attributes_count

1

attributes_info

attributes

attributes_count

之后的部分将针对这张表结构来展开。

注意在class文件里并没有这些所谓的类型,u2,u4,xxx_info等,class文件只是一个二进制流,以上的类型或者表结构存在于虚拟机规范,或者说存在于编写虚拟机的程序员的大脑中,他们会按照这个规定来生成一个二进制class文件,也会按照这个格式来解析二进制class文件。所以说class文件格式是十分严格的,必须按照规范来。

虚拟机拿到一个二进制流,就会按照上面的表类解析,先取出前四个字节,翻译为一个magic数,然后再拿出后续的两个字节,是版本号,这样一直解析下去。。。

3.魔数和版本号

文件的类型有两种方式体现,一种是通过扩展名,这种不严谨,因为别人可以随时修改。另一种就是魔数,即文件的前几个字节,通过设定特定的魔数来表明文件的格式。java把class文件的魔数定义为“0xCAFEBABE”。

接着是版本号,虚拟机只能运行小于等于当前版本的class文件,向前兼容。

4.常量池

这是class文件中占用空间最大的地方之一。常量池中会存放两种类型的常量:字面量和引用量。字面量就是int,boolean,double,string等这些基本的值。引用量本质上一个指向字面量的索引,主要对应类里面的:

类或者接口的全名;

字段名和修饰符;

方法名和修饰符以及参数列表。

以上三类内容是描述类的属性和方法的,对应的内容本质上是字符串,所以我们预先把所有的字面量存好,然后让后面具体的属性存指向字面量的索引即可,这样首先是节省了空间。比如有10个字段都是String类型,我们只需要在字面量里面存一个“String”字符串,然后剩下的10个字段的类型值就是一个指向“String”字面量的索引,而不需要为每一个字段都存一个“String”的字面量。

从class结构表可以看到,constant_pool之前有一个count字段,表明有几个常量,然后后面才是真正的常量。每一种常量都有自己的类型,比如字面量存的是具体的值,而引用量存的是索引,所以需要总结出每一种常量的类型或者说结构,最后再赋予每一种类型一个标识tag,这样虚拟机就可以根据这个tag解读这个常量。

常量池共有一下14中类型,以下是最基本的11种:

【Java】类文件结构 编译_二进制

其中的1-6是字面量,比如类名,方法名,类型等字符串都存储在这里。后面的是与类定义相关的,每一种类型都包含了一个指向前6中字面量的索引。

CONSTANT_Class_info结构:

【Java】类文件结构 编译_java编译_02

CONSTANT_NameAndType_info结构:

【Java】类文件结构 编译_虚拟机_03


CONSTANT_Fieldref_info结构:

【Java】类文件结构 编译_java编译_04

CONSTANT_Methodref_info结构:

与上面的一样。所以,常量池的逻辑是,首先在字面量存上名称或者描述符的字面量,比如“I”,“getData”,这里“I”代表Int。然后在符号常量里是与类定义相关的,比如有一个属性为“private int data;”的属性,那么常量池的字面量会有“I”,“data”。符号量会有一个“CONSTANT_NameAndType_info”的数据,第一个索引指向了“data”,第二个索引指向了“I”。还会有一个“CONSTANT_NameAndType_info”的数据,第一个索引指向了Class_info即全限定类名,第二个索引指向了刚才的NameAndType。那有人会问private呢?这个是一个修饰符,是固定的,索引不需要为其准备一个“private”的常量,修饰符是在后面的字段表里定义的,是一个布尔值,常量池只是定义了出现在方法和字段中的字符串。

5.访问标志

指的是类的访问标志。包括是class还是interface,是否为public,是否为abstract,是否final等。每一种都有一个标记值。

6.类索引,父类索引和接口索引

类索引只有一个指向了“CONSTANT_Class_info”,如果有父类,那么父类索引也只指向一个“CONSTANT_Class_info”,否则指向0。

接口索引可以有多个,所以接口索引前面会有一个count。

7.字段表集合

这里只包括类实例变量和类变量,而不包括局部变量。

因为字段可以有多个,所以前置一个count。

一个字段表结构如下:

【Java】类文件结构 编译_class文件_05

accessflag包括是否static,作用域(public,private,默认)是否final,是否vatalite等。

name的index和descriptor的index都指向了常量池的常量。最后的attributes对应的是属性表,这个后面再讲,是一些附加信息。

8.方法表集合

一个方法表结构和属性表基本相同。有一点需要搞清楚,目前讨论的是如何把一个类的定义包括方法属性等写入一个二进制文件。在方法表中,我们目前只是写入了每一个方法的接口信息,但是方法体呢?

这个就是attributes的作用。方法的具体内容被编译为二进制的java指令,存到了方法表的code属性表里。一个方法的描述符后面通常会附加属性表,所以count是大于等于1的,后面跟着类型为“Code”的属性表。

9.属性表集合

属性表跟在每一个字段或者方法的后面。携带了一些附加信息。每一种属性表携带一类型的信息,共有以下几种常见的属性表类型:

【Java】类文件结构 编译_java编译_06

Code属性表:

【Java】类文件结构 编译_二进制_07