【JVM 类加载机制】— 类加载总结

7. 虚拟机类加载机制

7.1 概述

在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能运行和使用。而虚拟机如何加载这些Class文件?Class文件中的信息进入到虚拟机后会发生什么变化?

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制

在Java语言里,类型的加载、连接和初始化过程都是在程序运行期间完成的(p:可以理解为需要才加载)

7.2 类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:

加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)
其中验证、准备、解析3各部分统称为连接(Linking)
记:lvp riuu

java虚拟机规范严格规定有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在之前开始)

  • 使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期间把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化,则需要先触发其初始化
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。接口这里有点不同。初始化接口的时候,不要求初始化父接口,只有在真正使用到父接口的时候才会初始化
  • 当虚拟机启动的时候,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类
  • java.lang.invoke.MethodHandle 方法句柄啥的(不懂)

7.3 类加载的过程

7.3.1 加载

加载是类加载的一个阶段,在加载阶段,虚拟机需要完成以下3件事:

  1. 通过一个类的全限定命名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

相对于类加载其他阶段,一个非数组类的加载阶段(准确地说,就是获取二进制字节流的动作)是开发人员可控性最强的,因为加载阶段可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员可以通过自定义的类加载去控制字节流的获取方式(即重写一个类加载器的loadClass()方法

对于数组类而言,本身不通过类加载器创建,它是由Java虚拟机直接创建的。但数组的元素类型最终是要靠类加载器去创建

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自定义。然后在内存中实例化一个java.lang.Class类的对象(并没有明确规定是在Java堆中,对于HotSpot虚拟机而言,Class放在方法区里面),这个对象作为程序访问方法区中这些类型数据的外部接口。

7.3.2 验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全

对于虚拟机的类加载机制来说,验证阶段是一个非常重要的阶段,但不是一定必要(因为对程序运行期没有影响)的阶段。如果所运行的代码都已被反复使用和验证,那么在实施阶段可以使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间

7.3.3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

这个阶段有两个容易混淆的概念:

  • 首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中
  • 其次,这里所说的初始值“通常情况”下是数据类型的零值(int 0 long 0L boolean false reference null等),真正赋值是在初始化阶段才会执行
  • 上面提到的“通常情况”下初始值是零值,那么会有一些“特殊情况”:如果类字段的字段属性表中存在ConstantValue属性(p:就是常量),那么在准备阶段变量就会初始化为指定的值

public static final int value = 123;

7.3.4 解析

7.3.5 初始化

类初始化是类加载过程的最后一步,在类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作前安全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java代码(或者说是字节码)

在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。

  • <clinit>()方法是有编译器自动收集类中所有列变量的赋值动作和静态语句块(static{})中的语句合并产生的。
  • <clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显示的调用父类构造器,虚拟机会保证子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object
  • <clinit>()方法对于类或接口来说不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法

7.4

虚拟机设计团队把类加载阶段的“通过一个类的全限定命名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取需要的类,实现这个动作的代码模块称为类加载器

类与类加载器

来加载器虽然只用于实现类的加载阶段,但他在Java程序中起到的作用远远不限于类的加载阶段。对于任意一个类,都需要由加载它的的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。

双亲委派模型

从Java虚拟机的角度来看,只存在两种不同的来加载器

  • 启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++实现,是虚拟机自身的一部分
  • 所有其他的类加载器,这些加载其由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader

从Java开发人员的角度看,来加载器还可以分为更细致一些,绝大部分Java程序都会使用到以下3种系统提供的类加载器

  • 启动类加载器(Bootstrap ClassLoader),这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数指定的路径中,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类加载到虚拟机内存中。启动类加载器无法直接被Java程序引用,用户在编写自己的类加载器时,如果想把加载请求委派给启动类加载器,直接使用null代替即可
  • 扩展类加载器(Extention ClassLoader):这个类加载器负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,码农可以直接使用扩展类加载器
  • 应用程序类加载器(Application ClassLoader):这个类加载器时ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。他负责加载用户类路径ClassPath上的所指定的类库,程序猿可以直接使用这个类加载器。如果程序中没有自定义自己的类加载器,一般情况下这个就是程序的默认类加载器。

类加载器中间的层次关系,称为类加载器的双亲委派模型,双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器应当有自己的父类加载器。这里类加载器之间的父子关系不会以继承的关系来实现,而都是使用组合关系来复用父加载器的代码。如下图所示
image

类加载代码

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 {
                    // 启动类加载器没有父加载器,则有启动类自己加载
                    // 如果不能加载,则返回null
                    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;
    }
}

破坏双亲委派模型

重写 findClass() 方法

在JDK 1.2之前,用户继承java.lang.ClassLoader的目的就是为了重写loadClass()方法,因为虚拟机在进行类加载时会调用加载器的私有方法loadClassInternal(),而这个方法唯一的逻辑就是去调用自己的loadClass()方法。

但是上面的代码显示,双亲委派的具体逻辑就在loadClass()之中,如果直接重写loadClass()方法,就直接破坏了双亲委派机制,所以在jdk1.2之后,推荐重写findClass()方法来完成加载,这样就可以保证自定义类加载器是符合双亲委派规则的。

线程上下文类加载器

什么是线程上下文类加载器

首先没有一个类来表示线程上下文类加载器,它只是一个角色,可以通过 java.lang.Thread 类的 setContextClassLoader() 方法进行设置。如果创建线程的时还未设置,它将会从父线程中继承一个。如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。为了理解,可以看下 JDK 中 sun.misc.Launcher 类源码,Launcher 类是 JVM 入口应用

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        // 创建扩展类加载器
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader");
    }

    try {
        // 创建应用程序类加载器,参数为扩展类加载器,也就是应用程序类加载器的父加载器
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader");
    }

    // 把应用程序类加载器设置为上下文类加载器
    Thread.currentThread().setContextClassLoader(this.loader);
    String var2 = System.getProperty("java.security.manager");
    if (var2 != null) {
        SecurityManager var3 = null;
        if (!"".equals(var2) && !"default".equals(var2)) {
            try {
                var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
            } catch (IllegalAccessException var5) {
                ;
            } catch (InstantiationException var6) {
                ;
            } catch (ClassNotFoundException var7) {
                ;
            } catch (ClassCastException var8) {
                ;
            }
        } else {
            var3 = new SecurityManager();
        }
        if (var3 == null) {
            throw new InternalError("Could not create SecurityManager: " + var2);
        }
        System.setSecurityManager(var3);
    }
}

先是创建扩展类加载器(ExtClassLoader),然后创建应用程序类加载器(AppClassLoader),这两个类加载在代码中都继承自 URLClassLoader。 顺着 getExtClassLoader() 方法会进到 ExtClassLoader 的构造方法,

public ExtClassLoader(File[] var1) throws IOException {
    // 调用父类(URLClassLoader)构造方法
    // 注意第二参数传的 null,该参数是父类加载器
    super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
}

public URLClassLoader(URL[] urls, ClassLoader parent,
                      URLStreamHandlerFactory factory) {
    super(parent);
    // this is to make the stack depth consistent with 1.1
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkCreateClassLoader();
    }
    ucp = new URLClassPath(urls, factory);
    acc = AccessController.getContext();
}

在创建扩展类加载器的时候传入的父加载器为 null,这也很好理解。ExtClassLoader 的父类加载器是启动类加载器(Bootstrap ClassLoader),但是启动类加载器是 C++ 实现,在 Java 中调用 getClassLoader() 方法的时候返回的是 null。示例代码如下

public class ClassLoaderDemo {
    public static void main(String[] args) {
        ClassLoader appClassLoader = ClassLoaderDemo.class.getClassLoader();
        ClassLoader extAppClassLoader = appClassLoader.getParent();
        ClassLoader bstClassLoader = extAppClassLoader.getParent();
        
        System.out.println(appClassLoader);
        System.out.println(extAppClassLoader);
        System.out.println(bstClassLoader);
    }
}

控制台输出

sun.misc.Launcher$AppClassLoader@545eb748
sun.misc.Launcher$ExtClassLoader@1653033e
null

为何需要上下文类加载器

一个典型例子便是 JNDI(Java Naming and Directory Interface,Java命名和目录接口) 服务,JNDI 现在已经是 Java 的标准服务,它的代码存在于 rt.jar 中,有启动类加载器加载。但 JNDI 的目的就是对资源进行集中管理和查找,它需要调用由第三方厂商实现并部署在应用程序的 ClassPath 下的 JNDI 接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能去加载器这些类。

为了解决这个问题,Java 设计团队只好引入了一个不太优雅的设计:线程上下文类加载器。于是 JNDI 服务使用线程上线文类加载器去加载所需要的 SPI 的代码,也就是父类加载器请求子类加载器去完成加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器。

线程上下文类加载器代码实例

来自 javax.xml.parsers.FactoryFinder 类中代码

static private Class getProviderClass(String className, ClassLoader cl,
        boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
{
    try {
        // 指定了类加载器
        if (cl == null) {
            // 是否用启动类加载器加载
            if (useBSClsLoader) {
                return Class.forName(className, true, FactoryFinder.class.getClassLoader());
            } else {
                // 不用启动类加载器加载,则使用线程上下文类加载器加载
                cl = ss.getContextClassLoader();
                if (cl == null) {
                    throw new ClassNotFoundException();
                } else {
                    return cl.loadClass(className);
                }
            }
        } else {
            return cl.loadClass(className);
        }
    } catch (ClassNotFoundException e1) {
        if (doFallback) {
            // Use current class loader - should always be bootstrap CL
            return Class.forName(className, true,
            // 
            FactoryFinder.class.getClassLoader());
        } else {
            throw e1;
        }
    }
}

试想,如果没有线程上下文类加载器,上面代码中 SPI 代码(className 对应的类)又怎能被加载呢。

posted @ 2022-06-05 21:13  Tailife  阅读(57)  评论(0编辑  收藏  举报