JVM 支持的类加载器是分成两类的:
1、引导类加载器(BootstrapClassLoader)
2、自定义类加载器(User-Defined ClassLoader)
但在上面图示中,我们发现了加载阶段涉及:BootstrapClassLoader、ExtClassLoader、ApplicationClassLoader 所以这个是不是觉得有点矛盾呢?
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是 Java虚拟机规范却没有这么定义
而是将所有 派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器(也就是继承于 ClassLoader 的类加载器)
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示:
此外,我们还可以在代码里面去获取到一些类加载器:
- /**
- * Date: 2022/10/26 20:23
- * Github: https://github.com/chenjjiaa
- */
- public class ClassLoaderTest {
- public static void main(String[] args) {
- // 获取系统类加载器
- ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
- System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
-
- // 获取上层
- ClassLoader extClassLoader = systemClassLoader.getParent();
- System.out.println(extClassLoader); // sun.misc.Launcher$ExtClassLoader@1b6d3586
-
- // 试图获取上层,会发现获取不到引导类加载器
- ClassLoader bootstrapClassLoader = extClassLoader.getParent();
- System.out.println(bootstrapClassLoader); // null
-
- // 用户自定义的类是使用哪个ClassLoader?答:AppClassLoader
- ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
- System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
-
- // String类 是通过引导类加载器进行加载的 ----> Java的核心类库都是通过BootstrapClassLoader进行加载的
- ClassLoader classLoader1 = String.class.getClassLoader();
- System.out.println(classLoader1); // null,获取不到,进而证明了String是通过引导类加载器进行加载的
- }
- }
- 复制代码
- /**
- * Date: 2022/10/27 13:11
- * Github: https://github.com/chenjjiaa
- */
- public class BootstrapURLTest {
- public static void main(String[] args) {
- System.out.println("*********** BootstrapClassLoader ***********");
- // 获取BootstrapClassLoader能够加载哪些路径下的jar
- URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
- for (URL urL: urLs) {
- System.out.println(urL);
- }
-
- System.out.println("*********** ExtClassLoader ***********");
- String paths = System.getProperty("java.ext.dirs");
- for (String path : paths.split(";")) {
- System.out.println(path);
- }
- // 从上面的路径中随意选择一个jar包中的,来看看他的类加载器是什么 ----> ExtClassLoader
- ClassLoader classLoader1 = CurveDB.class.getClassLoader();
- System.out.println(classLoader1); // sun.misc.Launcher$ExtClassLoader@4b67cf4d
- }
- }
- 复制代码
在这里不会过多说明,具体在下一个大模块:《字节码与类加载》会详细解读
在 Java 的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式
隔离加载类
在某些框架当中需要使用中间件,中间件和应用模块是隔离的,所以就要把这些类加载到不同的环境当中,确保我们引用的框架、jar包,与中间件使用的 jar包是不冲突的
主要是由于中间件都有自己依赖的 jar包,而在同一个工程里面引入多个框架有可能会出现某些类路径一样、类名相同的情况。在这种情况下就会出现类的冲突。那么此时就要进行类的仲裁。像现在主流的容器类框架都会自定义一些类的加载器,从而实现不同的中间件相互之间是隔离的,避免类的冲突
修改类加载的方式
BootstrapClassLoader 是一定需要的,因为要加载系统所需要的一些核心 api。但除了 BootstrapClassLoader 之外的其他类加载器,那就不一定需要了。可以通过实际需求来引入。所以可以通过自定义类加载器,在需要的时候动态加载
扩展加载源
也就是扩展类加载进 JVM 的来源。一般来说,类的加载可以通过 .zip
、.war
、.jar
获取、通过网络、通过本地系统直接加载。但还有可能要通过一些特殊的途径来加载:比如通过数据库,又或者通过机顶盒,这时我们就要扩展加载源了
防止源码泄漏。因为 Java 文件很容易被反编译,为了防止代码泄露或篡改,可以在字节码文件上加密,然后通过自定义的类加载器进行解密、加载
具体细节在下个大模块里说明
java.lang.ClassLoader
类的方式,实现自己的类加载器,以满足一些特殊的需求loadClass()
方法(这个方式有点复杂,就是需要指定的结构非常多),从而实现自定义的类加载类,但是在 JDK1.2 之后已不再建议用户去覆盖 loadClass ()
方法,而是建议把自定义的类加载逻辑写在 findClass()
方法中findClass()
方法及其获取字节码流的方式,使自定义类加载器编写更加简洁ClassLoader 类是抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器)
sun.misc.Launcher 是一个JVM 的入口应用。扩展类加载器和应用类加载器都是 Launcher 里定义的内部类
方式 | 途径 |
---|---|
方式1:获取当前类ClassLoader | XXX类.class.getClassLoader(); |
方式2:获取当前线程上下文ClassLoader | Thread.currentThread().getContextClassLoader(); |
方式3:获取系统的ClassLoader | ClassLoader.getSystemClassLoader(); |
方式4:获取调用者的ClassLoader (待证实) | DriverManager.getCallerClassLoader(); (待证实) |
至于方式4,现在在代码层面暂时敲不出来,只看到了反射类有涉及
Java 虚拟机对 class file 采用的是按需加载的方式,也就是说,当需要使用该类才会将他的 calss file 加载到内存中生成 class 对象。而且加载某个类的 class file 时,JVM 采取双亲委派模式,即把请求交由父类处理,他是一种任务委派模式
先随便写一个测试类
- public class StringTest {
- public static void main(String[] args) {
- // 这里会不会输出:"这是我们自定义的 String 类" 呢?
- String string = new java.lang.String();
- System.out.println("hello!");
- }
- }
- 复制代码
再在根目录下新建包 java.lang
,在此包下新建一个类,叫 String
- package java.lang;
- public class String {
- static {
- System.out.println("这是我们自定义的 String 类");
- }
- }
- 复制代码
那么此时,StringTest 类中的 main()
方法运行时,会不会打印出自定义 String 里的无参构造器中的内容呢?
答案是:没输出自定义 String 里的无参构造器中的内容
说明了此时使用的 String 还是 Java 核心 API 里面的 String 类
这种机制就蕴含了双亲委派机制。主要是为了防止我们的代码/项目受到恶意攻击(如果没有这个机制,那么这个 String 从网络上加载下来的,可能含有恶意代码,然后让整个项目挂掉)
BootstrapClassLoader 一看这个 String 是 java. 包下的,就说:好,这事儿归我管。 BootstrapClassLoader 能加载哪些包下的类?
在自定义 String 类下面运行 main()
方法
- /**
- * Date: 2022/10/31 22:37
- * Github: https://github.com/chenjjiaa
- */
- public class String {
- static {
- System.out.println("这是我们自定义的 String 类");
- }
- // 此时调用 main 方法会出现问题
- public static void main(String[] args) {
- System.out.println("hello,这是自定义String的main方法~");
- }
- }
-
- /** =================== 报错 ===================
- * 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
- * public static void main(String[] args)
- * 否则 JavaFX 应用程序类必须扩展javafx.application.Application
- */
- 复制代码
这就说明了:使用的 String 是 Bootstrap ClassLoader 加载的 Java 原生 api 中的 String 了,而核心包下的 String 没有 main()
。正好印证了双亲委派机制
例如我们在项目中加载 JDBC 的有关类
当我们加载 jdbc.jar 用于实现数据库连接的时候,首先我们需要知道的是 jdbc.jar 是基于 SPI 接口进行实现的,所以在加载的时候,会进行双亲委派机制,接口是由 BootstrapClassLoader 加载的,而具体接口的第三方实现类是由线程上下文类加载器加载的,也就是我们系统类加载器,来完成 jdbc.jar 的加载
扩展:Java 的 SPI 机制是什么?。SPI 机制的实际应用就是如:JDBC包的加载、SpringBoot 自动装配原理等,作用是极大的减轻了框架之间的耦合度,使得扩展性大大增强,能更加便捷的引入外部包
在自定义的 java.lang
包下新建自定义类,跑一遍
- package java.lang; // 禁止自定义类用 java 核心包的名字
- public class ChenjjiaHello {
- public static void main(String[] args) {
- System.out.println("hello!");
- }
- }
- 复制代码
会出现以下错误:
因为 java.lang
是引导类加载器负责管的,这个类试图用引导类加载器去加载,会失败。也是为了防止核心 API 被篡改,同时防止引导类加载器被破坏
自定义 String 类,但是在加载自定义 String 类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载 JDK 自带的文件(rt.jar包中 java\lang\String.class),报错信息说没有 main 方法,就是因为加载的是 rt.jar 包中的 String 类。这样可以保证对 Java 核心源代码的保护,这就是沙箱安全机制。
类的全限定类名必须一致
加载这两个类的 ClassLoader 必须相同
比如刚刚的两个 String 类:自定义的 String 和 系统自带的 String 不是相同的类。即使他们包名一样,但不是使用同一个 ClassLoader 加载,故不是统一个类
换句话说,在 JVM 中,即使这两个类对象(Class对象)来源同一个 class 文件,被同一个虚拟机所加载,但只要加载它们的 ClassLoader 实例对象不同,那么这两个类对象也是不相等的
JVM 必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。(很正常,通过这个类调用 getClassLoader()
就可以知道)
如果一个类型是由用户类加载器加载的,那么 JVM 会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中
当解析一个类型到另一个类型的引用的时候,JVM 需要保证这两个类型的类加载器是相同的( 动态链接部分讲解 )
Java 程序对类的使用方式分为:主动使用和被动使用
主动使用,又分为七种情况:
Class.forName("com.chenjjia.Test")
java.lang.invoke.MethodHandle
实例的解析结果 REF_getStatic
、REF_putStatic
、REF_invokeStatic
句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用 Java 类的方式都被看作是对类的被动使用,都不会导致类的初始化。