Java类加载机制

1. 引言

我们日常开发的Java代码都是保存在以.java为后缀的文件当中。想要执行java代码的话,首先需要将.java文件编译成以.class为后缀的字节码文件,然后类加载器将.class字节文件加载到JVM当中,最后在JVM中运行我们编写的代码。

2. 类的加载

2.1. 触发类加载的时机

在代码运行过程中,使用到哪个类就会触发哪个类的加载。具体来说,当遇到以下6种情况会触发:

  1. 通过new关键字创建对象;
  2. 访问类的静态变量;
  3. 访问类的静态方法;
  4. 对某个类进行反射,如Class.forName("")
  5. 加载子类时会导致父类的加载;
  6. 包含main方法的启动类。

当首次遇到上面6中情况之时,会导致类的加载(包含类加载的一系列过程)。

2.2. 类加载的过程

类的加载主要包含三个阶段:加载、连接、初始化,其中连接阶段分为:验证、准备、解析。


各个阶段主要的工作如下

  • 加载:查找并加载class文件到 JVM内存;
  • 验证:检查class文件是否符合JVM的规范;
  • 准备:1.给类以及类中的静态变量分配内存空间;2. 给静态变量设置默认的初始值
  • 解析:将符号引用替换成直接引用,即内存地址;
  • 初始化:执行静态代码块,并给静态变量赋值

public static Integer num=10 类变量num在准备阶段的值会设置成0,初始化阶段才会设置为10。

加载阶段可以和连接阶段交叉执行,即在加载阶段开始之前,验证阶段开始进行验证。

class被加载之后的内存情况,如下图所示:

class被加载后的内存情况

3. 类加载器

JVM为我们提供了三大内置类加载器,不同的类加载器负责将不同的类加载的JVM内存中。三大内置加载器分别是:

  • BootstrapClassLoader(根类加载器);
  • ExtClassLoader(扩展类加载器);
  • ApplicationClassLoader(应用类加载器);

3.1. 根类加载器

根类加载器是最顶层的类加载器,没有父类。它是用C++编写的,主要用来加载JDK安装目录下jre/lib/中用于支撑Java系统运行的核心类库,例如:java.lang包下的类。

可以通过-Xbootclasspath来指定根加载器的路径,也可以通过系统属性来得知当前JVM的根加载器都加载了哪些资源,如以下程序:

public class BootStrapClassLoaderTest {
    public static void main(String[] args) {
        /**
         * 输出 Bootstrap:null
         * 根类加载器获取不到引用,所以打印出来为null
         */
        System.out.println("Bootstrap:"+String.class.getClassLoader());
        System.out.println(System.getProperty("sun.boot.class.path"));
    }
}

3.2. 扩展类加载器

扩展类加载器主要用于加载JAVA_HOME下的jre/lib/ext子目录下的类库,它的父类是根类加载器。扩展类加载器是由纯Java代码实现的,它的完整类名是sun.misc.Launcher$ExtClassLoader。扩展类加载器所加载的路径,可以通过java.ext.dir系统属性获得。

3.3. 系统类加载器

系统类加载器主要负责classpath下的类资源加载,我们开发过程中所依赖的第三方jar包默认就是系统类加载器加载的。系统类加载器的父类是扩展类加载器,其类全名是sun.misc.Launcher$AppClassLoader。系统类加载器的加载路径一般通过-classpath或者-cp指定,同样也可以通过系统属性java.class.path进行获取。实例代码如下:

public class ApplicationClassLoaderTest {

    public static void main(String[] args) {
        System.out.println("classloader: "+ApplicationClassLoaderTest.class.getClassLoader());
        System.out.println(System.getProperty("java.class.path"));
    }
}

3.4. 自定义类加载器

除了上面介绍的三大内置类加载器,我们还能根据需求实现自己的类加载器。所有的自定义类加载器都需要直接或者间接继承ClassLoader类,这个类是一个抽象类,但是没有抽象方法,但是其中的findClass(String name)方法必须得重写,因为其默认实现会抛出一个异常。

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

下面的代码,演示了如何自定义类加载器

public class MyClassLoader extends ClassLoader{
    private final static Path DEFAULT_CLASS_DIR = Paths.get("/Users/classloader");

    private final Path classDir;

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

    public MyClassLoader (String classDir) {
        super();
        this.classDir = Paths.get(classDir);
    }

    public MyClassLoader (String classDir, ClassLoader parentClassLoader) {
        super(parentClassLoader);
        this.classDir = Paths.get(classDir);
    }

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

        byte[] classBytes = this.readClassBytes(name);

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

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

    private byte[] readClassBytes(String className) throws ClassNotFoundException {
        String classPath = className.replace(".","/");
        Path classFullPath = classDir.resolve(Paths.get(classPath+".class"));
        if (!classFullPath.toFile().exists()) {
            throw new ClassNotFoundException("The class "+className+" not found");
        }
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            Files.copy(classFullPath,baos);
            return baos.toByteArray();
        } catch (IOException e) {
            throw new ClassNotFoundException("load the class "+className+" occur error");
        }
    }
}

使用自定义类加载器来加载类:

private static void testMyClassLoader() throws Exception {
    MyClassLoader classLoader = new MyClassLoader();
    Class<?> aClass = classLoader.loadClass("thread.classloader.Hello");
    System.out.println(aClass.getClassLoader());

    Object hello = aClass.newInstance();
    System.out.println(hello);

    Method method = aClass.getMethod("hello", String.class);
    String result = (String) method.invoke(hello, "tomcat");
    System.out.println("Result:"+result);
}

完整的代码请访问:https://github.com/codingXcong/Java-Guide/tree/master/Thread/src/main/java/thread/classloader

上面的示例中,我们通过自定义类加载器对Hello.java进行加载,运行示例的时候,需要先将java文件编译成字节码文件,然后将字节码文件放入MyClassLoader类中指定的classDir路径下。
PS:如果在IDEA环境中,还需要将Hello.java文件删除,不删除的话,根据类加载器的双亲委派模型,会有MyClassLoader的父类加载器对Hello类进行加载。

3.5. 双亲委派模式

JVM的类加载器是有亲子层级结构的,如下图所示:

基于这个亲子层级结构,有个双亲委派的机制。在进行类加载的时候,当一个类被调用loadClass之后,它并不会直接去加载,而是交给当前类加载器的父加载器尝试加载,直到传递到最顶层的父加载器。

例如,现在JVM需要加载A类,此时系统类加载器会问问自己的爸爸,也就是扩展类加载器,你能加载到A类吗?

然后扩张类加载器会直接问自己的爸爸,启动类加载器,你能加载A类吗?

启动类加载器在Java安装目录下没有找到这个类,就告诉扩张类加载器,我没法加载这个类,你自己加载去吧。

扩展类加载器就尝试自己加载,它在jre/lib/ext下也没找到这个类,就会通知系统类加载器,没有加载到类A,你自己去加载。

最后系统类加载器在自己负责加载的范围内找到了类A,然后就将其加载到内存中。

为啥要设计成双亲委派模式么?防止同一个类被重复加载。

我们再通过源码来看看双亲委派的工作方式,其逻辑主要封装在ClassLoader.loadClass(String name)中:

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    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) {
                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;
        }
    }
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 前言 我们知道我们写的程序经过编译后成为了.class文件,.class文件中描述了类的各种信息,最终都需要加载到...
    5210167阅读 637评论 0 2
  • Java的核心是 JVM ,了解并熟悉JVM对于我们理解Java语言非常重要。 一、类加载机制 当程序主动使用某个...
    年少懵懂丶流年梦阅读 1,097评论 2 15
  • 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个...
    dinel阅读 427评论 0 0
  • 观山寺·回文诗 悠悠绿水傍林偎, 日落观山四望回。 幽林古寺孤明月, 冷井寒泉碧映台。 鸥飞满浦鱼舟泛, 鹤伴闲亭...
    圆梦入缀阅读 1,077评论 2 2
  • 你是不是也有这样的烦恼。
    林小舟阅读 239评论 1 3