JVM 类加载器机制(下)

简介: 本文主要是讲述 JVM 类加载过程和 JVM 提供的集中类加载器以及双亲委派机制,通过 Tomcat 的类加载机制阐述如何打破双亲委派机制的方法。

双亲委派机制


什么是双亲委派机制?


一个类加载器收到了类加载的请求, 它首先不会自己去尝试自己去加载这个类,而是吧这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(即搜索范围中没有找到所需的类)时,子加载器才会尝试自己完成加载。

类加载和双亲委派模型如下图所示


image.png


我们再来看看 ClassLoader 类的 loadClass 方法


// loadClass
protected Class<?> loadClass(String name, boolean resolve)
  throws ClassNotFoundException
{
  synchronized (getClassLoadingLock(name)) {
    // 首先检查当前类是否被加载
    Class<?> c = findLoadedClass(name);
    if (c == null) {
      long t0 = System.nanoTime();
      try {
        if (parent != null) {
          // 如果父类类加载器不为空,先尝试父类加载来加载
          c = parent.loadClass(name, false);
        } else {
          // 引导类加载器尝试加载
          c = findBootstrapClassOrNull(name);
        }
      } catch (ClassNotFoundException e) {
        // ClassNotFoundException thrown if class not found
        // from the non-null parent class loader
      }
      if (c == null) {
        // If still not found, then invoke findClass in order
        // to find the class.
        long t1 = System.nanoTime();
        // 尝试自己加载 
        c = findClass(name);
        // this is the defining class loader; record the stats
        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
        sun.misc.PerfCounter.getFindClasses().increment();
      }
    }
    if (resolve) {
      resolveClass(c);
    }
    return c;
  }
}
// 类加载器的包含关系
public abstract class ClassLoader {
    private static native void registerNatives();
    static {
        registerNatives();
    }
    // 当前 ClassLoader 和 parent ClassLoader 的包含关系
    private final ClassLoader parent;
}


总结:


  1. 不是树形结构(只是逻辑树形结构),而是包含/包装关系。


  1. 加载顺序,应用类加载器,拓展加载器,系统加载器。


  1. 如果有一个类加载器能够成功加载 Test 类,那么这个类加载器被称为定义类加载器,所有可能返回 Class 对象引用的类加载器(包括定义类加载器)都被称为初始类加载器。

设计双亲委派机制的目的?


  1. 保证 Java 核心库的类型安全:所有的java 应用都会至少引用 java.lang.Object 类, 也就是说在运行期, java.lang.Object 的这个类会被加载到 Java 虚拟机中,如果这个加载过程是由 Java 应用自己的类加载器所完成的,那么很有可能会在 JVM 中存在多个版本的 java.lang.Object 类,而且这些类之间还是不兼容的。互不可见的(正是命名空间发挥着作用)借助于双亲委托机制,Java 核心库中的类加载工作都是由启动类加载器统一来完成的。从而确保了Java 应用所使用的都是同一个版本的 Java 核心类库,他们之间是相互兼容的。


  1. 可以确保 Java 核心库所提供的类不会被自定义的类所替代。


  1. 不同的类加载器可以为相同类(binary name)的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要不同的类加载器来加载他们即可,不同的类加载器的类之间是不兼容的,这相当于在JAVA虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中得到了实际运用。


自定义类加载器


自定义类加载器加载类,下面是一个简单的 Demo


import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class ClassLoaderTest extends ClassLoader {
    private static String rxRootPath;
    static {
        rxRootPath = "/temp/class/";
    }
    @Override
    public Class findClass(String name) {
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }
    /**
     * 读取 .class 文件为字节数组
     *
     * @param name 全路径类名
     * @return
     */
    private byte[] loadClassData(String name) {
        try {
            String filePath = fullClassName2FilePath(name);
            InputStream is = new FileInputStream(new File(filePath));
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            byte[] buf = new byte[2048];
            int r;
            while ((r = is.read(buf)) != -1) {
                bos.write(buf, 0, r);
            }
            return bos.toByteArray();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return null;
    }
    /**
     * 全限定名转换为文件路径
     *
     * @param name
     * @return
     */
    private String fullClassName2FilePath(String name) {
        return rxRootPath + name.replace(".", "//") + ".class";
    }
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoaderTest classLoader = new ClassLoaderTest();
        String className = "com.test.TestAA";
        Class clazz = classLoader.loadClass(className);
        System.out.println(clazz.getClassLoader());
        // 输出结果 
        //cn.xxx.xxx.loader.ClassLoaderTest@3764951d
    }
}


Tomcat 类加载器


Tomcat 中的类加载器模型

image.png


Tomcat 类加载器说明


tomcat 的几个主要类加载器:


  • commonLoader: Tomcat 最基本的类加载器, 加载路径中的 class 可以被 Tomcat 容器本身以及各个 WebApp 访问。


  • catalinaLoader:Tomcat 容器私有的类加载器 加载路径中的 class 对于 Webapp 不可见;


  • sharaLoader:  各个Webapp 共享的类加载器, 加载路径中的 class 对于所有 webapp 可见, 但是对于 Tomcat 容器不可见。


  • webappLoader:  各个 Webapp 私有的类加载, 加载路径中的 class 只对当前 webapp 可见, 比如加载 war 包里面相关的类,每个 war 包应用都有自己的 webappClassLoader 对象,对应不同的命名空间,实现相互隔离,比如 war 包中可以引入不同的 spring 版本,实现多个 spring 版本 应用的同时运行。


总结:


从图中的委派关系中可以看出:


Commonclassloader 能加载的类都可以被 Catalinaclassloader和 Sharedclassloadert 使用, 从而实现了公有类库的共用,而Catalinaclassloader 和 Sharedclassloader自己能加载的类则与对方相互隔离 Webappclassloader 可以使用 Shared Loader 加载到的类,但各个 Webappclassloader 实例之间相互隔离而 Jasper Loader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 . class 文件,它出现的目的就是为了被丢弃: 当 Web 容器检测到 JSP 文件被修改时,会替换掉目前的 Jasperloader 的实例,并通过再建立一个新的 JSP 类加载器来实现 JSP 文件的热加载功能。


Tomcat 这种类加载机制打破了Java 推荐的双亲委派模型了吗? 答案是: 打破了

Tomcat 不是这样实现, Tomcat 为了实现隔离性, 没有遵守这个约定, 每个 webapp Loader加载自己的目录下的 class 文件, 不会传递给父类加载器,打破了双亲委派机制


参考资料


  1. 《深入理解 Java 虚拟机》 第三版 周志明


  1. Apache Tomcat Documentation


相关文章
|
2月前
|
存储 缓存 Java
金石原创 |【JVM盲点补漏系列】「并发编程的难题和挑战」深入理解JMM及JVM内存模型知识体系机制(1)
金石原创 |【JVM盲点补漏系列】「并发编程的难题和挑战」深入理解JMM及JVM内存模型知识体系机制(1)
42 1
|
5天前
|
前端开发 安全 Java
深入浅出JVM(八)之类加载器
深入浅出JVM(八)之类加载器
|
8天前
|
Java 程序员 Python
JVM的垃圾回收机制(GC机制)
Java的JVM实行自动垃圾回收机制(GC),主要针对堆中的对象。当对象无引用可达时,被视为垃圾。垃圾回收包含“找垃圾”和“回收垃圾”两步。找垃圾通过引用计数(非Java使用)和可达性分析(Java使用)来识别无用对象。可达性分析从根对象开始遍历,未被标记的对象视为垃圾。回收垃圾常用标记清除方法,但可能导致内存碎片。此过程消耗资源,且碎片化影响内存分配效率。
13 1
|
18天前
|
前端开发 Java 开发者
JVM类加载器的分类以及双亲委派机制
JVM类加载器的分类以及双亲委派机制
|
28天前
|
监控 前端开发 安全
JVM工作原理与实战(十四):JDK9及之后的类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了JDK8及之前的类加载器、JDK9及之后的类加载器等内容。
27 2
|
28天前
|
监控 Java 关系型数据库
JVM工作原理与实战(十三):打破双亲委派机制-线程上下文类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了打破双亲委派机制的方法、线程上下文类加载器等内容。
21 2
|
28天前
|
监控 安全 前端开发
JVM工作原理与实战(十二):打破双亲委派机制-自定义类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了打破双亲委派机制的方法、自定义类加载器等内容。
19 1
|
28天前
|
监控 前端开发 安全
JVM工作原理与实战(十一):双亲委派机制
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了双亲委派机制、父类加载器、双亲委派机制的主要作用、双亲委派机制常见问题等内容。
14 1
|
28天前
|
监控 安全 Java
JVM工作原理与实战(十):类加载器-Java类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了扩展类加载器、通过扩展类加载器去加载用户jar包、应用程序类加载器等内容。
29 4
|
28天前
|
监控 安全 Java
JVM工作原理与实战(九):类加载器-启动类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了启动类加载器、通过启动类加载器去加载用户jar包等内容。
32 8