类加载器ClassLoader角色

class file(在下图中就是Car.class文件)存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
class file加载到JVM中,被称为DNA元数据模板(在下图中就是内存中的Car Class),放在方法区。
在.class文件–>JVM–>最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。
image.png

类加载分类

  • 显式加载
    • 显式加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。
  • 隐式加载
    • 隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。
      1. //隐式加载
      2. User user=new User();
      3. //显式加载,并初始化
      4. Class clazz=Class.forName("com.test.java.User");
      5. //显式加载,但不初始化
      6. ClassLoader.getSystemClassLoader().loadClass("com.test.java.Parent");

      类加载器的分类

  1. JVM严格来讲支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
  2. 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
  3. 无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示,上3个

image.png
在Java虚拟机规范里,这Extension ClassLoaderApplication ClassLoader两个称为自定义类加载器。因为他俩都继承了ClassLoader抽象类。 :::danger 这个图不是代表继承的关系,是包含关系 ::: image.pngimage.png

  1. public class ClassLoaderTest {
  2. public static void main(String[] args) {
  3. //获取系统类加载器
  4. ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
  5. System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
  6. //获取其上层:扩展类加载器
  7. ClassLoader extClassLoader = systemClassLoader.getParent();
  8. System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
  9. //获取其上层:获取不到引导类加载器
  10. ClassLoader bootstrapClassLoader = extClassLoader.getParent();
  11. System.out.println(bootstrapClassLoader);//null
  12. //对于用户自定义类来说:默认使用系统类加载器进行加载
  13. ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
  14. System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
  15. //String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
  16. ClassLoader classLoader1 = String.class.getClassLoader();
  17. System.out.println(classLoader1);//null
  18. }
  19. }
  • 我们尝试获取引导类加载器,获取到的值为 null ,这并不代表引导类加载器不存在,因为引导类加载器右 C/C++ 语言,我们获取不到
  • 两次获取系统类加载器的值都相同:sun.misc.Launcher$AppClassLoader@18b4aac2 ,这说明系统类加载器是全局唯一的

虚拟机自带的加载器

引导类加载器

启动类加载器(引导类加载器,Bootstrap ClassLoader)

  1. 这个类加载使用C/C++语言实现的,嵌套在JVM内部
  2. 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
  3. 并不继承自java.lang.ClassLoader,没有父加载器
  4. 加载扩展类和应用程序类加载器,并作为他们的父类加载器
  5. 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

    扩展类加载器

    扩展类加载器(Extension ClassLoader)

  6. Java语言编写,由sun.misc.Launcher$ExtClassLoader实现

  7. 派生于ClassLoader类
  8. 父类加载器为启动类加载器
  9. 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载

    系统类加载器

    应用程序类加载器(也称为系统类加载器,AppClassLoader)

  10. Java语言编写,由sun.misc.Launchers$AppClassLoader实现

  11. 派生于ClassLoader类
  12. 父类加载器为扩展类加载器
  13. 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  14. 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
  15. 通过classLoader.getSystemclassLoader()方法可以获取到该类加载器

    代码测试启动类加载器和扩展加载器

    1. public class ClassLoaderTest1 {
    2. public static void main(String[] args) {
    3. System.out.println("**********启动类加载器**************");
    4. //获取BootstrapClassLoader能够加载的api的路径
    5. URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
    6. for (URL element : urLs) {
    7. System.out.println(element.toExternalForm());
    8. }
    9. //从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
    10. ClassLoader classLoader = Provider.class.getClassLoader();
    11. System.out.println(classLoader);//null
    12. System.out.println("***********扩展类加载器*************");
    13. String extDirs = System.getProperty("java.ext.dirs");
    14. for (String path : extDirs.split(";")) {
    15. System.out.println(path);
    16. }
    17. //从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
    18. ClassLoader classLoader1 = CurveDB.class.getClassLoader();
    19. System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d
    20. }
    21. }

    输出

    1. **********启动类加载器**************
    2. file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/resources.jar
    3. file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/rt.jar
    4. file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/sunrsasign.jar
    5. file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/jsse.jar
    6. file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/jce.jar
    7. file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/charsets.jar
    8. file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/jfr.jar
    9. file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/classes
    10. null
    11. ***********扩展类加载器*************
    12. C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext
    13. C:\WINDOWS\Sun\Java\lib\ext
    14. sun.misc.Launcher$ExtClassLoader@4b67cf4d

    用户自定义类加载器

    在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。那为什么还需要自定义类加载器?

  16. 隔离加载类(比如说我假设现在Spring框架,和RocketMQ有包名路径完全一样的类,类名也一样,这个时候类就冲突了。不过一般的主流框架和中间件都会自定义类加载器,实现不同的框架,中间价之间是隔离的)

  17. 修改类加载的方式
  18. 扩展加载源(还可以考虑从数据库中加载类,路由器等等不同的地方)
  19. 防止源码泄漏(对字节码文件进行解密,自己用的时候通过自定义类加载器来对其进行解密)

    关于ClassLoader

    ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
方法名称 描述
getParent() 返回该类加载器的超类加载器
loadClass(String name) 加载名称为name的类,返回结果为java.lang.Class类的实例。如果找不到类,则返回 ClassNotFoundException异常。该方法中的逻辑就是双亲委派模式的实现。
findClass(String name) 查找名称为name的类,返回结果为java.lang.Class类的实例。这是一个受保护的方法,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委托机制,该方法会在检查完父类加载器之后被loadClass()方法调用。
- 在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类。但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。
- 需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的。
findLoadedClass(String name) 查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例。这个方法是final方法,无法被修改。
defineClass(String name,byte[] b,int off,int len) 把字节数组b中的内容转换成一个Java类,返回结果为java.lang.Class类的实例
resolveClass(Class<?> c) 连接指定的一个Java类。使用该方法可以使用类的Class对象创建完成的同时也被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。

sun.misc.Launcher它是一个java虚拟机的入口应用

获取ClassLoader

方式一:获取当前类的ClassLoader clazz.getClassLoader()
方式二:获取当前线程上下文的ClassLoader Thread.currentThread().getContextClassLoader()
方式三:获取系统的ClassLoader ClassLoader.getSystemClassLoader()
方式四:获取调用者的ClassLoader DriverManager.getCallerClassLoader()

双亲委派机制

双亲委派机制原理

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。
而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
  4. 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常。

image.png

双亲委派机制演示

例子1:两个java.lang.String类

String类实在java.lang包下的,是java的核心类库。突发奇想我自己建一个java.lang.String类,会怎样?

  1. package java.lang;
  2. public class String {
  3. static{
  4. System.out.println("我是自定义的String类的静态代码块");
  5. }
  6. }

在另外的程序中加载 String 类,看看加载的 String 类是 JDK 自带的 String 类,还是我们自己编写的 String 类

  1. public class StringTest {
  2. public static void main(String[] args) {
  3. java.lang.String str = new java.lang.String();
  4. System.out.println("========分割线=========");
  5. StringTest test = new StringTest();
  6. System.out.println(test.getClass().getClassLoader());
  7. }
  8. }

打印结果如下
image.png
并没有输出自己写的String里面的静态代码块的内容,所以断定这个String是java自带的String。
要加载String这个类了,先交给系统加载器,系统加载器不忙加载,委托给父类加载器,扩展加载器,扩展加载器也不忙加载,又继续委托给启动类加载器。启动类加载器一看自己就是顶层加载器,试着加载一下,String这个类在java包下面的,启动类加载器可以加载,就直接加载了java自带的String,加载成功就返回。所以自己写的这个String直接被无视了。

例子2:自定义String类里面添加方法

我们改一下自己写的String类的代码,加一个main方法,运行

  1. package java.lang;
  2. public class String {
  3. static{
  4. System.out.println("我是自定义的String类的静态代码块");
  5. }
  6. //错误: 在类 java.lang.String 中找不到 main 方法
  7. public static void main(String[] args) {
  8. System.out.println("hello,String");
  9. }
  10. }

会报错,原因如上,最终递归到了启动类加载器,启动类加载器加载了String,不过加载的是java自带的String,这个String没有main方法,所以运行main方法直接报错。
image.png

例子3:同系统包名下添加自定义类

我们不写同类名,在同包名下建一个自定义的类,运行

  1. package java.lang;
  2. public class ShkStart {
  3. public static void main(String[] args) {
  4. System.out.println("hello!");
  5. }
  6. }

报错,即使类名没有重复,也禁止使用java.lang这种包名。这是一种保护机制,防止用启动类加载器加载这些乱七八糟的东西把启动类加载器搞挂了。
image.png

例子4:加载器各加载各的

当我们加载jdbc.jar 用于实现数据库连接的时候

  1. 我们现在程序中需要用到SPI接口,而SPI接口属于rt.jar包中Java核心api
  2. 然后使用双亲委派机制,引导类加载器把rt.jar包加载进来,而rt.jar包中的SPI存在一些接口,接口我们就需要具体的实现类了
  3. 具体的实现类就涉及到了某些第三方的jar包了,比如我们加载SPI的实现类jdbc.jar包【首先我们需要知道的是 jdbc.jar是基于SPI接口进行实现的】
  4. 第三方的jar包中的类属于系统类加载器来加载
  5. 从这里面就可以看到SPI核心接口由引导类加载器来加载,SPI具体实现类由系统类加载器来加载

image.png

双亲委派机制的优势

  1. 避免类的重复加载
  2. 保护程序安全,防止核心API被随意篡改

    破坏双亲委派机制

    双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。
    在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,直到Java模块化出现为止,双亲委派模型主要出现过3次较大规模“被破坏”的情况。
    具体的用到了再查询。

    沙箱安全机制

  3. 如上双亲委派机制例子2,自定义String类时:在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java.lang.String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。

  4. 这样可以保证对java核心源代码的保护,这就是沙箱安全机制。

    如何判断两个class对象是否相同?

    在JVM中表示两个class对象是否为同一个类存在两个必要条件:

  5. 类的完整类名必须一致,包括包名

  6. 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
  7. 换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的

    对类加载器的引用

  8. JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的

  9. 如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中
  10. 当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的