深入浅出类加载器及其用法

1 类产生的原因

在Linux上创建hello.c,使用gcc编译器进行编译gcc hello.c -o hello,得到绿色的可执行程序hello,执行./hello便会在控制台输出Hello World!

在这里插入图片描述

学过编译原理的同学都清楚,整个编译过程经历了以下阶段:
在这里插入图片描述

这种编译方式的优点很明显,就是可执行文件运行速度快。缺点也很明显,不能跨平台。1)硬件环境原因:如X86和ARM指令集不一样,输出的的汇编代码不同,最后生成的机器码也不一样;2)操作系统原因:Windows编译生产的*.exe可执行文件,放在Linux系统上就执行不了。

这时候,一种跨平台的技术产生了,将源程序编译为平台无关的字节码,在程序运行时再转换成具体的二进制本地机器码(Native Code),从而达到跨平台的目的。举个简单例子,你在Windows上编译web项目生成的war包,放到Linux的tomcat上照常能跑起来。

在这里插入图片描述

2 类加载过程

整个类的生命周期主要包含以下7个阶段:
在这里插入图片描述
1) 加载
查找并加载类的二进制数据。
2) 验证
确保被加载类的正确性。
3) 准备
把类种的符号引用转换为直接引用。
4) 解析
把类中的符号引用转换为直接引用。
5) 初始化
为类的静态变量赋予正确的初始值。
6) 使用
根据类创建实例对象,如new一个实例。
7) 卸载
类的Class对象结束生命周期。这里要特别注意的是由JVM自带的三种类加载加载的类在虚拟机的整个生命周期中是不会被卸载的,由用户自定义的类加载器所加载的类才可以被卸载。

3 JVM自带类加载器

JVM自带的类加载器有3种,如表格所示:

加载器种类 实现语言 作用域
根(Bootstrap)类加载器 C++ $(JAVA_HOME)/lib目录中或者被-Xbootclasspath参数指定的路径
拓展(Extension)类加载器 Java $(JAVA_HOME)/lib/ext目录中或者被java.ext.dirs系统变量指定的路径
系统(System)类加载器 Java 一般是用户自己编译的类

类加载器就是用来加载类的,问题来了,这么多种类加载器有什么区别?答案是有区别的。类的加载遵循双亲委派模型,一个类加载器收到类加载请求,首先是委派给自己的父类加载器,只有父类加载器无法加载的条件下才自身才会尝试去加载。因此,越是顶层的加载器,加载的越是基础的包,这样才能保证系统的可靠性和安全性, 如所有类的父类Object所在的java.lang包就是由根类加载器所加载。
在这里插入图片描述

3.1 查看类加载器

public class ClassloaderApp {
    public static void main(String[] args) throws Exception {
        System.out.println( String.class.getClassLoader());
        System.out.println(ClassloaderApp.class.getClassLoader());

    }
}

运行结果:
在这里插入图片描述

第一个打印结果为null说明根类加载器可能使用null来表示,因此这个方法返回null。
AppClassLoader是系统类加载器,使用 null 来表示引导类加载器。

3.2 巧用类加载的作用域

有两个类Student和Subject,依赖关系如图所示:
在这里插入图片描述

/**
 * 学生类
 */
public class Student {
    public static void main(String[] args) {
        Subject.mostLikeSubejct();
        System.out.println("Student is loaded by" + Student.class.getClassLoader());
    }
}


/**
 *科目类
 */
public class Subject {
    public static void mostLikeSubejct(){
        System.out.println("I like English");
        System.out.println("Subject is loaded by" + Subject.class.getClassLoader());
    }

}

编译:

root@ubuntu18:~/workspace/jvmstudy/target2/classes# javac Student.java 
root@ubuntu18:~/workspace/jvmstudy/target2/classes# javac Subject.java

打成jar包:

root@ubuntu18:~/workspace/jvmstudy/target2/classes#jar cfv Student.jar Student.class

将Subject.class添加到Student.jar中
在MANIFEST.MF末尾添加:
Main-Class: Student

运行:

root@ubuntu18:~/workspace/jvmstudy/target2/classes# java -jar Student.jar

控制台打印结果如下,说明此时两个类都是由系统类加载器加载。
在这里插入图片描述再新增一个同名的Subject类,爱好为计算机了。此时,如何不改动Student.jar包,而替换旧的Subjct类呢?我们可以利用双亲委派模型和加载器的作用域进行控制。

/**
 *科目类
 */
public class Subject {
    public static void mostLikeSubejct(){
        System.out.println("I like CS");
        System.out.println("Subject is loaded by" + Subject.class.getClassLoader());
    }
}

1)方法1,使用根类加载器加载新的Subject类
打成jar包:

root@ubuntu18:~/workspace/jvmstudy/target2/classes#javac Subject.java Subject.class
root@ubuntu18:~/workspace/jvmstudy/target2/classes#jar cfv Subject.jar Subject.class

运行:

root@ubuntu18:~/workspace/jvmstudy/target2/classes# java -Xbootclasspath/a:/root/workspace/jvmstudy/target2/classes/Subject.jar: -jar Student.jar
 

结果:
在这里插入图片描述
1)方法2,使用拓展类加载器加载新的Subject类
运行:

root@ubuntu18:~/workspace/jvmstudy/target2/classes#mkdir ext
root@ubuntu18:~/workspace/jvmstudy/target2/classes#mv Subject.jar ext
root@ubuntu18:~/workspace/jvmstudy/target2/classes# java -Djava.ext.dirs=./ext/ Student

结果:
在这里插入图片描述

虽然存在两个同名的Subject类,但是根据双亲委派模型,根类加载器和拓展类加载器的优先级别都高于应用类加载器,当JVM加载了一个Subject.class以后,Student.jar中同名的Subject.class是不会被加载的!

4 手动实现类加载器

在Java中,两个全类名一样的Class文件,只要加载他们的加载器不同,比如一个使用系统自带类加载器,另外一个使用自定义类加载器,JVM就会加载2次这个类,否则就只能加载一次。

public class ClassLoaderTest {

    public static void main(String[] args) throws  Exception {

        //使用自定义类加载器
        ClassLoader myCL = new ClassLoader(){
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException{
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1)+".class";

                    // /开头代表从项目的ClassPath根下获取资源
//                    InputStream is = getClass().getResourceAsStream("/classloader/" +fileName);

                    // 不以'/'开头时,默认是指所在类的相对路径
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if(is == null){
                        return  super.loadClass(name);
                    }

                    byte[] bytes = new byte[is.available()];
                    is.read(bytes);
                    return  defineClass(name, bytes, 0, bytes.length);
                } catch (IOException e) {
                    e.printStackTrace();
                }


                return super.loadClass(name);
            }
        };

        //获取加载类的一个对象
        Object instance = myCL.loadClass("classloader.ClassLoaderTest").newInstance();

        //返回instance对象运行时属于哪个类
        System.out.println(instance.getClass());

        //测试一个对象是否为一个类的实例
        System.out.println(instance instanceof classloader.ClassLoaderTest);
    }

}

备注:本例子的代码是来自周志明老师那本经典的JVM宝书。

5 类加载器结合反射

JVM并不是一次性把所有代码都加载到内存,而是需要使用的时候才进行类加载,这是Java链接过程的动态性。根据这个特点,我们可以在JVM运行过程中从文件系统、Jar包或者远程Http服务器加载代码,并使用反射进行调用。下面使用ClassLoader的一个子类URLClassLoader加载Jar包中的代码为例。

新建一个文件Hello.java

public class Hello {
    public Hello() {
        System.out.println("Hello Contruct!");
    }

    public static void say(){
        System.out.println("Hello World!");
    }

    public static void say(String str){
        System.out.println("Hello " + str);
    }

    public static void main(String[] args) {
        System.out.println(Hello.class.getClassLoader());
    }

}

编译和打包

root@ubuntu18:~/workspace/jvmstudy/target2/classes# javac Hello.java 
root@ubuntu18:~/workspace/jvmstudy/target2/classes# jar cvf Hello.jar Hello.class 

打开Helo.jar,
在MANIFEST.MF末尾添加:
Main-Class: Hello

public class URLClassLoaderApp {

    public static void main(String[] args) throws Exception

    {

        URL url = new URL("file:/root/workspace/jvmstudy/target2/classes/Hello.jar");
        URLClassLoader loader = new URLClassLoader (new URL[] {url});
        Class<?> cl = Class.forName ("Hello", true, loader);

		//反射
        //无参方法
        Method sayMethod = cl.getMethod("say");
        sayMethod.invoke(null);

        //有参方法
        Method sayMethod2 = cl.getMethod("say", String.class);
        sayMethod2.invoke(null,"GZ");

        //构造函数
        cl.newInstance().toString();

		//将打开的资源全部释放掉       
 		loader.close();

    }
}
 

运行结果:
在这里插入图片描述

5 参考文献

[1] 深入理解Java虚拟机:JVM高级特性与最佳实践 第3版.2019, 机械工业出版社
[2] C Primer Plus 第6版 中文版 第1版. 2016, 人们邮电出版社.
[3] JDK8 API文档

原创文章 32 获赞 12 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_35469756/article/details/105281866