Android运行时ART加载类和方法的过程分析

目录

一,概述

二,ART运行时的入口


一,概述

既然ART运行时执行的都是翻译DEX字节码后得到的本地机器指令了,为什么还需要在OAT文件中包含DEX文件,并且将它加载到内存去呢?这是因为ART运行时提供了Java虚拟机接口,而要实现Java虚拟机接口不得不依赖于DEX文件

ART运行时查找类方法的本地机器指令的过程

为了方便描述,我们将DEX文件中描述的类和方法称为DEX类(Dex Class)和DEX方法(Dex Method),而将在OAT文件中描述的类和方法称为OAT类(Oat Class)和OAT方法(Oat Method)。接下来我们还会看到,ART运行时在内部又会使用另外两个不同的术语来描述类和方法,其中将类描述为Class,而将类方法描述为ArtMethod。

       在图1中,为了找到一个类方法的本地机器指令,我们需要执行以下的操作:

       1. 在DEX文件中找到目标DEX类的编号,并且以这个编号为索引,在OAT文件中找到对应的OAT类。

       2. 在DEX文件中找到目标DEX方法的编号,并且以这个编号为索引,在上一步找到的OAT类中找到对应的OAT方法。

       3. 使用上一步找到的OAT方法的成员变量begin_和code_offset_,计算出该方法对应的本地机器指令

二,ART运行时的入口

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{ALOGD(">>>>>> START %s uid %d <<<<<<\n",className != NULL ? className : "(unknown)", getuid());static const String8 startSystemServer("start-system-server");/** 'startSystemServer == true' means runtime is obsolete and not run from* init.rc anymore, so we print out the boot start event here.*/for (size_t i = 0; i < options.size(); ++i) {if (options[i] == startSystemServer) {/* track our progress through the boot sequence */const int LOG_BOOT_PROGRESS_START = 3000;LOG_EVENT_LONG(LOG_BOOT_PROGRESS_START,  ns2ms(systemTime(SYSTEM_TIME_MONOTONIC)));}}const char* rootDir = getenv("ANDROID_ROOT");if (rootDir == NULL) {rootDir = "/system";if (!hasDir("/system")) {LOG_FATAL("No root directory specified, and /android does not exist.");return;}setenv("ANDROID_ROOT", rootDir, 1);}//const char* kernelHack = getenv("LD_ASSUME_KERNEL");//ALOGD("Found LD_ASSUME_KERNEL='%s'\n", kernelHack);/* start the virtual machine */JniInvocation jni_invocation;jni_invocation.Init(NULL);JNIEnv* env;if (startVm(&mJavaVM, &env, zygote) != 0) {return;}onVmCreated(env);/** Register android functions.*/if (startReg(env) < 0) {‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’
}

首先是通过调用函数startVm创建了一个Java虚拟机mJavaVM及其JNI接口env。这个Java虚拟机实际上就是ART运行时。在接下来的描述中,我们将不区分ART虚拟机和ART运行时,并且认为它们表达的是同一个概念。获得了ART虚拟机的JNI接口之后,就可以通过它提供的函数FindClass和GetStaticMethodID来加载com.android.internal.os.ZygoteInit类及其静态成员函数main。于是,最后就可以再通过JNI接口提供的函数CallStaticVoidMethod来调用com.android.internal.os.ZygoteInit类的静态成员函数main,以及进行到ART虚拟机里面去运行。
 在分析JNI接口FindClass和GetStaticMethodID的实现之前,我们先要讲清楚JNI接口是如何创建的

extern "C" jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {return JniInvocation::GetJniInvocation().JNI_CreateJavaVM(p_vm, p_env, vm_args);
}

JNI类的静态成员函数FindClass的实现如下所示

/Volumes/aosp/android-8.1.0_r52/art/runtime/jni_internal.cc

static jclass FindClass(JNIEnv* env, const char* name) {CHECK_NON_NULL_ARGUMENT(name);Runtime* runtime = Runtime::Current();ClassLinker* class_linker = runtime->GetClassLinker();std::string descriptor(NormalizeJniClassDescriptor(name));ScopedObjectAccess soa(env);mirror::Class* c = nullptr;if (runtime->IsStarted()) {StackHandleScope<1> hs(soa.Self());Handle<mirror::ClassLoader> class_loader(hs.NewHandle(GetClassLoader(soa)));c = class_linker->FindClass(soa.Self(), descriptor.c_str(), class_loader);} else {c = class_linker->FindSystemClass(soa.Self(), descriptor.c_str());}return soa.AddLocalReference<jclass>(c);}

NI类的静态成员函数FindClass首先是判断ART运行时是否已经启动起来。如果已经启动,那么就通过调用函数GetClassLoader来获得当前线程所关联的ClassLoader,并且以此为参数,调用前面获得的ClassLinker对象的成员函数FindClass来加载由参数name指定的类。一般来说,当前线程所关联的ClassLoader就是当前正在执行的类方法所关联的ClassLoader,即用来加载当前正在执行的类的ClassLoader。如果ART虚拟机还没有开始执行类方法,就像我们现在这个场景,那么当前线程所关联的ClassLoader实际上就系统类加载器,即SystemClassLoader。

       如果ART运行时还没有启动,那么这时候只可以加载系统类。这个通过前面获得的ClassLinker对象的成员函数FindSystemClass来实现的。在我们这个场景中,ART运行时已经启动,因此,接下来我们就继续分析ClassLinker类的成员函数FindClass的实现。

        ClassLinker类的成员函数FindClass的实现如下所示:


 /Volumes/aosp/android-8.1.0_r52/art/runtime/class_linker.cc

mirror::Class* ClassLinker::FindClass(Thread* self,const char* descriptor,Handle<mirror::ClassLoader> class_loader) {DCHECK_NE(*descriptor, '\0') << "descriptor is empty string";DCHECK(self != nullptr);self->AssertNoPendingException();self->PoisonObjectPointers();  // For DefineClass, CreateArrayClass, etc...if (descriptor[1] == '\0') {// only the descriptors of primitive types should be 1 character long, also avoid class lookup// for primitive classes that aren't backed by dex files.return FindPrimitiveClass(descriptor[0]);}const size_t hash = ComputeModifiedUtf8Hash(descriptor);// Find the class in the loaded classes table.ObjPtr<mirror::Class> klass = LookupClass(self, descriptor, hash, class_loader.Get());if (klass != nullptr) {return EnsureResolved(self, descriptor, klass);}// Class is not yet loaded.if (descriptor[0] != '[' && class_loader == nullptr) {// Non-array class and the boot class loader, search the boot class path.ClassPathEntry pair = FindInClassPath(descriptor, hash, boot_class_path_);if (pair.second != nullptr) {return DefineClass(self,descriptor,hash,ScopedNullHandle<mirror::ClassLoader>(),*pair.first,*pair.second);} else {// The boot class loader is searched ahead of the application class loader, failures are// expected and will be wrapped in a ClassNotFoundException. Use the pre-allocated error to// trigger the chaining with a proper stack trace.ObjPtr<mirror::Throwable> pre_allocated =Runtime::Current()->GetPreAllocatedNoClassDefFoundError();self->SetException(pre_allocated);return nullptr;}}ObjPtr<mirror::Class> result_ptr;bool descriptor_equals;if (descriptor[0] == '[') {result_ptr = CreateArrayClass(self, descriptor, hash, class_loader);DCHECK_EQ(result_ptr == nullptr, self->IsExceptionPending());DCHECK(result_ptr == nullptr || result_ptr->DescriptorEquals(descriptor));descriptor_equals = true;} else {ScopedObjectAccessUnchecked soa(self);bool known_hierarchy =FindClassInBaseDexClassLoader(soa, self, descriptor, hash, class_loader, &result_ptr);if (result_ptr != nullptr) {// The chain was understood and we found the class. We still need to add the class to// the class table to protect from racy programs that can try and redefine the path list// which would change the Class<?> returned for subsequent evaluation of const-class.DCHECK(known_hierarchy);DCHECK(result_ptr->DescriptorEquals(descriptor));descriptor_equals = true;} else {// Either the chain wasn't understood or the class wasn't found.//// If the chain was understood but we did not find the class, let the Java-side// rediscover all this and throw the exception with the right stack trace. Note that// the Java-side could still succeed for racy programs if another thread is actively// modifying the class loader's path list.if (!self->CanCallIntoJava()) {// Oops, we can't call into java so we can't run actual class-loader code.// This is true for e.g. for the compiler (jit or aot).ObjPtr<mirror::Throwable> pre_allocated =Runtime::Current()->GetPreAllocatedNoClassDefFoundError();self->SetException(pre_allocated);return nullptr;}
''''''''''''''''''''''''''''''''''''''''''''''''''''''''
}

ClassLinker类的成员函数FindClass首先是调用另外一个成员函数LookupClass来检查参数descriptor指定的类是否已经被加载过。如果是的话,那么ClassLinker类的成员函数LookupClass就会返回一个对应的Class对象,这个Class对象接着就会返回给调用者,表示加载已经完成
 

知道了参数descriptor指定的类定义在哪一个DEX文件之后,就可以通过ClassLinker类的另外一个成员函数DefineClass来从中加载它了。接下来,我们就继续分析ClassLinker类的成员函数DefineClass的实现

mirror::Class* ClassLinker::DefineClass(Thread* self,const char* descriptor,size_t hash,Handle<mirror::ClassLoader> class_loader,const DexFile& dex_file,const DexFile::ClassDef& dex_class_def) {StackHandleScope<3> hs(self);auto klass = hs.NewHandle<mirror::Class>(nullptr);// Load the class from the dex file.if (UNLIKELY(!init_done_)) {// finish up init of hand crafted class_roots_if (strcmp(descriptor, "Ljava/lang/Object;") == 0) {klass.Assign(GetClassRoot(kJavaLangObject));} else if (strcmp(descriptor, "Ljava/lang/Class;") == 0) {klass.Assign(GetClassRoot(kJavaLangClass));} else if (strcmp(descriptor, "Ljava/lang/String;") == 0) {klass.Assign(GetClassRoot(kJavaLangString));} else if (strcmp(descriptor, "Ljava/lang/ref/Reference;") == 0) {klass.Assign(GetClassRoot(kJavaLangRefReference));} else if (strcmp(descriptor, "Ljava/lang/DexCache;") == 0) {klass.Assign(GetClassRoot(kJavaLangDexCache));} else if (strcmp(descriptor, "Ldalvik/system/ClassExt;") == 0) {klass.Assign(GetClassRoot(kDalvikSystemClassExt));}}if (klass == nullptr) {// Allocate a class with the status of not ready.// Interface object should get the right size here. Regular class will// figure out the right size later and be replaced with one of the right// size when the class becomes resolved.klass.Assign(AllocClass(self, SizeOfClassWithoutEmbeddedTables(dex_file, dex_class_def)));}if (UNLIKELY(klass == nullptr)) {self->AssertPendingOOMException();return nullptr;}

调用ClassLinker类的成员函数DefineClass的时候,如果ClassLinker正处于初始化过程,即其成员变量init_done_的值等于false,并且参数descriptor描述的是特定的内部类,那么就将本地变量klass指向它们,其余情况则会通过成员函数AllocClass为其分配存储空间,以便后面通过成员函数LoadClass进行初始化
 

接下来,我们主要分析ClassLinker类的成员函数LoadClass的实现,以便可以了解类的加载过程。

        ClassLinker类的成员函数LoadClass的实现如下所示

void ClassLinker::LoadClass(Thread* self,const DexFile& dex_file,const DexFile::ClassDef& dex_class_def,Handle<mirror::Class> klass) {const uint8_t* class_data = dex_file.GetClassData(dex_class_def);if (class_data == nullptr) {return;  // no fields or methods - for example a marker interface}LoadClassMembers(self, dex_file, class_data, klass);
}
void ClassLinker::LoadClassMembers(Thread* self,const DexFile& dex_file,const uint8_t* class_data,Handle<mirror::Class> klass) {{// Note: We cannot have thread suspension until the field and method arrays are setup or else// Class::VisitFieldRoots may miss some fields or methods.ScopedAssertNoThreadSuspension nts(__FUNCTION__);// Load static fields.// We allow duplicate definitions of the same field in a class_data_item// but ignore the repeated indexes here, b/21868015.LinearAlloc* const allocator = GetAllocatorForClassLoader(klass->GetClassLoader());ClassDataItemIterator it(dex_file, class_data);LengthPrefixedArray<ArtField>* sfields = AllocArtFieldArray(self,allocator,it.NumStaticFields());size_t num_sfields = 0;uint32_t last_field_idx = 0u;for (; it.HasNextStaticField(); it.Next()) {uint32_t field_idx = it.GetMemberIndex();DCHECK_GE(field_idx, last_field_idx);  // Ordering enforced by DexFileVerifier.if (num_sfields == 0 || LIKELY(field_idx > last_field_idx)) {DCHECK_LT(num_sfields, it.NumStaticFields());LoadField(it, klass, &sfields->At(num_sfields));++num_sfields;last_field_idx = field_idx;}}// Load instance fields.LengthPrefixedArray<ArtField>* ifields = AllocArtFieldArray(self,allocator,it.NumInstanceFields());size_t num_ifields = 0u;last_field_idx = 0u;for (; it.HasNextInstanceField(); it.Next()) {uint32_t field_idx = it.GetMemberIndex();DCHECK_GE(field_idx, last_field_idx);  // Ordering enforced by DexFileVerifier.if (num_ifields == 0 || LIKELY(field_idx > last_field_idx)) {DCHECK_LT(num_ifields, it.NumInstanceFields());LoadField(it, klass, &ifields->At(num_ifields));++num_ifields;last_field_idx = field_idx;}}
'''''''''''''''
}

们首先要明确一下各个参数的含义:

       dex_file: 类型为DexFile,描述要加载的类所在的DEX文件。

       dex_class_def: 类型为ClassDef,描述要加载的类在DEX文件里面的信息。

       klass: 类型为Class,描述加载完成的类。

       class_loader:  类型为ClassLoader,描述所使用的类加载器
 ClassLinker类的成员函数LoadClass的任务就是要用dex_file、dex_class_def、class_loader三个参数包含的相关信息设置到参数klass描述的Class对象去,以便可以得到一个完整的已加载类信息。

static jmethodID FindMethodID(ScopedObjectAccess& soa, jclass jni_class,const char* name, const char* sig, bool is_static)REQUIRES_SHARED(Locks::mutator_lock_) {ObjPtr<mirror::Class> c = EnsureInitialized(soa.Self(), soa.Decode<mirror::Class>(jni_class));if (c == nullptr) {return nullptr;}ArtMethod* method = nullptr;auto pointer_size = Runtime::Current()->GetClassLinker()->GetImagePointerSize();if (c->IsInterface()) {method = c->FindInterfaceMethod(name, sig, pointer_size);} else {method = c->FindClassMethod(name, sig, pointer_size);}if (method == nullptr || method->IsStatic() != is_static) {ThrowNoSuchMethodError(soa, c, name, sig, is_static ? "static" : "non-static");return nullptr;}return jni::EncodeArtMethod(method);
}

函数FindMethodID的执行过程如下所示:

       1. 将参数jni_class的值转换为一个Class指针c,因此就可以得到一个Class对象,并且通过ClassLinker类的成员函数EnsureInitialized确保该Class对象描述的类已经初始化。

       2. Class对象c描述的类在加载的过程中,经过解析已经关联上一系列的成员函数。这些成员函数可以分为两类:Direct和Virtual。Direct类的成员函数包括所有的静态成员函数、私有成员函数和构造函数,而Virtual则包括所有的虚成员函数。因此:

           2.1. 当参数is_static的值等于true时,那么就表示要查找的是静态成员函数,这时候就在Class对象c描述的类的关联的Direct成员函数列表中查找参数name和sig对应的成员函数。这是通过调用Class类的成员函数FindDirectMethod来实现的。

           2.2. 当参数is_static的值不等于true时,那么就表示要查找的是虚拟成员函数或者非静态的Direct成员函数,这时候先在Class对象c描述的类的关联的Virtual成员函数列表中查找参数name和sig对应的成员函数。这是通过调用Class类的成员函数FindVirtualMethod来实现的。如果找不到对应的虚拟成员函数,那么再在Class对象c描述的类的关联的Direct成员函数列表中查找参数name和sig对应的成员函数。

       3. 经过前面的查找过程,如果都不能在Class对象c描述的类中找到与参数name和sig对应的成员函数,那么就抛出一个NoSuchMethodError异常。否则的话,就将查找得到的ArtMethod对象封装成一个jmethodID值返回给调用者。

       也就是说,我们通过调用JNI接口GetStaticMethodID获得的不透明jmethodID值指向的实际上是一个ArtMethod对象。得益于前面的类加载过程,当我们获得了一个ArtMethod对象之后,就可以轻松地得到它的本地机器指令入口,进而对它进行执行
 

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

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

相关文章

Javase 基础加强 —— 02 泛型

本系列为笔者学习Javase的课堂笔记&#xff0c;视频资源为B站黑马程序员出品的《黑马程序员JavaAI智能辅助编程全套视频教程&#xff0c;java零基础入门到大牛一套通关》&#xff0c;章节分布参考视频教程&#xff0c;为同样学习Javase系列课程的同学们提供参考。 01 认识泛型…

Oracle VirtualBox 在 macOS 上的详细安装步骤

Oracle VirtualBox 在 macOS 上的详细安装步骤 一、准备工作1. 系统要求2. 下载安装包二、安装 VirtualBox1. 挂载安装镜像2. 运行安装程序3. 处理安全限制(仅限首次安装)三、安装扩展包(增强功能)四、配置第一个虚拟机1. 创建新虚拟机2. 分配内存3. 创建虚拟硬盘4. 加载系…

RAGFlow 接入企业微信应用实现原理剖析与最佳实践

背景 近期有医美行业客户咨询我们智能客服产品&#xff0c;期望将自己企业的产品、服务以及报价信息以企微应用的方式给到客户进行体验互动&#xff0c;提升企业运营效率。关于企业微信对接&#xff0c;我们分享下最佳实践&#xff0c;抛砖引玉。效果图如下&#xff1a; 这里也…

【心海资源】子比主题新增注册与会员用户展示功能模块及实现方法

内容改写&#xff1a; 本次分享的是子比主题顶部展示注册用户与会员信息的功能模块及其实现方式。 你可以通过两种方式启用该功能&#xff1a; 直接在后台进入“外观 → 小工具”启用该展示模块&#xff0c;操作简便&#xff1b;也可将提供的代码覆盖至子比主题目录中&#…

CSDN积分详解(介绍、获取、用途)

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 积分**一、积分类型及用途****二、积分获取途…

【iview】es6变量结构赋值(对象赋值)

变量的解构赋值 以iview的src/index.js中Vue.prototype.$IVIEW改造为例练习下怎么使用变量的解构赋值 原来的写法&#xff1a; const install function(Vue, opts {}) {if (install.installed) return;locale.use(opts.locale);locale.i18n(opts.i18n);Object.keys(iview).fo…

【c++深入系列】:万字详解vector(附模拟实现的vector源码)

&#x1f525; 本文专栏&#xff1a;c &#x1f338;作者主页&#xff1a;努力努力再努力wz &#x1f4aa; 今日博客励志语录&#xff1a; 种子破土时从不问‘会不会有光’&#xff0c;它只管生长 ★★★ 本文前置知识&#xff1a; 模版 1.什么是vector 那么想必大家都学过顺…

MySQL基础关键_007_DQL 练习

目 录 一、题目 二、答案&#xff08;不唯一&#xff09; 1.查询每个部门薪资最高的员工信息 2.查询每个部门高于平均薪水的员工信息 3. 查询每个部门平均薪资等级 4.查询部门中所有员工薪资等级的平均等级 5.不用分组函数 max 查询最高薪资 6.查询平均薪资最高的部门编…

Jenkis安装、配置及账号权限分配保姆级教程

Jenkis安装、配置及账号权限分配保姆级教程 安装Jenkins下载Jenkins启动Jenkins配置Jenkins入门Jenkins配置配置中文配置前端自动化任务流新建任务拉取代码打包上传云服务并运行配置后端自动化任务流新建任务拉取代码打包上传云服务并运行账号权限分配创建用户分配视图权限安装…

虚函数 vs 纯虚函数 vs 静态函数(C++)

&#x1f9e9; 一图看懂&#xff1a;虚函数 vs 纯虚函数 特性虚函数&#xff08;Virtual&#xff09;纯虚函数&#xff08;Pure Virtual&#xff09;语法virtual void foo();virtual void foo() 0;是否必须实现✅ 必须在类中实现❌ 不在基类实现&#xff0c;派生类必须实现是…

2025年渗透测试面试题总结-拷打题库36(题目+回答)

网络安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 2025年渗透测试面试题总结-拷打题库36 PHP代码常见入口函数查找 PHP框架路由方法熟悉度 PHP变量覆盖…

STL之vector容器

vector的介绍 1.vector是可变大小数组的容器 2.像数组一样&#xff0c;采用连续的空间存储&#xff0c;也就意味着可以通过下标去访问&#xff0c;但它的大小可以动态改变 3.每次的插入都要开空间吗&#xff1f;开空间就要意味着先开临时空间&#xff0c;然后在拷贝旧的到新…

[学成在线]22-自动部署项目

自动部署 实战流程 下边使用jenkins实现CI/CD的流程。 1、将代码使用Git托管 2、在jenkins创建任务&#xff0c;从Git拉取代码。 3、拉取代码后进行自动构建&#xff1a;测试、打包、部署。 首先将代码打成镜像包上传到docker私服。 自动创建容器、启动容器。 4、当有代…

74HC123的电路应用场景

74HC123的电路应用场景 **1. 引脚功能示例****2. 核心功能****&#xff08;1&#xff09;单稳态触发器****&#xff08;2&#xff09;双独立通道****&#xff08;3&#xff09;灵活触发方式** **3. 工作原理****4. 典型应用场景****&#xff08;1&#xff09;定时与延时控制***…

【人工智能】大模型安全的深度剖析:DeepSeek漏洞分析与防护实践

《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 随着大语言模型(LLM)的广泛应用,其安全性问题日益凸显。DeepSeek作为中国领先的开源AI模型,以低成本和高性能著称,但近期暴露的数据库…

《ESP32音频开发实战:I2S协议解析与WAV音频录制/播放全指南》

前言 在智能硬件和物联网应用中&#xff0c;音频处理能力正成为越来越重要的功能——无论是语音交互、环境音采集&#xff0c;还是音乐播放&#xff0c;都离不开高效的音频数据传输与处理。而I2S&#xff08;Inter-IC Sound&#xff09;作为专为音频设计的通信协议&#xff0c…

大数据实时数仓的数据质量监控解决方案

实时数仓不仅仅是传统数据仓库的升级版,它更强调数据的实时性、流动性和高可用性,通过对海量数据的即时处理和分析,为企业提供近乎实时的洞察力。这种能力在金融、零售、制造、互联网等行业中尤为关键,例如,电商平台可以通过实时数仓监控用户行为,动态调整推荐算法;金融…

56认知干货:智能化产业

如果在不久的未来,一座高楼大厦的建设,只需将图纸输入系统,无数台机器人就能精准协作完成任务; 电影节的主角不再是人类,动漫与影视作品将不再需要人类创作; 当播种和收获的工作无人参与,所有过程都能自动化进行; 这将预示着我们将迎来一个智能化社会,在这个社会中,…

使用synchronized关键字同步Java线程

问题 在Java多线程编程中&#xff0c;你需要保护某些数据&#xff0c;防止多个线程同时访问导致数据不一致或程序错误。 解决方案 在需要保护的方法或代码段上使用synchronized关键字。 讨论 synchronized关键字是Java提供的同步机制&#xff0c;用于确保在同一时刻只有一…

MATLAB基于格拉姆角场与2DCNN-BiGRU的轴承故障诊断模型

本博客来源于CSDN机器鱼&#xff0c;未同意任何人转载。 更多内容&#xff0c;欢迎点击本专栏目录&#xff0c;查看更多内容。 目录 0 引言 1 格拉姆角场原理 2 2DCNN-BiGRU网络结构 3 应用实例 3.1 数据准备 3.2 格拉姆角场数据提取 3.3 网络模型搭建-重中之重 3.4 …