JVM 学习

JVM是什么,学了有什么用?

java虚拟机,是java代码执行的底层,java代码都会被翻译成class二进制字节码文件,然后java虚拟机运行这个字节码文件来执行java程序

所以学习java虚拟机实际上就是对底层代码的学习

为什么要学习java虚拟机

  1. 企业要求

企业需要我们的程序了解java代码底层,这一块的了解程度可以一定程度上代表程序员的java技能水平

  1. 可以解决异常问题

许多的异常,都可以通过jvm的监控进行排查处理,有了jvm的知识,解决异常问题更加有效

例如

内存泄漏问题

CPU占用过高问题

  1. 性能调优

因为java代码都是运行在Java虚拟机上的,可以通过调准Java虚拟机策略,提高执行在java虚拟机上的程序的性能

需要大型的java程序运行时很慢的,需要调优

Java虚拟机内容

Java虚拟机内容的内容大概是三个部分

  1. 字节码文件详解
  2. Java虚拟机内存区域
  3. Java虚拟机垃圾回收

除了基本内容之外,我们还需要了解的内容,就是Java虚拟机的各种工具

  1. 调优工具
  2. 检测工具

Java虚拟机的印象

java虚拟机全程就是java virtual machine,简称JVM

java虚拟机是一个程序

java虚拟机本质上是一个程序,这个程序用来读取字节码文件运行

职责就是运行字节码文件

所以要运行一个字节码文件,需要知道两个位置,一个是Java虚拟机程序的位置,一个是要运行的字节码文件的位置

image-20240213152801856

虚拟机的核心工作

1 解释字节码

拿到字节码文件之后,Java虚拟机会按照标准的字节码语法规则对拿到的字节码文件进行解释,翻译成对应机器的机器码

不同的机器上有不同版本的JVM,所以不同机器的差异就被java虚拟机屏蔽了,只有虚拟机要按照不同的机器,提供不同的版本,而字节码文件都是统一的

2 内存管理

java这种语言的特点,就是自动分配内存,自动垃圾回收,使得java更加方便好用

这个自动分配和回收的工作就是JVM实现的

3 即时编译

就是在运行过程中,优化热点代码(实际上就是解释成机器码,并保存在内存中,以后就节省了解释这个过程),提高执行效率,也就是局部性原理的应用

(提高代码效率的核心功能)

常见的Java虚拟机牌子

虚拟机需要遵循一定的规范,这样保证,字节码文件可以在不同的Java虚拟机上正常运行

名称 作者 支持java版本 社区活跃 特性 适用场景
HotSpot(oracle jdk版本) Oracle 所有 高活跃,闭源 使用最广泛,稳定可靠 默认使用
HotSpot(open jdk版本) Oracle 所有版本 开源,也是稳定可靠,使用广泛 默认使用,如果对于jdk有二次开发需求,那么这个就行
GraalVM Oracle 11,17,19,企业版可以支持8 高性能 微服务,云原生框架,多语言混合编程
Dragonwell JDK
龙井
Alibaba 标准版支持8,11,17
拓展版支持11,17
基于openJDk的增强版本,高性能,bug修复,安全性等等提升 电商,物流,金融等等性能要求高的领域
Eclipse OpenJ9(原IBM J9) IBM 8,11,17,19,20 高性能,可扩展 微服务,云原生框架

在安装了java jdk的机器上,cmd窗口上使用命令java -version,可以看到虚拟机

image-20240213155509755

这里安装的就是hotSpot虚拟机

我们主要学习的也是HotSpot(oracle jdk)版本的虚拟机

HotSpot虚拟机的发展历史

1999年4月Hotspot初出茅庐,首次在jdk中使用

2006年12月野蛮生长,此时JDK6发布,虚拟机大大优化HotSpot性能大大优化

2009-2013年,稳步前进,JDK7首次推出G1垃圾收集器(这种收集器效率很好),JDK8引入JMC等工具,除去用虚拟机中的永久代

2018-2019年,百家争鸣,JDK11优化了G1垃圾收集器性能,推出了新的ZGC垃圾回收器,JDK1推出shenan-doah垃圾回收器

2019年到现在,拥抱云原生,就是GraalVM虚拟机,解决多语言混合使用,提供效率,启动速度快等

云原生

云原生是一种应用程序开发和交付模式,它利用容器化、微服务架构、自动化和持续交付等现代化技术,将应用程序部署到云环境中。

具体来说,云原生应用程序具有以下几个特点:

  1. 容器化:云原生应用程序使用容器来打包应用程序及其所有依赖项。容器化使开发者能够更轻松地将应用程序在跨平台中进行部署。

  2. 微服务架构:云原生应用程序采用微服务架构,将应用程序拆分为更小的、互不依赖的服务。微服务使得应用程序更易于维护、扩展和部署,也使得开发者能够更快地回应新的市场需求及客户反馈。

  3. 自动化:云原生应用程序使用自动化来简化开发、测试、部署和运维工作流程。自动化使得应用程序更高效、更可靠、更安全,并减少了人为错误的可能性。

  4. 持续交付:云原生应用程序采用持续交付模式,这意味着开发者可以更快地交付新的代码,更快地修复错误,并更快地上线新功能。

总之,云原生是一种现代化的应用程序开发和部署模式,它专注于利用新技术和新工作流程,以更高效、更灵活、更可靠的方式构建、交付和运营应用程序。

字节码文件详解

学习字节码文件,第一步还得先了解一下Java虚拟机的组成,这样就能够知道字节码文件中Java虚拟机中起到的作用,以及字节码文件的处理过程

Java虚拟机的组成

根据运行字节码文件的要求,我们按照流程来理解java虚拟机的组成

1 加载字节码文件

虚拟机第一步需要将来自磁盘或者网络中的字节码文件加载到内存中,这样才能更快的解释执行

这个工作是名为 ClassLoader 的程序负责的,叫做 [类加载器]

2 运行字节码文件

当我们的字节码文件加载到内存之后(也是加载到运行时数据区),java虚拟机会实时的解释执行字节码代码,解释一句执行一句

我们在执行过程中创建的类,对象等都需要保存在内存中,这片内存就是 运行时数据区

JVM所谓的内存管理,就是管理的这片内存,创建和销毁对象就是操作这一片内存区

image-20240213161710090

3 执行引擎

解释执行来自运行时数据区的字节码代码

即时编译

垃圾回收

解释执行

这些操作都是属于执行引擎的

4 调用本地接口

执行引擎在执行字节码的时候,与底层机器交互还是使用的c/c++编写的方法,所以需要调用虚拟机提供的c/c++方法,这些方法已经编译成机器码

总结

Java虚拟机的组成大致上是四个部分

类加载器,运行时数据区,执行引擎,本地方法区

image-20240213162317718

字节码文件的组成

学习字节码文件,可以解决java语法中的一些问题,java版本冲突的一些问题,线上系统升级的问题等等

简单了解

字节码文件是二进制文件,无法直接翻译成字符

想要查看字节码文件中包含的信息可以通过一些专业的工具解释翻译字节码文件

jclasslib 就是一个很好的反编译字节码文件的工具

image-20240213163708788

字节码文件五部分内容组成

1 基础信息

基本信息主要目的就是引导虚拟机正确的去读取字节码的内容

不太重要的信息有

  1. 类的访问标识
  2. 父类
  3. 实现的接口

重要组成
  1. 魔数

每个文件的最开头的任意字符可以为定义成魔数,实际上就是任意的规定

例如 在utf-8编码文件中可以在文件开头添加bom也就是编码信息,当一些程序读取这个文件发现文件开开头是这么几个二进制,那么他就知道这是一个utf-8编码的文本文件,会自动使用utf-8解释文件

而我们的字节码文件在开头定义了一个16进制下的

image-20240213164346745

cafe ba be 代表这个文件是字节码文件

如果虚拟机读取到文件不是cafebabe开头的,那么java虚拟机可以知道这么文件一定不是满足字节码文件格式的文件,所以会直接报错

  1. 字节码版本号

主副版本号,来表示这个字节码是按照那个语法版本进行编译的,虚拟机判断当前jdk是否兼容这个字节码

查看主版本号代表那个jdk版本

通过主版本号-44就是jdk版本

例如 52 就是 52 - 44 = 8 就是jdk1.8版本(jdk 8)

一般不关心副版本号

当我们的项目字节码文件与运行环境中的jdk版本不兼容的时候,有两种方式解决问题

image-20240213203318244

1 升级jdk版本

2 降低第三方依赖的版本,使其适应运行时jdk版本

在实际开发场景中,我们都是使用第二种解决方案,因为第一种解决方案容易引发兼容性问题,需要大量的测试工作测能保证不出问题

2 常量池

主要保存了一个类中的使用到的常量,类和接口名,字段名,主要用于字节码指令的引用

这样我们的指令中就使用代号表示常量,可以大大减少执行中的字符数,以及减少冗余

主要的作用

主要为了避免相同的内容,重复占用内存空间,我们加载字节码到内存空间中,所以我们将重复的常量,只保存一份,其他地方引用常量池中的常量

注意

常量池中保存了所有唯一的东西

例如变量名是唯一的,所以也保存在常量池里面,方法名是唯一的,所以也保存在常量池里面,字符串字面值也是唯一的,保存在常量池里面,字符串比较特殊,因为没有字符串的基本数据类型,所以字符串除了保存字面值之外还需要用一个字符串对象的量来进行表示,为什么不直接使用字符串来代替字符串字面值呢?因为如果我们的变量名或方法名之类的就不需要引用字符串对象,而是一个字符串字面值即可,所以常量池中字符串和字符串字面值同时存在,字符串中保存的就是字符串字面值的引用

所以在字节码中,字段的名字是一个引用类似#number 他的值也可以是一个引用#number

字符串与字符串字面值不同

如果我们在类中写一个字段,给一个字符串的赋值,实际上我们是引用常量池中的一个字符串常量对象,而不是引用一个字符串字面值,因为java都字符串是一个对象,所以必须要创建对象

如果我们的一个字段或者方法的名字和某个字符串中的原有的字符串字面量重合了,那么我们可以直接引用这个字面值,进行替换,而不是使用对象!

例如

private String abc = “abc”

这里可以将

#1 abc

#2 String(#1) //表示这是一个字符串对象

那么字节码文件就可以这样写

#1 = #2

3 方法

字节码中的方法,使用字节码指令保存了方法中的内容和方法头的信息

实际上操作可以分成操作数栈和局部变量表

image-20240213210941705

iconst_0 可以看做是 iconst 和 _0 表示从常量池中加载0到操作数栈中

istore_1 看做是istore和_1 表示弹出操作数栈一个操作数到局部变量表中下表为1的变变量

iload_1 看做是iload和_1 表示局部变量表中的下表唯一的变量复制数据到操作数栈中

iadd 表示操作数栈中所有数执行加法操作变成一个数

return 方法返回

在方法写好的时候就可以知道局部变量表中有多大了,方法的参数变量也会占用局部变量表的空间,局部变量表的下标是按照变量申明顺序

赋值符号=和i++以及++i的本质

赋值操作实际上表现在字节码指令上就是istore_index

后++操作和前++操作的区别就是

iload_index操作iinc index by const_number的顺序

可以发现 我们表达式的值就表现为操作数栈的值,所以操作数栈的值是多少我们表达式的直接是多少

i = i++操作,实际上是先iload到操作数栈,然后再去iinc操作,所以实际上相当于i = i;

如果是i = ++i 那么就是先iinc操作再去iload,就是i = (i+1);

字节码常用工具

1 javap -v命令

这个命令行工具是jdk自带的一款反编译工具,用于将字节码反编译成刻度的字符串形式,而不是jclasslib工具通过列表显示字节码信息的方式

javap命令的优势

对于非图形化界面中,我们可以通过javap命令更加方便的查看字节码信息,而jclasslib更适合在windows等图形用户界面查看字节码文件信息

2 Idea插件Jclasslib工具

marketplace中所有插件jclasslib,在右边栏有一个查看字节码的工具栏

注意

必须要光标处于类文件中,然后点击视图,点击jclasslib选项,才能够添加字节码解析到边栏中

image-20240213215101806

image-20240213215115353

3 阿里的Arthas

这个工具可以检测正在运行的字节码文件,实时查看load,内存,gc,线程,和状态信息,诊断业务代码

arthas是一个jar包的命令行工具

我们使用java -jar filepath可以运行arthas

arthas运行之后会自动检测当前系统的运行的java程序,并显示程序的PID的arthas为其编号

我们选中一个编号输入并回车,arthas就会进入监控这个程序

比较关键的操作就是

  1. jad 类全路径名

这个命令可以将运行中的程序中的字节码文件反编译成java源代码显示在控制台

  1. dump -d filepath 类全路径名

这个命令可以将字节码反编译成字符版本的字节码文件保存在filepath那个文件中

  1. dashboard 命令

显示当前的程序的一些运行时信息

类的生命周期(class文件的生命周期)

在讲解类的加载器之前,我们需要先了解累的生命周期,知道类什么时候加载进内存,什么时候被垃圾回收器回收,什么时候退出内存卸载

这个部分非常重要

  1. 运行时常量池
  2. 类加载器的作用
  3. 多台的原理
  4. 类的加密解密

这些都是依赖于类的生命周期

五个阶段

image-20240213222544144

其中连接阶段还可以细分成三个小阶段

image-20240213222643803

加载阶段

  1. 类加载器的操作

这是类生命周期的第一步,类加载器通过各种渠道获取到字节码文件的二进制流

(例如动态代理生成的类也需要加载进内存,网络中也可以下载字节码文件动态加载进入虚拟机)

类加载器将字节码信息加载到内存中之后,类加载器的任务就完成了

  1. Java虚拟机的操作

Java虚拟机会将字节码文件中的信息保存在内存的方法区中.(方法区是一个抽象的概念,实际上没有任何一片内存叫做方法区)

方法区中实际上代表的是一个对象InstanceKlass

image-20240213223323012

除此之外Java虚拟机还会在堆区创建对象来保存类的信息

image-20240213223708663

为什么要创建两个表示类的信息呢?

方法区中的InstanceKlass实际上是c\c++编写的对象,java不能直接操作,而堆区中的是java.lang.Class类型的对象,是可以直接操作的

另一方法InstanceKlass中所包含的信息更多,许多开发者不需要关心的信息或者一些敏感的信息不能被开发者访问,所以将可以访问的信息保存在堆区中的Class对象中供开发者访问,这样就保证了数据的安全性

可以通过InstanceKlass找到堆区中的Class对象也可以反向查找到方法区中的类对象

堆区中比方法区中的多了静态字段数据

在jdk1.8之前静态字段数据是存放在方法区中的,在之后就是在堆区的Class对象中

image-20240213224723275

连接阶段

校验字节码内容是否合规—>准备为静态变量赋值—>解析,将对常量池中的符号引用替换为内存引用(运行时常量池中的内存)

连接-校验阶段

包含文件格式校验版本号是否符合兼容,元信息校验,语义校验,符号引用校验…

连接-准备阶段

主要的任务就是给静态变量分配内存并设置初始值

如果静态变量不是final修饰,那么他就是一个变量,做准备阶段Java虚拟机会先为变量赋默认初值,而不是自定义初始值,等到后面的初始化阶段才会将我们的值赋给静态变量

如果静态变量是fianl修饰,实际上就可以确定静态变量指向常量池中的某个常量,在字节码文件中都直接表示引用常量,所以在准备阶段Java虚拟机就将这个值赋给这个静态变量

连接-解析阶段

将符号引用替换为直接内存引用

例如父类引用可以直接换成内存地址

初始化阶段

这个阶段比较重要,因为这个阶段程序员可以进行干预

程序员通过静态代码块,在初始化阶段进行操作

例如

static int a = 1;

这个代码属于静态代码,赋值操作

static{

//这里面的代码也属于静态代码

}

实际上这个阶段执行的就是字节码指令中的clinit部分的字节码指令(class init)

在字节码文件中,一定会创建创建构造方法

如果有静态代码赋值语句就会有初始化阶段执行方法

一些情况不会生成clinit方法

image-20240213233239627

image-20240213231204060

clinit中字节码指令的顺序和Java源代码中的静态代码赋值顺序是一致的

例如

static int a = 1;

static{

a = 2;

}

就是先赋值为1再赋值为2

如果是static{}在先,static int a =1;在后那么就是先赋值为2再赋值为1

实际上clinit也是一个方法

触发初始化阶段的办法
  1. 访问一个类的静态变量或者静态方法,如果静态变量是fianl就不会触发

  2. 使用Class.forName()方法,传入一个类名,实际上这个forName有重载方法,可以选择false不进行类的初始化阶段

  3. new一个类的对象的时候

  4. 执行Main方法的当前类

static{}我们知道是静态代码块

那没有static单纯的{}中的是什么呢?

这个是构造代码块,所有的构造方法,在执行自己的构造之前,都会先执行构造代码块中的代码,再去执行自己的构造方法中的代码

pubic Test{

​ Test(){

System.out.println(“B”);

}

{

System.out.println(“A”);

}

}

调用new Test()输出的是AB而不是BA

一些问题

image-20240213233418727

如果我们访问通过子类的类名,访问父类的静态变量,实际上静态代码在父类的Class对象中,所以不会触发子类的初始化阶段

  1. new 对象数组不会导致new真的对象,所以不会触发对象类的初始化阶段
  2. 如果fianl静态变量的值不是字面值,而是需要执行代码才能得到的结果,会放在clinit方法初始化来在初始化阶段赋值

实际逻辑

实际上生命周期可以分成七个阶段

image-20240223134932711

之所以分成七个阶段是因为,其中的解析阶段并不一定在准备阶段之后/初始化阶段之前进行,在某些情况下可能会在初始化阶段之后进行解析阶段,这种情况又被称作动态绑定和晚期绑定

虚拟机规范规定,只有**下面五种情况,**才能而且必须立刻进行类的初始化阶段(此时如果类还没有进行加载和连接阶段会先进行加载连接等)

  1. new,getstatic,putstatic,invokestatic这四条字节码指令的时候,如果没有进行初始化需要进行初始化

​ 这四条字节码指令对应java代码就是使用new实例化对象的时候,读取类的静态字段的时候,设置类的静态字段的时候,调用类的静态方法的时候

总结就是只要需要用到Class对象,那么就需要进行初始化

  1. 使用java.lang.reflect包中的方法进行类的反射调用的时候,如果累没有进行初始化,先进行初始化

实际上也是反射调用需要用到Class对象,所以必须进行初始化

  1. 初始化一个类的时候,如果其父类没有进行初始化需要先对父类进行初始化

也就是子类的Class对象依赖于父类的class对象,所以需要先初始化父类

  1. 当虚拟机启动时,用户需要指定一个执行的主类(main()方法所在的类都可以被选择为主类),虚拟机会优先初始化这个主类

实际上就是需要调用public static void main这个静态方法,所以相当于触发了第一条invokestatic,需要初始化类

  1. JDK1.7的动态语言支持,使用java.lang.invoke.MethodHandle实例解析的结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

这个MethodHandle类似于反射机制,但是比invoke()方法直接调用方法的效率更高,也更加灵活,可以更快确定方法的参数类型

注意

  1. 数组的本质,是字节码newarray动态生成的类,和数组元素类型不是继承关系,是have关系,所以在new 类型数组的时候不会触发对应元素类型的初始化阶段

  2. final static字段如果值不是计算结果,那么可以在编译阶段确定,访问此字段在编译阶段通过常量传播优化,会将引用final static直接替换成对应常量,所以和引用的这个类就没有关系了,所以不会触发类的初始化阶段

  3. 接口初始化阶段的触发基本和类相似,唯有一点,接口初始化时不需要父接口都已经初始化,真正用到父接口的时候才会进行父接口的初始化

五个阶段(加载,校验,准备,解析,初始化)

加载阶段

加载阶段是类加载的第一个阶段,有三个工作

  1. 通过全限定名来获取定义此类的二进制字节流
  2. 将字节流所代表的静态结构转换为方法区的运行时数据结构InstanceClass
  3. 在内存中堆区生成代表这个类的java.lang.Class对象,作为方法区和这个类的给中数据的访问入口

虚拟机对此部分的规定不是很具体,例如我们可以自己实现获取二进制字节流的方式

从zip中读取就是日后的jar,ear,war格式的基础

从网络中获取典型就是applet

运行时计算生成,就是动态代理之类的,java.lang.reflect

有其他文件生成,典型的就是jsp应用,jsp会生成对应的class类

类加载器

了解类的生命周期,我们可以了解类加载器到底是什么技术了

类加载器是程序获取类和接口的字节码数据的技术

类加载器大部分是Java编写的也有一些是c\c++编写的,这些在jvm的源码中

类加载器接收到字节码文件源之后,就需要调用JNI本地方法,将信息创建对象保存在内存中

image-20240213235125584

类加载器的分类

类加载器有很多不同方面的分类

另一方面,类加载器在jdk9出现了很大的变化,所以在jdk8以及之前是一种,jdk9之后又是另一种样子

按照实现来分

一类是java代码实现的,一类是Java虚拟机源码实现的

虚拟机底层源码实现的类加载与虚拟机底层语言一致,这类加载器主要用于加载程序中运行时的基础类,保证Java程序中的基础类被正确的加载进程序,确保可靠性

JDK中默认提供或者自定义的类加载器,JDK提供了多种不同渠道类加载器,程序员也可以自己定制

自定义的类加载器就是继承自ClassLoader类即可

jdk8以及之前

image-20240214001008801

Bootstrap类加载器

叫做启动类加载器,是HotSpot提供的使用C++编写的类加载器

用于加载在jdk安装目录下的jre/lib下的类文件,例如rt.jar,tools.jar…

一般我们的类加载器可以通过

例如

String.class.getClassLoader()来获取

但是因为Bootstrap是虚拟机底层源码实现的,java是无法获取到的,而且获取底层类加载器也是不安全的行为,所以会返回null

当我们getClassLoader的返回值是null那么我们就可以推测这个类加载器是底层类加载器

试图通过Bootstrap类加载器加载我们的类
  1. 可以将我们的jar包放在jre/lib目录下,这样bootstrap类加载器会自动加载

不推荐这种做法,一方面尽量不要修改jdk安装目录中的内容,另一方面很可能因为jar名称不符合规范而导致加载失败

  1. 在启动的Java虚拟机参数中添加参数

-Xbootclasspath/a:jar包目录/jar包名

这样虚拟机启动的时候bootstrap会去加载对应目录下的jar包

默认类加载器

包括Extension加载器,Application加载器

image-20240214140211197

他们都继承自URLClassLoader

扩展类加载器

默认加载的是/jre/lib/ext目录下的类文件

通过扩展类加载器加载

当我们写了一些通用单不重要的类,我们就可以让扩展类加载器帮助我们加载

  1. 将jar文件放在jre/lib/ext目录下

不推荐使用,同样的问题,不要修改jdk安装目录中的内容

  1. 使用参数进行扩展

添加jvm启动参数

-Djava.ext.dirs=原来的ext目录;额外的jar包目录

因为这个会覆盖掉原本的ext目录,所以要使用;带上原本的目录

应用类加载器

这个类加载器加载的就是我们classpath下的类文件

以及我们为项目添加的依赖之类的都是通过applicationclassloader进行加载的

逐级加载机制(双亲委派机制)

这个机制就是要来解决一个类到底是由那个类加载器进行加载的(当同一个在在多个类加载器的加载目录下)

  1. 保证类加载的安全性和完整性

比如我们自己写的一个java.lang.String类不能替换加载jdk中的java.lang.String类?

  1. 避免重复加载

同一个类加载到内存只需要一次

机制的原理

实际上每个类加载器都有一个父类(实际上并不是继承,而是一种上级关系,在类加载中有一个成员变量parent,比较有意思的就是extension类加载器拿不到bootstrap类加载器对象,所以parent的值就是null,而bootstrap类加载器是c\c++编写,没有父类加载器)

image-20240214141802297

当这个类加载器要加载一个类的时候会向父类加载器确定这个类是否已经加载过

如果父类没有加载过,会从到下,父类先加载这个类,如果找得到这个类,父类加载器就加载了,找不到再去让子类去找加载

image-20240214141939578

向下尝试加载,实现了一个加载优先级的作用,类都是优先被父加载器尝试加载,再交给子类加载器

image-20240214143159333

类加载器的访问途径

兄弟级别的类加载器加载的类之间是相互独立、相互不可见的,即互相不认识。

Java中的类加载器采用了双亲委派模型,即当需要加载某个类时,首先会委托给父类加载器去加载,如果父类加载器无法加载,则再由子类加载器去加载。兄弟类加载器是指没有直接继承关系,但具有同一个父类加载器的类加载器,它们之间的类是相互独立的,不能相互访问。

例如,Tomcat中的Web应用程序就采用了多个并行的Web应用程序ClassLoader加载各自的Web应用程序,这些ClassLoader之间是相互独立的、相互不可见的,不能直接调用或引用其他的ClassLoader所加载的类。

因此,在应用程序中,如果要使用其他ClassLoader所加载的类,通常会通过反射等方式来进行调用。当然,如果一定需要多个ClassLoader之间能够相互访问,则可以采用一些特殊的技术,例如OSGi等,但这些技术相对较为复杂,一般只用在特定的场景中。

打破逐级加载机制

有三种方法打破

  1. 自定义类加载器,我们重写自己的loadClass方法即可
  2. 线程上下文类加载器
  3. Osgi框架的类加载器

实际上打破逐级加载机制是不明智的,几乎没有什么需求要打破逐级加载机制来实现,很多所谓的打破逐级加载机制实际上只是延展了逐级加载链而已!!!

自定义类加载器

我们自己定义的类加载器就不需要走双亲委派了

比如tomcat同时运行两个web应用,两个web应用有两个全限定名相同的类,如果直接使用AppClassLoader肯定会被认为是相同的类,所以tomcat就继承AppClassLoader,为每个web应用来了一个自己的ClassLoader这样就会被委托给各自的类加载器加载了

双亲委派机制的关键

关键就是从ClassLoader类中继承的loadClass方法

这个方法里面会先去findClass,实际上就是执行了加载阶段,这个方法会去调用defineClass方法

defineClass就是去堆和方法区创建对应的对象,调用的是本地方法c\c++方法 ,这个方法执行完了实际上类的加载阶段就完毕了

resolveClass方法实际上就是连接阶段

image-20240214144502324

findClass方法是加载类的字节码文件的核心方法

image-20240214145147990

实际逻辑

image-20240214145858931

image-20240214150729386

如果不想打破机制,自定义类加载器

我们此时就只需要重写findClass方法即可,双亲委派机制的逻辑代码都在loadClass方法中

第二种方法-线程上下文类加载器

有一种情况,例如我们的JDBC中的DriverMangager是谓语rj.jar中的类,使用启动类加载器加载,但是这个类需要我们以来中的Driver类,所以启动类加载器加载了DriverManager之后需要委派Application类加载器加载Driver实现类,实际上原始可以的,但是Application类加载可能会有多个,如果加载应用程序类的类加载器和驱动类的类加载器不同,这样可能就导致ClassNotFound问题,所以我们一般使用的是线程类加载器

解释

线程类加载器不是一个实际的加载器类型,而是对于某一个类加载器的指代

我们可以将任意的ClassLoader指定为线程类加载器,但是因为Bootstrap是C++编写的所以我们一般不使用BootStrap指定为线程类加载器,一般就是AppClassLoader作为我们的线程类加载器

使用线程类加载器就可以保证驱动所使用的类加载器和应用程序所使用的类加载器是同一个,保证了加载之后不出问题


JVM 学习
https://wainyz.online/wainyz/2024/02/13/JVM 学习/
作者
wainyz
发布于
2024年2月13日
许可协议