java类加载机制和自定义类加载器
阅读原文时间:2021年04月20日阅读:1

类加载顺序

上图所示的是类加载的顺序,按照大的顺序可以分为加载、链接、初始化
其中链接又可以分成验证、准备、解析三个步骤

加载

1.将类的class文件读入到内存中

加载类文件的方式有: 1. 本机文件加载 2.jar包加载 3.网络加载 4.源文件动态编译加载

2.创建一个java.lang.class文件

连接

1.验证:类结构是否正确,是否与其他类协调一致

2.准备:为类分配内存,并设置默认初始值

3.解析:将二进制文件的符号引用替换成直接引用

初始化

对类的变量进行初始化
使用静态代码块初始化、申明类变量初始值

  • 子类和父类的关系
    父类的静态变量
    父类的静态代码块
    子类的静态变量
    子类的静态代码块
    父类非静态变量
    父类非静态代码块
    父类构造方法
    子类非静态变量
    子类非静态代码块
    子类构造方法

双亲委派模式


java 里有3中默认的类加载器

加载器种类

解释

启动类加载器

从JAVA_HOME/lib 路径下找

扩展类架子器

从JAVA_HOME/lib/ext 路径下找

应用程序类加载器

从classpath 路径下找

自定义加载器

我们程序员自己定义的加载器

new 一个类的时候 ,首先jvm会去看下当前类加载器有没有父类的加载器,有就把这个加载请求交给父类,一直如此,直到到达最顶层的启动类加载器,然后,在一级一级的往下看能不能加载,能加载就加载,不能就把这个加载任务交给子类

举一个例子,当我new Dog() 的时候

  1. 首先这个请求会交给应用程序类加载器,然后会被交给扩展类加载器
  2. 扩展类加载器会将这个请求交给启动类加载器
  3. 启动类加载器开始尝试去JAVA_HOME/lib里找,没有,交还给扩展类加载器
  4. 扩展类加载器从JAVA_HOME/lib/ext 里找,没有交还给应用程序类加载器
  5. 应用程序类从classpath 里去找,没有交给自定义加载器
  6. 要是自定义加载器还没有就抛出异常
    上述的过程只要有一个加载器加载到了就不再下放了。
  • 双亲委派机制的优势
    可以避免java的核心api 被篡改,比如你自定义了一个String 类,这个类是无效的,他绝对不会被加载,这样有效避免了类加载导致的安全问题

自定义类加载器

接下去我们实现一个自定义的类加载器,利用这个加载器来加载一个类

  • 首先简单的构建一个类

    package com.example.demo;
    
    public class Dog {
        public String name;
    }

    这是一个很简单的类,我们把它编译成class 文件放在d:盘的根目录下

  • 构建我们自己的类加载器

    public class MyLoader extends ClassLoader {
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            File file = new File("D:/Dog.class");
            try {
                FileInputStream fileInputStream = new FileInputStream(file);
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                //每次读取的大小是4kb
                byte[] b = new byte[4 * 1024];
                int n = 0;
                while ((n = fileInputStream.read(b)) != -1) {
                    outputStream.write(b, 0, n);
                }
                //将Dog.class类读取到byte数组里
                byte[] classByteArray = outputStream.toByteArray();
                //调用defineClass 将byte 加载成class
                Class<?> aClass = this.defineClass(name, classByteArray, 0, classByteArray.length);
                return aClass;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return super.findClass(name);
        }
    }

    上面的代码中,我们自定义了一个类让他去继承ClassLoader,然后重写其中的findClass方法,我们重写后的逻辑就是自己的指定的目录,也就是d盘下找到Dog.class文件 然后读取到一个byte数组中,然后调用defineClass 加载这个类。

  • 下面用一个简单的程序测试下

    public class UserDemo {
        public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
            MyLoader myLoader = new MyLoader();
            Class<?> aClass = Class.forName("com.example.demo.Dog", true, myLoader);
            Object o = aClass.newInstance();
            System.out.println(o);
            System.out.println(o.getClass().getClassLoader());
        }
    }

    这里的Class.forname 和以前不一样,我们后面对了两个参数,true代表类重新加载,myLoader就是我们自定义的类加载器

    运行后显示:

    com.example.demo.Dog@2e817b38
    com.example.demo.MyLoader@4d405ef7

    可以看到已经使用了我们自定义的类加载器。

  • 这里有一点要注意,就是个别编译器比较智能,如我使用的idea,他们会自定编译,直接把你的java文件编译成class 放入classpath下去了,所以在运行请务必吧这个文件删除,如果你用的是idea 把整个target删除就行