《深入理解Java虚拟机》读书笔记4--类加载机制

所谓类加载机制,就是虚拟机将Class文件加载到内存,对数据进行校验、解析、初始化,然后转化为可被虚拟机使用的数据类型的过程

与静态连接的语言不通,Java采用动态连接方式,这种策略在运行时虽然会增加一些性能开销,但是却给程序提供了高度的灵活性。比如我们可以通过自定义类加载器的方式,在运行时通过网络进行类加载;也可以在运行时为一个接口指定其实现类

类加载时机

类的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载。其中验证、准备、解析这3个阶段称为连接阶段

加载、验证、准备、初始化和卸载,这5个阶段必须按照顺序开始,但并不要求按照这个顺序完成,因为这些阶段通常会在某一个阶段执行的过程中被调用或激活

类加载过程

1.加载

在加载阶段,虚拟机需要完成3件事:

(1)通过类的全限定名来获取该类的二进制字节流

(2)将二进制字节流转化为运行时数据结构

(3)在内存中生成java.lang.Class对象供后续使用

对于加载这个阶段,非数组类和数组类的情况有所不同:

#对于非数组类,加载阶段既可以使用系统提供的引导类加载器完成,也可以使用自定义的类加载器完成

#对于数组类,它本身并不通过类加载器加载,而是由虚拟机直接加载。但是数组类的元素类型依然需要靠类加载器加载

2.验证

验证是连接阶段的第一步,目的是确保Class字节流符合当前虚拟机的要求。虽然编译器本身可以编译出合法的字节码,但是字节码的来源并不一定是编译器(比如直接通过十六进制编辑器编辑),因此虚拟机并不会完全信任字节码,需要对其进行验证

验证过程大致可以分为4个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证

#文件格式验证

这一阶段要验证字节流是否符合类文件格式的规范(关于类文件格式可以参考上一篇系列文章:类文件结构),并且是否能够被当前版本的虚拟机所处理。验证点例如:

(1)魔数是否是0xCAFEBABE

(2)主次版本号能否被当前虚拟机兼容

(3)常量池中是否存在不被支持的常量类型(检查常量tag标志)

(4)指向常量池的索引值是否指向不存在的或者不符合类型的常量

...

实际上,这一阶段的验证还远不止上面列出这些。通过了这一阶段的验证后,字节流会被建立成内存中的数据结构,供后续3个验证阶段使用

#元数据验证

这个阶段主要是对字节码描述的信息进行语义分析及校验,保证其符合Java语言的规范。验证点例如:

(1)该类是否有父类(除java.lang.Object之外,所有类都应该有父类)

(2)该类是否继承了不允许被继承的类(被final修饰对类)

(3)如果该类不是抽象类,是否实现了其父类或者接口中要求被实现的所有方法

...

#字节码验证

该阶段将对类的方法体进行数据流和控制流分析,确保程序语义是合法、符合逻辑并且不会对虚拟机造成危害的。验证点例如:

(1)保证任意时刻操作数栈与指令序列都能配合工作(数据类型与操作指令匹配)

(2)保证跳转指令不会跳转到方法体以外的字节码指令上

(3)保证方法体中的类型转换是有效的(比如把一个List对象赋值给一个ArrayList类型的引用是不合法的)

...

#符号引用验证

这个阶段的校验是对类自身以外(常量池中各种符号引用)的信息进行匹配性校验。目的是确保“解析”阶段的正常运行。如果无法通过验证,将会抛出java.lang.IncompatibleChangeError异常的子类,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchFieldErrorMethod等。验证点例如:

(1)符号引用中通过字符串描述的全限定名是否能找到对应的类

(2)指定类中的字段和方法是否存在

(3)符号引用中的类、字段、方法的可访问性验证

...

3.准备

准备阶段是正式为类变量(不是实例变量)分配内存并设置初始值的阶段。这里说的初始值,通常情况下是对应数据类型的零值,比如定义一个类变量:

public static int abc = 123;

那么abc在准备阶段后的初始值是0,而给abc赋值的putstatic指令存在于类的构造器<cinit>()方法之中,所以给abc赋值为123的动作将在初始化阶段执行

但是如果abc的定义改为:

public static final int abc = 123;

那么abc的值在准备阶段就会被赋值为123。也就是说如果类变量的字段属性表存在ConstantValue属性,那么在准备阶段虚拟机将会根据ConstantValue的设置给该字段赋值

4.解析

解析阶段是虚拟机将符号引用替换成直接引用的过程

#符号引用

符号引用是以一组符号来描述引用的目标,它与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存当中。在Class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型常量出现

#直接引用

直接引用可以是直接指向目标的指针、相对偏移量或者是能间接定位到目标的句柄。它与虚拟机实现的内存布局息息相关。引用的目标一定已经加载到内存当中

下面分别对其中比较重要和基本的几种解析做下简单介绍:

#类和接口的解析

假设当前代码所处类为A,将要把一个符号引用B解析成一个类或者接口C的直接引用,那么虚拟机需要完成下面3个步骤:

(1)如果C不是数组类型,那么虚拟机会把代表B的全限定名传递给A的类加载器去加载C。在此过程中还有可能会触发其他相关类的加载动作,比如加载C的父类或者实现的接口。一旦加载过程中出现任何异常,解析过程就会失败

(2)如果C是数组类型,并且数组的元素类型为对象,那么会按照(1)中介绍的规则加载数组的元素类型,之后再生成一个代表此元素类型的数组对象

(3)如果上述步骤没有出现异常,那么C在虚拟机中实际上已经是一个有效的类或者接口了。但是依然需要对A是否具备对C的访问权限进行验证,如果验证失败,将会抛出java.lang.IllegalAccessError异常

#字段解析

要对一个字段的符号引用进行解析,首先需要对该字段所属的类或者接口的符号引用进行解析。如果在此过程中出现了任何异常,那么字段的解析都是失败的。如果该过程成功,那么将按照如下规则对该字段进行搜索(假设该字段所属的类或者接口是A):

(1)如果A本身就包含了简单名称和字段描述符都与目标相匹配的字段,则直接返回这个字段的直接引用,查找结束

(2)否则,如果A实现了接口,那么将会按照继承关系自下而上递归的搜索各接口,如果某接口包含了简单名称和字段描述符都与目标相匹配的字段,那么就返回这个字段的直接引用,查找结束

(3)否则,如果A不是java.lang.Object,则会按照继承关系自下而上递归的搜索其父类,如果某父类包含了简单名称和字段描述符都与目标相匹配的字段,那么就返回这个字段的直接引用,查找结束

(4)否则,查找失败,抛出java.lang.NoSuchFieldError异常

如果查找过程成功,将会对这个字段进行权限验证,如果不具备访问权限,将会抛出java.lang.IllegalAccessError异常

#类方法解析

类方法解析的第一个步骤与字段解析相同,都是要对所属的类或者接口的符号引用进行解析。如果该过程成功,那么将按照如下规则对该方法进行搜索(假设该字段所属的类或者接口是A):

(1)类方法(CONSTANT_Methodref_info)和接口方法(CONSTANT_InterfaceMethodref_info)的符号引用定义是分开的,如果在类方法表中发现class_index中索引的A是接口,那么将抛出java.lang.IncompatibleChangeError异常

(2)通过第一步后,如果A本身就包含了简单名称和描述符都与目标相匹配的方法,则直接返回这个方法的直接引用,查找结束

(3)否则,按照继承关系自下而上递归的搜索其父类,如果某父类包含了简单名称和描述符都与目标相匹配的方法,那么就返回这个方法的直接引用,查找结束

(4)否则,将会按照继承关系自下而上递归的搜索其实现的接口,如果某接口包含了简单名称和描述符都与目标相匹配的方法,说明A是一个抽象类,查找结束,抛出java.lang.AbstractMethodError异常

(5)否则,查找失败,抛出java.lang.NoSuchMethodError异常

如果查找过程成功,将会对这个方法进行权限验证,如果不具备访问权限,将会抛出java.lang.IllegalAccessError异常

#接口方法解析

接口方法也需要先对所属的类或者接口的符号引用进行解析。如果该过程成功,那么将按照如下规则对该接口方法进行搜索(假设该字段所属的接口是A):

(1)如果在接口方法表中发现class_index中索引的A是类而不是接口,那么将抛出java.lang.IncompatibleChangeError异常

(2)通过第一步后,如果A本身就包含了简单名称和描述符都与目标相匹配的方法,则直接返回这个方法的直接引用,查找结束

(3)否则,按照继承关系自下而上递归的搜索其父类,如果某父类包含了简单名称和描述符都与目标相匹配的方法,那么就返回这个方法的直接引用,查找结束

(4)否则,查找失败,抛出java.lang.NoSuchMethodError异常

由于接口中方法默认都是public的,因此不存在访问权限问题,也就不会抛出java.lang.IllegalAccessError异常

5.初始化

对于“加载”,Java虚拟机规范中并没有进行强制约束。但是对于“初始化”,则严格规定了有且仅有以下五种情况必须对类进行初始化

(1)在遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类还没有进行初始化,则必须先进行初始化。这四条字节码指令对应的Java代码场景分别是:使用new关键字实例化对象时、读取或设置静态变量时(非final变量)、调用类的静态方法时

(2)通过反射对类进行调用的时候,如果类还没有进行初始化,则必须先进行初始化

(3)当初始化一个类的时候,如果发现其父类还没有初始化,则必须先对其父类进行初始化

(4)程序运行的主类(包含main方法的类),虚拟机需要先对其进行初始化

(5)如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则必须先进行初始化

初始化是类加载的最后一个阶段。在前面的加载过程中,只有在加载阶段,用户可通过自定义的类加载器进行参与,后续几个阶段都是虚拟机自动控制完成的。直到初始化阶段,才真正开始执行类中编写的程序代码

在准备阶段,类变量已经被赋过一次零值。初始化阶段,类变量才会根据代码逻辑重新赋值。这个动作就是在类构造器<clinit>()中执行的。我们来看一下类构造器执行过程中可能会影响程序行为的特点和细节:

(1)类构造器是由编译器自动收集类变量赋值动作和静态代码块(static{}代码块)生成的。收集顺序是按照源代码中声明的顺序决定的。静态代码块只能访问到定义在它之前的类变量,定义在它之后的类变量,只能赋值,不能访问

(2)与实例构造器<init>()不同,类构造器不需要显式调用父类的类构造器。虚拟机会保证父类的类构造器先于子类的类构造器执行。因此,父类的静态代码块要优先于子类的静态代码块执行

(3)类构造器并不是必须的,当类中没有静态代码块,也没有对类变量赋值的操作,那么编译器不会生成类构造器

(4)接口中没有静态代码块,但是可能会有类变量的赋值操作,因此也有可能会有类构造器。但是与类的类构造器不同的是,执行接口的类构造器不需要先执行父接口的类构造器。只有当父接口中定义的类变量被使用时,父接口的类构造器才会执行。同样,接口的实现类在初始化时也不会执行接口的类构造器

(5)虚拟机会保证一个类的类构造器在多线程环境下只执行一次。因此当类构造器内代码耗时很长的时候,会造成其他线程阻塞

类加载器

类加载器,它的作用就是通过一个类的全限定名来获取对应类的二进制字节流。这个动作没有完全固化在虚拟机中,而是暴露给外部程序,目的就是为了提供类加载过程的灵活性

1.类与类加载器

如果两个类拥有完全相同的全限定类名,那么是否可以判定这两个类“相等”?答案是只有在这两个类是由同一个类加载器加载的前提下,才可以判定这两个类“相等”

这里的“相等”,包含Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceof关键字

2.双亲委派模型

从虚拟机的角度来看,只存在两种类加载器:一种是启动类加载器(Bootstrap ClassLoader),它内置在虚拟机中,是虚拟机的一部分;另外一种是其他所有类加载器,这类加载器由Java语言实现,不属于虚拟机,全部继承自java.lang.ClassLoader

从开发人员角度,类加载器可以再细分为:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)

#启动类加载器,负责将JAVA_HOME/jre/lib/目录下(我本机使用的是java 8)虚拟机识别的类库(按照文件名识别,比如rt.jar)加载到虚拟机内存中

#扩展类加载器,负责将JAVA_HOME/jre/lib/ext/目录下的类库加载到虚拟机内存中

#应用程序类加载器,负责加载CLASS_PATH下指定的类库,如果应用程序中没有自定义类加载器,一般情况下默认会使用这个类加载器。可通过ClassLoader的getSystemClassLoader()方法获取

一般情况下,程序使用系统提供的这三种类加载器,如果有必要,还可以自定义类加载器。类加载器通过称为“双亲委派模型(Parents Delegation Model)”的方式配合工作(并非强制约束,而是推荐方式),它们之间使用组合(而非继承)的方式来委派类加载行为

#工作过程

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己直接尝试加载这个类,而是将请求委派给它的父类加载器,每个层次的类加载器都是如此,因此所有的类加载请求最终都会落到顶层的启动类加载器。如果上层的类加载器反馈自己无法处理加载该类的请求(上面讲过每个类加载器都有自己负责的类加载路径),那么下层的类加载器才会去尝试自己加载

#为什么这么做

其中一个显而易见的好处就是,通过这种方式能够形成一种带有优先级的层次结构。例如java.lang.Object类,它在JAVA_HOME/jre/lib/rt.jar中,无论哪个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,因此能够保证在同一个虚拟机环境下Object类在程序的各种类加载器环境中都是同一个类。想象一个,如果不使用双亲委派模型,会出现怎样的混乱场景

#实现

双亲委派模型对于保证Java程序的稳定运作至关重要,但是它的实现却非常简单(详见java.lang.ClassLoader的loadClass()方法),核心逻辑是:首先检查该类是否已经被加载过,如果没有加载过则调用父类的loadClass()方法,如果父类是null则直接调用启动类加载器进行加载,如果仍然加载失败则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载

思维导图:

笔记4结束

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容