让类活起来——漫谈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语言的一种革新。

Bootstrap,3.1 类与类加载器之间的涉及

对于随意一个类,都亟待由它的类加载器和这些类本身一同确定其在就Java虚拟机中的唯一性,也就是说,纵使六个类来源于同一个Class文件,只要加载它们的类加载器不同,这这多少个类就必将不等于。这里的“相等”包括了代表类的Class对象的equals()isAssignableFrom()isInstance()等措施的归来结果,也囊括了使用instanceof一言九鼎字对对象所属关系的论断结果。

3.2 类加载器的归类

站在Java虚拟机的角度来讲,只设有二种不同的类加载器:

  • 开行类加载器:它选取C++实现(那里仅限于Hotspot,也就是JDK1.5后头默认的虚拟机,有好多其他的虚拟机是用Java语言实现的),是虚拟机自身的一有的
  • 负有其他的类加载器:这个类加载器都由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类加载机制详解(二)类加载器与养父母委派模型

相关文章