# 概述

# 虚拟机类加载机制

① 定义:虚拟机把描述类的数据从 class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 java 类型,这就是虚拟机的类加载机制;

② 区别于哪些在编译时需要进行连接工作的语言,Java 的类型的加载、连接和初始化过程都是在程序运行期间完成的,这种情况会增加一定性能开销,但是保证了高度灵活性,java 天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的;

# 类加载的时机

# 类加载生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载 7 个阶段,其中验证、准备、解析 3 个部分统称为连接,加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始(注意是 “按部就班地开始”,因为这些阶段通常都是互相交叉地混合式进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段),解析这一阶段某些情况下可以在初始化之后再开始,这是为了支持 Java 语言的运行时绑定(也称为动态绑定或晚期绑定);

# 类加载过程的第一个阶段加载发生情况

jvm 并未进行强制约束,可以交给虚拟机的具体实现来自由把握;

# 初始化阶段有 5 种情况必须立即对类进行初始化

初始化阶段有 5 种情况必须立即对类进行初始化(而加载、验证、准备自然需要在此之前进行):

① 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时;

② 使用 java.lang.reflect 包的方法对类进行反射调用时;

③ 初始化一个类时,发现其父类未进行过初始化,需要现对父类进行初始化;

④ 虚拟机启动时,用户需要指定一个要执行的主类(含 main () 方法的),虚拟机会先初始化这个主类;

⑤ 当使用 jdk1.7 的动态语言支持时,若一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要对其进行初始化; (“有且只有” 以上五种场景会触发初始化,这 5 个行为被称为对一个类进行主动引用;此外所有引用类的方式都不会触发初始化,称为被动引用)

# 被动引用

① 通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化;

# 接口的加载过程与类加载过程稍有一些不同

① 接口也有一些初始化过程,这点与类一致,但接口中不能使用 static {} 语句块,编译器会为接口生成 “<clinit>()” 类构造器,用于初始化接口中所定义的成员变量;

② 与 2 中第③条有差异,接口初始化时无需父接口全部完成初始化,只有在真正使用到父接口时候才会初始化;

# 类加载的过程

  1. 加载

① 通过一个类的全限定名来获取定义此类的二进制字节流:如 zip 包(jar、ear、war 包)、网络(Applet)、运行时计算生成(动态代理技术)、其他文件生成(jsp 转 class)、数据库读取等;

② 数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。但数组类的元素类型最终还是要靠类加载器去创建;

  1. 数组类创建过程遵循规则

① 如果数组的组件类型(指数组去掉一个维度的类型)是应用类型,那就递归采用加载过程去加载这个组建类型,数组类将在加载该组建类型的类加载器的类名称空间上被标识;

② 如果数组的组件类型不是引用类型(如 int []),Java 虚拟机将会把数组类标记为与引导类加载器关联;

③ 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public;

  1. 加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中(存储格式由虚拟机实现自行定义);

  2. 然后在内存中实例化一个 java.lang.Class 类的对象(并没有明确规定在 java 堆中,对于 Hotspot 而言,Class 对象比较特殊,它虽是对象,但存放在方法区里),这个对象将作为程序访问方法区中的这些类型数据的外部接口;

  3. 验证(非常重要但不一定必要,可以使用 - Xverify:none 参数关闭大部分的类验证措施以缩短类加载时间)

① 是连接阶段第一步,验证安全性,字节流检错,包含 4 个阶段:文件格式验证、元数据验证、字节码验证(验证过程中最复杂的一个阶段)、符号引用验证(发生在 jvm 将符号引用转换为直接引用的时候);

  1. 准备

① 正式为类变量(被 static 修饰的变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 java 堆中)分配内存并设置类变量初始值的阶段。这些变量所使用的内存都是在方法区中进行分配;

② 类似 public static int value = 123;代码,此时为准备阶段,value 的初始值还是为 0(即为零值,而不会是 123,只有到初始化阶段时才会被赋值为 123,但有例外,如果类字段的字段属性表中存在 ConstantValue 属性 - 即 final 修饰的属性,如 public static final int value=123;此时会被赋值为 123);

  1. 解析

① 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程;

② 符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,其字面量形式明确定义在 jvm 规范的 Class 文件格式中;

③ 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,与虚拟机内存布局相关,其存在代表指向目标在内存中已经存在;

④ 解析动作主要针对方向:类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类

  1. 初始化

① 类加载最后一步

② 前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与外,其余动作完全由虚拟机主导和控制,而到了类初始化阶段,才真正开始执行类中定义的 java 代码(或者说字节码);

③ 准备阶段已经赋过一次系统要求的初始值,而初始化阶段真正赋值;

④ 初始化阶段是执行类构造器 <clinit>() 方法的过程

# 类加载器

  1. 定义:虚拟机设计团队把类加载阶段中的 “通过一个类的全限定名来获取描述此类的二进制字节流” 这个动作放到 java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为 “类加载器”;

  2. 双亲委派模型

① 从 jvm 角度讲,只存在两种不同的类加载器:启动类加载器,所有其他的类加载器

② 启动类加载器:C++ 语言实现,是虚拟机一部分;

③ 所有其他的类加载器:java 语言实现,独立于虚拟机外部,并且全部继承类 java.lang.ClassLoader;

④ 更细致划分:启动类加载器、扩展类加载器(加载 <JAVA_HOME>/lib/ext 目录中的扩展)、应用程序类加载器(是 ClassLoader 中的 getSystemClassLoader () 方法的返回值,一般加载用户类路径 - classpath 上所指定的类库);

⑤ 双亲委派原则指类加载器之间之中层级关系,除了顶层的启动类加载器外,其余类加载器都应当有自己的父类加载器,这里的父子关系一般不会以继承的关系来实现,而是都是用组合关系来复用父加载器的代码;

⑥ 它是一种非强制性的约束模型,而是 java 设计者推荐给开发者的一种类加载器实现方式;

⑦ 双亲委派模型工作过程:一个类加载器接收到了类加载请求,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,因此最终加载请求都应该传送到顶层的启动类加载器中,只有当父类加载器说他无法完成这个加载请求时,子加载器才会尝试自己去加载,就像你自己写一个与 rt.jar 类库中重名的 java 类,将会发现可以正常编译,但是永远也无法被加载运行;;

⑧ 实现双亲委派原则的代码都集中在 java.lang.ClassLoader 的 loadClass () 方法之中;

  1. 破坏双亲委派原则(至今为止出现 3 次较大规模的破坏) 都是一些历史(像 osgi 的类搜索等)