Java 类的生命周期、ClassLoader的应用和演变(KeyWord:SpringBootClassLoader、委派模型)

17 篇文章 0 订阅
6 篇文章 0 订阅

一、类加载过程 / 类的生命周期:

总共分为五步,依次为:加载、连接(验证、准备、解析)、初始化、使用、卸载
在这里插入图片描述

1.1 加载:
主要完成下面 3 件事情:

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

   加载是通过 类加载器 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 双亲委派模型 决定(能打破该模型)。

1.2 连接:

1. 验证:
   JVM规范校验:JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。

   代码逻辑校验: JVM 会对代码组成的数据流和控制流进行校验,确保没有语法错误

2. 准备:
   内存分配的对象:JVM 为类中的静态成员变量分配内存空间

   初始化的类型:为 静态成员变量 赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。比如:对于int类型将赋值为0,而不是用户定义的数值;但对static final修饰的变量则在该阶段赋用户定义的值。

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

1.3 初始化:

   是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码。

当 JVM 遇到下面6种情况时,必须对类进行初始化(只有主动去使用类才会初始化类):

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

初始化过程中,会对类变量赋用户指定的值,且是线程安全的,因此可实现线程安全的单例模式(单例类用例可参考)。

1.4 使用:

   JVM 从入口方法开始执行用户的程序代码

1.5 卸载:

   该类的 Class 对象被 GC。

卸载类需要满足 2 个要求:

  • 该类没有在其他任何地方被引用,所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
  • 该类的 Classloader 实例已被 GC

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

只要想通一点就好了,JDK 自带的 BootstrapClassLoader, ExtClassLoader, AppClassLoader 负责加载 JDK 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的 Classloader 的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。

   更多类加载可参考

思考:

   在整个加载过程中,其实 初始化 时才是真正执行类中代码的一步,那么类中的代码是按顺序自上而下加载,还是按其他规则加载的呢?

二、加载阶段 - 类加载器:

特点:

  • 类加载器是一个负责加载类的对象,用于实现类加载过程中的“加载”这一步。ClassLoader 可以在内存中生成一个代表该类的 Class 对象
  • 每个 Java 类都有一个引用指向加载它的 ClassLoader。
  • 数组类是由 JVM 直接生成的,其 ClassLoader 和该数组的元素类型是一致的。

JVM 中内置了三个重要的 ClassLoader:

1. Bootstrap ClassLoader

   负责装载最核心的Java类,比如Object、System、 String等,用 C++ 来实现的,不存在于 JVM 中。

2. ExtensionClassLoader,JDK 9 及以后更换为Platform ClassLoader

   主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,以及被 java.ext.dirs 系统变量所指定的路径下的所有类,比如XML、加密、压缩相关的功能类等。

3. AppClassLoader

   负责加载当前应用 classpath 下的所有 jar 包和类。

User ClassLoader

   自定义的类加载器由使用方来实现,以满足自己的特殊需求。比如可以对 Java 类的字节码( .class 文件)进行加密,加载时再利用自定义的 ClassLoader 对其解密。

自定义类加载器继承至 ClassLoader 类,其中有两个关键的方法:

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

​​在这里插入图片描述

   当需要加载一个类时,递归的向上级父加载器查找类,然后逐层向下尝试加载类,确保了基础类的统一。

   在查找类时,通过类的 全限定类名其类加载器ID 作为唯一标识,这也是 JVM 中 判断两个类是否相等 的条件

代码用例:

public static void main(String[] args) {
        ClassLoader c = ATest.class.getClassLoader();
        ClassLoader cp = c.getParent();
        ClassLoader cpp = cp.getParent();

        System.out.println(c);//sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(cp);//sun.misc.Launcher$ExtClassLoader@1eb44e46
        System.out.println(cpp);//null
    }

更多介绍可参考 该文

委派模型下的类加载机制:

为什么不叫“双亲委派模型”?因为这里的“双亲”容易让人误解其中的流程,本文简化为“委派模型”

流程图:
在这里插入图片描述

委派模型的好处

   委派模型保证了 Java 程序的稳定运行,可以保证一个类只被一个 ClassLoader 加载且只被加载一次,避免了类的重复加载(JVM 中类的唯一标识:类全限定名 + ClassLoader),也保证了 Java 的核心 API 不被篡改

思考:

  1. 委派模型适用于所有场景吗?解答
  2. 确定类的加载器后,是如何加载类的?解答

打破委派模型的场景

委派模型在 Java 中,被破坏的场景:

场景一:重写 ClassLoader#loadClass(java.lang.String, boolean)方法

   Tomcat 作为 web 容器,当部署多个应用时,为了处理多个应用对同一个第三方类库的不同版本的依赖,在 WebappClassLoader (自定义的类加载器)中重写loadClass,优先使用 WebappClassLoader 进行加载,违背了委派原则中从父到子的逐级加载顺序。

场景二:Thread Context ClassLoader

   基础类(由Bootstrap加载,如JNDI 服务)为了调用子类(由 Application / User 加载)的服务,加入了 Thread Context ClassLoader 设计,用来选择合适的类加载器去完成子类的加载, 解决了基础类型要回调到 用户自定义类的类加载问题。

JNDI(Java Naming and Directory Interface)是Java提供的一种API,它提供了一种标准的方式,使Java 应用程序能够通过统一的接口访问各种命名和目录服务。

在 JNDI 中,Thread Context ClassLoader 的实际应用主要是为了解决类加载器的上下文切换问题,避免类加载冲突和类找不到的问题。

由于 JNDI 服务在不同的类加载器环境中执行,它不一定能够直接访问目标类所在的类加载器。这可能导致类加载冲突或无法找到类的问题。为了解决这个问题,JNDI 服务可以使用线程上下文类加载器(Thread Context ClassLoader)来切换到合适的类加载器上下文。用例如下:

在JNDI中,可以通过以下步骤使用线程上下文类加载器:
1.保存当前线程的上下文类加载器:
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
2.在需要加载类的地方,将线程上下文类加载器设置为目标类加载器:
Thread.currentThread().setContextClassLoader(targetClassLoader);
3.执行需要使用目标类加载器加载的操作,如通过JNDI查找和获取资源。
4.恢复线程的上下文类加载器为原来的类加载器:
Thread.currentThread().setContextClassLoader(currentClassLoader);

场景三:Hot Swap

   为了实现热更新,替换一个模块时,把这个模块连同类加载器一起换掉就实现了代码的热替换。

   在OSGI环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构

SpringBoot中,ClassLoader的实现与功能解析

   SpringBoot 的 FAT JAR 机制简化了应用的打包、部署、启动能力。为了实现 FatJar 机制,SpringBoot 对类加载机制做了对应的调整。

打包后的目录结构:

├── BOOT-INF
│   ├── classes # 应用配置和class文件
│   └── lib # 应用依赖jar文件
├── META-INF
│   ├── MANIFEST.MF # jar 描述信息文件。应用启动类,boot启动入口类等
│   ├── maven # maven 信息文件
└── org
    └── springframework
        └── boot
            └── loader # spring boot 启动相关类库
            |   ├── archive
            |   │   ├── Archive.class
            |....

Boot中 ClassLoader 特点以及它解决的问题

类加载流程图:

pom中引入 groupId:org.springframework.boot ; artifactId:spring-boot-loader ; version: 2.2.9

在这里插入图片描述

   SpringBoot 将创建的 LaunchedURLClassLoader 添加到 Thread 中,当一些基础的ClassLoader 无法加载到 应用所依赖的其他 jar 中所包含的类 时,使用 LaunchedURLClassLoader 完成它们的加载动作。


思考&问题:

1. 类中不同区域代码加载顺序测试

类变量(静态变量)、静态代码块:

  1. 随着类加载而被加载,在加载过程中,是线程安全的(可用于单例模式的创建)

  2. 是最先执行的

成员变量、代码块:

  1. 在类初始化时被调用

  2. 是在构造函数前执行的

构造函数:

  1. 在初始化时被调用

实列化一个对象的执行顺序用例:

  1. 父类静态变量和静态代码块(顺序执行);

  2. 子类静态变量和静态代码块(顺序执行);

  3. 父类的成员变量和代码块(顺序执行);

  4. 父类的构造函数;

  5. 子类的变量和代码块(顺序执行);

  6. 子类的构造函数。

package TestOrder;

public class Order
{
    public static void main(String[] args) {
        new Child();
    }
}
class Child extends Parent{
    static Foo FOO = new Foo("44444Child's static parameter");//4

    Foo foo = new Foo("99Child's parameter");//9

    static {
        System.out.println("5555Child's static code block");//5
    }

    {
        System.out.println("10 10 Child's code block");//10
    }

    public Child() {
        System.out.println("11 11 Child.Child()");//11
    }
}

 class Parent {
    static Foo FOO = new Foo("22222Parent's static parameter");//2

    Foo foo = new Foo("6666Parent's parameter");//6

    static {
        System.out.println("333333Parent's static code block");//3
    }

    {
        System.out.println("7777Parent's code block");//7
    }

    public Parent() {
        System.out.println("888Parent.Parent()");//8
    }
}

class Foo {
    static String str="this str in Foo";
    static {
        System.out.println("1111print in Foo static :"+str);//1
    }
    public Foo(String word) {
        System.out.println(word);
    }
}

2. 数组的 Classloader 是什么?

   数组的 Classloader与数组的元素类型的 Classloader相同。换句话说,数组的 Classloader 用的是数组中,元素类型的类 Classloader。

   当创建一个数组对象时,它的元素类型的 Classloader 将负责加载数组元素的类。如果数组的元素类型是Java提供的类型(例如int、boolean、String等,由Java虚拟机直接提供的),则缺少类加载器,由引导类加载器(Bootstrap ClassLoader)完成加载

   但如果数组的元素类型是引用类型,那么数组的 Classloader 将由加载这些引用类型的类加载器确定

	 @Test
    public void classloaderTest(){
        // 加载Java核心类
        String[] str = new String[2];
        ClassLoader stringClassLoader = str.getClass().getClassLoader();
        System.out.println("String ClassLoader: " + stringClassLoader);

        // 加载应用程序类
        ExtensionClass[] extensionClass = new ExtensionClass[2];
        ClassLoader extensionClassLoader = extensionClass.getClass().getClassLoader();
        System.out.println("ExtensionClass ClassLoader: " + extensionClassLoader);

        // 加载应用程序类
        ClassLoader appClassLoader = this.getClass().getClassLoader();
        System.out.println("Application ClassLoader: " + appClassLoader);
    }
    
class ExtensionClass {
    // Java扩展类
}

win-jdk 11.0.2-输出:
String ClassLoader: null
ExtensionClass ClassLoader: jdk.internal.loader.ClassLoaders$AppClassLoader@61064425
Application ClassLoader: jdk.internal.loader.ClassLoaders$AppClassLoader@61064425


参考文档:

《深入理解 Java 虚拟机》

JavaGuide - 类

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java类加载生命周期包括加载、验证、准备、解析、初始化和卸载六个步骤。加载阶段类加载器将字节码读取到内存中;验证阶段类加载器验证字节码是否完整、有效;准备阶段类加载器为静态变量分配内存,并将其初始化为默认值;解析阶段类加载器将符号引用转变为直接引用;初始化阶段类加载器为变量赋值;卸载阶段类加载器将从内存中卸载。 ### 回答2: Java的加载生命周期可以分为加载、连接和初始化三个阶段。 第一阶段是加载,当程序需要使用某个时,Java虚拟机会先在路径中寻找该的字节码文件,并读取到内存中。加载的过程可分为以下几个步骤: 1. 加载:通过类加载器将的字节码文件加载到内存中。 2. 验证:验证字节码文件的合法性,防止安全漏洞。 3. 准备:为静态变量分配内存,并设置默认值。 4. 解析:将符号引用转换为直接引用。 第二阶段是连接,连接阶段主要包括验证、准备和解析三个步骤: 1. 验证:再次验证字节码文件的合法性,检查之间的引用是否正确。 2. 准备:为静态变量分配内存,并设置默认值。 3. 解析:将符号引用转换为直接引用,将静态方法的调用转换为实际的内存地址。 第三阶段是初始化,当被初次使用时才会触发初始化,初始化阶段主要进行静态变量赋值和静态代码块的执行等操作。初始化的过程是按照声明的顺序由上至下执行的,可以通过静态代码块来执行一些静态属性的初始化操作,也可以在此进行一些必要的资源加载以及与外部系统的交互等操作。 在Java中,的加载是动态的,即在运行时根据需要加载,并且的加载采用了双亲委派机制,即类加载器在加载时会先委托给父类加载器,只有在父类加载器找不到所需的情况下才由自己来加载。 总结起来,Java的加载生命周期包括加载、连接和初始化三个阶段,其中加载阶段负责将的字节码文件加载到内存中,连接阶段负责验证、准备和解析相关的操作,初始化阶段进行静态变量赋值和静态代码块的执行等操作。 ### 回答3: Java的加载生命周期可以分为加载(Loading)、链接(Linking)和初始化(Initialization)三个阶段。 加载阶段: 在加载阶段,虚拟机通过类加载器(ClassLoader)将字节码文件加载到内存中,并为创建一个Class对象(在方法区中),用来封装的各种信息。 链接阶段: 链接阶段又可以分为验证(Verification)、准备(Preparation)和解析(Resolution)三个步骤。 1. 验证:验证阶段主要是对字节码进行各种静态的、安全性的验证操作,比如格式验证、语义验证等,确保的字节码是合法且安全的。 2. 准备:准备阶段主要是对的静态成员(静态变量和常量)进行内存分配,并设置默认值。 3. 解析:解析阶段是将、方法、字段等符号引用转化为直接引用的过程。 初始化阶段: 在初始化阶段,虚拟机对进行初始化操作,包括静态变量的赋值、静态代码块的执行等。初始化阶段类加载过程中的最后一步,只有在必要的时候才会进行。 需要注意的是,的初始化是一个被动过程,只有在首次使用的时候才会触发初始化操作,比如创建对象、访问静态变量或者静态方法等。 总结起来,Java类加载生命周期包括加载阶段、链接阶段和初始化阶段。加载阶段是将字节码文件加载到内存,并创建Class对象;链接阶段是验证、准备和解析的过程;初始化阶段是对进行初始化操作,只有在使用的时候才会触发初始化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值