🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# 三、类加载机制 ## 类加载过程 一个类从被加载到虚拟机内存开始到卸载出内存为止,经历的过程如下: :-: ![](https://img.kancloud.cn/59/0f/590f63792947b47be7524525b0373fd4_740x310.png) 与C语言这种编译型语言不同的是,Java的加载、链接和初始化都是在程序运行期间动态执行的。因此Java才被称为解释型语言。 ### 加载阶段 将类的字节码内容加载到方法区中时,JVM内部会使用一个C++的一个instanceKlass结构体来描述Java的类,其主要的字段有 1. \_java\_mirror:java的类镜像,是C++的结构和Java的类结构之间的桥梁,作用是将Klass暴露给Java使用。 2. \_super:父类。 3. \_fields:成员变量。 4. \_methods:方法。 5. \_constants:常量池。 6. \_class\_loader:类加载器。 7. \_vtable:虚方法表。 8. \_itable:接口方法表。 如果这个类的父类还没有被加载,就会先加载父类;加载和链接阶段可能是交替运行的。 > 注意:instanceKlass这样的元数据是存储在方法区的,当\_java\_mirror是存储在堆中。 :-: ![](https://img.kancloud.cn/ab/ef/abef3d01c29c0bb72e8d4d93a58aee87_1136x599.png) ### 链接阶段 链接阶段分为三个步骤: 1. 验证:验证类是否符合JVM规范,同时进行安全性检查。 可以使用二进制编辑器修改class文件的魔数,在控制台运行看看是否正确。 2. 准备:为静态变量分配空间,设置默认值。 * static变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于\_java\_mirror末尾。 * static变量分配空间和赋值分为两个步骤,分配空间在准备阶段(final修饰的是在分配空间阶段,使用了new关键字的发生在初始化阶段),赋值在初始化阶段完成(cinit<>方法)。 3. 解析:将常量池中的符号引用解析为直接引用。 ![](https://img.kancloud.cn/3a/bb/3abb91df330953dc058b0a44f557bc9c_1363x444.png) 类D只有用到的时候才会被加载和解析,HDBS中会有JVM\_CONSTANT\_UnresolvedClass修饰,表示其为未经解析的。 ### 初始化阶段 《Java虚拟机规范》中对初始化阶段有严格的要求,遇到下面4个字节码指令的时候如果类型还没有进行初始化则必须先进行初始化。 ~~~  new、getstatic、putstatic、invokestatic ~~~ > PS:初始化阶段会调用初始化方法,虚拟机会保证这个类的初始化方法的线程安全。 这些指令出现的场景有: * new 关键字实例化一个对象。 * 读取或者设置一个类型的静态字段,注意该字段没有被final修饰。 * 调用一个类型的静态方法。 > PS:这里的类型指的是类或者接口。 其他的初始化时机: 1. 虚拟机启动时,main方法所在的类会首先被初始化。 2. 使用java.lang.reflect包的方法对类型进行`反射`调用的时候,如果类型还没有初始化则会提前进行初始化。 3. 子类初始化的时候其父类也会初始化。 4. 如果一个接口中定义了JDK8之后的默认方法(default修饰),其实现类发生了初始化,则这个接口要在其之前进行初始化。 不会导致初始化的情况: 1. 访问类的static final静态常量(基本类型和字符串)不会触发初始化,这部分的内容在链接阶段就初始化了。 2. 类对象.class不会触发初始化。 3. 创建该类的数组不会触发初始化。 4. 类加载的loadClass方法不会触发初始化。 5. Class.forName的参数2为false时不会触发初始化。 6. 子类调用父类的静态变量时不会触发子类的初始化。 ~~~  package classloading;  ​  import javafx.util.converter.ShortStringConverter;  ​  /**   * 演示不会进行类初始化的情况   */  ​  public class NotInitialization {  ​  ​      public static void main(String[] args) {          // 1. 子类调用父类的静态变量          System.out.println(SubClass.a);          // 输出结果:super init          // 2. 调用常量          System.out.println(SubClass.str);          // 输出:hello          System.out.println(SubClass.superClass);          // 输出:sub init          // 3.创建数组对象          ArrayClass[] arrayClass = new ArrayClass[10];          // 输出:无          // 4. 调用class对象          System.out.println(ArrayClass.class);          // 输出:无               }  }  class SuperClass {      public static int a = 0;      static {          System.out.println("super init");     }  }  ​  class SubClass extends SuperClass {      public static final String str = "hello";      public static final SuperClass superClass = new SuperClass();      static {          System.out.println("sub init");     }  }  ​  class ArrayClass {      static {          System.out.println("array init");     }  }  ​ ~~~ ## 类加载器 作用:加载.class文件 类加载器的分类,级别从上到下: * 启动类(根)加载器:Bootstrap ClassLoader,加载Java的核心库`jre/lib/rt.jar`和下面两个类加载器,无法直接访问。 * 扩展类加载器:Extension ClassLoader,加载Java的扩展库`jre/ext/*.jar`。 * 应用程序加载器:Application ClassLoader,根据Java应用的类路径Classpath加载自定义的Java类。 * 自定义类加载器。 :-: ![](https://img.kancloud.cn/5a/4d/5a4d0d0b5afa5babf06bd22ccd5e951e_1022x469.png) ~~~  @Test  public void test1() {      Student student = new Student();      Class<? extends Student> aClass = student.getClass();  ​      System.out.println("aClass = " + aClass); // aClass = class domain.Student      // sun.misc.Launcher$AppClassLoader@18b4aac2      System.out.println(aClass.getClassLoader());      // sun.misc.Launcher$ExtClassLoader@2ff4acd0      System.out.println(aClass.getClassLoader().getParent());      // null java调用不到,底层用C或C++写的      System.out.println(aClass.getClassLoader().getParent().getParent());  } ~~~ 设置类加载路径的的虚拟机参数 ~~~  -Xbootclasspath/a:. ~~~ \-Xbootclasspath:用来设置启动类加载器的类加载路径,.表示追加当前目录。 /a:.表示将当前目录追加至启动类加载路径(jre/lib/rt.jar)之后。(a表示追加append) 可以用这个方法替换掉一些核心类: * java -Xbootclasspath: 新的启动类加载器的加载路径。 * java -Xbootclasspath/a: * java -Xbootclasspath/p: 例如: ~~~  public class TestLoad {     public static void main(String[] args) throws ClassNotFoundException {         Class<?> aClass = Class.forName("classloading.F");         System.out.println(aClass.getClassLoader());     }  }  ​  public class F {  ​     static {         System.out.println("加载了F类");     }  ​  } ~~~ 执行: ~~~  D:\MyCodes\IdeaProjects\jvm\out\production\jvm>java -Xbootclasspath/a:. classloading.TestLoad ~~~ ~~~  结果;  加载了F类  null ~~~ null就表示该类是由启动类加载器加载的。 ## 双亲(上级)委派机制 类加载器层级: 应用程序类加载器->扩展类加载器->启动类加载器。 加载请求转移规则: 1. 判断被加载的类是否已经被加载,如果是则结束,否则将加载任务委托给自己父亲加载器。 2. 父亲加载器在收到加载请求后,判断类是否已经被加载过,如果是则结束,否则继续委托给自己的父亲加载器。 3. 一直向上委托,直到启动类加载器(Bootstrap ClassLoader)。 类正式加载规则: 1. 启动类加载会判断是否能够完成加载任务,如果能就直接加载,不能就将加载任务委派给子类加载器。 2. 子类加载器判断是否能够完成加载任务,如果能就直接加载,如果不能就继续将加载任务委派给子类加载器。 3. 一直向下委派,直到Application ClassLoader,如果还不能加载则会抛出**ClassNotFoundException**。 ![](https://img.kancloud.cn/20/9b/209b50c1b578140b4904ba18f638e7ed_725x769.png) 好处: 1. 安全:保证Java核心类不被破坏。 2. 唯一性:保证同一个类不会被加载多次。 源码: ~~~  protected Class<?> loadClass(String name, boolean resolve)          throws ClassNotFoundException     {          synchronized (getClassLoadingLock(name)) {              // First, check if the class has already been loaded              Class<?> c = findLoadedClass(name);              if (c == null) {                  long t0 = System.nanoTime();                  try {                      if (parent != null) {                          c = parent.loadClass(name, false);                     } else {                          c = findBootstrapClassOrNull(name);                     }                 } catch (ClassNotFoundException e) {                      // ClassNotFoundException thrown if class not found                      // from the non-null parent class loader                 }  ​                  if (c == null) {                      // If still not found, then invoke findClass in order                      // to find the class.                      long t1 = System.nanoTime();                      c = findClass(name);  ​                      // this is the defining class loader; record the stats                      sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);                      sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);                      sun.misc.PerfCounter.getFindClasses().increment();                 }             }              if (resolve) {                  resolveClass(c);             }              return c;         }     } ~~~