一、加载步骤
Java代码通过编译成字节码,然后通过Java类加载器将字节码加载到JVM中运行,整体分成加载(Loading)、链接(Linking)、初始化(Initialization)三大步骤,其中链接有可以分成校验(Verification)、准备(Preparation)、解析(Resolution)三个小步骤,整体如下所示:
- 加载 根据class完全限定名,获取二进制的classfile格式的字节流
- 校验 检查class文件的字节流信息是否合法
- 准备 创建静态字段,并将其初始化为表中默认字段,并分配方法表,即在方法区中分配这些变量所使用的的内存空间
- 解析 解析常量池,主要是类或接口的解析、字段的解析、类方法解析、接口方法解析
- 初始化 在类的首次主动使用时才执行类的初始化,依次执行父类初始化、类构造器、static静态变量赋值语句、static静态代码块
- 使用 代码中正常使用
- 卸载 Java8之后,可以开启类的卸载:-XX:+CMSClassUnloadingEnabled
二、加载时机
JVM规范列出了如下触发类的初始化时机:
- 当虚拟机启动时,初始化用户指定的主类,就是启动执行的main方法所在的类
- 当遇到用以新建目标类实例的new指令时,初始化new指令的目标类,就是new一个类的时候要初始化
- 当遇到调用静态方法的指令时,初始化该静态方法中的类
- 当遇到访问静态字段的指令时,初始化该静态字段的类
- 子类的初始化会触发父类的初始化
- 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化
- 使用反射API对某个类进行反射调用时,初始化这个类
- 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类
但是以下的情况不会触发类的初始化,可能会触发类的加载:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化
- 定义对象数组,不会触发该类的初始化
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类
- 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化,Class.forName(“jvm.Hello”)默认会加载Hello类
- 通过ClassLoader默认的loadClass方法,也不会触发初始化动作(加载了,但是不初始化)
三、加载机制
Java字节码文件的加载过程是类加载器(ClassLoader)来完成,系统自带的类加载器如下:
- 启动类加载器(BootstrapClassLoader)
加载Java核心类,用原生C++代码来实现,负责加载JDK中jre/lib/rt.jar里所有的class - 扩展类加载器(ExtClassLoader)
负责加载JRE的拓展目录,lib/ext或者有java.ext.dirs系统属性指定 - 应用类加载器(AppClassLoader)
在没有使用自定义类加载器情况下,用户自定义类都由此加载器加载。JVM启动时加载来自Java命令的classpath或者cp选项、java.class.path系统属性指定的jar包和类路径。
类加载机制有如下特点: - 双亲委托 自定义加载器加载一个类时,先委托自己的父类加载器加载,一直委托到启动类加载器,再自己尝试加载
- 负责依赖 如果一个加载器在加载某个类的时候,发现这个类依赖于另外几个类或者接口,也会尝试加载这些依赖项
- 缓存加载 某个类被一个加载器加载后,会缓存加载结果