Java类加载器与双亲委派

前言

JVM对于java程序员来讲既是高级也是基础,刚入行的同窗没必要知道jvm的内存划分、不须要知道类的加载过程、GC的回收过程也能够舒舒服服的写代码,可是这种知其然不知其因此然的态度确定会限制咱们的上升空间,今天这篇文章开始走进jvm,咱们从第一步开始,先搞清楚类是怎么被加载的,这就是今天要分享的内容!java

JVM的组成结构

进入正题以前要先说一下JVM,JVM的组成结构主要是由 类装载子系统、运行时数据区、执行引擎、本地方法接口这4部分组成,而今天的文章主要围绕类状态子系统展开描述!c++

类加载器

  • 启动类加载器(BootstrapClassLoader):由c++语言提供,主要负责加载%{JAVA_HOME}\jdk1.8.0_261\jre\lib目录下的类;
  • 扩展类加载器(ExtClassLOader):sun.misc.Launcher.ExtClassLoader,主要负责加载%{JAVA_HOME}\jdk1.8.0_261\jre\lib\ext目录下的类;
  • 应用程序类加载器:sun.misc.Launcher.AppClassLoader,负责加载咱们配置的环境变量classpath目录下的类;
  • 自定义类加载器:经过继承ClassLoader类能够实现本身定制的类加载方式,这种方式能够用来打破双亲委派模型(双亲委派模型下面会讲到);

咱们能够经过一段代码很清晰的看到每一个类加载器的样子:程序员

public class TestClassLoader {
    public static void main(String[] args) {
        System.out.println(Object.class.getClassLoader());
        System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());
        System.out.println(TestClassLoader.class.getClassLoader());
    }
}
复制代码

输出结果:bootstrap

null
sun.misc.Launcher$ExtClassLoader@77459877
sun.misc.Launcher$AppClassLoader@18b4aac2
复制代码

Object类对应的加载器为何是null嘞? 上面已经说过了,jvm内部会建立一个c++编写的启动类加载器负责去加载%{JAVA_HOME}\jdk1.8.0_261\jre\lib目录下的类,这个加载器在java里面是获取不到的,因此是null; 下面两个,一个是ExtClassLoader,一个是AppClassLoader,是sun.misc.Launcher类的静态内部类;而这个Launcher类是由C++调用sun.misc.Launcher#getLauncher方法得到的;api

各个类加载之间的关系

能够经过如下代码看一下类加载器之间有什么联系安全

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);
复制代码

输出结果:markdown

the bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@77459877
the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2
复制代码

能够看到,默认的类加载器是AppClassLoader,往上走是ExtClassLoader,最上层是BootStrapClassLoader; 先来看一下类加载器的类图结构:app

image.png

类加载器都是继承的ClassLoader,为每一个子类都维护了一个parent属性:jvm

image.png

下面咱们就重点看parent属性作了什么事情,在实例化Launcher的时候 ,构造器里面对app和ext这两个对象作了初始化:ide

image.png

  • 上面的代码作了两件事情:
  1. 建立了一个ExtClassLoader,获得var1;
  2. 建立AppClassLoader,把var1传进去;

那继续点进去看看AppClassLoader是怎么建立的,中间套娃的代码我就跳过了,他是直接调用super(parent)这个构造器,直接看他就行了:

image.png

在这个地方维护了类加载器之间的父子关系,因此Ext也是App的父加载器,那么这么作他到底要干什么呢?这里涉及到了一个概念:双亲委派(下面会细说);上面的代码还反映了一个问题:默认的类加载器是AppClassLoader?在JVM内部会默认调用Launcher类的getClassLoader()方法来获取一个默认类加载器进行加载,而这个classLoader恰好就是在实例化Launcher类的时候生成的AppClassLoader:

image.png

image.png

双亲委派模型

image.png

一个类在被类加载器加载的时候,该类的加载器不会当即去加载,而是经过parent属性找到其父加载器进行加载,一直递归往上找,一直到顶层的BootStrap都没有被加载,就会返回到本类的加载器进行加载:

ClassLoader里面除了维护parent属性外,还维护了一个公共的loadClass方法,这个方法就是双亲委派的实现。咱们来详细分析下:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 这里会一直往上调,app的parent是ext,ext的parent是bootstrap
                        c = parent.loadClass(name, false);
                    } else {
                        // 这个方法最终会调用到一个native方法,加载不到类的时候会返回null
                        // return null if not found
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                // 父加载器没有加载到类,返回null
                if (c == null) {
                    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;
        }
    }
复制代码

看完上面的代码,咱们的思路就很清晰了,其实就是递归向上调用若是没有加载到就再向下返回;

  • 为何要设计双亲委派模型来加载类呢?
  1. 保证一个类在内存里只被加载一次;
  2. 保护了jdk内部的类的安全性,防止外部进行破坏;

第一点你们应该改都明白,第二点是什么意思嘞?咱们跑一段代码演示一下:

// 覆盖原有的java.lang包
package java.lang;

// 覆盖原有的Object类
public class Object {
    public static void main(String[] args) {
        System.out.println("object ....");
    }
}
复制代码

上面的代码执行完以后,会是什么结果呢?

错误: 在类 java.lang.Object 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args) 不然 JavaFX 应用程序类必须扩展javafx.application.Application 复制代码

因此,由于有双亲委派的存在,咱们这种恶意破坏原有api的行为就行不通了;

自定义类加载器

咱们能够经过自定义类加载器,来指定咱们本身要去加载的类;读完以上源码咱们不难发现,双亲委派的逻辑在loadClass方法里,而加载类的逻辑是在findClass方法里,我想要本身实现一个类加载器就应该去继承ClassLoader,重写findClass方法,在findClass方法里面去加载咱们本身指定的类:

public 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
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            byte[] bytes = loadByte(name);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    public static void main(String[] args) throws Exception {
        //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        // 在这个路径下面放一个User.class文件,由咱们本身的类加载器去加载
        Class clazz = classLoader.loadClass("com.maolin.User");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

复制代码

运行,输出结果:

com.maolin.MyClassLoader
复制代码

User.class的字节码是被MyClassLoader加载器加载的; 经过这种方式还能够打破双亲委派的机制,在重写findClass的基础上,再重写loadClass:

@Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c = findLoadedClass(name);
            /* 咱们把ClassLoader类的loadClass方法里的代码复制出来, 把双亲委派的那段代码去掉,让当前的类加载器直接加载,不向上委托 */
            if (c == null) {
                long t1 = System.nanoTime();
                c = findClass(name);

                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
复制代码
  • 什么状况下会打破双亲委派呢
  1. JDBC
  2. Tomcat

打破双亲委派机制篇幅太长,后续会单独写一篇文章来描述,想知道结果的能够参考这两篇文章:

  1. 聊聊JDBC是如何破坏双亲委派机制的
  2. 深刻理解 Tomcat(四)Tomcat 类加载器之为什么违背双亲委派模型

类加载器说完了,咱们再来瞅瞅一个类被加载的时候会经历哪些过程:

image.png

  • 一个完整的类加载过程会经历:加载-->验证-->准备-->解析-->初始化
  1. 加载:从磁盘上读取xx.class文件并生成其对应的Class对象;
  2. 校验字节码文件的格式(字节码也属于JVM系统的一种机器码,也是有必定的格式的)
  3. 准备:给类的静态变量分配内存并赋予默认值。boolean=false,int=0等;
  4. 解析:把符号引用替换为直接引用(也被称为静态连接),在加载阶段会对方法,静态变量生成的符号引用替换为内存中的真实引用地址;
  5. 初始化:给类的静态变量设置初始值,就是咱们本身设置的值,并执行静态代码块;

结语

感谢各位读者朋友耐心看完个人文章,文章中如有错误之处,还请留言指正,或者文章中有哪一个细节描述的不够清晰,也能够在评论区留言,我看到后必定会回复并改正;

不求作的最好,但求作的更好。

相关文章
相关标签/搜索