Java之类加载器

简介

  类加载器负责将class文件读入内存, 系统为载入内存的类创建对应的Class对象。类加载器通常由JVM提供, JVM提供的加载器通常被称为系统类加载器。此外, 开发者可以通过继承ClassLoader来实现自己的类加载器。

思维导图

在这里插入图片描述

类加载器

类加载器的层次结构:

  • Bootstrap ClassLoader: 根类加载器
  • Extension ClassLoader: 扩展类加载器
  • System ClassLoader: 应用程序类加载器(系统类加载器)

根类加载器负责加载核心类库。Java存放在%JAVA_HONE%/jre/lib目录中的, 或者被-XbootClasspath参数所指定的路径中的类。 它并不是java.lang.ClassLoader的子类, 是由JVM自身实现的。

public class BootstrapTest {
    public static void main(String[] args) {
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL url : urls) {
            System.out.println(url.toExternalForm());
        }
    }
}

/**
 * 输出:
 * 
 * file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/lib/resources.jar
 * file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/lib/rt.jar
 * file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/lib/sunrsasign.jar
 * file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/lib/jsse.jar
 * file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/lib/jce.jar
 * file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/lib/charsets.jar
 * file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/lib/jfr.jar
 * file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/classe
**/

可以看到, 常用到的rt.jar就是被根加载器加载的。

扩展类加载器负责加载JRE扩展目录(%JAVA_HONE%/jre/lib/ext或者由java.ext.dirs系统指定的目录)中jar包中的类。

应用程序类加载器(系统类加载器) 负责加载用户类路径(Class Path)上所指定的类库:
-来自java命令的-classpath选项
-java.class.path系统属性
-CLASSPATH环境变量

如果程序中没有定义自己的类加载器, 一般情况下系统类加载器就是程序中的默认的类加载器。如果没有特别指定, 用户自定义的类加载器都以系统类加载器作为父加载器。

public static void main(String[] args) throws IOException {
        ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
        System.out.println("系统类加载器:" + systemLoader);

        // 获取系统类加载器的加载路径
        Enumeration<URL> eml = systemLoader.getResources("");
        while(eml.hasMoreElements()){
            System.out.println(eml.nextElement());
        }
        
        //获取扩展类加载器
        ClassLoader extendsionLoader = systemLoader.getParent();

        System.out.println("扩展类加载器:" + extendsionLoader);
        System.out.println("扩展类加载器的加载路径: " + System.getProperty("java.ext.dirs"));
        System.out.println("扩展类加载器的parent:" + extendsionLoader.getParent());
    }

/**
 * 输出:
 *
 * 系统类加载器:sun.misc.Launcher$AppClassLoader@164dbd5
 * file:/C:/Users/peter1950/Desktop/T/
 * file:/C:/Program%20Files/Java/jdk1.7.0_80/lib/
 * 扩展类加载器:sun.misc.Launcher$ExtClassLoader@9cb0f4
 * 扩展类加载器的加载路径: C:\Program Files\Java\jdk1.7.0_80\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
 * 扩展类加载器的parent:null
 **/

  系统类加载器加载的路径为当前目录下的路径, 以及%JAVA_HONE%/jre/lib目录, 这正是ClASSPATH环境变量的值。
  扩展类加载器加载的路径为ext目录下的类。可以看到, 扩展类加载器的父类加载器为null, 实际上扩展类加载器的父类就是根类加载器(Bootstrap ClassLoader), 因为根类加载器没有继承ClassLoader, 是由JVM自身实现的, 因此返回null.


类加载器的关系一般如图所示——
在这里插入图片描述
  上图中的层次关系被称为双亲委派模型, 双亲委派模型要求:除了根加载器之外, 所有的加载器都要有父加载器。 类加载器的父子关系并非继承上的父子关系, 而是通过使用组合关系来复用父类加载器的代码。

// 双亲委派模型实现

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) {
                    // 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(c);
            }
            return c;
        }
    }

加载策略

类加载机制主要有3种:

  • 全盘负责。指类加载器不仅负责当前class的加载, 而且也要负责当前class所依赖的class的加载。
  • 双亲委托。就是在加载时先让父类加载器先尝试加载, 当父类加载器无法加载时子加载器才会进行加载。
  • 缓存机制。缓存机制保证所有的加载过的Class都会被缓存。当程需要使用某个Class时, 会先从缓存中搜索。当缓存中不存在Class时, 系统才会读取该类的二进制数据加载。

自定义类加载器

主要有2种方法:
(1) 重写loadClass方法
(2) 重写findClass方法

推荐重写findClass方法, 因为重写findClass方法可以避免覆盖默认类加载器的父类委托、缓存机制2种策略。

// ——代码参考:《疯狂Java讲义》

public class CompileClassLoader extends ClassLoader {
    // 读取一个文件的内容
    private byte[] getBytes(String filename) throws IOException {
        File file = new File(filename);
        long len = file.length();
        byte[] raw = new byte[(int) len];


        try (FileInputStream fin = new FileInputStream(file);) {
            int r = fin.read(raw);
            if (r != len) {
                throw new IOException("无法读取全部文件:" + "r" + r + "!=" + len);
            }
            return raw;
        }
    }

    // 定义编译指定java文件的方法
    private boolean compile(String javaFile) throws IOException {
        System.out.println("CompileClassLoader正在编译" + javaFile + "...");

        Process p = Runtime.getRuntime().exec("javac -encoding utf-8 " + javaFile);
        try {
            // 其它线程都等待这个线程完成
            p.waitFor();
        } catch (InterruptedException e) {
            System.out.println(e);
        }

        // 获取javac 线程退出值
        int ret = p.exitValue();

        // 返回编译是否成功
        return ret == 0;
    }

    // 重写ClassLoader的findClass方法
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = null;
        String fileStub = name.replace(".", "/");
        String javaFilename = fileStub + ".java";
        String classFilename = fileStub + ".class";
        File javaFile = new File(javaFilename);
        File classFile = new File(classFilename);

        // java源文件存在 && (class文件还未存在 或者 java源文件修改的时间比class文件生成的时间晚)
        if (javaFile.exists() && (!classFile.exists() || javaFile.lastModified() > classFile.lastModified())) {
            try {
                if (!compile(javaFilename) || !classFile.exists()) {
                    throw new ClassNotFoundException("ClassNotFoundException:" + javaFilename);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (classFile.exists()) {
            try {
                // 将class二进制文件读入数组
                byte[] raw = getBytes(classFilename);

                // 获取类的全限定名(包名.类名)
                String[] paths = name.split("java");
                String entireName = new StringBuilder(paths[1]).delete(0,1).toString().
                        replaceAll("\\\\", "\\.");

                // 调用ClassLoader的defineClass方法将二进制数据转化成Class对象
                clazz = defineClass(entireName, raw, 0, raw.length);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // 加载失败
        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }

        return clazz;
    }

    // 定义一个主方法
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        // 运行时缺少参数
        if (args.length < 1) {
            System.out.println("缺少目标类, 请按如下格式运行java源文件:");
            System.out.println("java CompileClassLoader ClassName");
        }

        // 第一个参数时需要运行的类
        String progClass = args[0];
        // 剩余参数作为运行目标类时的参数
        // 将这些参数放到新数组中。
        String[] proArgs = new String[args.length - 1];
        System.arraycopy(args, 1, proArgs, 0, proArgs.length);

        CompileClassLoader ccl = new CompileClassLoader();

        // 加载需要运行的类
        Class<?> clazz = ccl.loadClass(progClass);
        Method main = clazz.getMethod("main", (new String[0].getClass()));
        Object argsArray[] = {proArgs};
        main.invoke(null, argsArray);
    }
}

// 运行命令:java Hello ni hao
// 运行结果:
运行hello的参数: ni
运行hello的参数: hao
// 加载目标类
public class Hello {
    public static void main(String[] args) {
        for (String s : args) {
            System.out.println("运行hello的参数: " + s);
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值