在介绍jvm的类加载机制之前补充一些知识。
java虚拟机是一个通用的、机器无关的执行平台,是为了实现程序的”无关性“而设计的,这里的无关性包括平台无关性 + 语言无关性。各种不同平台的虚拟机都使用统一的程序存储格式--字节码,这是构成无关性的基石。Java虚拟机不与任何语言绑定(包括java语言),只与"Class文件”这种特定的二进制文件格式关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。也就是说,java虚拟机不只能执行java程序,一种语言如果可以通过编译器把程序代码编译成class文件,那虚拟机就可以运行这个程序,java虚拟机并不关心Class的来源是何种语言。
任何一个Class文件都对应唯一一个类或接口的定义信息,但是类或接口并不一定都定义在文件里(譬如类或接口可以通过类加载器直接生成),实际上这里提到的“Class文件”并不一定以磁盘文件的形式存在。
Class文件中不保存类定义中的各个方法、字段的最终内存布局信息,虚拟机运行时需要从常量池获取对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中。
Class文件中描述的各种信息需要加载到虚拟机之后才能运行和使用,下面介绍虚拟机如何加载这些Class文件。
虚拟机的类加载机制是指:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
类型的加载、连接和初始化过程都是在程序运行期间完成的,这种方式提高了程序的灵活性。例如,编写一个面向接口的应用程序,可以等到运行时再指定具体的实现类;用户可以通过Java预定义和自定义类加载器让一个本地的应用程序可以在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分。
类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,整个声明周期包括:加载、验证、准备、解析、初始化、使用、卸载共7个阶段,其中验证、准备、解析3个阶段统称为“连接”。
其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的“开始”(仅仅指的是开始,而非执行或者结束,因为这些阶段通常都是互相交叉的混合进行,通常会在一个阶段执行的过程中调用或者激活另一个阶段)。也就是说,在初始化开始之前,一定已经开始了加载、验证、准备阶段。而解析阶段则不一定(它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。)
类初始化时机
Java虚拟机规范中没有规定什么时候开始类加载过程中的第一个阶段“加载”,这由虚拟机具体实现决定。但是,虚拟机规范严格规定了“有且只有”5种情况必须立即对类进行“初始化”
1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类还没进行过初始化,则立即初始化。
常见场景:new创建对象、读取或设置类的静态字段(被final修饰的除外,因为已经在编译期把结果放入常量池),以及调用一个类的静态方法的时候。
2)使用java.lang.reflect包的方法对类进行反射调用的时候,立即初始化该类;
3)初始化一个类时,该类的父类还没进行初始化时,立即初始化父类;
4)虚拟机启动时,先初始化主类(即包含main()方法的类);
5)当使用动态语言支持时
这里我们可以从3)看到接口与类的区别,当一个类在初始化时,要求父类全部都已经初始化,但是接口在初始化时,并不要求其父接口全部完成了初始化,只有在使用到父类接口时(如引用接口中定义的常量)才会初始化。
上述5中场景中的行为成为对 类 进行 主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动应用。3个被动引用的例子:
1)通过子类引用父类的静态字段,不会导致子类初始化;
2)通过数组定义来引用类,不吹触发此类的初始化;
3)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
类加载过程
1.加载
这一阶段主要完成的工作包括:
1)通过一个类的全限定名来获取定义此类的二进制字节流,可以来自于Class文件、JAR包、WAR包、网络、运行时计算生成等;
2)将字节流代表的静态存储结构按照虚拟机所需的格式存储在方法区之中;
3)在内存中实例化一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2.验证
这一部分发主要是为了确保Class文件的字节流包含的信息符合当前虚拟机的要求。主要完成以下验证:
1)文件格式验证,确保字节流符合Class文件格式规范;
2)元数据验证,却把字节码描述的信息符合Java语言规范,如是否继承了不允许继承的类等,这一阶段主要盐城数据类型;
3)字节码验证,确保程序的语义是合法、符合逻辑的,这一阶段主要验证类的方法体;
4)符号引用验证,
3.准备
准备阶段正式为类变量分配内存,并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。这里需要强调两点:这时候进行内存分配的变量仅包括类变量(被static修饰的变量),而不包括实例变量,因为实例变量是要在对象实例化的时候随着对象一起分配到Java堆中的;这里提到的“初始值”在一般情况下,是只数据类型的零值,如:
public static int value = 123;
value变量在准备阶段的值是0,而不是123。
一些特殊的情况,如果类字段的字段属性表中存在ConstantValue属性,在准备阶段变量就会被初始化为ConstantValue属性所指定的值,如:
public static final int value = 123;
准备阶段value的值为123。
4.解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用是用一组符号来描述所引用的目标,这个目标不一定已经加载到内存中。直接引用可以是直接指向目标地指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不相同,如果有了直接引用,引用目标一定已经在内存中存在。
解析包括对类或接口的解析、字段解析、类方法解析、接口方法解析等。
5. 初始化
初始化阶段才真正开始执行类中定义的java程序代码,根据程序员通过程序制定的计划去初始化类变量和其他资源。