【Java基础】类加载机制

要明白类加载机制是什么,首先得知道什么是类

我们写的java代码都是以.java为后缀的java源码文件,想要让计算机识别,需要将.java文件编译成.class二进制文件,那么这个class就是我们今天讲的类,有.class文件还不行,java程序是运行在JVM上的,所以还需要将.class文件加载到JVM中,所有就引出了咱们今天的话题,类加载机制,那么类是如何加载的呢

为了避免开发工具为我们做的自动化操作,我们就用最原始的写代码的方式来看一下,用记事本
测试代码如下

public class Demo1{
	static {
        System.out.println("Demo1初始化了..");
    }

    public static void main(String[] args) {

        Class clazz = Object.class;
        System.out.println("Object类的加载器为:" + clazz.getClassLoader());
        System.out.println("Demo1类的加载器为:" + Demo1.class.getClassLoader());
    }
	
}

看一下运行结果
在这里插入图片描述
运行环境有问题的可以看博主这篇文章一看就懂的jdk环境变量配置

输出第一行Demo1初始化了..说明Demo1这个类加载到了JVM中,换句话说,如果不加载Demo1,程序也没法跑起来,这个是毋庸置疑的

输出第三行Demo1类的加载器为:sun.misc.Launcher$AppClassLoader@73d16e93通过Class对象的getClassLoader方法即可拿到该类的类加载器

那有人会问Object、System类我并没有去编译、也没有去加载,为何能在代码中执行呢?

Object、System类大家都知道是在rt.jar包中的,在JVM启动的时候根加载器会加载java程序中的基本类库,所以这些都是预加载好了的,就可以直接调用。

那我们能不能拿到根加载器呢,从第二行输出可以看到是不行的,根加载器不是用java实现的,所以是不可能拿到的

那么类加载器分别有哪些呢?
当JVM启动时,会形成由3个类加载器组成的初始类加载器层次结构。

  • Bootstrap ClassLoader:根加载器
  • Extension ClassLoader:扩展类加载器
  • System ClassLoader:系统加载器

也就是这3个类加载器会帮我们加载一些类,那么加载的类都在哪里呢?

public class BootstrapTest {

    public static void main(String[] args) {
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for (int i=0; i<urls.length; i++){
            System.out.println(urls[i].toExternalForm());
        }
    }
}

运行这段代码可以看到结果如下

file:/E:/software/jdk1.8.0_121/jre/lib/resources.jar
file:/E:/software/jdk1.8.0_121/jre/lib/rt.jar
file:/E:/software/jdk1.8.0_121/jre/lib/sunrsasign.jar
file:/E:/software/jdk1.8.0_121/jre/lib/jsse.jar
file:/E:/software/jdk1.8.0_121/jre/lib/jce.jar
file:/E:/software/jdk1.8.0_121/jre/lib/charsets.jar
file:/E:/software/jdk1.8.0_121/jre/lib/jfr.jar
file:/E:/software/jdk1.8.0_121/jre/classes

由运行结果可知,根加载器加载了咱们程序中标准的类库如rt.jar

再来个Demo看看系统加载器加载类的路径在哪

import java.net.URL;
import java.util.Enumeration;

public class Demo1 {
    public static void main(String[] args) throws Exception{
        Enumeration<URL> urls = Demo1.class.getClassLoader().getResources("");
        while (urls.hasMoreElements()){
            System.out.println(urls.nextElement());
        }

    }
}

在这里插入图片描述
可以看到输出结果为
file:/F:/study_demo/
file:/D:/
再看看我的系统环境变量
在这里插入图片描述
大家明白了为什么要配置classpath了吧

那有人要说了,我在IDE开发工具中没有配置classpath这个东西

拿intellij idea为例,它是在下图所示配置了在哪里去加载我们自己写的class文件
在这里插入图片描述
除了以上3种类加载器外,我们还可以自定义类加载器,只要继承了ClassLoader即可

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;

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)) {
            //一次读取Class文件的全部二进制数据
            int r = fin.read(raw);
            if (r != len) throw new IOException("无法读取全部文件:" + r + " !=" + len);
            return raw;
        }
    }
    //定义编译指定Java文件的方法
    private boolean compile(String javaFile) throws IOException{
        System.out.println("CompileClassLoader:正在编译" + javaFile + "...");
        //调用系统的javac命令
        Process p = Runtime.getRuntime().exec("javac " + javaFile);
        try {
            //其他线程都等待这个线程完成
            p.waitFor();
        }catch (InterruptedException ie){
            System.out.println(ie);
        }
        //获取javac线程的退出值
        int ret = p.exitValue();
        //返回编译是否成功
        return ret == 0;
    }

    //重写ClassLoader的findClass方法
    @Override
    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 {
                //如果编译失败,或者该Class文件不存在
                if (!this.compile(javaFilename) || !classFile.exists()){
                    throw new ClassNotFoundException("ClassNotFoundException:" + javaFilename);
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
        //如果Class文件存在,系统负责将该文件转换成Class对象
        if (classFile.exists()){
            try {
                //将Class文件的二进制数据读入数组
                byte[] raw = this.getBytes(classFilename);
                //调用ClassLoader的defineClass方法将二进制数据转换成Class对象
                clazz = this.defineClass(name, raw, 0, raw.length);
            }catch (IOException e){
                e.printStackTrace();
            }
        }
        //如果clazz为null,表明加载失败,则抛出异常
        if (clazz == null){
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

    //定义一个主方法
    public static void main(String[] args) throws Exception{
        //如果运行该程序时没有参数,即没有目标类
        if (args.length < 1){
            System.out.println("缺少目标类,请按如下格式运行Java源文件:");
            System.out.println("java CompileClassLoader ClassName");
        }
        //第一个参数是需要运行的类
        String progClass = args[0];
        //剩下的参数将作为目标类时的参数,将这些参数复制到一个新数组中
        String[] progArgs = new String[args.length - 1];
        System.arraycopy(args, 1, progArgs, 0, progArgs.length);
        CompileClassLoader ccl = new CompileClassLoader();
        //加载需要运行的类
        Class<?> clazz = ccl.loadClass(progClass);
        //获取需要运行的类的主方法
        Method main = clazz.getMethod("main", (new String[0]).getClass());
        Object argsArray[] = {progArgs};
        main.invoke(null, argsArray);
    }
}

Hello类如下

public class Hello {
	public static void main(String[] args){
		for(String arg : args){
			System.out.println("运行Hello的参数" + arg);
		}
	}
}

查看结果
在这里插入图片描述
我们无须手动编译Hello.java类,我们自定义的ClassLoder帮我们完成了,当然这里只是为了演示功能,真正的编译工作肯定不需要我们自己去做。

为了验证Hello类的加载器是我们自定义的CompileClassLoader,我们可以在Hello类中打印System.out.println(Hello.class.getClassLoader());
结果如下
在这里插入图片描述
bingo!
多说几句,其实从上面的代码中就可以看到类加载的过程,先要编译java源文件,然后读取class二进制文件到内存中,最后将二进制数据转换成Class对象,我们就可以通过Class对象去实例化对象了。

    //调用系统的javac命令
    Process p = Runtime.getRuntime().exec("javac " + javaFile);

这行代码就相当于我们在DOS窗口输入的javac命令

ps:本文部分示例来自李刚老师《疯狂java讲义》一书

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章