JVM学习笔记---2.类加载器

Java类加载机制解析
本文深入探讨Java类加载器的工作原理,包括父亲委托机制、双亲委托机制的优点、命名空间概念以及线程上下文类加载器的作用。同时,分析了不同类加载器之间的关系和类的卸载过程。

类加载器

类加载器用来把类加载到 Java虚拟机中, 从 JDK 1.2版本开始, 类的加载过程采用父亲委托机制,这种机制能更好的保证 Java平台的安全, 在此委托机制中, 除了Java虚拟机自带的根类加载器以外, 其余的类加载器都有且只有一个父加载器. 当 Java 程序请求加载器 loader1 加载 Sample类时, loader1 首先委托自己的父加载器去加载Sample类, 若父加载器能加载, 则由父加载器完成家在任务,否则由加载器 loader1 本身加载 Sample类(是一种双亲委托机制);

java 里面每一个类型,最终其 数据结构纳入到JVM的管辖范围内即进入到JVM内存当中,该过程就是由类加载器来实施完成的。

类加载器并不需要等到某个类被“首次主动使用”时再加载它。

  • JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了 .class文件缺失或者存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误);
  • 如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

1、类加载器的类型

  • Java 虚拟机自带的加载器
    • 根类加载器(Bootstrap)
    • 扩展类加载器(Extension)
    • 系统(应用)类加载器(System)
  • 用户自定义的类加载器
    • java.lang.ClassLoader的子类
    • 用户可以定制类的加载方式

在这里插入图片描述

以上加载器的关系,是包含的关系,而不是继承的关系

根(Bootstrap)类加载器:
该加载器没有父加载器,它负责加载虚拟机的核心类库,如 java.lang.*等。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,他并没有继承 java.lang.ClassLoader类;

根类加载器 在 HotSpot 虚拟机中,统一使用 null来表示

public class MyTest6 {
    public static void main(String[] args) {
        Class<?> clazz1 = String.class;
        System.out.println(clazz1.getClassLoader());
    }
}

打印结果
在这里插入图片描述

根据上述知识,我们知道,根类加载器负责加载虚拟机的核心类库,String就是核心类之一,因此String类由根类加载器加载。根类加载器 在 HotSpot 虚拟机中,统一使用 null来表示!

根类加载器还有一个比较特殊的地方:
根类加载器(又被称为启动类加载器),并不是 Java 类;它是内建于 JVM中的,它会加载 java.lang.ClassLoader以及其它的 Java平台类。当 JVM 启动时,一块特殊的机器码会运行,他会加载扩展类加载器与系统类加载器,这块特殊的机器码就是启动类加载器(Bootstrap)

启动类加载器是特定于平台的机器指令,它负责开启整个加载过程;

启动类加载器加载的内容:1. 拓展类加载器和系统类加载器;2. 负责加载供JRE正常运行所需要的基本组件。这包括 java.util 与 java.lang包中的类等等

启动类加载器并不是 Java类,而其它的加载器则都是 Java 类;

扩展(Extension)类加载器:
它的父加载器为根类加载器。它从 java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的 jre\lib\ext子目录下加载类库。如果把用户创建的 JAR文件放在这个目录下,也会自动由扩展类加载器加载。扩展类加载器是纯 Java 类,是 java.lang.ClassLoader类的子类。

系统(App )类加载器:
也称为应用类加载器,它的父加载器为扩展类加载器。它从环境变量 classpath 或者系统属性 java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。系统类加载器是纯 Java类,是 java.lang.ClassLoader类的子类。

2、类加载器的父亲委托机制

在父亲委托机制中,哥哥加载器按照父子关系形成了逻辑上的 树形结构,除了根类加载器之外,其余的类加载器都有且只有一个父加载器。
在这里插入图片描述
结合上面的图片,用比较通俗的话来解释以下类加载器的父亲委托机制:

程序中自定义的类加载器 loader1 想要加载类 Sample,这的时候,loader1并不会直接就进行加载的动作,而是将该动作委托给自己的父亲–系统类加载器,而系统类加载器也有自己的父亲,所以系统类加载器会将该动作委托给拓展类加载器,直到根类加载器。

但是,根类加载器它负责加载虚拟机的额核心类库,如 java.lang.* 等 ,Sample类不在它的加载范围之内,因此根类加载器会将该任务再次返回给拓展类加载器,但Sample类不在拓展类加载器的加载范围之内,因此将该任务再返回给系统类加载器,系统类加载器从环境变量 classpath 或者系统属性 java.class.path 所指定的目录中加载类。因此Sample类由系统类加载器真正的完成加载,并将加载结果反馈给自定义加载器 loader1,最终完成Sample类的加载动作。

若有一个类加载器能够成功加载 Test类,那么这个类加载器被称为 定义类加载器。所有能成功返回 Class对象引用的类加载器 (包括定义类加载器) 都被称为 初始类加载器;

调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化;

2.1 类加载器的双亲委托机制的优点

一、可以确保Java核心库的类型安全
所有的 Java 应用都至少会引用 java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载到 Java 虚拟机中;

如果这个加载过程是由 Java 应用自己的类加载所完成的,那么很可能就会在 JVM 中存在多个版本的 java.lang.Object类。而且这些类之间还是不兼容的,相互不可见的(这是命名空间在发挥着作用);

借助于双亲委托机制,Java 核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了 Java 应用所使用的都是同一个版本的 Java 核心类库,他们之间是相互兼容的;

二、可以确保Java核心类库所提供的类不会被自定义的类所替代

三、不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间
相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载他们即可。不同类加载器所加载的类之间是不兼容的,这就相当于在 Java虚拟机内部创建了一个又一个相互隔离的 Java类空间,这类技术在很多框架中都得到了实际应用;

3、命名空间

每个类加载器都有自己的命名空间, 命名空间由该加载器及所有父加载器所加载的类组成.
在同一个命名空间中,不会出现类的完整名字(包括类的包名) 相同的两个类.
在不同的命名空间中,有可能会出现类的完整名字(包括类的包名) 相同的两个类.

关于命名空间的重要说明
子加载器所加载的类能够访问到父加载器所加载的类,反之不成立!

3.1、不同类加载器的命名空间关系(p22最后一个例子)

1. 同一个命名空间内的类是相互可见的;

2. 子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。反之不成立!如:系统类加载器加载的类能看见根类加载器加载的类;

3. 如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见;

4、线程上下文类加载器

在学习线程上下文类加载器之前我们先看另外一个 当前类加载器
当前类加载器(Current Classloader)
每个类都会使用自己的类加载器(即加载自身的类加载器) 来去加载其他类(指的是所依赖的类)。
例如:
假设 ClassX 引用了 ClassY , 那么 ClassX 的类加载器就回去加载 ClassY(前提是 ClassY 尚未被加载)

下面看下 线程上下文类加载器
线程上下文类加载器(Context Classloader)
线程上下文类加载器是从 JDK1.2 开始引入的,类 Thread 中的 getContextClassLoader()setContextClassLoader(ClassLoader cls)分别用来获取和设置上下文类加载器。
如果没有通过 setContextClassLoader(ClassLoader cls)进行设置的话,线程将继承其父线程的上下文类加载器。Java运行时的初始线程的上下文类加载器都是系统类加载器,在线程中运行的代码可以通过该类加载器来加载类与资源。

Launcher类 的源码中,在得到 系统类加载器 之后会将 系统类加载器 立马设置为 线程上下文类加载器
在这里插入图片描述

线程上下文类加载器的 重要性:

SPI (Service Provider Interface): 服务提供者接口
SPI 的设计逻辑中,父 ClassLoader 可以使用当前线程 Thread.currentThread().getContextClassLoader()所指定的 classloader加载的类。这就改变了父 ClassLoader不能使用子 ClassLoader或是其他没有直接父子关系的 ClassLoader 加载的类的情况,即改变了双亲委托模型。

线程上下文类加载器就是当前线程的 Current Classloader
在双亲委托模型下,类加载器是由下至上的,即下层的类加载器会委托上层进行加载,但是对于 SPI 来说,有些接口是 Java 核心库所提供的,而 Java核心库是由启动类加载器来加载的。而这些接口的实现确实来自不同的 Jar包(厂商提供),Java的启动类加载器是不会加载其他来源的 Jar包的,这样传统的双亲委托模型就无法满足 SPI的要求,而通过给当前线程设置上下文加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。

ContextClassLoader 的作用就是为了破坏Java的类加载器委托机制;

当高层提供了统一的接口让低层去实现,同时又要在高层加载(或实例化)低层的类时,就必须要通过线程上下文类加载器来帮助高层的 ClassLoader找到并加载该类;

4.1、线程上下文类加载器的一般使用模式

大致步骤分三步:获取 - 使用 - 还原

写一个伪代码来解释一下:

	//获取
	ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
	//使用
	try{
		//TargetContextClassLoader 该类加载器是你之前通过某种方式已经得到的(接下来你希望使用该加载器加载类)
		Thread.currentThread().setContextClassLoader(targetContextClassLoader);
		//myMethod()里面调用了Thread.currentThread().getContextClassLoader(),获取当前线程上下文类加载器做某些事情
		myMethod();
	} finally {
		// 还原
		Thread.currentThread().setContextClassLoader(classLoader);
	}

类的卸载

由 Java 虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。Java 虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用他们所加载的类的 Class 对象,因此这些 Class 对象始终是可触及的。

用户自定义的类加载器所加载的类是可以被卸载的。

当某个类 MySample 被加载、连接和初始化后,它的生命周期就开始了。当代表 MySample 类的 Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,MySample 类在方法区内的数据也会被卸载,从而结束 MySample 类的生命周期。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值