JVM最详细知识点笔记-字节码与类的加载篇

3 篇文章 0 订阅

字节码与类的加载

注:笔者认为第二部分不需要死记硬背,只需要看一眼大概有哪些,在需要的时候到官网查询用法即可,第四部分会作为面试常出现的部分需要重点掌握

一、Class 文件结构

1.1 概述

Java虚拟机是一个跨语言的平台

Java虚拟机不和包括Java 在内的任何语言绑定,它只与“Class 文件”这种特定的二进制文件格式所关联。无论使用何种语言进行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行。可以说,统一而强大的Class文件结构,就是Java虚拟机的基石、桥梁。

前端编译器

想要让一个Java程序正确地运行在JVM中,Java源码就必须要被编译为符合JVM规范的字节码。

前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的字节码文件。

javac是一种能够将]ava源码编译为字节码的前端编译器。

Javac编译器在将Java源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法解析、语法解析、语义解析以及生成字节码。

image-20211116161237750

可以看到 java 代码看不到的细节

1.2 虚拟机的基石:Class文件

字节码文件里是什么

源代码经过编译器编译之后便会生成一个字节码文件,字节码是一种二进制的类文件,它的内容是 JVM 的指令,而不像C、C++经由编译器直接生成机器码

什么是字节码指令

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数( operand)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。
字节码指令 = 操作码 + (操作数)

解读 class 文件的方式

  • jclasslib
  • javap -v

1.3 Class文件结构

1.3.1 class类的本质

任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在。Class 文件是一组以8位字节为基础单位的二进制流

1.3.2 格式要求概述

Class 的结构不像XMNL等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。

Class 文件格式采用一种类似于C语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表。

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8编码构成字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明

1.3.3 内部结构

  • 魔数
  • Class文件版本
  • 常量池
  • 访问标志
  • 类索引,父类索引,接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合
image-20211116192907963
1.3.3.1 魔数

class 文件的前 4 个字节为 CA FE BA BE 用于标识这是一个class文件

1.3.3.2 Class文件版本号

接下来 4 个字节代表的是 class 文件的副版本和主版本

image-20211116194427854

Java 的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1。

不同版本的Java编译器编译的Class文件对应的版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器生成的Class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件。否则JVM会抛出java.lang.UnsupportedClassVersionError异常。(向下兼容)

在实际应用中,由于开发环境和生产环境的不同,可能会导致该问题的发生。因此,需要我们在开发时JDK版本和生产环境中的JDK版本是否一致。

虚拟机JDK版本为1.k (k >= 2)时,对应的class文件格式版本号的范围为45.0- 44+k.0(含两端)。

1.3.3.3 常量池

概述

常量池是Class文件中内容最丰富的区域之一。常量池对于class文件中的字段和方法解析也有着至关重要的作用。

在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。

常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。

常量池表项中,用于存放编译时期生成的各种字面量符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放

常量池计数器

计数从 1 开始,如果值为 1 ,常量池中的数量为 0

所以常量池中的数量为常量池计数器的值减去 1

常量池表概述

常量池主要存放两大类常量:字面量(Literal)符号引用(Symbolic References)

它包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。第1个字节作为类型标记,用于确定该项的格式,这个字节称为 tag byte(标记字节、标签字节)。

image-20211116202248620

全限定名:com/king/test/Test;

简单名称: 比如一个方法是 countNum() ,这里存的就是 countNum,或者一个字段名为 num 这里存的就是 num

描述符: 描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、 boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,详见下表:

image-20211116202852024

补充说明:

虚拟机在加载Class文件时才会进行动态链接,也就是说,class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。

这里说明下符号引用和直接引用的区别与关联:

  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定己经存在于内存之中了。

常量池表部分

每个常量的第一个字节标识了这个常量的类型,根据类型剩余的长度也不同,如果要手动解析需要对应类型表格,此处就不展开了。

常见的字符串类型:第一个字节是 01 ,第二个第三个字节是字符串长度,剩余字节是具体的字符串

1.3.3.4 访问标识

在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个class是类还是接口;是否定义为 public类型,是否定义为 abstract类型;如果是类的话,是否被声明为final等。各种访问标记如下所示:

image-20211118170312701

访问标识占两个字节,值是各种标识值的累加。

1.3.3.5 类索引,父类索引,接口索引集合

在访问标记后,会指定该类的类别、父类类别以及实现的接口,格式如下:

image-20211118171248944
1.3.3.6 字段表集合

用于描述接口或类中声明的变量。字段(field)包括类级变量以及实例级变量(静态、非静态变量),但是不包括方法内部、代码块内部声明的局部变量。(local variables)

字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

它指向常量池索引集合,它描述了每个字段的完整信息。比如**字段的标识符、访问修饰符(public、private或protected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)**等。

fields_counter(字段计数器 2 字节) + fields(具体字段)

字段

image-20211118172611732
1.3.3.7 方法表集合

method_counter(方法计数器 2 字节) + methods(方法)

方法

image-20211118183418125

其中属性集合的前两个字节代表属性名

code属性如下图显示(其余属性见官网):

image-20211118185010109

1.3.3.8 属性表集合

方法表集合之后的属性表集合,指的是class文件所携带的辅助信息,比如该 class文件的源文件的名称。以及任何带有RetentionPolicy.CLASS 或者RetentionPolicy.RUNTIMNE的注解。这类信息通常被用于Java虚拟机的验证和运行,以及ava程序的调试,一般无须深入了解。

1.4 使用 javap 解析 Class 文件

tip

使用 javac 编译源码时

使用 -g 会带上局部变量表信息,不使用就不会带

如何使用 javap

image-20211118194104751

一般解析class文件: javap -v

可以获得详细的信息

如果需要查看私有信息还需要加上 -p

总结

  1. 通过javap命令可以查看一个java类反汇编得到的Class文件版本号、常量池、访问标识、变量表、等信息。不显示类索引、父类索引、接口索引集合、()、()等结构

  2. 通过对前面例子代码反汇编文件的简单分析,可以发现,一个方法的执行通常会涉及下面几块内存的操作:

    1. java栈中:局部变量表、操作数栈。
    2. java堆。通过对象的地址引用去操作。
    3. 常量池。
    4. 其他如帧数据区、方法区的剩余部分等情况,测试中没有显示出来,这里说明一下。

二、字节码指令集与解析

2.1 概述

Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,0pcode〉以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码。

字节码与数据类型

  1. 对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:

    • i代表对int类型的数据操作

    • l代表long

    • s代表short

    • b代表byte

    • c代表char

    • f代表float

    • d代表double

  2. 也有一些指令的助记符中没有明确地指明操作类型的字母,如arraylength指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。

  3. 还有另外一些指令,如无条件跳转指令goto则是与数据类型无关的

指令分类

字节码指令集按用途大概分为9类:

  • 加载与存储指令
  • 算术指令
  • 类型转换指令
  • 对象的创建与访问指令
  • 方法调用与返回指令
  • 操作数栈管理指令
  • 比较控制指令
  • 异常处理指令
  • 同步控制指令

2.2 加载与存储指令

作用

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递。

常用指令

image-20211119115605161

上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如iload_ )。这些指令助记符实际上代表了一组指令(例如iload_代表了iload_0、iload_1、iload_2和iload_3这几个指令)。这几组指令都是某个带有一个操作数的通用指令(例如 iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中

2.3.1 局部变量压栈指令

指令xload_n表示将第n个局部变量压入操作数栈,比如iload_1、fload_0、aload_0等指令。其中aload_n表示将一个对象引用压栈(0<=n<=3)。

指令xload通过指定参数的形式,把局部变量压入操作数栈,当使用这个命令时,表示局部变量的数量可能超过了4个,比如指令iload、fload等。

2.3.2 常量入栈指令

常量入栈指令的功能是将常数压入操作数栈,根据数据类型和入栈内容的不同,又可以分为const系列、push系列和ldc指令。

const 系列

用于对特定的常量入栈,入栈的常量隐含在指令本身里。指令有: iconst_(i从-1到5)、lconst_(l从0到1)、fconst_(f从0到2)、dconst_ (d从0到1)、aconst_null。

从指令的命名上不难找出规律,指令助记符的第一个字符总是喜欢表示数据类型,i表示整数,l表示长整数,f表示浮点数,d表示双精度浮点,习惯上用a表示对象引用。如果指令隐含操作的参数,会以下划线形式给出。

push系列

主要包括bipushsipush。它们的区别在于接收数据类型的不同,bipush接收8位整数作为参数,sipush接收16位整数,它们都将参数压入栈。

bipush => [-128,127]

sipush => [-32768,32767]

ldc系列

如果以上指令都不能满足需求,那么可以使用万能的ldc指令,它可以接收一个8位的参数,该参数指向常量池中的int、 float或者String的索引,将指定的内容压入堆栈。类似的还有ldc_w,它接收两个8位参数,能支持的索引范围大于ldc。如果要压入的元素是1ong或者double类型的,则使用ldc2_w指令,使用方式都是类似的。

总结

image-20211119144809993

2.3.3 出栈装入局部变量表指令

出栈装入局部变量表指令用于将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值。

一般说来,类似像store这样的命令需要带一个参数,用来指明将弹出的元素放在局部变量表的第几个位置。但是,为了尽可能压缩指令大小,使用专门的istore_1指令表示将弹出的元素放置在局部变量表第1个位置。类似的还有istore_0、istore_2、istore_3,它们分别表示从操作数栈顶弹出一个元素,存放在局部变量表第0、2、3个位置。

2.3 算数指令

作用

算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新压入操作数栈

分类

大体上算术指令可以分为两种:对整型数据进行运算的指令与对浮点类型数据进行运算的指令。

byte、short、char、boolean 说明

image-20211119154456261

在处理boolean、 byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。

Infinity、NAN

double d1 = 0.0;
double d2 = d1 / 0.0;
System.out.println(d2); //NAN

double d1 = 10;
double d2 = d1 / 0.0;
System.out.println(d2); //Infinity

运算符

加法指令:iadd、 ladd、 fadd、dadd

减法指令:isub、lsub、fsub、dsub

乘法指令:imul、lmul、 fmul、dmul

除法指令: idiv、ldiv、fdiv、ddiv

求余指令: irem、lrem、frem、drem // remainder:余数

取反指令: ineg、lneg、fneg、dneg //negation:取反

自增指令: iinc

位运算指令,又可分为:

  • 位移指令: ishl、ishr、 iushr、lsh1、lshr、lushr
  • 按位或指令: ior、lor
  • 按位与指令: iand、land·按位异或指令: ixor、lxor

比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp

关于 ++

  1. 如果 i++/++i 是一个单独的语句,在字节码上是没有任何差别的

  2.  int i = 10;
     int a = i++;	// a == 10
     // 从局部变量表拿出i的值压入操作数栈
     // i自增
     // 将操作数栈的值赋给a
     
     int j = 20;
     int b = ++j;	// b == 21
     // j自增
     // 从局部变量表拿出i的值压入操作数栈
     // 将操作数栈的值赋给b
    

2.4 类型转换指令

1、类型转换指令说明

①类型转换指令可以将两种不同的数值类型进行相互转换。

②这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。

2.4.1 宽化类型转换

转换规则

Java虚拟机直接支持以下数值的宽化类型转换(widening numeric conversion,小范围类型向大范围类型的安全转换)。也就是说,并不需要指令执行,包括:

  • 从int类型到long、float或者double类型。对应的指令为: i21、i2f、 i2d
  • 从long类型到float、double类型。对应的指令为:12f、12d
  • 从float类型到double类型。对应的指令为:f2d

简化为: int --> long --> float --> double

精度损失问题

从int、long类型数值转换到float,或者long类型数值转换到double时,将可能发生精度丢失,比如丢失各位数

补充

byte、char、short实际上都是当成int类型来处理

2.4.2 窄化类型转换

可以理解为强制类型转换

转换规则

Java虚拟机也直接支持以下窄化类型转换:

  • 从int类型至byte、short或者char类型。对应的指令有: i2b、i2s、i2c
  • 从long类型到int类型。对应的指令有:12i
  • 从float类型到int或者long类型。对应的指令有:f2i、f21
  • .从double类型到int、long或者float类型。对应的指令有: d2i、d21、d2f

类似于long、short、double转换为byte、short、char会经历两部:先转换为int,再把int转换为对应的更小范围的类型

经度损失问题

窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,因此,转换过程很可能会导致数值丢失精度。

尽管数据类型窄化转换可能会发生上限溢出、下限溢出和精度丢失等情况,但是Java虚拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常

补充

  • 将float、double的NAN转换为long、int结果为0
  • 如果float、double转换为int、long则先向0舍,然后看能不能装下,装不下则结果为int、long能表示的最大值或最小值
  • double转float值太靠近0,float接不下则结果为0
  • double转float值太大,结果为float的无穷大
  • double的NAN转换为float的NAN

2.5 对象的创建与访问指令

Java是面向对象的程序设计语言,虚拟机平台从字节码层面就对面向对象做了深层次的支持。有一系列指令专门用于对象操作,可进一步细分为创建指令、字段访问指令、数组操作指令、类型检查指令。

2.5.1 创建指令

  1. 创建类实例的指令
    • 创建类实例的指令:new
      • 它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈。
  2. 创建数组的指令
    • 创建数组的指令: newarray、anewarray、multianewarray。
      • newarray:创建基本类型数组
      • anewarray:创建引用类型数组
      • multianewarray:创建多维数组

2.5.2 字段访问指令

  • 访问类字段(static字段,或者称为类变量)的指令: getstatic、 putstatic
  • 访问类实例字段(非static字段,或者称为实例变量)的指令: getfield、putfield

2.5.3 数组操作指令

image-20211119213145182

2.6 方法调用与返回指令

方法调用指令

方法调用指令: invokevirtual、invokeinterface、invokespecial、invokestatic . invokedynamic

  • invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),支持多态。这也是Java语言中最常见的方法分派方式。
  • invokeinterface指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用
  • invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法。这些方法都是静态类型绑定的,不会在调用时进行动态派发。
  • invokestatic指令用于调用命名类中的类方法**(static方法)**。这是静态绑定的。
  • invokedynamic:调用动态绑定的方法,这个是JDK 1.7后新加入的指令。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面4条调用指令的分派逻辑都固化在java 虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

方法返回指令

方法调用结束前,需要进行返回。方法返回指令是根据返回值的类型区分的。

image-20211119215135468

2.7 操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样,JVN提供的操作数栈管理指令,可以用于直接操作操作数栈的指令。

  • 将一个或两个元素从栈顶弹出,并且直接废弃:pop,pop2;
  • 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup,dup2,dup_x1,dup2_x1,dup_x2,dup2_×2;
  • 将栈最顶端的两个Slot数值位置交换:swap。Java虚拟机没有提供交换两个64位数据类型(long、double)数值的指令。
  • 指令nop,是一个非常特殊的指令,它的字节码为ex00。和汇编语言中的nop一样,它表示什么都不做。这条指令一般可用于调试、占位等。

2.8 控制转义指令

  • 比较指令的作用是比较栈顶两个元素的大小,并将比较结果入栈。
  • 比较指令有: dcmpg, dcmpl、 fcmpg、fcmpl、 lcmp。
    • 与前面讲解的指令类似,首字符d表示double类型,f表示float,l表示long。
  • 对于double和float类型的数字,由于NaN的存在,各有两个版本的比较指令。以float为例,有fcmpg和fcmpl两个指令,它们的区别在于在数字比较时,若遇到NaN值,处理结果不同。
  • 指令dcmpl和dcmpg也是类似的,根据其命名可以推测其含义,在此不再赘述。
  • 指令lcmp针对long型整数,由于long型整数没有NaN值,故无需准备两套指令。

2.8.1 条件跳转指令

条件跳转指令通常和比较指令结合使用。在条件跳转指令执行前,一般可以先用比较指令进行栈顶元素的准备,然后进行条件跳转。

它们的统一含义为:弹出栈顶元素,测试它是否满足某一条件,如果满足条件.则跳转到给定位置。

image-20211125170100776

2.8.2 比较条件跳转指令

比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一。

这类指令有: if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne 。其中指令助记符加上“if_”后,以字符“i”开头的指令针对int型整数操作(也包括short和byte类型),以字符“a”开头的指令表示对象引用的比较。

image-20211203201718332

2.8.3 多条件分支跳转

多条件分支跳转指令是专为switch-case语句设计的,主要有tableswitch和lookupswitch。

image-20211203201806396

tableswitch效率要高于lookupswitch

在 jdk1.7 switch引入了判断String,其底层是先比较String的Hash值,编译器会将 String 的 Hash 值从低到高进行排序,如果 Hash 比对成功不代表相等,还要判断具体的字符串是否相等。

2.8.4 无条件跳转

目前主要的无条件跳转指令为goto。指令goto接收两个字节的操作数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到偏移量给定的位置处。

如果指令偏移量太大,超过双字节的带符号整数的范围,则可以使用指令goto_w,它和goto有相同的作用,但是它接收4个字节的操作数,可以表示更大的地址范围。

image-20211203202259093

2.9 异常处理指令

抛出异常

( 1 )athrow指令

在Java程序中显示抛出异常的操作(throw语句)都是由athrow指令来实现。除了使用throw语句显示抛出异常情况之外,**JVM规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。**例如,在之前介绍的整数运算时,当除数为零时,虚拟机会在 idiv 或 ldiv 指令中抛出ArithmeticException异常。

(2)注意

正常情况下,操作数栈的压入弹出都是一条条指令完成的。唯一的例外情况是在抛异常时,Java虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上。

处理异常

在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的〈早期使用jsr、ret指令),而是采用异常表来完成的。

当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。

**不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行。**在这种情况下,如果方法结束后没有抛出异常,仍然执行finally块,在return前,它直接跳到finally块来完成目标

2.10 同步控制指令

不论是在方法的访问标识上声明 synchronized,还是方法内部通过同步代码块进行同步,都是通过 monitor 对象进行支持的。

2.10.1 方法级的同步

方法级的同步会在编译时在方法的属性中加上一个 ACC_SYNCHRONIZED 标识,方法级的同步是隐式的,底层调用的还是 monitor ,虚拟机在检查到这个标识后会先获取锁,在方法完成后会释放同步锁

如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放。

2.10.2 方法内指定指令序列的同步

同步一段指令集序列:通常是由java中的 synchronized 语句块来表示的。jvm的指令集有 monitorentermonitorexit 两条指令来支持 synchronized 关键字的语义。

当一个线程进入同步代码块时,它使用monitorenter指令请求进入。如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进行等待,直到对象的监视器计数器为0,才会被允许进入同步块。

当线程退出同步块时,需要使用monitorexit声明退出。在Java虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态。

指令monitorenter和monitorexit在执行时,都需要在操作数栈顶压入对象,之后monitorenter和monitorexit的锁定和释放都是针对这个对象的监视器进行的。

下图展示了监视器如何保护临界区代码不同时被多个线程访问,只有当线程4离开临界区后,线程1、2、3才有可能进入。

image-20211203205212719

三、类的生命周期

3.1 概述

在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。

image-20211204162412162

3.2 Loading阶段

加载:

所谓加载,简而言之就是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型――类模板对象。所谓类模板对象,其实就是Java类在JVM内存中的一个快照,JVIN将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样在运行期便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。

反射的机制即基于这一基础。如果JVM没有将Java类的声明信息存储起来,则JVM在运行期也无法反射

加载完成的操作:

加载阶段,简言之,查找并加载类的二进制数据,生成Class的实例。

在加载类时,Java虚拟机必须完成以下3件事情:

  • 通过类的全名、获取类的二进制数据流。
  • 解析类的二进制数据流为方法区内的数据结构(Java类模型)
  • 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口

获取二进制数据流

对于类的二进制数据流,虚拟机可以通过多种途径产生或获得。(只要所读取的字节码符合JVP规范即可)

  • 虚拟机可能通过文件系统读入一个class后缀的文件(最常见)
  • 读入jar、zip等归档数据包,提取类文件。
  • 事先存放在数据库中的类的二进制数据
  • 使用类似于HTTP之类的协议通过网络进行加载在运行时生成一段class的二进制信息等

类模型与Class实例的位置

加载的类在JVM中创建相应的类结构,类结构会存储在方法区(JDK1.8之前:永久代;JDK1.8及之后:元空间)。

类将.class文件加载至元空间后,会在堆中创建一个Java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个class类型的对象。

3.3 Linking阶段

3.3.1 Verification 验证阶段

它的目的是保证加载的字节码是合法、合理并符合规范的。

image-20211204164948686

  1. 格式验证:
    • 是否以魔数OxCAFEBABE开头,主版本和副版本号是否在当前Java虚拟机的支持范围内.数据中每一个项是否都拥有正确的长度等。
  2. 语义验证:
    • 是否所有的类都有父类的存在(在Java里,除了0bject外,其他类都应该有父类)是否一些被定义为final的方法或者类被重写或继承了
    • 非抽象类是否实现了所有抽象方法或者接口方法
    • 是否存在不兼容的方法(比如方法的签名除了返回值不同,其他都一样,这种方法会让虚拟机无从下手调度;abstract情况下的方法,就不能是final的了)
  3. 字节码验证:
    • 在字节码的执行过程中,是否会跳转到一条不存在的指令函数的调用是否传递了正确类型的参数
    • 变量的赋值是不是给了正确的数据类型等
  4. 符号引用验证:
    • Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法。因此,在验证阶段,虚拟机就会检查这些类或者方法确实是存在的,并且当前类有权限访问这些数据,如果一个需要使用类无法在系统中找到,则会抛出NoClassDefFoundError,如果一个方法无法被找到,则会抛出NoSuchethodError。
    • 此阶段在解析环节才会执行。

3.3.2 Preparation 准备阶段

准备阶段(Preparation),简言之,为类的静态变量分配内存,并将其初始化为默认值。

当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置默认初始值。

注意:

  • 这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显式赋值。
  • 注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
  • 在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。

3.3.3 Resoulution 解析阶段

解析阶段(Resolution),简言之,将类、接口、字段和方法的符号引用转为直接引用。

所谓解析就是将符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或者偏移量。因此,可以说,如果直接引用存在,那么可以肯定系统中存在该类、方法或者字段。但只存在符号引用,不能确定系统中一定存在该结构。

3.4 Initialization阶段

初始化阶段,简言之,为类的静态变量赋予正确的初始值。

到了初始化阶段才会执行在 java 程序中编写的代码。

初始化阶段的重要工作是执行类的初始化方法:()方法。

  • 该方法仅能由Java编译器生成并由JVM调用,程序开发者无法自定义一个同名的方法,更无法直接在Java程序中调用该方法,虽然该方法也是由字节码指令所组成。
  • 它是由类静态成员的赋值语句以及static语句块合并产生的。(显式赋值 + 静态代码块)

clinit 安全性问题

对于()方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性。

虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。

正是因为函数()带锁线程安全的,因此,如果在一个类的cclinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息。

如果之前的线程成功加载了类,则等在队列中的线程就没有机会再执行()方法了。那么,当需要使用这个类时,虚拟机会直接返回给它已经准备好的信息。

类的主动使用与被动使用

主动使用:

Class只有在必须要首次使用的时候才会被装载,Java虚拟机不会无条件地装载Class类型。Java虚拟机规定,一个类或接口在初次使用前,必须要进行初始化。这里指的“使用”,是指主动使用,主动使用只有下列几种情况:(即:如果出现如下的情况,则会对类进行初始化操作。而初始化操作之前的加载、验证、准备已经完成。)

  1. 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。

  2. 当调用类的静态方法时,即当使用了字节码invokestatic指令。

  3. 当使用类、接口的静态字段时(final修饰特殊考虑),比如,使用getstatic或者putstatic指令。(对应访问变量、赋值变量操作)

  4. 当使用java.lang.reflect包中的方法反射类的方法时。比如: Class.forName( " com.atguigu.java.Test")

  5. 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

  6. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。

  7. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

  8. 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类)

被动使用:

除了以上的情况属于主动使用,其他的情况均属于被动使用。**被动使用不会引起类的初始化。**也就是说:并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会初始化。

  1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。
    • 当通过子类引用父类的静态变量,不会导致子类初始化
  2. 通过数组定义类引用,不会触发此类的初始化
  3. 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。

3.5 类的使用

任何一个类型在使用之前都必须经历过完整的加载、链接和初始化3个类加载步骤。一旦一个类型成功经历过这3个步骤之后,便“万事俱备,只欠东风”,就等着开发者使用了。

开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用new关键字为其创建对象实例。

3.6 类的卸载

一、类、类的加载器、类的实例之间的引用关系

在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。另一方面,一个Class对象总是会引用它的类加载器,调用class对象的getClassLoader()方法,就能获得它的类加载器。由此可见,代表某个类的Class实例与其类的加载器之间为双向关联关系。
一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象

二、类的生命周期
当Sample类被加载、链接和初始化后,它的生命周期就开始了。当代表Sample类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。

一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。

三、图例

image-20211205155517567

一个类想要卸载需要满足:

  • 该类所有的实例都已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器已经被回收。这个条件除非是经过精心设计的可替换类加载器的场景,如osGi、JSP的重加载等,否则通常是很难达成的。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

四、再谈类的加载器

4.1 概述

ClassLoader的作用:

ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java. lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于它是否可以运行,则由Execution Engine决定。

ClassLoader加载分类:

class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式。

  • 显式加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader( ).loadClass()加载Class对象。
  • 隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的Class文件中引用了另外一个类的对象,此时额外引用的类将通过JVN自动加载到内存中。

为什么要有ClassLoader:

  • 避免在开发中遇到 java.lang.ClassNotFoundException异常或java.lang.NoClassDefFoundError异常时,手足无措。只有了解类加载器的加载机制才能够在出现异常的时候快速地根据错误异常日志定位问题和解决问题
  • 需要支持类的动态加载或需要对编译后的字节码文件进行加解密操作时,就需要与类加载器打交道了
  • 开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑

命名空间

类的唯一性:

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。每一个类加载器,都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

命名空间:

  • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成
  • 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类

4.2 类的加载器分类

image-20211205181347600

  • 除了顶层的启动类加载器外,其余的类加载器都应当有自己的“父类”加载器。
  • 不同类加载器看似是继承(Inheritance)关系,实际上是包含关系在下层加载器中,包含着上层加载器的引用

4.2.1 引导类加载器

启动类加载器(引导类加载器,Bootstrap classLoader)

  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
  • 它用来加载Java的核心库(JAVA_HONE/jre/lib/rt.jar或sun.boot.class.path路径下的内容)。用于提供JVM自身需要的类。
  • 并不继承自java.lang.classLoader,没有父加载器。
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类加载扩展类和应用程序类加载器,并指定为他们的父类加载器。

4.2.2 扩展类加载器

扩展类加载器(Extension ClassLoader)

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 继承于classLoader类父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。
  • 如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

4.2.3 系统类加载器

应用程序类加载器(系统类加载器,AppClassLoader)

  • java语言编写,由sun.misc.Launcher$AppClassLoader实现
  • 继承于classLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库
  • 应用程序中的类加载器默认是系统类加载器。
  • 它是用户自定义类加载器的默认父加载器
  • 通过classLoader的getSystemClassLoader()方法可以获取到该类加载器

4.2.4 用户自定义类加载器

  • 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。
  • 体现Java语言强大生命力和巨大魅力的关键因素之一便是,Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源。
  • 通过类加载器可以实现非常绝妙的插件机制,这方面的实际应用案例举不胜举。例如,著名的OSGI组件框架,再如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。
  • 同时,自定义加载器能够实现应用隔离,例如Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。
  • 自定义类加载器通常需要继承于classLoader。

4.3 测试不同类的加载器

每个Class对象都会包含一个定义它的ClassLoader的一个引用。

获取ClassLoader的途径

image-20211205210138526

说明:

站在程序的角度看,引导类加载器与另外两种类加载器(系统类加载器和扩展类加载器)并不是同一个层次意义上的加载器,引导类加载器是使用C++语言编写而成的,而另外两种类加载器则是使用Java语言编写而成的。由于引导类加载器压根儿就不是一个Java类,因此在Java程序中只能打印出空值。

数组类的Class对象,不是由类加载器去创建的,而是在Java运行期JVM根据需要自动创建的。对于数组类的类加载器来说,是通过Class.getClassLoader()返回的,与数组当中元素类型的类加载器是一样的;如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的。

4.4 ClassLoader源码解析

ClassLoader与现有的类加载器的关系

image-20211205210918377

除了以上虚拟机自带的加载器外,用户还可以定制自己的类加载器。Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承classLoader类。

4.4.1 ClassLoader 的主要方法

getParent

获取父加载器

loadClass

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 尝试从父类加载器获取加载
                if (parent != null) {
                    // 递归调用父类的 loadClass 方法
                    c = parent.loadClass(name, false);
                } else {
                    // 数组没有类加载器,里面会判断是不是数组然后尝试通过,bootstrapClassLoader 加载类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // 依然没找到则调用 findClass 方法,ClassLoader抽象类中是抛出一个异常
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

findClass

  • 在URLClassLoader中进行了重写

本质上是通过 getResource 从文件中寻找 .class 文件的二进制流,然后通过defindClass 创建一个 Class 文件

defindClass

通常是自定义ClassLoader重写了 findClass 方法并在内部调用 defindClass 来加载一个 class

resolveClass

在loadClass里有,进行一个解析操作,使符号引用转换为直接引用

4.4.2 SecurityClassLoader 与 URLClassLoader

接着SecureClassLoader扩展了ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般我们不会直接跟这个类打交道,更多是与它的子类

前面说过,ClassLoader是一个抽象类,很多方法是空的没有实现,比如 findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现。并新增了URLClassPath类协助取得Class字节码流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

4.4.3 ExtClassLoader 与 AppClassLoader

AppClassLoader,这两个类都继承自URLClassLoader,是sun.misc.Launcher的静态内部类。sun.misc.Launcher主要被系统用于启动主应用程序,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher创建的

其中 AppClassLoader 对loadClass 进行了重写,单内部还是调用了 super.loadClass 也就是说,依旧满足双亲委派机制。

4.4.4 Class.forName 与 ClassLoader.loadClass

  • Class.fonName():是一个静态方法,最常用的是Class.fonName(String className );根据传入的类的全限定名返回一个C1ass对象。该方法在将Class文件加载到内存的同时,会执行类的初始化。如:class.forName( “com.atguigu.java.Helloworld” );

  • ClassLoader.loadClass():这是一个实例方法,需要一个ClassLoader对象来调用该方法。该方法将Class 文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。该方法因为需要得到一个ClassLoader对象,所以可以根据需要指定使用哪个类加载器.如:

    ClassLoader cl = …;

    c1.loadClass( “com.atguigu.java.Helloworld” );

forName会进行 Initailization , loadClass 只会加载不会初始化。

4.5 双亲委派模型

4.5.1 定义与本质

类加载器用来把类加载到Java虚拟机中。从JDK1.2版本开始,类的加载过程采用双亲委派机制,这种机制能更好地保证Java平台的安全。

image-20211206161405826

定义

如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。

本质

规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。

4.5.2 优势与劣势

优势

  • 避免类的重复加载,确保一个类的全局唯一性

    Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子classLoader再加载一次。

  • 保护程序安全,防止核心API被随意篡改

缺点

检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。

通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。比如在系统类中提供了一个接口,该接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。

4.6 沙箱安全机制

4.6.1 jdk1.0

image-20211207154349117

4.6.2 jdk1.1

image-20211207154408798

4.6.3 jdk1.2

image-20211207154555734

4.6.4 jdk1.6

image-20211207154605936

4.7 自定义类加载器

4.7.1 为什么要自定义类加载器

  • 隔离加载类

    在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如:Tomcat这类Web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序。

  • 修改类加载的方式

    类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载

  • 扩展加载源

    比如从数据库、网络、甚至是电视机机顶盒进行加载

  • 防止源码泄漏

    Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。

4.7.2 实现方式

  • Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。
  • 在自定义ClassLoader 的子类时候,我们常见的会有两种做法:
    • 方式一:重写loadClass()方法
    • 方式二:重写findClass()方法–>推荐

4.8 jdk9新特性

  1. 扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform classloader)。可以通过classLoader的新方法getPlatformClassLoader()来获取。

    JDK 9时基于模块化进行构建(原来的rt.jar和 tools.jar被拆分成数十个JMOD文件),
    其中的Java类库就已天然地满足了可扩展的需求,那自然无须再保留<JAVA_HOME>\liblext目录,此前使用这个目录或者 java.ext.dirs系统变量来扩展JDK功能的机制已经没有继续存在的价值了。

  2. 平台类加载器和应用程序类加载器都不再继承自java.net.URLClassLoader。现在启动类加载器、平台类加载器、应用程序类加载器全都继承于jdk.internal.loader.BuiltinClassLoader。

  3. 在Java 9中,类加载器有了名称。该名称在构造方法中指定,可以通过**getName()**方法来获取。平台类加载器的名称是platform,应用类加载器的名称是app。类加载器的名称在调试与类加载器相关的问题时会非常有用。

  4. 启动类加载器现在是在jvm内部和java类库共同协作实现的类加载器(以前是C++实现),但为了与之前代码兼容,在获取启动类加载器的场景中仍然会返回null,而不会得到BootClassLoader实例。

  5. 类加载的委派关系也发生了变动。
    当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值