JVM类加载理解(线程上下文类加载器、Tomcat类加载器)

 2023-09-15 阅读 21 评论 0

摘要:类加载机制概念 Java虚拟机把描述类的class文件加载到内存,对其进行校验、转换解析、初始化等操作,最终得到可以被虚拟机直接使用的java类型,这就是虚拟机的加载机制。 主要有五个步骤: 加载 将class文件读入到内存中,并将其放在运行时数据

类加载机制概念

Java虚拟机把描述类的class文件加载到内存,对其进行校验、转换解析、初始化等操作,最终得到可以被虚拟机直接使用的java类型,这就是虚拟机的加载机制。
在这里插入图片描述
主要有五个步骤:

  1. 加载
    将class文件读入到内存中,并将其放在运行时数据区的方法区内,然后在堆中创建一个java.lang.Class对象,用来封装在方法区的数据结构。
    在这个阶段,主要完成如下三件事:

    1. 通过一个类的全限定名获取此类的二进制字节流(Class文件)。而获取的方式可以通过jar、war、zip、网络等方式
    2. 将字节流静态存储结构转换为方法区(永久代、元空间)的内部数据结构
    3. 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据访问入口。其中Class对象没有规定是在堆内存中,它比较特殊,存在方法区中
  2. 检测线程上下文注入?验证:验证加载后的类格式、语义、字节码、符号引用(判断符号是否存在)

  3. 准备(分配内存空间):为类的静态变量在方法去分配内存并赋默认值(0或null)。其中静态常量在这个阶段就赋程序设定的值,比如static final int = 666;

  4. 解析:将类的二进制数据中的符号引用转为直接引用

  5. tomcat线程模型?类的初始化:把类加载到系统中,这个阶段才是真正执行java代码。主要工作是为静态变量赋程序设定的初值

双亲委派

在这里插入图片描述
关于双亲委派介绍网上资料很多,包括自定义类加载器、线程上下文类加载器等,这里就不做详细赘述了

线程上下文类加载器(双亲委派破坏者)

java中存在很多服务的提供者接口(Service Provider Interface,SPI),这些接口允许第三方为他们提供实现,然后进行载入。常见的SPI有JDBC、JNDI等,接口属于java的核心库,一般存储与rt.jar中,有Bootstrap加载器加载。

spring自定义应用上下文。在这里插入图片描述

为什么需要线程上下文类加载器?

如果按照双亲委派的原则,我们该如何去加载到我们所实现的SPI呢,这就涉及到了线程上下文加载器(jdk 1.2开始引入),它是通过破坏双亲委派然后使Bootstrap加载器来反向委托线程上下文类加载器进行加载SPI实现类。

如何使用

初始化线程的上下文类加载器是系统类加载器(AppClassLoader),我们可以通过java.lang.Thread类中的getContextClassLoader()和setContextClassLoader(ClassLoader cl)方法进行获取或设置

源码解析

使用jdbc.jar为例来说明上下文类加载器是如何发现并加载实现类的

找到rt.jar中的java.sql.DriverManager类

public class DriverManager {...static {loadInitialDrivers();println("JDBC DriverManager initialized");}/*** 加载并初始化driver*/private static void loadInitialDrivers() {AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {// 加载外部的实现类,如com.mysql.cj.jdbc.DriverServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);try{// 这里执行hasNext()会初进行加载while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}return null;}});
}

在DriveManager类初始化时执行了loadInitialDrivers()方法,然后通过ServiceLoader.load(Driver.class)去加载外部实现的驱动类,配置位置固定为:META-INF/services
在这里插入图片描述
接下来我们看下ServiceLoader.load(Driver.class)方法

    public static <S> ServiceLoader<S> load(Class<S> service) {// 获取上下文类加载器ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);}// load方法public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){// 调用静态方法load会实例化一个ServiceLoader对象return new ServiceLoader<>(service, loader);}private ServiceLoader(Class<S> svc, ClassLoader cl) {service = Objects.requireNonNull(svc, "Service interface cannot be null");// 如果没有定义上下文类加载器则使用默认的系统系统加载器loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;reload();}

在ServiceLoader构造中会判断类加载器并保存到变量loader中,我们来看下最终加载实现类的方法:

        private S nextService() {if (!hasNextService())throw new NoSuchElementException();String cn = nextName;nextName = null;Class<?> c = null;try {// 使用cn类加载器加载指定的servicec = Class.forName(cn, false, loader);} catch (ClassNotFoundException x) {fail(service,"Provider " + cn + " not found");}if (!service.isAssignableFrom(c)) {fail(service,"Provider " + cn  + " not a subtype");}try {// 类型转换S p = service.cast(c.newInstance());providers.put(cn, p);return p;} catch (Throwable x) {fail(service,"Provider " + cn + " could not be instantiated",x);}throw new Error();          // This cannot happen}

可以看到使用类的全限定名+loader加载了实现类com.mysql.cj.jdbc.Driver

加载META-INF/services的过程:

  1. 实现延迟服务提供者查找
    DriverManager.loadInitialDrivers --> ServiceL oader.load --> reload() --> lookuplterator = new Lazylterator(service, loader);
  2. 加载META-INF/services,初始化驱动
    loadedDrivers.iterator() --> driverslterator.hasNext() -->hasNextService --> ClassLoader.getSystemResources(fullName); --> driversIterator.next() --> nextService() --> Class.forName(fullName, false, loader)

Tomcat类加载器结构

参考《Tomcat架构解析》

Tomcat作为一个容器,需要解决下面几个问题:

  1. Java类库可以实现相互隔离:因为我们在一个tomcat中会部署多个web应用,他们可能依赖同一个第三方库,不能要求所有类都只有一份,需要保证他们各自的类库要可以独立使用,不相互影响
  2. Java类库可以相互共享:上一个隔离问题的延申,比如我们部署10个Spring应用,那么90%的类库是冗余的,这时我们希望把他们进行类库共享以节约资源
  3. 不受部署web应用程序的影响:部署的web有着不确定性,可能那一天就崩溃了,所以,基于安全考虑,容器所使用的类库应该与应用程序的类库相互独立

Tomcat类加载架构如下图:
在这里插入图片描述

它破坏了双亲委派,每个类加载器作用如下:

  1. Common:以System为父 类加载器,是位于Tomcat应用服务器顶层的公用类加载器。其路径为common.loader,默认指向$CATALINA_ HOME/ib下的包。
  2. Catalina:以Common为父加载器,是用于加载Tomcat应用服务器的类加载器,其路径为server.loader,默认为空。此时Tomcat使用Common类加载器加载应用服务器。
  3. Shared:以Common为父加载器,是所有Web应用的父加载器,其路径为shared.loader,默认为空。此时Tomcat使用Common类加载器作为Web应用的父加载器。
  4. Web应用:以Shared为父加载器,加载/WEB-INF/classes目录下的未压缩的Class和资源文件以及/WEB-INF/lib目录下的Jar包。如前所述,该类加载器只对当前Web应用可见,对其他Web应用均不可见。
  5. JSP:顾名思义,就是专门加载jsp文件的加载器

我们平时的web应用类加载器默认加载顺序为:

1. 先从缓存中加载
2. 如果没有,则从JVM的Bootstrap类加载器加载
3. 如果没有,则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib的顺序)
4. 如果没有,则从父类加载器加载,加载顺序是AppClassLoader、Common、Shared


参考

  • 《Tomcat架构解析.刘光瑞》2.4节
  • 《Tomcat权威指南》

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/3/60274.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息