JVM系列-深入理解ClassLoader类加载器

1、类加载器:

1.1、基本概念:【作用于类加载的加载阶段】

  • 当你写完了一个*.java文件的时候,编译器会把他编译成一个由字节码组成的class文件,当程序运行时,JVM会首先寻找包含有main()方法的类,把这个class文件中的字节码数据读入进来,转化成JVM中运行时对应的Class对象。执行这个动作的,就叫类加载器。
  • 类的加载就是虚拟机通过一个类的全限定名来获取描述此类的二进制字节流,而完成这个加载动作的就是类加载器。
  • 类加载器将.class文件的二进制文件装入JVM的方法区,并且在堆区创建描述这个类的java.lang.Class对象。用来封装数据。 但是同一个类只会被类加载器加载一次,记住:只加载一次!利用加载的类可以实例化出各种不同的对象!

1.2、ClassLoader类:

  • 是Java层几乎所有类加载器的父类,它定义了加载器的基本行为和加载动作。

1.3、判断2个类是不是同一个:

  • 判定两个类是否相等,只有在这两个类被同一个类加载器加载的情况下才有意义,否则即便是两个类来自同一个Class文件,被不同类加载器加载,它们也是不相等的。
  • 这里的相等性包括Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果以及Instance关键字对对象所属关系的判定结果等。
  • 同一个Class = 相同的 ClassName + PackageName + ClassLoader

2、Java 提供三种类型的系统类加载器:

2.1、启动类加载器(bootstrap ClassLoader):

  • 由C++语言实现,通常在虚拟机启动不久后实例化,我们可以将其视作JVM的一部分,我们开发者无法直接使用该加载器。
  • 他的作用通常是负责加载系统的基础jar包($JAVA_HOME/jre/lib 里的核心 jar 文件)或者被-Xbootclasspath参数所指定的路径【这个路径下只有被虚拟机所识别的库才可以加载】,并且它不做验证工作。
  • BootstrapClassLoader 比较特殊,它不继承 ClassLoader,而是由 JVM 内部实现;

2.2、另外两种是 Java 语言自身实现的类加载器,包括:

2.2.1、扩展类加载器(ExtClassLoader) :
  • 这个类加载器用来加载 Java 的扩展库。
  • 负责加载$JAVA_HOME/jre/lib/ext 目录下的 jar 文件或者被java.ext.dirs系统变量所指定的路径的所有类库。
  • 我们开发者可以直接使用。
2.2.2、应用程序类加载器(AppClassLoader):
  • 负责加载用户类路径中的文件。用于加载我们自己定义编写的类。
  • 如果应用程序中没有实现自己的类加载器,一般就是这个类加载器去加载应用程序中的类库。

2.3、自定义类加载器(User ClassLoader):

  • 通过继承java.lang.ClassLoader类的方式实现自己的类加载器以满足一些特殊的需求。
    在这里插入图片描述

注意:

  • 这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。
  • 默认情况下,用户自定义的类使用 AppClassLoader 加载,AppClassLoader的父加载器为ExtClassLoader,但是 ExtClassLoader的父加载器却显示为空,这是什么原因呢?究其缘由,启动类加载器属于 JVM 的一部分,它不是由 Java 语言实现的,在 Java 中无法直接引用,所以才返回空。

3、双亲委托加载具体过程:

//ClassLoader.java类, 类加载逻辑代码:
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            //首先检查class是否已经被加载
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //如果class没有被加载且已经设置parent,那么请求其父加载器加载
                    if (parent != null) {
                        //注意当这里调用parent.loadClass()方法找不到Class时会抛出ClassNotFoundException异常,但是该异常是被捕获的
                        c = parent.loadClass(name, false);
                    } else {
                        //如果没有设定parent类加载器,则寻找BootstrapClss并尝试使用Boot loader加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                //如果当前这个loader所有的父加载器以及顶层的Bootstrap ClassLoader都不能加载待加载的类,那么则调用自己的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;
        }
    }

    /**
     * 	1. 检查目标 class 是否曾经加载过,如果加载过则直接返回;
     * 	2. 如果没加载过,把加载请求传递给 parent 加载器去加载;
     * 	3. 如果 parent 加载器加载成功,则直接返回;
     *  4. 如果 parent 未加载到,则自身调用 findClass() 方法进行寻找,并把寻找结果返回。
     */

3.1、双亲委派机制:

  • 比如A类的加载器是AppClassLoader,AppClassLoader不会自己去加载类,而会委托ExtClassLoader进行加载,那么到了ExtClassLoader类加载器的时候,它也不会自己去加载,而是委托BootStrap类加载器进行加载,就这样一层一层往上委托,如果Bootstrap类加载器无法进行加载的话,再一层层往下走。
  • 双亲委派机制优点:
    • 采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
    • 系统类防止内存中出现多份同样的字节码,每一个类都只会被加载一次,避免了重复加载。
    • 维护Java类加载的安全,有效避免了某些恶意类的加载。(比如自定义了Java.lang.Object类,一般而言在双亲委派模型下会加载系统的Object类而不是自定义的Object类)

4、自定义类加载器:

  • 通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java 类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。
  • 自定义类加载器一般都是继承自 ClassLoader类,从上面对loadClass方法来分析来看,我们只需要重写findClass方法即可。
  • 最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
    public class MyClassLoader extends ClassLoader {
        private String root;

        protected Class<?> findClass(String name) throws ClassNotFoundException {
            byte[] classData = loadClassData(name);
            if (classData == null) {
                throw new ClassNotFoundException();
            } else {
                return defineClass(name, classData, 0, classData.length);
            }
        }

        private byte[] loadClassData(String className) {
            String fileName = root + File.separatorChar
                    + className.replace('.', File.separatorChar) + ".class";
            try {
                InputStream ins = new FileInputStream(fileName);
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                int bufferSize = 1024;
                byte[] buffer = new byte[bufferSize];
                int length = 0;
                while ((length = ins.read(buffer)) != -1) {
                    baos.write(buffer, 0, length);
                }
                return baos.toByteArray();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }

        public String getRoot() {
            return root;
        }

        public void setRoot(String root) {
            this.root = root;
        }

        public static void main(String[] args) {
            MyClassLoader classLoader = new MyClassLoader();
            classLoader.setRoot("/Users/liuxp/tmp");
            Class<?> testClass = null;
            try {
                testClass = classLoader.loadClass("com.test.classloading.Test");
                Object object = testClass.newInstance();
                System.out.println(object.getClass().getClassLoader());
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

5、函数调用过程:

在这里插入图片描述

5.1、loadClass( )

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
  • loadClass(String,boolean)函数即实现了双亲委派模型。
  • 首先,检查下制定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
  • 如果此类没有加载过,那么,再判断下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name,false);)或者是调用bootstrap类加载器来加载。
  • 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。
  • 换句话说,如果自定义类加载器,就必须重写findClass方法!

5.2、findClass( )

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
  • 抽象类ClassLoader的findClass函数默认是抛出异常的。而前面我们知道,loadClass()在父加载器无法加载类的时候,就会用我们自定义的类加载器中的findClass函数。
  • 如果是读取一个指定的名称的类为字节数组的话,这个很好办。但是如何将字节数组转为class对象呢?很简单,Java提供了defineClass方法,通过这个方法,就可以把一个字节数组转为Class对象了。

5.3、defineClass( )

  • 将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
  • 如果class文件时加密过的,则需要解密后作为形参传入defineClass函数。
    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(name, b, off, len, null);
    }

了解更多,欢迎关注:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值