虚拟机的类加载机制

2019-04-07  本文已影响0人  SYFHEHE

0.前言

最近又开始看《深入理解Java虚拟机》这本书了,发现这东西很久不看忘得很快,还是写下来加深点影响吧,在这一篇文章里,我们要讨论的问题主要有以下几个方面:
(1)什么是虚拟机的类加载机制。
(2)类加载的过程
(3)类加载器

1. 什么是虚拟机的类加载机制

虚拟机把描述类的文件从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程,我们称为虚拟机的类加载机制。

2. 类加载的过程是怎么样的

整个过程如下图所示:


image.png

下面我们来分步解释下整个过程:

2.1 加载

在这个阶段,虚拟机主要完成一下三件事:
(1)通过一个全额限定名来获取定义此类的二进制字节流。
(2)将这个字节流所代表的静态储存结构转化为方法区的运行时数据结构。
(3)在Java堆中声称一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
Java虚拟机内存模型有不懂的可以看这篇文章:
https://www.jianshu.com/p/43d403b3eff7

2.2 连接

类的连接主要分为三个步骤:

(1)验证:验证被加载后的类是否有正确的结构,类数据是否符合虚拟机的要求,确保不会危害虚拟机安全。
包含四个阶段的校验动作:

(2)准备:为类的静态变量(static filed)在方法区分配内存,并设置默认初始值(0值或null值),这些内存都将在方法区分配。对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中。

这里有2点要特别注意:

(3)解析:将类的二进制数据内的符号引用替换为直接引用。
什么意思呢?要解答这个问题,首先我们需要知道什么是符号引用和直接引用:

2.3 初始化

初始化是类加载过程的最后一步,之前的4步都是完全有虚拟机主导的,直到这一步才由Java程序代码来控制。在准备阶段,变量已经赋过一次系统要求的初始值了,在初始化阶段,则是根据程序员的要求来初始化变量和其他资源。或者说是执行类的构造器<clinit>()的过程。
在Java类中,对类变量指定初始值有两种方式:
(1)在声明类变量时指定初始值。

public static int A = 1

(2)在使用静态初始化块时,为类变量指定初始值。

public int A;
static {
  A = 2;
}

JVM会按照这些语句在程序中的排列顺序依次执行他们。

3. 类加载器

3.1 什么是类加载器

虚拟机设计团队把类加载阶段中的"通过一个类的全限定名来获取描述此文件的二进制字节流"这个动作放到Java虚拟机外部趋势线,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。

3.2类与类加载器

对于任何一个类,都需要由加载它的类加载器和这个类本身一同确定其唯一性,怎么理解这句话呢?简单来说就是如果两个类即使来自于同一个文件,如果加载他们的类加载器不一样,那这两个类必定不相同。

可以看个书上的例子:

package demo.jvm;

import java.io.IOException;
import java.io.InputStream;

public class ClassLoadTest {

    public static void main(String[] args) throws Exception {
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException();
                }
            }
        };

        Object obj = myLoader.loadClass("demo.jvm.ClassLoadTest").newInstance();

        System.out.println(obj.getClass());
        System.out.println(obj instanceof demo.jvm.ClassLoadTest);

    }

}

输出:

class demo.jvm.ClassLoadTest
false

代码做了两件事,第一:自己构造了一个类加载器myLoader, 第二:使用这个类加载器去加载了一个名为demo.jvm.ClassLoadTest的类,并实例化了这个类对象。

从输出来看,这个对象却是是类demo.jvm.ClassLoadTest实例化出来的,但是从第二句来看,这个对象与系统实例化出来的对象类型并不相同,这也验证了之前的说法:如果两个类即使来自于同一个文件,如果加载他们的类加载器不一样,那这两个类必定不相同。

3.3 双亲委派模型

Java虚拟机只有两种不同的类加载器:

从Java程序员的角度,类加载器还可以继续细化,绝大部分Java程序都会使用到以下3种类加载器。

如下图所示的这种类加载器之间的关系,我们称为双亲委派模型。

image.png

它的工作过程是这样的
(1)如果一个类加载器收到了类加载的请求,他首先不会自己尝试去加载这个类,而是把这个请求发给父类加载器去完成。
(2)所有的加载请求都是会传送到顶层的启动类加载器。
(3)当父类加载器反馈自己无法完成这个类的加载,子加载器会尝试自己加载。

那么双亲委派的好处是什么呢?

双亲委派模型能保证基础类仅加载一次,不会让jvm中存在重名的类。比如String.class,每次加载都委托给父加载器,最终都是BootstrapClassLoader,都保证java核心类都是BootstrapClassLoader加载的,保证了java的安全与稳定性。

上一篇 下一篇

猜你喜欢

热点阅读