深入理解类加载机制及类加载器

首先理解类加载的总体过程

首先,要想真正的把握类加载的过程及对应的Java源码分析,理解大纲图是非常有必要的,案例代码及原理图如下图:

public class Math {
    public static final int initData = 666;
    public static User user = new User();

    public int compute() {  //一个方法对应一块栈帧内存区域
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
    }

}

在这里插入图片描述

在xxx这里插入图片描述

类加载流程

1、加载
思考一个问题,在这个过程中,加载的具体什么,针对我们开发人员,我们写的仅仅是java源文件而已,而Java虚拟机是如何进行加载的呢?
java文件---->class文件(前期编译经过了一系列的操作,如词法分析器–>tokens流–>语法分析器–>语法树/抽象语法树–>语义分析器–>注解抽象语法树–>字节码生成器–>class文件)
加载的过程【class文件 --> JVM内部各种折腾】:首先通过类的全限定名将该类转化为二进制的流文件,但在这个文件依然仅仅是静态的存储结构,将该字节流的静态存储结构转化为方法区运行时数据结构,同时在 Java堆中生成代表该类的Java.lang.Class对象,供外部访问
2、验证
此时我们知道已经生成了class文件,class文件是二进制文件,此阶段是校验字节码文件是否正确,那么仅仅是校验字节码文件吗?那可不是!校验包含文件格式验证、元数据校验、字节码验证、符号引用验证。
文件格式验证:验证字节流是否符合Class文件的规范,并且能被当前版本虚拟机所处理,这阶段的验证是基于二进制字节流进行的,只有经过该阶段验证后,字节流才会进入内存的方法区中存储,后面的验证都是基于方法区的存储结构进行的;如是否以16进制cafebaby开头,版本号是否正确等!
元数据校验:对类的元数据信息进行语义校验,实际上就是对Java语法的校验;如是否有父类,是否继承了final类(我们知道类上有final关键字的类是不能被继承的),一个非抽象类是否实现抽象类的所有方法!
字节码验证:进行数据流和控制流分析,确定程序语义是合法的,符合逻辑的;如运行检查,栈数据类型和操作码操作参数吻合,跳转指令指向合理的位置等!
符号引用验证:它发生在将符号引用转化为直接引用阶段,可以看做是对类自身以外的信息(常量池中的各种符号引用)进行匹配性校验,确保解析动作能正常进行;如常量池中描述类是否存在,访问的方法或者字段是否存在且有足够的权限!
3、准备
为已在方法区中类的静态成员变量分配内存,并将其初始化为默认值。进行分配内存的只是包括类变量(静态变量),而不包括实例变量。

public static int i = 1; 
//在准备阶段后的初始化值为0,不为1,这时候只是开辟了内存空间,并没有运行Java代码,
//i赋值为1的指令是程序被编译后,存放于类构造器()方法之中,所以i被赋值为1是在初始化阶段才会执行
/*在这里我们知道,初始化的值是通过类构造器获取的*/

在这里插入图片描述
这里不包含final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化。

   private int i = 1;
   //这里不会为实例变量(也就是没加static)分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

ConstantValue属性作用:ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static修饰的变量才可以使用这项属性。非static类型的变量的赋值是在实例构造器方法中进行的;static类型变量赋值分两种,在类构造其中赋值,或使用ConstantValue属性赋值。

   public final static int i = 2;
   /*对于一些特殊情况,如果类字段属性表中存在ConstantValue属性,现在准备阶段变量i就会被初始化为
   ConstantValue属性所指的值,只有同时被final和static修饰的字段才有ConstantValue属性,
   且限于基本类型和String,那么为什么是这两种类型?
   因为从常量池中只能引用到基本类型和String类型的字面量
   */
   //总结:编译时Javac将会为i生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为1,我们可以理解为final static常量在编译期就将其结果放入了调用它的类的常量池中,具体的ConstantValue属性,我们可以通过javap -v -p xxx.class反编译得到的结果看到该属性

4、解析
把类的符号引用转化为直接引用
解析阶段就是虚拟机将常量池内的符号引用替换为直接引用的过程
常量池字面量(由字母、数字等构成的字符串或者数值常量)、符号引用组成,例如:a=1, a为符号引用,1为字面量,同时符号引用也可以是类的全限定名、方法字段名称等!(想要深入理解常量池,后续会陆续更新)
5、初始化
初始化阶段是执行类构造器的过程
为静态成员变量赋值,赋的是程序员给定的值
6、使用
主动引用
只有当对类的主动使用的时候才会导致类的初始化,类的主动使用有六种:

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(如 Class.forName(“com.carl.Test”) )
  • 初始化某个类的子类,则其父类也会被初始化
  • Java虚拟机启动时被标明为启动类的类(JvmCaseApplication ),直接使用 java.exe 命令来运行某个主类

被动引用

  • 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
  • 定义类数组,不会引起类的初始化。
  • 引用类的static final常量,不会引起类的初始化(如果只有static修饰,还是会引起该类初始化的)。

7、销毁
在类使用完后,如果满足下面的条件,类就会被卸载:

  • 该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例
  • 加载该类的classLoader被回收
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

如果以上三个条件全部满足,JVM就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,Java类的整个生命周期就结束了。我们知道卸载类很难,每次进行垃圾回收的时候,元空间几乎没什么变动,因为类加载机制中类加载器几乎不会被回收。

类加载器和双亲委派机制

四种类加载器: bootstrapClassLoader(引导类加载器·),ExtClassLoader(扩展类加载器),appliactionClassLoader(应用程序类加载器),customerClassLoader(自定义类加载器)
含义:在加载(load)阶段,其中第(1)步,通过类的全限定名获取其定义的二进制字节流,需要借助类加载器完成,顾名思义,就是用来加载class文件的。
代码示例:

public class TestJDKClassLoader {

    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
        System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());

        System.out.println();
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        ClassLoader extClassloader = appClassLoader.getParent();
        ClassLoader bootstrapLoader = extClassloader.getParent();
        System.out.println("the bootstrapLoader : " + bootstrapLoader);
        System.out.println("the extClassloader : " + extClassloader);
        System.out.println("the appClassLoader : " + appClassLoader);

        System.out.println();
        System.out.println("bootstrapLoader加载以下文件:");
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urls.length; i++) {
            System.out.println(urls[i]);
        }

        System.out.println();
        System.out.println("extClassloader加载以下文件:");
        System.out.println(System.getProperty("java.ext.dirs"));

        System.out.println();
        System.out.println("appClassLoader加载以下文件:");
        System.out.println(System.getProperty("java.class.path"));

    }
}

运行结果:
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader

the bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@3764951d
the appClassLoader : sun.misc.Launcher$AppClassLoader@14dad5dc

bootstrapLoader加载以下文件:
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/resources.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/rt.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/sunrsasign.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/jsse.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/jce.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/charsets.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/jfr.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/classes

extClassloader加载以下文件:
D:\dev\Java\jdk1.8.0_45\jre\lib\ext;C:\Windows\Sun\Java\lib\ext

appClassLoader加载以下文件:
D:\Program Files\Java\jdk1.8.0_301\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\deploy.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\access-bridge-64.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\cldrdata.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\dnsns.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\jaccess.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\jfxrt.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\localedata.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\nashorn.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunec.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunjce_provider.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunmscapi.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunpkcs11.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\zipfs.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\javaws.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\jfxswt.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\management-agent.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\plugin.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\rt.jar;E:\workdir\projects\tuling\jvm\target\classes;D:\work\jars\m2\repository\org\openjdk\jol\jol-core\0.9\jol-core-0.9.jar;D:\work\ide\idea\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar

双亲委派机制
原理:首先先找自己是否加载过该类,如果加载过,则直接使用,没有的话,逐级往上层寻找,没有找到的话再逐级往下层去找,Java中具体的源码实现请看Launcher类源码分析。
双亲委派机制只是Java推荐的机制,并不是强制的机制
原理图:在这里插入图片描述在这里插入图片描述

思考一个问题,为什么要设计这种机制呢?可以从它的好处着手。

  • 沙箱安全
  • 避免类的重复加载
    沙箱安全机制可以避免自己写的java.lang.String.class类不会被加载,这样便可以防止核心
    API库被随意篡改;避免类的重复加载可以使得当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

深入Java源码理解Launcher类

从全文开篇的第二张图,我们可以知道,Java虚拟机首先通过C和C++创建了一个引导类加载器,这个类加载器是c、c++实现的,我们在Java层面是获取不到的,其次就是通过Launcher类去创建了ExtClassLoader、AppClassLoader,同时指定了系统类加载为AppClassLoader,通过parent属性指定AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是引导类加载器,不是通过继承实现,源码分析如下:

//Launcher类构造方法
public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            //构造扩展类加载器,在构造的过程中将其父加载器设置为null
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
          //构造应用类加载器,在构造的过程中将其父加载器设置为ExtClassLoader,
         //Launcher的loader属性值是AppClassLoader,我们一般都是用这个类加载器来加载我们自己写的应用程序
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        。。。 。。。 //省略一些不需关注代码
        }
    }

双亲委派的源码实现: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();
                    //如果委托父类都没有找到,就自己加载
                    //都会调用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;
        }
    }

从上述源码中,我们知道,如果要自定义类加载器·,首先要继承ClassLoader类,如果不打破双亲委派机制,重写findClass(name)方法;打破双亲委派机制,重写loadClass(name)方法

自定义类加载器

自定义类加载器只需要继承java.lang.ClassLoader类,该类有两个核心方法,一个是loadClass(),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所有我们自定义类加载器主要是重写findClass方法。

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

        private byte[] loadByte(String name) throws Exception {
            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;
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                //defineClass将一个字节数组转化为一个class对象,这个字节数组是class文件读取后最终的字节数组。
                return defineClass(name,data,0, data.length);
            }  catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
        }
    }

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
            Class<?> clazz = classLoader.loadClass("com.jiang.jvm.User1");
            Object obj = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("sout", null);
            method.invoke(obj, null);
            System.out.println(clazz.getClassLoader().getClass().getName());
    }

打破双亲委派机制

双亲委派这个模型并不是强制模型,而且会带来一些问题,就比如java.sql.Driver这个东西。JDK只能提供一个规范接口,而不能提供实现。提供实现的是实际的数据库提供商。提供商的库总不能放JDK目录里吧。
SPI: JDK提供接口,提供商提供服务,编程人员编码时面向接口编程,JDK能够自动找到合适的实现
OSGI:比如我们的JAVA程序员更加追求程序的动态性,比如代码热部署,代码热替换。也就是机器不用重启,只要部署上就能用。OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块都有一个自己的类加载器,当需要更换一个程序模块时,就把程序模块连同类加载器一起换掉以实现代码的热替换。

打破双亲委派,重写loadClass()方法

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

        private byte[] loadByte(String name) throws Exception {
            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;
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                //defineClass将一个字节数组转化为一个class对象,这个字节数组是class文件读取后最终的字节数组。
                return defineClass(name,data,0, data.length);
            }  catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

        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();
                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        if (!name.startsWith("com.jiang.jvm")) {
                           c = this.getParent().loadClass(name);
                        } else
                        {
                            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;
            }
        }
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
            Class<?> clazz = classLoader.loadClass("com.jiang.jvm.User1");
            Object obj = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("sout", null);
            method.invoke(obj, null);
            System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

  • 9
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值