LCODER之JVM系列:虚拟机类加载机制

235 阅读5分钟

本文大纲:

image.png

1、概述

什么是虚拟机的类加载机制呢?在上一篇博客LCODER之JVM系列:Class文件结构中了解到了Class文件中的各种细节,但是在Class文件中描述的各种信息,都需要加载到虚拟机中才能运行和使用。虚拟机如何加载这些Class文件?Class文件中的信息进入到虚拟机后会发生什么变化? 就是虚拟机的类加载机制需要讨论的内容。
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

2、类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证、准备、解析3个部分统称为连接。如下图所示:

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,但是解析阶段不一定,它在某些情况下,可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(动态绑定)。

那么什么情况下开始类加载的第一个过程加载? 1、 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先触发其初始化,当然,初始化之前的加载、验证、准备阶段自然也需要进行。生成这4条字节码最常见的Java代码是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的方法时。
2、使用java.lang.reflect包的方法对类进行反射调用时
3、当初始化一个类时,发现其父类还没有进行过初始化,则需要先触发其父类的初始化
4、当虚拟机启动时,会去初始化main()所在的类。

3、类加载的过程

3.1 加载

在加载阶段,虚拟机需要完成以下三件事情:
1、通过一个类的全限定名来获取定义此类的二进制字节流。 正是因为有这一点要求,所以,诞生了许多举足轻重的Java技术如:

  • 从ZIP包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。
  • 运行时计算生成,这种场景使用最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流 除了这些之外,还有一些不常用的应用场景,这里就不再举例说明。

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 也就是把Class文件中的二进制字节流,转化成Javap可以看懂的运行时需要的数据结构。
3、在堆内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

3.2 验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

3.2.1 文件格式验证

这一阶段的验证,是验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这一阶段的验证包括以下这些点:

  1. 是否以魔数0xCAFEBABE开头。只有以魔数0xCAFEBABE开头的文件才是Class文件。
  2. 主次版本号是否在当前虚拟机的处理范围之内。
  3. 常量池中的常量是否有不被支持的常量类型。(检查tag标志)根据上一节的知识Class文件结构可以知道,常量池中的表结构是有自己确定的类型的,这里就是要验证这个类型是否是正确的。
  4. 指向常量的各种索引值中,是否有指向不存在的常量或不符合类型的常量。
  5. CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
3.2.2 元数据验证

这一阶段是对字节码的描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,这个阶段可能包括的验证点如下:

  1. 这个类是否有父类。
  2. 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
  3. 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
  4. 类中的字段,方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。
3.2.3 字节码验证

这个阶段是验证过程最复杂的阶段,验证字节码指令是否正确,字节码指令在上一篇博客LCODER之JVM系列:Class文件结构中,有过详细的解释,并且给出了对比表: segmentfault.com/a/119000000… 这个阶段将对方法体进行验证,保证被验证类的方法在运行时不会做出危害虚拟机安全的事件,例如:

  1. 保证任意时刻操作数栈的数据类型和指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
  2. 保证跳转指令不会跳转到方法体以外的字节码指令上。
  3. 保证方法中类型转换是有效的,子类转父类有效,父类转子类无效等。
3.2.4 符号引用验证

最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用时,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常检验以下内容:

  1. 符号引用中通过字符串描述的全限定名是否能找到对应的类。
  2. 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
  3. 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。

符号引用的目的是确保解析动作能正常执行,如果无法通过符号引用验证将抛出一个java.lang.IncompatibleClass.ChangeError 异常的子类。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。

3.3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。所谓类变量就是由static修饰的变量。这个阶段进行内存分配时,只会分配类变量,而不包含实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。 这里所说的初始值,通常情况下说的是基本数据类型的零值,如下表所示:

数据类型零值
int0
long0L
short(short)0
char'\u0000'
byte(byte)0
booleanfalse
float0.0f
double0.0d
referencenull

假设定义一个类变量为:

public static int value = 123;

变量value在准备阶段过后初始值是0,而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器clint方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。但特殊情况是,如果value被final修饰之后,编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。

public static final int value = 123;

3.4 解析

解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程,符号引用在上一篇博客LCODER之JVM系列:Class文件结构已经出现过很多次,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现,通俗来说,符号引用就是以字面量的形式存放在常量池中的引用,如上一节中讲的,使用javap指令后的 #2、#23等。而直接引用则是指能直接指向目标的指针、偏移量或句柄。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行,分别对应于常量池的 7 中常量类型。分别是:

CONSTANT_Class_info
CONSTANT_Fieldref_info
CONSTANT_Methodref_info
CONSTANT_interfaceMethodref_info
CONSTANT_MethodType_info
CONSTANT_MethodHandler_info
CONSTANT_invokeDynamic_info

3.5 初始化

在准备阶段,变量已经赋过一次零值,在初始化阶段,给变量附上=右侧的值。初始化阶段是执行类构造器clinit方法的过程。

4.类加载器

虚拟机团队,把类加载阶段中的通过一个类的全限定名来获取描述此类的二进制字节流这个动作放到Java虚拟机的外部去实现,实现这个动作的代码块被称为类加载器

4.1 类与类加载器

类加载器虽然只用于实现类的加载动作,但它在Java中起的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,也就是说,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他它们的类加载器不同,那这两个类就必定不相等。

4.2 双亲委派模型

从开发人员的角度来看,类加载器可以分为三类:

启动类加载器(Bootstrap ClassLoader)

这个类加载器负责加载放在<JAVA_HOME>\lib文件夹中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar)类库加载到虚拟机内存中。

扩展类加载器(EXtension ClassLoader)

这个类加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。

应用程序类加载器(Application ClassLoader)

这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。

双亲委派模型

了解了上面那些背景知识,再来理解双亲委派模型就容易一些了。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会尝试去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己加载。通俗的说双亲委派模型就是:父类能加载的不给子类加载

双亲委派模型,并不是一个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载器实现方式。 这个模型的好处是相对安全,比如说Object类,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都会委派给处于模型顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个Java.lang.Object的类,并放在程序的CLassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也会一片混乱。

双亲委派模型的实现非常简单。源码中的代码如下图所示:


    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                    // 如果父类不为null,调用父类loadclass
                        c = parent.loadClass(name, false);
                    } else {
                     // 如果父类为null 默认使用启动类加载器作为父加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }