代码编译后产生字节码而不是本地机器码,是存储格式的一小步,却是编程语言的一大步。
一、类加载机制概述
虚拟机将描述类的.class文件加载到内存,并对数据进行校验,转换解析和初始化,最终生成可以被虚拟机直接使用的对象。Java中类型的加载、连接和初始化在程序运行期间完成,会有性能开销,但为Java程序提供了高度的灵活性,是天生的可动态扩展的语言。
二、类加载的时机
- 类在虚拟机中的生命周期
- 加载、验证、准备、解析、初始化、使用和卸载七个阶段且顺序是确定的,验证、准备、解析统称为连接。
- 解析阶段某些情况下可以在初始化之后开始,这是为了支持Java语言的运行时绑定(又称动态绑定或晚期绑定)。
- 初始化开始的时机
- 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,对应Java代码场景是:使用new实例化对象、读取或设置类的静态字段(final修饰,已在编译期把结果放入常量池的静态字段除外)、调用类的静态方法。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类未进行过初始化则触发其初始化。
- 初始化类时,其父类未进行初始化则先对父类进行初始化。
- 虚拟机启动时,要执行的主类(包含main方法的类)虚拟机会先初始化这个主类。
- 使用动态语言支持时,java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,需要先触发其初始化。
- 对于上述5中场景,是“有且只有”的能触发初始化的情况,称为对一个类进行主动引用,除此之外不会触发初始化,称为被动引用。
- 静态块(static{}修饰的代码块)只会在初始化时执行,定义数组时不会触发类的初始化,静态常量会在编译时被转化为对常量池中值的引用。
- 接口(interface)进行初始化时,不要求父接口全部完成初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。
- 数组初始化时,不会触发类的初始化。
三、类加载的过程
- 加载阶段
- “加载”是“类加载(class loading)”过程的一个阶段,需要在这个阶段完成三件事情。
- 通过类的全限定名来获取定义类的二进制字节流,将字节流代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成代表类的java.lang.Class对象作为方法区中这个类的访问入口。
- 加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中,方法区中数据存储格式又虚拟机实现自行定义,虚拟机规范没有规定。
- 验证阶段
- 验证是连接阶段的第一步,确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机的安全。
- 大致完成四个阶段的检查动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
- 文件格式验证,是否以魔数0xCAFEBABE开头,主、次版本号是否在当前虚拟机处理范围之内,常量池中是否有不被支持的常量类型等等。该阶段保证输入的字节流能正确地解析并存储于方法区,格式上符合Java类型信息的要求,因此后面的三个验证阶段是基于方法区的存储结构进行,不直接操作字节流。
- 元数据验证,对字节码描述的信息进行语义分析,保证其描述的信息符合Java语言规范的要求,如:这个类是否有父类、是否继承了被final修饰的父类、如果不是抽象类是否实现了父类或接口之中要求实现的方法等等。
- 字节码验证,是验证阶段最复杂的,通过数据流和控制流分析确定程序语义是合法的、符合逻辑的,保证被校验的类的方法在运行时不会危害虚拟机的安全,如类型不匹配的情况。
- 符号引用验证,对类自身以外的信息进行匹配性校验,如符号引用中通过字符串描述的全限定名是否能找到对应的类。
- 准备阶段
- 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,变量需使用的内存都将在方法区中进行分配。需要注意的是,该阶段进行内存分配的仅有类变量(被static修饰的变量),不包括实例变量。类变量按类型在内存分配空间后,初始值为零值(int:0,long:0L,short:(short)0,char:'\u0000',byte:(byte)0,boolean:false,float:0.0f,double:0.0d,reference:null)。如果是被final修饰的变量,编译时javac会为变量生成ConstantValue属性,在该阶段虚拟机根据ConstantValue属性将变量赋值。
- 解析阶段
- 该阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 虚拟机规范没有规定解析阶段发生的具体时间,只要求在执行用于操作符号引用的字节码指令之前,先对它们使用的符号引用进行解析。
- 初始化
- 该阶段根据程序员通过程序制定的主观计划去初始化类变量和其他资源,即该阶段是执行类构造器<clinit>()方法的过程。
- <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生,收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,在其之后定义的变量不能访问但可以赋值。
四、类加载器
- 类与类加载器
- 虚拟机把类加载阶段中,“通过全限定名获取描述此类的二进制字节流”交给虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为“类加载器”。
- 每一个类加载器都拥有独立的类名称空间,比较两个类是否“相等”,只有在两个类是由同一个类加载器加载的前提下才有意义。
- 双亲委派模式
- 从Java虚拟机角度看,有两种不同的类加载器,一种试启动类加载器(Bootstrap ClassLoader),在Hotspot中由C++语言实现;一种是所有其他的类加载器,由Java语言实现,独立于虚拟机外部,并且继承自java.lang.ClassLoader类。
- 大部分时候程序会用到三种系统提供的类加载器,包括:启动类加载器,负责将存在<JAVA_HOME>\lib目录中的,或被-Xbootclasspath参数指定的路径中的类库加载到虚拟机;扩展类加载器,它负责加载<JAVA_HOME>\lib\ext目录中,或java.ext.dirs系统变量指定路径中的所有类库;应用程序类加载器(系统类加载器),由ClassLoader.getSystemClassLoader()方法获取,如果程序没有自定义过类加载器,一般情况下这个就是程序默认的类加载器。
- 启动类加载器 <--- 扩展类加载器 <--- 应用程序类加载器 <--- 自定义类加载器,这样的层次关系称为类加载器的双亲委派模型(Parents Delegation Model)。
- 双亲委派模型在工作时,如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载,而是将请求委派给父类加载器去完成,每一个层次的类加载器都是如此。这样可以保证任何一个类加载请求都是由同一个类加载器完成。
说明
- 文中内容主要来自于《深入理解Java虚拟机》,更多内容请关注原著。
- 此文是读书时对重点知识的记录,如有错误请一定指出。
网友评论