JVM之类加载器

文章目录

版权声明

  • 本博客的内容基于我个人学习黑马程序员课程的学习笔记整理而成。我特此声明,所有版权属于黑马程序员或相关权利人所有。本博客的目的仅为个人学习和交流之用,并非商业用途。
  • 我在整理学习笔记的过程中尽力确保准确性,但无法保证内容的完整性和时效性。本博客的内容可能会随着时间的推移而过时或需要更新。
  • 若您是黑马程序员或相关权利人,如有任何侵犯版权的地方,请您及时联系我,我将立即予以删除或进行必要的修改。
  • 对于其他读者,请在阅读本博客内容时保持遵守相关法律法规和道德准则,谨慎参考,并自行承担因此产生的风险和责任。本博客中的部分观点和意见仅代表我个人,不代表黑马程序员的立场。

类加载器

  • 类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。
  • 类加载器只参与加载过程中的字节码获取并加载到内存这一部分

  • 类加载器的应用场景

类加载器的分类

  • 类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的。
  • 类加载器的设计JDK8和8之后的版本差别较大,JDK8及之前的版本中默认的类加载器有如下几种
  • 类加载器的详细信息可以通过classloader命令查看:
    • classloader - 查看 classloader 的继承树,urls,类加载信息,使用 classloader 去getResource
    • 详细的指令教程请参考Arthas官网
java 复制代码
[arthas@17048]$ classloader
 name                                       numberOfInstances  loadedCountTotal
 BootstrapClassLoader                       1                  2248
 com.taobao.arthas.agent.ArthasClassloader  1                  1351
 sun.misc.Launcher$ExtClassLoader           1                  47
 sun.reflect.DelegatingClassLoader          15                 15
 sun.misc.Launcher$AppClassLoader           1                  8
Affect(row-cnt:5) cost in 7 ms.
[arthas@17048]$ classloader -t
+-BootstrapClassLoader
+-sun.misc.Launcher$ExtClassLoader@7fe21cf8
  +-com.taobao.arthas.agent.ArthasClassloader@53e59711
  +-sun.misc.Launcher$AppClassLoader@18b4aac2
Affect(row-cnt:4) cost in 5 ms.
  • BootstrapClassLoader:c++编写的启动类加载器,位于虚拟机的源码中
    • numberOfInstances :1 类加载器的个数为一个
    • loadedCountTotal: 2248 共加载了2248个核心类
  • ArthasClassloader: ArthasClassloader工具的加载器
  • ExtClassLoader:拓展类加载器
  • DelegatingClassLoader: jdk底层实现用来提升反射效率的加载器
  • AppClassLoader:应用程序类加载器

启动类加载器

  • 启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供的、使用C++编写的类加载器
  • 默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar,resources.jar等。
  • 通过启动类加载器去加载用户jar包
    • 放入jre/lib下进行扩展:不推荐,尽可能不要去更改JDK安装目录中的内容,会出现即时放进去由于文件名不匹配的问题也不会正常地被加载
    • 使用参数进行扩展:推荐,使用-Xbootclasspath/a:jar包目录/jar包名 进行扩展

拓展类加载器&应用程序类加载器

  • 扩展类加载器和应用程序类加载器都是JDK中提供的、使用Java编写的类加载器

  • 它们的源码都位于sun.misc.Launcher中,是一个静态内部类,继承自URLClassLoader。通过目录或者指定jar包将字节码文件加载到内存中。

  • 扩展类加载器(Extension Class Loader)是JDK中提供的、使用Java编写的类加载器

  • 默认加载Java安装目录/jre/lib/ext下的类文件

  • 通过扩展类加载器去加载用户jar包
    • 放入/jre/lib/ext下进行扩展:不推荐,尽可能不要去更改JDK安装目录中的内容
  • 使用参数进行扩展
    • 推荐,使用-Djava.ext.dirs=jar包目录 进行扩展,这种方式会覆盖掉原始目录,可以用;(windows):(macos/linux)追加上原始目录
  • 类加载器的加载路径可以先使用classloader -l查看类的hash值,然后通过classloader --c hash值 查看

双亲委派机制

  • 由于Java虚拟机中有多个类加载器,那么一个类到底由哪个加载器进行加载?
  • 解决方案:双亲委派机制 双亲委派机制的核心是解决一个类到底由谁加载的问题
  • 每个Java实现的类加载器中保存了一个成员变量叫"父"(Parent)类加载器,可以理解为它的上级,并不是继承关系。
  • 启动类加载器使用C++编写,没有上级类加载器
  • 应用程序类加载器的parent父类加载器是扩展类加载器,而扩展类加载器的parent是空。扩展类加载器(Extension ClassLoader)在类加载过程中会委托给启动类加载器(Bootstrap ClassLoader)。这是因为Java的类加载器遵循所谓的"双亲委派模型"。

|-------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| 图1 真实的父类关系 | 图2 双亲委派机制中的逻辑关系 |

  • 尽管在类加载器的层次结构中,扩展类加载器的父类加载器是启动类加载器,但实际上,在类的加载过程中,应用类加载器的父类加载器是扩展类加载器,扩展类加载器没有父类加载器,但会委派给启动类加载器加载。这就是通常说扩展类加载器的父类加载器是启动类加载器。
  • 类加载器的继承关系可以通过classloader --t 查看
  • 双亲委派机制:当一个类加载器收到类加载请求时,它并不会尝试立即加载这个类,而是把这个请求委派给父类加载器。这样,每个类加载请求都会传递到启动类加载器,只有当父类加载器无法处理这个请求时(例如,类不在它的搜索路径中),子类加载器才会尝试自己去加载。

  • 双亲委派机制口诀:自底向上查找是否加载过,再由顶向下进行加载。


解决三个问题

  1. 重复的类:如果一个类重复出现在三个类加载器的加载位置,应该由谁来加载?
    • 启动类加载器加载,根据双亲委派机制,它的优先级是最高的
  2. String类覆盖问题:在自己的项目中去创建一个java.lang.String类,会被加载吗?
    • 不能,会交由启动类加载器加载在rt.jar包中的String类
  3. 类加载器的关系:这几个类加载器彼此之间存在关系吗?
    • 应用类加载器的父类加载器是扩展类加载器,扩展类加载器没有父类加载器,但是会委派给启动类加载器加载

打破双亲委派机制

  • 打破双亲委派机制的三种方式
  1. 自定义类加载器
    • 自定义类加载器并且重写loadClass方法,就可以将双亲委派机制的代码去除
  2. 线程上下文类加载器
    • Tomcat通过这种方式实现应用之间类隔离
  3. Osgi框架的类加载器
    • 历史上Osgi框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载

自定义类加载器

  • 一个Tomcat程序中是可以运行多个Web应用的,如果这两个应用中出现了相同限定名的类,比如Servlet类,Tomcat要保证这两个类都能加载并且它们应该是不同的类。
  • 如果不打破双亲委派机制,当应用类加载器加载Web应用1中的MyServlet之后,Web应用2中相同限定名的MyServlet类就无法被加载。
  • Tomcat使用了自定义类加载器来实现应用之间类的隔离。每一个应用会有一个独立的类加载器加载对应的类。

  1. 先来分析ClassLoader的原理,ClassLoader中包含了4个核心方法
  2. 双亲委派机制的核心代码就位于loadClass方法中
java 复制代码
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return this.loadClass(name, false); //resolve
}

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    //加锁 保证只有一个线程加载类
    synchronized(this.getClassLoadingLock(name)) {
        //查询当前类加载器是否加载过需要的类,true 返回类对象 否则,返回null
        Class<?> c = this.findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
			//向逻辑父类委派加载任务
            try {
                //直到委派到顶级父类------Extension,进入else分支
                if (this.parent != null) {
                    c = this.parent.loadClass(name, false);
                } else {
                    //调用启动类加载器,进行加载,无法加载返回null
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException var10) {
            }
			//所有类加载器加载失败,使用本类加载器进行加载
            if (c == null) {
                long t1 = System.nanoTime();
                c = this.findClass(name);
                PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                PerfCounter.getFindClasses().increment();
            }
        }

        if (resolve) {
            this.resolveClass(c);
        }

        return c;
    }
}

final Class<?> loadClass(Module module, String name) {
    synchronized(this.getClassLoadingLock(name)) {
        Class<?> c = this.findLoadedClass(name);
        if (c == null) {
            c = this.findClass(module.getName(), name);
        }

        return c != null && c.getModule() == module ? c : null;
    }
}
//读取二进制数据
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

protected Class<?> findClass(String moduleName, String name) {
    if (moduleName == null) {
        try {
            return this.findClass(name);
        } catch (ClassNotFoundException var4) {
        }
    }

    return null;
}
//类名校验 将字节码信息加载到虚拟机内存中
protected final Class<?> defineClass(byte[] b, int off, int len) throws ClassFormatError {
    return this.defineClass((String)null, b, off, len, (ProtectionDomain)null);
}

protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError {
    return this.defineClass(name, b, off, len, (ProtectionDomain)null);
}

protected final void resolveClass(Class<?> c) {
    if (c == null) {
        throw new NullPointerException();
    }
}
  • 阅读双亲委派机制的核心代码,分析如何通过自定义的类加载器打破双亲委派机制
  • 打破双亲委派机制的核心就是将下边这一段代码重新实现
java 复制代码
//parent等于null说明父类加载器是启动类加载器,直接调用findBootstrapClassOrNull
//否则调用父类加载器的加载方法
if (parent != null) {
	 c = parent.loadClass(name, false);
 } else {
	 c = findBootstrapClassOrNull(name);
 }
//父类加载器爱莫能助,我来加载!
if (c == null) 
	 c = findClass(name);

案例演示

java 复制代码
public class CustomClassLoader extends ClassLoader {

    public CustomClassLoader(ClassLoader parent) {
        super(parent);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        
        // 简单的例子, 只处理自定义的类
        if (name.startsWith("com.mydomain")) {
            return findClass(name);
        }

        // 其他的类还是采用双亲委派机制加载
        return super.loadClass(name);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] bytes = loadClassData(name);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException("Cannot load class " + name, e);
        }
    }

    private byte[] loadClassData(String name) throws IOException {
        // 根据类名加载类的二进制数据. 假设所有类都在一个固定的目录下
        Path path = Paths.get("classes", name.replace('.', '/') + ".class");
        return Files.readAllBytes(path);
    }

}
  1. 自定义类加载器默认父类是AppClassLoader
  • 以Jdk8为例,ClassLoader类中提供了构造方法设置parent的内容
  • 这个构造方法由另外一个构造方法调用,其中父类加载器由getSystemClassLoader方法设置,该方法返回的是AppClassLoader。
  • 两个自定义类加载器加载相同限定名的类,不会冲突
    • 在同一个Java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类

  • 正确的去实现一个自定义类加载器的方式是重写findClass方法,这样不会破坏双亲委派机制

线程上下文类加载器

  • 利用上下文类加载器加载类,比如JDBC和JNDI等
  • JDBC中使用了DriverManager来管理项目中引入的不同数据库的驱动,比如mysql驱动、oracle驱动。
  1. DriverManager类位于rt.jar包中,由启动类加载器加载

  2. 依赖中的mysql驱动对应的类,由应用程序类加载器来加载。

  • DriverManager属于rt.jar是启动类加载器加载的。而用户jar包中的驱动需要由应用类加载器加载,这就违反了双亲委派机制
  • DriverManage使用SPI机制,最终加载jar包中对应的驱动类
  • SPI中获取到应用程序类加载器
    • SPI中使用了线程上下文中保存的类加载器进行类的加载,这个类加载器一般是应用程序类加载器

案例梳理

⚫1. 启动类加载器加载DriverManager。

⚫ 2. 在初始化DriverManager时,通过SPI机制加载jar包中的myql驱动。

⚫ 3. SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。

  • 由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制


OSGi模块化

  • OSGi模块化框架。它存在同级之间的类加载器的委托加载。OSGi还使用类加载器实现了热部署的功能。
  • 热部署指的是在服务不停止的情况下,动态地更新字节码文件到内存中
相关推荐
ThisIsClark2 小时前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
王佑辉2 小时前
【jvm】内存泄漏与内存溢出的区别
jvm
大G哥4 小时前
深入理解.NET内存回收机制
jvm·.net
泰勒今天不想展开4 小时前
jvm接入prometheus监控
jvm·windows·prometheus
NoneCoder7 小时前
CSS系列(26)-- 动画性能优化详解
前端·css·性能优化
苏三说技术11 小时前
Redis 性能优化的18招
数据库·redis·性能优化
程序猿会指北13 小时前
【鸿蒙(HarmonyOS)性能优化指南】内存分析器Allocation Profiler
性能优化·移动开发·harmonyos·openharmony·arkui·组件化·鸿蒙开发
程序猿会指北16 小时前
【鸿蒙(HarmonyOS)性能优化指南】启动分析工具Launch Profiler
c++·性能优化·harmonyos·openharmony·arkui·启动优化·鸿蒙开发
东阳马生架构1 天前
JVM简介—3.JVM的执行子系统
jvm