JVM 类加载机制详解
JVM 类加载机制详解
在 Java 虚拟机(JVM)中,类加载机制是一个非常重要的组成部分,它负责将类的字节码文件加载到内存中,并进行一系列的处理,最终使类能够被虚拟机使用。本文将详细介绍 JVM 类加载机制的相关内容。
一、类加载的概念
在 JVM 虚拟机实现规范中,通过 ClassLoader
类加载器把 *.class
字节码文件(文件流)加载到内存,并对字节码文件内容进行验证、准备、解析和初始化,最终形成可以被虚拟机直接使用的 java.lang.Class
对象,这个过程被称作类加载。类是在运行期间第一次使用时,被类加载器动态加载至 JVM。JVM 不会一次性加载所有类,因为这样会占用很多内存。
二、类的生命周期
类的生命周期包括以下 7 个阶段:
- 加载(Loading):通过类的完全限定名称获取定义该类的
*.class
字节码文件的二进制字节流,将其转换为运行时存储结构,并在内存中生成代表该类的Class
对象。 - 验证(Verification):确保
*.class
字节码文件中包含的信息符合当前虚拟机的要求,且不会危害虚拟机的安全,包括文件格式验证、元数据验证、字节码验证和符号引用验证。 - 准备(Preparation):为类变量分配内存并设置初始值(一般为 0 值,常量除外),实例变量不在此阶段分配内存。
- 解析(Resolution):将常量池的符号引用替换为直接引用。
- 初始化(Initialization):真正开始执行类中定义的 Java 程序代码,是虚拟机执行类构造器
<clinit>()
方法的过程。 - 使用(Using):程序对类进行实例化、调用其方法等操作。
- 卸载(Unloading):当类不再被使用时,由垃圾回收器进行卸载。
结束类生命周期的几种场景:
- 执行
System.exit()
方法。 - 程序正常执行结束。
- 程序执行中遇到了异常或错误而异常终止。
- 操作系统出现错误或强制结束程序而导致 JVM 虚拟机进程终止。
三、类加载过程
(一)加载
在加载阶段,JVM 主要完成以下 3 件事:
- 获取字节流:通过类的完全限定名称获取定义该类的
*.class
字节码文件的二进制字节流。转换存储结构:将该字节流表示的静态存储结构转换为 Metaspace 元空间区的运行时存储结构。 - 生成 Class 对象:在内存中生成一个代表该类的
Class
对象,作为元空间区中该类各种数据的访问入口。
*.class
字节码文件的加载方式有多种:
-
本地文件系统直接读取。
// 例如,可以从本地文件系统直接读取 FileInputStream fis = new FileInputStream("MyClass.class"); byte[] byteArray = new byte[fis.available()]; fis.read(byteArray); fis.close(); // 也可以从网络中通过服务器响应读取,如 Web Applet 技术 // 还可以从 JAR、EAR、WAR 等压缩文件中读取等
-
从网络中通过服务器响应读取,例如 Web Applet 技术。
-
从 JAR、EAR、WAR 等压缩文件中读取。
-
运行时通过动态代理技术生成字节码文件,例如在
java.lang.reflect.Proxy
使用ProxyGenerator.generateProxyClass
的代理类的二进制字节流。 -
由其他文件或容器生成,例如由 tomcat 将
*.jsp
文件翻译成*.java
文件后,编译生成对应的*.class
字节码文件。
在加载阶段完成之后,*.class
字节码文件的类信息数据就会存储在元空间,同时在 JVM 虚拟机堆区生成一个该类的 Class
对象。
(二)验证
验证阶段主要确保 *.class
字节码文件中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的安全。验证阶段会完成下面四个阶段的检验:
-
文件格式验证:验证字节流是否符合
*.class
字节码文件格式的规范,且能被当前版本的虚拟机处理。// 检查是否以魔数 0xCAFEBABE 开头 if (byteArray[0]!= 0xCA || byteArray[1]!= 0xFE || byteArray[2]!= 0xBA || byteArray[3]!= 0xBE) {throw new ClassFormatError("Invalid magic number in class file"); } // 检查主、次版本号是否在当前虚拟机处理范围之内 int majorVersion = (byteArray[4] << 8) | byteArray[5]; int minorVersion = (byteArray[6] << 8) | byteArray[7]; if (majorVersion > JVM_SUPPORTED_MAJOR_VERSION || (majorVersion == JVM_SUPPORTED_MAJOR_VERSION && minorVersion > JVM_SUPPORTED_MINOR_VERSION)) {throw new UnsupportedClassVersionError("Unsupported class file version"); } // 检查常量池的常量中是否有不被支持的常量类型(检查常量 tag 标志)等其他验证内容 //...
-
元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
// 检查这个类是否有父类(除了 java.lang.Object 之外,所有的类都应当有父类) if (className!= "java.lang.Object" &&!classHasSuperclass(byteArray)) {throw new ClassFormatError("Class has no superclass"); } // 检查这个类的父类是否继承了不允许被继承的类(被 final 修饰的类)等其他验证内容 //...
-
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
// 保证跳转指令不会跳转到方法体以外的字节码指令上 for (int i = 0; i < bytecodeLength; i++) {int opcode = byteArray[i] & 0xFF;if (opcode == JUMP_OPCODE) {int targetOffset = ((byteArray[i + 1] << 8) | byteArray[i + 2]) & 0xFFFF;if (targetOffset < 0 || targetOffset >= bytecodeLength) {throw new BytecodeVerificationError("Invalid jump target");}}// 检查其他字节码验证规则,如类型转换的有效性、操作数栈与指令的配合等//... }
-
符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段 —— 解析阶段中发生,确保解析动作能正常执行。
// 检查符号引用中通过字符串描述的全限定名是否能找到对应的类 String classNameFromSymbol = getClassNameFromSymbolReference(byteArray); try {Class.forName(classNameFromSymbol); } catch (ClassNotFoundException e) {throw new SymbolReferenceVerificationError("Class not found in symbol reference"); } // 检查在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段等验证内容 //...
为什么需要验证呢?Java 语言本身是相对安全的语言,但
*.class
字节码文件并不一定要求用 Java 源码编译而来,可以使用任何途径,甚至可用十六进制编译器直接编写来产生*.class
字节码文件。类的加载是 JVM 针对*.class
字节码文件的读取加载机制,所以虚拟机如果不检查输入的字节流,可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。另外,通过类加载机制的验证环节,可以增强解释器的运行期执行性能,因为解释器在运行期间无需再对每条执行指令进行检查。
(三)准备
类变量是被 static
修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是元空间区的内存。实例变量不会在这阶段分配内存,它会在对象实例化时,随着对象一起被分配在堆中。初始值一般为 0 值。
// 例如,下面的类变量 value 被初始化为 0 而不是 123
public static int value = 123;
// 如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0
public static final int CONSTANT_VALUE = 123;
(四)解析
将常量池的符号引用替换为直接引用。
(五)初始化
初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>()
方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
<clinit>()
是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。所以,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。
// 例如以下代码中静态变量 i 只能赋值,不能访问,因为 i 定义在静态代码块的后面
static {// 这里不能访问 i,会报错// System.out.println(i);
}
static int i = 10;
<clinit>
线程安全,虚拟机会保证一个类的 <clinit>()
方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>()
方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>()
方法完毕。如果在一个类的 <clinit>()
方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中,该阻塞非常隐蔽,几乎不会被察觉。
四、类加载的时机
(一)主动引用
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了只有下列六种情况必须对类进行加载:
-
当遇到
new
、getstatic
、putstatic
或invokestatic
这 4 条字节码指令时,比如new
一个对象,读取一个静态字段(未被final
修饰)、或调用一个类的静态方法时。// 当 jvm 执行 new 指令时会加载类,即当程序创建一个类的实例对象 MyClass obj = new MyClass(); // 当 jvm 执行 getstatic 指令时会加载类,即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) int staticValue = MyClass.staticVariable; // 当 jvm 执行 putstatic 指令时会加载类,即程序给类的静态变量赋值 MyClass.staticVariable = 10; // 当 jvm 执行 invokestatic 指令时会加载类,即程序调用类的静态方法 MyClass.staticMethod();
-
使用
java.lang.reflect
包的方法对类进行反射调用时如Class.forName("...")
,或newInstance()
等等。如果类没初始化,需要触发类的加载。try {Class<?> clazz = Class.forName("MyClass");Object instance = clazz.newInstance(); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {e.printStackTrace(); }
-
加载一个类,如果其父类还未加载,则先触发该父类的加载。
class ChildClass extends ParentClass {} // 当加载 ChildClass 时,如果 ParentClass 未加载,会先加载 ParentClass
-
当虚拟机启动时,用户需要定义一个要执行的主类(包含
main()
方法的类),虚拟机会先加载这个类。 -
当一个接口中定义了 JDK8 新加入的默认方法(被
default
关键字修饰的接口方法)时,如果有这个接口的实现类发生了加载,则该接口要在实现类之前被加载。
(二)被动引用
除主动引用之外,所有引用类的方式都不会触发加载,称为被动引用。
被动引用的常见例子包括:
-
通过子类引用父类的静态字段,不会导致子类加载。
class ParentClass {public static int staticField = 10; } class ChildClass extends ParentClass {} // 这里不会加载 ChildClass,只会加载 ParentClass int value = ChildClass.staticField;
-
通过数组定义来引用类,不会触发此类的加载。该过程会对数组类进行加载,数组类是一个由虚拟机自动生成的、直接继承自
Object
的子类,其中包含了数组的属性和方法。MyClass[] array = new MyClass[10]; // 这里只会加载数组类,不会加载 MyClass
-
常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的加载。
class ConstantClass {public static final int CONSTANT = 10; } class OtherClass {public static void main(String[] args) {int value = ConstantClass.CONSTANT;// 这里不会加载 ConstantClass} }
五、类加载器
(一)什么是类加载器
在类加载过程的加载阶段,通过类的完全限定名,获取描述类的二进制流的实现类,被称为 “类加载器”。
(二)类加载器分类
从 JVM 虚拟机的角度来讲,只存在以下两种不同的类加载器:
- 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机的一部分。它负责将存放在
<JRE_HOME>\lib
目录中的,或者被-Xbootclasspath
参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar
,名字不符合的类库即使放在lib
目录中也不会被加载)类库加载到虚拟机内存中。例如java.util.*
,java.io.*
,java.lang.*
类等常用基础库都是由启动类加载器加载。启动类加载器无法被 Java 程序直接引用。 - 其它类的加载器:使用 Java 实现,独立于虚拟机,继承自抽象类
java.lang.ClassLoader
。
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
- 扩展类加载器(Extension ClassLoader):该类加载器是由
ExtClassLoader
(sun.misc.Launcher$ExtClassLoader
)实现,负责将<JRE_HOME>/lib/ext
或者被java.ext.dir
系统变量所指定路径中的所有类库加载到内存中,例如swing
系列、内置的js
引擎、xml
解析器等以javax
开头的扩展类库都是由扩展类加载器加载,开发者可以直接使用扩展类加载器。 - 应用程序类加载器(Application ClassLoader):该类加载器是由
AppClassLoader
(sun.misc.Launcher$AppClassLoader
)实现。由于这个类加载器是ClassLoader
中的getSystemClassLoader()
方法的返回值,因此也被称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,比如我们自己编写的自定义类或第三方jar
包。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
(三)什么情况下需要自定义类加载器?
- 隔离加载类。在某些框架内进行中间件与应用的模块之间进行隔离,把类加载到不同的环境。
- 修改类加载方式。
- 扩展加载源。比如从数据库、网络、电视机顶盒进行类加载。
- 防止源码泄漏。比如编译时字节码进行加密,需要通过自定义类加载器对字节码进行解密还原。
六、双亲委派模型
应用程序是由三种类加载器互相配合,从而实现类加载,除此之外还可以加入自己定义的类加载器。类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
(一)双亲委派工作机制
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
(二)双亲委派的作用
- 每个类只会加载一次,解决了各个类加载器加载基础类的统一问题(基础类库由上层的加载器进行加载)。
- 防止恶意破坏的类加载,内存中不会出现多份同样的字节码的系统类,保证 Java 程序安全稳定运行。
例如:
java.lang.Object
存放在rt.jar
中,如果编写另外一个java.lang.Object
并放到ClassPath
中,程序可以 - 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机的一部分。它负责将存放在
相关文章:
JVM 类加载机制详解
JVM 类加载机制详解 在 Java 虚拟机(JVM)中,类加载机制是一个非常重要的组成部分,它负责将类的字节码文件加载到内存中,并进行一系列的处理,最终使类能够被虚拟机使用。本文将详细介绍 JVM 类加载机制的相…...

在 JavaScript 中,`Array.prototype.filter` 方法用于创建一个新数组,该数组包含通过测试的所有元素
文章目录 1、概念在你的代码中的作用示例总结 2、实战3、formattedProducts4、filteredProducts 1、概念 在 JavaScript 中,Array.prototype.filter 方法用于创建一个新数组,该数组包含通过测试的所有元素。具体来说,filter 方法会遍历数组中…...

63 mysql 的 行锁
前言 我们这里来说的就是 我们在 mysql 这边常见的 几种锁 行共享锁, 行排他锁, 表意向共享锁, 表意向排他锁, 表共享锁, 表排他锁 意向共享锁, 意向排他锁, 主要是 为了表粒度的锁获取的同步判断, 提升效率 意向共享锁, 意向排他锁 这边主要的逻辑意义是数据表中是否有任…...
ubuntu文件编辑操作
Vim 基本操作指南 在 vim 中打开文件后,可以按照以下步骤进行编辑和保存: 进入插入模式 打开文件后,默认情况下 vim 处于命令模式,无法直接输入文本。按下 i 键进入插入模式(会看到左下角显示 -- INSERT --࿰…...

Nuxt.js 应用中的 nitro:config 事件钩子详解
title: Nuxt.js 应用中的 nitro:config 事件钩子详解 date: 2024/11/2 updated: 2024/11/2 author: cmdragon excerpt: nitro:config 是 Nuxt 3 中的一个生命周期钩子,允许开发者在初始化 Nitro 之前自定义 Nitro 的配置。Nitro 是 Nuxt 3 的服务器引擎,负责处理请求、渲…...
【前端】项目中遇到的问题汇总(长期更新)
一、联调交互类 1、出现一个数据在当前页面进行了修改,另外一个页面的同一数据并未同步更改 当前的数据经过调用接口修改更新以后,if(code 200) 将当前数据存入store.dispatch, 然后另一个地方获取该数据,直接获取存入的数据,这…...

DAY73WEB 攻防-支付逻辑篇篡改属性值并发签约越权盗用算法溢出替换对冲
知识点: 1、支付逻辑-商品本身-修改-数量&价格&属性等 2、支付逻辑-营销折扣-优惠券&积分&签约&试用等 3、支付逻辑-订单接口-替换&并发&状态值&越权支付等 支付逻辑常见测试: 熟悉常见支付流程:选择商品…...

2024 Rust现代实用教程:Ownership与结构体、枚举
文章目录 一、Rust的内存管理模型1.GC(Stop the world)2.C/C内存错误大全3.Rust的内存管理模型 二、String与&str1.String与&str如何选择2.Example 三、枚举与匹配模式1.常见的枚举类型:Option和Result2.匹配模式 四、结构体、方法、…...

MMed-RAG:专为医学视觉语言模型设计的多功能多模态系统
MMed-RAG:专为医学视觉语言模型设计的多功能多模态系统 论文大纲提出背景全流程优化空雨伞分析空:观察现象层雨:分析原因层伞:解决方案层 三问分析WHAT - 问题是什么?WHY - 原因是什么?HOW - 如何解决&…...
数据采集(全量采集和增量采集)
全量采集:采集全部数据 3、全量采集 vim students_all.json {"job": {"setting": {"speed": {"channel": 1},"errorLimit": {"record": 0,"percentage": 0.02}},"content": [{…...

GPT-Sovits-1-数据处理
1.1 切割音频 将音频切割为多个10s内的片段 1.2 降噪 这一步用的是modelscope的pipeline 如果要去除背景音,可以用傅立叶转为为频谱,去除低频部分后再转回来 1.3 提取音频特征 这里用到了 funasr 库 这一步目的是输出音频样本的《文本标签文件》&am…...

web前端多媒体标签设置(图片,视频,音频)以及图片热区(usemap)的设置
多媒体标签运用 在HTML中有以下常见多媒体标签: <img> (图像标签) - 作用:用于在网页中嵌入图像。 - 示例: <img src"image.jpg" alt"这是一张图片"> 。其中 src 属性指定图像的…...
尚硅谷react教程_扩展_stateHook
1.类式组件写 import React, {Component} from react;export default class Demo extends Component {state {count:0}add () > {this.setState(state>({count:state.count1}))}render() {return (<div><h2>当前求和为{this.state.count}</h2><b…...

专线物流公共服务平台:数据驱动,标准引领,共创金融双赢新时代
专线物流公共服务平台:数据驱动,标准引领,共创金融双赢新时代 在当今这个数据驱动、标准引领、金融赋能的经济发展新时代,专线物流作为商贸流通领域的重要一环,正面临着前所未有的机遇与挑战。为应对复杂多变的市场环…...

界面控件DevExpress JS ASP.NET Core v24.1亮点 - 支持Angular 18
DevExtreme拥有高性能的HTML5 / JavaScript小部件集合,使您可以利用现代Web开发堆栈(包括React,Angular,ASP.NET Core,jQuery,Knockout等)构建交互式的Web应用程序。从Angular和Reac,…...

Spring之依赖注入(DI)和控制反转(IoC)——配置文件、纯注解
依赖注入 依赖注入(Dependency Injection,简称 DI)与控制反转(loC)的含义相同,只不过这两 个称呼是从两个角度描述的同一个概念。对于一个 Spring 初学者来说,这两种称呼很难理解, 下面我们将通过简单的语言来描述这两个概念。 当Java对象&…...

基于SpringBoot的宠物健康咨询系统的设计与实现
摘 要 传统信息的管理大部分依赖于管理人员的手工登记与管理,然而,随着近些年信息技术的迅猛发展,让许多比较老套的信息管理模式进行了更新迭代,宠物健康知识信息因为其管理内容繁杂,管理数量繁多导致手工进行处理不…...

Lucene的使用方法与Luke工具(2)
文章目录 第2章 Lucene快速入门2.1 项目搭建2.1.1 SQL语句2.1.2 maven依赖2.1.3 实体类:2.1.4 编写DAO: 2.2 建立索引2.2.1 步骤:2.2.2 实现代码: 2.3 Luke工具2.3.1 运行界面介绍:1)主界面2)文…...
【客户端开发】electron 中无法使用 js-cookie 的问题
产生问题的原因 谷歌浏览器升级之后,出于安全考虑,cookie的SameSite属性默认值由None变为Lax,对于跨域的请求,禁止携带cookie。electron内核是chromium内核,所以也会有这个限制。 Cookie的SameSite属性用来限制第三方 Cookie&…...
kafka客户端消费者吞吐量优化
问题背景 业务场景 mq消息消费实时性要求不高,期望可以牺牲一部分实时性,换取吞吐量,例如:数据库单条insert优化为batchInsert。优化后结果不符合预期:消费者消费消息的batchSize远小于实际配置的max.poll.records&a…...
使用Caddy在Ubuntu 22.04上配置HTTPS反向代理
使用Caddy在Ubuntu 22.04上配置HTTPS反向代理(无域名/IP验证+密码保护) 一、 环境说明 环境说明:测试环境,生产环境请谨慎OS: Ubuntu 22.04.1 LTSCaddy版本:v2.10.0服务器IP: 192.168.3.88(内网)公网IP: 10.2.3.11(测试虚拟)代理端口: 9080后端服务: http://192.168.3…...
ps蒙版介绍
一、蒙版的类型 Photoshop中有多种蒙版类型,每种适用于不同的场景: 图层蒙版(Layer Mask) 作用:控制图层的可见性,黑色隐藏、白色显示、灰色半透明。特点:可随时编辑,适合精细调整。…...
Qwen大语言模型里,<CLS>属于特殊的标记:Classification Token
Qwen大语言模型里,<CLS>属于特殊的标记:Classification Token 目录 Qwen大语言模型里,<CLS>属于特殊的标记:Classification Token功能解析工作机制应用场景举例说明技术要点在自然语言处理(NLP)领域 都是<CLS> + <SEP>吗?一、CLS和SEP的作用与常见用法1. **CLS标…...
Faiss向量数据库全面解析:从原理到实战
Faiss向量数据库全面解析:从原理到实战 引言:向量搜索的时代需求 在AI技术爆发的今天,向量数据已成为表示文本、图像、音视频等内容的核心形式。Facebook AI研究院开源的Faiss(Facebook AI Similarity Search)作为高…...
Langchain学习笔记(十一):Chain构建与组合技巧
注:本文是Langchain框架的学习笔记;不是教程!不是教程!内容可能有所疏漏,欢迎交流指正。后续将持续更新学习笔记,分享我的学习心得和实践经验。 前言 在LangChain的发展过程中,API设计经历了重…...

Linux中su与sudo命令的区别:权限管理的关键差异解析
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storms…...

鸿蒙仓颉语言开发实战教程:购物车页面
大家上午好,仓颉语言商城应用的开发进程已经过半,不知道大家通过这一系列的教程对仓颉开发是否有了进一步的了解。今天要分享的购物车页面: 看到这个页面,我们首先要对它简单的分析一下。这个页面一共分为三部分,分别是…...
图像处理、图像分析和图像理解的定义、联系与区别
1. 定义 图像处理(Image Processing) 图像处理是低层操作,主要针对像素级的图像数据进行加工,目的是改善图像质量或为后续分析做准备。 典型任务:去噪、增强(如对比度调整)、锐化、边缘检测、图…...

Vue3学习(4)- computed的使用
1. 简述与使用 作用:computed 用于基于响应式数据派生出新值,其值会自动缓存并在依赖变化时更新。 缓存机制:依赖未变化时直接返回缓存值,避免重复计算(通过 _dirty 标志位实现)。响应式更新&…...

声音信号的基频检测(python版本)
import math import wave import array import functools from abc import ABC, abstractmethod import matplotlib import matplotlib.pyplot as plt from matplotlib.gridspec import GridSpec import os import sys# 设计模式部分 class PreprocessStrategy(ABC):"&q…...