深入浅出JVM(八)之类加载器

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 深入浅出JVM(八)之类加载器

前文已经描述Java源文件经过前端编译器后变成字节码文件,字节码文件通过类加载器的类加载机制在Java虚拟机中生成Class对象

前文深入浅出JVM(六)之前端编译过程与语法糖原理重点描述过编译的过程

前文深入浅出JVM(三)之HotSpot虚拟机类加载机制重点描述过类加载机制的过程

本篇文章将重点聊聊类加载器,围绕类加载器深入浅出的解析类加载器的分类、种类、双亲委派模型以及从源码方面推导出我们的结论

类加载器简介

什么是类加载器?

类加载器通过类的全限定类名进行类加载机制从而生成Class对象

Class对象中包含该类相关类信息,通过Class对象能够使用反射在运行时阶段动态做一些事情

显示加载与隐式加载

类加载器有两种方式进行加载,一种是在代码层面显示的调用,另一种是当程序遇到创建对象等命令时自行判断该类是否进行过加载,未加载就先进行类加载

显示加载:显示调用ClassLoader加载class对象

隐式加载:不显示调用ClassLoader加载class对象(因为虚拟机会在第一次使用到某个类时自动加载这个类)

 //显示类加载  第7章虚拟机类加载机制.User为全限定类名(包名+类名)
 Class.forName("第7章虚拟机类加载机制.User");
             
 //隐式类加载
 new User();    

唯一性与命名空间

判断两个类是否完全相同可能并不是我们自认为理解的那样,类在JVM中的唯一性需要根据类本身和加载它的类加载器

  • 唯一性
  • 所有类都由它本身和加载它的那个类在JVM中确定唯一性
  • 也就是说判断俩个类是否为同一个类时,如果它们的类加载器都不同那肯定不是同一个类
  • 命名空间
  • 每个类加载有自己的命名空间,命名空间由所有父类加载器和该加载器所加载的类组成
  • 同一命名空间中,不存在类完整名相同的俩个类
  • 不同命名空间中,允许存在类完整名相同的俩个类(多个自定义类加载加载同一个类时,会在各个类加载器中生成对应的命名,且它们都不是同一个类)

基本特征

类加载器中有一些基本特性,比如子类加载器可以访问父类加载器所加载的类、父类加载过的类子类不再加载、双亲委派模型等

  • 可见性
  • 子类加载器可以访问父类加载器所加载的类*
  • (命名空间包含父类加载器加载的类)
  • 单一性
  • 因为可见性,所以父类加载器加载过的类,子类加载器不会再加载
  • 同一级的自定义类加载器可能都会加载同一个类,因为它们互不可见
  • 双亲委派模型
  • 由哪个类加载器来进行类加载的一套策略,后续会详细说明

类加载器分类

类加载器可以分成两种,一种是引导类由非Java语言实现的,另一种是由Java语言实现的自定义类加载器

  • 引导类加载器 (c/c++写的Bootstrap ClassLoader)
  • 自定义类加载器:由ClassLoader类派生的类加载器类(包括扩展类,系统类,程序员自定义加载器等)

image.png

系统(应用程序)类加载器和扩展类加载器是Launcher的内部类,它们间接实现了ClassLoader

注意

image.png

平常说的系统(应用程序)类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是启动类加载器,都是"逻辑"上的父类加载器

实际上扩展类加载器和系统(应用程序)类加载器间接继承的ClassLoader中有一个字段parent用来表示自己的逻辑父类加载器

类加载器种类

  • 启动(引导)类加载器
  • Bootstrap Classloader c++编写,无法直接获取
  • 加载核心库<JAVA_HOME>\lib\部分jar包
  • 不继承java.lang.ClassLoader,没有父类加载器
  • 加载扩展类加载器和应用程序类加载器,并指定为它们的父类加载器
  • 扩展类加载器
  • Extension Classloader
  • 加载扩展库<JAVA_HOME>\lib\ext*.jar
  • 间接继承java.lang.ClassLoader,父类加载器为启动类加载器
  • 应用程序(系统)类加载器
  • App(System) Classloader 最常用的加载器
  • 负责加载环境变量classpath或java.class.path指定路径下的类库 ,一般加载我们程序中自定义的类
  • 间接继承java.lang.ClassLoader,父类加载器为扩展类加载器
  • 使用ClassLoader.getSystemClassLoader()获得
  • 自定义类加载器(实现ClassLoader类,重写findClass方法)

通过代码来演示:

 public class TestClassLoader {
     public static void main(String[] args) {
         URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
         /*
         启动类加载器能加载的api路径:
         file:/D:/Environment/jdk1.8.0_191/jre/lib/resources.jar
         file:/D:/Environment/jdk1.8.0_191/jre/lib/rt.jar
         file:/D:/Environment/jdk1.8.0_191/jre/lib/sunrsasign.jar
         file:/D:/Environment/jdk1.8.0_191/jre/lib/jsse.jar
         file:/D:/Environment/jdk1.8.0_191/jre/lib/jce.jar
         file:/D:/Environment/jdk1.8.0_191/jre/lib/charsets.jar
         file:/D:/Environment/jdk1.8.0_191/jre/lib/jfr.jar
         file:/D:/Environment/jdk1.8.0_191/jre/classes
         */
         System.out.println("启动类加载器能加载的api路径:");
         for (URL urL : urLs) {
             System.out.println(urL);
         } 
 ​
         /*
         扩展类加载器能加载的api路径:
         D:\Environment\jdk1.8.0_191\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
         */
         System.out.println("扩展类加载器能加载的api路径:");
         String property = System.getProperty("java.ext.dirs");
         System.out.println(property);
         
         //加载我们自定义类的类加载器是AppClassLoader,它是Launcher的内部类
         ClassLoader appClassLoader = TestClassLoader.class.getClassLoader();
         //sun.misc.Launcher$AppClassLoader@18b4aac2 
         System.out.println(appClassLoader);
         
         //AppClassLoader的上一层加载器是ExtClassLoader,它也是Launcher的内部类
         ClassLoader extClassloader = appClassLoader.getParent();
         //sun.misc.Launcher$ExtClassLoader@511d50c0
         System.out.println(extClassloader);
         
         //实际上是启动类加载器,因为它是c/c++写的,所以显示null
         ClassLoader bootClassloader = extClassloader.getParent();
         //null 
         System.out.println(bootClassloader);
         
         //1号测试:基本类型数组 的类加载器
         int[] ints = new int[10];
         //null 
         System.out.println(ints.getClass().getClassLoader());
         
         //2号测试:系统提供的引用类型数组 的类加载器
         String[] strings = new String[10];
         //null 
         System.out.println(strings.getClass().getClassLoader());
         
         //3号测试:自定义引用类型数组 的类加载器
         TestClassLoader[] testClassLoaderArray = new TestClassLoader[10];
         //sun.misc.Launcher$AppClassLoader@18b4aac2       
         System.out.println(testClassLoaderArray.getClass().getClassLoader());
 ​
         //4号测试:线程上下文的类加载器
         //sun.misc.Launcher$AppClassLoader@18b4aac2
         System.out.println(Thread.currentThread().getContextClassLoader());
     }
 }

从上面可以得出结论

  1. 数组类型的类加载器是数组元素的类加载器(通过2号测试与3号测试的对比)
  2. 基本类型不需要类加载 (通过1号测试与3号测试的对比)
  3. 线程上下文类加载器是系统类加载器 (通过4号测试)

关于类加载源码解析

用源码来解释上文结论
  • ClassLoader中的官方注释

image.png

虚拟机自动生成的一个类,管理数组,会对这个类进行类加载

对数组类类加载器是数组元素的类加载器

如果数组元素是基本类型则不会有类加载器

  • 源码解释扩展类加载器的父类是null

image.png

  • 源码解释系统类加载器的父类是扩展类加载器

image.png

  • 源码解释线程上下文类加载器是系统类加载器

image.png

ClassLoader主要方法

loadClass()

ClassLoaderloadClass方法(双亲委派模型的源码)

 public Class<?> loadClass(String name) throws ClassNotFoundException {
     return loadClass(name, false);
 }
                                             //参数resolve:是否要解析类
 protected Class<?> loadClass(String name, boolean resolve)
             throws ClassNotFoundException
     {
        //加锁同步 保证只加载一次
         synchronized (getClassLoadingLock(name)) {
             // 首先检查这个class是否已经加载过了
             Class<?> c = findLoadedClass(name);
             if (c == null) {
                 long t0 = System.nanoTime();
                 try {
                     // c==null表示没有加载,如果有父类的加载器则让父类加载器加载
                     if (parent != null) {
                         c = parent.loadClass(name, false);
                     } else {
                         //如果父类的加载器为空 则说明递归到bootStrapClassloader了
                         //则委托给BootStrap加载器加载
                         //bootStrapClassloader比较特殊无法通过get获取
                         c = findBootstrapClassOrNull(name);
                     }
                 } catch (ClassNotFoundException e) {
                     //父类无法加载抛出异常
                 }
                 //如果父类加载器仍然没有加载过,则尝试自己去加载class
                 if (c == null) {
                     long t1 = System.nanoTime();
                     c = findClass(name);
                     sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                     sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                     sun.misc.PerfCounter.getFindClasses().increment();
                 }
             }
             //是否要解析
             if (resolve) {
                 resolveClass(c);
             }
             return c;
         }
 }

先递归交给父类加载器去加载,父类加载器未加载再由自己加载

findClass()

ClassLoaderfindClass()

     protected Class<?> findClass(String name) throws ClassNotFoundException {
         throw new ClassNotFoundException(name);
     }

由子类URLClassLoader重写findClass去寻找类的规则

image.png

最后都会来到defineClass()方法

defineClass()

 protected final Class<?> defineClass(String name, byte[] b, int off, int len)

根据从off开始长度为len定字节数组b转换为Class实例

在自定义类加载器时,覆盖findClass()编写加载规则,取得要加载的类的字节码后转换为流调用defineClass()生成Class对象

resolveClass()

     protected final void resolveClass(Class<?> c) {
         resolveClass0(c);
     }

使用该方法可以在生成Class对象后,解析类(符号引用 -> 直接引用)

findLoadedClass()

     protected final Class<?> findLoadedClass(String name) {
         if (!checkName(name))
             return null;
         return findLoadedClass0(name);
     }

如果加载过某个类则返回Class对象否则返回null

Class.forName()与ClassLoader.loadClass()区别
  • Class.forName()
  • 传入一个类的全限定名返回一个Class对象
  • 将Class文件加载到内存时会初始化,主动引用
  • ClassLoader.loadClass()
  • 需要class loader对象调用
  • 通过上面的源码分析可以知道,双亲委派模型调用loadClass,只是将Class文件加载到内存,不会初始化和解析,直到这个类第一次使用才进行初始化

双亲委派模型

双亲委派模型源码实现对应ClassLoaderloadClass()

  • 分析:
  1. 先检查这个类是否加载过
  2. 没有加载过,查看父类加载器是否为空,
    如果不为空,就交给父类加载器去加载(递归),
    如果为空,说明已经到启动类加载器了(启动类加载器不能get因为是c++写的)
  3. 如果父类加载器没有加载过,则递归回来自己加载
  • 举例
  1. 假如我现在自己定义一个MyString类,它会自己找(先在系统类加载器中找,然后在扩展类加载器中找,最后去启动类加载器中找,启动类加载器无法加载然后退回扩展类加载器,扩展类加载器无法加载然后退回系统类加载器,然后系统类加载器就完成加载)
  2. 我们都知道Java有java.lang.String这个类
    那我再创建一个java.lang.String运行时,报错

image.png

可是我明明写了main方法
这是因为类装载器的沙箱安全机制
很明显这里的报错是因为它找到的是启动类加载器中的java.lang.String而不是在应用程序类加载器中的java.lang.String(我们写的)
而且核心类库的包名也是被禁止使用的

image.png

  • 类装载器的加载机制:启动类加载器->扩展类加载器->应用程序类加载器
  1. 如果自定义类加载器重写loadClass不使用双亲委派模型是否就能够用自定义类加载器加载核心类库了呢?
    JDK为核心类库提供一层保护机制,不管用什么类加载器最终都会调用defineClass(),该方法会执行preDefineClass(),它提供对JDK核心类库的保护

image.png

  • 优点
  1. 防止重复加载同一个class文件
  2. 保证核心类不能被篡改
  • 缺点
  • 父类加载器无法访问子类加载器
  • 比如系统类中提供一个接口,实现这个接口的实现类需要在系统类加载器加载,而该接口提供静态工厂方法用于返回接口的实现类的实例,但由于启动类加载器无法访问系统类加载器,这时静态工厂方法就无法创建由系统类加载器加载的实例
  • Java虚拟机规范只是建议使用双亲委派模型,不是一定要使用
  • Tomcat中是由自己先去加载,加载失败再由父类加载器去加载

自定义类加载器

  1. 继承ClassLoader
  2. 可以覆写loadClass方法,也可以覆写findClass方法
  • 建议覆写findClass方法,因为loadClass是双亲委派模型实现的方法,其中父类类加载器加载不到时会调用findClass尝试自己加载
  1. 编写好后调用loadClass方法来实现类加载

自定义类加载器代码

public class MyClassLoader extends ClassLoader {

    /**
     * 字节码文件路径
     */
    private final String codeClassPath;

    public MyClassLoader(String codeClassPath) {
        this.codeClassPath = codeClassPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //字节码文件完全路径
        String path = codeClassPath + name + ".class";
        System.out.println(path);

        Class<?> aClass = null;
        try (
                BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path));
                ByteArrayOutputStream baos = new ByteArrayOutputStream()
        ) {
            int len = -1;
            byte[] bytes = new byte[1024];
            while ((len = bis.read(bytes)) != -1) {
                baos.write(bytes,0,len);
            }
            byte[] classCode = baos.toByteArray();
            //用字节码流 创建 Class对象
            aClass = defineClass(null, classCode, 0, classCode.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return aClass;
    }
}

客户端调用自定义类加载器加载类

public class Client {
    public static void main(String[] args) {
        MyClassLoader myClassLoader = new MyClassLoader("C:\");
        try {
            Class<?> classLoader = myClassLoader.loadClass("HotTest");
            System.out.println("类加载器为:" + classLoader.getClassLoader().getClass().getName());
            System.out.println("父类加载器为" + classLoader.getClassLoader().getParent().getClass().getName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

记得对要加载的类先进行编译

image.png

  • 注意:
  • 要加载的类不要放在父类加载器可以加载的目录下
  • 自定义类加载器父类加载器为系统类加载器
  • JVM所有类类加载都使用loadClass

解释如果类加载器不同那么它们肯定不是同一个类

  MyClassLoader myClassLoader1 = new MyClassLoader("D:\代码\JavaVirtualMachineHotSpot\src\main\java\");
        MyClassLoader myClassLoader2 = new MyClassLoader("D:\代码\JavaVirtualMachineHotSpot\src\main\java\");
        try {
            Class<?> aClass1 = myClassLoader1.findClass("HotTest");
            Class<?> aClass2 = myClassLoader2.findClass("HotTest");
            System.out.println(aClass1 == aClass2);//false
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

优点

  • 隔离加载类 (各个中间件jar包中类名可能相同,但自定义类加载器不同)
  • 修改类加载方式
  • 扩展加载源 (可以从网络,数据库中进行加载)
  • 防止源码泄漏 (Java反编译容易,可以编译时进行加密,自定义类加载解码字节码)

热替换

热替换: 服务不中断,修改会立即表现在运行的系统上

对Java来说,如果一个类被类加载器加载过了,就无法被再加载了

但是如果每次加载这个类的类加载不同,那么就可以实现热替换

还是使用上面写好的自定义类加载器

        //测试热替换
        try {
            while (true){
                MyClassLoader myClassLoader = new MyClassLoader("D:\代码\JavaVirtualMachineHotSpot\src\main\java\");
                
                Class<?> aClass = myClassLoader.findClass("HotTest");
                Method hot = aClass.getMethod("hot");
                Object instance = aClass.newInstance();
                Object invoke = hot.invoke(instance);
                TimeUnit.SECONDS.sleep(3);
            }
        } catch (Exception e){
            e.printStackTrace();
        }

通过反射调用HotTest类的hot方法

中途修改hot方法并重新编译

image.png

总结

本篇文章围绕类加载器深入浅出的解析类加载器的分类与种类、双亲委派模型、通过源码解析证实我们的观点、最后还自定义的类加载器和说明热替换

类加载器将字节码文件进行类加载机制生成Class对象从而加载到Java虚拟机中

类加载只会进行一次,能够显示调用执行或者在遇到创建对象的字节码命令时隐式判断是否进行过类加载

类加载器分为非Java语言实现的引导类加载器和Java语言实现的自定义类加载器,其中JDK中实现了自定义类加载器中的扩展类加载器和系统类加载器

引导类加载器用来加载Java的核心类库,它的子类扩展类加载器用来加载扩展类,扩展类的子类系统类加载器常用于加载程序中自定义的类(这里的父子类是逻辑的,并不是代码层面的继承)

双亲委派模型让父类加载器优先进行加载,无法加载再交给子类加载器进行加载;通过双亲委派模型和沙箱安全机制来保护核心类库不被其他恶意代码替代

基本类型不需要类加载、数组类型的类加载器是数组元素的类加载器、线程上下文类加载器是系统类加载器

由于类和类加载器才能确定JVM中的唯一性,每次加载类的类加载不同时就能够多次进行类加载从而实现在运行时修改的热替换

最后

  • 参考资料
  • 《深入理解Java虚拟机》

本篇文章将被收入JVM专栏,觉得不错感兴趣的同学可以收藏专栏哟~

觉得菜菜写的不错,可以点赞、关注支持哟~

有什么问题可以在评论区交流喔~


相关文章
|
2月前
|
安全 前端开发 Java
【JVM的秘密揭秘】深入理解类加载器与双亲委派机制的奥秘!
【8月更文挑战第25天】在Java技术栈中,深入理解JVM类加载机制及其双亲委派模型是至关重要的。JVM类加载器作为运行时系统的关键组件,负责将字节码文件加载至内存并转换为可执行的数据结构。其采用层级结构,包括引导、扩展、应用及用户自定义类加载器,通过双亲委派机制协同工作,确保Java核心库的安全性与稳定性。本文通过解析类加载器的分类、双亲委派机制原理及示例代码,帮助读者全面掌握这一核心概念,为开发更安全高效的Java应用程序奠定基础。
77 0
|
16天前
|
安全 Java 应用服务中间件
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
|
16天前
|
Arthas Java 测试技术
JVM —— 类加载器的分类,双亲委派机制
类加载器的分类,双亲委派机制:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器;JDK8及之前的版本,JDK9之后的版本;什么是双亲委派模型,双亲委派模型的作用,如何打破双亲委派机制
JVM —— 类加载器的分类,双亲委派机制
|
2月前
|
数据库 C# 开发者
WPF开发者必读:揭秘ADO.NET与Entity Framework数据库交互秘籍,轻松实现企业级应用!
【8月更文挑战第31天】在现代软件开发中,WPF 与数据库的交互对于构建企业级应用至关重要。本文介绍了如何利用 ADO.NET 和 Entity Framework 在 WPF 应用中访问和操作数据库。ADO.NET 是 .NET Framework 中用于访问各类数据库(如 SQL Server、MySQL 等)的类库;Entity Framework 则是一种 ORM 框架,支持面向对象的数据操作。文章通过示例展示了如何在 WPF 应用中集成这两种技术,提高开发效率。
41 0
|
2月前
|
开发者 C# Windows
WPF布局大揭秘:掌握布局技巧,轻松创建响应式用户界面,让你的应用程序更上一层楼!
【8月更文挑战第31天】在现代软件开发中,响应式用户界面至关重要。WPF(Windows Presentation Foundation)作为.NET框架的一部分,提供了丰富的布局控件和机制,便于创建可自动调整的UI。本文介绍WPF布局的基础概念与实现方法,包括`StackPanel`、`DockPanel`、`Grid`等控件的使用,并通过示例代码展示如何构建响应式布局。了解这些技巧有助于开发者优化用户体验,适应不同设备和屏幕尺寸。
27 0
|
2月前
|
安全 前端开发 Java
【JVM 探秘】ClassLoader 类加载器:揭秘 Java 类加载机制背后的秘密武器!
【8月更文挑战第25天】本文全面介绍了Java虚拟机(JVM)中的类加载器,它是JVM的核心组件之一,负责将Java类加载到运行环境中。文章首先概述了类加载器的基本工作原理及其遵循的双亲委派模型,确保了核心类库的安全与稳定。接着详细阐述了启动、扩展和应用三种主要类加载器的层次结构。并通过一个自定义类加载器的例子展示了如何从特定目录加载类。此外,还介绍了类加载器的完整生命周期,包括加载、链接和初始化三个阶段。最后强调了类加载器在版本隔离、安全性和灵活性方面的重要作用。深入理解类加载器对于掌握JVM内部机制至关重要。
58 0
|
3月前
|
存储 前端开发 Java
(二)JVM成神路之剖析Java类加载子系统、双亲委派机制及线程上下文类加载器
上篇《初识Java虚拟机》文章中曾提及到:我们所编写的Java代码经过编译之后,会生成对应的class字节码文件,而在程序启动时会通过类加载子系统将这些字节码文件先装载进内存,然后再交由执行引擎执行。本文中则会对Java虚拟机的类加载机制以及执行引擎进行全面分析。
|
3月前
|
存储 安全 Java
开发与运维引用问题之JVM类加载过程如何解决
开发与运维引用问题之JVM类加载过程如何解决
22 0
|
4月前
|
Java 编译器
Java健壮性 Java可移植性 JDK, JRE, JVM三者关系 Java的加载与执行原理 javac编译与JAVA_HOME环境变量介绍 Java中的注释与缩进 main方法的args参数
Java健壮性 Java可移植性 JDK, JRE, JVM三者关系 Java的加载与执行原理 javac编译与JAVA_HOME环境变量介绍 Java中的注释与缩进 main方法的args参数
42 1
|
3月前
|
存储 算法 Java
JAVA程序运行问题之Java类加载到JVM中加载类时,实际上加载的是什么如何解决
JAVA程序运行问题之Java类加载到JVM中加载类时,实际上加载的是什么如何解决