类的加载过程是怎样的?

类从被加载到虚拟机中内存开始,到卸载除内存为止,它的生命周期包括如下图所示:

上图中的
加载验证准备初始化卸载这5个步骤的顺序是固定的,类的加载器也必须按这个顺序开始,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定l

在加载阶段,虚拟机需要完成以下三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类各种数据的访问入口

但虚拟机规范中并未指明二进制字节流要从哪里获取,应该怎样获取,因此加载阶段是非常灵活的。例如:

  • 我们可以从jarwar等格式的文件中获取
  • 也可以在运行的时候通过计算生成,最典型的就是动态代理技术
为什么加载之后要验证?

我们可以试想一下这种场景,开发人员自定义Classloader来加载指定的类,在要加载的类中插入恶意代码,如果虚拟机加载之后没有进行验证,对其完全信任,很容易导致因为加载了有问题的字节流导致系统崩溃,所以验证可以看作是虚拟机自身的一种保护措施。

验证有哪些步骤?
  • 文件格式验证:校验加载的字节流是否符合Class文件格式规范,并且兼容当前版本,以及主,次版本号是否在当前虚拟机处理范围之内…等等
  • 元数据验证:第二阶段是对前面字节码描述的语义进行分析校验,以保证其描述的信息符合Java语言规范的要求,这个阶段会验证:除了Object类以外的其他类是否有父类这个类是否继承了不被允许的final修饰的父类是否有实现父类或者接口中的抽象方法…等等
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的,不会危害虚拟机安全
  • 符号引用验证:对类自身以外的信息进行匹配性检验。
准备阶段作了哪些工作?

这个过程实际上做的就是正式为类变量分配内存并设置类变量初始值的属性,这些类变量所使用的内存都在方法区中进行分配。
注意:这里的内存分配仅仅只包括类变量(被static修饰的变量),而不包括实例变量,因为实例变量是要在对象实例化的时候分配在java堆中的。

public static int value = 123;

此时完成的工作仅仅是将value初始化为0而不是上述的123,而赋值为123的操作是在初始化阶段才会被执行。

解析中又做了哪些工作呢?

这一阶段实际上是虚拟机将常量池中的符号引用替换为直接引用的过程。

初始化阶段会执行哪些过程?

初始化阶段是类加载过程的最后一步了,前面的步骤中除了开发人员可以自定义类加载器之外,其余动作全部由虚拟机主导和控制,而到了初始化阶段,才开始真正的执行类中定义的字节码
准备阶段是给类变量赋了初始化的值,而在初始化阶段则是按开发人员定制的去初始化类变量。
通俗的讲:初始化阶段就是执行类构造器<clinit>()的过程。
首先要明确<clinit>()方法都干了些什么?
<clint>()方法是由编译器自动收集类中的所有类变量的赋值动作静态语句块(static{}块)

由于编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在语句块之前的变量,定义在它之后的变量,之前的静态语句块可以赋值,但是不能访问,如下图所示:

<clinit>()方法与类构造器方法不同,它不需要显示的调用父类的构造器,虚拟机会保证在子类的<clinit>()方法被执行之前,父类的<clinit>()已经被执行完毕,因此虚拟机中第一个执行<clinit>()方法的类就是java.lang.Object类。

另外,虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁同步,如果多线程同时去初始化一个类,那么只会有一个线程去执行这个类中的<clinit>()方法,其他线程都需要阻塞等待。

系统有哪些类加载器?


如上图所示

  • Bootstrap ClassLoader:这个类由C/C++实现的是虚拟机的自身的一部分
  • Extension ClassLoader:扩展类加载器,它默认加载<JAVA_HOME>\lib\ext目录下的,也可以加载由java.ext.dirs指定的路径中的所有类库
  • Application ClassLoader:应用程序类加载器,它主要负责加载用户类路径上所指定的类库,这个也是默认的程序中的类加载器。
类加载器的加载机制是怎样的呢?

上图所示的是类加载器相互配合进行加载的,也可以加入自定义的类加载器,这种方式就是双亲委派模型,双亲委派模型规定除了最顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,但是这里并不是用的继承关系,而是组合关系

双亲委派模型的流程是怎样的?

一个类加载器收到加载类的请求时,它不会首先去自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的加载请求最终都会委派到顶层的启动类加载器中,只有当父类加载器反馈无法加载这个类的时候(搜索范围内没有找到所需要的类),子加载器才会去加载。

为什么要使用双亲委派模型来完成加载?

我们试想一下这种场景,某个开发人员自定义了一个类加载器,然后自定义了一个和系统一样的类java.lang.String,这个类中的某个方法比如equals方法中插入一些恶意代码,这时候通过自定义的类加载器,加载到虚拟机中,系统中就会出现多个不同的java.lang.String类,当触发这些恶意代码,导致系统混乱崩溃。而双亲委派模型,由于在虚拟机启动的时候已经完成了系统的相关的类的加载,自定义的系统同名的相关类,则无法完成加载。

双亲委派模型的实现过程

我们来看看最核心的加载类的方法

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) {
                        //调用父类的ClassLoader来加载
                        c = parent.loadClass(name, false);
                    } else {
                        //查找最顶层的BootstrapClassLoader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    .....
                }

                if (c == null) {
                    //如果父类加载器都没找到,就直接调用查找类的方法去查找
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    ......
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

上面的代码很清楚,我们每个ClassLoader都会持有一个父类的ClassLoader对象,当调用当前的加载类的方法时候,其实内部会调用父类的ClassLoader来完成加载,如果最顶层的父类加载器抛出异常,说明父类无法完成加载请求,此时就由子类来完成,查找类加载类的过程了。

自定义ClassLoader

自定义类People

package com.onzhou.main;

public class People {

    public void run() {
        System.out.println("run...");
    }

}

自定义ClassLoader

public class ExClassLoader extends ClassLoader {

    private String classPath;

    public ExClassLoader(String classPath) {
        this.classPath = classPath;
    }

    private byte[] loadByte() throws Exception {
        FileInputStream fis = new FileInputStream(classPath);
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;

    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte();
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    public static void main(String args[]) throws Exception {
        ExClassLoader classLoader = new ExClassLoader("/opt/com.onzhou.main.People.class");
        Class clazz = classLoader.loadClass("com.onzhou.main.People");
        Object obj = clazz.newInstance();
        Method runMethod = clazz.getDeclaredMethod("run", null);
        runMethod.invoke(obj, null);
    }

}

最后输出结果:

run...

Process finished with exit code 0

github地址

参考:
《深入理解Java虚拟机:JVM高级特性与最佳实践》

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐