JVM——类文件结构

13 篇文章 0 订阅

Java诞生之初的口号:“一次编写,到处运行(Write Once, Run Anywhere)”。
各个平台的虚拟机与所有平台都同意使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石。实现语言无关性的基础人生是虚拟机和字节码存储格式。Java虚拟机不和包括Java在内的如何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集合符号表以及若干其他辅助信息。基于安全方面的考虑,Java虚拟机规范要求在Class文件使用许多强制性的语法和结构化约束,但任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为语言的产品交付媒介

Class类文件的结构

任何一个Class文化都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都要定义在文件里(譬如类或接口也可以通过类加载器直接生成)

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使整个Class文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

Class文件格式
类的结构由2种数据结构组成:无符号数
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节个8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯地以 “_info” 结尾。表用于描述有层次关系的复合结构的数据,整个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
u2attributes_count1
attribute_infoattributesattributes_count

魔数与Class文件的版本

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用就是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都是用魔数来进行身份识别,譬如图片格式,如gif或者jpeg等文件头中都存在有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面考虑,因为文件扩展名可以随意地改动。文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过又不会引起混淆即可。Class文件的魔数的获得很有“浪漫七夕”,值为:0xCAFEBABE(“咖啡宝贝”),这个魔数值在Java还称为做“Oak”语言的时候就确定下来了(这个就是Java图案是一杯咖啡的原因?)

紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK1.1之后的每一个JDK大版本发布主版本号上加1(JDK1.0~1.1使用了45.0 ~ 45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

常量池

紧接着主次版本号之后的就是常量池入口,常量池也可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。如下图所示,常量池容量(偏移地址:0x00000008)为十六进制0x0013,即十进制的19,这就代表常量池中有18项常量,索引值范围为1~18.在Class文件格式规范中与Java中语言习惯不一样的是,这个容量计数是从1开始而不是从0开始的。Class文件结构中只有常量池的容量计数是从1开始的,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等容量计数斗鱼一般习惯相同,从0开始的。

在这里插入图片描述常量池中主要存放两大类常量:字面(Literal)和符号引用(Symbolic References)。字面量比较接近Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量。

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符
  • 方法的名称和描述符

虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

常量池的项目类型
这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag)

类型标志(tag)描述
CONSTANT_Utf8_info1UTF-8编码的字符串
CONSTANT_Integer_info3整型字面量
CONSTANT_Float_info4浮点型字面量
CONSTANT_Long_info5长整形字面量
CONSTANT_Double_info6双精度浮点型字面量
CONSTANT_Class_info7类或接口的符号引用
CONSTANT_String_info8字符串类型字面量
CONSTANT_Fieldref_info9字段的符号引用
CONSTANT_Methodref_info10类中方法的符号引用
CONSTANT_InterfaceMethodref_info11接口中方法的符号引用
CONSTANT_NameAndType_info12字段或方法的部分符号引用
CONSTANT_MethodHandle_info15表示方法句柄
CONSTANT_MethodType16表示方法类型
CONSTANT_InvokeDynamic_info18表示一个动态方法调用点

这14种常量类型各均有自己的结构。
在这里插入图片描述

后面是0A,对照表的内容就是
0A–00-04–00-0F
就是第4个常量和第15个常量

Oracle公司已经为我们准备好一个专门用于分析Class文件字节码的工具:javap

G:\idea-workspace\richard-demo\jvm\src\main\java\com\vma\demo>javap -verbose Tes
tClass
警告: 二进制文件TestClass包含com.vma.demo.TestClass
Classfile /G:/idea-workspace/richard-demo/jvm/src/main/java/com/vma/demo/TestCla
ss.class
  Last modified 2019-4-25; size 288 bytes
  MD5 checksum 7bf673753aefb354bc368bae2de74f45
  Compiled from "TestClass.java"
public class com.vma.demo.TestClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // com/vma/demo/TestClass.m:I
   #3 = Class              #17            // com/vma/demo/TestClass
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               TestClass.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // m:I
  #17 = Utf8               com/vma/demo/TestClass
  #18 = Utf8               java/lang/Object
{
  public com.vma.demo.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>
":()V
         4: return
      LineNumberTable:
        line 11: 0

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 16: 0
}
SourceFile: "TestClass.java"

从清单可以看出已经把常量池中18项常量都计算出来
14种常量项的结构总表——太多了不想一个个慢慢打,想看自行谷歌百度什么的。

访问标志

常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括这个Class是类还是接口:是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final类等。

public class com.vma.demo.TestClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER

在这里插入图片描述0x0021转化为2进制为:0000000000100001
引入图片(这人博客写的贼详细在这里插入图片描述
根据信息可以判断为public和super吻合。

ACC_PUBLIC,ACC_SUPER

访问标志

标志名称标志值含义
ACC_PUBLIC0x0001是否为public型
ACC_FINAL0x0010是否被声明为final,只有类可设置
ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语意,invokespecial指令的语意在JDK1.0.2发生过改变,为了区别这条指令使用哪种语意,JDK1.0.2之后编译出来的类的这个标志都必须为真
ACC_INTERFACE0x0200表示这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或抽象类来说,此标志值为真,其他类值为假
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举

看完以后,我觉得可以分为5种类型:类、抽象类、接口、注解、枚举。(纯属个人看法误信,希望有大神矫正)

类索引、父类索引与接口索引集合

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于Java语言不允许多重继承,所以父类索引只有一个,除了Java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口按照implement语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺讯从左到右排列在接口索引集合中。

先查看一下二进制的数值:
在这里插入图片描述
值为0x0030040000
在常量表可以看出,#3#4都对应着CLass,接口索引集合为0000说明没有接口
在这里插入图片描述

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括方法内部声明的局部变量。在Java中描述一个字段可以包括的信息有:字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述这些信息中,各个修饰符都是布尔值,要么有修饰符,要么没有,很适合使用标志位来表述。而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池的常量来描述。

图片来源
在这里插入图片描述在这里插入图片描述
字段表结构在这里插入图片描述

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count

字段访问标志

标志名称标志值含义
ACC_PUBLIC0x0001字段是否public
ACC_PRIVATE0x0002字段是否private
ACC_PROTECTED0x0003字段是否protected
ACC_STATIC0x0008字段是否static
ACC_FINAL0x0010字段是否final
ACC_VOLATILE0x0040字段是否volatitle
ACC_TRANSIENT0x0080字段是否transient
ACC_SYNTHETIC0x1000字段是否由编译器自动生成
ACC_ENUM0x4000字段是否enum

描述符标识字符含义

标识字符含义
B基本类型byte
C基本类型char
D基本类型double
F基本类型float
I基本类型int
J基本类型long
S基本类型short
Z基本类型boolean
V基本类型void
L基本类型对象类型,如Ljava/lang/Object

对于数组类型,每一维度将使用一个前置的“[” 字符来描述,如一个定义为“ java.lang.String [][] ”,一个整型数组"int[]"将被记录为“[i”。

用描述符来描述方法时,按照县参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法void inc()的描述符为“()V”,方法java.lang.String toString()的描述符为“()Ljava/lang/String”,方法int indexOf(char[] source, int sourceOffsent, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述符为“([CII[CII)I”。

在这里插入图片描述
00 01: fields_count
00 02:access_flage
00 05:name_index
00 06:descriptor_index

方法表集合

方法表的结果如同字段表一样,一次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。

方法表结构

类型名称数量
u2access_flag1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count

因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATITLE标志和ACC_TRANSIENT标志。与之相对的,synchronized、native、stricftp和abstract关键字可以修饰方法,所有方法表的访问标志中增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。

方法访问标志

标志名称标志值含义
ACC_PUBLIC0x0001方法是否为public
ACC_PRIVATE0x0002方法是否为private
ACC_PROTECTED0x0004方法是否为protected
ACC_STATIC0x0008方法是否为static
ACC_FINAL0x0010方法是否为final
ACC_SYNCHRONIZED0x0020方法是否为synchronized
ACC_BRIDGE0x0040方法是否是由编译器产生的桥接方法
ACC_VARARGS0x0080方法是否接受不定参数
ACC_NATIVE0x0100方法是否为native
ACC_ABSTRACT0x0400方法是否为abstract
ACC_STRICTFP0x0800方法是否为strictfp
ACC_SYNTHETIC0x1000方法是否是由编译器自动生成的

在这里插入图片描述

在这里插入图片描述与字段表集合相对应的,如果父类方法在子类中没有被重写(Override),方法表集合中不会出现来自父类的信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“”方法和实例构造器“”方法。

在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要必须拥有一个与原方法不同的特征签名(在Java代码中的方法特征签名只包括了方法名称、参数顺序以参数类型,而字节码的特征签名还包括方法返回值以及受查异常表),特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包括在特征签名中,因此Java语言里面是无法仅仅依靠返回值的不同对一个已有方法进行重载。

属性表集合

对于每一个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结果这是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的为数即可。

属性表结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
u1infoattribute_length

Code属性
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中方法就不存在Code属性,如果方法表中有Code属性存在。

Code属性表的结构

类型名称属性
u2attribute_name_index1
u4attribute_length1
u2max_stack1
u2max_locals1
u4code_length1
u1codecode_length
u2exception_table_length1
exception_infoexception_tableexception_table_length
u2attributes_count1
attribute_infoattributesattributes_count

attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,它代表一个该属性的属性名称,attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为6字节,所以属性值的长度固定为整个属性表长度减去6。

max_stack代表了操作数栈(Operand Stacks)深度最大值。在方法执行的任意时刻,操作数栈都不会超过整个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。

max_locals代表了局部变量表所需的存储空间,存储单位为Slot。Slot是虚拟机为局部变量分配内存所使用的最小单位。对于Byte、char、float、int、short、boolean、returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要亮哥Slot来存放。方法参数(包括实例方法中的隐藏参数“this”)、显示异常处理器的参数(Exception Handler Parameter,就是try-catch语句中catch块所定义的异常)、方法体重定义的局部变量都需要使用局部变量表来存放。另外不是方法中要用到但是局部变量,就把这些局部变量所占Slot纸盒为max_locals的值,原因是局部变量表中Slot可重用,当代码执行超出一个局部变量的作用域,这个局部变量所占的Slot可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给谷歌变量使用,然后计算出max_locals的大小

code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码是,就可以对应找出这个字节码代表的是什么指令,并且可以自动这个指令后面是否需要跟随参数,以及参数应当如何理解。u1数据类型的取值范围为0x00~0xFF,对应0到255,也就是256条指令,目前Java虚拟机以及有200退编码值对应的指令含义。

code_length,虽然是一个u4类型的长度值,理论上最大值可以达到2^32-1,但是根据Java规范,实际只能使用u2长度,超过这个长度Java编译器会拒绝编译。

Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义以及其他信息)两部分,那么正规Class文件中,Code属性用于描述代码,所有的气压数据项目都用于描述元数据。

在这里插入图片描述
在javap中输出的“Args_size”的值为1,在类中的2个方法——实例构造器 < inti >()和inc(),这个两个方法很明显都是没有参数的,但是在方法体或者参数列表里都没有定义局部变量。**在任何实例方法里面,都可以通过“this”关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现却非常简单,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为堆一个普通方法参数的访问,然后在虚拟机调用实例方法是自动传入此参数而。因此在实例方法的局部变量表中至少一会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数才从1开始计算。

异常表
字节码指令之后的是这个方法的显式异常处理,异常表对于Code属性来说并不是必须存在的。

类型名称数量
u2start_pc1
u2end_pc1
u2handler_pc1
u2catch_type1

含义是:当字节码在第start_pc行到第end_pc行之间(不包含第end_pc)出现了类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。catch_tyoe的值为0时,代表任意异常情况都需要转向到handler_pc处进行处理。

public class DemoTest {

    public int inc() {
        int x;
        try {
            x = 1;
            return x;
        } catch (Exception e) {
            x = 2;
            return x;
        } finally {
            x = 3;
        }
    }
}
{
  public com.vma.demo.DemoTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>
":()V
         4: return
      LineNumberTable:
        line 11: 0

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=5, args_size=1
         0: iconst_1
         1: istore_1
         2: iload_1
         3: istore_2
         4: iconst_3
         5: istore_1
         6: iload_2
         7: ireturn
         8: astore_2
         9: iconst_2
        10: istore_1
        11: iload_1
        12: istore_3
        13: iconst_3
        14: istore_1
        15: iload_3
        16: ireturn
        17: astore        4
        19: iconst_3
        20: istore_1
        21: aload         4
        23: athrow
      Exception table:
         from    to  target type
             0     4     8   Class java/lang/Exception
             0     4    17   any
             8    13    17   any
            17    19    17   any
      LineNumberTable:
        line 16: 0
        line 17: 2
        line 22: 4
        line 17: 6
        line 18: 8
        line 19: 9
        line 20: 11
        line 22: 13
        line 20: 15
        line 22: 17
      StackMapTable: number_of_entries = 2
        frame_type = 72 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 72 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
}
SourceFile: "DemoTest.java"

在这里插入图片描述

编译器为这段Java源码生成了3条异常表记录,对应3条可能出现的代码执行路径。从Java代码的语义上讲,这3条执行路径分别为:

Exceptions属性
属性表

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2number_of_exceptions1
u2exception_index_tablenumber_of_exceptions

Exceptions属性中的number_of_exceptions项表示方法可能抛出number_of_exceptions种受查异常,每一种受查异常使用一个exception_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。

LineNumberTable
LineNumberTable属性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVM8的内存结构图如下所示:\[1\] - 程序计数器 - 虚拟机栈(JVM Stack) - 本地方法栈 - 元空间(MetaSpace) - Java堆(Heap) 其中,程序计数器用于记录当前线程执行的字节码指令的地址;虚拟机栈用于存储方法调用的局部变量、操作数栈、动态链接、方法出口等信息;本地方法栈用于支持本地方法的调用;元空间用于存储的元数据信息,取代了JDK1.8之前的永久代(PermGen);Java堆用于存储对象实例和数组。 此外,JVM8还有直接内存,它是独立于JVM内存之外的内存,可以直接和NIO接口交互,提升了程序性能。\[2\] 在Java堆内存中,内存需要划分成新生代和老年代。新生代又分为eden、from和to三块区域,默认比例是8:1:1。每次创建对象时,对象会先存储到eden区域,当eden区域满了后,会触发minor GC回收该区域,未回收的对象会放入from或to区域。每经过一次GC,from和to两块空间的对象会进行一次移动,未回收的对象年龄也会增加1。当对象年龄达到一定阈值(默认为15岁),就会被晋升到老年代。当老年代满了时,会触发Full GC回收。如果堆内存不足,就会出现OutOfMemoryError。可以通过配置JVM参数(如-Xmx)来设置最大堆内存大小。\[3\] #### 引用[.reference_title] - *1* [JVM内存结构详解](https://blog.csdn.net/weixin_42173451/article/details/105805231)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [最简单的JVM内存结构图](https://blog.csdn.net/duyabc/article/details/114679595)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值