Java在诞生时就以一次编写,到处运行特点在各个平台都可以进行运行。其实就是通过不同的编译器(Javac编译器,jrubyc编译器,groovyc编译器等等)将代码编译成规范的class文件,虚拟机只要接收到claas文件而并不关心是class文件时哪一种编译器编译的,这样就到达了(write one,run anywhere)。所以要想更好的了解虚拟机,下面我们走进class文件中!!
前言:
Class类文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分隔成若干8位字节进行存储。
Class文件采用一种类似C语言结构体的伪结构来存储数据。
伪结构中有两种数据类型:无符号数和表。
无符号数:以u1,u2,u3,u4来分别代表1,2,3,4个字节。它可以用来描述数字、索引引用、数量值或者按照utf-8编码构成字符串值。
表:由多个无符号数或者其他表作为数据项构成的符合数据结构。它可以用来描述有层次关系的符合结构的数据,整个Class文件本质上就是一张表。
一、魔数与Class文件的版本
Class文件开头的4个字节就是魔数,它的作用就是确定这个文件是个能被虚拟机接受的Class文件。它的值为:0xCAFEBABE。
紧接着魔数后面的4个字节就是Class文件的版本号,第5,6个字节是次版本号(Minor Version),第7,8个字节是主版本号(Major Version)。java版本号是从45开始的,高版本的jdk能向下兼容之前的版本,但是之前的版本不能向上兼容。
代表次版本号的第五个和第六个字节的值为0x0000,而主版本号的值为0x0034,也就是十进制的52,说明这个文件被JDK1.7或以上版本虚拟机执行的class文件
二、常量池
接着版本号之后的就是常量池了,它是Class文件中占用空间最大的数据项目之一。
由于常量数量的不确定,所以在入口会有一个u2类型的容量计数值,这个计数值是从1而不是从0开始的。设计者将0空出来是在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,对于其他集合类型,都是从0开始计数的。
常量池中的两大类常量:字面量和符号引用。
字面量:接近java语言层面的常量概念,如字符串,声明为final类型的常量等。
符号引用:属于编译原理方面的概念,如类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。
当虚拟机运行时,需要从常量池获得对应的符号引用(会有索引指向常量池中),再在类创建时或运行时解析、翻译到具体的内存地址之中。
jdk中共有14中结构各不相同的表结构数据。可以通过oracle提供的工具javap来分析Class文件字节码。javap -verbose ColorPrinter.class
使用示例:
常量池中项目类型
三、访问标志
常量池结束后,有两个字节代表访问标志,用于识别一些类或者接口层次的访问信息。比如这个Class是类还是接口,是public还是其他访问权限,是否是abstract,如果是个类,是否被声明为final等等。
1.我们知道,每个定义的类或者接口都会生成class文件(这里也包括内部类,在某个类中定义的静态内部类也会单独生成一个class文件)。
对于定义的类,JVM在将其编译成class文件时,会将class文件的访问标志的第11位设置为1。第11位叫做ACC_SUPER标志位;
2.对于定义的接口,JVM在将其编译成class文件时,会将class文件的访问标志的第8位 设置为 1 。第8位叫做ACC_INTERFACE标志位;
3.class文件表示的类或者接口的访问权限有public类型的和包package类型的。
如果类或者接口被声明为public类型的,那么,JVM将其编译成class文件时,会将class文件的访问标志的第16位设置为1。第16位叫做ACC_PUBLIC标志符;
4.类是否为抽象类型的,即我们定义的类有没有被abstract关键字修饰,即我们定义的类是否为抽象类。
定义某个抽象类时,JVM将它编译成class文件的时候,会将class文件的访问标志的第7位设置为1。第7位叫做ACC_ABSTRACT标志位。
5.另外值得注意的是,对于定义的接口,JVM在编译接口的时候也会对class文件的访问标志上的ACC_ABSTRACT标志位设置为 1;
6.该类是否被声明了final类型,即表示该类不能被继承。
此时JVM会在编译class文件的过程中,如果类被声明为final类型,会将class文件的访问标志的第12位设置为 1 。第12位叫做ACC_FINAL标志位;
7.如果我们这个class文件不是JVM通过java源代码文件编译而成的,而是用户自己通过class文件的组织规则生成的,那么,一般会对class文件的访问标志第4位设置为1 。通过JVM编译源代码产生的class文件此标志位为 0,第4位叫做ACC_SYNTHETIC标志位;
8. 枚举类,对于定义的枚举类如:public enum
EnumTest{…},JVM也会对此枚举类编译成class文件,这时,对于这样的class文件,JVM会对访问标志第2位设置为1 ,以表示它是枚举类。第2位叫做ACC_ENUM标志位;
9.注解类,对于定义的注解类如:public
@interface{…},JVM会对此注解类编译成class文件,对于这样的class文件,JVM会将访问标志第3位设置为1,以表示这是个注解类,第3位叫做ACC_ANNOTATION标志位!
当JVM确定了上述标志位的值后,就可以确定访问标志(access_flags)的值了。实际上JVM上述标志会根据上述确定的标志位的值,对这些标志位的值取或,便得到了访问标志(access_flags)。如下图所示:
例如上图中的示例,是个类,所以标志位如下:
四、类索引、父类索引和接口索引集合
1、类索引(this_class)
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口(interfaces)是一组u2类型给的数据的集合,class文件中由这三项来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于Java语言不允许多重继承,所以父类索引只有一个,除了Java.lang.Object之外,所有Java类都有父类,因此除了java.lang.Object外,所有java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口。
这里类索引值是0x0002,也就是指向常量池中的第二项,通过查找常量池可知这个类的全限定名是java_test/TestClass
2、父类索引(super_class)
因为java不允许多继承,所以父类索引只有一个,除了java.lang.Object外,所有的java类都有且只有一个父类。在类索引后的两个字节就是父类索引,同上可查找到父类索引为0x0003,对应于常量池中的第三项:
3、接口索引集合(interfaces)
由于类实现的接口数目不确定,所以接口索引集合的描述的前部分叫做接口计数器(interfaces_count),接口计数器占用两个字节,其中的值表示着这个类实现了多少个接口,紧跟着接口计数器的部分就是接口索引部分了,每一个接口索引占有两个字节,接口计数器的值代表着后面跟着的接口索引的个数。接口索引和类索引和父类索引一样,其内的值存储的是指向了常量池中的常量池项的索引,表示着这个接口的完全限定名。
因为我写的那个类没有实现任何接口,所以接口索引值为0.