JVM类加载

2022-05-27  本文已影响0人  生不悔改

一、类加载

什么是类加载?在IDEA中写一个Student.java文件,在对应的目录下执行

>javac Student.java

将Student.java文件编译成对应的Student.class文件,再通过ClassLoader里面的loadClass方法,将Student.class二进制字节码文件加载到jvm的元空间中成为Student.class的类类型对象。这一套流程就是所谓的 类加载

二、类的生命周期

类加载进内存也是分步骤的:


类的生命周期.png

1.加载

指的是把class字节码文件通过类加载器装载到jvm元空间中。
1.字节码来源,可以是本地磁盘class文件,jar包中的class文件,网络上的class文件,以及动态代理修改后的class文件实时编译
2.通过类加载器里面的classloader方法加载到元空间中

2.验证

保证加载进来的字节流符合jvm虚拟机规范,不会造成安全错误
1.文件格式的验证,文件中是否有不规范的或者附加的其他信息,例如常量池中是否有不被支持的常量
2.元数据的验证,保证器描述的信息符合java语言的规范要求,例如类是否有父类,是否继承了不被允许的final类
3.字节码的验证,保证程序语义的合理性,比如保证类型转换的合理性
4.符号引用的验证,比如校验引用中通过全限定名是否能够找到一应的类,校验符号引用中的访问性(public),是否可以被当前类访问等等

3.准备

主要是为基本数据类型的变量分配内存,并且赋予初始值。例如:int初始值是0,reference初始值为null

4.解析

将常量池的符号引用替换成直接引用的过程。例如final String = new String("java"),new String("java")在堆内存的地址是123
改成final String->123。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换成具体的内存地址或偏移量,也就是直接引用

5.初始化

在这个阶段主要对类变量初始化,是执行构造器的过程
1.支队static修饰的变量或语句进行初始化
2.如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类
3.如果同时包含静态变量和静态代码块,则按照自上而下的顺序依次执行

三、类加载的原则

1.类加载器的种类

JVM中的类加载器.png

Bootstrap Classloader(启动类加载器)

启动类加载器(Bootstrap Classloader)负责将<JAVA_HOME>/lib目录下并且被虚拟机识别的类库加载到虚拟机内存中。我们常用基础库,例如java.util.,java.io.,java.lang.**等等都是由根加载器加载。

Extension Classloader(扩展类加载器)

扩展类加载器(Extention Classloader)负责加载JVM扩展类,比如swing系列、内置的js引擎、xml解析器等,这些类库以javax开头,它们的jar包位于<JAVA_HOME>/lib/ext目录中。

Application Classloader(应用类加载器)

应用程序加载器(Application Classloader)也叫系统类加载器,它负责加载用户路径(ClassPath)上所指定的类库。我们自己编写的代码以及使用的第三方的jar包都是由它来加载的自定义加载器(Custom Classloader)通常是我们为了某些特殊目的实现的自定义加载器,后面我们得会详细介绍到它的作用以及使用场景。

XXXClassloader(自定义加载器)

通过

class XXXClassloader extends ClassLoader{}

实现父类ClassLoader中的 loadClass(String name)

    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;
        }
    }

findClass(查找.class是否被加载过)空方法,自定义加载器自己实现,

@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return super.findClass(name);
    }

线程上下文类加载器

JVM的SPI机制其实就是用到了线程上下文类加载器,每个线程启动的时候,jvm会默认将Application ClassLoader作为线程的类加载器。这时候线程直接使用Application ClassLoader去加载对应的类,并没有将需要加载的类交给自己的上一级类加载器。

public static <S> ServiceLoader<S> load(Class<S> service) {
         // 获取当前线程的类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

如何破坏双亲委托机制

1.首先根据源码可以知道,如果自定义的方法不想违背双亲委派模型,则只需要重写findclass方法即可,如果想违背双亲委派模型,则还需要重写loadclass方法。

2.常见的SPI机制,例如class.forName("")加载数据库驱动的方法,使用SPI机制破坏了双亲委托机制

3.使用线程上下文类加载器,默认为Application ClassLoader直接加载对应的类文件,所以也是打破双亲委派机制的一种方式。

2.双亲委托机制

classloader的双亲委托机制是指多个类加载器之间存在父子关系的时候,某个class类具体由哪个加载器进行加载的问题。其具体的过程表现为:当一个类加载的过程中,它首先不会去加载,而是委托给自己的父类去加载,父类又委托给自己的父类。因此所有的类加载都会委托给顶层的父类,即Bootstrap Classloader进行加载,然后父类自己无法完成这个加载请求,子加载器才会尝试自己去加载。使用双亲委派模型,Java类随着它的加载器一起具备了一种带有优先级的层次关系,通过这种层次模型,可以避免类的重复加载,也可以避免核心类被不同的类加载器加载到内存中造成冲突和混乱,从而保证了Java核心库的安全。、

注意:Bootstrap Classloader,Extention Classloader,Application Classloader三个加载器类仅仅是组合关系,有优先级,并不是实际开发中的父子类关系

JVM中的类加载器.png

双亲委托的作用

1、安全,可避免用户自己编写的类动态替换Java的核心类,如java.lang.String。,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
2、避免全限定命名的类重复加载(使用了findLoadClass()判断当前类是否已加载)。Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次

3.沙箱安全机制

主要用来防止恶意代码污染java源代码。
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。

四、类加载器的使用场景

1.用来做微服务module之间的jar包隔离

2.用来做热加载

3.用来做热部署

五、类加载相关面试题

1.class.forName()与classLoader()方法的区别

class.forName()前者除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。而classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
Class.forName得到的class是已经初始化完成的,Classloder.loaderClass得到的class是还没有链接的。

具体如何实现的可以看阿里的这篇技术文档,类加载器的使用场景

PS:本文很多内容也是引用自--《类加载器的使用场景

上一篇 下一篇

猜你喜欢

热点阅读