本文已经收录到个人Github我的博客,欢迎大佬们光临寒舍:java
个人GIthub博客git
今天想跟你们唠嗑唠嗑Java
的类加载机制,这是Java
的一个很重要的创新点,曾经也是Java
流行的重要缘由之一。github
Oracle
当初引入这个机制是为了知足Java Applet
开发的需求,JVM
咬咬牙引入了Java
类加载机制,后来的基于Jvm
的动态部署,插件化开发包括你们热议的热修复,总之不少后来的技术都源于在JVM
中引入了类加载器。面试
现在,类加载机制也在各个领域大放异彩,在面试中,由类加载机制所衍生出来各种面试题也层出不穷。数据库
因此,咱们要了解下类加载机制,为工做中或者是面试中实际的须要打好良好的基础。数组
Q1:JVM
类加载机制定义:安全
虚拟机把描述类的数据从Class
文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成可被虚拟机直接使用的Java
类型的过程网络
Q2:特性数据结构
运行期类加载。即在Java
语言里面,类型的加载、链接和初始化过程都是在程序运行期完成的,从而经过牺牲一些性能开销来换取Java
程序的高度灵活性多线程
什么是运行期,什么是编译期?
- 编译期是指编译器将源代码翻译为机器能识别的代码,
Java
被编译为Jvm
认识的字节码文件- 运行期则是指
Java
代码的运行过程
JVM
运行期动态加载+动态链接->Java
的动态扩展特性
类从被加载到虚拟机内存中开始、到卸载出内存为止,整个生命周期包括七个阶段:
加载
验证
准备
解析
初始化
使用
卸载
其中,验证、准备、解析这3个部分统称为链接,流程以下图:
注意:
- 『加载』->『验证』->『准备』->『初始化』->『卸载』这五个阶段的顺序是肯定的,而『解析』可能为了支持
Java
的动态绑定会在『初始化』后才开始- 上述阶段一般都是互相交叉地混合式进行的,好比会在一个阶段执行的过程当中调用、激活另一个阶段
想要了解Java
动态绑定和静态绑定区别的话,能够看下这篇文章:理解静态绑定与动态绑定
Q1:任务
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
异常的子类- 注意:该验证发生在虚拟机将符号引用转化为直接引用的时候,即『解析』阶段
Q1:任务
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
异常
Q1:字段(成员变量/域)和属性有什么区别?
- 属性,是指对象的属性,对于
JavaBean
来讲,是getXXX
方法定义的- 字段,是成员变量
class Person{
private String mingzi; //mingzi是字段,通常来讲字段和属性是相同的,可是这个例子是特例
public String getName(){ //name是属性
return mingzi:
}
public void setName(){
mingzi= "张三";
}
}
复制代码
Java
代码。而以前的类加载过程当中,除了在『加载』阶段用户应用程序可经过自定义类加载器参与以外,其他阶段均由虚拟机主导和控制
- 准备阶段:变量赋初始零值
- 初始化阶段:根据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();为例进行说明
若是文章对您有一点帮助的话,但愿您能点一下赞,您的点赞,是我前进的动力
本文参考连接: