jvm之类加载器

x33g5p2x  于2021-12-18 转载在 其他  
字(6.5k)|赞(0)|评价(0)|浏览(315)

类加载器

注意图中的类加载器之间并不是继承关系,而是包含关系。

根类加载器

根类加载器(BootstrapClassLoader):负责加载存放在JDK/jre/lib下的指定的jar,也可以使用-Xbootclasspath参数指定哪些jar要使用根类加载器加载。

下面的代码将会打印BootstrapClassLoader以及根类加载器负责加载哪些jar:

System.out.println("BootstrapClassLoader:" + String.class.getClassLoader());
Arrays.asList(System.getProperty("sun.boot.class.path").split(";")).stream().forEach(System.out::println);

运行结果如下所示,其中String.class的类加载器是根类加载器,根类加载器是获取不到引用的,因此输出为null,而根类加载器所加载的jar可以通过系统属性sun.boot.class.path获取。

BootstrapClassLoader:null
D:\Program Files\Java\jdk1.8.0_172\jre\lib\resources.jar
D:\Program Files\Java\jdk1.8.0_172\jre\lib\rt.jar
D:\Program Files\Java\jdk1.8.0_172\jre\lib\sunrsasign.jar
D:\Program Files\Java\jdk1.8.0_172\jre\lib\jsse.jar
D:\Program Files\Java\jdk1.8.0_172\jre\lib\jce.jar
D:\Program Files\Java\jdk1.8.0_172\jre\lib\charsets.jar
D:\Program Files\Java\jdk1.8.0_172\jre\lib\jfr.jar
D:\Program Files\Java\jdk1.8.0_172\jre\classes

要想直接使用根类加载器加载自定义的类,有以下几种方法:

  1. 将class文件放入上面打印的路径D:\Program Files\Java\jdk1.8.0_172\jre\classes中
  2. 将自定义的类压缩成jar,在运行时用-Xbootclasspath指定jar的路径

-Xbootclasspath的使用方法:

  • -Xbootclasspath:完全取代基本核心的Java class搜索路径,否则要重新写所有Java核心class文件
  • -Xbootclasspath/a:加在核心class文件搜索路径后面
  • -Xbootclasspath/p:加在核心class文件搜索路径前面

扩展类加载器

扩展类加载器(Extension ClassLoader):负责加载JDK\jre\lib\ext目录中或者由java.ext.dirs系统变量指定的路径中的所有类库,开发者可以直接使用扩展类加载器。扩展类加载器的父加载器是根类加载器,该加载器由sun.misc.Launcher$ExtClassLoader实现。

下面的代码将会打印ExtClassLoader以及扩展类加载器负责加载哪些目录的jar:

System.out.println("ExtClassLoader:" + com.sun.nio.zipfs.ZipDirectoryStream.class.getClassLoader());
System.out.println("ExtClassLoader parent:" + com.sun.nio.zipfs.ZipDirectoryStream.class.getClassLoader().getParent());
Arrays.asList(System.getProperty("java.ext.dirs").split(";")).stream().forEach(System.out :: println);

运行结果如下所示:

ExtClassLoader:sun.misc.Launcher$ExtClassLoader@2503dbd3
ExtClassLoader parent:null
D:\Program Files\Java\jdk1.8.0_172\jre\lib\ext
C:\Windows\Sun\Java\lib\ext

系统类加载器

系统类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)所指定的类,系统类加载器的加载路径可以通过-classpath来指定,同样也可以通过系统属性java.class.path来获取。系统类加载器的父加载器是扩展类加载器,该类加载器由sun.misc.Launcher$AppClassLoader来实现。

下面的代码将会打印AppClassLoader以及系统类加载器负责加载哪些目录的class:

System.out.println("AppClassLoader:" +AppClassLoaderTest.class.getClassLoader());
System.out.println("AppClassLoader parent:" +AppClassLoaderTest.class.getClassLoader().getParent());
Arrays.asList(System.getProperty("java.class.path").split(";")).stream().forEach(System.out::println);

运行结果如下:

AppClassLoader:sun.misc.Launcher$AppClassLoader@2a139a55
AppClassLoader parent:sun.misc.Launcher$ExtClassLoader@7852e922
.

自定义加载器

通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自ClassLoader类,我们只需要重写findClass方法即可。

下面的代码自定了一个类加载器来加载磁盘上的class文件:

package com.morris.jvm.classloader;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

// 自定义类加载器必须继承ClassLoader
public class MyClassLoader extends ClassLoader {

    private static final Path DEFAULT_CLASS_DIR = Paths.get("D:","classloader");

    private final Path classDir;

    public MyClassLoader() {
        this.classDir = DEFAULT_CLASS_DIR;
    }

    public MyClassLoader(ClassLoader parent) {
        super(parent);
        this.classDir = DEFAULT_CLASS_DIR;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] bytes = this.readByte(name);

        if(null == bytes || 0 == bytes.length) {
            throw new ClassNotFoundException("Can not load the class " + name);
        }

        return this.defineClass(name, bytes, 0, bytes.length);
    }

    // 将class文件读入内存
    private byte[] readByte(String name) throws ClassNotFoundException{
       String classPath = name.replace(".", "/");

        Path classFullPath = this.classDir.resolve(classPath + ".class");

        if(!classFullPath.toFile().exists()) {
            throw new ClassNotFoundException("The class " + name + " not found.");
        }

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        try {
            Files.copy(classFullPath, baos);
            return baos.toByteArray();
        } catch (IOException e) {
            throw new ClassNotFoundException("load the class " + name + " error", e);
        }
    }

}

下面写一个简单的HelloWorld程序,使用自定义类加载器对其进行加载。

package com.morris.jvm.classloader;

public class HelloWorld {

    static {
        System.out.println("HelloWorld Class is initialized.");
    }

}

将HelloWorld类编译后将class文件复制到D:\classloader\com\morris\jvm\classloader目录下,同时将class path中的HelloWorld.class删除,如果使用的集成开发环境,则需要将HelloWorld.java一并删除,否则将会由系统类加载器加载。

使用下面的代码使用自定义的累加器尝试加载HelloWorld:

package com.morris.jvm.classloader;

public class MyClassLoaderTest {

    public static void main(String[] args) throws ClassNotFoundException {
        MyClassLoader myClassLoader = new MyClassLoader();
        Class<?> clazz = myClassLoader.loadClass("com.morris.jvm.classloader.HelloWorld");
        System.out.println(clazz.getClassLoader());
    }
}

运行结果如下,虽然HelloWorld类被成功加载并且输出了自定义类加载器的信息,但是HelloWorld类的静态代码块并没有输出,因为使用类加载器的loadClass并不会导致类的主动初始化,只是执行了类加载过程中的加载阶段而已。

com.morris.jvm.classloader.MyClassLoader@7f31245a

Class.forName()和ClassLoader.loadClass()区别

  • Class.forName():除了将类的.class文件加载到jvm中之外,还会对类进行解析,执行类中的static块进行初始化;
  • ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块进行初始化。

注:Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采会调用构造函数,创建类的对象。

双亲委托机制

如果一个类加载器收到了一个类加载请求,它不会自己去尝试加载这个类,而是把这个请求转交给父类加载器去完成。每一个层次的类加载器都是如此。因此所有的类加载请求都应该传递到最顶层的启动类加载器中,只有到父类加载器反馈自己无法完成这个加载请求(在它的搜索范围没有找到这个类)时,子类加载器才会尝试自己去加载。双亲委托机制的好处就是避免有些类被重复加载。

源码分析如下:

摘自jdk1.8 java.lang.ClassLoader

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();
                    // 父类加载失败,则使用自己的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;
        }
    }

从上面对于java.lang.ClassLoader的loadClass(String name, boolean resolve)方法的解析来看,可以得出以下2个结论:

  1. 如果不想打破双亲委派模型,那么只需要重写findClass方法即可
  2. 如果想打破双亲委派模型,那么就重写整个loadClass方法

相关文章