第一篇:深入理解JVM类加载机制

在这篇专栏文章中,我们将深入探讨Java虚拟机(JVM)的奥秘。JVM是Java语言的核心组件,负责将Java字节码转换为特定计算机硬件能理解的本地机器代码。通过深入了解JVM,我们可以更好地理解Java应用程序的运行机制和性能调优,为Java开发者提供有价值的指导。通过本专栏的学习,您将掌握JVM的核心原理和优化技巧,从而成为一名更加出色的Java开发者。让我们一起,走进JVM的世界,探索其深邃奥妙!

文章目录

  • 深入理解JVM类加载机制
    • (重点源码)为什么Launcher类至关重要?
    • 类加载器初始化过程
    • Jdk为我们提供的类加载器有哪些?
    • (重点源码)什么是双亲委派机制?
    • 类加载运行全过程
    • 如何自定义类加载器?
    • 如何实现打破双亲委派机制?
    • (重点)Tomcat是如何打破双亲委派机制的呢?

深入理解JVM类加载机制

(重点源码)为什么Launcher类至关重要?

当我们使用java命令启动Java应用程序时,例如java MyApp,实际上是执行了JVM中的main函数。main函数位于JVM的启动模块,它首先使用引导类加载器加载核心类库,然后调用Launcher类的main方法启动Java应用程序。

Launcher类位于sun.misc包中,是Java应用程序启动器的核心组件。它负责加载和启动Java应用程序,以及设置类加载器(如系统类加载器和扩展类加载器)。

在Launcher构造方法中,确实创建了两个类加载器:sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。当JVM启动时,它会创建一个Launcher实例,这个实例负责配置和初始化类加载器。在构造方法中,首先创建扩展类加载器,然后创建应用类加载器。应用类加载器的父加载器被设置为扩展类加载器,这意味着在加载类时,应用类加载器会首先尝试使用扩展类加载器加载类。
如下源码:

//Launcher的构造方法
public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        //构造扩展类加载器,在构造的过程中将其父加载器设置为null
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        //构造应用类加载器,在构造的过程中将其父加载器设置为ExtClassLoader,
        //Launcher的loader属性值是AppClassLoader,我们一般都是用这个类加载器来加载我们自己写的应用程序
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }

    Thread.currentThread().setContextClassLoader(this.loader);
    String var2 = System.getProperty("java.security.manager");
    //省略一些不需关注代码

}

这段代码是一个构造方法,用于创建一个名为Launcher的对象。Launcher类的主要目的是创建两个类加载器:一个扩展类加载器(ExtClassLoader)和一个应用类加载器(AppClassLoader)。这两个类加载器用于加载Java程序的类。

  1. 首先,代码通过调用Launcher.ExtClassLoader.getExtClassLoader()方法创建一个ExtClassLoader实例。在创建过程中,其父加载器被设置为null。如果在此过程中出现任何I/O异常,代码将抛出一个InternalError异常。
  2. 接下来,代码通过调用Launcher.AppClassLoader.getAppClassLoader(var1)方法创建一个AppClassLoader实例,其中var1是之前创建的ExtClassLoader实例。在创建过程中,其父加载器被设置为ExtClassLoader。这意味着应用程序类加载器将首先尝试使用扩展类加载器加载类。同样,如果在此过程中出现任何I/O异常,代码将抛出一个InternalError异常。
  3. 然后,代码将当前线程的上下文类加载器设置为应用程序类加载器。这将允许线程上下文类加载器加载应用程序的类。
  4. 之后,代码获取了名为java.security.manager的系统属性,并存储在变量var2中。这个属性通常用于指定Java安全管理器的类名,以便为应用程序提供安全策略。这部分代码被省略了,因为它不是关注的重点。

总之,这个构造方法用于创建一个Launcher对象,它负责创建和配置两个类加载器(扩展类加载器和应用类加载器)。这些类加载器用于加载Java程序的类。在创建过程中,应用类加载器的父加载器被设置为扩展类加载器,以确保按照正确的顺序加载类。

Launcher类的主要作用有以下几点:

  • 初始化类加载器:Launcher类通过嵌套类ExtClassLoader和AppClassLoader分别实现了扩展类加载器和应用类加载器。Launcher类的构造方法会依次创建这两个类加载器,并设置它们的父类加载器。扩展类加载器的父类加载器是引导类加载器(负责加载Java核心类库),而应用类加载器的父类加载器是扩展类加载器。
  • 设置系统属性:Launcher类会设置一些与类加载器相关的系统属性,如java.class.path、java.ext.dirs和java.endorsed.dirs等。这些属性将作为类加载器加载类文件时的搜索路径。
  • 启动应用程序:Launcher类的主要功能是通过main方法加载并启动Java应用程序。main方法首先创建一个Launcher实例,然后使用AppClassLoader加载用户指定的主类。接着,main方法通过Java反射API找到主类的main方法,并将命令行参数传递给该方法,从而启动应用程序。
  • URLClassPath:Launcher类内部使用了sun.misc.URLClassPath类,该类负责处理类和资源的查找,可以从文件系统和网络加载类文件。URLClassPath会根据系统属性中的类路径、扩展路径和启动类路径等设置搜索路径,以便在运行时查找类和资源。

类加载器初始化过程

以下是详细的类加载器初始化过程:

  1. JVM启动:当我们使用java命令启动Java应用程序时,例如java MyApp,JVM会首先加载核心类库。这是由C/C++实现的引导类加载器(Bootstrap ClassLoader)完成的。引导类加载器主要加载位于$JAVA_HOME/jre/lib目录下的核心类库。
  2. 加载Launcher类:JVM在加载核心类库后,会调用sun.misc.Launcher类的main方法来启动Java应用程序。Launcher类位于sun.misc包中,不属于核心类库,因此需要由系统类加载器(System ClassLoader)加载。
  3. 创建Launcher实例:在Launcher类的main方法中,首先会创建一个Launcher实例。Launcher类的构造方法负责创建扩展类加载器(ExtClassLoader)和应用类加载器(AppClassLoader)。
  4. 创建扩展类加载器(ExtClassLoader):Launcher类通过嵌套类ExtClassLoader实现扩展类加载器。在Launcher类的构造方法中,会调用ExtClassLoader.getExtClassLoader()方法创建扩展类加载器实例。扩展类加载器负责加载$JAVA_HOME/jre/lib/ext目录(或由系统属性java.ext.dirs指定的目录)下的扩展类库。它的父类加载器是引导类加载器。
  5. 创建应用类加载器(AppClassLoader):Launcher类通过嵌套类AppClassLoader实现应用类加载器。在Launcher类的构造方法中,会调用AppClassLoader.getAppClassLoader(extcl)方法创建应用类加载器实例,其中extcl是步骤4中创建的扩展类加载器实例。应用类加载器负责加载应用程序类路径(-cp或-classpath指定)中的类。它的父类加载器是扩展类加载器。
  6. 设置系统类加载器:在Launcher类的main方法中,会调用System.setProperty(“java.system.class.loader”,
    loader.getClass().getName())方法将系统类加载器设置为应用类加载器。这样,Java应用程序可以通过ClassLoader.getSystemClassLoader()方法获取系统类加载器的实例。

Jdk为我们提供的类加载器有哪些?

JDK为我们提供了以下三种主要的类加载器:

  1. 引导类加载器(Bootstrap ClassLoader):引导类加载器是用C/C++实现的,负责加载JVM核心类库,如java.lang.*、java.util.*等。它是最顶层的类加载器,主要加载$JAVA_HOME/jre/lib目录下的核心类库。引导类加载器并不是Java类,所以无法在Java代码中直接引用。
  2. 扩展类加载器(Extension ClassLoader):扩展类加载器是Java实现的,继承自URLClassLoader。它的作用是加载$JAVA_HOME/jre/lib/ext目录(或由系统属性java.ext.dirs指定的目录)下的扩展类库。扩展类加载器的父类加载器是引导类加载器。
  3. 系统类加载器(System ClassLoader):也称为应用类加载器(Application ClassLoader),同样是Java实现的,继承自URLClassLoader。它负责加载应用程序类路径(-cp或-classpath指定)中的类。系统类加载器的父类加载器是扩展类加载器。在Java应用程序中,可以通过ClassLoader.getSystemClassLoader()方法获取系统类加载器的实例。

除了这三种主要的类加载器,JDK还允许我们自定义类加载器。自定义类加载器通常需要继承 java.lang.ClassLoader类,重写findClass方法以实现特定的类加载逻辑。自定义类加载器可以用于实现热部署、隔离不同应用程序的类加载等场景

(重点源码)什么是双亲委派机制?

双亲委派机制(Parent Delegation Model)是Java类加载器在加载类时遵循的一种工作原则。这种机制可以确保Java类的唯一性和安全性。双亲委派机制的核心思想是:当一个类加载器收到类加载请求时,它首先不会自己尝试加载这个类,而是将请求委派给其父类加载器。这个过程会一直向上委派,直到达到引导类加载器。只有当父类加载器无法完成类加载请求时,当前类加载器才会尝试自己加载该类。

双亲委派机制的具体工作过程如下:

  1. 当一个类加载器收到类加载请求时,首先检查该类是否已经加载。如果已经加载,直接返回已加载的类;否则,进入下一步。
  2. 请求被委派给父类加载器。这个过程会一直递归向上,直到达到引导类加载器。每个类加载器在尝试加载类之前,都会先将请求委派给它的父类加载器。
  3. 从引导类加载器开始,逐级尝试加载请求的类。如果某个类加载器能够加载该类,那么加载过程成功完成;否则,继续委派给下一个子类加载器。
  4. 如果所有的父类加载器都无法加载请求的类,那么最初收到请求的类加载器会尝试自己加载该类。如果仍然无法加载,将抛出ClassNotFoundException异常。

我们来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法

//ClassLoader的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();
                //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
                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;
    }
}

这段代码展示了ClassLoader类中的loadClass方法。该方法负责根据给定的类名加载类,并实现了双亲委派机制。双亲委派机制是Java类加载器中的一种策略,用于确保类按照正确的顺序被加载,避免重复加载以及安全问题。

方法loadClass的工作原理如下:

  1. 首先,使用findLoadedClass(name)方法检查当前类加载器是否已经加载了指定名称的类。如果找到了已加载的类,则直接返回。
  2. 如果当前类加载器尚未加载该类,则尝试委托其父类加载器(如果存在)加载该类。这可以通过调用parent.loadClass(name, false)方法实现。
  3. 如果父类加载器为空,表明当前类加载器是系统类加载器,会尝试使用引导类加载器加载该类。这通过调用findBootstrapClassOrNull(name)方法实现。
  4. 如果到目前为止仍未找到该类,则调用findClass(name)方法来查找并加载该类。这将在类加载器的类路径中查找并加载指定名称的类。
  5. 在加载过程中,会记录一些性能相关的统计数据,例如父类委派时间和查找类时间等。
  6. 如果resolve参数为true,则会调用resolveClass©方法来解析类。但是,实际上,在这个方法中,resolve参数通常为false,因此这部分代码通常不会执行。

双亲委派机制的优点包括:

  • 类的唯一性:由于类加载请求总是首先委派给最顶层的引导类加载器,这确保了同一个类只会被加载一次,避免了类的重复加载。
  • 安全性:双亲委派机制有助于防止恶意代码篡改核心类库。由于核心类库总是由引导类加载器加载,恶意代码无法通过自定义类加载器替换核心类库的实现。

需要注意的是,双亲委派机制并不是强制性的。自定义类加载器可以选择不遵循双亲委派机制,根据实际需求实现特定的类加载逻辑。但在大多数情况下,遵循双亲委派机制是有益的。

类加载运行全过程

Java类加载过程可以分为五个阶段:加载、验证、准备、解析和初始化。下面我们详细描述每个阶段的工作内容:

  1. 加载:类加载器负责从文件系统、网络或其他来源读取Java字节码文件,并将其映射为JVM内部的数据结构(主要是常量池、字段表和方法表)。类加载器的常见实现有系统类加载器、扩展类加载器和应用类加载器。开发者还可以通过继承java.lang.ClassLoader类实现自定义的类加载器。
  2. 验证:在加载阶段完成后,JVM需要对字节码文件进行验证,确保其符合Java语言规范并且不会对运行时环境造成危害。验证过程包括文件格式验证、元数据验证、字节码验证和符号引用验证等。
  3. 准备:在验证阶段通过后,JVM为类的静态变量分配内存,并为其设置初始值(通常为零值)。注意,这一阶段并不会执行任何Java代码,只负责为静态变量分配内存空间。
  4. 解析:解析阶段主要将常量池中的符号引用替换为直接引用,这涉及到类或接口的解析、字段解析、类方法解析和接口方法解析等。解析过程可能会触发其他类的加载,但只有在初始化阶段才会触发其他类的初始化。
  5. 初始化:初始化阶段是类加载过程的最后一个阶段,主要执行类构造器(包含静态变量赋值和静态代码块的执行)和接口初始化。初始化是按照代码编写顺序自上而下执行的,只有在类首次主动使用时才会触发初始化。

类加载完成后,JVM会将其放入方法区,并在堆内存中创建一个java.lang.Class对象,用于表示该类的元数据信息。接下来,JVM通过执行引擎执行该类的字节码,实现程序的运行。

在整个类加载运行过程中,JVM采用了双亲委派模型来保证类的唯一性。当类加载器收到类加载请求时,会先将请求委托给父类加载器,直至委派到根类加载器;若父类加载器无法加载该类,则尝试由当前类加载器加载。这种模式避免了类的重复加载,同时保证了Java核心类库的安全性。

如何自定义类加载器?

自定义类加载器主要包括以下几个步骤:

1、继承java.lang.ClassLoader类:自定义类加载器需要继承ClassLoader类,这是Java中所有类加载器的基类。

public class MyClassLoader extends ClassLoader {
    // ...
}

2、重写findClass方法:findClass方法是ClassLoader类中的一个受保护方法,用于在类加载器的类路径下查找并加载指定的类。自定义类加载器需要重写这个方法以实现特定的类加载逻辑。在findClass方法中,可以使用defineClass方法将字节码转换为Class对象。

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    byte[] classData = getClassData(name);
    if (classData == null) {
        throw new ClassNotFoundException();
    }
    return defineClass(name, classData, 0, classData.length);
}

3、实现类数据的加载:在上面的示例中,我们使用了一个名为getClassData的方法来获取类的字节码数据。这个方法需要根据实际需求进行实现。例如,可以从文件系统、网络、数据库或其他来源加载类数据。

private byte[] getClassData(String className) {
    // 实现类数据的加载,例如从文件系统、网络、数据库等来源加载类数据
    // ...
}

4、使用自定义类加载器:创建自定义类加载器的实例,并使用loadClass方法加载类。

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
    MyClassLoader classLoader = new MyClassLoader();
    Class<?> clazz = classLoader.loadClass("com.example.MyClass");
    Object instance = clazz.newInstance();
}

以上示例展示了如何创建一个简单的自定义类加载器。实际上,自定义类加载器可以根据需求实现更复杂的逻辑,例如实现热部署、隔离不同应用程序的类加载等。

如何实现打破双亲委派机制?

要实现打破双亲委派机制,可以在自定义类加载器中重写loadClass方法,改变类加载的顺序。以下是一个示例:

public class CustomClassLoader extends ClassLoader {

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 首先检查类是否已经加载
        Class<?> clazz = findLoadedClass(name);
        if (clazz != null) {
            return clazz;
        }
        // 尝试使用自定义类加载器加载类
        try {
            clazz = findClass(name);
        } catch (ClassNotFoundException e) {
            // 忽略异常,表示自定义类加载器无法加载该类
        }
        // 如果自定义类加载器无法加载类,委派给父类加载器
        if (clazz == null) {
            clazz = super.loadClass(name);
        }
        return clazz;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, classData, 0, classData.length);
    }

    private byte[] getClassData(String className) {
        // 实现类数据的加载,例如从文件系统、网络、数据库等来源加载类数据
        // ...
    }
}

在上面的示例中,我们重写了loadClass方法,首先尝试使用自定义类加载器加载类,如果无法加载,再委派给父类加载器。这样,我们就实现了打破双亲委派机制的自定义类加载器。

需要注意的是,打破双亲委派机制可能会导致类的重复加载以及安全问题。因此,在实现自定义类加载器时,应该根据实际需求和场景权衡利弊。

(重点)Tomcat是如何打破双亲委派机制的呢?

作为Web容器的Tomcat需要解决的一些关键问题:

  1. 类库隔离:为确保不同应用程序之间的类库相互独立且隔离,Tomcat为每个应用程序提供了一个独立的类加载器。这意味着每个应用程序都有自己的类加载器实例,可以加载其特定版本的类库,而不会干扰其他应用程序。
  2. 类库共享:对于部署在同一Web容器中的相同类库,可以将这些类库放在Tomcat的共享目录(如$CATALINA_BASE/lib)中。这样,这些类库可以被所有部署在同一Tomcat实例下的应用程序共享。这有助于减少内存消耗和重复加载。
  3. 容器类库与应用程序类库隔离:为了防止容器类库与应用程序类库发生冲突和混淆,Tomcat采用了一种分层的类加载器结构。其中,一个类加载器负责加载Tomcat容器的类库,而独立的类加载器负责加载部署的应用程序及其依赖的类库。这样可以确保容器类库与应用程序类库之间的隔离。
  4. 支持JSP修改:为了在修改JSP文件后无需重启容器即可看到更新的结果,Tomcat提供了一个特殊的类加载器,负责加载和管理JSP文件。当JSP文件被修改时,这个类加载器会检测到变更,然后重新编译JSP文件并将其转换为Java类文件。接着,新编译的类文件将替换原先的版本,并在应用程序中生效。这个过程不需要重启容器,从而实现了对JSP修改的动态支持。

通过解决这些问题,Tomcat作为一个Web容器能够提供良好的应用程序部署环境,确保类库之间的隔离、共享、容器与应用程序之间的隔离以及对JSP文件的动态修改支持。这些特性使得Tomcat能够满足多样化的应用程序需求,并确保在部署和运行Web应用程序时的稳定性和安全性。

以下这张图是Tomcat类加载器之间的关系:
第一篇:深入理解JVM类加载机制_第1张图片
以下是一个简要的总结:

  1. CommonClassLoader:负责加载公共类库。它加载的类可以被CatalinaClassLoader和SharedClassLoader使用,实现公共类库的共享。
  2. CatalinaClassLoader:负责加载Tomcat容器专用的类库。它可以使用CommonClassLoader加载的类,但与SharedClassLoader加载的类相互隔离。
  3. SharedClassLoader:负责加载各个Web应用程序共享的类库。它可以使用CommonClassLoader加载的类,但与CatalinaClassLoader加载的类相互隔离。
  4. WebAppClassLoader:负责加载各个Web应用程序私有的类库。它可以使用SharedClassLoader加载的类,但各个WebAppClassLoader实例之间相互隔离,以确保应用程序之间的独立性。
  5. JasperLoader:负责加载JSP文件编译后生成的Java类文件。它的加载范围仅限于当前JSP文件编译生成的类文件。当检测到JSP文件被修改时,会丢弃当前的JasperLoader实例,并创建一个新的实例来实现JSP文件的热加载功能。

Tomcat在某种程度上打破了双亲委派模型以实现Web应用程序之间的隔离性。在标准的双亲委派模型中,类加载器首先会委托给其父加载器来加载类,只有在父加载器无法找到类时,当前类加载器才会尝试加载该类。

然而,在Tomcat中,WebappClassLoader采用了一种被称为"child first"(子优先)策略。在这种策略下,WebappClassLoader首先尝试加载其自己的目录下的类(如WEB-INF/lib和WEB-INF/classes目录),而不是直接委托给父加载器。这样做的原因是为了确保Web应用程序能够使用其自己的类库版本,而不是使用父加载器提供的版本。这在处理不同Web应用程序使用不同版本的类库时非常重要,例如,不同的应用程序可能使用不同版本的Spring框架。

当WebappClassLoader无法在自己的目录下找到类时,它会将请求传递给父加载器(如SharedClassLoader或CommonClassLoader)以加载类。这样,在保持应用程序之间的隔离性的同时,仍然允许它们共享一些通用的类库。

总之,Tomcat通过修改双亲委派机制来实现Web应用程序之间的隔离。这使得每个应用程序可以使用自己的类库版本,同时仍然能够共享公共类库。虽然这与标准的双亲委派模型有所不同,但它在处理多个部署在同一容器中的Web应用程序时非常有效。

Tomcat主要使用了两个自定义类加载器:WebappClassLoader和StandardClassLoader。

  • WebappClassLoader:这是Tomcat中最重要的类加载器,负责加载Web应用程序中的类和资源。WebappClassLoader首先尝试加载Web应用程序的类,然后再委派给父类加载器。这样做的目的是让Web应用程序可以使用自己的类库,而不受容器的类库影响。这也有助于实现Web应用程序之间的类库隔离。
  • StandardClassLoader:这是一个辅助类加载器,主要用于加载Tomcat自身的类库。StandardClassLoader的行为类似于系统类加载器,遵循双亲委派机制。这个类加载器已经被废弃,在较新的Tomcat版本中已经不存在。在早期的Tomcat版本中,它负责加载共享类库,如$CATALINA_BASE/lib目录下的类库。在新版本的Tomcat中,这个功能已经被SharedClassLoader和CommonClassLoader取代。

以下是Tomcat如何打破双亲委派机制的示例:

在WebappClassLoader中,重写loadClass方法:

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 首先检查类是否已经加载
    Class<?> clazz = findLoadedClass(name);
    if (clazz != null) {
        return clazz;
    }

    // 尝试使用WebappClassLoader加载类
    try {
        clazz = findClass(name);
    } catch (ClassNotFoundException e) {
        // 忽略异常,表示WebappClassLoader无法加载该类
    }

    // 如果WebappClassLoader无法加载类,委派给父类加载器
    if (clazz == null) {
        clazz = getParent().loadClass(name);
    }

    return clazz;
}

在上面的示例中,WebappClassLoader首先尝试加载Web应用程序的类,然后再委派给父类加载器。这样,Tomcat就实现了打破双亲委派机制。

需要注意的是,Tomcat打破双亲委派机制的目的是为了实现Web应用程序之间的类库隔离,以及让Web应用程序可以使用自己的类库。在实际使用中,应该根据实际需求和场景权衡利弊。

模拟实现Tomcat的webappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离:

public class MyClassLoaderTest {
    static class MyClassLoader extends ClassLoader {
        private String classPath;

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

        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;

        }

        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

        protected Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    
                    try {
        				c= findClass(name);
   					} catch (ClassNotFoundException e) {
       				 	// 忽略异常,表示WebappClassLoader无法加载该类
   					}

                    //非自定义的类还是走双亲委派加载
                    if (c == null){
                        c = this.getParent().loadClass(name);
                    }

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    }

    public static void main(String args[]) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class clazz = classLoader.loadClass("com.wzr.jvm.Stu1");
        Object obj = clazz.newInstance();
        Method method= clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader());
        
        System.out.println();
        MyClassLoader classLoader1 = new MyClassLoader("D:/test1");
        Class clazz1 = classLoader1.loadClass("com.wzr.jvm.Stu1");
        Object obj1 = clazz1.newInstance();
        Method method1= clazz1.getDeclaredMethod("sout", null);
        method1.invoke(obj1, null);
        System.out.println(clazz1.getClassLoader());
    }
}

这个示例代码展示了一个自定义的类加载器MyClassLoader,它继承了ClassLoader。这个类加载器可以从指定的classPath目录加载类。MyClassLoader中的loadClass方法进行了重写,这样就可以实现自定义的加载逻辑,而不是直接委派给双亲加载器。
在main方法中,创建了两个不同的MyClassLoader实例,分别指定了D:/test和D:/test1路径。然后,使用这两个类加载器加载并实例化com.wzr.jvm.Stu1类,并调用sout方法。最后,打印了每个实例的类加载器。

你可能感兴趣的:(深入学习JVM,jvm,java,后端)