前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >详谈类加载的全过程

详谈类加载的全过程

作者头像
beifengtz
发布2019-06-12 13:46:04
6870
发布2019-06-12 13:46:04
举报

前言

上一篇文章简单介绍了一个Java类的生命周期,一个类的生命分成7个阶段,在这7个阶段中除了使用和回收之外,剩下的五个阶段都属于加载的过程,也是最重要最复杂的几个过程,今天就深入了解一下一个类的加载过程,也就是加载、验证、准备、解析和初始化5个阶段。

本文是我对《深入理解Java虚拟机》一书7.3节类加载过程的知识总结。

一、加载

加载是类加载的过程,也就是Class Loading,在此阶段主要完成3件事:

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

以上是《Java虚拟机规范》的要求,规范的定义并没有限定死一些条件,比如根据类的全限名获取二进制字节流,但是并没有说从哪儿获取,这提供给开发者无限的发挥空间,因此目前加载一个class字节流不一定只从本地文件中加载,还有很多其他方式,具体方式如下:

  • 从本地class文件中读取;
  • 从Jar、Ear、War等格式压缩文件中读取;
  • 从网络中获取,如Applet;
  • 运行时计算生成,比如众所周知的反射机制,它就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流;
  • 从其他文件生成,典型应用场景就是JSP;
  • 从数据库中读取,如中间件服务器SAP Netweaver,它可以选择把程序安装到数据库中来完成程序代码在集群间的分发。

在整个类加载过程中,此阶段是开发中控制能力最强的,一个非数组类的加载,可以使用系统提供的默认加载器来完成,也可以由用户自定义的类加载器去完成,开发者可以自定义类加载器去控制字节流的获取方式。但是相对于数组类则有所不同,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的,在上一篇文章中也有所提到数组类,数组类创建过程遵循以下规则:

  • 如果数组的组件类型是引用类型,那就递归采用上述的普通类的加载过程去加载这个组件类型,这个数组类将在加载该组件类型的类加载器的类名称上被标识。
  • 如果数组的组件类型不是引用类型(比如int[]数组),Java虚拟机将会把该数组类标记为与引导类加载器关联。
  • 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。

加载阶段与连接阶段的部分内容(比如验证)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始了,但这些夹在夹在阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然需要保持固定的先后顺序。

二、验证

验证是连接阶段的第一步,这一阶段是为了保证Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机自身的安全。这个阶段至关重要,该阶段是否严谨,直接决定了虚拟机是否能够承受恶意代码的攻击。

《Java虚拟机规范》规定,如果验证到输入的字节流不符合class文件格式的约束,虚拟机就应该抛出一个java.lang.VerifyError异常或其子类异常。从整体上来看,验证阶段大致分成4个阶段来完成检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

1.文件格式验证

文件格式验证是验证的第一步,其需要操作的步骤有很多很多,这个阶段主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。这个阶段是基于二进制字节流进行的,只有通过这个阶段验证之后才会进入方法区存储,即之后的三个验证操作都是基于方法区的存储结构进行的。

列举几个验证的内容:

  • 是否以魔数OxCAFEBABE开头。(至于为什么是这个,可以了解一下Class字节码的结构)
  • 主、次版本号是否在当前虚拟机处理范围之内。
  • 常量池的常量中是否有不被支持的常量类型。
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
  • class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
  • ......

2.元数据验证

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,保证不存在不符合Java语言规范的元数据信息。

列举几个验证的内容:

  • 这个类是否有父类(除了java.lang.Object之外,所有类都应该有父类)
  • 这个类的父类是否继承了被final修饰的类。
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
  • 类中的字段、方法是否与父类产生矛盾(例如覆盖父类被final修饰的属性或方法,或者出现不符合规则的方法重载)。
  • ......

3.字节码验证

第三阶段是整个验证过程最复杂的一个阶段,主要是通过数据流和控制流分析,确定程序语义是否合法、符合逻辑。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事。

如果一个方法体通过了字节码校验,也不一定说明其一定是安全的,因为通过程序去校验程序逻辑是无法做到绝对准确的,即不能通过程序准确地检查出程序是否能在有限时间之内结束运行。

此过程的数据流验证是非常复杂的,相对耗时也很高,但是在JDK 1.6之后java编译器和虚拟机对其进行了优化,给方法体的Code属性的属性表上增加一个名为“StackMapTable”的属性,这个属性描述了方法体中所有的基本块,在验证期间不需要根据程序推导来判断合法性,只需要检查StackMapTable属性中的记录是否合法即可,节省了很多时间。同时HotSpot虚拟机还提供了-XX:-UseSplitVerifier选项来关闭这项优化,或者使用参数-XX:+FailOverToOldVerifier要求在类型校验失败的时候退回到旧的类型推导方式进行校验。在JDK 1.7之后,主版本号大于50的class文件不允许再回退到类型推导的校验。

4.符号引用验证

这一阶段的验证发生在虚拟机将符号引用转化为直接引用的时候,而这个转化动作发生在连接的第三个阶段:解析。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,其目的是确保解析动作能正常执行,如果无法通过符号引用验证,那么会抛出IncompatibleClassChangeError的子类,比如IllegalAccessError、NoSuchFieldError、NoSuchMethodError等。

列举几个验证的内容:

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

对于虚拟机的类加载机制来说,验证阶段是一个非常重要的、但不是一定必要的阶段,如果你已经完全信任class文件,保证它是符合要求的,可以不必通过验证阶段,通过-Xverify:none参数来关闭大部分类验证措施,以缩短虚拟机类加载的时间。

三、准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。但是注意这里的默认值并不是赋给它的默认值,而是数据类型的零值,例如下面代码

public static boolean flag = true;

变量flag在准备阶段赋的值不会是true,而是false,因为boolean类型的值默认值是false,这个阶段的默认值均是数据类型的零值,比如int默认值是0、float默认值是0.0、引用类型是null等。

在这个阶段还有一个特例,就是被final修饰的常量,常量在准备阶段是直接赋给它对应的值

public static final boolean flag = true;

编译时Javac将会为flag生成ConstantValue属性,在准备阶段flag将会被直接赋值为true。

四、解析

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

  • 符号引用(Symbolic References):以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
  • 直接引用(Direct References):直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经在内存中存在。

对同一个符号引用进行多次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动作重复进行。

解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info七种常量类型。

  • 类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
  • 字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从下往上递归搜索该类所实现的各个接口和它的父接口,还没有,则按照继承关系从下往上递归搜索其父类,直至查找结束。
  • 类方法解析:对类方法的解析与对字段解析的搜索步骤一样,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。
  • 接口方法解析:与类方法解析步骤类似,知识接口不会有父类,只会递归向上查找父接口。

五、初始化

到了类初始化阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,变量已将赋过一次“零值”,而在初始化阶段,则根据代码逻辑去初始化类变量和其他资源,相当于一个类在实例化时执行类构造器<clinit>()方法的过程,而在观察类的反编译的时候时长会看到<clinit>()方法,该方法并不是单纯的无参构造函数,它的形成和初始化阶段息息相关。

  • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。
  • <clinit>()方法与类的构造函数不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。
  • 父类的静态语句块要优先于子类的变量赋值操作。
  • <clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态块,也没有对变量的赋值操作,那么编译器可以不生成<clinit>()方法。
  • 接口中不能使用静态语句块,但仍然会有变量初始化的赋值操作,因此接口和类都一样会生成<clinit>()方法。但接口的<clinit>()方法不需要先执行父类的<clinit>()方法,接口的实现类在初始化时也不会执行接口的<clinit>()方法。
  • 虚拟机会保证一个类的<clinit>()方法在多线程下呗正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法。

1

END

1

往期推荐

1

从源码角度深度分析JVM虚拟机监控工具

2

JVM垃圾回收机制(GC)总结

3

Java多线程下的协同控制,这些你都知道了吗?

4

聊一聊Java中的线程池

5

深入浅出生产者-消费者模式

欢迎关注我的微信公众号“北风IT之路”,一起分享有趣的编程知识!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 北风IT之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、加载
  • 二、验证
    • 1.文件格式验证
      • 2.元数据验证
        • 3.字节码验证
          • 4.符号引用验证
          • 三、准备
          • 四、解析
          • 五、初始化
          相关产品与服务
          消息队列 TDMQ
          消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档