《深入理解Java虚拟机》学习笔记之类文件结构

1、代码编译的结果:从本地机器码转变为字节码
  • 目的:编译器将机器码编译成字节码,特定平台上的虚拟机器将字节码转译为可以直接执行的指令

  • 定义解释:

    • (1)字节码(Bytecode):主要为了实现特定软件运行和软件环境、与硬件环境无关。

      • 字节码的实现方式是通过编译器和虚拟机器。

      • 典型应用:Java Bytecode

      • 每一个字节为8个二进制数字,有256个可能的代码值(2的8次方=256),因此一个字节的操作码最多可能有256种不同的操作。 其中,0x00、0xFE、0xCA、0xFF被指定保留

    • (2)机器码(machine code):学名机器语言指令,有时也被称为原生码(Native Code),是电脑的CPU可直接解读的数据。

2、越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式

二、无关性的基石(虚拟机和字节码存储格式是实现语言无关性的基础)

1、初步实现
  • 位置:操作系统的应用层

  • 方式:Sun公司以及其他虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了“一次编写,到处运行”

  • 流程:各种语言的程序 -> javac编译器 -> 字节码(*.class)-> Java虚拟机

2、理论基础
  • 特点:Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联。

    • Class文件中包含了Java虚拟集指令集和符号表以及若干其他辅助信息
  • 安全:基于安全方面的考虑,Java虚拟机规范要求在Class文件中使用许多强制性的语法和结构化约束,但任意一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效Class文件。

    • 作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机做为语言的产品交付媒介

三、Class类文件的结构

  • 任何一个Class文件都对应着唯一一个类或接口的定义信息,但类或接口不一定都得定义在文件里(不一定以磁盘文件格式存在)
    • eg:类或接口也可以通过类加载器直接生成
0、基本理解
  • Class文件是一组以8位字节为基础单位的二进制流

    • 各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符

      • 因此Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在
    • 当遇到需要占用8位字节以上空间的数据时,则会按照高位在前的方式分割成若干个8位字节进行存储

      • 这种排序称为“Big-Endian”:具体是指最高位字节在地址最低位、最低位字节在地址最高位的顺序来存储数据。而x86等处理器使用的是相反的“Little-Endian”
  • Class文件格式采用一种类似与C语言结构体的伪结构来存储数据,只有两种数据类型:无符号数、表

    • (1)无符号数:基本的数据类型

      • 以u1/u2/u4/u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数

      • 无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值

    • (2)表:由多个无符号数或者其他表作为数据项构成的复合类型数据

      • 表用于描述有层次关系的复合结构的数据

      • 所有表都习惯性的以“_info”结尾

    • 某一类型的集合:无论是无符号数还是表,当需要描述同一个类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这一系列的连续的某一类型的数据为某一类型的集合

  • Class文件格式

    类型名称数量注解
    u4magic1魔数
    u2minor_version1Class文件次版本号
    u2major_version1Class文件主版本号
    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属性表
1、魔数与Class文件的版本
  • (1)魔数:

    • 位置:每个Class文件的第1-4个字节

    • 作用:唯一作用是确定这个文件是否为一个能被虚拟机接收的Class文件(用于身份识别)

    • 出现原因:基于安全方面的考虑,使用魔数而不是易于改动的扩展名

    • 内容:0xCAFEBABE(咖啡宝贝?)

  • (2)Class文件的版本号

    • 次版本号(Minor Version)

      • 位置:第5-6个字节
    • 主版本号(Major Version)

      • 位置;第7-8个字节
    • Java的版本号(主)从45开始,从JDK1.1开始每次大版本升级版本号+1

      • eg:JDK1.7支持到最大版本号为51
      • 版本支持向下兼容
2、常量池
  • (1)常量池容量计数值(constant_pool_count)

    • 位置:第9-10个字节,常量池之前,主版本号之后

    • 特点:

      • 容量计数是从1开始的

      • 第0项常量空出来:用于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可以将索引值置为0来表示

  • (2)常量池(constant_pool)

    • 位置:常量池容量计数值之后 constant_pool_count - 1 个字节

    • 存放两大类内容:字面量、符号引用

    • 1⃣字面量:接近于Java语言层面的常量,如文本字符串,声明为final的常量值等

    • 2⃣符号引用:编译原理方面的概念。用于在虚拟机加载Class文件动态链接时,在常量池中获取符号引用以获取对应方法、字段的最终内存布局信息

      • a:类和接口的全限定名
      • b:字段的名称和描述符
      • c:方法的名称和描述符
  • 常量池中每一项常量都是一个表(目前有14种)

    • 共同点:14种表开始的第一位都是一个u1类型(1字节无符号数)的标志位(tag),代表当前这个常量属于哪种常量类型

    • 不同点:14种常量类型各自均有自己的结构

  • 为什么一个Class文件中字段名、方法最大只能是64KB?

    • 因为Class文件中的方法字段等都需要引用CONSTANT_Utf8_info型常量来描述名称

      • CONSTANT_Utf8_info型常量的结构
      类型名称数量
      u1tag1
      u2length1
      u1byteslength
      • length值说明了这个UTF—8编码的字符串长度是多少字节
    • 所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度

      • 最大长度就是lenth的最大值,即u2类型所能表达的最大值65535(2的十六次方-1),即64KB
      • 有变量名或方法超过这个长度将无法编译
3、访问标志
  • 位置:常量池结束之后的两个字节

  • 作用:用于识别一些类或者接口层次的访问信息

  • 内容:最终的值是各个标志值的和

  • 注意:access_flags中一共有16个标志位可以使用

    • 当前只定义了其中8个,没有使用到的标志位要求一律为0
标志名称标志值含义
ACC_PUBLIC0x0001是否为public类型
ACC_FINAL0x0010是否被声明为final,只有类可设置
ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语意。此字节码指令的语义在JDK1.0.2中发生过改变,为了区别这条指令是否使用新语义,JDK1.0.2之后编译出来的类这个标志都要为真
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类值为假
ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生的
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举
4、类索引、父类索引与接口索引集合(用于确定类的继承关系)
  • 位置:依次排列于访问标志之后

  • 类型:类索引和父类索引都是u2类型的数据,接口索引集合是一组u2类型的数据集合

  • 作用:

    • 类索引:确定这个类的全限定名
    • 父类索引:确定这个类的父类的全限定名
    • 接口索引集合:描述这个类实现了哪些接口
  • 特征:

    • 类索引、父类索引:其各自指向一个类型为CONSTANT_Class_info类型的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型常量中的全限定名字符串

    • 接口索引集合:入口的第一项为一个u2类型的接口计数器,表示索引表的容量

      • 如果该类没有实现任何接口,则该计数器的值为0,后面接口的索引表不再占用任何字节
5、字段表集合
  • 字段表容量计数器(fields_count)

    • 位置:接口索引集合之后,字段表之前
    • 类型:u2
  • 定义:

    • 字段表(field_info):用于描述接口或者类中声明的变量
    • 字段(field):包括类级变量以及实例级变量,但不包括方法内部声明的局部变量
  • 字段表结构:

    类型名称数量注解
    u2access_flags1字段访问标志
    u2name_index1字段的简单名称
    u2descriptor_index1字段和方法的描述符
    u2attributes_count1属性表计数器
    attribute_infoattributesattributes_count存储额外信息的属性表
  • 字段访问标志(access_flags)结构:

    标志名称标志值含义
    ACC_PUBLIC0x0001字段是否public
    ACC_PRIVATE0x0002字段是否private
    ACC_PROTECTED0x0004字段是否protected
    ACC_STATIC0x0008字段是否static
    ACC_FINAL0x0010字段是否final
    ACC_VOLATILE0x0040字段是否字段是否volatile(并发可见性,是否强制从主内存读写)
    ACC_TRANSIENT0x0080字段是否transient(可否被序列化)
    ACC_SYNTHETIC0x1000字段是否由编辑器自动产生的
    ACC_ENUM0x4000字段是否enum
  • 全限定名、简单名称、方法和字段描述符区别

    • 全限定名:将完整类名(含包名)中的 . 替换为 /

      • eg:java/lang/Object
    • 简单名称:没有类型和参数修饰的方法或字段名称

      • eg:inc()方法和字段m的简单名称就是inc和m
    • 方法和字段的描述符:用于描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值

  • 描述符的查找流程

    • 根据描述符的值(数字)到常量池中寻找对应的字符串(字母)从而还原代码信息
  • 描述符规则:

    • (1)基本数据类型以及无返回值的void类型:都用一个规定好的大写字符表示(有标准表可查找)

    • (2)对象类型:用字符L加对象的全限定名来表示

      • eg;Ljava/lang/Object
    • (3)数组类型;每一个维度将使用一个前置的 “ [ ” 来描述

      • eg:String类型的二维数组表示为 “[[Ljava/lang/String”
      • eg: int类型的一维数组可表示为 “[I”
    • (4)方法:先参数列表,后返回值的顺序描述

      • eg:String类型的方法 “()Ljava/lang/String”
  • 存储额外信息的属性表(attributes)

    • eg:fianl static int m = 123,可能会存在一项ConstantValue属性,指向常量123
  • 特别注意:

    • 字段表集合中不会列出从超类或父接口中继承来的字段,但可能列出如内部类持有的外部类引用等字段

    • Java语言中字段不能重载,但对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的

6、方法表集合
  • 结构:与字段表一致

    • 方法访问标志也基本与字段表的信息一致
  • 方法中的Java代码去哪了?

    • 方法体中的代码经过编译器编译程字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面
      • 属性表是Class文件格式中最具扩展性的一种数据项目
  • 特征签名:一个方法中各个参数在常量池中的字段符号引用的集合(Java中体现为参数的类型、顺序和数量)

    • Class文件中为只要描述符不完全一致也可以共存
    • 因此如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的
7、属性表集合
  • 定义:在Class文件、字段表、方法表都可以携带自己的属性表集合,用以描述某些场景专有的信息

  • 特点:不再要求各个属性表具有严格的顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性(虚拟机规范中有预定义属性)

  • 一个符合规则的属性表结构

    类型名称数量
    u2attribute_name_index1
    u4attribute_lenth1
    (可变)infoattribute_length
    • 对于每个属性,他的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示
    • 属性值的结构是完全自定义的
    • 需要通过一个u4长度的属性去说明属性值所占用的位数
  • (1)Code属性

    • 定义:Java程序方法体重的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。

      • Code属性出现在方法表的属性集合之中
    • Slot:是虚拟机为局部变量分配内存所使用的最小单位

      • 局部变量表中的Slot可以重用
      类型大小
      byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型1个Slot
      double、long这两种64位数据类型2个Slot
  • (2)Exceptions属性

    • 定义:列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在throws关键字后面列举的异常

    • 结构

      类型名称数量
      u2attribute_name_index1
      u4attribute_length1
      u2number_of_exceptions1
      u2exception_index_tablenumber_of_exceptions
      • 每一种受查异常使用一个exception_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型
  • (3)LineNumberTable属性

    • 定义:用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系
      • 并不是运行时必需的属性,但默认会生成到Class文件之中
  • (4)LocalVariableTable属性

    • 定义:LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系
      • 并不是运行时必须的属性,但默认会生成到Class文件之中
      • 如果没有这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE会使用arg0等占位符代替原有参数名。对程序运行没有影响,但是在调试期间无法根据参数名称从上下文中获得参数值
  • (5)SourceFile属性

    • 定义:用于记录生成这个Class文件的源码文件名称
  • (6)ConstantValue属性

    • 定义:用于通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量(类变量)才可以使用这项属性

    • 1⃣对于非static类型的变量(实例变量)的赋值:在实例构造器的<init>方法中进行

    • 2⃣对于类变量:两种方式

      初始化方式目前javac编译器的选择使用该方式的情况
      生成ConstantValue属性来初始化同时使用final static修饰,且该变量的数据类型为基本数据类型或字符串(String)
      类构造器<client>方法初始化非final或非基本数据类型/字符串
  • (7)InnerClasses属性

    • 定义:用于记录内部类与宿主类之间的关联
  • (8)Deprecated及Synthetic属性

    • 特征:都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念

    • 定义:

      • Deprecated:用于表示某个类、字段或者方法(不再推荐使用,可在代码中通过@Deprecated注解进行设置)

      • Synthetic:代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的

    • JDK1.5之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置他们的访问标识中的ACC_SYNTHETIC标志位

      • eg:Bridge Method
    • 所有由非用户代码产生的类、方法及字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的一项,唯一例外的是实例构造器<init>方法和类构造器<client>方法

  • (9)StackMapTable属性(字节码验证)

    • 定义:会在虚拟机类加载的字节码验证阶段,被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器

      • 新的验证器在同样能保证Class文件合法性的前提下,省略了在运行期通过数据流分析去确认字节码的行为逻辑合法性的步骤,而是在编译阶段将一系列的验证类型直接记录在Class文件之中,通过检查这些验证类型代替了类型推导过程,从而大幅提升了字节码验证的性能
    • 特点;

      • 变长
      • 位于Code属性的属性表中
      • 一个方法的Code属性最多只能有一个StackMapTable属性,否则将抛出ClassFormatError异常
    • 验证方式

      • StackMapTable属性中包含零至多个栈映射帧,每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时,局部变量表和操作数栈的验证类型
    • 类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束

  • (10)Signature属性(记录泛型类型)

    • 定义:任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则Signature属性会为它记录泛型签名信息

    • 出现原因:Java语言的泛型采用的是擦除法实现的伪泛型,在字节码(Code属性)中,泛型信息编译之后类型变量、参数化类型都会被擦除掉

      • 类型擦除的好处:实现简单、分厂容易实现Backport,运行期能够节省一些类型所占的内存空间

      • 类型擦除的坏处:运行期无法将泛型类型与用户定义的普通类型同等对待,如无法进行运行期反射获取泛型信息(目前Java的反射API能够获取泛型类型,就是因为这个参数的存在)

  • (11)BootstrapMethods属性

    • 定义:用于保存invokedynamic指令引用的引导方法限定符

    • 特点:

      • 变长
      • 位于类文件的属性表中
      • 类文件的属性表中最多也只能又一个BootstrapMethods属性

四、字节码指令简介

  • Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成

  • 字节码指令集优缺点

    • 缺点:

      • 1⃣指令集的操作码总数不可能超过256条(原因:限制了Java虚拟机操作码的长度为1个字节,即0-255)

      • 2⃣解释执行字节码时会损失性能(原因:由于Class文件格式放弃了编译后代码的操作数长度对齐,这就意味着虚拟机处理那些超过一个字节数据的时候,不得不在运行时从字节中重建出具体数据的结构)

    • 优点;

      • 1⃣放弃了操作数对齐,就意味着可以省略很多填充和间隔符号

      • 2⃣用一个字节来代表操作码,也是为了尽可能获得短小精干的编译代码(尽可能小数据量、高传输效率的设计是由Java语言设计之初面向网络、智能家电的技术背景所决定的)

  • Java虚拟机的解释器的基本执行模型(不考虑异常处理)

    do {
        自动计算PC寄存器的值加1;
        根据PC寄存器的指示位置,从字节码流中取出操作码;
        if (字节码存在操作数) 从字节码流中取出操作数;
        执行操作码所定义的操作;
    } while(字节码流长度 > 0)   
    
1、字节码与数据类型
  • 在Java虚拟机的指令集中,大多数指令都包含了其操作所对应的数据类型信息

    • eg:iload、fload分别代表int操作和float操作

    • 注:这两条指令的操作在虚拟机内部可能会是由同一段代码来实现的,但在Class文件中它们必须拥有各自独立的操作码

  • 对于大部分的数据类型相关的字节码指令,它们的操作码助记符中都由特殊的字符来表明专门为哪种数据类型服务

    字符类型
    iint
    llong
    sshort
    bbyte
    cchar
    ffloat
    ddouble
    areference
    arraylenth数组
    goto与数据类型无关
  • 由于Java虚拟机的操作码长度只有一个字节,Java虚拟机的指令集对于特定的操作只提供了有限的类型相关指令去支持它(并非每种数据类型和每一种操作都有对应的指令)。有一些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型。

    • 查表6-31可知,通过使用数据类型列所代表的特殊字符替换opcode列的指令模版中的T,就可以得到一个具体的字节码指令

    • eg:没有指令支持boolean类型。

      • 大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型的
2、字节码操作按用途分为9类
  • (1)加载和存储指令

    • 作用:用于将数据在栈帧中的局部变量表和操作数栈之间来回传输

    • iload_<n>中的<n>是代表了一组指令

  • (2)运算指令

    • 作用:用于对两个操作数栈上的值进行某种特定的运算,并把结果重新存入到操作栈顶

    • 分类:大体上算术指令可以分为两种

      • 对整型数据进行运算的指令
      • 对浮点型数据进行运算的指令
    • 如果某个操作结果没有明确的数学定义的话,将会使用NaN值来表示

  • (3)类型转换指令

    • 作用:可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题

    • 窄化类型转换:存在上限溢出、下限溢出和精度丢失等情况

  • (4)对象创建与访问指令

    • 作用:Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素
  • (5)操作数栈管理指令

    • 作用:直接操作操作数栈
  • (6)控制转移指令

    • 作用:可以让Java虚拟机有条件或无条件的从指定的位置指令而不是控制转移指令的下一条指令继续执行程序
      • 从概念模型上理解,可以认为控制转移指令就是在有条件或无条件的修改PC寄存器的值
  • (7)方法调用和返回指令

    • 作用:调用相关对象、类、接口方法(与数据类型无关),根据返回值类型调用方法返回指令
  • (8)异常处理指令

    • 作用:显式抛出异常(throw语句)都由athrow指令来实现
  • (9)同步指令

    • 作用:Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用 管程(Monitor) 来支持的

    • 1⃣方法级同步:方法级同步是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中

      • 虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法

      • 当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置

      • 如果设置了,执行线程就被要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程

      • 在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程

      • 如果一个方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放

    • 2⃣同步一段指令集序列:通常是由Java语言中的synchronized语句块来表示的

      • Java虚拟机的指令集中有monitorentermonitorexit两条指令来支持synchronized关键字的语义

五、公有设计和私有实现

  • Java虚拟机规范描绘了Java虚拟机应用的共同程序存储格式:Class文件格式和字节码指令集

    • 这些内容与硬件、操作系统及具体的Java虚拟机实现之间是完全独立的
  • 虚拟机在外部接口合乎规范的情况下可以有多种实现

    • 虚拟机实现者可以使用这种伸缩性来让Java虚拟机获得更高的性能、更低的内存消耗或者更好的可移植性
  • 目前虚拟机实现的两种主要方式

    • (1)将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集

    • (2)将输入的Java虚拟机代码在加载或执行时翻译成宿主机CPU的本地指令集(即JIT代码生成技术)

六、Class文件结构的发展

  • Class文件格式所具备的平台中立(不依赖于特定硬件及操作系统)、紧凑、稳定和可扩展的特点,是Java计数体系实现平台无关、语言无关两项特性的重要支柱
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值