Bootstrap受类似在起来——漫谈JVM类加载机制

JVM类加载机制,点击查阅原图

所谓类加载机制,就是虚拟机把叙类的数目由Class文件加载到外存中,并针对那个进行校验,转换,分析与初始化,并最后形成虚拟机可以于采用java类型的历程。

Java作解释型语言,支持动态加载动态连接,类型的加载、连接和初始化过程都当程序运行是成功,虽然这样会招致类似加载的经过变慢,但是为Java语言提供了又好的油滑,实现了动态的扩展。

1. 类加载概述

1.1 类的生命周期

看似从给加载到虚拟机内存中开始,到卸载出内存为止,它的全体生命周期包括:加载验证准备解析初始化使用卸载七独号。

里头接近加载的历程包括了装载验证准备解析初始化五只级次。其中验证、准备、解析三个步骤又合称为连接

类似加载的长河

在当时五独阶段中,加载、验证、准备与初始化这四只级次起的各个凡规定的,而解析阶段则未必然,它当某些情况下得以以初始化阶段之后开始,这是以支持Java语言的运作时绑定(也变为动态绑定或后期绑定)。

此地大概说明下Java中之绑定:绑定指的凡把一个术的调用与方所在的切近(方法主体)关联起来,对java来说,绑定分为静态绑定和动态绑定:

  • 静态绑定:即前期绑定。在程序执行前方法就让绑定,此时出于编译器或另连接程序实现。针对java,简单的可以了解呢次编译期的绑定。java当中的艺术就来finalstaticprivate构造方法大凡早期绑定的。
  • 动态绑定:即晚期绑定,也叫运行时绑定。在运转时根据实际目标的类别进行绑定。在java中,几乎有的章程都是终绑定的。

1.2 类文件从何而来

既加载机制是虚拟机把叙类的数量从Class文件加载到外存中的历程,那Class文件从何而来?

好像公事来包括

  • 自从本地文件系统加载的class文件
  • 从JAR包加载class文件
    从今网加载class文件
  • 拿一个Java源文件动态编译,并履行加载

1.3 何时执行类的初始化

JVM规范中尚无强烈说明确切开始类的加载,但是指明一下动静下得要对类经行初始化(加载、验证、准备当阶段自然而于当时前面进行):

  1. 创办类实例。也便是new的法子;
  2. 调用某个类的切近措施(静态方法,invokeStatic指令码);
  3. 拜某个类还是接口的类似变量(getStatic指令码),或为此类变量赋值(putStatic指令码);
  4. 用反射方式强制创建有类或接口对应的java.lang.Class对象;
  5. 初始化某个类的子类,则该父类也会叫初始化;
  6. 直利用java.exe命令来运行有主类(含有Main函数);

  7. 类加载的历程


2.1 装载

载是找并加载类的二进制数据(查找和导入Class文件)的历程。作为类加载过程的首先个等级,在装载等,JVM需要就以下三件工作:

  1. 透过一个像样的全限定名来博取其定义之次上制字节流;

  2. 将以此字节流所表示的静态存储结构转化为方法区的运作时数据结构;

  3. 在Java堆中转一个象征是近乎的java.lang.Class对象,作为对方法区中这些数量的拜会入口。

开发人员既可以使系统提供的类加载器来形成加载,也可于定义自己的切近加载器来成功加载。这有些情在后边的段介绍。

2.2 连接

接近的加载过程后生成了近似的java.lang.Class对象,接着会进去连接等,连接路负将类的二进制数据统一入JRE(Java运行时环境)中。类的连天大致分三单级次。

  • 验证:检验被加载的类似是否来不错的内部结构,并和外类协调一致;
  • 准备:负责为类的切近变量分配内存,并设置默认初始值;
  • 解析:将类的二进制数据被之符引用替换成直接引用;

2.2.1 验证

证的目的是管让加载的近乎的对

证是连路的第一步,这同品级的目的是以保Class文件的字节流中含有的信息相符当下虚拟机的渴求,并且不会见损伤虚拟机自身的安全。验证阶段约会得4独号的查检动作:

  • 文件格式验证:验证字节流是否顺应Class文件格式的规范;验证通过之后,装载等获得字节流才会保留及方法区;

  • 首数据证实:对许节码描述的音进行语义分析(注意:对比javac编译阶段的语义分析),以保其讲述的音称Java语言专业之要求;例如:这个看似是否生父类,除了java.lang.Object之外。

  • 字节码验证:通过数据流和控制流分析,确定程序语义是官方的、符合逻辑的。

  • 标志引用验证:它起在虚拟机将标志引用转化为直接引用的时(解析阶段中来拖欠转会,后面会发教学),主要是本着近似自身之外的信(常量池中之各种符号引用)进行匹配性的校验。

征等是可怜关键之,但非是须的,它对程序运行期没影响,如果所引述的近乎经过再三证明,那么可以设想采取-Xverifynone参数来关闭大部分底类验证措施,以缩短虚拟机类加载的岁月。

2.2.2 准备

备:为类的静态变量分配内存,并以该初始化为默认值。

预备等是正统为接近变量分配内存并设置类变量初始值的流,这些内存都用于方法区中分红。对于该级来以下几点需要专注:

  1. 这时候进行内存分配的单独包括类变量(static),而不包括实例变量,实例变量会于对象实例化时趁对象同块分配在Java堆中。

  2. 此处所设置的初始值通常状态下是数据类型默认的零值(如0、0L、null、false等),而不是于当Java代码中受显式地予以的价。

一经一个类似变量的定义为:public static int value = 3;
那么变量value在预备等之后之初始值为0,而休是3,因为这时尚未开始执行外Java方法,而将value赋值为3底putstatic指令是当程序编译后,存放于类构造器<clinit>()方法中的,所以把value赋值为3之动作将以初始化阶段才见面执行。

Java中有着中心数据类以及reference类型的默认零值

2.2.3 解析

分析:把看似中之符引用转换为直接引用。

解析阶段是虚拟机将常量池内的符引用替换为一直引用的过程,解析动作根本针对类或接口、字段、类方式、接口方法、方法类型、方法句柄和调用限定符7近似标志引用进行。

  • 标志引用就是平组符号来叙述目标,可以是另外字面量;
  • 直引用就是一直对目标的指针、相对偏移量或一个间接定位及对象的句柄。

2.3 初始化

初始化,即对类的静态变量,静态代码块执行初始化操作。这是近似加载过程的末尾一步,到了这号,才真正开始执行类中定义之Java程序代码

始于化为类的静态变量赋予正确的开始值,在Java中对类似变量进行初始值设定有少种办法:

  • 声明类变量是乘定初始值。
  • 行使静态代码块呢接近变量指定初始值。

仿佛的初始化步骤 / JVM初始化步骤:

  1. 假设这个近乎还尚未叫加载与链接,那先进行加载与链接

  2. 若是这仿佛在直接父类,并且是类似还未曾受初始化(注意:在一个近似加载器中,类只能初始化一软),那就算初始化直接的父类(不适用于接口)

  3. 而类中是初始化语句(如static变量和static块),那就算相继执行这些初始化语句。

一方面,初始化阶段是行类构造器<clinit>()方的长河:

  • <clinit>()术是出于编译器自动采集类吃的具有类似变量的赋值动作以及静态语句块中的言语合并出的,编译器收集的顺序是出于语句在源文件被出现的逐一所主宰的;
  • JVM会保证每个接近的<clinit>()且只是实行同样满,不会见叫频繁加载;
  • JVM保证<clinit>()实行过程被的多线程安全;

3. 类加载器

好像的加载器是Java语言的一律种植创新。

3.1 类与类似加载器之间的涉及

对自由一个接近,都待由它们的类似加载器和之近乎本身并确定那于就Java虚拟机中之唯一性,也就是说,即便简单单近乎来源于与一个Class文件,只要加载它们的类加载器不同,那这有限个近乎即必将不抵。这里的“相等”包括了代表类的Class对象的equals()isAssignableFrom()isInstance()对等措施的回到结果,也包罗了用instanceof一言九鼎字对对象所属涉的论断结果。

3.2 类加载器的归类

立于Java虚拟机的角度来讲,只存在个别种植不同之近乎加载器:

  • 起先类加载器:它用C++实现(这里只有限于Hotspot,也不怕是JDK1.5下默认的虚拟机,有不少其他的虚拟机是因此Java语言实现之),凡是虚拟机自身的同有
  • 怀有其他Bootstrap的切近加载器:这些类似加载器都是因为Java语言落实,单身于虚拟机之外,并且布满继往开来自抽象类java.lang.ClassLoader,这些类似加载器需要由启动类加载器加载到内存中之后才会去加载其他的类。

看似加载器分类

立于Java开发人员的角度来拘禁,类加载器可以大体分也以下三类似:

  • 启动类加载器:Bootstrap
    ClassLoader
    ,跟方一样。它担负加载存放于$JAVA_HOME/jre/lib/rt.jar
    里具的class或-Xbootclassoath挑指定的jar包。由C++实现,不是ClassLoader子类。
    开行类加载器是力不从心被Java程序直接引用的。
  • 扩大类加载器:Extension
    ClassLoader
    ,该加载器由sun.misc.Launcher$ExtClassLoader实现,它肩负加载java平台遭遇扩张功能的有jar包,比如$JAVA_HOME\jre\lib\ext目录中,或者由java.ext.dirs系变量指定的不二法门中之具备类库,开发者可以一直利用扩展类加载器。
  • 应用程序类加载器:Application
    ClassLoader
    ,该类加载器由sun.misc.Launcher$AppClassLoader来促成,它承担加载classpath遭指定的jar包及
    Djava.class.path
    所指定目录下之好像与jar包。开发者可以直接使用该类加载器,如果应用程序中从未自定义了好之类似加载器,一般情况下之就是是次中默认的切近加载器

3.3 双亲委派模型

应用程序都是由于上述三种植类似加载器互相配合进行加载的,如果产生必不可少,我们尚足以参加由定义之近乎加载器。

加载器之间存在着层次关系,如下所示:

加载器的层次关系

这种层次关系称为类加载器的父母委派模型。注意这里是盖做关系复用父类加载器的父子关系,而非是以连续关系贯彻的。

类似加载器的养父母委派加载机制:当一个类收到了近乎加载请求,他率先不见面尝试自己去加载是看似,而是把这要委派给父类去做到,每一个层次类加载器都是这般,因此所有的加载请求都应传送至启动类加载中,只有当父类加载器反馈自己没辙做到这个要的时(在她的加载路径下并未找到所用加载的Class),子类加载器才见面尝试自己去加载。

以下代码可以验证类加载器之间的父子层次关系

public class ClassLoaderTest {
    public static void main(String[] args) {
        //获取系统/应用类加载器
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("系统/应用类加载器:" + appClassLoader);
        //获取系统/应用类加载器的父类加载器,得到扩展类加载器
        ClassLoader extcClassLoader = appClassLoader.getParent();
        System.out.println("扩展类加载器" + extcClassLoader);
        System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
        //获取扩展类加载器的父加载器,但因根类加载器并不是用Java实现的所以不能获取
        System.out.println("扩展类的父类加载器:" + extcClassLoader.getParent());
    }
}

输出如下:

系统/应用类加载器:sun.misc.Launcher$AppClassLoader@7f31245a
扩张类加载器sun.misc.Launcher$ExtClassLoader@45ee12a7
恢宏类加载器的加载路径:/Users/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
放大展类的父类加载器:null

怎根类加载器为NULL?
根类加载器并无是Java实现的,而且由于程序通常要访问根加载器,因此访问扩展类加载器的父类加载器时返回NULL。

下对亲委派模型来团类加载器之间的关系,有一个非常显著的补,就是Java类就其的好像加载器(说白了,就是它们所当的目录)一起有了一如既往种植含有优先级的层次关系,这对确保Java程序的稳定运转十分重大,保证与一个好像以不同的环境中都由和一个近似加载器来加载,保证一致性。

3.4 自定义类加载器

JVM中除去根类加载器之外的所有类的加载器都是ClassLoader子类的实例,通过还写ClassLoader中之法,实现由定义的切近加载器

  • loadClass(String name,boolean resolve):
    为ClassLoader的入口点,根据指定名称来加以载类,系统就是是调用ClassLoader的拖欠方式来博制定类似对应的Class对象
  • findClass(String name):根据指定名称来查找类

脚是落实findClass法的自定义类加载器的实例:

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;

public class MyClassLoader extends ClassLoader {
    // 读取一个文件的内容
    @SuppressWarnings("resource")
    private byte[] getBytes(String filename) throws IOException{
        File file = new File(filename);
        long len = file.length();
        byte[] raw = new byte[(int) len];
        FileInputStream fin = new FileInputStream(file);

        // 一次读取class文件的全部二进制数据
        int r = fin.read(raw);
        if (r != len)
            throw new IOException("无法读取全部文件" + r + "!=" + len);
        fin.close();
        return raw;
    }

    // 定义编译指定java文件的方法
    private boolean compile(String javaFile) throws IOException {
        System.out.println("CompileClassLoader:正在编译" + javaFile + "……..");
        // 调用系统的javac命令
        Process p = Runtime.getRuntime().exec("javac" + javaFile);
        try {
            // 其它线程都等待这个线程完成
            p.waitFor();
        } catch (InterruptedException ie) {
            System.out.println(ie);
        }

        // 获取javac 的线程的退出值
        int ret = p.exitValue();
        // 返回编译是否成功
        return ret == 0;
    }

    // 重写Classloader的findCLass方法

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = null;
        // 将包路径中的.替换成斜线/
        String fileStub = name.replace(".", "/");
        String javaFilename = fileStub + ".java";
        String classFilename = fileStub + ".class";
        File javaFile = new File(javaFilename);
        File classFile = new File(classFilename);

        // 当指定Java源文件存在,且class文件不存在,或者Java源文件的修改时间比class文件//修改时间晚时,重新编译
        if (javaFile.exists() && (!classFile.exists())
                || javaFile.lastModified() > classFile.lastModified()) {

            try {
                // 如果编译失败,或该Class文件不存在
                if (!compile(javaFilename) || !classFile.exists()) {
                    throw new ClassNotFoundException("ClassNotFoundException:"
                            + javaFilename);
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }

        // 如果class文件存在,系统负责将该文件转化成class对象
        if (classFile.exists()) {
            try {
                // 将class文件的二进制数据读入数组
                byte[] raw = getBytes(classFilename);
                // 调用Classloader的defineClass方法将二进制数据转换成class对象
                clazz = defineClass(name, raw, 0, raw.length);
            } catch (IOException ie) {
                ie.printStackTrace();
            }
        }

        // 如果claszz为null,表明加载失败,则抛出异常
        if (clazz == null) {
            throw new ClassNotFoundException(name);

        }
        return clazz;
    }

    // 定义一个主方法

    public static void main(String[] args) throws Exception {
        // 如果运行该程序时没有参数,即没有目标类
        if (args.length < 1) {
            System.out.println("缺少运行的目标类,请按如下格式运行java源文件:");
            System.out.println("java CompileClassLoader ClassName");
        }

        // 第一个参数是需要运行的类
        String progClass = args[0];
        // 剩下的参数将作为运行目标类时的参数,所以将这些参数复制到一个新数组中
        String progargs[] = new String[args.length - 1];
        System.arraycopy(args, 1, progargs, 0, progargs.length);
        MyClassLoader cl = new MyClassLoader();

        // 加载需要运行的类
        Class<?> clazz = cl.loadClass(progClass);
        // 获取需要运行的类的主方法
        Method main = clazz.getMethod("main", (new String[0]).getClass());
        Object argsArray[] = { progargs };
        main.invoke(null, argsArray);

    }

}

参考文章

  1. 【深入Java虚拟机】之四:类加载机制
  2. Java类加载机制
  3. JAVA类加载机制全解析
  4. hotpot java虚拟机Class对象是身处 方法区 还是堆着
    ?
  5. JVM类加载机制详解(二)类加载器与养父母委派模型

相关文章