1.1 类的生命周期和加载过程
一个类在 JVM 中的生命周期分为七个阶段,分别是加载(Loading)、校验(Verification)、 准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)
第一步到第五步统称为类加载阶段,第二步到第四步称为链接阶段。1)、加载
- 此阶段的主要操作是读取文件系统中、jar包中或存在于任何地方的 class 字节码文件,如果找不到二进制 class 字节码文件,就会抛出 NoClassDefFoundError。
- 加载过程中不会校验 class 字节码文件的格式。类加载的整个过程由 JVM 和 Java 的类加载器系统共同完成。
2)、校验
- 校验过程是确保 class 字节码文件的格式(包括校验魔数、版本号信息、常量池中的符号、类型检查)都符合当前的 JVM。此过程如果校验不合法可能会抛出 VerifyError、ClassFormatError、UnsupportedClassVersionError
- 验证属于链接阶段的一部分,所以这个过程会加载所有依赖的类,例如所有的父类和实现的接口。 如果类层次结构有问题(例如,某个类是其自身的(间接)父类,某个接口(间接)对其自身进行扩展或类似操作),则 JVM 将抛出 ClassCircularityError 。
- 而如果实现的接口并不是一个 interface,或者声明的超类是一个 interface,也会抛出 IncompatibleClassChangeError 。
3)、准备
- 准备阶段会创建静态字段,并将其初始化为默认值(比如 null 或 0),并且会在方法区分配这些变量所使用的内存空间。
- 准备阶段不会执行任何 Java 代码。
4)、解析
- 解析阶段是解析符号引用阶段,也就是解析常量池,主要有四种: 类或接口的解析、字段解析、类方法解析、接口方法解析。
- 编写的 Java 源代码中,当有一个变量引用某个对象时,这个引用在 .class 字节码文件中是以符号引用来存储的,相当于做了一个索引记录。
- 在解析阶段就需要将其解析并链接为直接引用,相当于指向实际对象,此过程也叫动态链接。如果有了直接引用,那么引用的目标对象必定堆中存在。
5)、初始化
- JVM 规范规定,必须在类首次 “主动使用” 时才能执行类的初始化。
- 初始化过程包括执行: 类构造器、static 静态变量赋值语句、static 静态代码块。
- 如果是一个子类进行初始化,会先对其父类进行初始化。
1.2 类加载时机
- 当虚拟机启动时,初始化用户指定的主类,就是启动执行的 main 方法所在的类;
- 当 new 指令创建一个类的实例,也就是 new 一个类的对象时;
- 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
- 子类的初始化会触发父类的初始化;
- 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
- 使用反射 API 对某个类进行反射调用时,初始化这个类,其实跟前面一样,反射调用要么是已经有实例了,要么是静态方法,都需要初始化;
- 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类.
1.3 不执行类初始化的情况
- 通过子类访问父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
- 通过类名获取 Class 对象,不会触发类的初始化,Hello.class 不会让 Hello 类初始化。
- 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,这个参数是告诉虚拟机,是否要对类进行初始化。 Class.forName("jvm.Hello") 默认会加载 Hello 类。
- 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作(加载了,但是不初始化)。
1.4 类加载器机制
系统自带的类加载器分为三种:
- 启动类加载器(BootstrapClassLoader)
- 扩展类加载器(ExtClassLoader)
- 应用类加载器(AppClassLoader)
一般启动类加载器是由JVM内部实现的,由 C++ 编写,并不继承自 java.lang.ClassLoader 在 Java 的 API 里无法拿到,但是我们可以侧 面看到和影响它。 后2种类加载器在 Oracle Hotspot JVM 里,都是在中 sun.misc.Launcher 定义的,扩展类加载器和应用类加载器一般都继承自 URLClassLoader 类, 这个类也默认实现了从各种不同来源加载 class 字节码转换成 Class 的方法。
1.4.1 启动类加载器(BootstrapClassLoader):
它用来加载 Java 的核心类库,是用原生 C++ 来实现的,并不继承自 java.lang.ClassLoader,负责加载 JDK 中 jre/lib/rt.jar 里所有的 class, 它可以看做是 JVM 自带的,我们再代码层面无法直 接获取到启动类加载器的引用,所以不允许直接操作它。比如 java.lang.String 是由启动类加载器加载的, 所以 String.class.getClassLoader() 就会返回 null。
1.4.2 扩展类加载器(ExtClassLoader):
ExtClassLoader 由 BootstrapClassLoader 加载,是 sun.misc.Launcher 中定义的一个内部类,它负责加载 jre 的扩展目录, lib/ext 或者由 java.ext.dirs 系统参数指定的目录中的 jar 包中的类,代码里直接获取它的父类加载器为 null,因为无法拿到启动类加载器。
1.4.3 应用类加载器(AppClassLoader):
AppClassLoader 由 ExtClassLoader 加载,是 sun.misc.Launcher 中定义的一个内部类,它负责在 JVM 启动时加载来自 java 命令的 -classpath 或者 -cp 选项、java.class.path 系统参数指定的 jar 包和类路径。如果没有特别指定,则在没有使用自定义类加载器情况下, 用户自定义的类都由此加载器加载。
1.4.4 类加载器的类层级关系
1.4.5 自定义类加载器
如果用户自定义了类加载器,则自定义类加载器都以应用类加载器(AppClassLoader)作为父加载器。可以直接继承 ClassLoader 来自定义类加载器, 也可以继承 SecureClassLoader 或 URLClassLoader 实现自定义类加载器。继承 SecureClassLoader 将会保留有关安全策略的检查逻辑。
1.4.6 类加载机制的特点
1) 双亲委派机制
当一个类加载器(除了 BootstrapClassLoader)加载一个类时,都会将该类委托给自己的父类加载器,父类加载器如果发现自己还存在父类加载器 会继续往上层委托,直到最顶层的类加载器。只要顶层的类加载器加载到了类就会完成加载过程,如果顶层类加载器没有加载到类,就会一级一级往下 让各级的子类加载器尝试加载,只要级层中某一个类加载器加载到了就完成加载过程,如果所有类加载器都没有加载到该类,就会抛出 ClassNotFountException
打破双亲委派的方法就是同时重写 ClassLoader 的 findClass 和 loadClass 这两个方法,只重写 findClass 不会打破。
2) 负责依赖
如果一个类加载器在加载某个类的时候,发现这个类依赖于另外几个类或接口,也会去尝试加载这些依赖的类。
3) 缓存加载
为了提升加载效率,消除重复加载,一旦某个类被一个类加载器加载,那么它会缓存这个加载结果,不会重复加载。