jvm类加载器详解和如何打破双亲委派机制

类加载过程:

项目启动的时候,并不是加载项目中的所有类,是在使用的时候加载,类加载器加载类的时候首先加载父类,所以Object类一定先被加载.
类加载器
AppClassLoader:
应用类加载器,又称为系统类加载器,负责在JVM启动时,加载来自在命令java中的classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径.
ExtClassLoade:
称为扩展类加载器,主要负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包或者由java.ext.dirs系统属性指定的jar包.放入这个目录下的jar包对AppClassLoader加载器都是可见的(因为ExtClassLoader是AppClassLoader的父加载器,并且Java类加载器采用了委托机制).
BootstrapClassloader:
是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等
加载过程
C++先调用sun.misc包下的Launcher类,在初始化中赋值,Launcher为单例模式,初始化AppClassLoader和ExtClassLoader,并将AppClassLoader的parent 赋值为ExtClassLoader,将ExtClassLoader的parent赋值为空,因为ExtClassLoader的parent是BootstrapClassloader,但是BootstrapClassloader是C语言编写,所以为null(父加载器并不是继承关系).
继承图


image.png

首先虚拟机调用Launcher构造函数,初始化类加载器, ClassLoader的初始化,跟进源码发现AppClassLoader的parent时ExtClassLoader, ExtClassLoader的parent是null


image.png
image.png

图上可以看到,首先调用父类的loadClass类,父类为null,则是bootStrap类加载器,如果返回的c为null,证明父类没有加载到,则调用自己的findClass

LoadClass的类加载过程

引用(https://baijiahao.baidu.com/s?id=1636309817155065432&wfr=spider&for=pc)
加载->验证->准备->解析->初始化->使用->卸载

加载

”加载“是”类加机制”的第一个过程,在加载阶段,虚拟机主要完成三件事:
(1)通过一个类的全限定名来获取其定义的二进制字节流
(2)将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
(3)在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。
相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为程序员可以使用系统的类加载器加载,还可以使用自己的类加载器加载。我们在最后一部分会详细介绍这个类加载器。在这里我们只需要知道类加载器的作用就是上面虚拟机需要完成的三件事,仅此而已就好了。

验证

验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。说白了也就是我们加载好的.class文件不能对我们的虚拟机有危害,所以先检测验证一下。他主要是完成四个阶段的验证:
(1)文件格式的验证:验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验(魔数、主版本号都是.class文件里面包含的数据信息、在这里可以不用理解)。
(2)元数据验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。
(3)字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出威海虚拟机安全的事。
(4)符号引用验证:它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。
对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用-Xverfity:none来关闭大部分的验证。

准备

准备阶段主要为类变量分配内存并设置初始值。这些内存都在方法区分配。在这个阶段我们只需要注意两点就好了,也就是类变量和初始值两个关键词:
(1)类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中,
(2)这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。比如
public static int value = 1; //在这里准备阶段过后的value值为0,而不是1。赋值为1的动作在初始化阶段。
当然还有其他的默认值。


image.png

注意,在上面value是被static所修饰的准备阶段之后是0,但是如果同时被final和static修饰准备阶段之后就是1了。我们可以理解为static final在编译器就将结果放入调用它的类的常量池中了。

解析

解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。什么是符号应用和直接引用呢?
符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

初始化

这是类加载机制的最后一步,在这个阶段,java程序代码才开始真正执行。我们知道,在准备阶段已经为类变量赋过一次值。在初始化阶端,程序员可以根据自己的需求来赋值了。一句话描述这个阶段就是执行类构造器< clinit >()方法的过程。
在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
①声明类变量是指定初始值
②使用静态代码块为类变量指定初始值
JVM初始化步骤
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
创建类的实例,也就是new的方式访问某个类或接口的静态变量,或者对该静态变量赋值调用类的静态方法反射(如 Class.forName(“com.shengsiyuan.Test”))初始化某个类的子类,则其父类也会被初始化Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类

双亲委派

上面提到了3中默认类加载器,看一下他们之间的关系


image.png

下面两张图解释getSystemClassLoader的返回值是AppClassLoader


image.png
image.png

l.getClassLoader()在文章的最开始可知,返回的就是AppClassLoader,
那么getSystemClassLoader的返回值是AppClassLoader也就证明了系统默认的类加载器为AppClassLoader

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 {
//如果parent不为空则调用parent的classLoader方法
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
//直到调用到BootstrapClassCladdLoader,使用顶级的类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                //如果没加载调用自己的findClass,并返回,由下一级的类加载器继续处理
                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;
        }
    }

上面就是双亲委派的核心,先调用AppClassLoader判断类是否被加载而不是直接BootstrapClassloader 加载(这样设计应该是绝大部分的类加载是我们自己类路径下的类,核心包和扩展包下的类少),在一级一级向上调用,如果没有再一级一级向下调用
这样做是 沙箱安全机制和避免类被重复加载.
沙箱安全机制: 保证核心包不能被篡改
避免类被重复加载:保证类只被加载一次

比如我这里定义Ineger类,包名也是java.lang,发现是不能使用print方法的,因为使用的还是rt.jar包下的类,
读者还可以尝试,定义main方法,直接执行,发现也是不行的.


image.png

全盘委托机制:
当类被一个ClassLoader加载的时候,该类所依赖的类也将由该ClassLoader加载,除非显示的调用要使用哪个类加载器.当然核心类和扩展类还是由各自的类加载器加载.
这句话的理解呢,就是A类中由成员变量B,A是被自定义类加载器LoaderA加载的,那B也是由LoaderA加载

自定义类加载器

自定义MyClassLoaser重写findClass方法,当调用ClassLoader的loadClass方法时,会调用自己重写的findClass方法,从而达到自定义的目的

public class MyClassLoader extends ClassLoader{
    String classPath;
    public MyClassLoader(String classPath){
        this.classPath = classPath;
    }

    @SneakyThrows
    @Override
//重写findClass方法
    protected Class findClass(String name) throws ClassNotFoundException{
        byte[] bytes = loadByte(name, classPath);
        Class aClass = defineClass(name, bytes, 0, bytes.length);
        return aClass;
    }

    private byte[] loadByte (String name,String classPath) throws IOException{
        name = name.replaceAll("\\.", "/");
         FileInputStream fis = new FileInputStream(classPath + "/" + name+ ".class");
         int len = fis.available();
         byte[] data = new byte[len];
         fis.read(data);
         fis.close();
         return data;
    }
}

代码准备好了,下面说明如何达到自定义类加载目的
通过debug发现当用MyClassLoader加载类的时候,MyClassLoader的parent时AppClassLoader,


image.png

根据双亲委派机制,AppClassLoader->ExtClassLoader-> BootstrapClassloader向上委托,
先有BootstrapClassloader加载,没有,再ExtClassLoader没有,再AppClassLoader发现有User.class则加载到方法区,不会再用MyClassLoader来加载.下图是在target有User.class的情况下,这里稍微解释一下自定义的MyClassLoader是要去D盘下的test目录去找User.class类,target是项目本身存在class文件,也就是AppClassLoader要去加载的类


image.png

将target的Use.class去掉的时候AppClassLoader找不到User.class类,那么类加载器ClassLoader已经变为MyClassLoader


image.png

打破双亲委派机制

错误示范 打破双亲委派需要重写loadClass方法,因为加载User类之前要加载Object类,但是在D:\test\java\lang目录下没有Object.class类文件,当我们手动的将Object.class类放到D:\test\java\lang的时候,依然会报错,因为加载父类的时候默认使用的还是加载本类的类加载器,所以在加载Object.class类的时候依然使用的是MyClassLoader类加载器,因为打破了双亲委派,所以不能向上委托,jvm是不允许我们自己加载核心类的(沙箱安全机制)


image.png
image.png
image.png

因为沙箱机制,就算存放Object.class类之后依然报错,因为这种核心类必须是rt.jar下才可以


image.png

如何解决上面的问题呢,就是除了加载我们自己的类时候,打破双亲委派,其余的类包括扩展类和核心类都有jvm自己去加载


image.png

那在实际项目中我们也很少的去打破双亲委派机制,除非像tomcat那样的中间件才有这个需求
那真的想要打破双亲委派机制,可以用spi技术,下篇文章继续分析.

你可能感兴趣的:(jvm类加载器详解和如何打破双亲委派机制)