plsql 无法解析指定的连接标识符_Java方法加载、解析、存储、调用

    方法调用在项目中是数不胜数,除了一些常量类其他的类都会定义方法并调用,那你有想过他是怎么从一个java语言写的方法到计算机执行的吗,下面我们就来学习Class字节码文件中保存java中的方法、方法加载解析、方法信息存储、方法的调用、方法执行过程……

一、java方法在class字节码文件中的存储

1、从.java文件到.class 文件是javac编译器的输入和输出,也就是将高级java语言编译为JVM字节码文件。

2、编译后.class文件中是怎么存储方法的?

    我们先看看.class 文件的结构,.class 文件结构主要有两种类型的变量即无符号数和表,无符号数有u1、u2、u4、u8 分别代表1字节变量 2字节变量 4字节变量和8字节变量而表是一种数据结构,用来描述具体的属性,表有点类似于c语言结构体或java类,表可以由另一个表和无符号数组成……

3、.class字节码文件其实就是一个大表表结构如下:

类型名称数量
u4magic1
u2minor_version1
u2major_version1
u2constant_pool_count1
cp_infoconstant_poolconstant_pool_count - 1
u2access_flags1
u2this_class1
u2super_class1
u2interfaces_count1
u2interfacesinterfaces_count
u2fields_count1
field_infofieldsfields_count
u2methods_count1
method_infomethodsmethods_count
u2attribute_count1
attribute_infoattributesattributes_count

    我们编译的.java文件被编译成.class 文件后按照这个格式保存,变量大小和顺序都是严格按照这个顺序的……或者说所有的.java文件被编译后生成的.class文件都是这样的,唯一不同的可能就是文件中常量池表个数、字段表、方法表、属性表的个数不同而已,其他的如顺序、类型都是一样的……

4、换个角度说来就是一个.java 文件编译之后生成的.class 文件我们可以用一个C语言结构体、java类来描述而这个结构体或类的属性是固定的。

5、.class文件的结构体是Java虚拟机的规范。

6、回到正题我们来看看java方法在.class 字节码文件中的存储

(1)、方法的描述有两个变量,u2无符号数来描述该类中有多少个方法,而methods变量中的method_info 表就是用来描述一个具体方法的结构体(c)、类(java)。

(2)、method_info 表的构成                                                                                                  69eb12e3817a1c60795ff8a27e97bee3.png

    method_info这个表结构也是java虚拟机规范,所有的java方法被编译后都要按照这个表结构存储。

    method_info 表结构中包含4个u2类型的变量分别表示访问标识符、方法名在常量池中的索引位置,方法描述符在常量池中的索引位置,attributes 数组中attribute_info 表结构个数。

    通过method_info 结构体我们能找到一个方法的名称、方法描述符(方法参数信息、方法返回值信息)、方法访问标识符(private、public、static、synchronized 等……供应access_flag 一个标记),这些相当于是描述一个方法类似于java类中的Class对象一样,我们找到了方法,但还没看到方法中的执行逻辑即代码部分,接着接续找……

(3)、attribute_info 表结构

    b06d459967d53404811c4497f8d15cec.png

    attribute_info 表中多个属性是用属性名称来区分的,比如Code属性表、Exceptions属性表、AnnotationDefault属性表、MethodParameters属性表……

    attribute_info 表结构其中包含了很多属性这里就不展开来说了,感兴趣的可以看看《java虚拟机规范》这本书,这里只说code属性表。

    说一句attribute_info 属性表的定义没有之前的几种表严格,如果attribute_info属性表中的属性java虚拟机不认识则忽略。

    attribute_info 属性表结构中找不到我们想要的答案,继续。

(4)、Code属性表

    c5c55f530401d938734d1a3271be9806.png

(5)、Code属性是变长的属性,位于method_info 结构的属性表中。Code 属性中包含某个方法、实例初始化方法、类或接口初始化方法的java虚拟机指令以及相关辅助信息。

(6)、如果方法申明为native或者abstract方法,那么method_info 结构表的属性绝不能有Code属性。在其他情况下method_info 必须有且只能有一个Code属性。

(7)、Code 属性表中的u4类型的code_length 代表字节码长度,u1类型的code是用于存储字节码指令的一些列字节流。既然叫做字节码指令那么每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令并且可以知道这条指令后面是否需要跟随参数,以及参数应当如何理解。这也就是我们要找的java方法的具体逻辑执行部分。

(8)、通过一路梳理发现之前的属性都是在描述方法,如方法名称、方法描述符、方法修饰符,而只有code属性中记录了方法具体执行时需要的数据比如字节码指令、异常表个数、异常表、异常表的生命周期、栈帧中的局部变量表大小、栈帧中的操作数栈大小等运行期信息。

(9)、如果为java程序中的信息分为代码(Code,方法体里面的java代码)和元数据(MetaData,包括类、字段、方法定义以及其他信息)两部分那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。

    .java文件中的方法在.class文件中的存储如下:

.java文件中的方法—>.class 文件中的method_info表—>attribute_info属性表—>Code属性表—>code字节码指令。

    OK通过上面一个个表结构我们看到了一个.java文件到.class 文件中的表示方式和存储格式,同样也看到了.java文件中的方法在.class 文件中的标识方式和存储。

    .java 文件是高级语言java的文件是我们能看懂,但是计算机看不懂、JVM也看不懂,为了能让JVM看懂,我们通过javac编译器将.java文件按照JVM规范编译为JVM能认识的字节码文件,那下一步就是JVM加载.class 字节码文件并按照JVM规范来解析.class 字节码文件。

二、方法信息加载解析

    通过javac编译器编译后生成的.class 文件是JVM运行时数据的来源,下面来看看.class 文件被加载解析并获得java方法信息的流程。

    一个类在JVM中的生命周期是:类加载、连接(校验、准备、解析)、初始化、运行、停止、卸载。

1、我们知道一个java文件被编译后会生成一个.class 文件,这里先不考虑内部类等。

2、在什么时候会JVM会加载.class 文件?

    之前的《new一个对象》那篇文章中有提到过,这里我们在来看看,JVM执行到getstatic、putstatic、new、invokestatic、java.lang.reflect包反射调用这些指令时需要初始化类,而初始化是在类已加载的基础上,所以只要JVM执行到这些指令则如果所属类还没加载则一定会进行加载操作。

3、类的.class文件加载是通过类加载器加载的,类加载器有

(1)、bootstrap class loader类加载器:它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。

(2)、extensions class loader类加载器:它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。

(3)、system class loader类加载器: 被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换为变量所指定的JAR包和类路径。

(4)、用户自定义类加载器(自定义类加载器是想使用非双亲委派模式来加载一个之前已加载过的类)

4、类加载器的核心方法

 (1)、findClass(String name) 查找指定的全类名类并加载该类的.class字节码文件,加载到JVM内存中是byte[]。通过这一步将类加载到了JVM内存中。

 ( 2)、defineClass0、defineClass1、defineClass2三个native方法,将加载的.class字节码流进行验证、解析操作,即生成该字节码文件对应的instanceKlass对象、java.lang.Class 对象,其实这个过程中是在classFileParse::parseClassFile()方法中进行的。

5、classFileParse::parseClassFile()解析加载的.class文件

(1)、.class 字节码文件是二进制流的文件,被加载后会严格按照.class字节码文件格式解析即按照.class字节码表来顺序的解析。

(2)、.class 字节码文件是二进制的那怎么来获取表中的属性,因为每个属性都有固定大小或能计算出大小所以按照规定的大小来做相应的偏移量就OK了。

(3)、方法中调用parse_method()方法对.class字节码文件中的methods变量按照表格式进行解析,我们知道methods变量中存储的都是method_info 表,其整个组成关系如下:  6457553ff1bda22ae6bc0f3d3b22b6c0.png

(4)、ClassFileStream * cfs = Stream(); 获取的字节码文件。

(5)、u2  flags = cfs->get_u2_fast();获取方法method_info 表中的u2类型的access_flag属性。

(6)、u2  name_index = cfs->get_u2_fast();获取方法名在常量池中的索引。

(7)、方法属性解析、code属性解析、code属性中的行号、方法局部变量表、异常属性等。以此类推按照method_info 表结构解析完方法。                                                                          

(8)、methodOop  m_oop=oopFactory::new_method() 方法来生成JVM内部方法对象。

(9)、字节码复制、将从.class 字节码文件中解析出来的java方法代码复制到method_oop 对象中。

  具体的方法解析完成 哦也。

三、方法信息存储

    上面方法信息解析完之后创建了一个methodOop对象,那methodOop对象是干什么用的?

    methodOop包含java方法的一切信息,列如方法名、方法返回值类型、入参、字节码指令、栈深度、局部变量表、字节码对应的java行号等信息,总之HotSpot 通过methodOop将java class字节码文件中的方法信息存储到了内存中,并且这篇内存区域是结构化的,使得可以在JVM运行期方便的访问java方法的各种属性信息。

1、这么看来方法信息的存储有了地方了即.class 字节码文件中的Methods变量被JVM加载解析后都会生成methodOop对象保存在方法区(元数据区)。

2、说方法信息保存到methodOop对象不是错误的但也不准确,因为我们再看.class 字节码文件中的method_info表的时候发现,其实方法由两部分组成即代码和元数据,同样在JVM内部也是分开存放的,methodOop对象中存储了方法元数据而代码其实是存储在constMethodOop 对象中。

3、methodOop 主要存储java方法的名称、签名、访问标识、解释入口等信息,而constMethodOop则用于存储方法的字节码指令、行号表、异常表等信息。

4、那constMethodOop 对象是在哪里创建的,其实在创建methodOop 对象后里立马就创建了constMethodOop对象;methodOop = oopFactory::new_method(); constMethodOop cm = new_constMethod() ;

5、methodOop对象内部布局:

6a635f1cba71643afddc8dfa9afeaaaf.png

这里我们看到了一个属性属性字段constMethod,他引用就是constMethodOop对象的地址。

6、constMethodOop 对象内存布局:

98c62e6ff2d3a08ae5d7cbabd00ca31d.png

    同样这里我们也看到了很多熟悉的属性名称比如bytecodes、linenumbertable、localvariabletable、 这些都是.class 字节码文件中的code_attribute 属性表中的属性和包含的属性表。

7、那方法代码即编译后的字节码具体存储在哪里?

    其实一个方法的字节码指令就保存在constMethodOop 对象的末尾,在上面的图中我们也看到了bytecode属性,其中就保存了引用constMethodOop对象所属方法的字节码指令。

    OK 到这里我们知道了.class字节码文件中的methods变量中的每个method_info表被JVM加载后解析生成methodOop对象、constMethodOop 对象保存在方法区(元数据区即metaData)。

8、那问题来了类的方法信息即methodOop对象和类是怎么关联的?

(1)、在调用系统加载器System Class Loader 对应用程序的java类进行加载的过程中,完成方法符号连接、验证,最重要的是完成vtable与itable的构建,从而支持在JVM运行的方法动态绑定。

(2)、java类在JVM内部对应的对象是instanceKlassOop(JDK8中是instanceKlass)。在JVM加载java类的过程中,JVM会动态解析java类的方法及其父类方法的重写,进而构建出一个vtable并将vtable分配到instanceKlassOop内存区域的末尾,从而支持运行期的方法动态绑定。

(3)、方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。

(4)、其实在方法解析后还有一步那就是将methodOop对象地址保存到vtable 中。

(5)、方法信息保存在methodOop对象中而methodOop对象地址保存在vtable 中,而vtable保存在instanceKlassOop对象的内存区域末尾,这样类、方法表、方法对象关联起来了。

(6)、类、方法关联关系

instanceKlassOop-->vtable-->methodOop-->constMethodOop-->bytecode 

    OK 在类方法解析存储中我们看到有一个关联点就是vtable 即虚方法表,在学习方法调用前我们先来看看vtable 具体是什么?

四、vtable (virtual table) 虚方法表

1、vtable的构建

  java类在JVM内部对应的对象是instanceKlassOop(JDK8中是instanceKlass)。在JVM加载java类的过程中,JVM会动态解析java类的方法及其父类方法的重写,进而构建出一个vtable并将vtable分配到instanceKlassOop内存区域的末尾,从而支持运行期的方法动态绑定。

2、vtable 大小计算

  JVM在第一次加载java类时会调用classFileParse.cpp::parseClassFile()函数对java class字节码进行解析,在parseClassFile()函数中会调用parse_method()函数解析java类中的方法,parse_method()函数执行完之后,会继续调用klassVtable::compute_vtable_size_and_num_mirandas()函数,计算当前java类的vtable大小。

3、vtable 大小计算步骤

(1)、获取父类vtable的大小,并将当前类的vtable的大小设置为父类的vtable的大小。

(2)、循环遍历当前java类的每一个方法,调用needs_new_vtable_entry()函数进行判断,如果判断结果是true则vtable 大小加1。

4、什么情况下needs_new_vtable_entry()函数返回true

    当这个方法满足重写方法条件下才会返回true,那什么样的条件满足重写?

(1)、java方法是非static、非private、非final、类是非final的。

(2)、包含和父类方法名、方法描述符一样的方法。

    如果满足上面2个条件则needs_new_vtable_entry()函数返回true,也就是虚拟方法表大小加1。

5、JVM内部vtable的实现机制

 (1)、每一个java类在JVM内部都有一个对应的instanceKlassOop,vtable就被分配在这个oop内存区域的后面。

 (2)、vtable表中的每一个位置存放一个指针,指向java方法在内存中所对应的methodOop的内存首地址。

 (3)、如果一个java类继承了父类,则该java类就会直接继承父类的vtable。

 (4)、若该java类中声明了一个非private、非final、非static的方法,若该方法是对父类方法的重写,则JVM会更新父类vtable表中指向父类被重写的方法的指针,使其指向子类中该方法的内存地址。若该方法并不是对父类方法的重写,则JVM会向该java类的vtable中插入一个新的指针元素,使其指向该方法的内存位置。

6、vtable 特点总结

(1)、vtable 分配在instanceKlassOop对象实例的内存末尾。

(2)、所谓vtable,可以看作是一个数组,数组中的每一项成员元素都是一个指针,指针指向java方法在JVM内部所对应的method实例对象的内存首地址。

(3)、vtable 是java实现面向对象的多态性的机制,如果一个java方法可以被继承和重写,则最终通过invokevirtual 字节码指令完成java方法动态绑定和分发。事实上很多面向对象的语言都是基于vtable 机制去实现多态,例如c++。

(4)、java子类会继承父类的vtable 。

(5)、java中所有类都继承自java.lang.Object,java.lang.Object中有5个虚方法:void finalize()、boolean equals(Object)、String toString()、int hashCode()、Object clone() 因此,如果一个java类中不声明任何方法则其vtable 的长度默认为5。

(6)、java类中不是每一个java方法的内存地址都会保存到vtable 表中。只有当java子类中声明的java方法是public、protected的、且没有final、static 修饰,并且java子类中的方法并非对父类方法的重写时,JVM才会在vtable 表中为该方法增加一个引用。

(7)、如果java子类某个方法重写了父类方法,则子类的vtable 中原本对父类方法的指针引用会被替换为对子类的方法引用。

OK 我们知道了vtable 是什么、什么时候创建、里面保存什么 那下面开始方法调用的学习……

五、方法调用

    方法调用主要的任务就是确定调用哪个方法,定位到唯一的方法,如果没有继承、没有多态那定位一个方法可能比较容易,但是java是面向对象语言,有继承、多态的特性所以很多时候在编译阶段是无法确定调用的方法是子类的方法还是父类的方法,只有真正到了运行的时候才能确定……

   按照方法特性、方法唯一性的确定时间,方法可以分为虚方法(包括接口方法)、和非虚方法,当然就有了两种调用方式即解析调用、分派调用。

    什么是解析调用、什么样的方法是被解析调用的、什么是虚方法、什么是非虚方法、什么是分派调用 …… 带着这些疑问开始。

1、解析

    方法在程序正真运行前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说调用目标在程序代码写好,编译器进行编译时就确定了。这类方法的调用称为解析。

2、什么样的方法在编译器就能确定他的唯一性

    静态方法、私有方法、final 方法、父类方法,静态方法是类型直接关联的不会有重载、重写多个版本,私有方法是外部不可访问,final修饰的方法是不能继承所以他们在整个程序运行期都会只有一个版本。

3、符号引用到直接引用

     .class字节码文件中的引用都是符号引用,比如调用某个方法则使用符号引用引用到该方法就完事了,但是当类加载器加载到JVM内存之后进行解析操作时如果这个方法是非虚方法即方法在运行期只有一个版本则会将方法的符号引用解析为直接引用即这个方法在内存中的入口地址的引用。

4、虚方法、非虚方法

    之前我们在学习vtable时其实已经知道只有满足public、protected、非private、非static、非final 、非final类、并且没有重写父类方法的话则都会在instanceKlassOop对象的内存区域默认的vtable 大小加1 ,也就是说满足这些条件的方法是虚方法、不满足这些条件的就是非虚方法。

5、虚方法表和分派调用(动态绑定)

    虚方法表是一种技术方案,这种技术方案解决的就是动态分派、运行时动态绑定的问题。

6、解析调用、分派调用

    解析调用一定是静态的过程,在编译期间就能确定,在类装载的解析阶段就会把涉及的符号引用全部转化为可确定的直接引用,不会延迟到运行期再去完成。而分派调用则可能是静态的,也可能是动态的。

7、静态分派、动态分派

(1)、静态分派:所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。

(2)、所谓的静态分派就是在编译期确定方法的符号引用。这个可以和方法重载的场景联系在一起想想。

(3)、动态分派:动态分派是在JVM执行方法时按照运行类型来动态查找确定具体的调用方法。

(4)、动态分派时其实已经知道了要调用的方法名和方法描述(方法参数、方法返回值类型)但是不确定调用属于哪个类的方法,比如子类继承父类、重写父类方法和不重写父类方法两种情况下子类调用的方法是不同的。

(5)、我们来看看父类、子类、重写、不重写场景下的符号引用、具体调用方法

 重写父类方法:

    父类

/*** @author BitterCaffe* @date 2020/8/30* @description: TODO*/public class Father {    public void say() {        System.out.println("father say");    }}

子类重写父类方法

/*** @author BitterCaffe* @date 2020/8/30* @description: TODO*/public class Son extends Father {    @Override    public void say() {        System.out.println("son say");    }}

测试代码

/*** @author BitterCaffe* @date 2020/8/30* @description: TODO*/public class TestDynamicDispatcher {    public static void main(String[] args) {        //多态,        Father father = new Son();        //符号引用的确定是按照静态类型来的,所以这里符号引用是父类的类名和方法        //因为方法满足public、非static、非private、非final、非final类所以是虚方法,动态分派        //具体调用的方法是按照运行时类型确定        //因为这里继承了父类并重写了父类方法所以vtable 中存储的是子类的methodOop对象,所以执行子类的方法        father.say();    }}

字节码反编译验证

287a6f8f31092bfeb61b92aed2e61fbf.png

不重写父类方法:

子类只继承不重写父类方法

/*** @author BitterCaffe* @date 2020/8/30* @description: TODO*/public class Son extends Father {}

测试代码

/*** @author BitterCaffe* @date 2020/8/30* @description: TODO*/public class TestDynamicDispatcher {    public static void main(String[] args) {        //多态        Father father = new Son();        //符号引用的确定是按照静态类型来的,所以这里符号引用是父类的类名和方法        //因为方法满足public、非static、非private、非final、非final类所以是虚方法,动态分派        //具体调用的方法是按照运行时类型确定,具体运行的对象是子类        //继承了父类但没有重写父类方法所以vtable 中存储的是父类的methodOop对象,所以具体运行的是子类但执行的是父类的方法        father.say();    }}

字节码反编译验证

ef126287861be7018d13bf89d3c23fb7.png

反编译后的字节码都是一致的,因为他们的静态类型都是Father类所以应该是一致的……

8、其实到这里方法调用方式也完了,但是可能还是有疑问对于上面的继承父类重写父类方法、继承父类不重写方法他的符号引用都是父类的方法为何在重写父类方法后调用的是子类方法、不重写调用的是父类的方法,这个还要从invokevirtual 指令的多态查找过程开始说起,invokevirtual 指令的运行是解析过程大致分为如下步骤:

(1)、找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C;

(2)、如果在类型C中找到与常量池中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError 异常。

(3)、否则,按照继承关系从下往上一次对C的各个父类进行第2步的搜索和校验过程。

(4)、如果始终没找到合适的方法,则抛出java.lang.AbstractMethodError 异常。

9、按照上面4步,在重写父类方法后,第1步、第2步就确定了方法是子类的方法所以调用的就是子类方法,如果没有重写父类方法则到第3步发现方法是父类的则引用的方法就是父类方法。

10、由于invokevirtual 指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual 指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是java语言中方法重写的本质。

11、上面5中说明了虚拟方法表和动态分派的关系,其实理解了虚拟方法表,个人感觉对方法方法重写有更清晰、简单的认识

(1)、invokevirtual 指令执行的第一步就是在运行期确定接收者的实际类型,实际类型确定了就确定了instanceOop。

(2)、instanceOop 确定了按照对象头的meta_data 就能确定instanceKlassOop对象。

(3)、确定了instanceKlassOop 对象就能找到vtable 。

(4)、按照classFilePare::parseClassFile()方法中方法解析完之后会给该类的instanceKlassOop 对象的vtable 中记录虚拟方法对象的地址即methodOop对象的地址。

(5)、按照vtable 中methodOop对象指针添加规则如果子类重写父类方法则会把对父类methodOop对象的指针引用改为子类自己的methodOop对象指针。

(6)、这样我们就能从vtable 中找到具体methodOop对象,这样我们就找到了具体执行那个类的方法。

(7)、具体代码即字节码指令存储在methodOop对象的constMethodOop引用的类中即ConstMethodOop对象的bytecode 字段中。

    这样一路下来对象、类、方法、字节指令都找确定了……

六、总结

    类加载-->解析-->方法解析--> 生成methodOop对象--> methodOop对象地址保存到vtable(虚拟方法表)--> 如果有父类则把父类的methodOop对象地址也保存到本类的vtable中--> 这样解析完之后一个类的instanckKlassOop对象内存模型最底部就是该类的vtable即这个类的虚拟方法表,其中保存了该类以及该类的父类中的method0op对象指针即methodOop * methodoop 也就是每个方法的入口-->如果本类重写了父类方法则本类instanceKlassOop对象的vtable中会把父类的methodOop对象指针地址替换为子类自己的方法指针--> 这样类加载后的解析就结束了-->运行程序-->调用方法-->当着对象调用方法时会去这个对象的元数据指针所指instanceKlassOop下找方法,找到了则调用,这时有两种情况,一种是子类重写了父类方法则调用的就是子类自己的方法,一种是子类没有重写父类方法则这里存储的是父类的方法地址则调用的就是父类的方法。

    本来想着方法调用和方法执行一起来写,但是方法执行涉及到栈内存、栈帧、局部变量表、形参、方法返回地址、操作数栈、局部变量表操作数栈重叠、字节码执行器、寄存器这些知识点,但到月底了还是没明白字节码执行器和寄存器,cpu、cpu寄存器他们之间的关系所以没法写方法的执行了……

8月唯一一篇文章终于写完了…… 2020.08.30 22:00

参考书籍:

《深入理解java虚拟机》第二版 周志明 著

《揭秘java虚拟机JVM设计原理与实现》 封亚飞 著


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部