JVM类加载

前言

在我们写一个.java 文件时,这个文件是怎么被处理的呢。Java 可以解释执行也可以编译执行,大多数JVM采用第三种混合的方式。冯诺依曼体系的计算机模型中,任何程序都需要加载到内存中才能和CPU进行交流。.java文件被编译成.class的字节码文件之后交给JVM执行时也同样需要被加载到内存中,才可以实例化类。

JVM的类加载机制指的是虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。简单来说就是将.class字节码文件实例化成Class对象并进行相关初始化的过程。

1. 类加载过程

通过双亲委派模型来进行类加载的,其中的重要部分就是类加载器。类加载器对类进行加载(Loding)、连接(Linkind)、初始化(Init)。其中连接阶段指的是验证、准备、解析这三部分。

加载:

Load 阶段读取类文件产生二进制流,并转化为特定的数据结构,初步校验 cafe babe魔法数、常量池、文件长度、是否有父类等,然后创建对应类的 java.lang.Class 实例。

连接:

  • 验证: 验证是更详细的校验,比如final 否合规、类型是否正确、静态变量是否合理等
  • 准备: 准备阶段是为静态变量分
    配内存,并设定默认值
  • 解析: 解析指找到相关引用,进行相关的类加载。

初始化: Init 阶段执行类构造器<clinit> 方法

类加载器有哪些

  • 启动类加载器(Bootstrap ClassLoader)
    • 使用 C++ 语言实现,是虚拟机自身的一部分
    • 负责<JAVA_HOME>\lib 目录中的

下面的类加载器都是由 Java 语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader

  • 拓展类加载器(Extension ClassLoader)

    • 负责加载<JAVA_HOME>\lib\ext 目录
  • 应用程序类加载器(Application ClassLoader)

    • 负责加载用户路径上所指定的类库
  • 自定义类加载器(User ClassLoader)

    • 开发者自己拓展定义的

2. 双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把则会个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。因此所有的加载请求都是应该传送到顶层的类加载器去处理,只有当父加载器反馈自己无法完成这个加载(它的搜索范围内没有找到所需的类时),子类才会去尝试自己去加载。

双亲委派模型是如何实现的

java.lang.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 {
                // 让父类加载器进行加载
                    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类会有一种带有优先级的层次关系,比如java.lang.String 它存放在rt.jar 中 无论那个类加载都会往上级询问,也就是最终到BootStrap类加载器进行加载。也就是说如果你自己实现一个名为java.lang.String的类,并放在程序的ClassPath中,可以正常编译,但无法被加载运行。(如果可以的话,程序就会产生混乱了)。

自定义类加载器:

public class MyClassLoader extends ClassLoader {

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] result = getClassFromMyPath(name);
        try {
            if (result == null) {
                throw new FileNotFoundException();
            } else {
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new ClassNotFoundException();
    }

    private byte[] getClassFromMyPath(String name) {
        // 从自定义路径中加载指定类
        return null;
    }

    public static void main(String[] args) {
        MyClassLoader classLoader = new MyClassLoader();
        try {
            Class<?> clazz = Class.forName("Student", true, classLoader);
            Object obj = clazz.getInterfaces();
            System.out.println(obj.getClass().getClassLoader());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

ctrl + alt + t idea 快捷 try catch

3. 那种情况需要自定义类加载器

  • 隔离加载类

    • 在某些框架内进行中间件与应用的模块隔离 把类加载到不
      同的环境。比如 阿里内某容器框架通过自定义类加载器确保应用中依赖的 jar 包不
      会影响到中间件运行时使用的 jar 包。
  • 修改类加载方式

    • 类的加载模型并非强制 Bootstrap 其他的加载并非一
      定要引入 或者根据实际情况在某个时间点进行按需进行动态加载。
  • 扩展加载源

    • 比如从数据库、网 ,甚 是电视机机顶盒进行加载
  • 防止源码泄露

    • Java 代码容易被编译和篡改,可以进行编译加密 那么
      加载器也需要自定义,还原加密的字节码。

Reference

  • 《深入理解Java虚拟机》
  • 《码出高效》
posted @ 2020-04-26 22:35  胖大星-  阅读(181)  评论(0编辑  收藏  举报