Java类是如何加载的?
@[toc] 有小伙伴最近在面试过程中遇到这样一个问题:
Java 中的类是如何加载的?
这个问题还是很有意思,今天松哥来尝试和大伙梳理一下。
一 整体思路
整体上来说,类的加载主要是下面这几个步骤:
上面这张图就是一个类的完整生命周期了,一共要经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个不同的步骤。
这七个步骤中,验证、准备和解析一般又统一称之为 Linking。
这是整体的流程,接下来,松哥就和大家来分析每一个具体的步骤都干了啥。
二 Loading
首先第一步 Loading,也就是加载类。
这里如果被面试官细问,有两个方向:
- 什么时候加载?
- 怎么加载?
2.1 类的加载时机
先说类的加载时机。
如果需要一个权威的文档来说明问题,抱歉,官方没有任何文档来说明类在什么时候会被加载。但是,官方文档给出了六种类必须进行初始化的场景,毫无疑问,如果需要对类进行初始化,那么就必须先 Loading。
这六种场景分别是:
- new 一个类或者使用某一个类的静态属性/静态方法,给某个类的静态属性赋值等等,不过对于被 final 修饰的的 static 变量除外。
- 通过反射调用某个类的时候。
- 当要初始化某个类,但是发现其父类尚未初始化,那么就要去初始化父类(如果一个接口在初始化的时候发现其父类未初始化,这个时候并不会初始化其父类,只有在真正用到了其父类的时候,才会初始化)。
- main 方法所在的主类。
- 对于含有 default 方法的接口,如果该接口的实现类需要进行初始化,那么就会触发该类的加载。
- 最后一种情况和动态语言相关的,跟我们 Java 关系不大,这里就不讨论了(因为 Java 虚拟机不仅能跑 Java,也能跑 Groovy、Kotlin 等,所以虚拟机支持的内容会更加广泛一些)。
只有这六种场景会触发类的初始化,凡是不符合这六种情况的,都不会触发类的初始化。
这是类的加载时机问题。
2.2 类的加载步骤
那么怎么加载呢?这就涉及到类加载的双亲委派问题,这个问题网上有很多文章介绍,内容本身也不算难,这里松哥就不啰嗦了。
通过双亲委派找到具体的类加载器之后,接下来就要开始执行加载了。
加载主要干三件事。
- 通过类的全限定名来获取定义该类的二进制字节流。
全限定名也就是类的全路径,例如 org.javaboy.HelloWorld 这种,通过这个名字去获取类的二进制字节流。去哪里获取呢?可以从磁盘上获取,这是我们最容易想到的,除了从磁盘上获取之外,也可以从网络获取,甚至可以在运行时通过动态计算生成,我们所熟知的 Java 动态代理就属于这种情况。
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
-
在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。
三 Linking
Linking 这个环节分为三个步骤,分别是:
- Verification
- Preparing
- Resolution
我们分别来看。
3.1 Verification
验证这个环节就要就是检查输入的二进制字节流是否符合要求。
正常来说,我们的 Java 代码写完之后会进行编译,有问题的话,编译阶段就报错了,等不到类加载阶段。
不过由于 JVM 读取的二进制字节流不一定是通过 Java 源代码编译后获取到的,也有可能是其他语言编译得到的,甚至可能有某个大神直接用二进制编辑器 0、1、0、1 这样敲出来的,所以站在 JVM 的角度,必须要对输入的二进制流进行校验,确保读取的数据没有问题。
验证的内容主要有这些:
- 魔数是否以
0xCAFEBABE
开始。
魔数是 Class 文件的开始标记,这个位置是一个固定的字符,CAFE BABE。
松哥这里随便用二进制编辑器打开一个 Class 文件给大家看下:
- 主次版本号是否在当前 Java 虚拟机所能接受的范围内。
CAFEBABE 后面紧跟着的是次版本号,次版本号后面紧跟着的是主版本号。以上图为例,次版本号为 0,主版本号 3D 转为十进制是 61。高版本的 JDK 可以向下兼容以前旧版本的 Class 文件,但是无法运行以后版本的 Class 文件,Class 文件的主版本号和 JDK 的关系如下图。
|JDK 版本号|Class 主版本号| |:--|:---| |JDK 19|63| |JDK 18|62| |JDK 17|61| |JDK 16|60| |JDK 15|59| |JDK 14|58| |JDK 13|57| |JDK 12|56| |JDK 11|55| |JDK 10|54| |JDK 9|53| |JDK 8|52| |JDK 7|51| |JDK 6.0|50| |JDK 5.0|49| |JDK 1.4|48| |JDK 1.3|47| |JDK 1.2|46| |JDK 1.1|45.0-45.6|
- 常量池中是否有不被支持的常量类型
- 当前类是否存在父类(所有类都应当有父类)?当前类是否继承了 final 类(不应当继承 final 类)?如果当前类不是抽象类,是否实现了其父类或者接口中要求实现的方法等等。
- 对字节码进行校验。
- 符号引用能否找到对应的类,符号引用中涉及到的类、字段、方法等的访问性是否满足要求。
由于验证这块的环节非常复杂,流程也多,因此,如果自己有办法确认自己的代码是 OK 的,那么也可以使用 -Xverify:none 来关闭大部分的类验证,这样可以缩短虚拟机加载类的时间。
这里检查的内容其实非常多,官方文档足足有 100 多页,松哥这里就不逐一列举了,小伙伴们主要是知道这里的核心目的是检查并确保读入到内存中的字节流是没有问题的。
3.2 Preparing
这一阶段主要是给类中的静态属性设置初始值。
例如定义了 public static int a = 5;
,那么就会为该变量在内存中(堆)分配存储空间,并设置初始值(int 类型初始值是 0),注意这个时候并不会将 a 设置为 5,因为还没到最终的初始化阶段。
但是如果属性在定义的时候就已经定义为常量了,例如 public final static int a = 5;
,则会直接给属性最终赋值。
3.3 Resolution
接下来是解析,解析主要是将常量池内的符号引用替换为直接引用的过程。
什么是符号引用呢?
符号引用是以一组符号来描述引用的目标,因为在编译阶段,虚拟机并不知道所引用的类的具体位置,因此就使用符号引用来代替。符号可以是任何字面量,只要在使用时能够无歧义的定位到目标即可。
什么是直接引用呢?
直接引用就是一个可以直接指向目标的指针,相对偏移量等。
所以,符号引用转为直接引用其实就是原本是通过字符去引用某个变量,现在直接改为通过内存地址来访问该变量了。
解析的符号主要有七种,分别是类或者接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符。
松哥这里以一个类的解析为例,和小伙伴们简单说明一下这个解析过程。
假设当前类是 C1,当前类中存在一个符号引用 F,我们要将这个符号引用 F 解析为一个类 C2,那么流程是这样:
- 如果 C2 是一个普通对象而不是接口,那么 JVM 会把代表 F 的全限定名传递给 C1 的类加载器去加载这个类,当然,这个加载过程又是一整套的类加载流程。
- 如果 C2 是一个对象数组,那么首先按照第一步的方式先去加载数组中的元素类型,然后由虚拟机去生成一个代表该数组的对象。
就这样简单两个步骤,当然,在这个流程中,也会去检查 C1 是否具备对 C2 的访问权限,这个主要是检查 module 访问权限和类的访问权限。
四 Initialization
接下来就是类的初始化阶段了,如果想让这个阶段更加具象化,那么这个阶段实际上是调用类的 clinit 方法,这个方法并不是开发者写的,而是由 javac 编译器自动生成的。
javac 自动生成的 clinit 方法主要是将静态变量赋值和静态代码块的相关内容合并起来。在执行 clinit 方法的过程中,并不会显式的调用父类的 clinit 方法,而是由虚拟机去确保在执行子类的 clinit 方法之前,父类的 clinit 方法已经被执行过了。
例如为 static 类型的变量赋值,就是在这个环节完成的。
五 Using/Unloading
最后就是 Using 和 Unloading 了,这块就简单了,不多说。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
二十万分之一几率:if语句变do-while卡死问题分析|得物技术
一、背景 某次灰度发布之后没多久就收到线上ANR告警,经排查定位到是某个页面onCreate方法执行太久导致,而火焰图中的耗时堆栈指向了我们用于监控页面启动速度的一段插桩代码,反编译Apk之后发现本该是if语句的代码竟变成了一个do-while语句,形成了死循环最终导致主线程卡死。 此后每构建二、三十次都会复现一次该问题,且每次的异常页面,异常方法完全随机。 二、问题分析 if和do-while两个完全不相干的语句为什么出现互相转化的情况?在jadx反编译而来的smali代码中不难看出,if语句对应的标签正常情况下应该指向的是return语句,和Java源码中if语句块后面紧跟着return语句对应。而异常情况下标签跑到了整个函数的开头,故被jadx翻译成了do-while,因此问题的关键就在这个label上面。 初步分析 出现此问题的这段插桩代码出自我们的APM页面启动监控,原本是插桩在Activity和Fragment的onCreate等关键生命周期中用于耗时统计。其所在的类是由我们自定义的插桩plugin weaver所生成(基于byteX开发的一个plugin,支持插入,代理和...
- 下一篇
如何动态调试线程池?
这是有小伙伴最近在面深信服的时候遇到的一个问题,感觉比较有意思,松哥和大伙来聊一聊。 如何动态调试线程池? 面试官表示设置线程池核心线程数是一个非常具有挑战性的事情,问有无办法能够动态的设置线程池核心数,并观察其执行效果? 这个问题的难点在于它涉及到的技术点不是特别常用,该小伙伴面试的技术团队刚好是做运维工具的,做一些监控软件,所以刚好就问到这里。 那么松哥和大家简单聊一聊这个话题。 其实这里主要是涉及到 Java 里边一个比较古老的工具,JMX。 一 什么是 JMX JMX(Java Management Extensions)是 Java 平台的一部分,它提供了一种管理和监控 Java 应用程序的标准方法。JMX 允许你监控和管理系统资源、应用程序和服务,以及获取关于这些实体的运行时信息。 简单来说,就是通过 JMX 可以动态查看对象的运行信息,并且可以动态修改对象属性。 JMX 架构如下图: 分析这张图我们可以发现,JMX 底层是由很多不同的 MBeans 组成的,MBeans 是 JMX 的核心,它们是实现了特定接口的 Java 对象,用于表示可以被监控和管理的资源。MBean...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS6,CentOS7官方镜像安装Oracle11G
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS关闭SELinux安全模块
- Red5直播服务器,属于Java语言的直播服务器
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS7,CentOS8安装Elasticsearch6.8.6