JVM原理

JVM是JavaVirtualMachine的缩写,它是一种规范,通过仿真模拟计算机功能来实现。JVM包括堆、方法区、虚拟机栈等结构,具有跨平台特性。垃圾收集主要针对堆和方法区,使用不同的算法如可达性分析。类加载器遵循双亲委派模型,确保基础类的统一加载。文章还详细介绍了内存区域的划分、垃圾回收机制、类加载的生命周期和类加载器的工作原理。
摘要由CSDN通过智能技术生成

什么是JVM

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
在这里插入图片描述

特点

  1. 跨平台(移植性高)
    Java能够跨平台的重要原因就是得益于JVM。Java 代码被编译成字节码(javac编译),这个字节码在不同的机器上被解释,在主机系统和 Java 源代码之间,字节码是一种中介语言。

  2. 自动内存管理

结构

线程共享

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。
在这里插入图片描述
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。

新生代中由Eden和Survivor0,Survivor1组成,三者的比例是8:1:1,新生代的回收机制采用复制算法,在Minor GC的时候,我们都留一个存活区用来存放存活的对象,真正进行的区域是Eden+其中一个存活区,当我们的对象时长超过一定年龄时(默认15,可以通过参数设置),将会把对象放入老生代,当然大的对象会直接进入老生代,老生代采用的回收算法是标记整理算法。

java -Xms1M -Xmx2M HackTheJava

方法区

用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。

对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。

HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。

方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。

线程私有

虚拟机栈

每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小,在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M:

java -Xss2M HackTheJava
该区域可能抛出以下异常:

当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

本地方法栈

本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。

本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。

程序计数器

记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。

垃圾收集

垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。

判断一个对象是否可被回收

引用计数算法

为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。

在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

可达性分析

以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。

Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:

  • 虚拟机栈中局部变量表中引用的对象
  • 本地方法栈中 JNI 中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象
跨代引用

问题:
从GC Roots出发需要扫描整个Heap空间,MonitorGC耗时长。
采用空间换时间,不需要扫描整个Heap空间,降低MonitorGC耗时,避免遍历整个老年代。
解决:记忆集
记忆集(Remembered Set):一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,在对象层面来说就是非收集区域对象对收集区域对象的引用的记录。

它存放在收集区域,比如在新生代里面存放着老年代对新生代对象的每一个引用。这样在收集新生代的时候,我们就可以根据记忆集知道哪些对象被老年代对象所引用,不能回收,这就解决了跨代引用的问题。

记忆集根据记录的精度分三类:

  • 字长精度:记录的是老年代指向新生代地址。
  • 对象精度:记录的是老年代引用的新生代对象。
  • 卡精度:记录的是新生代一段地址是否存在被老年代引用的记录。

引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。

Java 提供了四种强度不同的引用类型。

  1. 强引用
    被强引用关联的对象不会被回收。
    使用 new 一个新对象的方式来创建强引用。
Object obj = new Object();
  1. 软引用
    被软引用关联的对象只有在内存不够的情况下才会被回收。
    使用 SoftReference 类来创建软引用。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使对象只被软引用关联
  1. 弱引用
    被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
    使用 WeakReference 类来创建弱引用。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
  1. 虚引用
    又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
    为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。
    使用 PhantomReference 来创建虚引用。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;

三色标记

三色标记法是一种垃圾回收法,它可以让JVM不发生或仅短时间发生STW(Stop The World),从而达到清除JVM内存垃圾的目的。
JVM中的CMS、G1垃圾回收器所使用垃圾回收算法即为三色标记法。

三色
  1. 黑色:该对象已经被标记过了,且该对象下的属性也全部都被标记过了。(程序所需要的对象)
  2. 灰色:该对象已经被标记过了,但该对象下的属性没有全被标记完。(GC需要从此对象中去寻找垃圾)
  3. 白色:该对象没有被标记过。(对象垃圾)
流程
  1. 从GC Root开始沿着他们的对象向下查找,用黑灰白的规则,标记出所有跟GC Root相连接的对象
  2. 扫描一遍结束后,一般需要进行一次短暂的STW(Stop The World),再次进行扫描,此时因为黑色对象的属性都也已经被标记过了,所以只需找出灰色对象并顺着继续往下标记(且因为大部分的标记工作已经在第一次并发的时候发生了,所以灰色对象数量会很少,标记时间也会短很多)
  3. 此时程序继续执行,GC线程扫描所有的内存,找出被标记为白色的对象(垃圾)清除
问题
  1. 浮动垃圾:并发标记的过程中,若一个已经被标记成黑色或者灰色的对象,突然变成了垃圾,此时,此对象不是白色的不会被清除,重新标记也不能从GC Root中去找到,所以成为了浮动垃圾,这种情况对系统的影响不大,留给下一次GC进行处理即可。
  2. 对象漏标问题(需要的对象被回收):并发标记的过程中,一个业务线程将一个未被扫描过的白色对象断开引用成为垃圾(删除引用),同时黑色对象引用了该对象(增加引用)(这两部可以不分先后顺序);因为黑色对象的含义为其属性都已经被标记过了,重新标记也不会从黑色对象中去找,导致该对象被程序所需要,却又要被GC回收,此问题会导致系统出现问题,而CMS与G1,两种回收器在使用三色标记法时,都采取了一些措施来应对这些问题,CMS对增加引用环节进行处理(Increment Update),G1则对删除引用环节进行处理(SATB)。

Increment Update:
在一个未被标记的对象(白色对象)被重新引用后,引用它的对象,若为黑色则要变成灰色,在下次二次标记时让GC线程继续标记它的属性对象。
但是就算时这样,其仍然是存在漏标的问题:

  1. 在一个灰色对象正在被一个GC线程回收时,当它已经被标记过的属性指向了一个白色对象(垃圾)
  2. 而这个对象的属性对象本身还未全部标记结束,则为灰色不变
  3. 而这个GC线程在标记完最后一个属性后,认为已经将所有的属性标记结束了,将这个灰色对象标记为黑色,被重新引用的白色对象,无法被标记

SATB(Snapshot At The Beginning):
在应对漏标问题时,G1使用了SATB方法来做:

  1. 在开始标记的时候生成一个快照图标记存活对象
  2. 在一个引用断开后,要将此引用推到GC的堆栈里,保证白色对象(垃圾)还能被GC线程扫描到
  3. 配合Rset,去扫描哪些Region引用到当前的白色对象,若没有引用到当前对象,则回收

对象状态

finalize()
类似 C++ 的析构函数,用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。

当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。
在这里插入图片描述

垃圾回收算法

在这里插入图片描述

方法区的回收

因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。

主要是对常量池的回收和对类的卸载。
为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。
类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:

  • 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 Class对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

垃圾回收器

在这里插入图片描述
常见组合serial+seralOld/ParNew+CMS/Parallel+Parallel Old/G1

CMS

在这里插入图片描述
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。

流程
  1. 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
  2. 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
  3. 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
  4. 并发清除:不需要停顿。

在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。

缺点
  1. 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
  2. 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
  3. 标记 - 清除算法导致的空间碎片,CMS会让Serial Old来清理这些垃圾碎片,而Serial Old是单线程操作进行清理垃圾的,效率偏低。可以使用Mark-Sweep-Compact算法,减少垃圾碎片。因为垃圾往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。

-XX:+UseCMSCompactAtFullCollection 开启CMS的压缩
-XX:CMSFullGCsBeforeCompaction 默认为0,指经过多少次CMS FullGC才进行压缩

G1

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
在这里插入图片描述

流程

在这里插入图片描述

  1. 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
  2. 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
  3. 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
  4. 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
优点
  1. 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
  2. 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
缺点
  1. CMS 在小内存应用上的表现要优于 G1,而大内存应用上 G1 更有优势,大小内存的界限是6GB到8GB。
  2. 用户可掌控性低,像个黑盒
  3. 内存占用和执行负载高

类与类加载器

类文件

类生命周期

加载(Loading)

加载过程完成以下三件事:

  1. 通过类的完全限定名称获取定义该类的二进制字节流。
  2. 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。

其中二进制字节流可以从以下方式中获取:

  • 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
  • 从网络中获取,最典型的应用是 Applet。
  • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
  • 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。

验证(Verification)

  • 文件格式验证:是否按照Class字节码的格式组织的,同时对索引的访问是否越界等
  • 元数据验证:对字节码(Code中的属性)描述的信息进行语义分析,以保证其符合Java语言规范
  • 字节码验证:对数据流和控制流分析,确定程序语义是合法的,而不会在运行时危害虚拟机安全
  • 符号引用验证:在解析阶段发生,主要是验证符号引用转换为直接引用的时候。若无法通过符号引用验证,那么将抛出IncompatibleClassChangeError的子类。符号引用验证在转换为直接引用的时候,还要判断当前类是都对转换的直接引用有访问权限,如果没有,则抛出java.lang.IllegalAccessError异常

准备(Preparation)

类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。

实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。

初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。

public static int value = 123;

如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。例如下面的常量 value 被初始化为 123 而不是 0。

public static final int value = 123;

解析(Resolution)

将常量池的符号引用替换为直接引用的过程。
符号引用指的是在class文件中的ConstantClassinfo,ConstantFieldrefinfo,ConstantMethodrefinfo,ConstantInterfaceMethodrefinfo这四个常量。以及ConstantMethodTypeinfo,ConstantMethodHandleinfo,ConstantInvokedynamicinfo。而这三种常量类型与JDK1.7新增的动态语言支持相关,这里暂时不替换
在这里插入图片描述

初始化(Initialization)

初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>() 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:

public class Test {
    static {
        i = 0;                // 给变量赋值可以正常编译通过
        System.out.print(i);  // 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
}

由于父类的 () 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:

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);  // 2
}

接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法。但接口与类不同的是,执行接口的 () 方法不需要先执行父接口的 () 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 () 方法。

虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都会阻塞等待,直到活动线程执行 () 方法完毕。如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。

初始化的5种情况
  1. 字节码指令:new/getstatic/putstatic/invokestatic
  2. 使用反射对类进行调用
  3. 初始化一个类时,如果父类还未初始化,先触发父类初始化
  4. main方法类初始化
  5. 如果一个MethodHandle实例最后的解析结果是REF_getStatic/REF_putStatic,REF_invokeStatic的方法句柄时,会触发对应的REF类加载

使用(Using)

卸载(Unloading)

类加载器

两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。

这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。

类加载器分类

从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分;

  2. 所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。

从 Java 开发人员的角度看,类加载器可以划分得更细致一些:

  1. 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。

  2. 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。

  3. 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派

应用程序是由三种类加载器互相配合从而实现类加载,除此之外还可以加入自己定义的类加载器。

下图展示了类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
在这里插入图片描述

原理

类加载器中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。

好处

使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一

打破

Tomcat中,部署在同一个服务器上的两个Web应用程序

  • 两个web应用程序使用相同的类全限定名时需要隔离,优先加载当前web应用目录下的类
  • Redis依赖希望Web应用程序之间共享,委托SharedClassLoader去加载
  • 隔绝Web应用程序与Tomcat本身的类,CatalinaClassLoader装载Tomcat本身的依赖
  • Tomcat本身的依赖和Web应用还需要共享,CommonClassLoader装载进而达到共享

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值