【深入理解JVM 一】一个Java程序的执行流程

本文是《深入理解JVM》系列的第一篇,介绍了Java程序从编辑源码到执行的整体流程。首先,程序经历编辑源码、编译生成字节码阶段。接着,JVM加载并执行字节码文件,包括类加载过程、运行字节码指令。在JVM内存模型中,涉及方法区、栈、堆等部分。文章还探讨了IDEA中查看字节码的方式,并详细解析了main方法、构造方法、study方法的执行过程。总结了从源代码到垃圾回收的完整生命周期。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本篇是《深入理解JVM》系列博客的第一篇,旨在全局把控,先对整体流程有个认识,然后再分阶段详解。程序从编写到执行整体可以划分为以下几个步骤:编辑源码->编译生成class文件->(加载class文件、运行class字节码文件、垃圾回收),其中后两个步骤都是在jvm虚拟机上执行的,整体的执行流程如下:
在这里插入图片描述

编辑生成源代码

编辑源代码是经历的第一个环节,编辑源代码,就是我们在任何一个工具上编写源代码,可以是记事本,也可以是IDE,这部分相当于我们在IDEA上 新建一个.java的Class 然后写内容,这里我们创建几个类和接口:

//父类Person
public class Person {
    //成员变量
    private String name;
    private int age;
      
    //构造方法
    public Person(int age, String name){
        this.age = age;
        this.name = name;
    }
    //成员方法
    public void run(){

    }
}
//接口IStudy 
public interface IStudy {
     int study(int a, int b);
}

真正的Strudent类,实现接口IStudy 和继承父类Person:

public class Student extends Person implements IStudy {
    //私有静态成员变量
    private static int cnt=5;
    //静态方法块
    static{
        cnt++;
    }
    //私有成员变量
    private String sid;
    //构造方法
    public Student(int age, String name, String sid){
        //继承父类构造方法
        super(age,name);
        this.sid = sid;
    }
    //父类方法重写
    public void run(){

        System.out.println("run()...");

    }
    //实现接口方法
    public int study(int a, int b){
        int c = 10;
        int d = 20;
        return a+b*c-d;
    }
    //成员方法
    public static int getCnt(){
        return cnt;
    }
    
    //方法加载入口
    public static void main(String[] args){

        Student s = new Student(28,"tml","20210201");
        //接口方法调用
        s.study(5,6);
        //成员方法调用
        Student.getCnt();
        //父类重写方法调用 
        s.run();

    }

}

可以从文件路径看到已经有三个Java文件生成了:
在这里插入图片描述

编译生成字节码

编译的过程就是生成.class字节码文件,输入命令javac Student.java将该源码文件.java编译生成.class字节码文件。由于在源码文件中定义了两个类,一个接口,所以生成了3个.clsss文件。
在这里插入图片描述
字节码文件是真正实现Java语言跨平台的基石,JVM运行的是class字节码文件,只要是这种格式的文件就行

  • 各种不同平台的虚拟机都统一使用这种相同的程序存储格式 跨平台
  • 其他语言编写的源码编译成字节码文件,交给jvm去运行,只要是合法的字节码文件,JVM都会正确地跑起来。 跨语言

所以其实可以看的出它实现了跨平台和跨语言,用一张图可以描述清楚
在这里插入图片描述

在类文件夹目录下可以执行类问题,用命令javap -c Student 执行类class文件结构如下 :

"C:\Program Files\Java\jdk1.8.0_251\bin\javap.exe" -c Student.class
Compiled from "Student.java"
public class com.company.Student extends com.company.Person implements com.company.IStudy {
  public com.company.Student(int, java.lang.String, java.lang.String);
    Code:
       0: aload_0
       1: iload_1
       2: aload_2
       3: invokespecial #1                  // Method com/company/Person."<init>":(ILjava/lang/String;)V
       6: aload_0
       7: aload_3
       8: putfield      #2                  // Field sid:Ljava/lang/String;
      11: return

  public void run();
    Code:
       0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String run()...
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  public int study(int, int);
    Code:
       0: bipush        10
       2: istore_3
       3: bipush        20
       5: istore        4
       7: iload_1
       8: iload_2
       9: iload_3
      10: imul
      11: iadd
      12: iload         4
      14: isub
      15: ireturn

  public static int getCnt();
    Code:
       0: getstatic     #6                  // Field cnt:I
       3: ireturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #7                  // class com/company/Student
       3: dup
       4: bipush        28
       6: ldc           #8                  // String tml
       8: ldc           #9                  // String 20210201
      10: invokespecial #10                 // Method "<init>":(ILjava/lang/String;Ljava/lang/String;)V
      13: astore_1
      14: aload_1
      15: iconst_5
      16: bipush        6
      18: invokevirtual #11                 // Method study:(II)I
      21: pop
      22: invokestatic  #12                 // Method getCnt:()I
      25: pop
      26: aload_1
      27: invokevirtual #13                 // Method run:()V
      30: return

  static {};
    Code:
       0: iconst_5
       1: putstatic     #6                  // Field cnt:I
       4: getstatic     #6                  // Field cnt:I
       7: iconst_1
       8: iadd
       9: putstatic     #6                  // Field cnt:I
      12: return
}

Process finished with exit code 0

可以看到字节码文件存放了这个类的各种信息:字段、方法、父类、实现的接口等各种信息

IDEA配置字节码查看方式

在IDEA的Settings里进行相关配置即可,配置截图如下:
在这里插入图片描述
填写的相关属性信息如下:

  1. 其中Name为工具的名称,可以随意填写,这里我命名:ShowByteCode
  2. Program表示你所使用的程序,这里我们选择jdk里面的javap工具。点击文件夹找到路径即可,我的路径为:C:\Program Files\Java\jdk1.8.0_251\bin\javap.exe
  3. Arguments表示你使用Program程序时跟随的参数,这里直接填写-c $FileNameWithoutExtension$.class
  4. Working directory表示当前工作目录,这里直接填写:$OutputPath$\$FileDirRelativeToSourcepath$

配置好以上信息在对应的类右键查看字节码即可:
在这里插入图片描述

IDEA集成Jclasslib

File -> setting -> plugins路径下直接安装jclasslib:
在这里插入图片描述
重启后直接打开view 查看:
在这里插入图片描述
可以直观的看到每个方法调用时局部变量表的内容以及操作数栈的操作,如果用Jclasslib来查看字节码比IDEA集成的看起来更清晰一些,内容如下,

// class version 52.0 (52)
// access flags 0x21
public class com/company/Student extends com/company/Person implements com/company/IStudy {

  // compiled from: Student.java

  // access flags 0xA
  private static I cnt

  // access flags 0x2
  private Ljava/lang/String; sid

  // access flags 0x1
  public <init>(ILjava/lang/String;Ljava/lang/String;)V
   L0
    LINENUMBER 15 L0
    ALOAD 0
    ILOAD 1
    ALOAD 2
    INVOKESPECIAL com/company/Person.<init> (ILjava/lang/String;)V
   L1
    LINENUMBER 16 L1
    ALOAD 0
    ALOAD 3
    PUTFIELD com/company/Student.sid : Ljava/lang/String;
   L2
    LINENUMBER 17 L2
    RETURN
   L3
    LOCALVARIABLE this Lcom/company/Student; L0 L3 0
    LOCALVARIABLE age I L0 L3 1
    LOCALVARIABLE name Ljava/lang/String; L0 L3 2
    LOCALVARIABLE sid Ljava/lang/String; L0 L3 3
    MAXSTACK = 3
    MAXLOCALS = 4

  // access flags 0x1
  public run()V
   L0
    LINENUMBER 21 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "run()..."
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 23 L1
    RETURN
   L2
    LOCALVARIABLE this Lcom/company/Student; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1
  public study(II)I
   L0
    LINENUMBER 26 L0
    BIPUSH 10
    ISTORE 3
   L1
    LINENUMBER 27 L1
    BIPUSH 20
    ISTORE 4
   L2
    LINENUMBER 28 L2
    ILOAD 1
    ILOAD 2
    ILOAD 3
    IMUL
    IADD
    ILOAD 4
    ISUB
    IRETURN
   L3
    LOCALVARIABLE this Lcom/company/Student; L0 L3 0
    LOCALVARIABLE a I L0 L3 1
    LOCALVARIABLE b I L0 L3 2
    LOCALVARIABLE c I L1 L3 3
    LOCALVARIABLE d I L2 L3 4
    MAXSTACK = 3
    MAXLOCALS = 5

  // access flags 0x9
  public static getCnt()I
   L0
    LINENUMBER 32 L0
    GETSTATIC com/company/Student.cnt : I
    IRETURN
    MAXSTACK = 1
    MAXLOCALS = 0

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 38 L0
    NEW com/company/Student
    DUP
    BIPUSH 28
    LDC "tml"
    LDC "20210201"
    INVOKESPECIAL com/company/Student.<init> (ILjava/lang/String;Ljava/lang/String;)V
    ASTORE 1
   L1
    LINENUMBER 40 L1
    ALOAD 1
    ICONST_5
    BIPUSH 6
    INVOKEVIRTUAL com/company/Student.study (II)I
    POP
   L2
    LINENUMBER 42 L2
    INVOKESTATIC com/company/Student.getCnt ()I
    POP
   L3
    LINENUMBER 44 L3
    ALOAD 1
    INVOKEVIRTUAL com/company/Student.run ()V
   L4
    LINENUMBER 46 L4
    RETURN
   L5
    LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
    LOCALVARIABLE s Lcom/company/Student; L1 L5 1
    MAXSTACK = 5
    MAXLOCALS = 2

  // access flags 0x8
  static <clinit>()V
   L0
    LINENUMBER 5 L0
    ICONST_5
    PUTSTATIC com/company/Student.cnt : I
   L1
    LINENUMBER 8 L1
    GETSTATIC com/company/Student.cnt : I
    ICONST_1
    IADD
    PUTSTATIC com/company/Student.cnt : I
   L2
    LINENUMBER 9 L2
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0
}

JVM加载执行字节码文件

在命令行中输入java Student这个命令,就启动了一个java虚拟机,然后加载Student.class字节码文件到内存,然后运行内存中的字节码指令了。这部分的操作就相当于我们在IDEA这样的ide上 点击运行按钮。整个字节码class文件执行的整体宏观如图所示:
在这里插入图片描述
虚拟机JVM负责核心的加载.class文件、将.class文件转为机器码,最终执行机器码。JVM的功能模块主要包括类加载器、执行引擎垃圾回收系统。同时JVM有自己的内存模型。

JVM内存模型

JVM中把内存分为方法区、Java栈、Java堆、本地方法栈、PC寄存器 5部分数据区域:

  1. 方法区:用于存放类、接口、常量以及静态变量等元数据信息,加载进来的字节码数据都存储在方法区
  2. 虚拟机栈 :执行引擎运行字节码时的运行时内存区,采用栈帧的形式保存每个方法的调用运行数据
  3. 本地方法栈:执行引擎调用本地方法时的运行时内存区
  4. Java堆:运行时数据区,各种对象一般都存储在堆上
  5. PC寄存器(程序计数器):功能如同CPU中的PC寄存器,指示要执行的字节码指令。

这部分内容在Jvm运行时内存分析这篇blog详细解析

类加载过程

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化五个阶段,其中验证、准备、解析三个部分统称链接
在这里插入图片描述

各个阶段需要对代码操作内容如下:

  1. 加载阶段
    1)类加载器会在指定的classpath中找到Student.class(通过类的全限定名)这个文件,然后读取字节流中的数据,将其存储在方法区中。
    2)类加载器根据Student.class的信息建立一个Class对象,这个对象比较特殊,一般也存放在方法区中,用于作为运行时访问Student类的各种数据的接口。
  2. 验证阶段:必要的验证工作,格式、语义等
  3. 准备阶段: 为Student中的静态字段分配内存空间,也是在方法区中,并进行零初始化,即数字类型初始化为0,boolean初始化为false,引用类型初始化为null等。private static int cnt=5; 此时,并不会执行赋值为5的操作,而是将其初始化为0。
  4. 解析阶段:由于已经加载到内存了,所以原来字节码文件中存放的部分方法、字段等的符号引用可以解析为其在内存中的直接引用了,而不一定非要等到真正运行时才进行解析。
  5. 初始化阶段:初始化阶段是执行类构造器()方法的过程,是真正意义上开始执行阶段

类加载过程中主要是将class文件(准确地讲,应该是类的二进制字节流)加载到虚拟机内存中,真正执行字节码的操作,在加载完成后才真正开始

运行字节码指令

执行引擎找到main这个入口方法,执行其中的字节码指令,这里就依赖到了Java栈,也就是虚拟机栈,其结构如下:
在这里插入图片描述
每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。执行引 擎运行的所有字节码指令都只针对当前栈帧进行操作

  • 当前正在运行的方法的栈帧位于栈顶,当前方法返回,则当前方法对应的栈帧出栈,当前方法的调用者的栈帧变为栈顶
  • 当前方法的方法体中若是调用了其他方法,则为被调用的方法创建栈帧,并将其压入栈顶。简单查看我们上述方法的运行过程:

注意当存在方法调用关系时遵循以上两个原则

main方法执行

main方法对应的下面是我们执行的代码:

  //方法加载入口
    public static void main(String[] args){

        Student s = new Student(28,"tml","20210201");
        //接口方法调用
        s.study(5,6);
        //成员方法调用
        Student.getCnt();
        //父类重写方法调用 
        s.run();
    }

其对应的字节码指令如下:

  public static void main(java.lang.String[]);
    Code:
       0: new           #7                  // class com/company/Student
       3: dup
       4: bipush        28
       6: ldc           #8                  // String tml
       8: ldc           #9                  // String 20210201
      10: invokespecial #10                 // Method "<init>":(ILjava/lang/String;Ljava/lang/String;)V
      13: astore_1
      14: aload_1
      15: iconst_5
      16: bipush        6
      18: invokevirtual #11                 // Method study:(II)I
      21: pop
      22: invokestatic  #12                 // Method getCnt:()I
      25: pop
      26: aload_1
      27: invokevirtual #13                 // Method run:()V
      30: return

首先会在虚拟机栈中为main方法创建栈帧,局部变量表长度为2,slot0存放参数args,slot1存放局部变量Student s,操作数栈最大深度为5:
在这里插入图片描述

然后执行new#7指令,在java堆中创建一个Student对象,并将其引用值放入main栈帧中
在这里插入图片描述
最后初始化一个对象所需的参数入操作数栈(通过实例构造的方式):

  1. up:复制栈顶的值,然后将复制的结果入操作数栈。
  2. bipush 28:将单字节常量值28入操作数栈。
  3. ldc #8:将#8这个常量池中的常量即tml取出,并入操作数栈。
  4. ldc #9:将#9这个常量池中的常量即20210201取出,并入操作数栈。

入栈后的整体效果如下

在这里插入图片描述

Strudent构造方法执行

invokespecial #10:调用#10这个常量所代表的方法,即Student.<init>()这个方法,这步是为了初始化对象s的各项值,<init>()方法,是编译器将调用父类的<init>()的语句、构造代码块、实例字段赋值语句,以及自己编写的构造方法中的语句整合在一起生成的一个方法。保证调用父类的<init>()方法在最开头,自己编写的构造方法语句在最后,而构造代码块及实例字段赋值语句按出现的顺序按序整合到<init>()方法中。此时需注意:上边从dup到ldc #9这四条指令向栈中添加了4个数据,而Student.()方法刚好也需要4个参数:

 //构造方法
    public Student(int age, String name, String sid){
        //继承父类构造方法
        super(age,name);
        this.sid = sid;
    }

其对应的字节码文件为:

 public com.company.Student(int, java.lang.String, java.lang.String);
    Code:
       0: aload_0
       1: iload_1
       2: aload_2
       3: invokespecial #1                  // Method com/company/Person."<init>":(ILjava/lang/String;)V
       6: aload_0
       7: aload_3
       8: putfield      #2                  // Field sid:Ljava/lang/String;
      11: return

虽然定义中只显式地定义了传入3个参数,而实际上会隐含传入一个当前对象的引用作为第一个参数,所以四个参数依次为this,age,name,sid。上面的4条指令刚好把这四个参数的值依次入栈,进行参数传递,然后调用了Student.<init>()方法,会创建该方法的栈帧,并入栈。栈帧中的局部变量表的第0到4个slot分别保存着入栈的那四个参数值。创建Studet.<init>()方法的栈帧:
在这里插入图片描述

Student.<init>()方法中的字节码指令如下:

  1. aload_0:将局部变量表slot0处的引用值入操作数栈

  2. aload_1:将局部变量表slot1处的int值入操作数栈

  3. aload_2:将局部变量表slot2处的引用值入操作数栈
    在这里插入图片描述

  4. invokespecial #1:调用Person.<init>()方法,同调用Student.<init>过程类似,创建栈帧,将三个参数的值存放到局部变量表等,并且给堆上的对象赋值
    在这里插入图片描述

  5. Person.<init>()返回之后,用于传参的操作数栈的3个值被回收了, Person栈帧的使命完成。

  6. aload_0:将slot0处的引用值入栈,也就是父类构造好的引用。

  7. aload_3:将slot3处的引用值入栈,也就是子类独有的参数sid。

  8. putfield #2:将当前栈顶的值”20210201”赋值给0x2222所引用对象的sid字段,然后两个参数出栈。

  9. return:返回调用方即main()方法,当前方法栈帧出栈,main栈帧上操作数栈上使用的几个slot销毁,只保留了最底部的引用0x222
    在这里插入图片描述

回到main方法中

重新回到main()方法中,继续执行下面的字节码指令:

  public static void main(java.lang.String[]);
    Code:
       0: new           #7                  // class com/company/Student
       3: dup
       4: bipush        28
       6: ldc           #8                  // String tml
       8: ldc           #9                  // String 20210201
      10: invokespecial #10                 // Method "<init>":(ILjava/lang/String;Ljava/lang/String;)V
      13: astore_1
      14: aload_1
      15: iconst_5
      16: bipush        6
      18: invokevirtual #11                 // Method study:(II)I
      21: pop
      22: invokestatic  #12                 // Method getCnt:()I
      25: pop
      26: aload_1
      27: invokevirtual #13                 // Method run:()V
      30: return

astore_1:将当前栈顶引用类型的值赋值给slot1处的局部变量,然后出栈。
在这里插入图片描述

接下来继续执行main方法的相关指令:

  1. aload_1:slot1处的引用类型的值入栈,也就是要开始使用s对象的方法,所以先将引用入栈
  2. iconst_5:将常数5入栈,int型常数只有0-5有对应的iconst_x指令
  3. bipush 6:将变量6入栈
  4. invokevirtual #11:调用虚方法study(),这个方法是重写的接口中的方法,需要动态分派,所以使用了invokevirtual指令。
    在这里插入图片描述

Study方法的执行

构造方法执行完成后,即顺序执行study方法的调用,代码如下:

 //实现接口方法
    public int study(int a, int b){
        int c = 10;
        int d = 20;
        return a+b*c-d;
    }

字节码如下:


  public int study(int, int);
    Code:
       0: bipush        10
       2: istore_3
       3: bipush        20
       5: istore        4
       7: iload_1
       8: iload_2
       9: iload_3
      10: imul
      11: iadd
      12: iload         4
      14: isub
      15: ireturn

最大栈深度3,局部变量表5
在这里插入图片描述

字节码指令执行如下:

  1. bipush 10:将10入栈
  2. istore_3:将栈顶的10赋值给slot3处的int局部变量,即c,出栈。
  3. bipush 20:将20入栈
  4. istore 4:将栈顶的20赋值给slot4处的int局部变量,即d,出栈。上面4条指令,完成对c和d的赋值工作。iload_1、iload_2、iload_3这三条指令将slot1、slot2、slot3这三个局部变量入栈:
    在这里插入图片描述
  5. imul:将栈顶的两个值出栈,相乘的结果入栈,也就是计算b*c:
    在这里插入图片描述
  6. iadd:将当前栈顶的两个值出栈,相加的结果入栈,也就是计算a+b*c
  7. iload 4:将slot4处的int型的局部变量入栈,也就是将d入栈
  8. isub:将栈顶两个值出栈,相减结果入栈,也就是计算a+b*c-d:
    在这里插入图片描述
  9. ireturn:将当前栈顶的值返回到调用方,到这里为止study方法执行完毕,study栈帧出虚拟机栈,main栈帧上的操作数栈清空,方法继续向下执行。

以上study的方法就执行完成了。

回到main方法中

重新回到main()方法中,继续执行下面的字节码指令:

  public static void main(java.lang.String[]);
    Code:
       0: new           #7                  // class com/company/Student
       3: dup
       4: bipush        28
       6: ldc           #8                  // String tml
       8: ldc           #9                  // String 20210201
      10: invokespecial #10                 // Method "<init>":(ILjava/lang/String;Ljava/lang/String;)V
      13: astore_1
      14: aload_1
      15: iconst_5
      16: bipush        6
      18: invokevirtual #11                 // Method study:(II)I
      21: pop
      22: invokestatic  #12                 // Method getCnt:()I
      25: pop
      26: aload_1
      27: invokevirtual #13                 // Method run:()V
      30: return

下面的字节码指令就不再重复画图示意了:

  1. invokestatic #12 调用静态方法getCnt()不需要传任何参数
  2. pop:getCnt()方法有返回值,将其出栈
  3. aload_1:将slot1处的引用值入栈
  4. invokevirtual #13:调用0x2222对象的run()方法,重写自父类的方法,需要动态分派,所以使用invokevirtual指令
  5. return:main()返回,程序运行结束。

这样整体的main方法也执行完毕,整个程序调用完毕。

总结

总结而言,从一个Java程序被编写,最后一直到创建的对象被垃圾回收,全流程包括以下几步,加粗部分为本系列接下来的blog重点讲解内容:

  1. 编辑生成源代码.java文件
  2. 编译(javac编译和jit编译)生成字节码文件
  3. 类文件被加载到虚拟机(类Class文件结构,虚拟机运行时内存分析,类加载机制
  4. 虚拟机执行二进制字节码(虚拟机字节码执行系统
  5. 垃圾回收(JVM垃圾回收机制

虚拟机发挥作用的部分从第3步到第5步之间:

  1. 类加载阶段:一个类文件首先加载到方法区,一些符号引用被解析(静态解析)为直接引用或者等到运行时分派(动态绑定),经过一系列的加载过程(class文件的常量池被加载到方法区的运行时常量池,各种其它的静态存储结构被加载为方法区运行时数据解构等等),程序通过Class对象来访问方法区里的各种类型数据
  2. 字节码执行阶段:当加载完之后,程序发现了main方法,也就是程序入口,那么程序就在栈里创建了一个栈帧,逐行读取方法里的代码所转换为的指令,而这些指令大多已经被解析为直接引用了,那么程序通过持有这些直接引用使用指令去方法区中寻找变量对应的字面量来进行方法操作。
  3. JVM垃圾回收阶段:操作完成后方法返回给调用方,该栈帧出栈。内存空间被GC回收,堆里被new的那些也就被垃圾回收机制GC了。

以上就是整个Java程序的生命周期。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

存在morning

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值