JVM 类加载机制及双亲委派模型

一 、整体的流程

Java 中的所有类,必须被装载到 jvm 中才能运行,这个装载工作是由 jvm 中的类加载器完成的,类加载器所做的工作实质是把类文件从硬盘读取到内存中,JVM 在加载类的时候,都是通过 ClassLoader 的 loadClass()方法来加载 class 的,loadClass 使用双亲委派模型。
JVM 类加载机制及双亲委派模型_第1张图片
先解析一下这张图,图表示类的整个声明周期,类从被加载到虚拟机内存开始,到卸载出内存为止,包含 7 个阶段,其中验证、准备、解析 3 个阶段统称为连接
加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持 Java 语言的运行时绑定(动态绑定或晚期绑定)。

1、 装载

装载两个字说起来简单,但是对于 JVM 来说,这是个复杂的流程,也就是虚拟机的类加载机制:虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型

2、加载

这里所说的「加载」是「类加载」过程的一个阶段,「类加载」描述的是整个过程,「加载」仅表示「类加载」的第一阶段,需要完成以下三件事情:

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

说这么多其实就完成了一件事情:根据一个类的名字(全限定名)在内存中生成一个 Class 对象,注意 Class 对象不是关键字 new 出来的那个对象,Class 是一种类型,表示的是一个对象的运行时类型信息

接下来的三个阶段,都属于连接(Linking)。加载阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。

3、连接 - 验证

验证是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。如果验证到输入的字节流不符合 Class 文件格式的约束,虚拟机就会抛出一个 java.lang.VerifyError 异常或其子类异常。
验证阶段大致完成 4 个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

4、连接 - 准备

准备阶段是正式为类变量(static 修饰的变量)分配内存并设置类变量初始值的极端,这些变量所使用的内存都将在方法区中进行分配。注意此时进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中

并且这里提到的初始值是指零值,每种基本数据类型都有对应的零值。
假设一个类变量的定义为:

public static int value = 234

那这个变量在准备阶段过后的初始值是0而不是234,把value赋值为123的动作将在初始化阶段才会执行。

5、连接 - 解析

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

符号引用:只包含语义信息,不涉及具体实现,以一组符号来描述引用目标,是字面量;符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

直接引用:与具体实现息息相关,是直接指向目标的指针;直接引用是可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

6、初始化

初始化阶段,才真正开始执行类中定义的 Java 程序代码(或者说是字节码)

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。

也就是我们通常理解的赋初始值以及执行静态代码块。

二、类加载器

1、类与类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

比较两个类是否「相等」,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

2、加载器的种类

  • 启动类加载器(Bootstrap ClassLoader):负责将存放在 \lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。
  • 扩展类加载器(Extension ClassLoader):负责加载 \lib\ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。
  • 应用程序类加载器(Application ClassLoader):也称为系统类加载器,负责加载用户类路径(ClassPath)上所指定的类库。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
    JVM 类加载机制及双亲委派模型_第2张图片

三、 双亲委派模型

JVM 类加载机制及双亲委派模型_第3张图片
上图所示的类加载器之间的层次关系,称为类加载器的双亲委派模型。

双亲委派模型除了要求顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不会以继承的关系类实现,而是都使用组合关系来复用父加载器的代码。

1、双亲委派模型的工作原理

1)如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成;
2)如果父类加载器还存在父类加载器,则进一步向上委托,一次递归,请求最终将到达顶层的启动类加载器;
3)如果父类加载器可以完成类的加载任务,就成功返回,如果父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

JVM 类加载机制及双亲委派模型_第4张图片

举例1:
自定义一个String类,并且创建的包也是java.lang包

public class String {

    static {
        System.out.println("我是自定义类的String类的静态代码块");
    }
}

在Test类中使用String类

public class Test {
    public static void main(String[] args) {
        java.lang.String s = new java.lang.String();
        System.out.println("hello world!!!");
    }
}

打印结果:
JVM 类加载机制及双亲委派模型_第5张图片
通过结果发现,String类使用的还是java核心类库里面的String类,并没有使用到用户自定义的String类。这个执行的过程里面就使用到了双亲委派机制。

举例2:

public class String {
    static {
        System.out.println("我是自定义类的String类的静态代码块");
    }

    public static void main(String[] args) {
        System.out.println("hello,String");
    }
}

运行结果:

错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

Process finished with exit code 1

为什么会报错呢?是因为启动类加载器加载的是java核心类库里面的String类,加载完成后,在执行main方法的时候就报错了,是由于加载的String类中没有mian这个方法,所有就出现了上面报错的信息。

2、 双亲委派机制的优势

1)避免类的重复加载
2)保护程序安全,防止核心API被随意篡改

在java.lang包下创建一个Test类
JVM 类加载机制及双亲委派模型_第6张图片
运行结果:
JVM 类加载机制及双亲委派模型_第7张图片
报错原因:由于是java.lang包下,所有就会选用启动类加载器去加载,又因为Test这个类在java.lang包下不存在,启动类加载器为了安全考虑,避免破坏核心类库,所有抛出安全异常。

为什么要使用双亲委派模型

借用一个例子:黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。

而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。

也就是说,无论哪一个类加载器去加载一个系统中已有的类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此系统里在程序的各种类加载器环境中都是同一个类

双亲委派模型是如何实现的

实现双亲委派的代码都几种在 java.lang.ClassLoader 的 loadClass() 方法中:先检查是否已经被加载过,若没有加载则调用父加载器的 loadClass() 方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载器加载失败,抛出 ClassNotFoundException 异常后,再调用自己的 findClass() 方法进行加载。(看源码后发现这里的抛出异常是被吞了,catch 之后不会做任何操作)。

破坏双亲委派模型

双亲委派模型并不是一个强制性的约束模型,而是 Java 设计者推荐给开发者的类加载器的实现方式。大部分的类加载器都遵循这个模型,但双亲委派模型也可以被破坏,破坏并不是不好,而是在有足够意义和理由的情况下,突破已有的规则进行创建,实现特定的功能。

三种破坏双亲委派模型的方式

  • 重写 loadClass() 方法
  • 逆向使用类加载器,引入线程上下文类加载器
  • 追求程序的动态性:代码热替换、模块热部署等技术

你可能感兴趣的:(JVM虚拟机,java,开发语言)