JVM工作原理与实战(十二):打破双亲委派机制-自定义类加载器

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了打破双亲委派机制的方法、自定义类加载器等内容。

一、打破双亲委派机制的方法

双亲委派机制的核心思想是:当一个类加载器接收到加载类的请求时,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器(Bootstrap ClassLoader)中去,只有当父类加载器无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过再由顶向下进行加载。

image.gif

双亲委派机制是Java类加载器的重要特性,但在某些情况下,可能需要打破这种机制。以下是打破双亲委派机制的方法:

  • 自定义类加载器:在Java中,可以通过继承ClassLoader并重写其loadClass方法来创建自定义类加载器。通过这种方式,可以打破双亲委派机制,实现类的隔离。例如,在Tomcat中,每个Web应用都有自己的类加载器,从而实现了应用之间的类隔离。当两个Web应用中有相同限定名的类时,如Servlet类,Tomcat通过自定义类加载器保证它们是不同的类。
  • 线程上下文类加载器:在Java中,每个线程都有一个关联的上下文类加载器。通过设置线程的上下文类加载器,可以实现类的加载。例如,JDBC和JNDI等就是利用线程上下文类加载器来加载类的。
  • Osgi框架的类加载器:Osgi框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载。在Osgi中,每个Bundle都有自己的类加载器,当需要加载类时,会先在自己的存储空间中查找,如果找不到,则委托给父级Bundle的类加载器进行查找。这种机制打破了传统的双亲委派模型。

二、自定义类加载器

1.Tomcat自定义类加载器案例

在Tomcat环境中,一个显著的特点是其能够同时运行多个Web应用。这就引出了一个重要的问题:如果两个应用中存在相同限定名的类,例如Servlet类,那么Tomcat如何保证这两个类能够被正确加载,并且它们实际上是不同的类。

在传统的类加载机制中,双亲委派机制(Parent Delegation Mechanism)是核心。这个机制规定,当一个类加载器收到类加载请求时,它首先不会自己去加载,而是把这个请求委派给父类加载器去执行。这就形成了一个从上到下的“类加载委托层次”。然而,在多Web应用环境下,这种机制可能会导致类加载的问题。比如,当Web应用1中的MyServlet已经被其应用类加载器加载后,由于双亲委派机制的存在,Web应用2中相同限定名的MyServlet类可能就无法被其应用类加载器加载。

image.gif

为了解决这个问题,Tomcat采用了一种自定义类加载器的策略。每个Web应用都有其独立的类加载器,负责加载该应用中的类。这样,即使两个应用中有相同名称的类,由于它们是由不同的类加载器加载的,因此它们实际上是不同的类。

image.gif

在同一个Java虚拟机中也同理,两个自定义类加载器加载相同限定名的类不会冲突,只有相同类加载器和相同的类限定名才会被认为是同一个类。

2.自定义类加载器详解

ClassLoader中包含了4个核心方法,双亲委派机制的核心代码就位于loadClass方法中。

  • loadClass()是类加载的入口,提供了双亲委派机制,内部会调用findClass:
public Class<?> loadClass(String var1)

image.gif

loadClass()源码:

public Class<?> loadClass(String var1) throws ClassNotFoundException {
        return this.loadClass(var1, false);
    }
    protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
        synchronized(this.getClassLoadingLock(var1)) {
            Class var4 = this.findLoadedClass(var1);
            if (var4 == null) {
                long var5 = System.nanoTime();
                try {
                    if (this.parent != null) {
                        var4 = this.parent.loadClass(var1, false);
                    } else {
                        var4 = this.findBootstrapClassOrNull(var1);
                    }
                } catch (ClassNotFoundException var10) {
                }
                if (var4 == null) {
                    long var7 = System.nanoTime();
                    var4 = this.findClass(var1);
                    PerfCounter.getParentDelegationTime().addTime(var7 - var5);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (var2) {
                this.resolveClass(var4);
            }
            return var4;
        }
    }

image.gif

  • findClass()由类加载器的子类实现,其核心功能是获取二进制数据并调用defineClass ,比如URLClassLoader会根据文件路径去获取类文件中的二进制数据。
protected Class<?> findClass(String var1)

image.gif

  • defineClass()会做一些类名的校验,然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中。
protected final Class<?> defineClass(String var1, byte[] var2, int var3, int var4)

image.gif

  • resolveClass()会执行类生命周期中的连接阶段。
protected final void resolveClass(Class<?> var1)

image.gif

loadClass()核心代码解析:

parent等于null说明父类加载器是启动类加载器,直接调用findBootstrapClassOrNull,否则调用父类加载器的加载方法。

if (this.parent != null) {
                        var4 = this.parent.loadClass(var1, false);
                    } else {
                        var4 = this.findBootstrapClassOrNull(var1);
                    }

image.gif

父类加载器无法找到所需的类时,当前类加载器将承担起加载的责任。

if (var4 == null) {
                    ...
                    var4 = this.findClass(var1);
                    ...
                }

image.gif

在实际开发中,为了正确地实现一个自定义类加载器,并确保不破坏双亲委派机制,应当重写findClass()方法。这样的做法确保了类加载请求的正确委派,同时允许开发者根据特定需求定制类加载的行为。

3.案例解析

自定义Test类:

public class Test {
    public static void main(String[] args) {
        System.out.println("自定义Test类");
    }
}

image.gif

将Test类放到相应的目录下:

image.gif

自定义类加载器:

public class BreakClassLoader extends ClassLoader {
    private String basePath;
    private final static String FILE_EXT = ".class";
    public void setBasePath(String basePath) {
        this.basePath = basePath;
    }
    private byte[] loadClassData(String name)  {
        try {
            String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
            FileInputStream fis = new FileInputStream(basePath + tempName + FILE_EXT);
            try {
                return IOUtils.toByteArray(fis);
            } finally {
                IOUtils.closeQuietly(fis);
            }
        } catch (Exception e) {
            System.out.println("自定义类加载器加载失败,错误原因:" + e.getMessage());
            return null;
        }
    }
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if(name.startsWith("java.")){
            return super.loadClass(name);
        }
        byte[] data = loadClassData(name);
        return defineClass(name, data, 0, data.length);
    }
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
        BreakClassLoader classLoader1 = new BreakClassLoader();
        classLoader1.setBasePath("D:\\Test\\com\\rye\\");
        Class<?> aClass = classLoader1.loadClass("Test");
        System.out.println(aClass.getClassLoader());
     }
}

image.gif

运行结果(获取自定义类加载器):

image.gif

需要注意的是加载的类名不能以java.开头,源码解析:

private ProtectionDomain preDefineClass(String var1, ProtectionDomain var2) {
        if (!this.checkName(var1)) {
            throw new NoClassDefFoundError("IllegalName: " + var1);
        } else if (var1 != null && var1.startsWith("java.")) {
            throw new SecurityException("Prohibited package name: " + var1.substring(0, var1.lastIndexOf(46)));
        } else {
            if (var2 == null) {
                var2 = this.defaultDomain;
            }
            if (var1 != null) {
                this.checkCerts(var1, var2.getCodeSource());
            }
            return var2;
        }
    }

image.gif

image.gif

获取自定义类加载器的父类加载器:

public class BreakClassLoader extends ClassLoader {
    private String basePath;
    private final static String FILE_EXT = ".class";
    public void setBasePath(String basePath) {
        this.basePath = basePath;
    }
    private byte[] loadClassData(String name)  {
        try {
            String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
            FileInputStream fis = new FileInputStream(basePath + tempName + FILE_EXT);
            try {
                return IOUtils.toByteArray(fis);
            } finally {
                IOUtils.closeQuietly(fis);
            }
        } catch (Exception e) {
            System.out.println("自定义类加载器加载失败,错误原因:" + e.getMessage());
            return null;
        }
    }
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if(name.startsWith("java.")){
            return super.loadClass(name);
        }
        byte[] data = loadClassData(name);
        return defineClass(name, data, 0, data.length);
    }
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
        BreakClassLoader classLoader1 = new BreakClassLoader();
//        classLoader1.setBasePath("D:\\Test\\com\\rye\\");
//
//        Class<?> aClass = classLoader1.loadClass("Test");
//        System.out.println(aClass.getClassLoader());
        ClassLoader parent = classLoader1.getParent();
        System.out.println(parent);
    }
}

image.gif

运行结果:

image.gif

解析:

image.gif

ClassLoader类中提供了构造方法设置parent的内容(JDK8中):

private ClassLoader(Void var1, ClassLoader var2) {
        this.classes = new Vector();
        this.defaultDomain = new ProtectionDomain(new CodeSource((URL)null, (Certificate[])null), (PermissionCollection)null, this, (Principal[])null);
        this.packages = new HashMap();
        this.nativeLibraries = new Vector();
        this.defaultAssertionStatus = false;
        this.packageAssertionStatus = null;
        this.classAssertionStatus = null;
        this.parent = var2;
        if (ClassLoader.ParallelLoaders.isRegistered(this.getClass())) {
            this.parallelLockMap = new ConcurrentHashMap();
            this.package2certs = new ConcurrentHashMap();
            this.domains = Collections.synchronizedSet(new HashSet());
            this.assertionLock = new Object();
        } else {
            this.parallelLockMap = null;
            this.package2certs = new Hashtable();
            this.domains = new HashSet();
            this.assertionLock = this;
        }
    }

image.gif

这个构造方法由另外一个构造方法调用,其中父类加载器由getSystemClassLoader方法设置,该方法返回的是AppClassLoader。

protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

image.gif

验证getSystemClassLoader方法返回的是AppClassLoader:

public static void main(String[] args){
        System.out.println(getSystemClassLoader());
    }

image.gif

运行结果:

image.gif


总结

JVM是Java程序的运行环境,负责字节码解释、内存管理、安全保障、多线程支持、性能监控和跨平台运行。本文主要介绍了打破双亲委派机制的方法、自定义类加载器等内容,希望对大家有所帮助。

相关文章
|
1月前
|
存储 监控 算法
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程 ?
尼恩提示: G1垃圾回收 原理非常重要, 是面试的重点, 大家一定要好好掌握
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程  ?
|
2月前
|
安全 Java 应用服务中间件
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
|
1月前
|
缓存 前端开发 Java
JVM知识体系学习二:ClassLoader 类加载器、类加载器层次、类过载过程之双亲委派机制、类加载范围、自定义类加载器、编译器、懒加载模式、打破双亲委派机制
这篇文章详细介绍了JVM中ClassLoader的工作原理,包括类加载器的层次结构、双亲委派机制、类加载过程、自定义类加载器的实现,以及如何打破双亲委派机制来实现热部署等功能。
42 3
|
2月前
|
Arthas Java 测试技术
JVM —— 类加载器的分类,双亲委派机制
类加载器的分类,双亲委派机制:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器;JDK8及之前的版本,JDK9之后的版本;什么是双亲委派模型,双亲委派模型的作用,如何打破双亲委派机制
JVM —— 类加载器的分类,双亲委派机制
|
1月前
|
前端开发 Java 应用服务中间件
JVM进阶调优系列(1)类加载器原理一文讲透
本文详细介绍了JVM类加载机制。首先解释了类加载器的概念及其工作原理,接着阐述了四种类型的类加载器:启动类加载器、扩展类加载器、应用类加载器及用户自定义类加载器。文中重点讲解了双亲委派机制,包括其优点和缺点,并探讨了打破这一机制的方法。最后,通过Tomcat的实际应用示例,展示了如何通过自定义类加载器打破双亲委派机制,实现应用间的隔离。
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
7天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
5天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
8 1
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
63 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
存储 缓存 算法
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!