Java类加载器ClassLoader

简介: 1. 什么是类加载器?类加载的实际过程为:通过一个类的全限定名来获取描述此类的二进制字节流。我们把实现这个动作的代码模块成为“类加载器”。2. 怎么比较两个类"相等"?我们知道使用关键字instanceof,可以判断某个对象是否是某个Class的实例对象,但是一旦涉及到类加载器ClassLoader之后,就会出现很多令人迷惑的现象。

1. 什么是类加载器?

类加载的实际过程为:通过一个类的全限定名来获取描述此类的二进制字节流。我们把实现这个动作的代码模块成为“类加载器”。

2. 怎么比较两个类"相等"?

我们知道使用关键字instanceof,可以判断某个对象是否是某个Class的实例对象,但是一旦涉及到类加载器ClassLoader之后,就会出现很多令人迷惑的现象。
我们来先看个具体例子:

public class Test { 
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        Test test = new Test();
        System.out.println(test instanceof Test);
        
        ClassLoader classLoader = new ClassLoader() {
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                InputStream is = getClass().getResourceAsStream(fileName);
                if (is == null) {
                    return super.loadClass(name);
                }
                try {
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    e.printStackTrace();
                    throw new ClassNotFoundException(name);
                }
            }
        };
        
        Object obj = classLoader.loadClass("Test").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj.getClass() == Test.class);
        System.out.println(test.getClass() == Test.class);
        System.out.println(obj instanceof Test);
    }
}

这段代码的运行结果为:

true
class Test
false
true
false

从结果中可以看到,obj对象的class也为Test,但是与Test.class确是“不相等”的,而test对象的class与Test.class是“相等”的。它们两者之间的区别是,前者是由我们自定义的ClassLoader加载出来的,而后者是由虚拟机默认的ClassLoader加载出来的。虽然两者都是同一份class文件,但是加载的ClassLoader确不同,这说明要判断两个类是否“相等”,是由2个因素来决定的:

  1. 一是class信息是否“相等”,这里的“相等”指的是描述类的class信息是一致的,包括包名一致、类名一致、类里的信息一致等;
  2. 另一个就是加载该class的ClassLoader是否是同一个。

3. ClassLoader的双亲委派模型

img_39f89737d58a3b0ce7f06dde60970261.jpe
类加载器双亲委派模型

双亲委派模型要求所有的类加载器都有一个父加载器,除了最顶层的启动类加载器之外。它的执行逻辑是:当一个类加载器收到加载类的请求时,它不会自己去尝试加载类,而是委托给其父类来加载,每一个层级都是如此,直至到达启动类加载器为止,如果父类加载器反馈自己无法加载时,子类才会自己尝试去加载类。

由此可见,所有的类最终都是由顶层的启动类加载器来加载完成。前面一节中描述了怎么判断class是否“相等”,而双亲委派模型保证了同一个类都是由同一个ClassLoader来加载的,避免了class类型的不一致。

我们可以通过一个实例来看看不同的类加载时的ClassLoader有什么不同:

public class Test { 
    public static void main(String[] args) {                
        System.out.println(Test.class.getClassLoader());
        Object obj = new Object();
        System.out.println(obj.getClass().getClassLoader());
        List list = new ArrayList();
        System.out.println(list.getClass().getClassLoader());   
    }
}

执行结果为:

sun.misc.Launcher$AppClassLoader@338bd37a
null
null

可以看到,我们自定义的类Test是通过AppClassLoader来加载的,而Object、List的ClassLoader确是null,这是因为这些都是由启动类加载器来加载的,启动类加载器是采用c++写的,在java环境里无法获取到该类的实例,因此为null。

同样,我们依次打印下每个ClassLoader的父ClassLoader:

public class Test { 
    public static void main(String[] args) {                        
        ClassLoader loader = Test.class.getClassLoader();
        while (loader != null) {
            System.out.println(loader);
            loader = loader.getParent();
        }
    }
}

结果为:

sun.misc.Launcher$AppClassLoader@20e90906
sun.misc.Launcher$ExtClassLoader@234f79cb

这里也与双亲委派模型里ClassLoader层次结构是一致的,这里需要注意的是,AppClassLoader并不是直接继承自ExtClassLoader的,它们是通过组合的方式来实现父子关系的。

4. Class.forName()加载类

Class类有个静态方法名为forName,可以通过类的字符串名加载返回代表该类的Class对象,它有两个重载的方法,一个只有一个参数,一个有三个参数,我们来先看看有3个参数的方法定义:

public static Class<?> forName(String name, boolean initialize, ClassLoader loader)

这三个参数的含义如下:
name: 类或接口的全限定名
initialize:前面介绍类加载机制时有讲过,共有加载、验证、准备、解析、初始化、使用、卸载等步骤,该参数为true表示加载该类时会进行类的初始化,false表示不会进行类的初始化。
loader:表示采用哪个ClassLoader来加载该类

我们通过一个例子来看看,类加载时不同的参数会有什么不同的结果。

public class Test { 

    public static int COUNT = 0;
    
    static {
        System.out.println("Test init...");
    }
    
    public static void printCount() {
        System.out.println("COUNT: " + COUNT);
        COUNT++;
    }
    
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {       
        ClassLoader classLoader = new ClassLoader() {
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                InputStream is = getClass().getResourceAsStream(fileName);
                if (is == null) {
                    return super.loadClass(name);
                }
                try {
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    e.printStackTrace();
                    throw new ClassNotFoundException(name);
                }
                
            }
        };
        Test.printCount();
        Test.printCount();      
        Test.printCount();
        
        Class clazz = Class.forName("Test", false, classLoader);
        System.out.println("==========");
        Method m = clazz.getMethod("printCount", null);
        m.invoke(null, null);
    }
    
}

执行结果如下:

Test init...
COUNT: 0
COUNT: 1
COUNT: 2
==========
Test init...
COUNT: 0

在该例子中,执行Test.printCount()时,首先会触发Test类的初始化,然后连续共执行了3次,COUNT的值应该为3。接着我们使用自定义ClassLoader又加载了Test类,并且initialize参数设置为false,所以并没有触发类的初始化。然后我们通过反射调用了刚加载的Test类的printCount()方法,发现这个时候触发了类的初始化,并且打印出COUNT的值为0,这都说明采用自定义ClassLoader加载的Test类,与虚拟机默认加载的Test类压根是不同的对象。

如果把加载类的代码改为Class clazz = Class.forName("Test", true, classLoader),结果会是什么样呢?

Test init...
COUNT: 0
COUNT: 1
COUNT: 2
Test init...
==========
COUNT: 0

这就很明显的看出initialize为true或者false时,其加载过程的不同了。

那么另外一个方法的执行逻辑是什么呢?

public static Class<?> forName(String className)

其实相当于Class.forName(className, true, appClassLoader),也即采用默认的ClassLoader来加载类,并且在加载时会进行类初始化。

5. 为什么要自定义类加载器?

大部分情况下,我们都不需要自定义类加载器。但是默认的类加载器有一个局限性,就是它只能加载特定目录下的class文件,但是如果我们想要加载远程服务器上的class文件,或者就是一个符合class规范的二进制字节流,那么就需要自定义类加载器来实现了。

现在流行的热修复、热部署技术,其实都是利用了自定义类加载器来实现的。以Android应用中的热修复技术为例,一般情况下安卓应用发布到应用市场后,用户下载安装应用软件,如果应用软件出了比较致命的bug,通常必须由用户重新下载更新新的安装包才能解决问题。这些都要求用户升级软件,但是热修复技术可以不用升级软件就能动态解决原有软件的致命bug。其核心原理就是原本发布的软件里,通常是采用自定义ClassLoader来加载执行代码的,当某些代码出现问题后,发布修复问题的补丁包代码,客户端获取到补丁包代码后,采用自定义来加载器来加载补丁包里的类,而不是加载原来有问题的类,这样就达到了不升级软件就能解决问题的目标。

java类加载机制系列文章:

目录
相关文章
|
1天前
|
JSON Java Apache
Java基础-常用API-Object类
继承是面向对象编程的重要特性,允许从已有类派生新类。Java采用单继承机制,默认所有类继承自Object类。Object类提供了多个常用方法,如`clone()`用于复制对象,`equals()`判断对象是否相等,`hashCode()`计算哈希码,`toString()`返回对象的字符串表示,`wait()`、`notify()`和`notifyAll()`用于线程同步,`finalize()`在对象被垃圾回收时调用。掌握这些方法有助于更好地理解和使用Java中的对象行为。
|
29天前
|
存储 缓存 安全
java 中操作字符串都有哪些类,它们之间有什么区别
Java中操作字符串的类主要有String、StringBuilder和StringBuffer。String是不可变的,每次操作都会生成新对象;StringBuilder和StringBuffer都是可变的,但StringBuilder是非线程安全的,而StringBuffer是线程安全的,因此性能略低。
46 8
|
1月前
|
Java 开发者
在 Java 中,一个类可以实现多个接口吗?
这是 Java 面向对象编程的一个重要特性,它提供了极大的灵活性和扩展性。
65 1
|
1月前
|
存储 安全 Java
java.util的Collections类
Collections 类位于 java.util 包下,提供了许多有用的对象和方法,来简化java中集合的创建、处理和多线程管理。掌握此类将非常有助于提升开发效率和维护代码的简洁性,同时对于程序的稳定性和安全性有大有帮助。
67 17
|
1月前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
1月前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
125 4
|
1月前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
70 2
|
1月前
|
存储 安全 Java
如何保证 Java 类文件的安全性?
Java类文件的安全性可以通过多种方式保障,如使用数字签名验证类文件的完整性和来源,利用安全管理器和安全策略限制类文件的权限,以及通过加密技术保护类文件在传输过程中的安全。
53 4
|
1月前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
46 5
|
1月前
|
Java API Maven
如何使用 Java 字节码工具检查类文件的完整性
本文介绍如何利用Java字节码工具来检测类文件的完整性和有效性,确保类文件未被篡改或损坏,适用于开发和维护阶段的代码质量控制。
81 5