JVM-类的加载机制


一、类的生命周期

当编写完一个 java 类之后,经过编译就能够得到一个 .class(字节码)文件,这种字节码文件需要在 JVM 中运行。

在这里插入图片描述

java 类的生命周期是指一个 .class 文件从被加载到虚拟机内存中开始,到卸载出内存结束的全过程。一个类完整的生命周期会经历 加载、连接、初始化、使用和卸载五个阶段。其中连接又包含了验证、准备、解析这三个部分。加载、验证、准备、初始化、卸载这 5 个阶段的顺序是确定的。解析阶段不一定,它在某些情况下可以初始化阶段之后在开始,这是为了支持 Java 语言的运行时绑定。

在这里插入图片描述


二、类加载的过程

当程序要使用某个类时,如果类还未被加载到内存中,则系统会通过类的加载、类的连接、类的初始化这三个步骤进行初始化,详细点说就是加载、验证、准备、解析、初始化这五大部分。如果不出意外情况,JVM 将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或者类初始化

  • 加载:根据路径找到相应的 .class 文件然后通过 IO 写入 JVM 的方法区中,同时在堆中创建一个 java.lang.Class 对象
  • 验证:校验加载到的 .class 文件是否正确
  • 准备:给类中的静态变量分配内存空间,将其初始化为默认值。此阶段只为静态变量(即 static 修饰的字段变量)分配内存,并且设置该变量的初始值(比如:static int num = 5; 在这一阶段的时候 num 会被初始化为 0 而不是 5),对于 static final 修饰的变量,在编译的时候就会分配,也不会分配实例变量的内存
  • 解析: 将类的二进制数据中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,直接引用直接指向内存中的地址
  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块

类加载底层详细流程:

在这里插入图片描述


三、类加载的时机

虚拟机规范中并没有强制约束何时进行加载,但以下几种情况下必须要对类进行加载(加载-初始化):

  • 使用 new 关键字实例化对象
  • 读取或者设置一个类的静态变量的时候
  • 调用类对应的静态方法的时候
  • 对类进行反射调用的时候
  • 初始化子类时,父类会先被初始化
  • 虚拟机启动时,定义了main()方法的那个类先初始化

以上几种场景的行为称为对一个类的主动引用,除此之外,所有引用类的方式都不会触发初始化,称为被动引用,常见的被动引用有:

  • 通过子类引用父类的静态字段,不会导致子类初始化
System.out.println(SubClass.value);  // value 字段在 SuperClass 中定义
  • 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法
SuperClass[] sca = new SuperClass[10];
  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
System.out.println(ConstClass.HELLOWORLD);

四、类加载器

类加载器的作用就是将 class 文件加载到内存中,并为之生成对应的 java.lang.Class 对象。在 JDK 中主要有三种类加载器,它们分别是引导类加载器扩展类加载器应用类加载器。同样,我们可以通过继承 java.lang.ClassLoader自定义类加载器

类加载器的层级关系:

在这里插入图片描述

  • 启动类加载器(bootstrap ClassLoader)
    • 它负责加载 Java 的核心类库((JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路径下的内容) ,用于提供JVM自身需要的类。
  • 扩展类加载器(extension ClassLoader)
    • 从java.ext.dirs系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的 JAR 放在此目录下,也会自动由扩展类加载器加载
  • 应用程序加载器(application ClassLoader)
    • 负责加载 ClassPath 路径下的 .class 字节码文件,主要是用于加载程序员自己写的类
  • 自定义加载器
    • 负责加载程序员定义路径下的 class 字节码文件

类加载器之间是继承关系吗?

在这里插入图片描述

不是,从上面可以看到 ExtClassLoaderAppClassLoader 这两个加载器都是 Launcher 的内部类,而且全部继承于 URLClassLoader,而 URLClassLoader 最后又继承于 ClassLoader

在这里插入图片描述
ClassLoader 中有一个 parent 属性记录了它们之间的关系

在这里插入图片描述

所以类加载器的体系不是继承,而是委派体系。

我们可以通过以下代码证实类的加载器之间的关系:

public class LoaderDemo {
    
    
    public static void main(String[] args) {
    
    

        // 获取系统默认的类加载器:应用类加载器
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(appClassLoader);

        // 获取应用类加载器的父类加载器:扩展类加载器
        ClassLoader extClassLoader = appClassLoader.getParent();
        System.out.println(extClassLoader);

        // 获取扩展类加载器的父类加载器:引导类加载器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);
    }
}

在这里插入图片描述

因为 bootstrapClassLoader 在 JVM 中,而 JVM 又是 C++ 编写的,在 Java 环境中是获取不到该类的,所以会显示为 null


如何获取类的加载器?

如果想获取加载某个类的类加载器,可以通过类的字节码对象(XXX.class.getClassLoader() 方法获取

例如:

public class LoaderDemo {
    
    
    public static void main(String[] args) {
    
    

        System.out.println(String.class.getClassLoader());
        System.out.println(DNSNameService.class.getClassLoader().getClass().getName());
        System.out.println(LoaderDemo.class.getClassLoader().getClass().getName());

    }
}

在这里插入图片描述


五、双亲委派模型

在这里插入图片描述
双亲委派模型是指当调用类加载器的 loadClass 方法进行类加载时,该类加载器会首先请求它的父类加载器进行加载,依次递归。如果所有父类加载器都加载失败,则当前类加载器自己进行加载操作。

简单来说就是自底向上检查是否加载成功,自顶向下尝试加载。

我们可以通过 ClassLoader 类的 loadClass 方法了解到双亲委派的实现,源码如下:

    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 {
    
    
                	// 检查该类的父级加载器是否不为 null
                    if (parent != null) {
    
    
                    	// 调父级加载器的 loadClass 方法
                    	// 假如当前类加载器为 AppClassLoader,那么它就会去调 ExtClassLoader 的 loadClass 方法,而 AppClassLoader 和 ExtClassLoader 实际上都是调用了 ClassLoader 中的 loadClass 方法
                        c = parent.loadClass(name, false);
                    } else {
    
    
                    	// 这里就会到 C++ 写的类加载器来加载类
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
    
    
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
    
    
                	// 如果 c 为 null,则表明 bootstrap 类加载器也没有加载成功
                    // 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;
        }
    }


    /**
     * Returns a class loaded by the bootstrap class loader;
     * or return null if not found.
     */
    private Class<?> findBootstrapClassOrNull(String name)
    {
    
    
        if (!checkName(name)) return null;

        return findBootstrapClass(name);
    }

URLClassLoader 类的 findClass 方法源码:

    protected Class<?> findClass(final String name)
         throws ClassNotFoundException
    {
    
    
        try {
    
    
            return AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
    
    
                    public Class<?> run() throws ClassNotFoundException {
    
    
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
    
    
                            try {
    
    
                                return defineClass(name, res);
                            } catch (IOException e) {
    
    
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
    
    
                            throw new ClassNotFoundException(name);
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
    
    
            throw (ClassNotFoundException) pae.getException();
        }
    }

为什么要用双亲委派模型?

  1. Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父类加载器已经加载了该类时,就没有必要再 ClassLoader 再加载一次。
  2. 考虑到安全因素,java 核心 API 中定义类型不会被随意替换,这样便可以防止核心 API 库被随意篡改。
  3. 保证类的唯一性

双亲委派模型能否被打破?

可以,双亲委派模型不是一个强制性的约束模型,而是一个建议型的类加载器实现方式。如果想要打破这种模型,就需要自定义一个类加载器,重写其中的 loadClass 方法,使其不进行双亲委派即可。


什么情况下需要打破这种双亲委派模型?

JDBC 中在核心类库 rt.jar 的加载过程中需要加载第三方厂商的类(比如常用的数据库),直接指定使用应用程序类加载器来加载这些类。就需要打破双亲委派模型。

Tomcat 中的 web 容器类加载器也破坏了双亲委托模式的,自定义的 WebApplicationClassLoader除入了核心类库外,都是优先加载自己路径下的Class这样有利于隔离安全热部署。

六、自定义类加载器

自定义加载器需要:

  • 继承 ClassLoader
  • 覆盖 findClass() 方法或者 loadClass() 方法
import java.io.IOException;
import java.io.InputStream;

public class MyClassLoader extends ClassLoader {
    
    

    /**
     * 如果重写 loadClass 方法则会打破双亲委派
     */
//    @Override
//    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    
    
//        // todo ...
//        return null;
//    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    
    
        try {
    
    
            String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
            InputStream inputStream = getClass().getResourceAsStream(fileName);
            if (inputStream == null) {
    
    
                throw new ClassNotFoundException(name);
            }
            byte[] b = new byte[inputStream.available()];
            int read = inputStream.read(b);
            return defineClass(name,b,0, b.length);
        } catch (IOException e) {
    
    
            throw new ClassNotFoundException(name);
        }
    }
}

参考博客:
JVM类加载机制:https://blog.csdn.net/weixin_41812379/article/details/124107027

猜你喜欢

转载自blog.csdn.net/xhmico/article/details/130127161