JVM 基础篇:类加载器

一.了解JVM

1.1什么是JVM

JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

虚拟机可以分为系统虚拟机程序虚拟机

  • 系统虚拟机是一种虚拟化技术,它模拟整个计算机硬件环境,包括处理器、内存、存储和外部设备。它的主要目标是在单个物理计算机上同时运行多个操作系统。每个虚拟机都具有独立的操作系统和应用程序,就像在不同的物理计算机上运行一样。系统虚拟机的例子包括VMware、VirtualBox和Hyper-V。
  • 程序虚拟机是一种虚拟化技术,它仅模拟计算机上的一个单独的应用程序运行环境,而不是整个操作系统。它的主要目标是提供一个独立的运行环境,使应用程序能够在不同的操作系统上运行而无需修改。程序虚拟机通常用于解决跨平台兼容性的问题,模拟一个应用程序的运行环境,使应用程序能够跨平台运行。常见的程序虚拟机包括Java虚拟机(JVM)。

1.2JRE/JDK/JVM

  • JDK(Java Development Kit) 是整个JAVA的核心,包括了Java运行环境JRE(Java Runtime Envirnment),一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API)。
  • JRE(Java Runtime Environment,Java运行环境), 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)。JRE是Java运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器)
  • JVM(Java Virtual Mechinal)是JRE的一部分,叫做JAVA虚拟机,它是整个java实现跨平台的最核心的部分,负责解释执行并运行字节码文件(.class)。

1.3JVM的功能

1.4常见的JVM

1.5JVM的整体结构

二.字节码文件

2.1字节码文件的组成

这里重点看一下基本信息、常量池和方法

2.1.1基本信息

Magic魔数

主副版本号

主版本号不兼容会引发以下错误:

2.1.2常量池

字节码文件中常量池的作用:避免相同的内容重复定义,节省空间。

我们看下面一个案例

public class ConstantPoolTest2 {public static final String a1 = "abc";public static final String a2 = "abc";public static final String abc = "abc";public static void main(String[] args) {ConstantPoolTest2 constantPoolTest = new ConstantPoolTest2();}
}

首先看字段,我们看a1的常量值实际上指向了常量池中的8号

这实际上是常量池中的一个String_info,但是里面并没有存储真正的字符串字面量,而是一个10号常量的索引

10号常量存储的就是真正的字符串字面量

为什么字段不直接存储常量池里字符串字面量的索引?而要先找String_info,然后再找字符串字面量

因为字节码文件被加载的时候会把常量池中String_info加载到字符串常量池中,所以String_info需要存一份引用

那为什么String_info里不直接存储字符串字面量,而是存一份索引?

字段中的变量名也可能要引用常量池里的字符串字面量,如果用String_info存储字符串字面量则不合理,因为字段中的变量名并不是一个字符串的对象

符号引用

2.1.3方法

字节码中的方法区域是存放字节码指令的核心位置,字节码指令的内容存放在方法的Code属性中。

注意:

  • iload是将值复制一份,局部变量表中的值还在
  • istore是将操作数栈的栈顶弹出,并放入局部变量表的某个位置,此时操作数栈的值是不存在的
i=i++的执行流程

iinc x by y:将局部变量表的x号位置增加y

很明显操作数栈中的值一直都是0,只要istore,那么局部变量表中的值也会被覆盖,所以最终i为0

i=++i的执行流程

由于iinc指令在iload指令之前,所以i的最终值是1

2.2字节码文件常用工具

2.2.1 Java 字节码的字节码查看器:javap -v

2.2.2 阿里arthas

Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,大大提升线上问题排查效率。

详细请看官网:简介 | arthas (aliyun.com)

dashboard显示当前系统的实时数据面板,-i刷新实时数据的时间间隔 (ms),-n刷新实时数据的次数

dump已加载类的字节码文件到特定目录:

dump -d 特定目录  类的全限定名(即包名+类名) 

反编译已加载类,得到源码:

jad 类的全限定名(即包名+类名) 

三.类的生命周期

3.1加载阶段

  1. 类加载器ClassLoader根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息
  2. 类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到内存的方法区中。生成一个InstanceKlass对象,保存类的所有信息,里边还包含实现特定功能比如多态的信息。
  3. 同时,Java虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.Class对象。作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中)

对于开发者来说,只需要访问堆中的Class对象而不需要访问方法区中所有信息。这样Java虚拟机就能很好地控制开发者访问数据的范围。

加载阶段过后,字节码文件就已经被读取到了内存中,并且会创建一个代表该类的Class对象。

3.2连接阶段

  1. 第一个环节是验证,验证的主要目的是检测Java字节码文件是否遵守了《Java虚拟机规范》中的约束。这个阶段一般不需要程序员参与。
  2. 准备阶段为静态变量(static)分配内存并设置初始值。准备阶段只会给静态变量赋初始值,而每一种基本数据类型和引用数据类型都有其初始值。但注意如果字段是final修饰的基本类型或者字符串常量(经过编译器优化),则会在准备阶段直接赋予最终值。
  3. 解析阶段主要是将常量池中的符号引用替换为直接引用。 符号引用就是在字节码文件中使用编号来访问常量池中的内容。 直接引用不再使用编号,而是使用内存中地址进行访问具体的数据。

3.3初始化阶段

初始化阶段会执行字节码文件中 clinit 部分的字节码指令。

<clinit>方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

以下几种方式会导致类的初始化:

  1. 访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化
  2. 调用Class.forName(String className),注意 Class<?> forName(String name, boolean initialize, ClassLoader loader) 这个构造器可以指定不初始化
  3. new一个该类的对象时。
  4. 执行Main方法的当前类。

我们可以在JVM设置里添加 -XX:+TraceClassLoading 参数,这样可以看到有哪些类被加载

通过测试以下程序可以发现

类被加载时不一定会被初始化,而是在需要初始化的时候才初始化。

类加载但不初始化的情况

  • Class<?> forName(String name, boolean initialize, ClassLoader loader) 这个构造器可以指定不初始化
  • ClassLoader的loadClass(String className);方法也只会加载并编译某类,并不会对其执行初始化
  • 类名.class

面试题分析

clinit指令在以下情况下不会出现

  1. 无静态代码块且无静态变量赋值语句。
  2. 有静态变量的声明,但是没有赋值语句。如 public static int a;
  3. 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化。如 public final static int a = 1;

当出现继承关系时

  • 直接访问父类的静态变量,不会触发子类的初始化。
  • 子类的初始化clinit调用之前,会先调用父类的clinit初始化方法。

如果把new B02()去掉会怎么样呢?

数组的创建不会导致数组中元素的类进行初始化。

四.类加载器

类加载器的设计JDK8和8之后的版本差别较大,JDK8及之前的版本中默认的类加载器有如下几种:

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

启动类加载器是最底层的类加载器,是虚拟机的一部分,它是由C++语言实现的,无法在Java代码中直接获取到,且没有父加载器(这里形容的是父子关系的层次结构,并非继承关系),也没有继承java.lang.lassLoader类。

它主要负责加载由系统属性 “sun.boot.cass.path” 指定的路径下的核心类库(即<JAVA_HOME>/jre/lib),包含了Object、String、Math、装箱类型、日期类等核心类

public class Demo {public static void main(String[] args) {//Bootstrap 引导类加载器//打印为null,是因为Bootstrap是C++实现的ClassLoader classLoader = Object.class.getClassLoader();System.out.println(classLoader);//查看引导类加载器会加载那些jar包URL[] urLs = Launcher.getBootstrapClassPath().getURLs();for (URL urL : urLs) {System.out.println(urL);}}
}

4.2扩展类加载器(ExtClassLoader)

  • 全类名:sum.misc.Launch$ExtClassLoader,Java语言实现。  
  • 扩展类加载器的父加载器是Bootstrap启动类加载器 (注:不是继承关系)  
  • 扩展类加载器负责加载<JAVA_HOME>\jre\lib\ext目录下的类库。

注: JDK9是jdk.internal.loader.ClassLoaders$PlatformClassLoader类

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

  • 全类名: sun.misc.Launcher$AppClassLoader  
  • 系统类加载器的父加载器是ExtClassLoader扩展类加载器 (注:不是继承关系)  
  • 系统类加载器负责加载 classpath环境变量所指定的类库,包括项目中自己编写的类文件以及第三方jar包中的类文件,是用户自定义类的默认类加载器。

注: JDK9是jdk.internal.loader.ClassLoaders$AppClassLoader类

4.4三者之间的关系

  • AppClassLoader的父加载器是ExtClassLoader
  • ExtClassLoader的父加载器是Bootstrap
  • Bootstrap是根加载器  
  • AppClassLoader和ExtClassLoader都实现了抽象类ClassLoader

三者之间是没有继承关系的,而是一种组合关系

  • 抽象类ClassLoader有一个字段parent, AppClassLoader和ExtClassLoader通过设置该字段引用,指定父加载器。(是组合关系)
  • AppClassLoader 的parent指向 ExtClassLoader
  • ExtClassLoader 的parent指向 null,(null的原因是因为Bootstrap是C++实现的,通过代码中逻辑判断来转向Bootstrap)

4.5双亲委派机制

双亲委派机制指的是:自底向上查找是否加载过,再由顶向下进行加载。

双亲委派机制的好处

  • 避免类的重复加载:当父加载器已经加载该类时,就没有必要子加载器再加载一遍,保证被加载类的唯一性。
  • 避免核心类篡改:通过双亲委派机制,让顶层的类加 载器去加载核心类,避免恶意代码 替换JDK中的核心类库,比如 java.lang.String,确保核心类 库的完整性和安全性。

我们可以看看下面的程序

这里我自定义了一个String类,并且类的全限定名和JDK内置的String类完全一样。但运行结果如下:

异常提示:在类 java.lang.String 中找不到 main 方法。

why?这里程序在执行时识别的是src中的java.lang.String,src就是classpath,因此会调用系统加载器。但根据双亲委派机制,系统加载器会逐层委派上层加载器来加载此类,在委派的时候,最上层的加载器是根加载器,即根加载器优先级最高。而根加载器能够在jre\lib\rt.jar包中找到一个重名的java.lang.String(即jdk自带的String),因此根据双亲委派最终会由最顶层的根加载器来执行jdk自带的java.lang.String。显然,jdk中的String并没有main()方法,因此报错找不到main()

4.6自定义类加载器

Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。

loadClass默认实现如下:

再看看loadClass(String name, boolean resolve)函数,双亲委派机制的核心代码就位于这里

从上面代码可以明显看出,loadClass(String, boolean)函数即实现了双亲委派模型!整个大致过程如下:

  1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
  2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);)或者是调用bootstrap类加载器来加载。
  3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。

整个调用过程如下图所示

在自定义ClassLoader的子类时候,我们常见的会有两种做法:

  • 重写loadClass()方法:这样会打破双亲委派模型,可能会导致一些Java的核心类无法加载,不建议重写
  • 重写findClass()方法:是在双亲委派模型的框架下进行小范围的改动,建议重写

实战代码如下:

public class MyClassLoader extends ClassLoader {private String root;@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte[] classData = loadClassData(name);if (classData == null) {throw new ClassNotFoundException();} else {return defineClass(name, classData, 0, classData.length);}}private byte[] loadClassData(String className) {String fileName = root + File.separatorChar+ className.replace('.', File.separatorChar) + ".class";try {InputStream ins = Files.newInputStream(Paths.get(fileName));ByteArrayOutputStream baos = new ByteArrayOutputStream();int bufferSize = 1024;byte[] buffer = new byte[bufferSize];int length = 0;while ((length = ins.read(buffer)) != -1) {baos.write(buffer, 0, length);}return baos.toByteArray();} catch (IOException e) {e.printStackTrace();}return null;}public String getRoot() {return root;}public void setRoot(String root) {this.root = root;}public static void main(String[] args)  {MyClassLoader classLoader = new MyClassLoader();classLoader.setRoot("D:\\");Class<?> testClass = null;try {//需要为com.字节码文件.classloader.A 格式,否则defineClass方法会抛异常testClass = classLoader.loadClass("com.字节码文件.classloader.A");Object object = testClass.newInstance();System.out.println(object.getClass().getClassLoader());} catch (Exception e) {e.printStackTrace();}}
}

注意

  • 实例中的 class文件不能放在classpath下,否则根据双亲委派机制会被应用程序类加载器加载,而不会通过我们自定义类加载器来加载。
  • 这里传递的文件名需要是类的全限定性名称,即 com.字节码文件.classloader.A 格式的,因为 defineClass 方法是按这种格式进行处理的。

4.7Launcher类

AppClassLoader和ExtClassLoader是Launcher的静态内部类,在程序启动时JVM会创建Launcher对象,Launcher构造器会同时会创建扩展类加载器和应用类加载器。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/167758.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【React】高频面试题

1. 简述下 React 的事件代理机制&#xff1f; React使用了一种称为“事件代理”&#xff08;Event Delegation&#xff09;的机制来处理事件。事件代理是指将事件处理程序绑定到组件的父级元素上&#xff0c;然后在需要处理事件的子元素上触发事件时&#xff0c;事件将被委托给…

Python —— hou.NetworkItem class

在一个network内&#xff0c;所有可见元素的基类&#xff1b; 此类没有方法&#xff0c;仅作为 hou.NetworkMovabelItem、hou.NodeConnection 基类存在&#xff0c;这两个子类在网络编辑器内均是可见的&#xff0c;是没有真正有意义的基类的&#xff1b;通过提供一个公共的基类…

deque的简单了解

介绍 deque是一种结合了list和vector两者优势的一种容器&#xff0c;它既可以支持下标的随机访问&#xff0c;并且头插头删的效率都不低&#xff0c;但相对也存在一定的缺陷&#xff0c;在中间插入和遍历上&#xff0c;消耗相对较大。 deque并不是真正连续的空间&#xff0c;…

读卡器+芯片

RFID Reader 产品参数 产品型号 RFID Reader 尺寸(mm) 104*70*11.7mm 外观颜色 黑 材质 ABS 接口 MINI 功率 0.1W 电源 5V/1A 读卡距离 40mm 工作温度 -40~85℃ 工作频率 13.56Mhz 串口功能 支持 支持协议 ISO/IEC 14443、ISO/IEC 15693 产品参数 …

分布式存储 vs. 全闪集中式存储:金融数据仓库场景下的性能对比

作者&#xff1a;深耕行业的 SmartX 金融团队 张德敏 近年来随着金融行业的高速发展&#xff0c;经营决策者及监管机构对信息时效性的要求越来越高&#xff0c;科技部门面临诸多挑战。例如&#xff0c;不少金融机构使用数仓业务系统&#xff0c;为公司高层提供日常经营报表&am…

【剑指Offer】36.二叉搜索树与双向链表

题目 输入一棵二叉搜索树&#xff0c;将该二叉搜索树转换成一个排序的双向链表。如下图所示 数据范围&#xff1a;输入二叉树的节点数 0≤n≤1000&#xff0c;二叉树中每个节点的值 0≤val≤1000 要求&#xff1a;O(1)&#xff08;即在原树上操作&#xff09;&#xff0c;时间…

YOLOv5论文作图教程(1)— 软件介绍及下载安装(包括软件包+下载安装详细步骤)

前言:Hello大家好,我是小哥谈。在学习YOLOv5算法的过程中,很多同学都有发表论文的需求。作为文章内容的支撑,图表是最直接的整合数据的工具,能够更清晰地反映出研究对象的结果、流程或趋势。在发表论文的时候,审稿人除了关注论文的内容和排版外,也会审核图表是否清晰美观…

华为数通方向HCIP-DataCom H12-831题库(多选题:21-40)

第21题 网络管理员A希望使用ACL匹配特定的路由条目,请问以下哪些路由条目将被图中的ACL规侧匹配? acl number 2000 rule 10 permit source 10.0.0.0 0.0.6.0A、10.0.0.1/32 B、10.0.0.0/24 C、10.0.1.0/24 D、10.0.2.0/24 答案: 解析: 通配符十进制6转换二进制为00000110,…

从Excel到智能化:智能报表的演进与未来发展趋势

摘要&#xff1a;本文由葡萄城技术团队于CSDN原创并首发。转载请注明出处&#xff1a;葡萄城官网&#xff0c;葡萄城为开发者提供专业的开发工具、解决方案和服务&#xff0c;赋能开发者。 报表的迭代历程 报表工具的诞生与计算机技术的出现和信息技术的进步密不可分。下图是报…

springBoot--web--路径匹配

路径匹配 前言在配置文件中配置路径匹配结果 前言 spring5.3之后加入了更多的请求路径匹配的实现策略 以前只支持antPathMatcher策略&#xff0c;现在提供了PathPatternParse策略&#xff0c;并且可以让我们指定到底使用哪种策略 PathPatternParser: 在jmh基准测试下&#xff…

YOLOv5/v7/v8改进实验(五)之使用timm更换YOLOv5模型主干网络Backbone篇

&#x1f680;&#x1f680; 前言 &#x1f680;&#x1f680; timm 库实现了最新的几乎所有的具有影响力的视觉模型&#xff0c;它不仅提供了模型的权重&#xff0c;还提供了一个很棒的分布式训练和评估的代码框架&#xff0c;方便后人开发。更难能可贵的是它还在不断地更新迭…

QT学习day5(QT实现TCP协议)

作业&#xff1a;利用TCP客户端和服务器实现网络聊天室&#xff08;简单版QQ&#xff09; 1.服务器代码 widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QTcpServer> //服务器头文件 #include<QTcpSocket> …

“香蕉大王”的转型升级,能否扩大市场份额?

佳农食品控股 ( 集团 ) 股份有限公司,于2023年10月11日同海通证券签署上市辅导协议&#xff0c;计划登陆上交所主板。据了解这已经不是佳农食品第一次IPO了&#xff0c;2019 年&#xff0c;佳农集团曾向上交所递交过招股说明书&#xff0c;当时的招股书披露&#xff0c;佳农集团…

磁盘分区如何分? 电脑磁盘分区免费软件指南!

列出并比较顶级免费磁盘分区管理器软件&#xff0c;以选择适用于 Windows 的最佳分区软件&#xff1a; 系统分区在现代计算机设备中起着非常重要的作用。它们可以存储数据&#xff0c;使系统文件远离用户数据&#xff0c;并在同一台设备上安装多个操作系统。但是&#xff0c;这…

阿里云对象存储OSS怎么停止扣费

阿里云对象存储OSS一直扣费如何停止&#xff1f;如何关闭对象存储OSS&#xff1f;阿里云对象存储OSS没有关闭功能&#xff0c;如果不再使用对象存储OSS可以删除存储空间Bucket下的所有文件&#xff0c;详细说下阿里云对象存储OSS停止收费的方法&#xff1a; 阿里云对象存储OSS…

TS和JS的区别

1.TS和JS的区别 ts 是js的超集。 从执行环境上来看&#xff0c;浏览器、node.js 可以直接执行js,但不能执行ts;编译层面&#xff0c;Ts 有编译阶段&#xff0c;js 没有&#xff0c;只有转译阶段和lint阶段&#xff1b;ts更难写一点&#xff0c;但类型更安全。ts 代码写出来就是…

绿盾加密软件支持文件 CAD图纸加密 多平台Windows、Linux、Mac

绿盾加密软件支持文件CAD图纸加密&#xff0c;并且可以在多平台如Windows、Linux、Mac上运行。适用于各种商务服务、软件开发以及行业专用软件。 PC访问地址&#xff1a; https://isite.baidu.com/site/wjz012xr/2eae091d-1b97-4276-90bc-6757c5dfedee 绿盾加密软件的具体功能…

【三维重建】DreamGaussian:高斯splatting的单视图3D内容生成(原理+代码)

文章目录 摘要一、前言二、相关工作2.1 3D表示2.2 Text-to-3D2.3 Image-to-3D 三、本文方法3.1生成式 高斯 splitting3.2 高效的 mesh 提取3.3 UV空间的纹理优化 四. 实验4.1实施细节4.2 定性比较4.3 定量比较4.4 消融实验 总结&#xff08;特点、局限性&#xff09; 五、安装与…

C++ 中的模型预测路径积分 (MPPI) 控制

一、说明 模型预测路径积分控制&#xff08;MPPI&#xff09;是一种基于采样的模型预测控制算法。是MPC控制模型的延申和拓宽&#xff0c;要了解MPPI需要先理解MPC&#xff0c;参见文章&#xff1a;MPC预测控制概述和C 中的模型库-CSDN博客 二、模型预测路径积分 (MPPI) 控制 模…

如何破解压缩包密码,CTF压缩包处理

I. 引言 压缩包我们经常接触&#xff0c;用于对文件进行压缩存储/传输。压缩包处理在CTF比赛中是非常重要的一块&#xff0c;因为压缩包中可能包含重要信息&#xff1a;许多CTF题目会将关键信息隐藏在压缩包中&#xff0c;参赛者需要解压并查看其中的内容才能获取有用的线索。…