虚拟机把描述类的 class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。
类加载时机
类从加载到内存开始,到卸载出内存结束,他的整个生命周期包括:
加载(Loading)-> 验证(Verification) -> 准备(Preparation)-> 解析(Resolution)-> 初始化(Initialization) -> 使用(Using)-> 卸载(Unloading)
其中,验证(Verification) -> 准备(Preparation)-> 解析(Resolution) 称之为连接(Linking)
虚拟机对于何时 加载(Lading)没有做强制要求,但是对 初始化(Initialization)确实作了严格规定的。有且仅有以下5种情况下必须立即对类进行初始化:
- 遇到 new、putstatic、getstatic 或 invokestattic 这四条字节码指令时,必须先要触发其初始化
- 使用 java.lang.reflect 包的方法对类进行反射调用时,必须先要触发其初始化
- 初始化一个类的时候,必须先要触发其父类初始化
- 当虚拟机启动时,用户需要指定一个执行的主类(main()方法所在类),虚拟机会先触发其初始化
- 使用 JDK 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getstatic、REF_putstatic、REF_invikestatic 的方法句柄时,并且这个句柄对应的类还未初始化时,必须先要触发其初始化
加载
在 加载 阶段,虚拟机需要完成以下3件事:
- 通过一个类的全类限定名来获取此类的二进制字节流
- 将这个二进制字节流表示的静态存储结构转化为方法区的运行时数据结构
- 内存中生成一个代表该类的 java.lang.Class 对象,作为方法区该类的各种数据的访问入口
由于没有指定从哪里获取二进制字节流,故而有许多途径可以获取:
- 从 zip 包中找到;JAR、EAR 或 WAR
- 从网络获取;Applet
- 运行时产生;动态代理
- 其他文件生成
对于类加载过程中的其他阶段,一个非数组类的加载阶段(准确来说是即在阶段中获取类的二进制字节流的动作)是开发人员可控性最强的,因为加载阶段可以使用系统的引导类加载器去完成,也可以使用用户自定义的类加载器去完成(即重写一个类加载器的 loadClass 方法)。
数组类不是通过类加载器来创建的,而是由 Java 虚拟机直接创建的。但数组类仍然和虚拟机有着密切的关系,因为数据类的元素类型(Element Type,指的是数组去掉所有维度之后的类型)仍然是由类加载器去创建。一个数组类(简称为C)的创建过程就遵循以下规则:
- 如果数组的 元素类型 是引用类型,那么递归加载这个 元素类型,数组C将被标记为与引导类加载器关联
- 如果数组的 元素类型 不是引用类型(例如 int[] ),数组C将在加载该组件类型的类加载器的类名称上被标识
- 数组类的可见性与它的元素类型得可见性一致。假设元素类型不是引用类型,那数组类的可见性将默认为public
验证
验证是连接阶段的第一步,其目的是保证 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会威胁虚拟机的安全。
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都是在方法区中进行分配。
- 仅仅为类变量分配内存,不包括实例变量
- 类变量初始值指的是零值,把真实值赋值给类变量的操作是在类构造器
之后执行的
解析
解析阶段是将常量池内的符号引用替换为直接引用的过程
初始化
类初始化时类加载过程的最后一步。前面的过程除了加载阶段用户应用程序可以通过自定义类加载器参与之外,其余的动作都是由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中的 Java 代码,或者说是字节码。
初始化阶段,可以说是执行
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,收集顺序由源代码中出现的顺序决定。静态语句块中只能访问到它之前的变量,对于定义在其之后的的变量,在静态语句块中可以赋值,但不能访问
class Test{static{i = 0; // 通过,可以赋值System.out.println(i); // 报错,非法向前引用}static int i = 1;}
与 不同,他不需要显示地调用父类构造器,虚拟机会保证子类的 执行之前,父类的 已经执行完毕。因此虚拟机中第一个执行的 肯定是 java.lang.Object - 由于父类的
方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作 方法对于类或接口来说不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,可以不生成 方法 - 接口中不能使用静态语句块,但是依然有变量初始化的赋值操作。因此接口和类一样都会生成
方法。与类不同的是,执行接口的 方法不需要先执行父接口的 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类的初始化时也一样不会执行接口的 方法 - JVM 会保证一个类的
在多线程环境下是线程安全的
类加载器
JVM 设计团队把类加载阶段中 “通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到了 JVM 外部去执行,以便让程序自己决定如何去获取所需要的类。
比较两个类是否相等,只有在其类加载器是同一个的前提下才有意义。
equals()isAssignableFrom()isInstance()instanceOf
双亲委托机制
从 Java 开发人员的角度来看类加载器的分类:
- 启动类加载器(Bootstrap Classload)
该类只负责加载存放在
- 扩展类加载器(Extension ClassLoader)
这个加载器由 sun.misc.Launcher$ExtClassLoader 实现,他负责加载
- 应用程序加载器(Application ClassLoader)
这个加载器由 sun.misc.Launcher$AppClassLoader 实现,由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以称之为系统类加载器。他负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该加载器。
双亲委托模型

双亲委托模型除了 Bootstrap Classload 之外,其余的类加载器都需要有自己的父类加载器。这里的父子关系都是以 组合 的方式来实现的。
工作过程
如果一个类加载器收到类加载请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围内没有找到所需的类)时,子加载器才会尝试自己去加载。
