一、Class类文件分析
1、Class文件中的数据类型
无符号数:无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节;无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成的字符串值;
表:表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“info”结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表。
2、Class文件结构
Class 文件是一组以(8位bit的)byte 字节为基础单位的二进制流。
如下图所示 Class 文件的字节码示意图:
![]()
上图中被绿色框圈起来的则为标准的 Class 文件的样子。左侧为软件本身提供的辅助信息,记录当前行前面总共有多少个 byte (或者说多少个 u1 ),用于快速定位数据(通过数据偏移量的方式。右侧为直接以编辑器打开 Class 文件的样子,显示为乱码。
3、Class文件内容
魔数:每个 Class 文件的头 4 个字节(u4)称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。所有 Class 文件,魔数均为 0xCAFEBABE。
次版本号和主版本号:次版本号与主版本号共同标识了我们所使用的的 JDK 版本,如 JDK 1.8.0 版本的次版本号为 u2 大小,用字节码表示为 00 00,主版本号也是 u2 大小,用字节码表示为 00 34。JDK 版本的小版本号为次版本号;JDK 版本的大版本号为主版本号。
常量池计数器:记录常量池中的常量的数量。由于常量池中的常数的数量是不固定的,所以在常量池的入口放置了一个 u2 类型的数据,来代表常量池容器记数值(constant_pool_count)。常量池计数器也是无符号数类型数据。
常量池:Class 文件中的资源仓库,它是 Class 文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最多的数据项目之一,同时它还是 Class 文件中第一个出现的表类型数据项目。
访问标志(access_flags):在常量池结束之后,紧接着的 2 个字节代表访问标志(access_flags),访问标志用于识别一些类或接口层次的访问信息。
类索引:确定当前类的全限定名。类索引(this_class)和父类索引(super_class)都是一个 u2 大小的数据。
父类索引:确定当前类的父类的全限定名。
接口索引计数器:代表了接口索引集合中接口的数量;
接口索引集合:按照当前类 implements(或当前接口extends)的接口的顺序,从左到右依次排列在接口索引集合中,此部分集合称为接口索引集合。
字段表计数器:记录字段表中字段的数量,为无符号数类型。
字段表:字段表(fields)用于描述接口或者类中声明的变量。字段(field)包括类级变量(即静态变量)以及实例变量(即:非静态变量),但不包括在方法内部声明的局部变量。字段表为表类型结构。
方法表计数器:记录方法表中字段的数量,为无符号数类型。
方法表:存储了当前类或者当前接口中的 public 方法,protected 方法,default 方法,private 方法等。方法表为表结构类型。
属性表计数器:记录属性表中属性的数量,为无符号数类型。
属性表:属性表(attributes)与 Class 文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不能识别的属性。
二、在jvm中类加载过程
我们在JVM 总体架构的讲解过程中,提到过类加载子系统的工作流程分为三步:加载->链接->初始化。如下图所示:
1、加载阶段
1.1、类加载器:
启动(Bootstrap)类加载器:启动(Bootstrap)类加载器也称为引导类加载器,该加载器是用本地代码实现的类加载器,它所加载的类库绝大多数都是出自 %JAVA_HOME%/lib 下面的核心类库,当然还有其他少部分所需类库。
扩展(Extension)类加载器:扩展类加载器是由 Sun 公司提供的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将 %JAVA_HOME%/lib/ext 或者少数由系统变量 -Djava.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
系统(System Application)类加载器:系统类加载器是由 Sun 公司提供的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将 用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径)下的类库加载到内存中。开发者可以直接使用系统类加载器。(负责记载classpath中指定的jar包及目录中class).
自定义类加载器(Custom ClassLoader):属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader
1.2、类加载机制(双亲委派机制):
双亲委派机制原理:
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。
双亲委派机制的优势:
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。示意图(向上委托,向下委派):
2、链接阶段
2.1、链接阶段
我们将链接(Linking)这一步,再进行下细致的模块划分,如下图所示:
链接-验证(verify):
定义:验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的自身安全。
主要验证的信息:
元数据验证:
字节码验证:字节码验证主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会产生危害虚拟机安全的事件,例如:
符号引用验证:符号引用验证可以看作是类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验以下内容:
链接-准备(prepare):
定义:准备阶段是正式为类变量分配内存并设置类变量默认值(通常情况下是数据类型的零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中。
不同类型的类变量的默认值是不同的,如int类型默认值为0、long类型默认为0L等。
链接-解析(resolve):
定义:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标一定是已经存在于内存中。
解析过程具体的解析内容:解析过程中,主要对如下4种类型的数据进行验证:
- 类或接口的解析;
- 字段解析;
- 类方法解析;
- 接口方法解析。
3、初始化阶段
定义:进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。
类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。
实例的初始化顺序:在进行初始化时,实例变量的初始化顺序如下图所示: