系统学习jvm请点击jvm目录

类文件存在的意义

Class类文件,可以叫它字节码文件,class文件,类文件其实都行。

java代码无法直接在操作系统上运行,所以必须先转换成jvm能够运行的语言,然后再由jvm转换成操作系统能够运行的语言。
当我们写完代码之后,经过编译器编译,就会生成一个字节码文件。之后jvm来运行字节码文件中的字节码指令,将字节码指令转换成机器码指令来执行。这就是整个java代码从编写到编译再到运行的过程。
为了实现java的跨平台的特点,设计了jvm,针对不同的OS,开发不同的jvm。而用户编写的代码都可以在所有的jvm上运行,那么jvm运行的指令就必须遵从一定的规范,从而字节码文件诞生。
以统一的规范要求编译器将代码编译成所有jvm都能运行的字节码文件。从而对于java来说,就实现了一次编写,多处运行的特性。

打这样一个比方,jvm是一个抽象方法,那么类文件就是该方法的输入。输入是统一规定的,不能改变,而实现抽象方法的细节是可以改变的。

Class类文件结构总览

Class文件主要包含以下:

  • 魔数 版本
  • 常量池
  • 访问标志
  • 类索引 父类索引 接口索引
  • 字段表集合
  • 方法表集合

类文件是一组以8个字节为单位的二进制流,每个数据项目之间是没有分隔符的,那类文件又是怎么把上面的部分区分开的呢?
是这样实现的:
首先规定一开始的几个字节是魔数,然后仅接着几个字节是什么。之后呢就到常量池,由于不同类的常量池大小不一样,所以在常量池的开头设置两个字节,来表示常量池有多大。这样jvm就能推算出,到什么位置常量池就结束了。
如此,便实现了将类文件的各个部分区分开。
不过,这样也带来了问题,那就是,你必须要一口气读完类文件才能搞清类文件中每个部分是干什么的,因为上文的阅读会对下文的阅读产生巨大的影响。
也正是如此,想象一下为什么要有类加载这个步骤呢?因为类文件无法随记读取。(部分原因)

在类文件中,要注意,常量池,接口索引,字段表集合,方法表集合的长度都是可变长的,其余部分的长度是定长的。
然后,常量池,可以说是整个类文件的核心了,它也是占用了类文件的大部分空间。其他的部分基本都是围绕这常量池来的。为什么这样说呢,看下面的详细介绍就知道了。

类Class文件详细分析

这里我写了一个简单的类文件来对照着讲解。

public class Test{
	private String name;
	
	public String getName(){
		return this.name;
	}
}

经过编译之后,得到类class文件,以二进制格式打开它。

java 字节码写文件 什么是java字节码文件_class


我们从头开始分析。


首先是魔数(magic number),它的唯一作用是来确定该文件是不是类class文件。可以看到前四个字节为ca fe ba be(咖啡宝贝cafe babe),这就是类class文件的标志。

java 字节码写文件 什么是java字节码文件_jvm_02


紧跟着魔数后面的便是jdk的版本信息,它占4个字节:00 00 00 35。其中前面两个字节代表词版本号,后面两个字节是主版本号,我这里是0x35,十进制下就是53,对应的版本是jdk9.0。

java 字节码写文件 什么是java字节码文件_class_03


可以看到,对于所有的class文件来说,前面8个字节都是固定的。
而从第9个字节开始,每个部分的大小就不一定了。
紧跟着版本信息后面的是常量池。
常量池是用来存放两大类型的常量:字面量,符号引用。
字面量基本就是字符串,还有一些被final的常量。
而符号引用呢,我不确定之前讲没讲过,讲一下吧。符号引用对应的另一个概念是直接引用。因为编译之后的class文件的信息并没有进入内存,类A引用类B时,因为B并没有进内存,所以A没有通过内存地址办法定位到B。于是便需要用符号来定位B在哪里。这便是符号引用。

常量池中包含这些项目类型:

java 字节码写文件 什么是java字节码文件_常量池_04


下面我们接着便来分析之前写的代码的字节码文件。首先一上来的两个字节是常量池的容量,表示常量池中有多少项。

java 字节码写文件 什么是java字节码文件_jvm_05


可以看到这里是0x13,其十进制就是19,说明有十八项常量。下面来读取第一项常量,每个常量类型都会拿出最前面的一个字节来表示自己的类型,从而根据类型来确定自己的大小。这里的0a便是指第10个项目类型constant_methodref_info,其中00 04指的是声明方法的类描述符的索引项为04,也就是该常量池中第四项目。

java 字节码写文件 什么是java字节码文件_jvm_06


这里我们可以看到常量池中的一些项目指向了常量池中的另一些项目。

于是我们便去寻找第四个项目(先找第二个,第三个,然后就是第四个),找到了:

java 字节码写文件 什么是java字节码文件_常量池_07


这是第四个项目constant_class_info,它的tag是07,可以看到这里它的全限定名是0x12,也就是常量池中第18个项目,发现第十八个项目里存的就是java/lang/Object,如图所示:

java 字节码写文件 什么是java字节码文件_java_08


如此进行。

这里就不继续往下了,基本通过上面的例子,也是基本知道了字节码文件中常量池是如何如何。下面,使用javap工具来更清楚的理解。

java 字节码写文件 什么是java字节码文件_java 字节码写文件_09


可以看到第一项指向了4,4指向了18。


在常量池之后,便是整个类的访问标志了,用于标识这个class是接口还是类?是public还是啥?有没有被final等。

java 字节码写文件 什么是java字节码文件_java_10


这里,看我们的字节码文件,为00 21

java 字节码写文件 什么是java字节码文件_java 字节码写文件_11


所以判断是public,没有继承除Object的其他类。


接下来是类索引和父类索引,这俩都是两个字节,而接口索引集合是一组两字节的数据的集合。

java 字节码写文件 什么是java字节码文件_class_12


这里类索引是00 03,这是指向常量池中第三个项目,根据上面javap的结果,可以看到常量池中第三个项目又指向第17个项目,第17个项目就是字符串Test,我们的类名。

而父类索引是00 04,我们还记得,之前的例子中不就有常量池第4个项目么,它指向第18个项目:Object,本类的父类是Object。

后面两个00 00,就是说没有实现接口。


之后便是字段表集合。

这一部分是用来描述该类中声明的变量。

java 字节码写文件 什么是java字节码文件_jvm_13


java 字节码写文件 什么是java字节码文件_java 字节码写文件_14


java 字节码写文件 什么是java字节码文件_class_15


如图所示,首先,前两个字节是字段表集合的大小,00 01表示只有一个变量。

紧接着两个字节是字段访问标志。00 02 代表private。

之后两个字节是变量名的索引,它指向常量池中第5个项目,查看一下上面讲的常量池,第5项是字符串name。

之后两个字节是变量的描述符,同样,它指向常量池中第6个项目,其中也是字符串Ljava/lang/string,代表其类型为String。

最后两个00 00是来存储额外信息的。

所以根据以上信息可以知道:

private String name;

接下来是方法表集合。

剩下的都是方法表集合了,基本和字段表集合差不多。所以这里暂时就不写了。一样的分析。

只是在attribute_info中,会有各种属性,其中code属性非常的重要。

java 字节码写文件 什么是java字节码文件_java 字节码写文件_16


其中attribute_name_index是属性名称,length代表属性值的长度(不考虑name_index和length),max_stack为局部变量的存储空间(变量槽,一般变量占一个,double和long占两个)。code就是方法中的代码转换成的字节码指令。

总结

整个字节码是一个二进制流,某些部分是定长的,而某些部分采用长度(可变长)+内容的形式。所以需要从头到尾一直读下去才能明白,不能随机读取。
同时,常量池是非常重要的部分,其中存放着字符串,常量和符号引用,相当于字节码的数据库,而其他部分,基本都是存入一个数据库的索引。