Java虚拟机笔记 五、类加载机制

以下图文基本来自周志明《深入理解Java虚拟机 第3版》

目录

    • 什么是类加载?
    • 类加载的过程
      • 加载
      • 连接(验证、准备、解析)
        • 验证
        • 准备
        • 解析
      • 初始化
      • 类加载过程总结
    • 类加载器
      • 类加载器结构/划分
        • 三层类加载器
        • 双亲委派模型

什么是类加载?

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

.class是从.java编译而来,理解了class文件结构之后,就需要知道这些数据是如何加载到虚拟机里的。

与那些在编译时需要进行连接的语言不同, 在Java语言里面, 类型的加载、 连接和初始化过程都是在程序运行期间完成的, 这种策略让Java语言进行提前编译会面临额外的困难, 也会让类加载时稍微增加一些性能开销,但是却为Java应用提供了极高的扩展性和灵活性, Java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。

类加载的过程

一个类型从被加载到虚拟机内存中开始, 到卸载出内存为止, 它的整个生命周期将会经历加载(Loading) 、 验证(Verification) 、 准备(Preparation) 、 解析(Resolution) 、 初始化(Initialization) 、 使用(Using) 和卸载(Unloading) 七个阶段, 其中验证、 准备、 解析三个部分统称为连接(Linking) 。
在这里插入图片描述
加载、 验证、 准备、 初始化和卸载这五个阶段的顺序是确定的, 类型的加载过程必须按照这种顺序按部就班地开始, 而解析阶段则不一定: 它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定) 。

加载

“加载”(Loading) 阶段是整个“类加载”(Class Loading) 过程中的一个阶段, 在这个阶段, Java虚拟机需要完成以下三件事情:
1) 通过一个类的全限定名来获取定义此类的二进制字节流
2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3) 在内存中生成一个代表这个类的java.lang.Class对象, 作为方法区这个类的各种数据的访问入
口。
《Java虚拟机规范》 对这三点要求其实并不是特别具体, 留给虚拟机实现与Java应用的灵活度都是
相当大的。

例如“通过一个类的全限定名来获取定义此类的二进制字节流”这条规则, 它并没有指明二进制字节流必须得从某个Class文件中获取, 确切地说是根本没有指明要从哪里获取、 如何获取。 仅仅这一点空隙, Java虚拟机的使用者们就可以在加载阶段搭构建出一个相当开放广阔的舞台, Java发展历程中, 充满创造力的开发人员则在这个舞台上玩出了各种花样, 许多举足轻重的Java技术都建立在这一基础之上, 例如:

  • 从ZIP压缩包中读取, 这很常见, 最终成为日后JAR、 EAR、 WAR格式的基础。
  • 从网络中获取, 这种场景最典型的应用就是Web Applet。
  • 运行时计算生成, 这种场景使用得最多的就是动态代理技术, 在java.lang.reflect.Proxy中, 就是用
    了ProxyGenerator.generateProxyClass()来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。
  • 由其他文件生成, 典型场景是JSP应用, 由JSP文件生成对应的Class文件。
  • 从数据库中读取, 这种场景相对少见些, 例如有些中间件服务器(如SAP Netweaver) 可以选择
    把程序安装到数据库中来完成程序代码在集群间的分发。
  • 可以从加密文件中获取, 这是典型的防Class文件被反编译的保护措施, 通过加载时解密Class文
    件来保障程序运行逻辑不被窥探。

关于在什么情况下需要开始类加载过程的第一个阶段“加载”, 《Java虚拟机规范》 中并没有进行
强制约束, 这点可以交给虚拟机的具体实现来自由把握。

加载阶段结束后, Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中了, 方法区中的数据存储格式完全由虚拟机实现自行定义, 《Java虚拟机规范》 未规定此区域的具体数据结构。 类型数据妥善安置在方法区之后, 会在Java堆内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的类型数据的外部接口。

连接(验证、准备、解析)

加载阶段与连接阶段的部分动作( 如一部分字节码文件格式验证动作) 是交叉进行的, 加载阶段尚未完成, 连接阶段可能已经开始, 但这些夹在加载阶段之中进行的动作, 仍然属于连接阶段的一部分, 这两个阶段的开始时间仍然保持着固定的先后顺序。

验证

验证是连接阶段的第一步, 这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》 的全部约束要求, 保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

Java语言本身是相对安全的编程语言(起码对于C/C++来说是相对安全的) , 使用纯粹的Java代码无法做到诸如访问数组边界以外的数据、 将一个对象转型为它并未实现的类型、 跳转到不存在的代码行之类的事情, 如果尝试这样去做了, 编译器会毫不留情地抛出异常、 拒绝编译。 但Class文件不一定由Java源码编译而来, 它可以使用包括靠键盘0和1直接在二进制编辑器中敲出Class文件在内的任何途径产生。 Java代码无法做到的事情在字节码层面上可以实现的, 至少语义上是可以表达出来的。 Java虚拟机如果不检查输入的字节流, 对其完全信任的话, 很可能会因为载入了有错误或有恶意企图的字节码流而导致整个系统受攻击甚至崩溃, 所以验证字节码是Java虚拟机保护自身的一项必要措施。

验证阶段大致完成四个检验动作: 文件格式验证、 元数据验证、 字节码验证和符号引用验证。

  • 文件格式验证:验证字节流是否符合Class文件格式的规范, 并且能被当前版本的虚拟机处理。

    可能包括下面这些验证点:
    - 是否以魔数0xCAFEBABE开头。
    - 主、 次版本号是否在当前Java虚拟机接受范围之内。
    - 常量池的常量中是否有不被支持的常量类型(检查常量tag标志) 。
    - 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
    - CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据。
    - Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
    ……

  • 元数据验证:对字节码描述的信息进行语义分析, 以保证其描述的信息符合《Java语言规范》 的要

    可能包括的验证点如下:
    - 这个类是否有父类(除了java.lang.Object之外, 所有的类都应当有父类) 。
    - 这个类的父类是否继承了不允许被继承的类(被final修饰的类) 。
    - 如果这个类不是抽象类, 是否实现了其父类或接口之中要求实现的所有方法。
    - 类中的字段、 方法是否与父类产生矛盾(例如覆盖了父类的final字段, 或者出现不符合规则的方法重载, 例如方法参数都一致, 但返回值类型却不同等) 。

  • 字节码验证:最复杂的一个阶段, 主要目的是通过数据流分析和控制流分析, 确定程序语义是合法的、 符合逻辑的。

在第二阶段对元数据信息中的数据类型校验完毕以后, 这阶段就要对类的方法体(Class文件中的Code属性) 进行校验分析, 保证被校验类的方法在运行时不会做出危害虚拟机安全的行为, 例如:
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作, 例如不会出现类似于“在操作
栈放置了一个int类型的数据, 使用时却按long类型来加载入本地变量表中”这样的情况。
- 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
- 保证方法体中的类型转换总是有效的, 例如可以把一个子类对象赋值给父类数据类型, 这是安全
的, 但是把父类对象赋值给子类数据类型, 甚至把对象赋值给与它毫无继承关系、 完全不相干的一个
数据类型, 则是危险和不合法的。
……

其实这个阶段就是防止出现异常,但异常不可能完全用程序来判定找出来的。所以一个方法体通过了字节码验证, 也仍然不能保证它一定就是安全的

  • 符号引用验证:主要确保解析行为能正常执行。

    如果无法通过符号引用验证, Java虚拟机将会抛出一个java.lang.IncompatibleClassChangeError的子类异常, 典型的如:java.lang.IllegalAccessError、 java.lang.NoSuchFieldError、 java.lang.NoSuchMethodError等。

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

准备

准备阶段是正式为类中定义的变量(即静态变量, 被static修饰的变量) 分配内存并设置类变量初始值的阶段。

从概念上讲, 这些变量所使用的内存都应当在方法区中进行分配, 但必须注意到方法区本身是一个逻辑上的区域, 在JDK 7及之前,HotSpot使用永久代来实现方法区时, 实现是完全符合这种逻辑概念的; 而在JDK 8及之后, 类变量则会随着Class对象一起存放在Java堆中, 这时候“类变量在方法区”就完全是一种对逻辑概念的表述了。

  • 这时候进行内存分配的仅包括类变量, 而不包括实例变量, 实例变量将会在对象实例化时随着对象一起分配在Java堆中。
  • 这里所说的初始值“通常情况”下是数据类型的零值。(类静态变量有默认值则是该值)

解析

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

符号引用参见class字节码的常量池部分–常量分类–符号引用解释

初始化

根据程序编码制定的主观计划去初始化类变量和其他资源。即:执行类构造器()方法的过程。

()并不是程序员在Java代码中直接编写的方法, 它是Javac编译器的自动生成物, 但我们非常有必要了解这个方法具体是如何产生的, 以及()方法执行过程中各种可能会影响程序运行行为的细节, 这部分比起其他类加载过程更贴近于普通的程序开发人员的实际工作。

类加载过程总结

类加载的过程就是:将类的二进制字节流装入内存,形成Class对象(加载),验证字节码是否符合规范(验证),为类变量分配内存赋零值,类变量存储于其类对象(准备),将符号引用转为直接引用(解析),执行类构造器初始化类包括类变量和静态代码块的实现(初始化)。


类加载器

Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现, 以便让应用程序自己决定如何去获取所需的类。 实现这个动作的代码被称为“类加载器”(Class Loader) 。

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

    即使这两个类来源于同一个Class文件, 被同一个Java虚拟机加载, 只要加载它们的类加载器不同, 那这两个类就必定不相等。

类加载器结构/划分

站在Java虚拟机的角度来看, 只存在两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader) ,使用C++语言实现, 是虚拟机自身的一部分;
  • 其他所有的类加载器, 由Java语言实现, 独立存在于虚拟机外部, 并且全都继承自抽象类java.lang.ClassLoader。

站在Java开发人员的角度来看, 类加载器就应当划分得更细致一些。 自JDK 1.2以来, Java一直保
持着三层类加载器、 双亲委派的类加载架构, 尽管这套架构在Java**模块化系统*8出现后有了一些调整变动, 但依然未改变其主体结构。

三层类加载器

绝大多数Java程序都会使用到以下3个系统提供的类加载器来进行加载:

  • 启动类加载器(Bootstrap Class Loader):负责加载存放在\lib目录, 或者被-Xbootclasspath参数所指定的路径中存放的、Java虚拟机能够识别的(*.jar, 名字不符合的类库即使放在lib目录中也不会被加载) 类库加载到虚拟机的内存中。

    启动类加载器无法被Java程序直接引用, 用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理, 那直接使用null代替即可

  • 扩展类加载器(Extension Class Loader) : 在sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。 负责加载\lib\ext目录中或者被java.ext.dirs系统变量所指定的路径中所有的类库。

    根据“扩展类加载器”这个名称, 就可以推断出这是一种Java系统类库的扩展机制, JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能, 在JDK 9之后, 这种扩展机制被模块化带来的天然的扩展能力所取代。 由于扩展类加载器是由Java代码实现的, 开发者可以直接在程序中使用扩展类加载器来加载Class文件。

  • 应用程序类加载器(Application Class Loader) : 这个类加载器由sun.misc.Launcher$AppClassLoader实现。 由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值, 所以有些场合中也称它为“系统类加载器”。 它负责加载用户类路径(ClassPath) 上所有的类库, 开发者同样可以直接在代码中使用这个类加载器。 如果应用程序中没有自定义过自己的类加载器, 一般情况下这个就是程序中默认的类加载器。

JDK 9之前的Java应用都是由这三种类加载器互相配合来完成加载的,且用户可以加入自定义的类加载器来进行拓展。
在这里插入图片描述
上图展示的各种类加载器之间的层次关系被称为类加载器的双亲委派模型(Parents Delegation Model)

双亲委派模型

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

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


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部