【Java】一文讲解Java类加载机制

Java 类加载机制是 Java 运行时的核心组成部分,负责在程序运行过程中动态加载和连接类文件,并将其转换为可执行代码。理解类加载机制,能更容易理解你一行行敲下的Java代码是如何在JVM虚拟机上运行起来。并且理解类加载机制之后,我们也能掌握如何自定义类加载器,如何做热更新等。

// 准备好了吗,要开始咯!(下图需要离远点看)
java

一、JVM如何启动

JVM启动

启动过程如下:

  • 配置JVM装载环境
    • 查找JVM.dll文件
    • 装载JVM.dll文件
  • 解析虚拟机参数
    • 参数解析
    • 参数验证
  • 设置线程栈大小
  • 执行main方法(jdk源码中java.c的JavaMain方法)
    • 创建JVM实例
    • 加载主类class(调用jvm的java层代码的loadClass)
    • 查找main方法
    • 执行main方法

二、类加载器

  1. 引导类加载器(Bootstrap ClassLoader)

加载路径:sun.boot.class.path

引导类加载器主要负责加载最最核心的java类型。 这些类库位于jre目录的lib目录下**. 比如:rt.jar, charset.jar等,

引导类加载器是由C++帮我们实现的, 然后c++语言会通过一个Launcher类将扩展类加载器(ExtClassLoader)和应用程序类加载器(AppClassLoader)构造出来, 并且把他们之间的关系构建好.

  1. 扩展类加载器(Ext ClassLoader)

加载路径:java.ext.dirs

扩展类加载器主要是用来加载扩展的jar包。 加载jar的目录位于jre目录的lib/ext扩展目录中的jar包

  1. 应用程序类加载器(App ClassLoader)

加载路径:java.class.path

主要是用来加载用户自己写的类的。 负责加载classPath路径下的类包

  1. 自定义类加载器

负责加载用户自定义路径下的类包

三、类加载过程

类加载过程

  1. 加载(Loading):把class文件加载到内存
  2. 链接(Linking)
    1. 验证(Verification):校验文件是否符合class规范
    2. 准备(Preparation):静态变量赋默认值
    3. 解析(Resolution):把类型方法属性等解析为直接引用
  3. 初始化(Initializing):静态变量赋初始值,调用静态代码块
  4. 使用
  5. 卸载

类加载机制:

  • 全盘委托机制:当ClassLoader加载类时,除非显示指定另一个ClassLoader,否则该类的引用和依赖也由这个ClassLoader载入
  • 双亲委派机制:ClassLoader在加载类时,会首先让父类去加载,只有当父类无法加载的时候,才会由子类来加载

四、双亲委派原则

双亲委派原则是指ClassLoader在类加载时,会自下而上询问父类是否加载,如果没有加载先由父类加载,父类加载不到再由其子类自上而下加载

双亲委派

双亲委派的好处是安全

相关源码:

// ClassLoader.class
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException
{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// 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 statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}
}

Class.forName和ClassLoader.loadClass的区别

  1. class.forName()将类的.class文件加载到jvm中后,还会对类进行解释,执行类中的static块。也可通过传参指定是否初始化
  2. loadClass只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块

jvm

五、类加载应用:热更新

1. ClassLoader热更新

自定义ClassLoader的子类(打破双亲委派原则),使用ClassLoader的defineClass即可加载新的byte数组覆盖原有的字节码

  1. 自定义ClassLoader
  2. 读取要热更的class文件并转换成byte数组
  3. 重写findClass方法并调用ClassLoader的defineClass

2. Instrument热更新

Java Instrumentation 是 JDK5 之后提供接口。使用这组接口,我们可以获取到正在运行 JVM 相关信息,使用这些信息我们构建相关监控程序检测 JVM。另外, 最重要我们可以替换修改类的,这样就实现了热更新。

Instrumentation提供premain和agentmain两种方式

1. premain方式

这种方式需要在虚拟机参数指定 Instrumentation 程序。使用方式如下:

java -javaagent:jar Instrumentation_jar -jar xxx.jar

并且在执行java的main方法之前,会先执行在mainfest中指定的premainClass中的类里的premain方法(需要提前定一个用于热更新的类,并加上premain方法)。之后就可以通过Instrumentation接口调用其中的redefineClasses方法来热更新类了

应用示例-热更新实现:

  1. 新建reload工程,定义热更新工具类ClassReloadUtils,并添加premain方法,缓存JVM层传进来的Instrumentation接口的实例
private static Instrumentation inst = null;
private static final Object LOCK = new Object();
private ClassReloadUtils() {
}
/*** 此方法由JAVA虚拟机调用* * @param agentArgs* @param ins*/
public static void premain(String agentArgs, Instrumentation ins) {synchronized (LOCK) {if (inst == null) {inst = ins;StringBuilder builder = new StringBuilder("[");builder.append(new Timestamp(System.currentTimeMillis()));builder.append("]-");builder.append(CLASS_RELOAD_OPEN_TIPS);System.out.println(builder.toString());}}
}
  1. reload的pom文件添加Premain-Class标签指定premain方法所在的类,并指定Can-Redefine-Classes为true
<build><finalName>mmo.reload</finalName><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>2.3.1</version><configuration><archive><manifestEntries><Premain-Class>com.xxx.ClassReloadUtils</Premain-Class><Can-Redefine-Classes>true</Can-Redefine-Classes></manifestEntries></archive></configuration></plugin></plugins>
</build>
  1. 用ClassLoader加载热更类,并把要热更的class文件读到byte数组,创建ClassDefinition类
String className = classFile.getName();
className = className.replace(CLASS_EXT, "");
// loadClass
Class<?> clasz = classLoader.loadClass(className);
byte[] bs = FileUtils.toByteArray(classFile);
return new ClassDefinition(clasz, bs);
  1. 使用启动时缓存的Instrumentation接口调用redefineClasses,并传入要热更的ClassDefinition类,完成热更
ClassDefinition[] definitions = classDefinitions.toArray(new ClassDefinition[classDefinitions.size()]);
try {inst.redefineClasses(definitions);
} catch (Exception e) {return ReloadResult.failed(String.format(CLASS_RELOAD_FAILED, e.getMessage()));
}

2. agentmain方式

arthas使用agentmain加attach方式实现动态监控以及动态修改字节码

不同于premain方式,agentmain允许在JVM启动之后进行代理,它的实现方式和premain类似,先定义一个用于热更新的类,并添加agentmain方法。接着读取外部传入 class 文件,调用 Instrumentation#redefineClasses,这个方法将会使用新 class 替换当前正在运行的 class,这样我们就完成了类的修改。

步骤如下:

  1. 创建热更代理工程,定义热更工具类AgentMain
  2. 类似premain方式,pom文件中添加指定工具类已经定义为可重定义class为true
<!--指定 class 名字-->
<Agent-Class>com.andyxh.AgentMain
</Agent-Class>
<Can-Redefine-Classes>true
</Can-Redefine-Classes>
  1. 在热更工具类AgentMain实现agentmain方法,在其中调用Instrumentation.redefineClasses完成热更逻辑

至此热更逻辑已经结束,后面则需要利用JVM提供的Attach功能把代理动态加进去

  1. 通过JVM的attach动态添加agent
System.out.println("当前热更新工具 jar 路径为 "+jarPath);
VirtualMachine vm = VirtualMachine.attach(pid);//7997是待绑定的jvm进程的pid号
// 运行最终 AgentMain 中方法
vm.loadAgent(jarPath, classPath);

其中的Attach原理:Attach API 位于 tools.jar 包,可以用来连接目标 JVM。Attach API 非常简单,内部只有两个主要的类,VirtualMachineVirtualMachineDescriptor

VirtualMachine 代表一个 JVM 实例, 使用它提供 attach 方法,我们就可以连接上目标 JVM。

 VirtualMachine vm = VirtualMachine.attach(pid);

VirtualMachineDescriptor 则是一个描述虚拟机的容器类,通过该实例我们可以获取到 JVM PID(进程 ID),该实例主要通过 VirtualMachine#list 方法获取。

for (VirtualMachineDescriptor descriptor : VirtualMachine.list()){System.out.println(descriptor.id());
}

java

3. 热更新的局限性

  • premain和agentmain均在类文件加载后,因此不能重新定义一个不存在类
  • 热更的类和旧的类继承的父类必须相同
  • 热更的类和旧的类继承的接口必须相同
  • 热更的类和旧的类的访问修饰符,字段必须相同
  • 热更的类和旧的类新增或删除的方法必须是private static/final修饰
  • 热更的类可以修改方法体

更多技术干货,欢迎关注我

qrcode

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Qt QAction添加图片

QAction用的时候&#xff0c;时常需要添加图片&#xff0c;如上图所示&#xff0c;代码如下所示&#xff1a; 测试的图片格式包含png,jpg,bmp,svg&#xff0c;其他未测试

C单片机数据类型与格式化

C语言数据类型 关键字位数表示范围stdint关键字ST关键字举例unsigned char80 ~ 255uint8_tu8u8 data 128char8-128 ~ 127int8_ts8s8 temperature 25unsigned short160 ~ 65535uint16_tu16u16 counter 5000short16-32768 ~ 32767int16_ts16s16 position 32767unsigned int3…

性能优化-如何提高cache命中率

本文主要介绍性能优化领域常见的cache的命中率问题&#xff0c;旨在全面的介绍提高cache命中率的方法&#xff0c;以供大家编写出性能友好的代码&#xff0c;并且可以应对性能优化领域的面试问题。 &#x1f3ac;个人简介&#xff1a;一个全栈工程师的升级之路&#xff01; &am…

湘潭大学-2023年下学期-c语言-作业0x0a-综合1

A 求最小公倍数 #include<stdio.h>int gcd(int a,int b) {return b>0?gcd(b,a%b):a; }int main() {int a,b;while(~scanf("%d%d",&a,&b)){if(a0&&b0) break;printf("%d\n",a*b/gcd(a,b));}return 0; }记住最大公约数的函数&…

Hampel滤波器是一种基于中位数的离群值检测方法【异常值检测方法】

Hampel滤波器是一种基于中位数的离群值检测方法&#xff0c;也是一种线性滤波器&#xff0c;由德国数学家和统计学家John Hampel在1974年提出。它主要用于去除信号中的脉冲噪声&#xff0c;具有很强的抗干扰能力&#xff0c;因此被广泛应用于信号处理、通信系统等领域。 1.基本…

操作系统期末复习(100道)

(单选题)在计算机系统中配置操作系统的主要目的是&#xff08; &#xff09;。 A. 增强计算机系统的功能 B. 提高系统资源的利用率 C. 提高系统的运行速度 D. 合理组织系统的工作流程&#xff0c;以提高系统吞吐量 (单选题)操作系统的主要功能是管理计算机系统中的&#xff08…

WPF 消息日志打印帮助类:HandyControl+NLog+彩色控制台打印+全局异常捕捉

文章目录 前言相关文章Nlog配置HandyControl配置简单使用显示效果文本内容 全局异常捕捉异常代码运行结果 前言 我将简单的HandyControl的消息打印系统和Nlog搭配使用&#xff0c;简化我们的代码书写 相关文章 .NET 控制台NLog 使用 WPF-UI HandyControl 控件简单实战 C#更改…

【Unity动画系统】Animator有限状态机参数详解

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;Uni…

Vue : 监视属性

目录 一个案例 监听属性 handler immediate vm.$watch(xxx) 深度监视 监视的简写 computed和watch之间的区别 一个案例 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport"…

RO-NeRF论文笔记

RO-NeRF论文笔记 文章目录 RO-NeRF论文笔记论文概述Abstract1 Introduction2 Related Work3 Method3.1 RGB and depth inpainting network3.2 Background on NeRFs3.3 Confidence-based view selection3.4 Implementation details 4 Experiments4.1 DatasetsReal ObjectsSynthe…

OpenCV 笔记(14):图像的轮廓和轮廓的基础特征

Part11. 图像的轮廓 在该系列第三篇文章中&#xff0c;曾经简单地介绍过轮廓和轮廓发现。 11.1 轮廓的基本概念 图像的轮廓是指图像中具有相同颜色或灰度值的连续点的曲线。轮廓和边缘是有联系的&#xff0c;边缘是轮廓的基础&#xff0c;轮廓是边缘的连续集合。 轮廓和边缘的…

Java进阶(第八期): Java中递归的的使用和递归解决一些算法问题 Java中的异常机制、异常的处理逻辑 自定义异常

文章目录 一、递归1.1 递归的介绍1.2 递归的简单练习1.3 图解递归执行流程&#xff1a;1.4 使用递归完成悲波那契数列1.5 猴子吃桃子问题 二、异常三 、异常的处理逻辑3.1 try catch 捕获异常3.2 throws抛出异常 四、自定义异常 Java进阶&#xff08;第八期&#xff09; 一、递…

java三种注释方式

Java 中的注释有三种&#xff1a; 单行注释&#xff1a;通常用于解释方法内某单行代码的作用。 多行注释&#xff1a;通常用于解释一段代码的作用。 文档注释&#xff1a;通常用于生成 Java 开发文档。

re:Invent 2023技术上新|Amazon DynamoDB与OpenSearch Service的Zero-ETL集成

Amazon DynamoDB 与 Amazon OpenSearch Service 的 Zero-ETL 集成已正式上线&#xff0c;该服务允许您通过自动复制和转换您的 DynamoDB 数据来搜索数据&#xff0c;而无需自定义代码或基础设施。这种 Zero-ETL 集成减少了运营负担和成本&#xff0c;使您能够专注于应用程序。这…

Redis事务悄然而至:命令的背后故事

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 Redis事务悄然而至&#xff1a;命令的背后故事 前言redis事务概述redis事务基础事务中的命令事务的一致性与隔离性事务的异常处理并发环境下的事务结语 前言 在数字化时代&#xff0c;数据处理的要求…

[C#]opencvsharp进行图像拼接普通拼接stitch算法拼接

介绍&#xff1a; opencvsharp进行图像拼一般有2种方式&#xff1a;一种是传统方法将2个图片上下或者左右拼接&#xff0c;还有一个方法就是融合拼接&#xff0c;stitch拼接就是一种非常好的算法。opencv里面已经有stitch拼接算法因此我们很容易进行拼接。 效果&#xff1a; …

Jmeter 性能测试 —— 评估一个系统TPS与并发数!

问题&#xff1a;性能压测&#xff0c;如何评估一个系统的TPS和并发数&#xff1f; 1、对于新系统 由业务部门或开发人员预估交易量和TPS指标 可以参考公式&#xff1a;并发用户 在线用户数 * 10%。 当一个系统还没有上线时&#xff0c;我们可以预判的是这个系统准备要给多…

一篇五分生信临床模型预测文章代码复现——Figure 10.机制及肿瘤免疫浸润(九)——ssGSEA——倒数第三节

之前讲过临床模型预测的专栏,但那只是基础版本,下面我们以自噬相关基因为例子,模仿一篇五分文章,将图和代码复现出来,学会本专栏课程,可以具备发一篇五分左右文章的水平: 本专栏目录如下: Figure 1:差异表达基因及预后基因筛选(图片仅供参考) Figure 2. 生存分析,…

车牌识别技术,如何用python识别车牌号

目录 一.前言 二.运行环境 三.代码 四.识别效果 五.参考 一.前言 车牌识别技术&#xff08;License Plate Recognition, LPR&#xff09;在交通计算机视觉&#xff08;Computer Vision, CV&#xff09;领域具有非常重要的研究意义。以下是该技术的一些扩展说明&#xff1…

VSCODE 修改Test模式下的的java jvm堆内存大小

在settings.json中添加如下语句 "java.test.config": {"vmArgs": ["-Xmx12G"]},