类加载器

概述

类加载器负责读取Java字节代码,并转换成java.lang.Class类的一个实例的代码模块。

类加载器除了用于加载类外,还可用于确定类在Java虚拟机中的唯一性。

任意一个类,都由加载它的类加载器和这个类本身一同确定其在 Java 虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间,而不同类加载器中是允许同名(指全限定名相同)类存在的。

比较两个类是否“相等”,前提是这两个类由同一个类加载器加载,否则,即使这两个类来源于同一个Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。

这里“相等”是指:类的Class对象的equals()方法、isInstance()方法的返回结果,使用instanceof关键字做对象所属关系判定等情况。

加载器的种类

1.启动类加载器:Bootstrap ClassLoader

最顶层的加载类,由 C 实现,负责加载%JAVA_HOME%/lib目录下的jar包和类或者被 -Xbootclasspath参数指定的路径中的所有类。

2.拓展类加载器:Extension ClassLoader

负责加载java平台中扩展功能的一些jar包,如加载%JRE_HOME%/lib/ext目录下的jar包和类,或-Djava.ext.dirs所指定的路径下的jar包。

3.系统类加载器/应用程序加载器:App ClassLoader

负责加载当前应用classpath中指定的jar包及-Djava.class.path所指定目录下的类和jar包。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

4.自定义类加载器:Custom ClassLoader

通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader

验证不同加载器

每个类加载都有一个父类加载器,可以通过程序来验证

    public static void main(String[] args) {
        // App ClassLoader
        System.out.println(new User().getClass().getClassLoader());
        // Ext ClassLoader
        System.out.println(new User().getClass().getClassLoader().getParent());
        // Bootstrap ClassLoader
        System.out.println(new User().getClass().getClassLoader().getParent().getParent());
        // Bootstrap ClassLoader
        System.out.println(new String().getClass().getClassLoader());
    }

AppClassLoader的父类加载器为ExtClassLoader, ExtClassLoader的父类加载器为 null,null 并不代表ExtClassLoader没有父类加载器,而是 BootstrapClassLoader 。

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@5fdef03a
null
null

核心方法

查看类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 {
                	// 父加载器不为空,调用父加载器loadClass()方法处理
                    if (parent != null) {
                    	// 让上一层加载器进行加载
                        c = parent.loadClass(name, false);
                    } else {
                    	// 父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
                        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();
                    // 调用此类加载器所实现的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方法是当字节码加载到内存后进行链接操作,对文件格式和字节码验证,并为 static 字段分配空间并初始化,符号引用转为直接引用,访问控制,方法覆盖等
                resolveClass(c);
            }
            return c;
        }
    }

JVM类加载机制的三种方式

全盘负责

当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

注意:

系统类加载器AppClassLoader加载入口类(含有main方法的类)时,会把main方法所依赖的类及引用的类也载入。只是调用了ClassLoader.loadClass(name)方法,并没有真正定义类。真正加载class字节码文件生成Class对象由双亲委派机制完成。

父类委托、双亲委派

父类委托即双亲委派,双亲委派模型是描述类加载器之间的层次关系。它要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。父子关系一般不会以继承的关系实现,而是以组合关系来复用父加载器的代码。

双亲委派模型是指:子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。

双亲委派模型的好处

保证Java程序的稳定运行,避免类的重复加载:JVM区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类

保证Java核心API不被篡改:如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,如编写一个称为java.lang.Object 类,程序运行时,系统就会出现多个不同的Object类。反之使用双亲委派模型:无论使用哪个类加载器加载,最终都会委派给最顶端的启动类加载器加载,从而使得不同加载器加载的Object类都是同一个。

双亲委派机制加载Class的具体过程:

1. ClassLoader先判断该Class是否已加载,如果已加载,则返回Class对象,如果没有则委托给父类加载器

2. 父类加载器判断是否加载过该Class,如果已加载,则返回Class对象,如果没有则委托给祖父类加载器

3. 依此类推,直到始祖类加载器(引用类加载器)

4. 始祖类加载器判断是否加载过该Class,如果已加载,则返回Class对象

如果没有则尝试从其对应的类路径下寻找class字节码文件并载入

如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的子类加载器

5. 始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入

如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的孙类加载器

6. 依此类推,直到源ClassLoader

7. 源ClassLoader尝试从其对应的类路径下寻找class字节码文件并载入

如果载入成功,则返回Class对象;如果载入失败,源ClassLoader不会再委托其子类加载器,而是抛出异常

注意:

双亲委派机制是Java推荐的机制,并不是强制的机制。可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,应该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法。

缓存机制

缓存机制将会保证所有加载过的Class都将在内存中缓存,当程序中需要使用某个Class时,类加载器先从内存的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。

对于一个类加载器实例来说,相同全名的类只加载一次,即loadClass方法不会被重复调用。因此,这就是为什么修改Class后,必须重启JVM,程序的修改才会生效的原因。

JDK8使用的是直接内存,所以会用到直接内存进行缓存。因此,类变量为什么只会被初始化一次的原因。

打破双亲委派

在加载类的时候,会一级一级向上委托,判断是否已经加载,从自定义类加载器 --> 应用类加载器 --> 扩展类加载器 --> 启动类加载器,如果到最后都没有加载这个类,则回去加载自己的类。

双亲委派模型并不是强制模型,而且会带来一些些的问题。例如:java.sql.Driver类,JDK只能提供一个规范接口,而不能提供实现。提供实现的是实际的数据库提供商,提供商的库不可能放JDK目录里。

重写loadclass方法

自定义类加载,重写loadclass方法,即可破坏双亲委派机制

因为双亲委派的机制都是通过这个方法实现的,这个方法可以指定类通过什么类加载器来进行加载,所有如果改写加载规则,相当于打破双亲委派机制

import cn.ybzy.demo.Test;

import java.io.*;

public class MyClassLoader extends ClassLoader {

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

    private byte[] loadClassData(String className) throws IOException {
        String replace = className.replace('.', File.separatorChar);
        String path = ClassLoader.getSystemResource("").getPath()   replace   ".class";
        InputStream inputStream = null;
        ByteArrayOutputStream byteArrayOutputStream = null;
        try {
            inputStream = new FileInputStream(path);
            byteArrayOutputStream = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = inputStream.read(buffer)) != -1) {
                byteArrayOutputStream.write(buffer, 0, length);
            }
            return byteArrayOutputStream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (byteArrayOutputStream != null) {
                byteArrayOutputStream.close();
            }
            if (inputStream != null) {
                inputStream.close();
            }
        }

        return null;
    }

    @Override
    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 {
                    // 修改classloader的原双亲委派逻辑,从而打破双亲委派
                    if (name.startsWith("cn.ybzy.demo")) {
                        c = findClass(name);
                    } else {
                        c = this.getParent().loadClass(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();
                    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 {
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> aClass = classLoader.loadClass(Test.class.getName());
        System.out.println(aClass.getClassLoader());
    }
cn.ybzy.demo.MyClassLoader@2f410acf

自定义类加载器

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在类中对文件进行解密。

准备字节码文件

创建Test类,同时进行javac Test.class编译成字节码文件,放到目录下:D:\Temp\cn\ybzy\demo

package cn.ybzy.demo;

public class Test {
    public static void main(String[] args) {
        System.out.println("Test...");
    }
}

创建自定义类加载器

import java.io.*;

public class MyClassLoader extends ClassLoader {
    private String root;

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

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

        return null;
    }

    public String getRoot() {
        return root;
    }

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

执行测试

启动main方法,执行测试

    public static void main(String[] args) {
        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("D:\\Temp");
        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("cn.ybzy.demo.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();
        }
    }
cn.ybzy.demo.MyClassLoader@5679c6c6

将Test类放到项目类路径下,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader 加载,而不会通过自定义类加载器来加载

sun.misc.Launcher$AppClassLoader@18b4aac2

注意事项

1、这里传递文件名需要是类的全限定性名称,因为defineClass方法是按这种方式/格式进行处理

因此,若没有全限定名,需要将类的全路径加载进去

2、不要重写loadClass方法,因为这样容易破坏双亲委托模式

3、Test类本身可以被AppClassLoader类加载,因此不能把Test.class放在类路径下

否则,由于双亲委托机制的存在,会直接导致该类由AppClassLoader加载,而不会通过自定义类加载器来加载

以上就是JVM类加载器之ClassLoader的使用详解的详细内容,更多关于JVM类加载器ClassLoader的资料请关注Devmax其它相关文章!

JVM类加载器之ClassLoader的使用详解的更多相关文章

  1. android – 基于JVM的语言,没有语言运行时

    ProGuard可以删除特定程序中未使用的部分运行时.如果速度和时间非常关键,另一种方法是使用NDK.

  2. android – 如何在gradle中调整dex内存的jvm args?

    我有一个Android项目,它在dex步骤中当前没有堆空间:我想在gradle中提高jvmmin/max设置,就像我们以前使用Maven插件一样:但是在gradle中的android插件的文档中我只看到这些选项:有办法吗?

  3. android – Timer()作为守护进程与非守护进程

    什么时候应该在Android应用程序中作为守护进程启动计时器?

  4. JVM的常用命令汇总

    监测java应用,最方便的就是直接使用jdk提供的现成工具,在jdk的安装的bin目录下,已经提供了多种命令行监测工具。本文为大家总结了几个JVM的常用命令,需要的可以参考一下

  5. Java JVM虚拟机调优详解

    JVM是JavaVirtualMachine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的,本文主要介绍了jvm调优,感兴趣的小伙伴们可以参考一下<BR>

  6. Java JVM中线程状态详解

    这篇文章主要介绍了Java JVM中线程状态详解,文章围绕主题展开详细的内容介绍,具有一定的参考价值,感兴趣的朋友可以参考一下

  7. JVM jstack实战之死锁问题详解

    如果在生产环境发生了死锁,我们将看到的是部署的程序没有任何反应了,这个时候我们可以借助 jstack进行分析,下面我们实战操作查找死锁的原因

  8. java自旋锁和JVM对锁的优化详解

    这篇文章主要为大家介绍了java自旋锁和JVM对锁的优化示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  9. Java 对象在 JVM 中的内存布局超详细解说

    这篇文章主要介绍了Java 对象在 JVM 中的内存布局超详细解说,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下

  10. Java ClassLoader虚拟类实现代码热替换的示例代码

    本文主要介绍了Java ClassLoader虚拟类实现代码热替换的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

随机推荐

  1. 基于EJB技术的商务预订系统的开发

    用EJB结构开发的应用程序是可伸缩的、事务型的、多用户安全的。总的来说,EJB是一个组件事务监控的标准服务器端的组件模型。基于EJB技术的系统结构模型EJB结构是一个服务端组件结构,是一个层次性结构,其结构模型如图1所示。图2:商务预订系统的构架EntityBean是为了现实世界的对象建造的模型,这些对象通常是数据库的一些持久记录。

  2. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  4. (jsp/html)网页上嵌入播放器(常用播放器代码整理)

    网页上嵌入播放器,只要在HTML上添加以上代码就OK了,下面整理了一些常用的播放器代码,总有一款适合你,感兴趣的朋友可以参考下哈,希望对你有所帮助

  5. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  6. Java异常Exception详细讲解

    异常就是不正常,比如当我们身体出现了异常我们会根据身体情况选择喝开水、吃药、看病、等 异常处理方法。 java异常处理机制是我们java语言使用异常处理机制为程序提供了错误处理的能力,程序出现的错误,程序可以安全的退出,以保证程序正常的运行等

  7. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  8. 面试突击之跨域问题的解决方案详解

    跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。那怎么解决这个问题呢?接下来我们一起来看

  9. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  10. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部