JVM类加载器之ClassLoader的使用详解

目录
  • 类加载器
    • 概述
    • 加载器的种类
    • 验证不同加载器
    • 核心方法
  • JVM类加载机制的三种方式
    • 全盘负责
    • 父类委托、双亲委派
    • 缓存机制
  • 打破双亲委派
    • 重写loadclass方法
  • 自定义类加载器
    • 准备字节码文件
    • 创建自定义类加载器
    • 执行测试
    • 注意事项

类加载器

概述

类加载器负责读取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的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java 类加载机制详细介绍

    一.类加载器 类加载器(ClassLoader),顾名思义,即加载类的东西.在我们使用一个类之前,JVM需要先将该类的字节码文件(.class文件)从磁盘.网络或其他来源加载到内存中,并对字节码进行解析生成对应的Class对象,这就是类加载器的功能.我们可以利用类加载器,实现类的动态加载. 二.类的加载机制 在Java中,采用双亲委派机制来实现类的加载.那什么是双亲委派机制?在Java Doc中有这样一段描述: The ClassLoader class uses a delegation mo

  • JVM中ClassLoader类加载器的深入理解

    JVM的体系结构图 先来看一下JVM的体系结构,如下图: JVM的位置 JVM的位置,如下图: JVM是运行在操作系统之上的,与硬件没有直接的交互,但是可以调用底层的硬件,用JIN(Java本地接口调用底层硬件) JVM结构图中的class files文件 class files文件,是保存在我们电脑本地的字节码文件,.java文件经过编译之后,就会生成一个.class文件,这个文件就是class files所对应的字节码文件,如下图: JVM结构图中的类加载器ClassLoader的解释 类加

  • JVM类加载器之ClassLoader的使用详解

    目录 类加载器 概述 加载器的种类 验证不同加载器 核心方法 JVM类加载机制的三种方式 全盘负责 父类委托.双亲委派 缓存机制 打破双亲委派 重写loadclass方法 自定义类加载器 准备字节码文件 创建自定义类加载器 执行测试 注意事项 类加载器 概述 类加载器负责读取Java字节代码,并转换成java.lang.Class类的一个实例的代码模块. 类加载器除了用于加载类外,还可用于确定类在Java虚拟机中的唯一性. 任意一个类,都由加载它的类加载器和这个类本身一同确定其在 Java 虚拟

  • Java类加载器ClassLoader的使用详解

    目录 BootstrapClassLoader ExtClassLoader AppClassLoader 类加载器的具体实现在哪里 类加载器的初始化时机 如何进行的类加载 Loader.getResource(resourceName) 调用获取资源方法的一些常见问题 BootstrapClassLoader 加载范围(根据系统参数): System.getProperty("sun.boot.class.path"); 负责加载核心类库,以我的本地的环境来展示获取的内容: D:\d

  • java中类加载与双亲委派机制详解

    目录 类加载是什么 类加载器 双亲委派机制 BootStrapClassLoader ExtClassLoader AppClassLoader 为什么使用双亲委派机制 全盘负责委托机制 自定义类加载器 打破双亲委派机制 类加载是什么 把磁盘中的java文件加载到内存中的过程叫做类加载 当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM. 有如下 User 类 package dc.dccmmtop; public Class User { publi

  • JVM 方法调用之静态分派(详解)

    分派(Dispatch)可能是静态也可能是动态的,根据分派依据的宗量数可分为单分派和多分派.这两种分派方式的两两组合就构成了静态单分派,静态多分派,动态单分派,动态多分派这4种组合.本章讲静态分派. 1.静态分派 所有依赖静态类型来定位方法执行版本的分派动作称为静态分派.静态分派的典型应用是方法重载.静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的. 那么什么是静态类型(static type)呢? Super object = new Sub(); 像上面的语句,Sup

  • JVM 方法调用之动态分派(详解)

    1. 动态分派 一个体现是重写(override).下面的代码,运行结果很明显. public class App { public static void main(String[] args) { Super object = new Sub(); object.f(); } } class Super { public void f() { System.out.println("super : f()"); } public void f(int i) { System.out

  • Python周期任务神器之Schedule模块使用详解

    目录 1.准备 2.基本使用 参数传递 获取目前所有的作业 取消所有作业 标签功能 设定作业截止时间 立即运行所有作业,而不管其安排如何 3.高级使用 装饰器安排作业 并行执行 日志记录 异常处理 如果你想在Linux服务器上周期性地执行某个 Python 脚本,最出名的选择应该是 Crontab 脚本,但是 Crontab 具有以下缺点: 1.不方便执行秒级的任务. 2.当需要执行的定时任务有上百个的时候,Crontab的管理就会特别不方便 另外一个选择是 Celery,但是 Celery 的

  • Golang验证器之validator是使用详解

    目录 前言 什么是validator 安装 使用方法 校验规则 跨字段验证 错误处理 小结 前言 对于HTTP请求,我们要在脑子里有一个根深蒂固的概念,那就是任何客户端传过来的数据都是不可信任的.那么开发接口的时候需要对客户端传提交的参数进行参数校验,如果提交的参数只有一个两个,这样我们可以简单写个if判断,那么要是有很多的参数校验,那么满屏都是参数校验的if判断,效率不仅低还不美观,接下来我们介绍一个参数校验器validator. 什么是validator Validator 是一个 Gola

  • JVM中的守护线程示例详解

    前言 在Java中有两类线程:User Thread(用户线程).Daemon Thread(守护线程) 用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆: 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作:只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作. Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者. 在之前的<详解JVM如何处理异常>提到了守护线程,

  • JVM处理未捕获异常的方法详解

    前言 继之前的文章详解JVM如何处理异常,今天再次发布一篇比较关联的文章,如题目可知,今天聊一聊在JVM中线程遇到未捕获异常的问题,其中涉及到线程如何处理未捕获异常和一些内容介绍. 什么是未捕获异常 未捕获异常指的是我们在方法体中没有使用try-catch捕获的异常,比如下面的例子 private static void testUncaughtException(String arg) { try { System.out.println(1 / arg.length()); } catch

  • JVM内存区域划分相关原理详解

    学过C语言的朋友都知道C编译器在划分内存区域的时候经常将管理的区域划分为数据段和代码段,数据段包括堆.栈以及静态数据区.那么在Java语言当中,内存又是如何划分的呢? 由于Java程序是交由JVM执行的,所以我们在谈Java内存区域划分的时候事实上是指JVM内存区域划分.在讨论JVM内存区域划分之前,先来看一下Java程序具体执行的过程: 如上图所示,首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加

随机推荐