趣文网 > 作文大全

做了这么久的 JAVA 你真的懂 JVM 的类加载机制吗?

2020-12-17 07:45:01
相关推荐

学习导图

一.为什么要学习类加载机制?

今天想跟大家唠嗑唠嗑Java的类加载机制,这是Java的一个很重要的创新点,曾经也是Java流行的重要原因之一。

Oracle当初引入这个机制是为了满足Java Applet开发的需求,JVM咬咬牙引入了Java类加载机制,后来的基于Jvm的动态部署,插件化开发包括大家热议的热修复,总之很多后来的技术都源于在JVM中引入了类加载器。

如今,类加载机制也在各个领域大放异彩,在面试中,由类加载机制所衍生出来各类面试题也层出不穷。

所以,我们要了解下类加载机制,为工作中或者是面试中实际的需要打好良好的基础。

二.核心知识点归纳

2.1 概述

Q1:JVM类加载机制定义:

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

Q2:特性

运行期类加载。即在语言里面,类型的加载、连接和初始化过程都是在程序运行期完成的,从而通过牺牲一些性能开销来换取Java程序的高度灵活性

什么是运行期,什么是编译期?

编译期是指编译器将源代码翻译为机器能识别的代码,Java被编译为Jvm认识的字节码文件,运行期则是指Java代码的运行过程

JVM运行期动态加载+动态连接->Java的动态扩展特性

2.2 类加载的过程

类从被加载到虚拟机内存中开始、到卸载出内存为止,整个生命周期包括七个阶段:

加载验证准备解析初始化使用卸载其中,验证、准备、解析这3个部分统称为连接,流程如下图:

注意:

『加载』->『验证』->『准备』->『初始化』->『卸载』这五个阶段的顺序是确定的,而『解析』可能为了支持Java的动态绑定会在『初始化』后才开始上述阶段通常都是互相交叉地混合式进行的,比如会在一个阶段执行的过程中调用、激活另外一个阶段想要了解Java动态绑定和静态绑定区别的话,可以看下这篇文章:理解静态绑定与动态绑定

2.2.1 加载

Q1:任务

通过类的全限定名来获取定义此类的二进制字节流。如从ZIP包读取、从网络中获取、通过运行时计算生成、由其他文件生成、从数据库中读取等等途径......想要详细了解类的全限定名的知识,可以看下这篇文章:全限定名、简单名称和描述符是什么东西?

将该二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构,该数据存储数据结构由虚拟机实现自行定义在内存中生成一个代表这个类的java.lang.Class对象,它将作为程序访问方法区中的这些类型数据的外部接口2.2.2 验证

是连接阶段的第一步,且工作量在JVM类加载子系统中占了相当大的一部分目的:为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全由此可见,它能直接决定

JVM能否承受恶意代码的攻击,因此验证阶段很重要,但由于它对程序运行期没有影响,并不一定必要,可以考虑使用

-Xverify:none

参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。检验过程包括下面四个阶段:A.文件格式验证:B.元数据验证:C.字节码验证:D.符号引用验证:内容:对类自身以外(如常量池中的各种符号引用)的信息进行匹配性校验目的:确保解析动作能正常执行,如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError异常的子类注意:该验证发生在虚拟机将符号引用转化为直接引用的时候,即『解析』阶段是验证过程中最复杂的一个阶段内容:对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件目的:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的例子:内容:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求目的:对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息例子:内容:验证字节流是否符合Class文件格式的规范、以及是否能被当前版本的虚拟机处理目的:保证输入的字节流能正确地解析并存储于方法区之内,且格式上符合描述一个Java类型信息的要求。只有保证二进制字节流通过了该验证后,它才会进入内存的方法区中进行存储,所以后续3个验证阶段全部是基于方法区而不是字节流了例子:保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现“在操作数栈的数据类型中放置了int类型的数据,使用时却按long类型来载入本地变量表中”保证任何跳转指令都不会跳转到方法体外的字节码指令上......类是否有父类(除了java.lang.Object之外,所有类都应有父类)父类是否继承了不允许被继承的类(final修饰的类)如果该类不是抽象类,是否实现了其父类或接口中要求实现的所有方法......是否以魔数0xCAFEBABE开头主次版本号是否在JVM接受范围内索引值是否有指向不存在/不符合类型的常量......2.2.3 准备

Q1:任务

为类变量(静态变量)分配内存:因为这里的变量是由方法区分配内存的,所以仅包括类变量而不包括实例变量,后者将会在对象实例化时随着对象一起分配在Java堆中设置类变量初始值:通常情况下零值2.2.4 解析

之前提过,解析阶段就是虚拟机将常量池内的符号引用替换为直接引用的过程

符号引用:以一组符号来描述所引用的目标可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可与虚拟机实现的内存布局无关,因为符号引用的字面量形式明确定义在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方法定义的字段,是成员变量

2.2.5 初始化

是类加载过程的最后一步,会开始真正执行类中定义的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的方法句柄,且这个方法句柄所对应的类未进行初始化,需先触发其初始化。2.3 类加载器&双亲委派模型

每个类加载器,都拥有一个独立的命名空间,它不仅用于加载类,还和这个类本身一起作为在JVM中的唯一标识。所以比较两个类是否相等,只要看它们是否由同一个类加载器加载,即使它们来源于同一个Class文件且被同一个JVM加载,只要加载它们的类加载器不同,这两个类就必定不相等

2.3.1 类加载器

从JVM的角度,可将类加载器分为两种:

启动类加载器由C++语言实现,是虚拟机自身的一部分负责加载存放在<JAVA_HOME>lib目录中、或被-Xbootclasspath参数所指定路径中的、且可被虚拟机识别的类库无法被Java程序直接引用,如果自定义类加载器想要把加载请求委派给引导类加载器的话可直接用null代替其他类加载器:由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader,可被Java程序直接引用。常见几种:1.扩展类加载器

A.由sun.misc.Launcher$ExtClassLoader实现B.负责加载<JAVA_HOME>libext目录中的、或者被java.ext.dirs系统变量所指定的路径中的所有类库

2.应用程序类加载器

A.是默认的类加载器,是ClassLoader#getSystemClassLoader()的返回值,故又称为系统类加载器B.由sun.misc.Launcher$App-ClassLoader实现C.负责加载用户类路径上所指定的类库

3.自定义类加载器:如果以上类加载起不能满足需求,可自定义

需要注意的是:虽然数组类不通过类加载器创建而是由JVM直接创建的,但仍与类加载器有密切关系,因为数组类的元素类型最终还要靠类加载器去创建

2.3.2 双亲委派模型

定义:表示类加载器之间的层次关系前提:除了顶层启动类加载器外,其余类加载器都应当有自己的父类加载器,且它们之间关系一般不会以继承关系来实现,而是通过组合关系来复用父加载器的代码工作过程:若一个类加载器收到了类加载的请求,它先会把这个请求委派给父类加载器,并向上传递,最终请求都传送到顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载注意:不是一个强制性的约束模型,而是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();为例进行说明

如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力

想了解更多精彩内容,快来关注计算机java编程

阅读剩余内容
网友评论
显示评论内容(2) 收起评论内容
  1. 2021-07-31 15:18突然心动[四川省网友]IP:3407340057
    我觉得我还需要更深入地学习一下JVM的类加载机制,谢谢你提醒!
    顶1踩0
  2. 2020-02-14 10:29山野幽草[天津市网友]IP:1731774767
    哇,这个问题有点挑战性,JVM的类加载机制确实是JAVA学习的重点之一。
    顶36踩0
相关内容
延伸阅读
小编推荐

大家都在看

我的梦想三百字作文 雅思小作文多少字 以环保为主题的作文 写春天的作文200字 考试反思作文600字 彼岸并不遥远作文 我心中的英雄英语作文 认识自己作文600字 什么的冬天作文 写桃花的作文300字 拔河比赛作文600字 一幅漫画的启示作文 写事情的作文300字 打女生的屁股作文 人物传记作文800字 全国一卷高考语文作文 我梦想的工作英语作文 我心中最美的风景作文 我眼中的父亲作文 我爱家乡河作文 写景物的作文400字 放鞭炮作文600字 高考语文万能作文素材 关于自强不息的作文 就这样被感动 作文 回老家作文300字 钓鱼的作文500字 赶集作文300字 新年计划的英语作文 童年的回忆作文600字