如果你还不了解Java类的加载过程,来看看这一篇吧

文章首发于【Java天堂】,跟随我探索Java进阶之路!

虚拟机类加载机制

在Java代码被编译成Class文件之后,最终需要加载到Java虚拟机中才能被运行和使用,Java虚拟机加载Class文件到内存,并对数据进行校验、转换、解析和初始化之后,才变成了我们真正可以使用的Java类型,这个过程就叫做Java虚拟机的类加载机制。

C++等语言在程序编译时有一个连接的过程,在连接时相当于就是把需要依赖的资源进行整合到一起,变成一个可执行程序。但Java的编译不同,在Java语言中,类型的加载、连接和初始化这些动作都是在程序运行期间动态完成的,这样会导致Java语言在提前编译方面变得困难,因为要到实际运行的时候才能知道实际的实现类。

例如,你写了一个接口,可以等到程序实际运行的时候再动态的加载具体的实现,而且加载的方式也不一定非得从Class文件加载,你甚至可以从网络上或者其他地方加载一个二进制流来作为程序的一部分,这种Java运行期类加载的方式,大大提升的Java语言的灵活性。

类加载的时机

一个类型从加载开始到最终的消亡,整个生命同期会经历加载(Loading)验证(Verification)准备(Preparation)解析(Resolution)初始化(Initialization)使用(Using)卸载(Unloading)共七个阶段,整个过程如下:

pke9PJg.png

验证、准备、解析三个过程,可以统称为连接(Linking)过程。

上图中的各个过程,并不是严格按照指定的顺序按部就班的执行,其中加载、验证、准备、初始化和卸载,这几个的顺序是确定的,其他阶段可能会穿插在这个过程当中交叉混合的执行,会在一个阶段执行的过程当中调用另外一个阶段。

关于什么时候触发第一个过程"加载",《Java虚拟机规范》中没有强制的规定,可以交给虚拟机自由发挥,可能不同的虚拟机触发的时机会存在差异,这部分内容不做重点介绍。但对于初始化的触发条件,《Java虚拟机规范》有严格的规则,有且只有六种情况必须对类进行初始化动作

  1. 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要触发其初始化。这四个字节码指令,分别表示对创建新对象、获取静态字段、设置静态字段、调用静态方法
  2. 使用java.lang.reflect包的方法对类型进行反射调用时,如果类型没有初始化,需要触发其初始化
  3. 对于父类,如果子类在初始化的时候发现父类没有初始化,会触发父类的初始化
  4. 当虚拟机启动时,需要先指定一个启动类(包含main方法),虚拟机会先初始化这个启动类
  5. 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
  6. 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化

1 - 4对于熟悉Java基础的同学,应该比较好理解,基本上都是常规操作。5-6相对来说使用的不太多,了解一下即可

类的加载过程

上面我们介绍了类生命周期的7个过程,加载过程主要包括加载、验证、准备、解析和初始化,重点介绍这几个部分的过程

1、加载

加载阶段,Java虚拟机主要完成三件事情:

  1. 通过类的全限定名称,获取类的二进制字节流
  2. 将二进制流中的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个此类的java.lang.Class对象

在根据全限定名称获取二进制字节流时,并没有具体的规则,从哪里获取?如何获取?这就给了开发者很大的发挥空间。

我们前面说过,Java虚拟机加载的类型不仅限于从Class文件加载, 它可以从网络或者其他任何地方加载,只要遵循Java虚拟机的规则就行。

在Java的发展历程中,充满创造力的开发者们玩出了各种花样,Java许多重要的技术都基于这种特性发展起来的

  • 从压缩包中读取,比如War包、Jar包,有没有很熟悉?

  • 从网络中获取,比如Web Applet等

  • 运行时计算生成,这个动态代理技术使用的最多,比如Spring框架大量使用到动态代理

    ......

还有许多其他的加载途径,这里就不一一列举了。

加载阶段结束后,表示Java类型的二进制字节流就按照虚拟机的格式存储在方法区之中了,之后会在Java堆内存中实例化一个java.lang.Class类的对象,这个对象作为程序访问方法区中的类型数据的外部接口。

2、验证

验证阶段主要是为了保证Class字节流中包含的信息是符合《Java虚拟机规范》所要求的,不能出现一些危害Java虚拟机自身安全的内容。

可能有的人会有疑问,为什么不在编译的时候就完验证?直接在编译的时候发现有恶意的代码,编译器就直接拒绝编译,这就可以避免后面加载的时候再去验证合法性。但是前面我们说过,Class文件二进制字节流不一定非得从Java文件编译而来,它可以从很多其他途径加载而来,Java虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有错误或有恶意企图的字节码流而导致整个系统受攻击甚至崩溃,所以验证字节码是Java虚拟机保护自身的一项必要措施

从整体上看,验证阶段会完成四个方面的验证

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

1、文件格式验证

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

  • 魔数验证,是否以0xCAFEBABE开头

  • 主、次版本号是否在当前Java虚拟机接受范围之内

  • 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)

  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量

    .......

还有很多其他的验证,总之这一阶段侧重于对格式的验证,保证字节流能被正确的解析并存储于方法区内,格式上符合一个Java类型的基本要求。

2、元数据验证

这一阶段主要是对于字节码的描述文件进行分析,以保证其符合《Java语言规范》的要求,主要的验证点包括

  • 是否有除了Object类以外的父类

  • 是否继承了不被允许继承的类

  • 类中的方法、字段是否与父类有冲突

    ......

还有很多其他类型的验证,总之这一阶段侧重于对类的元数据信息进行分析,保证不存在与《Java语言规范》相违背的内容出现

3、字节码验证

这一阶段主要是对字节码进行分析,通过分析数据流和控制流,确定程序的语义是合法的,符合逻辑的。这一阶段是对类的方法体进行分析,保证类的方法在运行时不会做出危害Java虚拟机的行为。主要验证点包括:

  • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上

  • 保证方法体中的类型转换总是有效的

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作

    ......

还有很多其他类型的验证,总之这一阶段侧重于对代码逻辑进行分析和验证,确保不会有非法行为。由于数据流分析和控制流分析的高度复杂性,所以这一阶段是整个验证过程最复杂的部分

4、符号引用验证

这一阶段可以看作是对类自身以外的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。主要验证点包括:

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类

  • 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段

  • 符号引用中的类、字段、方法的可访问性(private、protected、public、 )是否可被当前类访问

    ......

还有很多其他类型的验证,总之这一阶段的主要目的是确保解析行为能正常执行

验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、但却不是必须要执行的阶段,因为验证阶段只有通过或者不通过的差别,只要通过了验证,其后就对程序运行期没有任何影响了。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间

3、准备

准备阶段主要的任务就是为类中的静态(static修饰)变量分配内存空间并设置初始值。这里需要注意,准备阶段仅仅是为静态变量分配内存,因为静态变量是属于类的变量,普通变量分配内存需要等到创建对象时才会进行。而且在这一阶段设置初始值,是设置各个类型的零值,比如:

public static int hello = 123456;

变量hello在准备阶段会被赋初始值0,而不是123456。

当然,上面是一般情况下,也会有特殊情况,如果类的字段存在常量值,那么在准备阶段就会被赋值常量值,比如:

public static final int hello = 123456;

javac在编译时,就会将123456赋给hello,那么在准备阶段虚拟机就会根据123456的值来设置给hello

4、解析

解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,先来解释一下两个概念:符号引用、直接引用

  • 符号引用:是以一组符号来表示所引用的目标,引用的目标不一定是已经加载到虚拟机当中的的内容
  • 直接引用:表示可以直接指向目标的指针、相对偏移量或者是能间接定位到目标的句柄。如果有了直接引用,引用的目标必然已经存在于虚拟机的内存中

《Java虚拟机规范》之中并未规定解析阶段发生的具体时间,只要求了在执行ane-warray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invoke-special、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield和putstatic这17个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行,分别对应于常量池的CONSTANT_Class_info、CON-STANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_Dyna-mic_info和CONSTANT_InvokeDynamic_info 8种常量类型

具体解析动作较复杂,对于每一种类型都有不同的解析方法,如果对于解析过程想深入研究,可以参考《Java虚拟机规范》,这里再不进行过多的阐述

5、初始化过程

前面的过程,都是由Java虚拟机来主导的。从初始化开始,Java虚拟机才真正开始执行类中编写的Java程序代码,这是类加载的最后一个阶段。

在准备阶段,静态变量已经被赋值过一次零值。在初始化阶段才会真正赋予程序员编码时给定的值。

其实简单来讲,初始化阶段就是执行类构造器<init>()方法的过程,<init>()并不是程序员自己写的,是由javac编译器自动调用的

<init>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

Java虚拟机必须保证一个类的<init>()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的<init>()方法,其他线程都需要阻塞等待,直到活动线程执行完毕<init>()方法

本文由博客一文多发平台 OpenWrite 发布!

  • 16
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值