虚拟机类加载机制
此博客内容均取自网上热度比较高的三位作者的笔记:CyC2018、JavaGuide、一份名为《java核心知识整理》的笔记(作者的笔记中没有留个人信息)
3. JVM 类加载机制
Java 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可被虚拟机直接使用的 Java 类型,这个过程称为虚拟机的类加载机制。
类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。一个原因是程序运行会用到哪些类时未知的,另一个原因是因为如果一次性加载,那么会占用很多的内存。
类的生命周期
3.1 类的加载过程
包含了加载、验证、准备、解析和初始化这 5 个阶段。其中验证、准备、解析这三个阶段又称为连接阶段。
3.1.1 加载
加载是类加载的一个阶段,注意不要混淆。
加载阶段中虚拟机完成以下三件事:
① 通过类的全限定名称获取定义该类的二进制字节流。
② 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
③ 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。
注意二进制流不一定非得要从一个 Class 文件获取,既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。
3.1.2 验证
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。这个阶段是否严谨,直接决定了 Java虚拟机能否承受恶意代码的攻击。且验证阶段的工作量在虚拟机累加过程中占了相当大的比重。
验证阶段大致是对文件格式、元数据、字节码、符号引用四部分数据进行验证。
3.1.3 准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,这里的类变量指的是被 static 修饰的静态变量,而不是实例变量,实例变量对象将会在对象实例化之后随着对象一起分配在 Java 堆中。初始值指的是数据类型的零值。但是如果这个静态变量同时又被 final 修饰,即是一个静态常量,那么这个变量会被初始化成指定的初始值。
3.1.4 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。也就是得到类或者字段、方法在内存中的指针或者偏移量。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
符号引用就是以一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java
虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方发表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。
3.1.5 初始化
初始化阶段是执行类构造器<clinit>方法的过程。<clinit>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子<clinit>()方法执行之前,父类的<clinit>方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<clinit>()方法。
Java 虚拟机必须保证一个类的<clinit>()方法在多线程环境中被正确地同步加锁,否则可能会引起死锁,并且这种死锁很难被发现。在同一各类加载器下,一个类型只会被初始化一次。
3.1.5.1 类初始化时机
对于初始化阶段,虚拟机严格规范了有且只有6 种情况下,必须对类进行初始化:
① 当遇到 new 、 getstatic、putstatic 或 invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
② 使用
java.lang.reflect
包的方法对类进行反射调用时 ,如果类没初始化,需要触发其初始化。 ③ 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
④ 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
⑤ 当使用 JDK1.7 的动态动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始化,则需要先触发器初始化。
⑥ 当一个接口中定义了JDK 8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那么该接口要在其之前被初始化。
3.1.5.1.1 主动引用和被动引用
以上 6 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
① 通过子类引用父类的静态字段,不会导致子类初始化。
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义,且SubClass 继承了 SuperClass
② 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动
生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
SuperClass[] sca = new SuperClass[10];// 不会初始化 SuperClass 类
③ 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的
类的初始化。
System.out.println(ConstClass.HELLOWORLD);
3.2 类加载器
把 实现"通过一个类的全限定名来获取描述该类的二进制字节流" 的这个动作的代码称为“类加载器”。虚拟机设计团队把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类。
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性,每一个类加载器都有一个独立的类名称空间,话句话说:就是比较两个类是否相等,只有这两个类是由同一个类加载器加载的前提下才有意义。
3.2.1 类加载器的分类
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
① 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分;
② 所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。
从 Java 开发人员的角度看,类加载器可以划分得更细致一些(分为三层)(也称为双亲委派模型):
① 启动类加载器(BootstrapClassLoader):最顶层的加载类,由C++实现,负责加载
%JAVA_HOME%/lib
目录下的jar包和类或者或被 -Xbootclasspath
参数指定的路径中的所有类。 ② 扩展类加载器(ExtensionClassLoader): 主要负责加载目录
%JRE_HOME%/lib/ext
目录下的jar包和类,或被 java.ext.dirs
系统变量所指定的路径下的jar包。 ③ 应用程序类加载器(AppClassLoader):负责加载用户路径(classpath)上的类库。如果应用程序中没有自定义过自己的类加载器,那么一般情况下这个就是程序中默认的类加载器。
3.2.2 双亲委派模型
应用程序是由三种类加载器互相配合从而实现类加载,除此之外还可以加入自己定义的类加载器。
下图展示了类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
3.2.2.1 工作过程
一个类加载器首先将类加载请求转发到父类加载器,一层一层向上转发,只有当父类加载器无法完成时才尝试自己加载。
3.2.2.2 好处
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为
java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的 Object
类。3.2.2.3 源码分析
以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。
1 protected Class<?> loadClass(String name, boolean resolve) 2 throws ClassNotFoundException 3 { 4 synchronized (getClassLoadingLock(name)) { 5 // 首先,检查请求的类是否已经被加载过 6 Class<?> c = findLoadedClass(name); 7 if (c == null) { 8 long t0 = System.nanoTime(); 9 try { 10 if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理 11 c = parent.loadClass(name, false); 12 } else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载 13 c = findBootstrapClassOrNull(name); 14 } 15 } catch (ClassNotFoundException e) { 16 //抛出异常说明父类加载器无法完成加载请求 17 } 18 19 if (c == null) { 20 long t1 = System.nanoTime(); 21 //自己尝试加载 22 c = findClass(name); 23 } 24 } 25 if (resolve) { 26 resolveClass(c); 27 } 28 return c; 29 }
3.2.2.4 自定义类加载器
java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。
3.3 Java 模块化系统
扩展类加载器被平台类加载器取代。
当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到一个系统模块中,如果可以找到这样的归属关系,就要优先委派给那个负责那个模块的类加载器完成加载。