轻松搞定jvm类加载器

余生长醉 提交于 2020-03-01 09:20:12

什么是类的加载

我们平时所编写的“xx.java”文件需要经过我们所知的java编译器(javac)编译成“xx.class”文件,这个文件存放着编译后jvm指令的的二进制信息。而**当我们需要用到某个类时,jvm将会加载它,并在内存中创建对应的class对象,这个过程称之为类的加载。**过程如下:

类的加载、连接、初始化

1. 加载
通过类的包名和雷鸣查找到此类的字节码文件,将xx.class文件中的二进制数据读入到jvm内存,并存入其中的方法区内,然后利用字节码文件创建一个class对象存入到堆之中,用来封装类的数据结构等。
连接(验证、准备、解析):

2. 连接包括三步:
验证:确保被加载类的正确性,是否对jvm有害
准备:为**类的静态变量(static)**分配内存并将其赋值为默认变量(非赋值)eg:static int a = 1;(为a分配内存并设置为0,将a赋值为1的动作是在初始化过程中完成的,而final static int a = 1赋值是在javac过程中完成的。)
解析:把类中的符号引用转化为直接引用,符号引用:我们所写的int a 就是符号引用,直接引用:目标的指针、偏移量内存中的引用,类似c/c++中的指针。

3. 初始化
如果有父类,此时加载父类并进行初始化,静态变量、成员变量等,再加载子类。

类加载器

类的加载是由类加载器完成的。类加载器分为jvm自带的类加载器和用户自定义的类加载器。
jvm内置加载器():
启动类加载器Bootstrap ClassLoader
扩展类加载器Extention ClassLoader
系统类加载器System(Application) ClassLoader(应用类加载器)
用户自定义加载器:
java.lang.ClassLoader的子类实例。

Bootstrap ClassLoader

最底层的类加载器,是jvm的一部分,由c++实现,没有父加载器
主要负责加载由系统属性sun.boot.class.path指定的路径下的核心类库(jre/lib),出于安全考虑该类加载器只加载只加载java,javax,sun相关包里的类。

Extention ClassLoader

sun公司实现的sun.misc.Launcher$ExtClassLoader类(jdk8),由java编写,负责加载jre/lib/ext目录下的类库或者由系统变量“java.ext.dirs”指定的目录下的类库。父加载器是Bootstrap ClassLoader(非继承)。

System(Application) ClassLoader

一个纯java类,sun公司实现的sun.misc.Launcher$AppClassLoader类(jdk8),父加载器是扩展加载器(Extention ClassLoader)(非继承),负责从classpath环境变量或者java.class.path所指定的目录中加载类。是用户自定义的类加载器的默认父加载器。一般情况下是程序默认类加载器,可通过*ClassLoader.getSystemClassLoader()*直接获得。

双亲委派模型

在这里插入图片描述
双亲委派机制
当需要使用该类时,才会将它的class文件加载到内存生成class对象(按需加载),加载时,使用的是双亲委派模式:如果一个类加载器收到了类请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每一层都是如此,因此所有类加载的请求都会传到启动类加载器,只有当父加载器无法完成该请求时,子加载器才去自己加载。(父加载器、子加载器:非继承关系,而是用组合模式来复用父加载器代码)

双亲委派机制的优点:

  1. 避免类的重复加载,当父加载器加载成功时,就没必要子加载器再去加载
  2. 安全。防止java核心API不会被随意替换。比如一个网络上的java.lang.Object类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载的,因此Object类在程序的各种类加载环境中都是一个类。如果不委托,那么java.lang.Object类存放在classpath中,那么系统中就会出现多个Object类,程序变得很混乱。

ClassLoader

除了启动类加载器Bootstrap ClassLoader,其它类加载器都必须继承java.lang.ClassLoader(abstract)
主要方法:
loadClass();!!!不要覆写此方法
findClass();
defineClass();
resolveClass();
相关使用参考此处

URLClassLoader

java.net.URLClassLoader,扩展了ClassLoader。可以从本地、网络上的指定url加载类。可使用该类作为自定义的类加载器使用。

  1. 加载磁盘上的class文件:

现在有一class文件 F:/Test01.class在这里插入图片描述
Test01中有默认构造方法:

public Test01(){
  System.out.println("test01");
 }

此时我们利用URLClassLoader就可以加载磁盘上的class文件:

File file = new File("F:/");
URI uri = file.toURI();
URL url = uri.toURL();

URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
System.out.println(classLoader.getParent()); //父加载器
Class aClass = classLoader.loadClass("Test01");
aClass.newInstance();//验证是否调用默认构造方法

对应输出:
在这里插入图片描述
可以看到默认构造方法已经被调用。

  1. 加载网络上的class文件
    类似,请读者自行测试

自定义类加载器

方法:继承ClassLoader,并覆写findClass()方法。

自定义文件类加载器

这里同上面一样,存在 F:/Test01.class
我们自定义一个文件类加载器来实现这个类的加载:

import java.io.*;

/*
 * 自定义本地类加载器
 * director 类所在的目录
 */
public class MyFileClassLoader extends ClassLoader{
    private String director;

    //默认父加载器为应用类加载器(ApplicationClassLoader)
    public MyFileClassLoader(String director) {
        this.director = director;
    }

    //指定父加载器
    public MyFileClassLoader(String director, ClassLoader parentClassLoader) {
        super(parentClassLoader);
        this.director = director;
    }

    /**
     * @param name 类名
     * @return 返回字节码对象
     */
    @Override
    protected Class<?> findClass(String name) {
        try {
            //将类转化为真正对应class文件的目录
            String file = director + File.separator + name.replace(".", "/") + ".class";

            //构建输入输出流
            InputStream is = new FileInputStream(file);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            byte[] bytes = new byte[1024];
            int len = -1;
            while ((len = is.read(bytes))!= -1) {
                baos.write(bytes, 0, len);
            }

            //读取到的字节码的二进制数据
            byte[] data = baos.toByteArray();
            is.close();
            baos.close();

            /*
             * name - 预期的 binary name的类
             * data - 构成类数据的字节。
             * 0 - 类数据中的起始偏移量为 0
             * data.length - 类数据的长度
             */
            return defineClass(name, data, 0, data.length);
        } catch (IOException e) {
           throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws Exception {
        MyFileClassLoader myClassLoader = new MyFileClassLoader("F:/");
        Class aClass = myClassLoader.loadClass("Test01");
        aClass.newInstance();
    }
}

网络类加载器

网络类加载器类似(相关流API的使用)

热部署类加载器

**避开双亲委派模式,实现一个类的不同类加载器的加载。**我们已经知道双亲委派模式主要是以loadClass()方法实现的,那么我们可以不使用loadClass()而是用findClass()方法,避开双亲委派,实现一个类的多次加载,达到热部署。

public static void main(String[] args) throws Exception {
    MyFileClassLoader myClassLoader = new MyFileClassLoader("F:/");
    MyFileClassLoader myClassLoader2 = new MyFileClassLoader("F:/", myClassLoader);

    Class aClass = myClassLoader.findClass("Test01");
    Class aClass2 = myClassLoader2.findClass("Test01");
    System.out.println(aClass.hashCode());
    System.out.println(aClass2.hashCode());
}

输出结果:
455896770
1323165413
可以看到哈希码不同,证明一个类被两个加载器都加载了。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!