JVM 类加载机制

本文深入探讨了Java虚拟机的类加载机制,包括加载、验证、准备、解析和初始化五个阶段,以及类加载器的工作原理。重点介绍了双亲委派模型、符号引用到直接引用的解析过程,以及如何通过自定义类加载器打破双亲委派模型。此外,还讨论了类加载过程中的安全性和动态性需求。
摘要由CSDN通过智能技术生成

学习导图:

类加载机制概述

       定义: 虚拟机 把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制
       特性: 运行期类加载。即Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,从而通过牺牲一些性能开销来换取Java应用极高的的扩展性和灵活性。

什么是运行期,什么是编译期?

编译期是指编译器将源代码翻译为机器能识别的代码,Java被编译为Jvm认识的字节码文件,而运行期则是指Java代码的运行过程

类加载机制时机

        一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历 加载(Loading) 验证(Verification)准备( Preparation)解析(Resolution)初始化(Initialization)使用(Using)卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。
这七个阶段的发生顺序如下图 所示:

类的生命周期 

          加载、验证、准备、初始化和卸载 这五个阶段的 顺序是确定的类型的加载过程必须按照这种顺序按部就班地开始 ,而解析阶段则不一定它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的 运行时绑定特性 (也称为 动态绑定或晚期绑定
        注意: 按部就班地“开始”,而不是按部就班地“进行”或按部就班地“完成”,强调这点是因为这些阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段

        类加载过程中的第一个阶段:"加载",Java虚拟机并没有进行强制约束,而是交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

          对于这六种会触发类型进行初始化的场景,虚拟机使用了一个非常强烈的限定语——“有且只有”,这六种场景中的行为称为对一个类型进行 主动引用 除此之外, 所有引用类型的方式都不会触发初始化,称为 被动引用

需要特别指出的是,类的实例化和类的初始化是两个完全不同的概念:

  • 类的实例化是指创建一个类的实例(对象)的过程;
  • 类的初始化是指为类各个成员赋初始值的过程,是类生命周期中的一个阶段。

被动引用常见示例:

package org.fenixsoft.classloading;
/**
* 被动使用类字段演示一: 通过子类引用父类的静态字段,不会导致子类初始化
**/
public class SuperClass {
        static {
                System.out.println("SuperClass init!");
        }
        public static int value = 123;
}
public class SubClass extends SuperClass {
        static {
                System.out.println("SubClass init!");
        }
}
public class NotInitialization {
        public static void main(String[] args) {
                System.out.println(SubClass.value);
        }
}
 上述代码运行之后,只会输出“SuperClass init ,而不会输出 “SubClass init
         对于静态字段,只有直接定义这个字段的类才会被初始化 因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化 至于是否要触发子类的加载和验证阶段,在虚拟机规范中并未明确规定,所以这点取决于虚拟机的具体实现。对于HotSpot 虚拟机来说,可通过 -XX:+TraceClassLoading参数观察到此操作是会导致子类加载的。
package org.fenixsoft.classloading;
/**
* 被动使用类字段演示二: 通过数组定义来引用类,不会触发此类的初始化
**/
public class SuperClass {
        static {
                System.out.println("SuperClass init!");
        }
        public static int value = 123;
}
public class SubClass extends SuperClass {
        static {
                System.out.println("SubClass init!");
        }
}
public class NotInitialization {  
        public static void main(String[] args) {
                SuperClass[] sca = new SuperClass[10];
        }
}
运行之后发现没有输出 “SuperClass init! ,说明并没有触发类 org.fenixsoft.classloading.SuperClass 的初始化阶段。但是这段代码里面触发了另一个名为“[Lorg.fenixsoft.classloading.SuperClass” 的类的初始化阶段,对于用户代码来说,这并不是一个合法的类型名称,它是一个由虚拟机自动生成的、直接继承于java.lang.Object 的子类,创建动作由字节码指令newarray 触发。
package org.fenixsoft.classloading;
/**
* 被动使用类字段演示三:
* 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
**/
public class ConstClass {
        static {
                System.out.println("ConstClass init!");
        }
        public static final String HELLOWORLD = "hello world";
}
/**
* 非主动使用类字段演示
**/
public class NotInitialization {
        public static void main(String[] args) {
                System.out.println(ConstClass.HELLOWORLD);
        }
}
        上述代码运行之后,也没有输出“ConstClass init ,这是因为虽然在 Java 源码中确实引用了 ConstClass 类的常量 HELLOWORLD ,但其实 在编译阶段通过常量传播优化,已经将此常量的值“hello world”直接存储在NotInitialization类的常量池中 以后NotInitialization对常量ConstClass.HELLOWORLD的引用,实际都被转化为NotInitialization类对自身常量池的引用了 。也就是说,实际上NotInitialization Class 文件之中并没有 ConstClass 类的符号引用入口,这两个类在编译成Class文件后就已不存在任何联系了。
        
        接口的加载过程与类加载过程稍有不同,针对接口需要做一些特殊说明:接口也有初始化过程,这点与类是一致的,上面的代码都是用静态语句块“static{}” 来输出初始化信息的,而接口中不能使用“static{}” 语句块,但编译器仍然会为接口生成 “<clinit>()”类构造器 用于初始化接口中所定义的成员变量。接口与类真正有所区别的是前面讲述的六种“ 有且仅有 需要触发初始化场景中的第三种: 当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

类加载的过程

        在类的加载时机中,我们已经通过图看到了类的生命周期,也即类的加载过程。接下来主要了解一下JVM在加载、验证、准备、解析和初始化五个阶段所执行的具体动作。

加载

“加载”Loading)阶段是整个类加载Class Loading)过程中的一个阶段,在加载阶段,Java虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
虚拟机规范对这三点要求其实并不是特别具体,留给虚拟机实现与Java 应用的灵活度都是相当大的。例如“ 通过一个类的全限定名来获取定义此类的二进制字节流 这条规则,它并没有指明二进制字节流必须得从某个Class 文件中获取,确切地说是根本没有指明要从哪里获取、如何获取。仅仅这一点空隙,Java 虚拟机的使用者们就可以在加载阶段搭构建出一个相当开放广阔的舞台, Java 发展历程中,充满创造力的开发人员则在这个舞台上玩出了各种花样,许多举足轻重的Java 技术都建立在这一基础之上,例如:
  • ZIP压缩包中读取,这很常见,最终成为日后JAREARWAR格式的基础。
  • 从网络中获取,这种场景最典型的应用就是Web Applet
  • 运行时计算生成,这种场景使用得最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass()来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。
  • 由其他文件生成,典型场景是JSP应用,由JSP文件生成对应的Class文件。
  • 从数据库中读取,这种场景相对少见些,例如有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
  • 可以从加密文件中获取,这是典型的防Class文件被反编译的保护措施,通过加载时解密Class文件来保障程序运行逻辑不被窥探。
  • ...........等等方式获取
        相对于类加载过程的其他阶段, 非数组类型的加载阶段 (准确地说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的阶段。 加载阶段既可以使用Java虚拟机里内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成 ,开发人员通过定义自己的类加载器去控制字节流的获取方式(重写一个类加载器的findClass() loadClass() 方法--此方法会破坏双亲委派),实现根据自己的想法来赋予应用程序获取运行代码的动态性。
        
        对于数组类而言,情况就有所不同,数组类本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构造出来的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型(Element Type ,指的是数组去掉所有维度的类型)最终还是要靠类加载器来完成加载,一个数组类(下面简称为C )创建过程遵循以下规则:
  1. 如果数组的组件类型是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将被标识在加载该组件类型的类加载器的类名称空间上
  2. 如果数组的组件类型不是引用类型(例如int[]数组的组件类型为int),Java虚拟机将会把数组C标记为与引导类加载器关联。
  3. 数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的 可访问性将默认为public,可被所有的类和接口访问到。
        加载阶段结束后, Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中了 ,方法区中的数据存储格式完全由虚拟机实现自行定义,《Java虚拟机规范》未规定此区域的具体数据结构。类型数据妥善安置在方法区之后,会在Java 堆内存中实例化一个 java.lang.Class 类的对象,这个对象将作为程序访问方法区中的类型数据的外部接口
        加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的一部分,这两个阶段的开始时间仍然保持着固定的先后顺序。

验证

        验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全
         虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为 载入了有错误或有恶意企图的字节码流而导致整个系统受攻击甚至崩溃,所以验证字节码是Java虚拟机保护自身的一项必要措施

        验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证元数据验证字节码验证符号引用验证

文件格式验证

        主要验证字节流是否符合Class 文件格式的规范,并且能被当前版本的虚拟机处理
这一阶段可能包括下面这些验证点:
  • 是否以魔数0xCAFEBABE开头。
  • 主、次版本号是否在当前Java虚拟机接受范围之内。
  • 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
  • CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据。
  • Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
  • ……等等规则
        该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java 类型信息的要求。这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证之后,这段字节流才被允许进入Java 虚拟机内存的方法区中进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了。

元数据验证

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求
这个阶段可能包括的验证点如下:
  • 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
  • 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
  • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。
  • ……等等规则
第二阶段的主要目的是对类的元数据信息进行语义校验,保证不存在与《Java语言规范》定义相悖的元数据信息。

字节码验证

第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的在第二阶段对元数据信息中的数据类型校验完毕以后,这阶段就要对类的方法体(Class 文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。
例如:
  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中这样的情况。
  • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
  • 保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。
  • ……等等

符号引用验证

        最后一个阶段的校验行为 发生在虚拟机将符号引用转化为直接引用 的时候,这个转化动作将在连接的第三阶段—— 解析阶段中发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部 类、方法、字段等资源
本阶段通常需要校验下列内容:
  • 符号引用中通过字符串描述的全限定名是否能找到对应的类。
  • 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
  • 符号引用中的类、字段、方法的可访问性(privateprotectedpublic<package>)是否可被当前类访问。
  • ……等等
        符号引用验证主要是确保解析行为能正常执行如果无法通过符号引用验证, 虚拟机将会抛出一个java.lang.IncompatibleClassChangeError 的子类异常,典型的如: java.lang.IllegalAccessError、 java.lang.NoSuchFieldError java.lang.NoSuchMethodError 等。
        验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、但却不是必须要执行的阶段,因为验证阶段只有通过或者不通过的差别,只要通过了验证,其后就对程序运行期没有任何影响了。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑 使用-Xverify:none参数来关闭大部分的类验证措施 ,以缩短虚拟机类加载的时间。

准备

        准备阶段是正式为类中定义的变量(即静态变量(类变量),被static修饰的变量。不包括实例变量)分配内存并设置类变量初始值的阶段。这个阶段不会执行任何的虚拟机字节码指令,在初始化阶段才会显示的初始化这些字段,所以准备阶段不会做这些事情。
假设有:

public static int value = 123;

        变量value 在准备阶段过后的初始值为 0而不是123 因为这时尚未开始执行任何 Java 方法,而把value赋值为 123 putstatic 指令是程序被编译后,存放于类构造器 <clinit>() 方法之中,所以把 value 赋值为123 的动作要到类的初始化阶段才会被执行。
Java 中所有基本数据类型的零值:

         上述基本数据类型在“通常情况下初始值是零值,那言外之意是相对的会有某些特殊情况如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值,假设上面类变量value的定义修改为:

public static final int value = 123;
编译时 Javac 将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据 Con-stantValue 的设置将value 赋值为 123

ConstantValue属性在类加载过程的准备阶段做的事情是什么?

在编译时Javac将会为被static和final修改的常量生成ConstantValue属性(此时ConstantValue属性的值是多少,暂时不知道,),在类加载的准备阶段虚拟机便会根据ConstantValue为常量设置相应的值(这个值是什么意思呢,比如我们在程序中定义final static int a = 100,那么这个a就是ConstantValue属性,然后在准备阶段中a的值就会变成100),这就是ConstantValue属性在类加载过程的准备阶段做的事

解析

         解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程 符号引用就是Class文件中 CONSTANT_Class_info、** CONSTANT_Fieldref_info CONSTANT_Methodref_info**等类型的常量。
符号引用与直接引用的定义:
  • 符号引用(Symbolic References)符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。
  • 直接引用(Direct References直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。
        《Java虚拟机规范》之中并未规定解析阶段发生的具体时间,只要求了在执行 ane-warray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invoke-special、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield和putstatic 17 个用于 操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析

        

        对同一个符号进行多次解析请求是很常见的,除了invokedynamic指令以外虚拟机基本都会对第一次解析的结果进行缓存,后面再遇到时,直接引用,从而避免解析动作重复。

        对于invokedynamic指令,上面规则不成立。当遇到前面已经由invokedynamic指令触发过解析的符号引用时,并不意味着这个解析结果对于其他invokedynamic指令同样生效。这是由invokedynamic指令的语义决定的,它本来就是用于动态语言支持的,也就是必须等到程序实际运行这条指令的时候,解析动作才会执行。其它的命令都是“静态”的,可以再刚刚完成记载阶段,还没有开始执行代码时就解析。

        解析动作主要针对 类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符 这7类符号引用进行,这7类符号引用分别对应于常量池的CONSTANT_Class_info、 CON-STANT_Fieldref_info 、CONSTANT_Methodref_info、 CONSTANT_InterfaceMethodref_info
CONSTANT_MethodType_info CONSTANT_MethodHandle_info CONSTANT_Dyna-mic_info 和 CONSTANT_InvokeDynamic_info 8种常量类型
接下了解下 4 种引用的解析过程
1.类或接口的解析
假设当前代码所处的类为 D ,如果要把一个从未解析过的符号引用 N 解析为一个类或接口 C 的直接引用,那虚拟机完成整个解析的过程需要包括以下3 个步骤:
  1. 如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析过程就将宣告失败。
  2. 如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类似“[Ljava/lang/Integer”的形式,那将会按照第一点的规则加载数组元素类型。如果N的描述符如前面所假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表该数组维度和元素的数组对象。
  3. 如果上面两步没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成前还要进行符号引用验证,确认D是否具备对C的访问权限。如果发现不具备访问权限,将抛出java.lang.IllegalAccessError异常。
针对上面第 3 点访问权限验证,在 JDK 9 引入了模块化以后,一个 public 类型也不再意味着程序任何位置都有它的访问权限,我们还必须检查模块间的访问权限。
如果我们说一个D拥有C 的访问权限,那就意味着以下 3 条规则中至少有其中一条成立:
  1. 被访问类Cpublic的,并且与访问类D处于同一个模块。
  2. 被访问类Cpublic的,不与访问类D处于同一个模块,但是被访问类C的模块允许被访问类D的模块进行访问。
  3. 被访问类C不是public的,但是它与访问类D处于同一个包中
2.字段解析
        要解析一个未被解析过的字段符号引用,首先将会对字段表内class_index 项中索引的 CONSTANT_Class_info 符号引用进行解析,也就是字段所属的类或接口的符号引用。如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败。如果解析成功完
成,那把这个字段所属的类或接口用 C 表示,《Java虚拟机规范》要求按照如下步骤对 C 进行后续字段的搜索:
  1. 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
  2. 否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口, 如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
  3. 否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
  4. 否则,查找失败,抛出java.lang.NoSuchFieldError异常。
如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError 异常。
        以上解析规则能够确保Java 虚拟机获得字段唯一的解析结果,但在实际情况中, Javac 编译器往往会采取比上述规范更加严格一些的约束,譬如有一个同名字段同时出现在某个类的接口和父类当中,或者同时在自己或父类的多个接口中出现,按照解析规则仍是可以确定唯一的访问字段,但Javac 编译器就可能直接拒绝其编译为Class 文件。
在下图所示代码 中演示了这种情况,如果注释了 Sub 类中的“public static int A=4 ,接口与父类同时存在字段 A ,那 Oracle 公司实现的 Javac 编译器将提示 “The field Sub.A is ambiguous”,并且会拒绝编译这段代码。
//字段解析
package org.fenixsoft.classloading;
public class FieldResolution {
        interface Interface0 {
                int A = 0;
        }
        interface Interface1 extends Interface0 {
                int A = 1;
        }
        interface Interface2 {
                int A = 2;
        }
        static class Parent implements Interface1 {
                public static int A = 3;
        }
        static class Sub extends Parent implements Interface2 {
                public static int A = 4;
        }
        public static void main(String[] args) {
                System.out.println(Sub.A);
        }
}
3.方法解析
        方法解析的第一个步骤与字段解析一样,也是需要先解析出方法表的class_index 项中索引的方法所属的类或接口的符号引用,如果解析成功,那么我们依然用C 表示这个类,接下来虚拟机将会按照如下步骤进行后续的方法搜索:
  1. 由于Class文件格式中类的方法和接口的方法符号引用的常量类型定义是分开的,如果在类的方法表中发现class_index中索引的C是个接口的话,那就直接抛出java.lang.IncompatibleClassChangeError异常。
  2. 如果通过了第一步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  3. 否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  4. 否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时候查找结束,抛出java.lang.AbstractMethodError异常。
  5. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError
4.接口方法解析
        接口方法也是需要先解析出接口方法表的class_index 项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C 表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索:
  1. 与类的方法解析相反,如果在接口方法表中发现class_index中的索引C是个类而不是接口,那么就直接抛出java.lang.IncompatibleClassChangeError异常。
  2. 否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  3. 否则,在接口C的父接口中递归查找,直到java.lang.Object类(接口方法的查找范围也会包括Object类中的方法)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  4. 对于规则3,由于Java的接口允许多重继承,如果C的不同父接口中存有多个简单名称和描述符都与目标相匹配的方法,那将会从这多个方法中返回其中一个并结束查找,《Java虚拟机规范》中并没有进一步规则约束应该返回哪一个接口方法。但与之前字段查找类似地,不同发行商实现的Javac译器有可能会按照更严格的约束拒绝编译这种代码来避免不确定性。
  5. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。
        在JDK 9之前,Java 接口中的所有方法都默认是 public 的,也没有模块化的访问约束,所以不存在访问权限的问题,接口方法的符号解析就不可能抛出java.lang.IllegalAccessError 异常。但在 JDK 9 中增加了接口的静态私有方法,也有了模块化的访问约束,所以从JDK 9 起,接口方法的访问也完全有可能因访问权限控制而出现java.lang.IllegalAccessError 异常。

初始化

        初始化是类加载的最后一步,在前面的阶段里,除了加载阶段可以通过用户自定义的类加载器加载,其余部分基本都是由虚拟机主导的。但是到了初始化阶段,才开始真正执行用户编写的java代码了。

        在准备阶段,变量都被赋予了初始值,但是到了初始化阶段,所有变量还要按照用户编写的代码重新初始化。换一个角度,初始化阶段是执行类构造器<clinit>()方法的过程<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static语句块)中的语句合并生成的编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。如下面示例:

//非法前向引用变量
public class Test {
        static {
                i = 0; // 给变量复制可以正常编译通过
                System.out.print(i); // 这句编译器会提示“非法向前引用”
        }
        static int i = 1;
}
         <clinit>()方法与类的构造函数不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。因此在Java虚拟机中第一个被执行的<clinit>()方法的类型肯定是java.lang.Object。
        由于父类的<clinit>()方法先执行,也就说父类中定义的静态语句块要优先于子类的变量赋值操作,下方示例代码中 字段 B 的值将会是 2 而不是 1
// <clinit>() 方法执行顺序
static class Parent {
        public static int A = 1;
        static {
                A = 2;
        }
}
static class Sub extends Parent {
        public static int B = A;
}
public static void main(String[] args) {
        System.out.println(Sub.B);
}
         <clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
         接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。 但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
        Java虚拟机必须保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的<clinit>() 方法,其他线程都需要阻塞等待,直到活动线程执行完毕<clinit>() 方法。如果在一个类的 <clinit>() 方法中有耗时很长的操作,那就可能造成多个进程阻塞 ,在实际应用中这种阻塞往往是很隐蔽的。下面代码 演示了这种场景。
static class DeadLoopClass {
        static {
                // 如果不加上这个if 语句,编译器将提示 “Initializer does not complete normally” 并拒绝编译
                if (true) {
                        System.out.println(Thread.currentThread() + "init DeadLoopClass");
                        while (true) {
                        }
                   }
                }
}
public static void main(String[] args) {
        Runnable script = new Runnable() {
                public void run() {
                        System.out.println(Thread.currentThread() + "start");
                        DeadLoopClass dlc = new DeadLoopClass();
                        System.out.println(Thread.currentThread() + " run over");
                }
        };
        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
}
运行结果如下,一条线程在死循环以模拟长时间操作,另外一条线程在阻塞等待:
Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]init DeadLoopClass
需要注意, 其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出<clinit>()方法后,其他线程唤醒后则不会再次进入<clinit>()方法。同一个类加载器下,一个类型只会被初始化一 次。

类加载器

概念

        Java虚拟机设计团队有意把类加载阶段中的 通过一个类的全限定名来获取描述该类的二进制字节流 这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“ 类加载器 Class Loader )。

类与类加载器

        类加载器虽然只用于实现类的加载动作,但它在Java 程序中起到的作用却远超类加载阶段。 对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相 等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
        这里所指的“ 相等 ,包括代表类的 Class 对象的 equals() 方法、 isAssignableFrom() 方法、 isInstance()方法的返回结果,也包括了使用instanceof 关键字做对象所属关系判定等各种情况。
        如果没有注意到类加载器的影响,在某些情况下可能会产生具有迷惑性的结果,下方代码演示了不同的类加载器对instanceof关键字运算的结果的影响。
/**
* 类加载器与 instanceof关键字演示---- 不同的类加载器对instanceof关键字运算的结果的影响
*
* @author zzm
*/
public class ClassLoaderTest {
        public static void main(String[] args) throws Exception {
                ClassLoader myLoader = new ClassLoader() {
                        @Override
                        public Class<?> loadClass(String name) throws ClassNotFoundException {
                        try {
                                String fileName = name.substring(name.lastIndexOf(".") + 1)+".class";
                                InputStream is = getClass().getResourceAsStream(fileName);
                                if (is == null) {
                                        return super.loadClass(name);
                                }
                                byte[] b = new byte[is.available()];
                                is.read(b);
                                return defineClass(name, b, 0, b.length);
                        } catch (IOException e) {
                                throw new ClassNotFoundException(name);
                        }
                }
        };
        Object obj = myLoader.loadClass("org.fenixsoft.classloading.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj instanceof org.fenixsoft.classloading.ClassLoaderTest);
        }
}
运行结果:
class org.fenixsoft.classloading.ClassLoaderTest
false
        代码中构造了一个简单的类加载器, 它加载与自己在同一路径下的 Class 文件,使用这个类加载器去加载了一个名为“org.fenixsoft.classloading.ClassLoaderTest” 的类,并实例化了这个类的对象。
        两行输出结果中,从第一行可以看到这个对象确实是类org.fenixsoft.classloading.ClassLoaderTest 实例化出来的,但在第二行的输出中却发现这个对象与类org.fenixsoft.classloading.ClassLoaderTest 做所属类型检查的时候返回了false 这是因为Java虚拟机中同时存在了两个ClassLoaderTest类,一个是由虚拟机的应用程序类加载器所加载的另外一个是由我们自定义的类加载器加载的,虽然它们都来自同一个Class文件,但在Java虚拟机中仍然是两个互相独立的类,做对象所属类型检查时的结果自然为false。

双亲委派模型

 在Java 虚拟机的角度来看,只存在两种不同的类加载器:
  • 一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
  • 用户自定义类加载器(User-Defined Class Loader): Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
站在 Java 开发人员的角度来看,类加载器就应当划分得更细致一些。看下图所示:

双亲委派模型图

绝大多数 Java 程序都会使用到以下 3 个系统提供的类加载器来进行加载:
  1. 启动类加载器(Bootstrap Class Loader)这个类加载器负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jartools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可。
  2. 扩展类加载器(Extension Class Loader)这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
  3. 应用程序类加载器(Application Class Loader)这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。  

双亲委派模型要求除了顶层的Bootstrap ClassLoader外,其它所有类加载器都要有自己的父类加载器。这里的父子关系一般不会议继承实现,而是通过组合实现。 

双亲委派模型的工作过程是 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

重点来了,为什么要使用双亲委派机制呢?

如果面试官问这个问题,一定要答出关键字:安全性

反证法来辩证。假设不采用双亲委派机制,那我可以自定义一个类加载器,然后我写一个java.lang.String类用自定义的类加载器加载进去,原来java本身又有一个java.lang.String类,那么类的唯一性就没法保证,就不就给虚拟机的安全带来的隐患了吗。所以要保证一个类只能由同一个类加载器加载,才能保证系统类的的安全

以下摘自JVM第三版电子书:

使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是 Java 中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object ,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object 类在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object 的类,并放在程序的ClassPath中,那系统中就会出现多个不同的 Object 类, Java 类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。

双亲委派模型的实现

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
                synchronized (getClassLoadingLock(name)) {
                    // 首先,检查请求的类是否已经被加载过了
                    Class<?> c = findLoadedClass(name); 
                    if (c == null) {//没加载过的情况
                        long t0 = System.nanoTime();  
                        try {
                            if (parent != null) {
                                //如果自定义的类加载器的parent不为null,就调用parent的loadClass进行加载类 
                                c = parent.loadClass(name, false);   
                            } else {
                                //否则就去找bootstrap ClassLoader
                                c = findBootstrapClassOrNull(name);
                            }
                        } catch (ClassNotFoundException e) {
                           // 如果父类加载器抛出ClassNotFoundException ,说明父类加载器无法完成加载请求
                        }

                        if (c == null) {
                            // 在父类加载器无法加载时,再调用本身的findClass方法来进行类加载

                            long t1 = System.nanoTime();
                            c = findClass(name);
                            sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                            sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                            sun.misc.PerfCounter.getFindClasses().increment();
                        }
                    }
                    if (resolve) {
                        resolveClass(c);
                    }
                    return c;
                }
    }

这段代码的逻辑清晰易懂:先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败,抛出ClassNotFoundException 异常的话,才调用自己的 findClass() 方法尝试进行加载。

自定义类加载器

        一般来说,自己开发的类加载器只需要覆写findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了双亲委派模型的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写findClass()方法。

被自定义加载器UserClassLoader调用的类方法:

package com.springboot.aop;

public class TestUserClassLoader {
	
	public void hello() {
		System.out.println("this is myUserClassLoader....");
	}
	
}

自定义类加载器UserClassLoader:

package com.springboot.aop;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;

public class UserClassLoader extends ClassLoader  {
	private String path;

    public UserClassLoader(String path) {
        this.path = path;
    }

    /**
     * 编写findClass方法的逻辑
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 获取类的class文件字节数组
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            //直接生成class对象
            return defineClass(name, classData, 0, classData.length);
        }
    }

    /**
     * 编写获取class文件并转换为字节码流的逻辑
     * @param className
     * @return
     */
    private byte[] getClassData(String className) {
        // 读取类文件的字节
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            // 读取类文件的字节码
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 类文件的完全路径
     * @param className
     * @return
     */
    private String classNameToPath(String className) {
        return path + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
    }

    public static void main(String[] args) throws ClassNotFoundException {
        String path="D:\\tz\\test\\springboot-helloword\\target\\classes\\com\\springboot\\aop";
        //创建自定义文件类加载器
        UserClassLoader loader = new UserClassLoader(path);

        try {
            //加载指定的class文件
            Class<?> clazz=loader.loadClass("com.springboot.aop.TestUserClassLoader");
            System.out.println("使用类加载器:" + clazz.getClassLoader());
            Method method = clazz.getDeclaredMethod("hello");
            Object obj = clazz.newInstance();
            method.invoke(obj);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

控制台输出结果:

使用类加载器:sun.misc.Launcher$AppClassLoader@2a139a55
this is myUserClassLoader....

另外一种加载类的方法:Class.forName
        Class.forName是一个静态方法,同样可以用来加载类该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader)和 Class.forName(String className)。第一种形式的参数 name表示的是类的全名;initialize表示是否初始化类;loader表示加载时使用的类加载器。第二种形式则相当于设置了参数initialize的值为true,loader的值为当前类的类加载器。Class.forName的一个很常见的用法是在加载数据库驱动的时候。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用来加载 Apache Derby 数据库的驱动。

破坏双亲委派

1、重写loadClass()方法,前面说过java.lang.ClassLoader类的方法loadClass()封装了双亲委派模型的实现如果被子类重写loadClass(),很容易破坏身亲委派机制。

        因此在JDK 1.2之后的 java.lang.ClassLoader 中添加一个新的protected方法 findClass() ,并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。上节我们已经分析过 loadClass() 方法,双亲委派的具体逻辑就实现在这里面,按照loadClass() 方法的逻辑,如果父类加载失败,会自动调用自己的 findClass() 方法来完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的。

2、双亲委派模型的第二次“被破坏是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类 加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被 称为“基础,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变 的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?

        线程上下文类加载器(Thread Context ClassLoader) 是从 JDK 1.2 开始引入的 。这个类加载器可以通过java.lang.Thread 类的 setContext-ClassLoader() 方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

        双亲委派机制就是底层的类加载器一直委托上层的类加载器,如果上层的已经加载了,就无需加载,上层的类加载器没有加载则自己加载。这就突出了双亲委派机制的一个缺陷,就是只能子的类加载器委托父的类加载器,不能反过来用父的类加载器委托子的类加载器

那你会问,什么情况会出现父的类加载器委托子的类加载器呢?

案例一:

        加载JDBC的数据库驱动。在JDK中有一个所有 JDBC 驱动程序需要实现的接口Java.sql.Driver。而Driver接口的实现类则是由各大数据库厂商提供。那问题就出现了,DriverManager(JDK的rt.jar包中)要加载各个实现了Driver接口的实现类,然后进行统一管理,但是DriverManager是由Bootstrap类加载器加载的,只能加载JAVA_HOME下lib目录下的文件(可以看回上面双亲委派机制的第一张图),但是实现类是服务商提供的,由AppClassLoader加载,这就需要Bootstrap(上层类加载器)委托AppClassLoader(下层类加载器),也就破坏了双亲委派机制

案例二:

        JNDI服务, JNDI 现在已经是 Java 的标准服务,它的代码由启动类加载器来完成加载(在JDK 1.3 时加入到 rt.jar 的),肯定属于 Java 中很基础的类型了。但JNDI 存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath 下的 JNDI 服务提供者接口(
Service Provider Interface SPI )的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的。
3、 于用户对程序动态性的追求而导致的,这里所说的 动态 指的是一些非常 门的名词:代码热替换( Hot Swap )、模块热部署( Hot Deployment )等。
        说白了就是希望Java应用程序能像我们的电脑外设那样,接上鼠标、 U 盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用关机也不用重启。对于个人电脑来说,重启一次其实没有什么大不了的,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故,这种情况下热部署就对软件开发者,尤其是大型系统或企业级软件开发者具有很大的吸引力。

CSDN海神之光上传的代码均可运行,亲测可用,直接替换数据即可,适合小白; 1、代码压缩包内容 主函数:main.m; 调用函数:其他m文件;无需运行 运行结果效果图; 2、代码运行版本 Matlab 2019b或2023b;若运行有误,根据提示修改;若不会,私信博主; 3、运行操作步骤 步骤一:将所有文件放到Matlab的当前文件夹中; 步骤二:双击打开main.m文件; 步骤三:点击运行,等程序运行完得到结果; 4、仿真咨询 如需其他服务,可私信博主或扫描博客文章底部QQ名片; 4.1 博客或资源的完整代码提供 4.2 期刊或参考文献复现 4.3 Matlab程序定制 4.4 科研合作 功率谱估计: 故障诊断分析: 雷达通信:雷达LFM、MIMO、成像、定位、干扰、检测、信号分析、脉冲压缩 滤波估计:SOC估计 目标定位:WSN定位、滤波跟踪、目标定位 生物电信号:肌电信号EMG、脑电信号EEG、心电信号ECG 通信系统:DOA估计、编码译码、变分模态分解、管道泄漏、滤波器、数字信号处理+传输+分析+去噪(CEEMDAN)、数字信号调制、误码率、信号估计、DTMF、信号检测识别融合、LEACH协议、信号检测、水声通信 1. EMD(经验模态分解,Empirical Mode Decomposition) 2. TVF-EMD(时变滤波的经验模态分解,Time-Varying Filtered Empirical Mode Decomposition) 3. EEMD(集成经验模态分解,Ensemble Empirical Mode Decomposition) 4. VMD(变分模态分解,Variational Mode Decomposition) 5. CEEMDAN(完全自适应噪声集合经验模态分解,Complementary Ensemble Empirical Mode Decomposition with Adaptive Noise) 6. LMD(局部均值分解,Local Mean Decomposition) 7. RLMD(鲁棒局部均值分解, Robust Local Mean Decomposition) 8. ITD(固有时间尺度分解,Intrinsic Time Decomposition) 9. SVMD(逐次变分模态分解,Sequential Variational Mode Decomposition) 10. ICEEMDAN(改进的完全自适应噪声集合经验模态分解,Improved Complementary Ensemble Empirical Mode Decomposition with Adaptive Noise) 11. FMD(特征模式分解,Feature Mode Decomposition) 12. REMD(鲁棒经验模态分解,Robust Empirical Mode Decomposition) 13. SGMD(辛几何模态分解,Spectral-Grouping-based Mode Decomposition) 14. RLMD(鲁棒局部均值分解,Robust Intrinsic Time Decomposition) 15. ESMD(极点对称模态分解, extreme-point symmetric mode decomposition) 16. CEEMD(互补集合经验模态分解,Complementary Ensemble Empirical Mode Decomposition) 17. SSA(奇异谱分析,Singular Spectrum Analysis) 18. SWD(群分解,Swarm Decomposition) 19. RPSEMD(再生相移正弦辅助经验模态分解,Regenerated Phase-shifted Sinusoids assisted Empirical Mode Decomposition) 20. EWT(经验小波变换,Empirical Wavelet Transform) 21. DWT(离散小波变换,Discraete wavelet transform) 22. TDD(时域分解,Time Domain Decomposition) 23. MODWT(最大重叠离散小波变换,Maximal Overlap Discrete Wavelet Transform) 24. MEMD(多元经验模态分解,Multivariate Empirical Mode Decomposition) 25. MVMD(多元变分模态分解,Multivariate Variational Mode Decomposition)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值