基础篇Java类加载器详解

Posted Sivan_Xin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基础篇Java类加载器详解相关的知识,希望对你有一定的参考价值。

类加载过程详解

  • 类的生命周期

    类从被加载到虚拟机内存到开始卸载出内存为止,生命周期可以简单概括为7个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,前三个阶段可以统称为连接(Linking)。

类加载过程

类加载过程描述的是类的生命周期从加载到初始化的阶段。

  • 加载

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

    加载这一步主要是通过 类加载器 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 双亲委派模型 决定。Java中的每个类都有一个引用指向它的ClassLoader。数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。

    加载阶段与连接阶段是交叉进行的,加载尚未结束,连接阶段可能就开始了。

  • 验证

    1. 文件格式验证(Class 文件格式检查,比如版本号是否在当前虚拟机的处理范围之内)

      基于类的二进制字节流进行,目的是保证输入的字节流能正确地解析并存储在方法区内。

      而其他三个验证阶段都是基于方法区的存储结构上进行的,不会再直接读取、操作字节流。

    2. 元数据验证(字节码语义检查,比如这个类是否有父类,是否继承了final类等)

    3. 字节码验证(程序语义检查)

    4. 符号引用验证(类的正确性检查,比如该类使用的其他类是否存在,字段是否存在等)

      发生在类加载过程中的解析阶段,具体点说就是JVM将符号引用转化为直接引用的时候。

      用来确保解析阶段能够正常执行。如果无法通过符号引用验证,JVM会抛出异常。

  • 准备

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

    1. 此时分配的变量仅包括静态/类变量(static 修饰的变量),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

    2. JDK7之前,类变量使用的内存都在方法区(永久代)中分配,JDK7之后,HotSpot使用元空间来代替方法区,而字符串常量池和静态变量移动到了堆中。

      那么类变量就随着Class对象一起存放在Java堆中。

    3. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如 0、0L、null、false 等),比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111。

  • 解析

    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

    • 直接引用:直接引用是可以直接指向目标的指针。

    举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。

    综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。

  • 初始化

    初始化阶段是执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

    对于初始化阶段,虚拟机严格规范了有且只有 5 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):

    1. 当遇到 new、getstatic、putstatic 或 invokestatic 这 4 条直接码指令时,比如 new一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
      • 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。
      • 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
      • 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。
      • 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。
    2. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("..."), newInstance() 等等。如果类没初始化,需要触发其初始化。
    3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
    4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
    5. 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
  • 类卸载

    卸载类需要满足如下要求:

    1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
    2. 该类没有在其他任何地方被引用
    3. 该类的类加载器的实例已被 GC

    所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

类加载器详解

加载

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

类加载器

  • 类加载器作用

    类加载器的主要作用就是加载Java类字节码(.class文件)到JVM中(在内存中生成CLass对象代表该类)。类加载器实现了类加载过程中的加载这一步。

    每个 Java 类都有一个引用指向加载它的 ClassLoader

    不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。

  • 类加载器加载规则

    大部分类在具体用到才会去加载,这样对内存更为友好。对于已经加载的类,会被存放在ClassLoader中,在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

  • 类加载器总结

    JVM 中内置了三个重要的 ClassLoader

    1. BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jar、resources.jar 、charsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。
      • rt.jar是Java基础类库,包含Java doc里面看到的所有的类的类文件。也就是说,我们常用内置库 java.xxx.* 都在里面,比如java.util.*java.io.*java.nio.*java.lang.*java.sql.*java.math.*
    2. ExtensionClassLoader(扩展类加载器) :主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
    3. AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

    除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader 抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。

    每个 ClassLoader 可以通过getParent()获取其父 ClassLoader,如果获取到 ClassLoader 为null的话,那么该类是通过 BootstrapClassLoader 加载的。

    • 因为是用C ++ 实现的。
  • 自定义类加载器

    除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader抽象类。

    ClassLoader 类有两个关键的方法:

    • protected Class loadClass(String name, boolean resolve)加载指定二进制名称的类,实现了双亲委派机制 。name 为类的二进制名称,resolve 如果为 true,在加载时调用 resolveClass(Class<?> c) 方法解析该类。
    • protected Class findClass(String name)根据类的二进制名称来查找类,默认实现是空方法。

    建议 ClassLoader的子类重写 findClass(String name)方法而不是loadClass(String name, boolean resolve) 方法。

    也就是,如果不想打破双亲委派模型,使用findClass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。否则重写loadClass()方法。

双亲委派模型

双亲委派模型用来判断我们使用哪个类加载器来加载类。反过来说,就是类加载器ClassLoader使用委派模型来搜索类和资源。

双亲委派模型(Parents Delegation Model)如下所示:

模型要求顶层的启动类加载器除外,其余的类加载器必须有自己的父类加载器

另外,在查找类或资源之前,搜索类和资源的任务会委托给父类加载器

类加载器之间的父子关系一般不是以继承的关系来实现的,而是通过组合关系,来复用父加载器的代码。

  • 因为组合优于继承,多用组合少用继承。

双亲委派模型的执行流程

双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoaderloadClass() 中,相关代码如下所示。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException

    synchronized (getClassLoadingLock(name)) 
        //首先,检查该类是否已经加载过
        Class c = findLoadedClass(name);
        if (c == null) 
            //如果 c 为 null,则说明该类没有被加载过
            long t0 = System.nanoTime();
            try 
                if (parent != null) 
                    //当父类的加载器不为空,则通过父类的loadClass来加载该类
                    //--》这里进入递归调用
                    c = parent.loadClass(name, false);
                 else 
                    //当父类的加载器为空,则调用启动类加载器来加载该类
                    c = findBootstrapClassOrNull(name);
                
             catch (ClassNotFoundException e) 
                //非空父类的类加载器无法找到相应的类,则抛出异常
            

            if (c == null) 
                //当父类加载器无法加载时,则调用findClass方法来加载该类
                //用户可通过覆写该方法,来自定义类加载器
                long t1 = System.nanoTime();
                c = findClass(name);

                //用于统计类加载器相关的信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            
        
        if (resolve) 
            //对类进行link操作
            resolveClass(c);
        
        return c;
    


每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。

结合上面的源码,简单总结一下双亲委派模型的执行流程:

  • 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
    • 判断两个Java类是否相同,主要在两个方面:全类名 + 类加载器都一致。
  • 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。

双亲委派模型好处

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。

打破双亲委派模型

自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

  • 因为类加载器进行类加载的过程中,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。

比如 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader 来打破双亲委托机制。使得 Tomcat 可以加载不同Web应用下相同名的Servlet类。(Tomcat中可以运行多个Web应用程序,并且不会冲突)

Java之类加载器基础详解

一、类加载器

(一)类加载

  • 当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过类的加载,类的连接,类的初始化这三个步骤来对类进行初始化。如果不出现意外情况,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或者类初始化

1. 类的加载

  • ①、就是指将class文件读入内存,并为之创建一个 java.lang.Class 对象
  • ②、任何类被使用时,系统都会为之建立一个 java.lang.Class 对象

2. 类的连接

  • ①、验证阶段:用于检验被加载的类是否有正确的内部结构,并和其他类协调一致
  • ②、准备阶段:负责为类的类变量分配内存,并设置默认初始化值
  • ③、解析阶段:将类的二进制数据中的符号引用替换为直接引用

3. 类的初始化:在该阶段,主要就是对类变量进行初始化

4. 类的初始化步骤

  • ①、假如类还未被加载和连接,则程序先加载并连接该类
  • ②、假如该类的直接父类还未被初始化,则先初始化其直接父类
  • ③、假如类中有初始化语句,则系统依次执行这些初始化语句
  • 注意:在执行第2个步骤的时候,系统对直接父类的初始化步骤也遵循初始化步骤1-3

5. 类的初始化时机:

  • ①、创建类的实例
  • ②、调用类的类方法
  • ③、访问类或者接口的类变量,或者为该类变量赋值
  • ④、使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
  • ⑤、初始化某个类的子类
  • ⑥、直接使用java.exe命令来运行某个主类

(二)类加载器

1. 类加载器的作用

  • 负责将.class文件加载到内存中,并为之生成对应的 java.lang.Class 对象
  • 虽然我们不用过分关心类加载机制,但是了解这个机制我们就能更好的理解程序的运行

2. JVM的类加载机制

  • ①、全盘负责:就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • ②、父类委托:就是当一个类加载器负责加载某个Class时,先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • ③、缓存机制:保证所有加载过的Class都会被缓存,当程序需要使用某个Class对象时,类加载器先从缓存区中搜索该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存储到缓存区
  • ClassLoader:是负责加载类的对象

3. Java运行时具有以下内置类加载器

  • ①、Bootstrap class loader:它是虚拟机的内置类加载器,通常表示为null ,并且没有父null
  • ②、Platform class loader:平台类加载器可以看到所有平台类 ,平台类包括由平台类加载器或其祖先定义的Java SE平台API,其实现类和JDK特定的运行时类
  • ③、System class loader:它也被称为应用程序类加载器 ,与平台类加载器不同。 系统类加载器通常用于定义应用程序类路径,模块路径和JDK特定工具上的类
  • 类加载器的继承关系:System的父加载器为Platform的父加载器为Bootstrap

4. ClassLoader 中的两个方法

  • ①、static ClassLoader getSystemClassLoader​():返回用于委派的系统类加载器
  • ②、ClassLoader getParent​():返回父类加载器进行委派
public class ClassLoadDemo{
    public static void main(String[] args) {
        //static ClassLoader getSystemClassLoader():返回用于委派的系统类加载器
        ClassLoader c = ClassLoader.getSystemClassLoader();
        System.out.println(c);//AppClassLoader


        //ClassLoader getParent():返回父类加载器进行委派
        ClassLoader c2 = c.getParent();
        System.out.println(c2);

        ClassLoader c3 = c2.getParent();
        System.out.println(c3);//null
    }
}

以上是关于基础篇Java类加载器详解的主要内容,如果未能解决你的问题,请参考以下文章

Java基础之反射详解

「JAVA」Java基础之堆栈方法区类加载器——JVM内存模型分析

JVM虚拟机详解类加载器的分类

JVM虚拟机详解类加载器的分类

jvm之java类加载机制和类加载器(ClassLoader)的详解

JVM基础详解