Java虚拟机 —— 类的加载机制

咱们知道class文件中存储了类的描述信息和各类细节的数据,在运行Java程序时,虚拟机须要先将类的这些数据加载到内存中,并通过校验、转换、解析和初始化事后,最终造成能够直接使用的Java类型。java

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分统称为链接。
android

类的生命周期
类的生命周期

类的加载机制实际上就是类的生命周期中加载、验证、准备、解析、初始化5个过程。c++

加载

加载是类的加载过程的第一个阶段,在加载阶段,虚拟机须要完成如下3件事情:程序员

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

经过全限定名来获取二进制流能够有不少种方式,好比从JAR、EAR、WAR文件包中读取,从网络获取,也能够由其余文件来生成(jsp文件生成对应的Servlet类),甚至还能够经过运行时动态生成(Java动态代理)。安全

相比类加载过程的其余阶段,加载阶段是可控性最强的。由于开发者既能够利用系统提供的启动类加载器来完成,也能够经过自定义类加载去完成(重写loadClass方法,控制字节流的获取方式)。bash

关于类加载器的详细介绍将放在文章最后。网络

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。而后在内存中实例化一个java.lang.Class类的对象,这样就能够经过这个对象来访问方法区中的这些数据。数据结构

验证

验证是链接阶段的第一步,这一阶段的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。验证阶段大体上会完成下面4个阶段的检验动做:文件格式验证、元数据验证、字节码验证、符号引用验证。多线程

  • 文件格式验证: 验证字节流是否符合Class文件格式的规范,而且能被当前版本的虚拟机处理。该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区以内,格式上符合描述一个Java类型信息的要求。经过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的
    3个验证阶段所有是基于方法区的存储结构进行的,不会再直接操做字节流。
  • 元数据验证: 对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。这个主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。
  • 字节码验证: 对类的方法体进行校验分析,保证被校验类的方法在运行时不会作出危害虚拟机安全的事件。
  • 符号验证: 对类自身之外(常量池中的各类符号引用)的信息进行匹配性校验,这个阶段发生在将符号引用转化为直接引用的时候(解析阶段中发生),目的是确保解析动做能正常执行。

准备

准备阶段是正式为类变量(静态变量)分配内存并设置初始值的阶段,这些类变量所使用的内存都将在方法区中进行分配。jsp

这里有两点须要注意:

  1. 成员变量不是在这里分配内存的,成员变量是在类实例化对象的时候在堆中分配的。
  2. 这里设置初始值是指类型的零值(好比0,null,false等),而不是代码中被显示的赋予的值。

好比:

public class Test {
    public int number = 111;
    public static int sNumber = 111; 
}复制代码

成员变量number在这个阶段就不会进行内存分配和初始化。而类变量sNunber会在方法区中分配内存,并设置为int类型的零值0而不是111,赋值为111是在初始化阶段才会执行。

Java基本数据类型和引用数据类型零值
Java基本数据类型和引用数据类型零值

可是呢,若是类变量若是是被final修饰,为静态常量,那么在准备阶段也会在方法区中分配内存,而且将其值设置为显示赋予的值。

好比:

public class Test {
    public static final int NUMBER = 111; 
}复制代码

此时,就会在准备阶段将NUMBER的值设置为111。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 符号引用: 符号引用以一组符号来描述所引用的目标,符号能够是任何形式的字面量,只要使用时能无歧义地定位到目标便可。
  • 直接引用: 直接引用能够是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

解析动做主要就是在常量池中寻找类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符等7类符号引用,把这些符号引用替换为直接引用。下面主要介绍下类或接口、字段、类方法、接口方法的解析:

  1. 类或接口解析: 假设当前的类A经过符号X引用了类B,虚拟机会把表明类B的全限定名传递给A的类加载器去加载BB通过加载、验证、准备过程,在解析过程又可能会触发B引用的其余的类的加载过程,至关于一个类引用链的递归加载过程,整个过程只要不出现异常,B的就是一个加载成功的类或接口了,也就是能够获取到表明Bjava.lang.Class对象。在验证了A具有对B的访问权限后,就将符号引用X替换为B的直接引用。
  2. 字段解析: 解析未被解析过的字段,要先解析字段所属的类或接口的符号引用。若是类自己就包含了简单的名称和字段描述与目标字段相匹配,就直接返回这个字段引用;若是实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,若是接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段;若是是继承自其余类的话,将会按照继承关系从下往上递归搜索其父类,若是在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用。
  3. 类方法解析:类方法解析和字段解析的方式相似,也是依据继承和实现关系从小到上搜索,只不过是先搜索类,后搜索接口。若是有简单名称和字段描述符都与目标相匹配的字段,就返回字段引用。
  4. 接口的方法解析: 与类方法解析相似,从小到上搜索接口(接口没有父类,只可能有父接口)。若是存在简单名称和字段描述符都与目标相匹配的字段,就返回字段引用。

初始化

类的初始化类加载过程的最后一步,在前面的过中,除了在加载阶段开发者能够自定义加载器以外,其他的动做都是彻底有虚拟机主导和控制完成。到了初始化阶段,才真正开始执行类中定义的Java代码。

在准备阶段,类变量已经设置了系统要求的零值,而在初始化阶段,则根据程序员经过程序制定的主观计划去初始化类变量和其余资源,或者能够从另一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>()方法是由编译器自动收集类中全部的类变量(static变量)和静态代码块(static{}块)中的语句合并生成的。编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态代码块中只能访问到定义在静态代码块以前的变量,定义在它以后的变量,在前面的静态代码块能够赋值,可是不能访问。

public class Test {
    static {
        number = 111;               // 能够赋值
        System.out.println(number); // 不能读取,编辑器或报错Illegal forward reference
    }
    static int number;
}复制代码

<clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不一样,它不须要显式地调用父类的<clinit>()方法,虚拟机会保证在子类的<clinit>()方法执行以前,父类的<clinit>()方法已经执行完毕。因此,父类定义的静态代码块要先与子类的赋值操做。

class Parent {
    public static int A = 1;
    static {
        A = 2;
    }
}

class Sub extends Parent {
    public static int B = A;
    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}复制代码

<clinit>()方法对于类或接口来讲并非必需的,若是一个类中没有静态语句块,也没有对变量的赋值操做,那么编译器能够不为这个类生成<clinit>()方法。

接口中不能使用静态语句块,但仍然有变量初始化的赋值操做,所以接口与类同样都会生成<clinit>()方法。但接口与类不一样的是,执行接口的<clinit>()方法不须要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也同样不会执行接口的<clinit>()方法。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,若是多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其余线程都须要阻塞等待,直到活动线程执行<clinit>()方法完毕。若是在一个类的<clinit>()方法中有耗时很长的操做,就可能形成多个进程阻塞。

类加载器

在以前的加载过程当中,提到了类加载器经过一个类的全限定名来获取描述此类的二进制字节流,这个过程可让开发中自定义类加载器来决定如何获取须要的字节流。那么,什么是类加载器呢?

对于任意一个Java类,都必须经过类加载器加载到方法区,并生成java.lang.Class对象才能使用类的各个功能,因此咱们能够把类加载器理解为一个将class类文件转换为java.lang.Class对象的工具。

对于任意一个类,都须要由加载它的类加载器和这个类自己一同确立其在Java虚拟机中的惟一性,每个类加载器,都拥有一个独立的类名称空间。也就是说,若是两个类“相等”,那么这两个类必须是被同一个虚拟机中的同一个类加载器加载,而且来自同一个class文件。

在Java当中,已经有3个预制的类加载器,分别是BootStrapClassLoaderExtClassLoader、AppClassLoader

  • BootStrapClassLoader: 启动类加载器,它是由C++来实现的,在Java程序中不能显氏的获取到。它负责加载存放在JDK\jre\lib(JDK表明JDK的安装目录,下同)下的类。
  • ExtClassLoader: 扩展类加载器,它是由sun.misc.Launcher$ExtClassLoader实现,负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的全部类库。开发者能够直接使用它。
  • AppClassLoader: 应用程序类加载器,由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者能够直接使用该类加载器。通常来讲,开发者自定义的类就是由应用程序类加载器加载的。

ExtClassLoader做为类加载器,但它也是一个Java类,是由BootStrapClassLoader来加载的,因此,ExtClassLoader的parent是BootStrapClassLoader。可是因为BootStrapClassLoaderc++实现的,咱们经过ExtClassLoader.getParent获取到的是null。一样地,AppClassLoader是由ExtClassLoader加载,AppClassLoader的parent是ExtClassLoader

public class Test {
    public static void main(String[] args) {
        ClassLoader cl = Test.class.getClassLoader();
        while (cl != null) {
            System.out.println(cl);
            cl = cl.getParent();
        }
    }
}复制代码

打印结果:

sun.misc.Launcher$AppClassLoader@232204a1
sun.misc.Launcher$ExtClassLoader@74a14482复制代码

同时咱们能够定义本身的类加载器CustomClassLoader,那么它的parent确定就是AppClassLoader了。类加载器的这种层次关系称为双亲委派模型。

类加载器
类加载器

双亲委派模型

双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应当有本身的父类加载器。这里类加载器之间的父子关系不是以继承的关系来实现,而是都使用递归的方式来调用父加载器的代码。

双亲委派模型的工做过程是:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个层次的类加载器都是如此,所以全部的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈本身没法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试本身去加载。

ClassLoader的源码:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}复制代码

先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,依次向上递归。若父类加载器为空则说明递归到启动类加载器了。若是从父类加载器到启动类加载器的上层次的全部加载器都加载失败,则调用本身的findClass()方法进行加载。

使用双亲委派模型能使Java类随着加载器一块儿具有一种优先级的层次关系,保证同一个类只加载一次,避免了重复加载,同时也能阻止有人恶意替换加载系统类。

自定义类加载器

通常地,在ClassLoader方法的loadClass方法中已经给开发者实现了双亲委派模型,在自定义类加载器的时候,只须要复写findClass方法便可。

public class CustomClassLoader extends ClassLoader {

    private String root;

    public CustomClassLoader(String root) {
        this.root = root;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String name) {
        String fileName = root + File.separatorChar
                + name.replace('.', File.separatorChar)
                + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}复制代码

新建一个类com.xiao.U,编译成class文件,放到桌面,来测试一下:

public class Test {
    public static void main(String[] args) {
        CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\PC\\Desktop");
        try {
            Class clazz = customClassLoader.loadClass("com.xiao.U");
            Object o = clazz.newInstance();
            System.out.println(o.getClass().getClassLoader());
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
    }
}复制代码

打印结果:

CustomClassLoader@1540e19d复制代码

自定义类加载器在能够实现服务端的热部署,在移动端好比android也能够实现热更新。


参考:

  1. 深刻理解Java虚拟机(第二版)
  2. Java 类加载机制详解
相关文章
相关标签/搜索