.class字节码类型识别:魔数、常量池和内存模型

目录

.class文件

组成结构

魔数

常量池

oop-klass模型


      我们都知道C语言执行依赖编译器,同一段C程序,在不同的操作系统平台(或者说是硬件平台上)上,由不同的编译器将其编译成对应的机器指令,编译后的C程序里各种数据类型信息都被改写成汇编,这是在编译时期完成的,在编译器对C程序进行解析时,识别里面的各种数据类型,例如结构体,并翻译成机器可以理解的二进制程序,待操作系统加载执行。Java则不同,JIT即时编译器虽然也能在编译时识别出Java程序中的各种数据类型信息,但是还记得以前说过的,Java程序编译后形成的是字节码.class文件,而不是底层机器能直接运行的机器指令,.class字节码文件再由JVM在程序运行时解释执行,因此,真正处理起Java数据类型是在程序运行期间,一边解释字节码文件,一边识别出里面的数据类型。

 

.class文件

      Java程序中不论你想描述的任何东西,一些动物的属性,一台电脑的参数,还是一种算法的不同实现方式,最后都会被概括成一个“类”,可以说是Java最基本的数据结构,这些一个个“类”,在程序执行过程中JVM会加载这些Java类,将它们编译成.class字节码文件,文件里面是连续的二进制流,有大端和小端两种排序方式,如果数据大小超过了八个位,那么采用大端方式排列,将低字节位的数据存放到高地址处,高字节位的数据存放到低地址处,小端排序方式则相反,这是数据的排列方式。数据的组成部分有十个,组成方式类似于结构体。

组成结构

.class字节码文件的数据结构表示用伪码描述如下:

lassFile {
	u4 magic;
	u2 minor_version;
	u2 major_version;
	u2 constant_pool_count;
	cp_info constant_pool[constant_pool_count-1];
	u2 access_flags;
	u2 this_class;
	u2 super_class;
	u2 interfaces_count;
	u2 interfaces[interfaces_count];
	u2 fields_count;
	field_info fields[fields_count];
	u2 methods_count;
	method_info methods[methods_count];
	u2 attributes_count;
	attribute_info attributes[attributes_count];
}

      可以看到有很多u2,u4数据类型,它们表示无符号数,有u1、u2、u4、u8四种,分别表示1、2、4、8字节的无符号数。从上往下看,第一个字段magic,表示的是MagicNumber魔数,魔数的作用是用来标识该文件是一个.class字节码文件,是一个固定值十六进制0xCAFEBABE,占四个字节,位于每一个.class文件的开头。如果JVM在加载Java类时,发现某一个字节码文件的魔数不是0xCAFEBABE,那么JVM就会停止加载该文件。

      第二第三个字段都是表示version版本号,major_version和minor_version分别表示.class字节码文件的主版本号和次版本号,对于高版本的JVM可以向下兼容低版本的.class文件,但低版本的JVM无法向上兼容,举个例子,如果你用高版本的JDK来编译Java程序得到的字节码文件,然后用较低版本的JRE(Java Rountime Enviroment运行环境)来执行.class文件,是不行的,JVM会报错,相反,用较高版本的JRE来执行低版本JDK编译出来的字节码文件是可以的。

      第四个字段,constant_pool_count,常量池长度,标识的是常量池中有多少个常量,常量池中的常量有final声明的,如整型、浮点型和字符串型的常量,还有符号引用常量,符号引用就是一些类和接口的名称,和方法的名称,它们以字符串的形式保存在常量池中。

      继续往下走,access_flags类的表示访问权限;this_class是类在常量池中的索引;super_class顾名思义是类的父类在常量池中的索引,interface_count标识当前类实现的接口数量,下面的数组就是接口列表。

      下一个字段,fileds_count和field_info数组,这个比较重要,标识的是类中的变量总和,例如类变量和实例变量,fileds_count是数量,field_info数组就是详细的变量列表。methods_count和数组method_info数组与Field字段类似,它们保存的是类的方法数量和具体方法信息的列表。最后一个字段,attributes_count和attribute_info标识的就是具体的字节码信息了,非常多,下面我尽可能把自己曾经理解的总结一下(可能要几篇日志- -)。

 

魔数

      魔数,前面说了用来标识.class字节码文件的标志,是一个固定值0xCAFEBABE,位于.class文件最前面占四个字节,从上面文件结构中可以看到魔数的类型是u4,表示4字节的无符号数,它的最终类型如下:

#ifndef _UINT32_T
#define _UINT32_T
typedef unsigned int uint32_t;
#endif

typedef uint32_t juint;
typedef juint u4;

可以看到u4类型最终代表的是unsigned int,无符号整型,占4个字节,前面还说到有u1、u2和u8,它们分别对应的是:

u1 --> uint8_t --> unsigned char

u2 --> uint16_t --> unsigned short

u4 --> uint32_t --> unsigned int

u8 --> uint64_t --> unsigned long

这就是魔数的数据类型,魔数的重要性不可忽视,作为.class字节码文件的标志,JVM每次都会先检查魔数的值是否为0xCAFEBABE,如果不是此固定值,那么JVM会停止加载。

 

常量池

      常量池保存着Java类中的所有变量和方法,例如成员变量,成员方法,构造函数和静态变量/方法等,JVM用constantPoolOopDesc*类型来记录这个常量池的信息,由C++实现:

typedef class constantPoolOopDesc* constantPoolOop;

JVM中为每一个Java类都创建一个oop-kalss模型与之对应,来表示每一个类里的对象实例信息,例如每创建一个新的对象,JVM都会为其创建一个opp(ordinary object pointer普通对象指针)对象和klass对象。

oop-klass模型

      在JVM中,不光普通的Java类实例是对象,对于类中的方法,它也被当作一个对象,常量池也不例外,JVM为每一个对象都创建了opp-klass模型,例如上面的常量池是constantPoolOopDesc*。opp用来描述类的实例信息,例如实例对象在程序运行时的锁状态标志,持有的锁等,所以opp是在Java程序运行时通过new关键字创建实例对象后才生成,对应的是instanceOopDesc类,存放在堆区,对于它的引用存放在栈区。klass则是在JVM加载.class字节码文件时就被创建,因为它用来描述Java类信息,包括常量池还有类里面的方法等,对应的是instanceKlass类,存放在方法区。

      上面说opp是指针,ordinary object pointer一个普通对象指针,它指向的是klass类实例 — instanceKlass,instanceOopDesc描述类的实例对象,那么它们之间的关系其实就是:

类的实例 --> 类的instanceKlass --> Java

(即oop --> klass --> Class)

      所以,我们可以通过栈空间中的instanceOopDesc引用找到堆空间中的instanceOopDesc对象,该对象指向了方法区中的instanceKlass,来得到一个实例对象的类型。
.class字节码类型识别:魔数、常量池和内存模型_第1张图片

      具体的oop-klass模型就如上图所示,方法栈存放局部变量,也就是Java类的实例,因为JVM会为每一个实例创建oop对象,并存放在堆空间中,堆空间中的oop对象保存了该实例对象的三部分数据,对象头mark word保存的是对象运行时的信息,例如线程ID,获得的锁和时间戳等;元数据指针指向方法区的instanceKlass实例,最后实例对象数据就是对象中具体的的成员变量的数据,例如你执行:

Animal a = new Animal( “lion”, “9” );

      属性”lion”和”9”就是实例对象a中的具体数据。最后元数据指针指向方法区中的instanceKlass,它保存了Java类的信息,类方法和类变量等,假如一个类实例化了多个对象,这些对象调用类中相同的方法时,访问的都是同一个方法区里的方法。

你可能感兴趣的:(JVM)