JVM学习-详解类加载器(一)

类加载器

  • 类加载器是JVM执行类加载机制的前提
ClassLoader的作用
  • ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类型对应的java.lang.Class对象实例,然后交给Java虚拟机进行链接,初始化等操作,因此ClassLoader整个装载阶段,只能影响类的加载,而无法通过ClassLoader去改变类的链接和初始化行为,至于是否可以运行,由Execution Engine决定
  • 类加载器最早在java1.0版本中,那时只是单纯地为了满足Java Applet应用而被研发出来,但如今类加载器却在OSGi,字节码加解密领域大放异彩,这主要归功于Java虚拟机的设计者们当初在设计类加载器的时候,并没有考虑将它绑定在JVM内部,这样做的好处就是能够更加灵活和动态执行类加载操作。
类加载分类
显式加载
  • 显示加载指的是在代码中通过Classloader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象
隐式加载
  • 隐式加载则不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中
类加载必要性
  • 避免在开发中遇到java.lang.ClassNotFoundException异常或java.lang.NoClassDefFoundError异常时,手足无措,只有了解类加载器的加载机制才能在出现异常时根据错误异常日志定位问题和解决问题
  • 需要支持类的动态加载或需要对编译后的字节码文件进行加密操作时,就需要与类加载器打交道了
  • 开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑
命名空间
何为类的唯一性
  • 对于任意一个类,都需要加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间,**比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。**否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等
命名空间
  • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成
  • 在同一个命名空间中,不会出现类的完整名字相同的两个类
  • 在不同的命名空间中,有可能会出现类的完整名字相同的两个类
类加载机制的基本特征
  • 双亲委派模型:不是所有类加载都遵守这个模型,启动类加载器所加载的类型,是可能要加载用户代码的,比如JDK内部的ServiceProvider/ServiceLoader机制,用户可以在标准API框架上,提供自己的实现,JDK也需要提供些默认的参考实现,如Java中JNDI,JDBC,文件系统,Cipher等很多方面,都是利用这种机制,这种情况不会用双亲委派模型去加载,而是利用所谓的上下文加载器
  • 可见性:子类加载器可以访问父类加载器加载的类型,但是反过来是不允许的,不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑
  • 单一性:由于父加载器的类型对于子加载器可见,所以父加载器中加载过的类型,就不会在子加载器中重复加载,但是注意,类加载器"邻居"间,同一类型仍然可以被加载多次,因为互相并不可见。
类加载器
  • JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
  • 从概念上讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器 ,无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构如下:
  • 除了顶层的启动类加载器外,其余的类加载器都应有自己的"父类"加载器
  • 不同类加载器看似是继承(Inheritance)关系,实际上是包含关系,在下层加载器中,包含着上层加载器的引用
引导类加载器
  • 这个类加载器使用C/C++语言实现,嵌套在JVM内部
  • 它用来加载Java核心类库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)。用于提供JVM自身需要的类
  • 并不继承自java.lang.ClassLoader,没有父加载器
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java,javax,sun等开头的类
  • 加载扩展类加载器和应用程序类加载器,并指定为他们的父类加载器
  • 使用-XX:+TraceClassLoading
扩展类加载器
  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
  • 继承于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库,如用户创建的JAR放在此目录下,也会自动由扩展类加载器加载

    类加载器实验
系统类加载器(应用程序类加载器)
  • java语言编写,由sun.misc.Launcher$AppClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为扩展类加载器ExtClassLoader
  • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  • 该类为程序默认的类加载器,java应用的类由它来完成加载
  • 通过ClassLoader#getSystemClassLoader()方法可以获得此类加载器
用户自定义类加载器
  • 在Java的日常应用开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式
  • 体现Java语言的强大生命力和巨大魅力的关键因素之一便是,Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源
  • 通过类加载器可以实现非常绝妙的插件机制,这方面的实际应用安全举不胜举,例如,著名的OSGI组件框架,再如Eclipse的插件机制,类加载器为应用程序提供一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现
  • 同时,自定义加载器能够实现应用隔离,例如Tomcat,Spring等中间件和组件框架都在内部实现自定义的加载器,并通过自定义加载器隔离不同的组件模块,这种机制比C/C++程序要好很多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅一个兼容性便能阻挡住所有美好设想
  • 自定义类加载器通常需要继承于ClassLoader
ClassLoader源码解析
ClassLoader与现有类加载器的关系
  • 除了以上虚拟机自带的加载器外,用户还可以定制自己的类加载器,Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器继承ClassLoader
抽象类ClassLoader主要方法
  • public final ClassLoader getParent():返回类加载器的超类加载器
  • public Class<?> loadClass(String name) throws ClassNotFoundException :加载名称为name的类,返回结果为java.lang.Class类的实例,如果找不到类,则返回ClassNotFoundException异常,该方法中的逻辑就是双亲委派模式的实现
java 复制代码
//测试代码ClassLoader.getSystemClassLoader().loadClass("com.chapter11.User")涉及以下方法调用
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) {
                        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) {   //当前类加载器的父类加载器未加载此类或当前类加载器未加载此类
                    // 调用当前ClassLoader的findClass()
                    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;
        }
    }
  • protected Class<?> findClass(String name) throws ClassNotFoundException:查找二进制名称为name的类,返回结果为java.lang.Class类的实例,这是一个受保护的方法,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委派机制,该方法会在检查完父类加载器之后被loadClass()方法调用
  • 在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,1.2后不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面分析可知,findClass()方法是在loadClass()方法中被调用,当loadClass()方法中父加载器加载失败后,则会调用自己findClass()方法来完成类加载,这样就可以保证自定义类加载器也符合双亲委派模式
  • ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常和defineClass方法一起使用,一般情况下,在自定义加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象
java 复制代码
//在URLClassLoader中重写了findClass方法,代码如下
protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }
  • protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError:根据给定的字节数组b转换为Class实例,off和len参数表示实际Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的,这是受保护的方法,只有在自定义ClassLoader子类中可以使用
  • defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象,通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象
  • defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码转换成流,然后调用defineClass()方法生成类的Class对象
java 复制代码
private Class<?> defineClass(String name, Resource res) throws IOException {
        long t0 = System.nanoTime();
        int i = name.lastIndexOf('.');
        URL url = res.getCodeSourceURL();
        if (i != -1) {
            String pkgname = name.substring(0, i);
            // Check if package already loaded.
            Manifest man = res.getManifest();
            definePackageInternal(pkgname, man, url);
        }
        // Now read the class bytes and define the class
        java.nio.ByteBuffer bb = res.getByteBuffer();
        if (bb != null) {
            // Use (direct) ByteBuffer:
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, bb, cs);
        } else {
            byte[] b = res.getBytes();
            // must read certificates AFTER reading bytes.
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, b, 0, b.length, cs);
        }
    }
  • protected final void resolveClass(Class<?> c) :链接指定的一个Java类,使用该方法可以使用类的Class对象创建完成的同时也被解析,前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用
  • protected final Class<?> findLoadedClass(String name):查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例,这个方法是final方法,无法被修改
  • private final ClassLoader parent :它也是一个ClassLoader的实例,这个字段表示的ClassLoader也称为这个ClassLoader的双亲,在类加载过程中,ClassLoader可能会将某些请求交予自己的双亲处理
SecureClassLoader与URLClassLoader
  • SecureClassLoader扩展了ClassLoader,新增了几个与使用相关的代码源和权限定义类验证的方法,一般不直接与此类打交道,更多与其子类URLClassLoader有所关联
  • ClassLoader是一个抽象类,很多方法是空没有实现,如findClass,findResource等,而URLClassLoader这个实现类为这些方法提供了具体实现,新增了URLClassPath类协助取得Class字节码流等功能,在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClassLoader()方法及其获取字节码流的方式,使自定义类更加简洁
ExtClassLoader与AppClassLoader
  • 这两个类继承URLClassLoader,是sun.misc.Launcher的静态内部类,sun.misc.Launcher主要被系统用于启动主应用程序,ExtClassLoader和AppClassLoader都由sun.misc.Launcher创建
  • ExtClassLoader没有重写loadClass()方法,AppClassLoader重写了loadClass方法,但最终调用还是父类的loadClass()方法,因此两个类都遵循双亲委派模式
Class.forName()与ClassLoader.loadClass()
  • Class.forName()是一个静态方法,最常用的Class.forName(String className)根据传入的类的全限定名返回一个Class对象,该方法在将Class文件加载到内存的同时,会执行类的初始化
  • ClassLoader.loadClass():是一个实例方法,需要一个ClassLoader对象来调用该方法,该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化,该方法因为需要得到一个ClassLoader对象,所以可以根据需要指定使用哪个类加载器
相关推荐
憨子周40 分钟前
2M的带宽怎么怎么设置tcp滑动窗口以及连接池
java·网络·网络协议·tcp/ip
懒惰的bit2 小时前
基础网络安全知识
学习·web安全·1024程序员节
霖雨2 小时前
使用Visual Studio Code 快速新建Net项目
java·ide·windows·vscode·编辑器
SRY122404192 小时前
javaSE面试题
java·开发语言·面试
Fiercezm3 小时前
JUC学习
java
无尽的大道3 小时前
Java 泛型详解:参数化类型的强大之处
java·开发语言
ZIM学编程3 小时前
Java基础Day-Sixteen
java·开发语言·windows
我不是星海3 小时前
1.集合体系补充(1)
java·数据结构
P.H. Infinity3 小时前
【RabbitMQ】07-业务幂等处理
java·rabbitmq·java-rabbitmq
爱吃土豆的程序员3 小时前
java XMLStreamConstants.CDATA 无法识别 <![CDATA[]]>
xml·java·cdata