06 Java Class Loaders the Mountain Doesn't Turn, Thus It Can Reach Its Height

06 Java Class Loaders- The Mountain Doesn’t Turn, Thus It Can Reach Its Height #

Previously, we learned about Java bytecode. After writing code, it is compiled into bytecode and can be packaged into Jar files.

Then we can let the JVM load the required bytecode and turn it into a Class object on the persistent generation/metadata area, and then execute our program logic.

We can use the Java command to specify the main class to start, or the Jar file. With the predefined mechanism, the JVM will automatically load the corresponding bytecode (which may be a class file or a Jar file).

We know that when a Jar file is opened, it is actually equivalent to a folder that contains many class files and resource files, but for convenience, it is packaged in a zip format. Of course, after decompression, we can still directly execute it using the Java command.

$ java Hello

Alternatively, you can package the Hello.class and other dependencies into a jar file:

Example 1: Archive the class file and Java source file into an archive named hello.jar: jar cvf hello.jar Hello.class Hello.java Example 2: Archive and specify the startup class Hello using the e option: jar cvfe hello.jar Hello Hello.class Hello.java

Then execute the jar file using the -jar option:

$ java -jar hello.jar

Of course, we can also decompress the jar file and run it using the Java command mentioned above.

The first step in running a Java program is to load the class file or the bytecode contained in the input stream.

  1. Class lifecycle and loading process
  2. Class loading timing
  3. Class loading mechanism
  4. Custom class loader example
  5. Some practical techniques
  • How to troubleshoot issues with missing Jar files?
  • How to troubleshoot inconsistencies in class methods?
  • How to see which classes have been loaded and the loading order?
  • How to adjust or modify the ext and local loading paths?
  • How to load additional jar files or classes at runtime?

According to the Java Language Specification and the Java Virtual Machine Specification, we use “Class Loading” to represent the entire process of mapping class/interface names to Class Objects. This process can also be divided into more specific stages: loading, linking, and initialization.

So what exactly happens during the process of loading a class? Let’s take a detailed look.

5.1 Class Lifecycle and Loading Process #

3de64ff2-77de-4468-af3a-c61bbb8cd944.png

A class in the JVM has 7 stages in its lifecycle: loading, verification, preparation, resolution, initialization, usage, and unloading. The first five parts (loading, verification, preparation, resolution, initialization) are collectively called class loading. Now let’s talk about these five processes separately.

  1. Loading: The loading phase can also be called the “loading” phase. The main operation in this phase is to obtain the binary classfile-formatted bytecode stream based on the fully qualified name of the class that is known. In other words, it finds the “class file” located in the file system, jar package, or anywhere else. If the binary representation cannot be found, a NoClassDefFound error will be thrown.

The loading phase does not check the syntax and format of the classfile. The entire process of class loading is mainly completed by the JVM and the Java class loading system together. Of course, the specific loading phase is completed by the JVM in cooperation with a specific class loader (java.lang.ClassLoader).

  1. Verification: The first phase of the linking process is verification, which ensures that the byte stream information in the class file complies with the requirements of the current virtual machine and does not harm the virtual machine’s security.

The verification process checks the semantics of the classfile, judges the symbols in the constant pool, and performs type checking. The main purpose is to determine the bytecode’s legality, such as the magic number and the verification of the version number. During these checks, a VerifyError, ClassFormatError, or UnsupportedClassVersionError may be thrown.

Because the verification of the classfile is part of the linking phase, this process may require loading other classes. During the loading process of a certain class, the JVM must load all its superclasses and interfaces.

If there is a problem with the class hierarchy (for example, the class is its own superclass or interface, which results in an infinite loop), the JVM will throw a ClassCircularityError. And if the implemented interface is not an interface or the declared superclass is an interface, an IncompatibleClassChangeError will be thrown.

  1. Preparation: Then it enters the preparation phase. In this phase, static fields are created and initialized to the default values (null or 0). The method table is also allocated, which means that memory space is allocated in the method area for these variables.

Please note that no Java code is executed during the preparation phase.

For example:

public static int i = 1;

In the preparation phase, the value of i will be initialized to 0. It will be assigned the value 1 during the class initialization phase. However, if final is used as a static constant, the behavior of some JVMs will be different:

public static final int i = 1; Corresponding to the constant i, it will be assigned the value 1 during the preparation phase. In fact, this is still quite puzzling. For example, other languages (such as C#) have a direct constant keyword const, which allows the compiler to replace it with a constant during the compilation phase, similar to a macro instruction, making it simpler.

  1. Resolution: Then it enters the optional resolution of symbolic references stage. This involves resolving the constant pool, including class/interface resolution, field resolution, class method resolution, and interface method resolution.

Simply put, in the code we write, when a variable references an object, this reference is stored as a symbolic reference in the .class file (which is essentially an indexed record).

During the resolution phase, it needs to be resolved and linked as a direct reference (pointing to the actual object). If a direct reference exists, the target of the reference must exist in the heap.

When loading a class, all superclasses and super interfaces need to be loaded.

  1. Initialization: According to the JVM specification, class initialization can only be executed when the class is “actively used” for the first time.

The initialization process includes:

  • Execution of class constructors
  • Assignment of static variables
  • Execution of static code blocks

If a subclass is being initialized, its superclass is initialized first to ensure that the superclass is initialized before the subclass. Therefore, when initializing a class in Java, the java.lang.Object class must be initialized first because all Java classes inherit from java.lang.Object.

As long as we respect the semantics of the language and complete the loading, linking, and initialization steps before executing the next operation, if an error occurs, the class loading system can perform symbol resolution and other linking processes flexibly according to its own strategy. In order to improve performance, HotSpot JVM usually waits until class initialization to load and link classes. Therefore, if class A references class B, loading class A does not necessarily load class B (unless verification is required). Class B will be initialized only when the first instruction that actively uses class B is executed, which requires completing the loading and linking of class B first.

5.2 Timing of Class Loading #

After understanding the class loading process, let’s see when class initialization is triggered. The JVM specification enumerates the following multiple triggering conditions:

  • When the virtual machine is started, initialize the user-specified main class, which is the class where the main method for startup execution is located.
  • When encountering the new instruction used to create an instance of the target class, initialize the target class of the new instruction, that is, initialize the class when creating a new instance.
  • When encountering an instruction to invoke a static method, initialize the class where the static method is located.
  • When encountering an instruction to access a static field, initialize the class where the static field is located.
  • The initialization of a subclass triggers the initialization of its parent class.
  • If an interface defines a default method, initializing a class that implements this interface directly or indirectly will trigger the initialization of the interface.
  • When using reflection API to invoke a class through reflection, similar to the above, whether it is an instance or a static method, it needs to be initialized.
  • When calling a MethodHandle instance for the first time, initialize the class where the MethodHandle points to.

At the same time, the following situations will not trigger class initialization:

  • Accessing a parent class’s static field through a subclass reference only triggers the parent class’s initialization, not the subclass’s.
  • Defining an object array does not trigger the initialization of the class.
  • Constants are stored in the constant pool of the calling class at compile time and do not directly reference the class defining the constant, so the initialization of the class defining the constant will not be triggered.
  • Getting a Class object by class name does not trigger class initialization. Hello.class does not initialize the Hello class.
  • When loading a specified class through Class.forName, if the initialize parameter is set to false, class initialization will not be triggered. This parameter tells the virtual machine whether to initialize the class. Class.forName(“jvm.Hello”) by default loads the Hello class.
  • The default loadClass method of ClassLoader does not trigger initialization (it loads but does not initialize).

Example: Java APIs such as Class.forName(), classLoader.loadClass(), reflection APIs, and JNI_FindClass can all initiate class loading. The JVM itself also performs class loading. For example, core classes such as java.lang.Object and java.lang.Thread are loaded when the JVM starts.

5.3 Class Loader Mechanism #

The class loading process can be described as “obtaining the Class object that describes a class by its fully qualified name a.b.c.XXClass”, which is completed by the “class loader”. The advantage of this is that subclass loaders can reuse classes loaded by parent loaders. The system’s built-in class loaders can be divided into three types:

  • Bootstrap Class Loader
  • Extension Class Loader
  • Application Class Loader

The bootstrap class loader is usually implemented internally by the JVM and cannot be obtained directly through the Java API. However, we can indirectly observe and affect it (as demonstrated later). The latter two class loaders are usually defined in sun.misc.Launcher in Oracle HotSpot JVM. The extension class loader and application class loader usually inherit from the URLClassLoader class, which by default provides methods for loading class bytecode from different sources and converting them into Class objects.

c32f4986-0e72-4268-a90a-7451e1931161.png

  1. Bootstrap Class Loader: It is used to load Java’s core classes and is implemented using native C++ code. It does not inherit from java.lang.ClassLoader (responsible for loading all classes in jre/lib/rt.jar in the JDK). It can be regarded as a built-in loader of the JVM. We cannot directly manipulate it at the code level, so direct manipulation of it is not allowed. If printed, it will be null. For example, java.lang.String is loaded by the bootstrap class loader, so String.class.getClassLoader() will return null. However, it can be influenced by command-line parameters as shown later.
  2. Extension Class Loader: It is responsible for loading classes in the JRE extension directory, lib/ext, or JAR files specified by the java.ext.dirs system property. The parent class loader of the extension class loader is null (because the bootstrap class loader cannot be obtained directly).
  3. Application Class Loader: It is responsible for loading jar packages and classpaths specified by the -classpath or -cp options of the Java command and the java.class.path system property when the JVM starts. In application code, the application class loader can be obtained using the static method getSystemClassLoader() of ClassLoader. If not specified, user-defined classes are loaded by this loader by default, unless custom class loaders are used.

In addition, custom class loaders can also be created. If a user-defined class loader is created, it will have the application class loader as its parent loader. The parent class loader of the application class loader is the extension class loader. These class loaders have a hierarchical relationship, and the bootstrap loader, also known as the root loader, is the parent loader of the extension loader, but its reference cannot be obtained directly from ExClassLoader, and will also return null.

8a806e88-cd41-4a28-b552-76efb0a1fdba.png

The class loading mechanism has three characteristics:

  1. Delegation Model: When a custom class loader needs to load a class, such as java.lang.String, it is lazy and does not immediately try to load it, but delegates to its parent loader to load it. If the parent loader has its own parent loader, it will keep looking for it. In this way, if a higher-level loader, such as the bootstrap class loader, has already loaded a class, such as java.lang.String, all lower-level loaders do not need to load it themselves. If none of the class loaders can load a class with the specified name, a ClassNotFoundException will be thrown.
  2. Dependency Loading: If a loader is loading a class and finds that this class depends on several other classes or interfaces, it will also try to load these dependencies.
  3. Cached Loading: In order to improve loading efficiency and eliminate duplicate loading, once a class is loaded by a class loader, the loading result will be cached and will not be loaded again.

5.4 Custom Class Loader Example #

Meanwhile, we can implement a class loader to load classes in other formats and customize the loading method and data format. As long as the classloader returns a Class instance, it greatly enhances the flexibility of the loader. For example, let’s try implementing a class loader that can handle simple encrypted bytecode to protect our class bytecode files from being directly cracked by users.

Let’s first take a look at a Hello class that we want to load:

package jvm;

public class Hello {
    static {
        System.out.println("Hello Class Initialized!");
    }
}

This Hello class is very simple. It just prints “Hello Class Initialized!” when it is initialized. Suppose this class is very important and we don’t want to give the compiled Hello.class to others, but we still want others to be able to call or execute this class. What should we do? One simple idea is to encrypt the bytecode of this class as a byte stream, and then try to load the encrypted data through a custom class loader. For the sake of simplicity in this demonstration, we use the Base64 algorithm provided by the JDK to encrypt the bytecode into text. In the example below, we implement a HelloClassLoader, which extends the ClassLoader class. However, we want it to restore and execute the logic of printing a string in our Hello class by providing a segment of Base64 string.

package jvm;

import java.util.Base64;

public class HelloClassLoader extends ClassLoader {

    public static void main(String[] args) {
        try {
            new HelloClassLoader().findClass("jvm.Hello").newInstance(); // Load and initialize the Hello class
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        String helloBase64 = "yv66vgAAADQAHwoABgARCQASABMIABQKABUAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2N" +
                "hbFZhcmlhYmxlVGFibGUBAAR0aGlzAQALTGp2bS9IZWxsbzsBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBAApIZWxsby5qYXZhDAAHAAgHABkMABoAGwEAGEhlb" +
                "GxvIENsYXNzIEluaXRpYWxpemVkIQcAHAwAHQAeAQAJanZtL0hlbGxvAQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2" +
                "YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgAhAAUABgAAAAAAAgABAAcACA" +
                "ABAAkAAAAvAAEAAQAAAAUqtwABsQAAAAIACgAAAAYAAQAAAAMACwAAAAwAAQAAAAUADAANAAAACAAOAAgAAQAJAAAAJQACAAAAAAAJsgACEgO2AASxAAAAAQAK" +
                "AAAACgACAAAABgAIAAcAAQAPAAAAAgAQ";

        byte[] bytes = decode(helloBase64);
        return defineClass(name,bytes,0,bytes.length);
    }

    public byte[] decode(String base64){
        return Base64.getDecoder().decode(base64);
    }

}

直接执行这个类:

$ java jvm.HelloClassLoader Hello Class Initialized!

可以看到达到了我们的目的,成功执行了Hello类的代码,但是完全不需要有Hello这个类的class文件。此外,需要说明的是两个没有关系的自定义类加载器之间加载的类是不共享的(只共享父类加载器,兄弟之间不共享),这样就可以实现不同的类型沙箱的隔离性,我们可以用多个类加载器,各自加载同一个类的不同版本,大家可以相互之间不影响彼此,从而在这个基础上可以实现类的动态加载卸载,热插拔的插件机制等,具体信息大家可以参考OSGi等模块化技术。

5.5 一些实用技巧 #

1)如何排查找不到Jar包的问题? #

有时候我们会面临明明已经把某个jar加入到了环境里,可以运行的时候还是找不到。那么我们有没有一种方法,可以直接看到各个类加载器加载了哪些jar,以及把哪些路径加到了classpath里?答案是肯定的,代码如下:

package jvm;

import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;

public class JvmClassLoaderPrintPath {

    public static void main(String[] args) {

        // 启动类加载器
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        System.out.println("启动类加载器");
        for(URL url : urls) {
            System.out.println(" ==> " +url.toExternalForm());
        }

        // 扩展类加载器
        printClassLoader("扩展类加载器", JvmClassLoaderPrintPath.class.getClassLoader().getParent());

        // 应用类加载器
        printClassLoader("应用类加载器", JvmClassLoaderPrintPath.class.getClassLoader());

    }

    public static void printClassLoader(String name, ClassLoader CL){
        if(CL != null) {
            System.out.println(name + " ClassLoader -> " + CL.toString());
            printURLForClassLoader(CL);
        }else{
            System.out.println(name + " ClassLoader -> null");
        }
    }

    public static void printURLForClassLoader(ClassLoader CL){

        Object ucp = insightField(CL,"ucp");
        Object path = insightField(ucp,"path");
        ArrayList ps = (ArrayList) path;
        for (Object p : ps){
            System.out.println(" ==> " + p.toString());
        }
    }

    private static Object insightField(Object obj, String fName) {
        try {
            Field f = null;
            if(obj instanceof URLClassLoader){
                f = URLClassLoader.class.getDeclaredField(fName);
            }else{
                f = obj.getClass().getDeclaredField(fName);
            }
            f.setAccessible(true);
            return f.get(obj);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

代码执行结果如下:

启动类加载器
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/resources.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/rt.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/sunrsasign.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/jsse.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/jce.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/charsets.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/jfr.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/classes

扩展类加载器 ClassLoader -> sun.misc.Launcher$ExtClassLoader@15db9742
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/access-bridge-64.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/cldrdata.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/dnsns.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/jaccess.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/jfxrt.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/localedata.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/nashorn.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/sunec.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/sunjce_provider.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/sunmscapi.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/sunpkcs11.jar
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/zipfs.jar

应用类加载器 ClassLoader -> sun.misc.Launcher$AppClassLoader@73d16e93
   ==> file:/D:/git/studyjava/build/classes/java/main/
   ==> file:/D:/git/studyjava/build/resources/main

从打印结果,我们可以看到三种类加载器各自默认加载了哪些 jar 包和包含了哪些 classpath 的路径。

2)如何排查类的方法不一致的问题? #

假如我们确定一个 jar 或者 class 已经在 classpath 里了,但是却总是提示java.lang.NoSuchMethodError,这是怎么回事呢?很可能是加载了错误的或者重复加载了不同版本的 jar 包。这时候,用前面的方法就可以先排查一下,加载了具体什么 jar,然后是不是不同路径下有重复的 class 文件,但是版本不一样。

3)怎么看到加载了哪些类,以及加载顺序? #

还是针对上一个问题,假如有两个地方有 Hello.class,一个是新版本,一个是旧的,怎么才能直观地看到他们的加载顺序呢?也没有问题,我们可以直接打印加载的类清单和加载顺序。

只需要在类的启动命令行参数加上-XX:+TraceClassLoading 或者 -verbose 即可,注意需要加载 Java 命令之后,要执行的类名之前,不然不起作用。例如:

$ java -XX:+TraceClassLoading jvm.HelloClassLoader 
[Opened D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.Object from D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.io.Serializable from D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.Comparable from D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.CharSequence from D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.String from D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.reflect.AnnotatedElement from D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.reflect.GenericDeclaration from D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.reflect.Type from D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.Class from D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.Cloneable from D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.ClassLoader from D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.System from D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
// .......  此处省略了100多条类加载信息
[Loaded jvm.Hello from __JVM_DefineClass__]
[Loaded java.util.concurrent.ConcurrentHashMap$ForwardingNode from D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
Hello Class Initialized!
[Loaded java.lang.Shutdown from D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from D:\Program Files\Java\jre1.8.0_231\lib\rt.jar]

上面的信息,可以很清楚的看到类的加载先后顺序,以及是从哪个 jar 里加载的,这样排查类加载的问题非常方便。

4)怎么调整或修改 ext 和本地加载路径? #

从前面的例子我们可以看到,假如什么都不设置,直接执行 java 命令,默认也会加载非常多的 jar 包,怎么可以自定义加载哪些 jar 包呢?比如我的代码很简单,只加载 rt.jar 行不行?答案是肯定的。

$ java -Dsun.boot.class.path="D:\Program Files\Java\jre1.8.0_231\lib\rt.jar" -Djava.ext.dirs= jvm.JvmClassLoaderPrintPath

启动类加载器
   ==> file:/D:/Program%20Files/Java/jdk1.8.0_231/jre/lib/rt.jar
扩展类加载器 ClassLoader -> sun.misc.Launcher$ExtClassLoader@15db9742
应用类加载器 ClassLoader -> sun.misc.Launcher$AppClassLoader@73d16e93
   ==> file:/D:/git/studyjava/build/classes/java/main/
   ==> file:/D:/git/studyjava/build/resources/main

我们看到启动类加载器只加载了 rt.jar,而扩展类加载器什么都没加载,这就达到了我们的目的。

其中命令行参数-Dsun.boot.class.path表示我们要指定启动类加载器加载什么,最基础的东西都在 rt.jar 这个包了里,所以一般配置它就够了。需要注意的是因为在 windows 系统默认 JDK 安装路径有个空格,所以需要把整个路径用双引号括起来,如果路径没有空格,或是 Linux/Mac 系统,就不需要双引号了。

参数-Djava.ext.dirs表示扩展类加载器要加载什么,一般情况下不需要的话可以直接配置为空即可。

5)怎么运行期加载额外的 jar 包或者 class 呢? #

有时候我们在程序已经运行了以后,还是想要再额外的去加载一些 jar 或类,需要怎么做呢?

简单说就是不使用命令行参数的情况下,怎么用代码来运行时改变加载类的路径和方式。假如说,在d:/app/jvm路径下,有我们刚才使用过的 Hello.class 文件,怎么在代码里能加载这个 Hello 类呢?

两个办法,一个是前面提到的自定义 ClassLoader 的方式,还有一个就是直接在当前的应用类加载器里,使用 URLClassLoader 类的方法 addURL,不过这个方法是 protected 的,需要反射处理一下,然后又因为程序在启动时并没有显示加载 Hello 类,所以在添加完了 classpath 以后,没法直接显式初始化,需要使用 Class.forName 的方式来拿到已经加载的 Hello 类(Class.forName(“jvm.Hello”)默认会初始化并执行静态代码块)。代码如下:

package jvm;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class JvmAppClassLoaderAddURL {

    public static void main(String[] args) {

        String appPath = "file:/d:/app/";
        URLClassLoader urlClassLoader = (URLClassLoader) JvmAppClassLoaderAddURL.class.getClassLoader();
        try {
            Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            addURL.setAccessible(true);
            URL url = new URL(appPath);
            addURL.invoke(urlClassLoader, url);
            Class.forName("jvm.Hello"); // 效果跟Class.forName("jvm.Hello").newInstance()一样
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行以下,结果如下:

$ java JvmAppClassLoaderAddURL Hello Class Initialized!

结果显示 Hello 类被加载,成功的初始化并执行了其中的代码逻辑。

参考链接 #