Java虚拟机类加载器机制

一、类加载器 ClassLoader

1. 什么是类加载器?

  通过类的全限定名获取描述该类的二进制字节流这个过程通过类加载器(Class Loader)完成。

  classloader类加载就是动态加载class文件到内存当中。Java虚拟机并不是一次性加载所有class文件到内存当中的,是按需加载到内存中。

  类加载器用于实现类的加载,但是,类加载器对Java的影响超过类加载阶段。任何一个类和类加载器在Java虚拟机中确立唯一性。每个类加载器都有自己独立的类名称空间。比如:判断两个类相等,还需要保证这两个类是由同一个类加载器加载,如果,一个类的Class文件有两个类加载器加载,被同一个Java虚拟机加载,由于类加载是由两个类加载器加载,这两个类也是不同的。这里的相等指的是Class对象的equals()、isInstance()、isAssignableFrom()这些函数的返回值及instanceof关键字。

2. 类加载器类型

BootStrap ClassLoader 启动类加载器

Extension ClassLoader 扩展类加载器

App ClassLoader 应用类加载器

PS:BootStrap ClassLoader 启动类加载器是用C++实现的,并且App ClassLoader并不是继承于启动类加载器。

3. 双亲委托模型

         

  从上至下依赖委托类加载器加载class,扩展类加载器和应用程序类加载器是Java实现的,而启动类加载器是C++实现。

  双亲委托模型的好处是优化性能,加载类是比较耗时的,通过双亲委托模型先检查父类是否加载过此类。如果,该类已经被父类加载,可以直接拿过来用,反之,父类没有加载,就需要自己加载该类了。

4. 类加载器加载过程

  1. 加载

    加载是类加载过程的一个阶段,类加载器加载阶段,通过类全限定名获取定义此类的二进制字节流。将字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存中生成代表类的java.lang.Class对象,作为方法区此类的各种数据的访问入口。根据《Java虚拟机规范》中说明“通过类全限定名加载类的二进制字节流”并没有要去一定是Class文件,也可以是ZIP、JAR等格式。

  2. 验证

    验证阶段是连接阶段的第一步,验证阶段主要目的是确保Class文件的二进制字节流符合《Java虚拟机规范》的全部约束。保证这些信息在作为代码运行时不会危害虚拟机安全。验证包括:文件格式验证、元数据验证、字节码验证、符号引用验证。

  3. 准备

    准备阶段是为类中定义的静态变量(static修饰的变量)分配内存并为类静态变量设置初始值。

  4. 解析

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

  5. 初始化

    初始化是类加载过程的最后一个阶段,类加载中在准备阶段为类静态变量赋过初始值0,而在初始化阶段是虚拟机真正执行应用程序代码,在初始化阶段前是虚拟机主导,除了自定义类加载器的局部参。初始化虚拟机将主导权交给应用程序。

    初始化阶段就是执行类构造函数<clinit>()的过程。类构造函数<clinit>()不是程序员编写的,是javac编译器自动生成的。

    类构造函数是javac编译器收集类中静态变量和静态语句块组成的。javac编译器收集语句的顺序是语句在源文件中的顺序决定的。需要注意的是,静态语句块只能访问在静态语句块区的静态变量,在静态语句块后的只能赋值,不能访问。

// 错误访问
public class Test {
    
    static {
        i = 0;
        System.out.println(i); // 编译不能通过
    }

    static int i = 1;
}

// 正确访问
public class Test {

    static int i = 1;
    static {
        i = 0;
        System.out.println(i);
    }

}

     继承类中<clinit>()构造函数不会显示的调用父类的<clinit>()构造函数,由Java虚拟机会保证在执行子类的<clinit>()构造函数前,父类<clinit>()构造函数已经执行完成。

    

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

public class Sub {
    public static int B = A;
}

public static void main(Stirng[] args) {
    System.out.println(Sub.B);
}

 

    其结果:2。

    原因就是在子类<clinit>()构造函数执行前,虚拟机保证父类<clinit>()构造函数已经执行完成。

    <clinit>()构造函数对于类或者接口(interface)来说并不是必须的,如果,类中没有静态语句块或者没有静态变量,就不会生成<clinit>()构造函数。

    Java中,接口(interface)是不能有静态语句块的,可是允许有静态变量,就仍然有静态变量初始化赋值操作。所以,接口(interface)和类一样有<clinit>()构造函数,但是,接口(interface)和类也有不同,接口不会调用父接口(Parent interface)的<clinit>()构造函数,只有父接口有静态变量是父接口的<clinit>()构造函数才会调用。另外,接口的实现类在初始化是也不会调用<clinit>()构造函数。

    在多线程下类<clinit>()构造函数如何执行。

    Java虚拟机保证一个类的<clinit>()构造函数在多线程下正确的同步加锁。如果,在多线程下同时初始化一个类,只有活动线程调用执行<clinit>()构造函数,其他线程堵塞等待,直到活动线程执行完类的<clinit>()构造函数,其他线程不会再执行这个类的<clinit>()构造函数,Java虚拟机保证类的<clinit>()构造函数在多线程下只被执行一次。

    假设,多线程初始化一个类,由于,类的<clinit>()函数只会被活动线程执行,其他线程堵塞等待,但是,类的<clinit>()函数执行耗时操作,其他线程会一直堵塞等待。这种堵塞是很隐蔽的。

public class ParentClass {

    static {
        System.out.println("init parent class.");
    }

}

public static void main(String[] args) {

    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread() + "start");
            ParentClass parentClass = new ParentClass();
            System.out.println(Thread.currentThread() + "end");

        }
    };

    Thread thread1 = new Thread(runnable);
    Thread thread2 = new Thread(runnable);
    thread1.start();
    thread2.start();

}

    结果:

Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]start
init parent class.
Thread[Thread-1,5,main]end
Thread[Thread-0,5,main]end

    类的初始化构造函数多线程加锁同步是由Java虚拟机实现的。

  6. 使用

  7. 卸载

类加载器加载类通过以上七个步骤完成类的加载使用和卸载的过程。

 

posted @ 2021-08-21 19:27  naray  阅读(59)  评论(0编辑  收藏  举报