本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍:
我的GIthub博客
今天想跟大家唠嗑唠嗑 Java
的类加载机制,这是 Java
的一个很重要的创新点,曾经也是 Java
流行的重要原因之一。
Oracle
当初引入这个机制是为了满足 Java Applet
开发的需求, JVM
咬咬牙引入了 Java
类加载机制,后来的基于 Jvm
的动态部署,插件化开发包括大家热议的热修复,总之很多后来的技术都源于在 JVM
中引入了类加载器。
如今,类加载机制也在各个领域大放异彩,在面试中,由类加载机制所衍生出来各类面试题也层出不穷。
所以,我们要了解下类加载机制,为工作中或者是面试中实际的需要打好良好的基础。
虚拟机把描述类的数据从 Class
文件 加载 到内存,并对数据进行 校验 、 转换解析 和 初始化 ,最终形成可被虚拟机直接使用的 Java
类型的过程
运行期类加载。即在 Java
语言里面,类型的加载、连接和初始化过程都是在程序 运行期 完成的,从而通过牺牲一些性能开销来换取 Java
程序的高度灵活性
什么是运行期,什么是编译期?
Java
被编译为 Jvm
认识的 字节码文件 Java
代码的 运行 过程 JVM
运行期动态加载+动态连接-> Java
的动态扩展特性
类从被加载到虚拟机内存中开始、到卸载出内存为止,整个生命周期包括七个阶段:
其中,验证、准备、解析这3个部分统称为 连接 ,流程如下图:
注意:
Java
想要了解 Java
动态绑定和静态绑定区别的话,可以看下这篇文章: 理解静态绑定与动态绑定
ZIP
包读取、从网络中获取、通过运行时计算生成、由其他文件生成、从数据库中读取等等途径...... 想要详细了解类的全限定名的知识,可以看下这篇文章: 全限定名、简单名称和描述符是什么东西?
java.lang.Class
对象,它将作为程序访问方法区中的这些类型数据的外部接口 JVM
类加载子系统中占了相当大的一部分 Class
文件的字节流中包含的信息 符合 当前 虚拟机的要求 ,并且 不会危害虚拟机自身的安全 由此可见,它能直接决定 JVM
能否承受恶意代码的攻击,因此验证阶段 很重要 ,但由于它对程序运行期没有影响,并 不一定必要 ,可以考虑使用 -Xverify:none
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
检验过程包括下面四个阶段:
A.文件格式验证:
内容:验证 字节流是否符合 Class
文件格式的规范 、以及是否能被 当前版本的虚拟机处理
目的:保证输入的 字节流 能正确地解析并存储于 方法区 之内,且格式上符合描述一个 Java
类型信息的要求。只有保证二进制字节流通过了该验证后,它才会进入内存的方法区中进行存储,所以 后续3个验证阶段全部是基于方法区 而不是字节流了
例子:
是否以魔数 0xCAFEBABE
开头
主次版本号是否在 JVM
接受范围内
索引值是否有指向不存在/不符合类型的常量
......
B.元数据验证:
内容:对字节码描述的信息进行 语义 分析,以保证其描述的信息符合 Java
语言规范的要求
目的:对类的 元数据信息 进行语义校验,保证不存在不符合 Java
语言规范的元数据信息
例子:
类是否有父类(除了 java.lang.Object
之外,所有类都应有父类)
父类是否继承了不允许被继承的类( final
修饰的类)
如果该类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
......
C.字节码验证:
是验证过程中 最复杂 的一个阶段
内容:对类的 方法体 进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件
目的:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
例子:
保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现“在操作数栈的数据类型中放置了 int
类型的数据,使用时却按 long
类型来载入本地变量表中”
保证任何跳转指令都不会跳转到方法体外的字节码指令上
......
D.符号引用验证:
java.lang.IncompatibleClassChangeError
异常的子类 Java
堆中 之前提过,解析阶段就是虚拟机将 常量池 内的 符号引用替换为直接引用 的过程
Java
虚拟机规范的 Class
文件格式中,所以即使各种虚拟机实现的内存布局不同,但是能接受符号引用都是一致的 JVM
会根据需要来判断,是在类被加载器 加载时 就对常量池中的符号引用进行解析,还是等到一个符号引用将要被 使用前 才去解析 CONSTANT_Class_info
) CONSTANT_Fieldref_info
) CONSTANT_Methodref_info
) CONSTANT_InterfaceMethodref_info
) CONSTANT_MethodType_info
) CONSTANT_MethodHandle_info
) CONSTANT_InvokeDynamic_info
) 举个例子,设当前代码所处的为类 D
,把一个从未解析过的 符号引用 N
解析为一个 类或接口 C
的直接引用 ,解析过程分三步:
C
不是数组类型: JVM
将会把代表 N
的全限定名传递给 D
类加载器去加载这个类 C
。在加载过程中,由于 元数据验证 、 字节码验证 的需要,又可能触发其他相关类的加载动作。一旦这个加载过程出现了任何异常,解析过程就宣告失败。 C
是数组类型且数组元素类型为对象: JVM
也会按照上述规则加载数组元素类型 C
在 JVM
中已成为一个有效的类或接口,但在解析完成前还需进行 符号引用验证 ,来确认 D
是否具备对 C
的访问权限。如果发现不具备访问权限,将抛出 java.lang.IllegalAccessError
异常 JavaBean
来说,是 getXXX
方法定义的 class Person{ private String mingzi; //mingzi是字段,一般来说字段和属性是相同的,但是这个例子是特例 public String getName(){ //name是属性 return mingzi: } public void setName(){ mingzi= "张三"; } }
Java
代码。而之前的类加载过程中,除了在『 加载 』阶段用户应用程序可通过 自定义类加载器 参与之外, 其余阶段均由虚拟机主导和控制 clinit
clinit
:由编译器自动收集类中的所有 类变量(静态变量)的赋值动作 和静态语句块 static{}
中的语句合并产生
clinit
clinit
不需要先执行父接口 的 clinit
,只有当父接口中定义的变量使用时,父接口才会初始化。另外, 接口的实现类在初始化时 也一样不会执行接口的 clinit
想详细了解 clinit
以及其与 init
的区别的读者,可以看下这篇文章: 深入理解jvm--Java中init和clinit区别完全解析
new
、 getstatic
、 putstatic
或 invokestatic
这4条字节码指令时 java.lang.reflect
包的方法对类进行反射调用的时候 JDK1.7
的动态语言支持时,若一个 java.lang.invoke.MethodHandle
实例最后的解析结果为 REF_getStatic
、 REF_putStatic
、 REF_invokeStatic
的方法句柄,且这个方法句柄所对应的类未进行初始化,需先触发其初始化。 每个类加载器,都拥有一个独立的命名空间,它不仅用于加载类,还和这个类本身一起作为在 JVM
中的唯一标识。所以比较两个类是否相等,只要看它们是否由同一个 类加载器 加载,即使它们来源于同一个 Class
文件且被同一个 JVM
加载,只要加载它们的 类加载器不同,这两个类就必定不相等
从 JVM
的角度,可将类加载器分为两种:
C++
语言实现,是虚拟机自身的一部分 <JAVA_HOME>/lib
目录中、或被 -Xbootclasspath
参数所指定路径中的、且可被虚拟机识别的类库 Java
程序直接引用,如果自定义类加载器想要把加载请求委派给引导类加载器的话,可直接用 null
代替 Java
语言实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader
,可被 Java
程序直接引用。常见几种: A.由 sun.misc.Launcher$ExtClassLoader
实现
B.负责加载 <JAVA_HOME>/lib/ext
目录中的、或者被 java.ext.dirs
系统变量所指定的路径中的所有类库
A.是 默认 的类加载器,是 ClassLoader#getSystemClassLoader()
的返回值,故又称为 系统类加载器
B.由 sun.misc.Launcher$App-ClassLoader
实现
C.负责加载用户类路径上所指定的类库
自定义类加载器:如果以上类加载起不能满足需求,可自定义
需要注意的是:虽然 数组类 不通过类加载器创建而是由 JVM
直接创建的,但仍与类加载器有密切关系,因为 数组类的元素类型最终还要靠类加载器去创建
Java
设计者推荐给开发者的一种类加载器实现方式 Java
程序的稳定运作;实现简单,所有实现代码都集中在 java.lang.ClassLoader的loadClass()
中 比如,某些类加载器要加载 java.lang.Object
类,最终都会委派给最顶端的启动类加载器去加载,这样 Object
类在程序的各种类加载器环境中都是同一个类。
相反,系统中将会出现多个不同的 Object
类, Java
类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱
恭喜你!已经看完了前面的文章,相信你对 JVM
类加载机制已经有一定深度的了解,下面,进行一下课堂小测试,验证一下自己的学习成果吧!
Q1:类加载的全过程是怎样的?
Q2:什么是双亲委派模型?
Q3: String
类如何被加载的
上面问题的答案,在前文都提到过,如果还不能回答出来的话,建议回顾下前文
Q4:请你谈谈类加载过程,以 Person a = new Person();
为例进行说明
这道题是在牛客的暑假实习 Tencent
一面的面筋上找的,附上标准答案: 类的加载过程,Person person = new Person();为例进行说明
如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力
本文参考链接: