摘要:这也许是全网”最大“、”最细“、“最深”的java类加载原理图解了。

本文分享自华为云社区《【读书会第12期】这也许是全网”最大“、”最细“、“最深”的java类加载原理图解了》,作者: breakDawn。

关于类初始化的时机和误区

书籍的第一步部分上来就先讲了类初始化的时机,整理成图片如下:

看起来非常多,很难记住,很折磨。

个人认为,书籍把这一部分放到章节的最前面不太合理,曾经一度让我把上面的这些事件,理解成了类加载的时机,也不懂这些规则的缘由(根本原因还是此时读者对类加载的理解不够深。)

先贴一下类加载和类初始化的区别:

  • 类加载概念:将class文件加载到jvm中并生成class对象,并根据情况做初始化。
  • 类初始化概念:调用类class文件中默认存在的<cinit>类初始化方法。

而我们容易产生误解的原因,是因为书中没有这句话:所谓的类初始化时机,只是针对cinit类初始化方法的调用,并不是指的类加载时机!

以上图中红色的部分为例:

这里书籍中没有解释这3个规则的原因,在没理解原理前,强行记忆这3条是没有任何意义的。我认为是作者的失误。

在这里我挑其中一个做补充:

“使用类里的static final 常量,不会触发初始化”
想要理解这个规则,需要先理解class文件原理。
对于类的static final常量字段,它的常量值是存放在字段的constanValue属性中。

正因为如此,static final常量并不需要通过cinit方法中的指令来完成赋值。

所以也就没有必要在这时候调用<cinit>方法了。

因此对于“儿子类调用父类的静态成员,不用对儿子类做类初始化”也是一个道理,儿子类的类静态成员没有被使用到,没必要做cinit。

对于上面的分析,可以浓缩为一句话:

“如果我们急需使用static成员,且这个成员的值是要通过cinit方法赋值的,那么我们才做cinit初始化”

新的疑问:那为什么仅仅是new一个对象时,也一定要做cinit类初始化呢?
假设此时我还没用到static成员,那么new一个对象时,是否可以省去cinit,等用到静态成员的时候,再去触发cinit?

这涉及到了类初始化的另一个容易被忽视的点:“cinit类初始化方法,并不仅仅是做类成员的赋值,其实还可能包含一些初始化行为调用”,这可以是资源的启动或者加载等类对象必须要用到的内容。

因此在一切可能触发类对象实际行为前,必须触发cinit避免出错。

所以刚才的长篇大论,可以再次进行优化,浓缩为:
“当需要用到static成员的初始赋值,或者对类对象进行正式使用时,才会触发cinit类初始化,目的是为了保证类对象或者类成员的正确使用”
拿着这一句话,去回看前面的类初始化时机的触发时机和不触发的时机时,相信你就会有更深的理解了,甚至也不需要强行去记忆每一条规则了。

有误导的“加载三部曲”

有一个很经典的回答,叫做类加载三部曲:加载、连接、初始化
好像类加载过程就是这三步按照顺序串行拼装起来的。

实际上这3个过程是存在交叉的!
只能说,“最早发生”的时机,是按照这个顺序发生,但是中间加载过程是有很多的,具体后面会结合我画的图以及原理解释进行呈现。

加载:不仅仅是读取字节流

对于加载,很容易只理解成只是“从文件里加载二进制字节到内存”。

这个过程显然是必须最先执行的,否则连类的基本信息都获取不到。

可以看到这个过程很灵活,只要你从你能想到的地方拿到字节流即可,任意形式都行。

然而,对于“加载”,除了获取字节流,实际上还包含了“把字节流转成方法区里的数据结构,进行存储defineClass”、“生成一个class对象,存储在堆中”这两步。

这2步是穿插在连接过程中的。

比如字节流转数据结构的过程,必须在确认字节流的正确性之后完成。

而生成class对象同理,符合一个class对象的条件时,才能将其在堆中生成。

连接

连接过程可以说是最难记住的一个过程, 里面包含了各种校验啊之类的,让人摸不清头脑。这里会通过更细致的解释和图解,让你明白连接过程究竟做了什么。

首先连接过程分为 验证、准备和解析,“解析”并不是连接的最后一步,而是在验证过程中实时发生的!。 下文会为你详细解释为什么。

验证

文件格式校验(class文件对不对)

注意这里的校验,都是一些最简单的校验,相当于无需做太多的语法分析操作等操作, 都是基于class文件格式定义进行的基础校验。

然而如果对加载的文件有充分的自信,来源可靠,那么确实可以省去这个步骤,提升连接效率,因此会有一个-Xverify:none的选项供使用。

元数据验证(我的父亲对不对)

这里验证了class文件里面继承特性相关的重要信息,例如继承关系是否合理、是否实现了抽象类或接口的方法

注意,这个元数据验证的过程,会触发父类或者接口的解析(加载)操作!

书上提到了4个解析情况以及流程:

  • 类解析
  • 字段解析
  • 类方法解析
  • 接口方法解析
    却没有解释这4个解析过程是在哪里发生的。后面我会逐一提到,来真正理解这4个解析过程。

元数据验证中的类解析

还记得class文件中,父类是指向一个constant_class_info吗?这个东西当时看就是一个utf字符串,没什么意义。你没法知道父类究竟有什么方法,是不是抽象类。因此必须拿到父类的类信息,要么是已经在方法区中,要么需要重新加载。

而类解析的过程如下:

可以看到这个过程中也会发生加载,甚至好多次加载。

字节码验证(我的指令对不对)

这个验证不要和前面的“文件格式验证”搞混了。
前面的“元数据验证”都只是针对类、方法、字段等和父类进行确认、校验。
但是还没有涉及到每个方法里的code属性。

code属性虽然在编译出来时是正确的,但是无法保证传输过程中被人篡改。

如果发生操作操作数栈时,栈里没东西,或者试图在局部变量表边界外写入局部变量,就可能导致不可估量的后果。

因此此刻会进行最基本的指令分析,确认对操作数栈、局部变量表的操作是安全、正确的。

但是,逐个指令分析,会不会太慢了?如果代码很长的话。

还记得class文件的code属性中,还包含了一个stackMapTable属性么,估计很多人都跳过了这个属性。

这个属性就是用在字节码验证这个过程,可以立即让编译器编译出class时,提前把各位置的情况写入stackMap中,jvm加载时只对这个stackMap做校验确认是对的即可。

但代价就是可能不安全了,因为这个stackMap是可以被篡改的。

符号引用验证(我的指令调用的目标对不对)

注意前面的“字节码验证”是简单的确认,但不会持有过多的其他类的信息。但是方法肯定会涉及对其他类的调用。

此时就会涉及到符号引用验证,确认自己是否拥有对方方法的访问权限。
那么你就需要找到目标类的类信息存放地址,确认方法权限,或者字段权限。
于是会在这里触发字段解析、类方法解析或者接口解析!

书上只提到了这3个解析过程的流程,却没有详细解释其中的一些缘由,我会做更详细的补充。

符号引用验证中的字段解析

class中的constant_filed_info终于露出了它的真面目,原来是用在这个地方,即和字段相关的指令会用到它,并通过字段符号引用, 解析到这个字段真正的定义位置。

像经常遇到的NoSuchFieldError报错,就是在这个过程中爆出来的。而且接口字段的优先级是大于父类的字段的。

符号引用验证中的类方法解析

当调用方法前,需要先确认对象方法是否有权限访问。那么就必须这个类的信息进行确认。

注意:这个过程并不是动态分派的那个过程,此刻并没有触发任何的方法调用!仅仅是确认代码中静态类型的访问权限是否正确之类的!

  • 对类方法做解析的时候,会判断此时是类还是接口。如果是接口,竟然会报“IncompatibleClassChangeError”。
  • 还有如果是抽象类,也会报“AbstractMethodError”,因为正常情况下,你的jvm指令调用的方法,必须是实例化的对象所对应的方法,不可能直接调用抽象类方法的。

符号引用验证中的接口方法解析

看起来像是将类方法解析中的接口和方法互换了位置。

疑问1:为什么接口方法还要解析?接口不是没有代码吗?

因为接口类里每个interface方法,本身也是一个方法,只不过没有详细的code属性。但方法的访问修饰符之类的都存在,因此验证阶段还是需要进行校验。

疑问2:为什么要区分类的方法和接口方法?不能用同一种思路去解析么?

我理解的几个原因:

  1. 向上搜索时的逻辑不同,对于类方法,直接找父类即可, 而接口则需要遍历所有父接口。而且类方法还要考虑抽象类的问题,接口不需要。
  2. 类方法和接口方法本身就是两个不同的符号引用, 一个是constant_method_ref,另一个是constant_interface_ref,用2套逻辑没什么毛病
  3. 如果硬要问为什么要区分这2个符号引用,明明内容都是类索引+描述符索引?
    这是因为后面在实际调用方法时,二者有显著区别,具体见下文的“方法表的准备”。

准备

类静态成员默认值的准备

对于准备阶段,大家一般只记得需要对一些非final的类静态成员做默认初始值操作。

方法表的准备

除了这个默认值赋值,还有一个动作,是准备方法表。

方法表就是为了多态而生,简化动态分派时频繁的迭代循环带来的不必要消耗:

通过前面的验证过程,我们已经获知了父类信息。因此可以准备一个方法表,把父类方法堆到最前面,自己的方法堆到后面,后面直接根据索引获取方法调用地址即可!

重要问题:interface的接口方法,会有方法表吗?

intefacer接口是不具有方法表的!
因此这可能也是jvm特地区分了class_inteface_info和class_method_info这2个常量,以及特地用invoke_inteface和invoke_virtual指令来区分2类方法的调用。因为他们的调用逻辑可能大相径庭。

为什么接口不能有方法表?

这是由于Java可以实现多个接口,不同的类可能会实现了多个或者不同的接口,在虚表里该接口所实现方法的索引会不一致。

假设有A、B、C三个接口类

  • 类X实现了A、B两个接口,假设A和B接口放在虚表里,那么调用A接口方法我们假设它是在t位置。
  • 类T实现了B、C、A接口,按照实现顺序,先放B的方法,再放A的方法,最后放C的方法。这样调用接口A时,就不一定是t位置了,我们无法直接确定A里面方法的位置,因为一个类可以实现多个接口,而且顺序可以随意更改!

这样每次解析的虚表索引都可能会不同,因此不能进行缓存,需要每次都进行重新的解析。因此,接口的方法调用会比普通的子类继承的虚函数调用要慢。

解析

解析其实分为“静态解析”和“动态解析”。
因此将解析说成是“连接”中的一部分是不严谨的, 只有静态解析,才是“连接”的一部分。
静态解析用于解析私有方法、父类构造器、final方法等不存在多态可能的方法。

而动态解析则会在类加载的范围外去使用。

初始化

cinit方法细节解析

关于初始化时机的解释,在开头就已经阐述过了,这里不再重复解释。

疑问1:cinit方法中的代码是如何生成的?

cinit方法 是编译器收集所有类静态变量的赋值动作和静态语句块static{}中的语句合并产生,按照顺序收集。
因此类加载赋值的顺序和类定义顺序有关,原理就取决于cinit生成的原理。

疑问2:cinit类初始化是线程安全的吗?

是线程安全的,虚拟机会保证一个类的加载和cinit方法会被正确的加锁、同步。

因此多线程场景下,同时使用一个之前没初始化过的类,且类初始化过程耗时非常久的话, 且可能会造成线程阻塞。

而这也是可以利用类初始化+内部类的方式,来做单例模式的实现的原理:

初始化中的动态解析

而初始化过程中,可能会涉及其他对象实例方法的调用,因此是可能发生动态解析过程的!
类方法和接口方法的解析过程如下
类方法的解析可以借助虚方法表简化解析过程。

扩展:invoke_dynamic是什么

对于invoke_dynamic指令做什么的?涉及动态分派、类加载和解析吗?

我们首先看下invoke_dynamic指令调用的dynamic_info常量长什么样的:

可以看到它只包含了一个方法索引和描述,但似乎没包含方法属于哪个类。

它的作用是用java实现一些类似于脚本语言的逻辑,脚本语言不关心静态类型,不做编译检查,只关心运行期的内容。所以invoke_dynamic以及constant_dynamic_info应运而生。但书本和工作中对这块的接触都不是太深,因此我的理解也只能局限于此了。

最后的完整大图

好累,终于写完了,感觉能看到最后的人不会太多,但一通详细地分析和解决中间发现的问题,还是收获了不少。

最后贴上完整的大图,欢迎保存和收藏。

图解笔记系列也会持续更新下去,争取做全网最细又最大的java分享文章。在线地址:https://www.processon.com/view/link/5e7eed6ce4b0ffc4ad43fda8

欢迎点击该链接报名参加读书会,一起成长学习和交流!报名链接

点击关注,第一时间了解华为云新鲜技术~

手绘图解java类加载原理的更多相关文章

  1. 【转载】Java类加载原理解析

    Java类加载原理解析 原文出处:http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html 1       基本信息 摘要: 每个j ...

  2. 深入理解Java类加载器(一):Java类加载原理解析

    摘要: 每个开发人员对java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这个异常背后涉及到的是Java技术体系中的类加载机制.本文简述了JVM三种预定义类加载器,即 ...

  3. Java类加载原理解析

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt229 2       Java虚拟机类加载器结构简述 2.1    JVM三 ...

  4. 深入理解Java类加载器(1):Java类加载原理解析

    1 基本信息 每个开发人员对Java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这背后就涉及到了java技术体系中的类加载.Java的类加载机制是技术体系中比较核心的 ...

  5. 【转】Java类加载原理解析

    原链接 1 基本信息 每个java开发人员对java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这背后就涉及到了java技术体系中的类加载. Java的类加载机制是j ...

  6. Java类加载原理解析(转)

    1 基本信息 每个开发人员对java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这背后就涉及到了java技术体系中的类加载.Java的类加载机制是技术体系中比较核心的 ...

  7. java类加载器深入研究

    看了下面几篇关于类的加载器的文章,豁然开朗.猛击下面的地址开始看吧. Java类加载原理解析      深入探讨 Java 类加载器 分析BootstrapClassLoader/ExtClassLo ...

  8. 深入理解:java类加载器

    概念理解:Java类加载器总结 1.深入理解Java类加载器(1):Java类加载原理解析 2.深入理解Java类加载器(2):线程上下文类加载器

  9. Java类加载机制总结

    关于Java类加载机制的几个基本概念: JDK提供的基本类加载器:引导类加载器(Bootstrap Class Loader)-用于加载JDK中的核心类.扩展类加载器(Ext Class Loader ...

  10. 深入理解Java类加载器(二):线程上下文类加载器

    摘要: 博文<深入理解Java类加载器(一):Java类加载原理解析>提到的类加载器的双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器的实现方式.在Java ...

随机推荐

  1. 《C++ Primer》 ---- 关于变量 与 基本类型

    类型是所有程序的基础;    C++ 定义了几种基本类型: 字符型(char 和 wchar_t),整型(short int long bool),浮点型(float doubel) 并且提供自定义数 ...

  2. 【bzoj1853】 Scoi2010—幸运数字

    http://www.lydsy.com/JudgeOnline/problem.php?id=1853 (题目链接) 今天考试考了容斥,结果空知道结论却不会写→_→ 题意 求区间中不含6,8两个数字 ...

  3. Java中类的初始化顺序

    一.一个类的初始化顺序(没继承情况)  规则: 1.静态变量>普通变量>构造方法   2.变量定义的顺序决定初始化的顺序 3.静态变量和静态块是一样的,普通变量和非静态块是一样的,即能够把 ...

  4. Html5新增加的属性

    用2中方法给单复选框增加新的特性,使直接点击文字就可以被选中 1.将选项放入label标签内添加for属性,并在input标签内添加id,两者值相同. 2.将input标签放到label标签内,注意l ...

  5. 一、Python-----之变量

    1.变量存在的意义就是在程序中存储一些临时的数据.2.程序运行的时候回调用变量的临时数据. 变量起名规则: 变量名只能是字符.数字或下划线的任意组合 变量名的第一个字符不能是数字 以下关键字不能声明为 ...

  6. Python 文本解析器

    Python 文本解析器 一.课程介绍 本课程讲解一个使用 Python 来解析纯文本生成一个 HTML 页面的小程序. 二.相关技术 Python:一种面向对象.解释型计算机程序设计语言,用它可以做 ...

  7. 重温.NET下Assembly的加载过程

    最近在工作中牵涉到了.NET下的一个古老的问题:Assembly的加载过程.虽然网上有很多文章介绍这部分内容,很多文章也是很久以前就已经出现了,但阅读之后发现,并没能解决我的问题,有些点写的不是特别详 ...

  8. xmind-postman

    参考: https://www.jianshu.com/p/61cfcb436ee4 https://www.jellythink.com/archives/category/tool-tutoria ...

  9. laravel——ajax分页&amp;删除&amp;搜索

    一.视图代码 /*搜索*/<form action="javascript:search_brand()" name="searchForm"> & ...

  10. php无法连接mysql问题解决方法总结

    http://www.163ns.com/zixun/post/5295.html     本文章总结了在php开发中可能会常常碰到的一些php连接不了mysql数据库的一些问题总结与解决方法分享,有 ...