JVM系列之ClassLoader和classLoad

类装载器子系统,作为JVM最底层的一部分。

概述

用途:用来加载Class文件到JVM的方法区,在方法区中创建一个java.lang.Class对象(后面简称类对象)通过此实例的newInstance()方法就可以创建出该类的一个对象。

比较两个类是否相等,只有当这两个类由同一个加载器加载才有意义;否则即使同一个 class 文件被不同的类加载器加载,那这两个类必定不同,即通过类的 Class 对象的 equals 执行的结果必为false。

分类

JVM提供三种类加载器:

  • 启动类Bootstrap加载器:负责加载Java_Home\lib中,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的class文件。Java的核心类都由该ClassLoader加载。在Sun JDK中,这个类加载器是由C++实现的,并且在Java语言中无法获得它的引用。
  • 扩展类Extension加载器:负责加载Java_Home\lib\ext目录下,或通过java.ext.dirs系统变量指定路径中的class文件;
  • 应用程序类Application加载器:负责加载用户 classpath 下的 class 文件。可通过 ClassLoader.getSystemClassLoader()来获取。

可通过继承java.lang.ClassLoader类的方式实现自定义类装载器。ClassLoader源码简略版:

public abstract class ClassLoader {
	// 返回该类加载器的父类加载器
	public final ClassLoader getParent();
	// 加载名称为name的类,返回java.lang.Class类的实例
	public Class<?> loadClass(String name);
	// 查找名称为name的类,返回java.lang.Class类的实例
	protected Class<?> findClass(String name);
	// 查找名称为name的已经被加载过的类,返回java.lang.Class类的实例
	protected final Class<?> findLoadedClass(String name);
	// 把字节数组b中的内容转换成Java类,返回java.lang.Class类的实例
	protected final Class<?> defineClass(String name, byte[] b, int off, int len);
	// 链接指定的Java类
	protected final void resolveClass(Class<?> c);
}

双亲委派模型

Parent Delegation Model。
工作过程:如果一个类加载器收到加载类的请求,它首先将请求交由父类加载器加载;若父类加载器加载失败,当前类加载器才会自己加载类。
作用:像java.lang.Object这些存放在rt.jar中的类,无论使用哪个类加载器加载,最终都会委派给最顶端的启动类加载器加载,从而使得不同加载器加载的Object类都是同一个。
原理:双亲委派模型的代码在 java.lang.ClassLoader.loadClass()方法中实现:

  • 首先检查类是否被加载;
  • 若未加载,则调用父类加载器的 loadClass 方法;
  • 若该方法抛出 ClassNotFoundException 异常,则表示父类加载器无法加载,则当前类加载器调用 findClass 加载类;
  • 若父类加载器可以加载,则直接返回 Class 对象。

java.lang.ClassLoader.loadClass源码:

// 提供class类的二进制名称,加载对应class,加载成功则返回表示该类对应的Class<T> instance 实例
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;
    }
}

故加载过程:

  • 查找jvm.dll ,初始化 JVM
  • 产生 Bootstrap Loader,并加载 JAVA_HOME/jre/lib 下的Java核心API类,都在rt.jar里。
  • Bootstrap Loader 加载 Extended Loader,Extended Loader 加载 JAVA_HOME/jre/lib/ext 下的扩展类
  • Bootstrap Loader 加载 AppClass Loader,并将其父加载器设置为 Extended Loader
  • AppClass Loader 加载 CLASSPATH 目录下的 HelloWorld 类

双亲委派机制的优点:

  1. 避免重复加载,保证类的唯一性,将Java类与它的类加载器绑定到一起,当父类加载器加载完成后,子类加载器不会再次加载
  2. 安全性的考虑,如果用户自己定义的类加载器加载JDK的核心类, 就可能对系统安全性造成破坏

任何以java.开头的是核心API包,需要访问权限,强制加载会抛出异常:Exception in thread "main" java.lang.SecurityException: Prohibited package name

涉及类相等的方法有:Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法以及instanceof对象所属关系判定。

如果想要自己加载类,不使用双亲委派机制?可以用线程上下文加载器来加载这些类。

过程

的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。
在这里插入图片描述
其中,类加载过程:加载、链接(验证、准备、解析)、初始化。
C/C++ 在运行前需要完成预处理、编译、汇编、链接;在Java中,类加载 (加载、连接、初始化) 是在程序运行期间完成的。
在程序运行期间进行动态类加载会稍微增加程序的开销,好处:提高程序的灵活性,可以节省内存空间、灵活地从网络上加载类,可以通过命名空间的分隔来实现类的隔离,增强整个系统的安全性。
灵活性体现在它可以在运行期动态扩展,即在运行期间动态加载和动态连接。

其中加载、验证、准备、初始化的开始顺序是依次进行的,这些步骤开始之后的过程可能会有重叠。而解析过程会发生在初始化过程中。这是为了支持Java语言的运行时绑定(又动态绑定或晚期绑定)

加载

流程:

  1. 通过一个类的全限定名获取描述此类的二进制字节流;
  2. 将这个字节流所代表的静态存储结构保存为方法区的运行时数据结构;
  3. 中生成代表这个类的类对象,作为访问方法区的入口;

虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类。JVM提供3种类加载器。

加载的二进制字节流的来源:

  • 已经编译好的本地class文件(绝大多数情况)
  • 压缩包,如 Jar、War、Ear
  • 其它文件中动态生成,如从 JSP 文件中生成 Class 类
  • 数据库中读取,将二进制字节流存储至数据库中,然后在加载时从数据库中读取。有些中间件会这么做,用来实现代码在集群间分发
  • 从网络中,如Applet

加载过程的注意点:

  1. JVM规范并未给出类在方法区中存放的数据结构;
    类完成加载后,二进制字节流就以特定的数据结构存储在方法区中,但存储的数据结构是由虚拟机自己定义的,JVM 规范并没有指定。
  2. JVM规范并没有指定 Class 对象存放的位置;
    在二进制字节流以特定格式存储在方法区后,JVM 会创建一个类对象,作为本类的外部接口。
    既然是对象就应该存放在堆内存中,不过 JVM 规范并没有给出限制,不同的虚拟机根据自己的需求存放这个对象。HotSpot 将 Class 对象存放在方法区。
  3. 加载阶段和连接阶段是交叉的;
    类加载过程中每个步骤的开始顺序都有严格限制,但每个步骤的结束顺序没有限制。也就是说,类加载过程中,必须按照如下顺序开始:加载、连接、初始化,但结束顺序无所谓,因此由于每个步骤处理时间的长短不一,就会导致有些步骤会出现交叉。

类和数组加载过程的区别?
数组也有类型,称为数组类型,String[] str = new String[10];此数组类型是Ljava.lang.String,String只是这个数组中元素的类型。
当程序在运行过程中遇到 new 关键字创建一个数组时,由 JVM 直接创建数组类,再由类加载器创建数组中的元素类。
而普通类的加载由类加载器完成。既可以使用系统提供的引导类加载器,也可以使用用户自定义的类加载器。

验证

为了确保Class文件符合当前虚拟机要求,需要对其字节流数据进行验证,主要包括格式验证、元数据验证、字节码验证和符号引用验证。

  1. 格式验证
    验证字节流是否符合class文件格式的规范,并且能被当前虚拟机处理,如是否以魔数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内、常量池是否有不支持的常量类型等。只有经过格式验证的字节流,才会存储到方法区的数据结构,剩余3个验证都基于方法区的数据进行。
  2. 元数据验证
    对字节码描述的数据进行语义分析,以保证符合Java语言规范,如是否继承final修饰的类、是否实现父类的抽象方法、是否覆盖父类的final方法或final字段等。
  3. 字节码验证
    对类的方法体进行分析,确保在方法运行时不会有危害虚拟机的事件发生,如保证操作数栈的数据类型和指令代码序列的匹配、保证跳转指令的正确性、保证类型转换的有效性等。
  4. 符号引用验证
    为了确保后续的解析动作能够正常执行,对符号引用进行验证,如通过字符串描述的全限定名是都能找到对应的类、在指定类中是否存在符合方法的字段描述符等。

为什么需要验证?
编译器和虚拟机是相互独立的,虚拟机只认二进制字节流,不管获得的二进制字节流的来源。为防止字节流中有安全问题,因此需要验证。

验证阶段比较耗时,它非常重要但不一定必要,若所运行的代码已经被反复使用和验证过,可使用-Xverify:none参数关闭,以缩短类加载时间。

准备

在准备阶段,为类变量(static修饰)在方法区中分配内存并设置初始值。如private static int var = 100;准备阶段完成后,var 值为0,而不是100。在初始化阶段,才会把100赋值给val。

final,特殊情况:private static final int VAL= 100;在编译阶段会为VAL生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将VAL赋值为100。

解析

解析阶段是可选的,将常量池中的符号引用替换为直接引用的过程,两者的不同之处:

  1. 符号引用,类似于OS中的逻辑地址,使用一组符号来描述所引用的目标,可以是任何形式的字面常量,定义在Class文件格式中。
  2. 直接引用,类似于OS中的物理地址,可以是直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。

初始化

初始化类中的静态变量,并执行类中的static代码、构造函数。类初始化的过程是不可逆的,如果中间一步出错,则无法执行下一步。JVM规范严格定义何时需要对类进行初始化:

  1. 通过new关键字、反射、clone、反序列化机制实例化对象时
  2. 调用类的静态方法时
  3. 使用类的静态字段或对其赋值时
  4. 通过反射调用类的方法时
  5. 初始化该类的子类时(初始化子类前其父类必须已经被初始化)
  6. JVM启动时被标记为启动类的类(简单理解为具有main方法的类)

初始化阶段就是执行类构造器clinit()的过程。clinit()方法由编译器自动产生,收集类中static{}代码块中的类变量赋值语句和类中静态成员变量的赋值语句。
在准备阶段,类中静态成员变量已经完成默认初始化,而在初始化阶段,clinit()方法对静态成员变量进行显示初始化。
该过程的注意点:

  1. clinit()方法中静态成员变量的赋值顺序是根据 Java 代码中成员变量的出现的顺序决定的;
  2. 静态代码块能访问出现在静态代码块之前的静态成员变量,无法访问出现在静态代码块之后的成员变量;
  3. 静态代码块能给出现在静态代码块之后的静态成员变量赋值;
  4. 构造函数init()需显示调用父类构造函数,而类的构造函数clinit()不需要调用父类的类构造函数,因为虚拟机会确保子类的clinit()方法执行前已经执行父类的clinit()方法;
  5. 如果一个类/接口中没有静态代码块,也没有静态成员变量的赋值操作,编译器不会生成clinit()方法;
  6. 接口也需要通过clinit()方法为接口中定义的静态成员变量显示初始化;
  7. 接口中不能使用静态代码块;
  8. 接口在执行clinit()方法前,虚拟机不会确保其父接口的clinit()方法被执行,只有当父接口中的静态成员变量被使用到时才会执行父接口的clinit()方法;
  9. 虚拟机会给clinit()方法加锁,因此当多条线程同时执行某一个类的clinit()方法时,只有一个方法会被执行,其它的方法都被阻塞。只要有一个clinit()方法执行完,其它的clinit()方法就不会再被执行。因此在同一个类加载器下,同一个类只会被初始化一次。

初始化开始时机

JVM 规范中只定义类加载过程中初始化过程开始的时机,加载、连接过程都应该在初始化之前开始 (解析除外),这些过程具体在何时开始,JVM 规范并没有定义,不同的虚拟机可以根据具体的需求自定义。

在运行过程中遇到如下字节码指令时,如果类尚未初始化,那就要进行初始化:new、getstatic、putstatic、invokestatic。这四个指令对应的 Java 代码场景是:

  • 通过 new 创建对象;
  • 读取、设置一个类的静态成员变量(不包括final修饰的静态变量);
  • 调用一个类的静态成员函数。
  • 使用java.lang.reflect进行反射调用时,如果类没有初始化,那就需要初始化。

当初始化一个类的时候,若其父类尚未初始化,那就先要让其父类初始化,然后再初始化本类;当虚拟机启动时,虚拟机会首先初始化带有main方法的类,即主类。

JVM 规范中要求在程序运行过程中,当且仅当出现上述4个条件之一的情况才会初始化一个类。如果间接满足上述初始化条件是不会初始化类的。直接满足上述初始化条件的情况叫做主动使用;间接满足上述初始化过程的情况叫做被动使用

引用

类加载过程的最后一个阶段,到初始化阶段,才真正开始执行类中的Java程序代码。

主动引用

只有这四种情况才会触发类的初始化,称为对一个类进行主动引用,除此之外所有引用类的方式都不会触发其初始化,称为被动引用。
VM规范严格规定有且只有四种情况必须立即对类进行初始化:

  1. 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类还没有进行过初始化,则需要先触发其初始化。生成这四条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或设置一个类的静态字段(static)时(被static修饰又被final修饰的,已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法时。
  2. 使用Java.lang.refect包的方法对类进行反射调用时,如果类还没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
  4. 当VM启动时,用户需要指定一个要执行的主类,VM会先执行该主类。

被动引用

  1. 通过子类引用父类中的静态字段,这时对子类的引用为被动引用,因此不会初始化子类,只会初始化父类;
  2. 常量在编译阶段会存入调用它的类的常量池中,本质上没有直接引用到定义该常量的类,因此不会触发定义常量的类的初始化;
  3. 通过数组定义来引用类,不会触发类的初始化

使用

卸载

拓展

类加载的用途:类层次划分、OSGI、热部署、代码加密

绑定

把一个方法的调用与方法所在的类 (方法主体)关联起来,对Java来说,绑定分为静态绑定和动态绑定:

  1. 静态绑定:即前期绑定。在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。针对Java,可以简单的理解为程序编译期的绑定。Java当中的方法只有final,static,private和构造方法是前期绑定的。
  2. 动态绑定:即晚期绑定,运行时绑定。在运行时根据具体对象的类型进行绑定。Java中几乎所有的方法都是后期绑定的。

Launcher & ExtClassLoader & AppClassLoader

ExtClassLoader与AppClassLoader,都在sun.misc.Launcher类内部定义,是其静态内部类。JVM负责调用已经加载在方法区的类sun.misc.Launcher的静态方法getLauncher(),获取sun.misc.Launcher实例,且sun.misc.Launcher使用单例设计模式,保证一个JVM内只有一个Launcher实例。

线程上下文加载器

线程上下文类加载器是从线程的角度来看待类的加载,为每一个线程绑定一个类加载器,可以将类的加载从单纯的双亲加载模型解放出来,进而实现特定的加载需求。

Class.forName和ClassLoader.loadClass区别

一个 Java类加载到 JVM 中会经过三个步骤,装载(查找和导入类或接口的二进制数据)、链接(校验:检查导入类或接口的二进制数据的正确性;准备:给类的静态变量分配并初始化存储空间;解析:将符号引用转成直接引用)、初始化(激活类的静态变量的初始化 Java 代码和静态 Java 代码块)。
Class.forName(className)方法,内部实际调用方法Class.forName(className, true, classloader);

// name:要加载 Class 的名字,initialize:是否要初始化,loader:指定的 classLoader
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)

ClassLoader.loadClass(className)方法,内部实际调用方法ClassLoader.loadClass(className, false);

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
/**
两个参数的含义分别为:
name:class 的名字
resolve:是否要进行链接
*/

通过传入的参数可知 Class.forName方法执行之后已经对被加载类的静态变量分配完存储空间,而ClassLoader.loadClass方法并没有一定执行完链接这一步;当想动态加载一个类,且这个类又存在静态代码块或者静态变量,而你在加载时就想同时初始化这些静态代码块,则应偏向于使用Class.forName方法。

instanceof和Class.isInstance区别

instanceof是一个运算符,用来在运行时指出对象是否是特定类的一个实例,通过返回一个布尔值来指出这个对象是否是这个特定类或者是它的子类的一个实例:
result = object instanceof class
result是布尔类型,object 为必选项的实例,class 为必选项的任意已定义的对象类,如果 object 是 class 的一个实例则 instanceof 运算符返回 true,如果 object 不是指定类的一个实例或者 object 是 null 则返回 false。

instanceof 在 java 的编译状态和运行状态是有区别的,在编译状态中 class 可以是 object 对象的父类、自身类、子类,在这三种情况下 java 编译时不会报错,在运行转态中 class 可以是 object 对象的父类、自身类但不能是子类,当为父类、自生类的情况下 result 结果为 true,为子类的情况下为 false。

Class.isInstance(obj)表明这个对象能不能被转化为这个类,如果 obj 是调用这个方法的 Class或接口的实例则返回true,这个方法是 instanceof 运算符的动态等价,如果 obj 为 null 则返回 false。

实例:

public class Test {
    static class ClassA {
    }

    static class ClassB extends ClassA {
    }

    public static void main(String[] args) {
        ClassB b = new ClassB();
        ClassA a = new ClassA();
        ClassA ba = new ClassB();
        System.out.println(b instanceof Object); //true
        System.out.println(null instanceof Object); //false
        System.out.println(b.getClass().isInstance(a)); //false
        System.out.println(a.getClass().isInstance(ba)); //true
        System.out.println(b.getClass().isInstance(ba)); //true
        System.out.println(b.getClass().isInstance(null)); //false
        System.out.println(ClassA.class.isInstance(ba)); //true
        System.out.println(ClassB.class.isInstance(a)); //false
        System.out.println(ClassB.class.isInstance(ba)); //true
        System.out.println(Object.class.isInstance(b)); //true
        System.out.println(Object.class.isInstance(null)); //false
    }
}

参考

深入探讨Java类加载器
类加载机制
JVM类加载器机制与类加载过程

posted @ 2020-05-06 18:46  johnny233  阅读(24)  评论(0编辑  收藏  举报  来源