深入学习Java虚拟机学习笔记-虚拟机类加载机制

1. 类加载时机

深入学习Java虚拟机学习笔记-虚拟机类加载机制

    加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持java语言的运行时绑定(也称动态绑定)

    什么情况下需要开始类加载过程的第一个阶段:加载?虚拟机规范并没有规定。

    初始化阶段,规范有严格规定,有且只有5种情况,必须立即对类进行初始化(加载、验证、准备自然需要在此之前开始):
a. 遇到new, getstatic, putstaticc或ivokestatic这4条指令码时,这4条指令码的最常见场景:使用new实例化对象,读取或设置一个类的静态字段(被final修饰,已在编译阶段把结果放入常量池的静态字段除外),以及调用静态方法。
b. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化则先出发初始化
c. 初始化一个类时,其父亲还没有初始化,则需要先触发父类的初始化
d. 虚拟机启动时,主类需要被初始化

e. 使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,这个句柄对应的类没有进行过初始化,则需要先触发初始化。

    除了这5种情况,所有类的引用方式都不会触发初始化。

    接口的初始化跟父类类似,但只有1种场景是“有且只有”:接口初始化时,并不要求父接口全部完成初始化,只有在真正使用到父接口时(如引用到接口定义的常量)才会初始化。

2. 各个阶段

    加载阶段,虚拟机要完成以下3件事:

a. 通过一个类的全限定名来获取定义此类的二进制字节流
        b. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据
        d. 在内存中生成一个代表这个类的java.lang.Class,作为方法区这个类的各种数据的访问入口

对于非数组的加载阶段,程序员的可控性非常强,因为除了使用默认的类加载器来完成,也可以自定义类加载器来控制字节流的获取方式(充值loadClass())。
    
对于数组,本身不通过类加载器加载,而是虚拟机直接创建的,但数组的类型需要通过类加载器加载。一个数组(下面简称C)的加载过程:
a. 如果数组的组件类型(去掉一个维度的类型)是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识。
b. 如果不是引用类型,虚拟机将会把C标记为与引导类加载器关联。
c. 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。
    
验证阶段:确保程序是能够正常运行的。验证过程有4个阶段:
a. 字节码文件格式验证,例如:是否以魔数开头,版本号是否在虚拟机能处理的范围之内,常量池的常量中是否有不被支持的类型等;
b. 元数据验证,验证数据类型,例如:是否有父类,是否继承了不存在的类,是否实现了父类或接口的必须要实现的方法等;
c. 字节码验证,对方法体进行验证,例如:保证跳转指令不会跳出方法体以外,保证类型转换是有效的
d. 符号引用验证,例如:符号引用中的全额限定名能否找到对应的类,在指定类中是否存在符合方法的字段描述符,符合引用中的类/方法/字段的访问性(private,public...)是否可以被当前类访问。

最后一阶段发生在虚拟机将符号引用转换为直接引用的时候,这个转换动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。

准备阶段:正式为类变量分配内存并设置类变量(被static修饰)初始值(数据类型的零值),这些变量所使用的内存都将在方法区中进行分配。注意,实例变量会在对象实例化时随着对象一起分配在java堆中。如果类变量还被final修饰,那么会被初始化为指定的值。

解析阶段:将常量池内符号引用替换为直接引用的过程。
符号引用,使用一组符号来描述所引用的目标,符合可以使任何形式的字面量,只要能无歧义地定位到目标即可,它与内存布局无关。
直接引用,可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,它和内存布局相关。
虚拟机规范并未规定解析的时间,只要求在执行anewarray,checkcast,getfield,getstatic,instanceof,invokednamic等16个用于操作符号引用的指令码之前,要先解析符号引用。所以虚拟机可以在加载类的时候,就初始化常量池中的符合引用,也可以在使用前再去解析。对同一个符合引用,会有多次解析请求,所以虚拟机可以做一个缓存。

解析的内容有:
a. 类或接口解析
b. 字段解析
c. 类方法解析
d. 接口方法解析

初始化阶段:到这一阶段,才开始执行java代码。根据程序员通过程序制定的主观计划去改变类变量和其它资源,也可以这样描述:初始化阶段是执行类构造器<init>()方法的过程。

<init>()方法是由编译器自动收集类中的所有类变量赋值动作和静态语句块中的 语句合并产生的。它与构造函数不同,不需要显式调用父类构造器。

3. 类加载构造器

类加载构造器有多种:
a. 启动类加载构造器(Bootstrap ClassLoader),负责将<JAVA_HOME>/lib目录下的,或者被-Xbootclasspath目录下的,能被虚拟机识别的(例如rt.jar)类库加载进来。开发者不能直接使用,但可以通过自定义类加载器,把加载请求委派过来。
b. 扩展类加载器(Extension ClassLoader),负责将lib/ext下的类库加载进来,开发者可以直接使用扩展类加载器。

c. 应用程序类加载器(Application ClassLoader),负责加载用户路径下的类库。开发者如果不自定义类加载器,那么会默认使用这个。

深入学习Java虚拟机学习笔记-虚拟机类加载机制

    我们的应用程序都是由这3种类加载器相互配合加载的,如果有必要刻意自定义类加载器。它们直接的关系如上图所示,这种关系称之为:类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型的工作过程:如果是一个类加载器收到了类加载的请求,它首先不会去尝试加载这个类,而是把它委派给父类加载器完成,每一个层次的类加载器都如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时(它的搜索范围没有找到这个类),子加载器才会尝试自己加载。