JavaEE笔记(一)类加载器(ClassLoader)

JavaEE笔记(一)类加载器(ClassLoader)

The Java Classloader is a part of the Java Runtime Environment that dynamically loads Java classes into the Java Virtual Machine. Usually classes are only loaded on demand. The Java run time system does not need to know about files and file systems because of classloaders. Delegation is an important concept to understand when learning about classloaders.[1]
Java类加载器是JRE的一部分,它负责将Java类文件(字节码文件)动态加载到Java虚拟机以生成Class类对象,通常类根据需要加载。基于类加载的存在, Java运行环境不需要了解文件及文件系统。为了理解类加载器,负责委托机制是非常重要的概念。


1. 类加载器的层次结构

当 JVM 启动时,会形成由三个类加载器组成的初始类加载器层次结构:
1) 引导类加载器 BootStrap jre/lib/rt.jar
BootStrap是Java虚拟机启动后初始化的底层加载器,它负责加载%JAVA_HOME%/jre/lib的类。简而言之,JRE类由BootStrap负责加载。BootStrap加载器是用C++写的,因此一般无法通过Java调用方法获得对象。

    ClassLoader cl1 = String.class.getClassLoader();
    System.out.println(cl1);

    // output: null

2) 扩展类加载器 ExtClassLoader JRE/lib/ext/*.jar
ExtClassLoader 负责加载 jre/lib/ext下的类。这些类是JRE的扩展类。该加载器是用Java写的,可以通过getClassLoader() 调用得到:

    ClassLoader cl2 = AccessBridge.class.getClassLoader();
    System.out.println(cl2);

    // output: sun.misc.Launcher$ExtClassLoader@5ca881b5

3) 应用类加载器(系统类加载器) AppClassLoader SystemClassLoader
应用类加载器主要负责加载 classpath 所指定的所有jar或目录下的类。应用类加载器也是用Java写的,同样可以通过getClassLoader() 调用得到:

    ClassLoader cl3 = this.class.getClassLoader();
    System.out.println(cl3);

    // sun.misc.Launcher$AppClassLoader@6f94fa3e (this为该测试类)

注意:
在java中ClassLoader代表类加载器,所有的类加载器都是ClassLoader的子。但是BootStrap除外,因为它底层是用C++写的,并非继承自ClassLoader.

类加载器层次结构 [2]


2. 类加载器的全盘负责,(双亲)委托模式

全盘负责:即是当一个classloader加载一个Class对象的时候,这个对象所依赖的和引用的其它Class通常也由这个classloader负责载入。
委托机制:委托机制可以总结为以下3个步骤:

1). 当前ClassLoader首先从自己已经加载的类中寻找是否已经加载的待加载类,如果已经加载了则返回原来已经加载的类,即cache机制。
2). 当缓存中没有找到待加载类,加载器委托父加载器去加载,父加载器采用相同的缓存、委托机制。
3). 当所有的父类都没有加载的时候,再由原始的加载类加载,并将它存入缓存以便日后再有加载请求可以直接返回。这就是为什么修改了Class但是必须重新启动JVM才能生效,并且类只加载一次的原因。

加载器ClassLoader类中有ClassLoader getParent() 方法用于返回父类加载器。在加载器结构层次图中可以看出应用类加载器的父加载器为扩展加载器,扩展加载其的父加载器为Bootstrap加载器。


注意:
加载器之间并非是子父类继承关系,除了BootStrap加载器,其余加载器(包括自定义加载器)均继承自ClassLoader类。但是每个加载器类中都维护一个父加载器的引用用于双亲缓存机制。


3. 自定义加载器

自定义加载器继承自ClassLoader并重写findClass()defineClass() 方法,用于告诉JVM应用那个加载器加载类文件。

  • 应用场景:
    假设我们新建一个类名称为MimeType放在默认包下,创建一个方法show() 打印一行字符串。JRE提供一个自带的MimeType类位于javax.activation 下不带show() 方法,但由于我们自建的MimeType与JRE的MimeType并不位于同一个包下,因此当调用MimeType的show()方法,并不会报错。
    现在我们重新将自定义的MimeType放置于自建的包名为javax.activation包下,如果再次调用show() 方法,JVM会默认根据包名用BootStrap加载JRE的MimeType.class,但此时show() 并不存在,因此会报出java.lang.NoSuchMethodError错误。
package javax.activation;

public class MimeType { 
    public void show() {
        System.out.println("hello MimeType");
    }
}
import javax.activation.MimeType;

public class ClassLoaderTest {
    public static void main(String[] args) {
        MimeType mt = new MimeType();
        ClassLoader cl = mt.getClass().getClassLoader();
        mt.show(); // output: java.lang.NoSuchMethodError       
    }
}
  • 用自定义加载类提供解决方案:

    • 创建MyClassLoader类继承ClassLoader父类。维护一个私有路径String对象,并创建有参构造方法传入类文件路径字符串用于传入findClass()方法,旨在告知JVM待加载的类的真实class文件路径。
    • 重写findClass()方法,构造传入的路径会自动传入方法,我们还需要把包名传入该方法。该方法的功能是将class文件通过IO写入字节数组,然后返回defineClass()来生成由字节码产生Class对象。该对象为我们确认需要的由指定加载器产生的对象。
    • 这样的做法仍然遵循双亲机制,因为真是的字节码文件路径是作为参数传入有参构造,findClass()通过文件路径得到最终得字节码文件并转化成字节数组输入给Class生成方法,这样,最终在类对象的生成过程中,字节码为当前工程编译后生成的类文件而不是JRE classpath下的同名类文件,即当加载请求自下而上到达BootStrap加载器,加载器不会将其识别为已经缓存加载了的MimeType.class,因为字节码文件包含了完全不同的路径。这样当所有父加载器无法找到缓存加载类,则加载请求就回回到初始请求源进行加载,这正是我们希望的应用类加载器。
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class MyClassLoader extends ClassLoader{
    private String rootDir;

    public MyClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {

        String extname = name.replace(".", "\\");
        String filename = rootDir + "\\" + extname + ".class";

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            InputStream is = new FileInputStream(filename);
            int len = -1;
            byte[] b = new byte[1024];

            while ((len = is.read(b)) != -1) {
                baos.write(b, 0, len);
            }
            baos.flush();
            baos.close();
            is.close();

            byte[] data = baos.toByteArray();

            return defineClass(name, data, 0, data.length);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }
}

需要注意,如何得到工程中真实的类文件路径;

import java.lang.reflect.InvocationTargetException;
import javax.activation.MimeType;
import cn.itcast.utils.MyClassLoader;

public class ClassLoaderTest1 {

    public static void main(String[] args) throws ClassNotFoundException,
            IllegalAccessException, IllegalArgumentException,
            InvocationTargetException, NoSuchMethodException,
            SecurityException, InstantiationException {

        // 得到类文件的地址     
        String rootDir = ClassLoaderTest1.class.getResource("/").getPath();

        // 将类文件地址传入自定义加载器有参构造
        MyClassLoader mcl = new MyClassLoader(rootDir);

        // 调用自定义加载器的findClass()方法,并传入包名。
        Class clazz = mcl.findClass("javax.activation.MimeType");

        //利用反射创建MimeType对象,并调用show()方法
        clazz.getDeclaredMethod("show").invoke(clazz.newInstance());
    }
}

运行结果: hello MimeType


以上

© 著作权归作者所有

引用:
[1] Java Classloader https://en.wikipedia.org/wiki/Java_Classloader
[2] Java Classloader机制解析 http://my.oschina.net/aminqiao/blog/262601

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值