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

类加载器

类加载器用来把类加载到 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 类的生命周期。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值