垃圾收集入门

11 篇文章 0 订阅

前言:各类默认值都指的是2014年最高版本为Java8时的默认值。

现代 JVM 的类型繁多,最主流的四个垃圾收集器分别是:Serial 收集器(常用于单 CPU 环境)、Throughput(或者 Parallel)收集器、Concurrent 收集器(CMS)和 G1 收集器。

5.1 垃圾收集概述

垃圾回收的步骤:找到不再使用的对象、回收它们使用的内存、对堆的内存布局进行压缩整理

按逻辑将线程分为应用程序线程和处理垃圾收集的线程。

垃圾收集器回收对象,或者在内存中移动对象时,必须确保应用程序线程不再继续使用这些对象。这一点在收集器移动对象时尤其重要:在操作过程中,对象的内存地址会发生变化,因此这个过程中任何应用线程都不应再访问该对象。

所有应用线程都停止运行所产生的停顿被称为时空停顿(stop-the-world)。通常这些停顿对应用的性能影响最大,调优垃圾收集时,尽量将这种停顿时间最小化(minimizing)是关键的考量因素。

5.1.1 分代垃圾收集器

所有的 GC 算法都将堆划分成了老年代(Old Generation 或 Tenured Generation)和新生代(Young Generation)。

新生代又分为 Eden 空间和 Survivor 空间

采用分代机制的原因是很多对象的生存时间非常短。

新生代是堆的一部分,对象首先在新生代中分配

所有的 GC 算法在清理新生代对象时,都使用了“时空停顿”(stop-the-world)方式的垃圾收集方法,通常这是一个能较快完成的操作。

Minor GC(新生代的GC):新生代填满时,垃圾收集器会暂停所有的应用线程,清空新生代Eden空间。不再使用的对象会被回收,仍然在使用的对象会被移动到其他地方(Survivor空间或老年代)

采用这种设计有两个性能上的优势。

其一,由于新生代仅是堆的一部分,与处理整个堆相比,处理新生代的速度更快。而这意味着应用线程停顿的时间会更短,但是应用程序会更频繁地发生停顿

第二,由于所有的对象都被移走,相当于新生代空间在垃圾收集时自动地进行了一次压缩整理

所有的垃圾收集算法在对新生代进行垃圾回收时都存在“时空停顿”(STW)现象。

Full GC:直接停掉所有的应用线程,找出不再使用的对象,对其进行回收,接着对堆空间进行整理。通常导致应用程序线程长时间的停顿

CMS 和 G1 收集器被称为 Concurrent 垃圾收集器,它们不需要停止应用线程就能找出不再用的对象。同时,由于它们将停止应用程序的可能降到了最小,也被称为低停顿(Low-Pause)收集器。Concurrent 收集器也使用各种不同的方法对老年代空间进行压缩**。

使用 CMS 或 G1 收集器时,应用程序经历的停顿会更少(也更短)。其代价是应用程序会消耗更多的 CPU。CMS 和 G1 收集也可能遭遇长时间的 Full GC 停顿(尽量避免发生那样的停顿是这些调优算法要考虑的重要方面)

评估垃圾收集器时,想想你需要达到的整体性能目标。每一个决定都需要权衡取舍。

如果应用对单个请求的响应时间有要求(譬如 Java 企业版服务器),你应该考虑下面这些因素。

  • 单个请求会受到暂停时间的影响——更重要的是受到 full GC 长时间暂停时间的影响。 如果目标是最小化暂停对响应时间的影响,那么 Concurrent 收集器将更合适。
  • 如果平均响应时间比异常值(即 90% 响应时间)更重要,Throughput 收集器通常会产生更好的结果。
  • 使用并发收集器避免长时间暂停的好处是以额外的 CPU 使用为代价的。

类似地,批处理应用程序中垃圾收集器的选择遵循以下权衡

  • 如果 CPU 足够强劲,使用 Concurrent 收集器避免发生 Full GC 停顿可以让任务运行得更快。
  • 如果 CPU 有限,那么 Concurrent 收集器额外的 CPU 消耗会让批量任务消耗更多的时间。

5.1.2 GC算法

1. Serial垃圾收集器

最简单的一种垃圾收集器。Client型虚拟机(Windows 平台上的 32 位 JVM 或者是运行在单处理器机器上的 JVM)上的默认垃圾收集器。

单线程回收新生代老年代

回收新生代(Minor GC)回收老年代(Full GC)都会 STW,Full GC 会压缩整理老年代空间

-XX:-UseSerialGC 启用。

2. Throughput垃圾收集器

Throughput 收集器是 Server 级虚拟机(多 CPU 的 Unix 机器以及任何 64 位虚拟机)的默认收集器。

多线程回收新生代老年代

回收新生代(Minor GC)回收老年代(Full GC)都会 STW,Full GC 会压缩整理老年代空间

Throughput 是 JDK 7u4 及之后的版本的默认收集器

由于 Throughput 收集器使用多线程,Throughput 收集器也常常被称为 Parallel 收集器。

由于在大多数适用的场景,它已经是默认的收集器,所以你基本上不需要显式地启用它。如果需要,可以使用 “-XX:+UseParallelGC -XX:+UseParallelOldGC” 标志启用 Throughput 收集器。(注:Java 9之后默认为 G1)

3. CMS收集器

属于Concurrent收集器:老年代的垃圾收集工作由后台线程完成,大多数的工作不需要暂停应用线程。

CMS 收集器设计的初衷是为了消除 Throughput 收集器和 Serial 收集器 Full GC 周期中的长时间停顿。

多线程回收新生代空间。多后台线程扫描回收老年代。

Minor GC 时会暂停所有的应用线程。收集老年代时不再暂停应用线程,多后台线程扫描回收老年代,不压缩整理。例外是无法获得完成他们任务所需的CPU资源或过度碎片化时,还是回到Serial收集器行为:暂停所有应用线程,使用单线程回收,整理老年代空间

代价是消耗额外的 CPU 周期。

通过 “-XX:+UseConcMarkSweepGC -XX:+UseParNewGC” 标志(默认情况下,这两个标志都是禁用的)可以启用 CMS 垃圾收集器。

4. G1垃圾收集器

属于Concurrent收集器:老年代的垃圾收集工作由后台线程完成,大多数的工作不需要暂停应用线程。

G1 垃圾收集器(或者垃圾优先收集器)的设计初衷是为了尽量缩短处理超大堆(大于 4 GB)时产生的停顿

G1 收集算法将堆划分为若干个区域(Region),不过它依旧属于分代收集器。

多线程回收新生代空间。多后台线程扫描回收老年代。

Minor GC 时会暂停所有的应用线程。G1 收集器属于 Concurrent 收集器:老年代的垃圾收集工作由后台线程完成,大多数的工作不需要暂停应用线程。由于老年代被划分到不同的区域,G1 收集器通过将少数活着的对象从一个区域复制到另一个区域,完成对象的清理工作,这也意味着在正常的处理过程中,G1 收集器实现了堆的压缩整理(至少是部分的整理)。因此,G1 堆发生碎片的可能性要小得多——尽管这仍然是可能的。更不容易Full GC。

代价是消耗额外的 CPU 周期。

通过标志 -XX:+UseG1GC(默认值是关闭的)可以启动 G1 垃圾收集器。

触发及禁用显式的垃圾收集

System.gc() 会触发 Full GC(即使 JVM 使用 CMS 或者 G1 垃圾收集器),应用程序线程会因此而停顿相当长的一段时间。试图通过调用这个方法显式触发 GC 都不是个好主意

快速小结
  1. 这四种垃圾收集算法分别采用了的不同的方法来缓解 GC 对应用程序的影响。

  2. 当只有一个 CPU 可用并且额外的 GC 线程会干扰应用程序时,Serial 收集器才有意义(并且是默认设置)。

  3. Throughput 收集器是其他机器上的默认设置;它最大限度地提高了应用程序的总吞吐量,但可能会使单个操作长时间暂停。

  4. CMS 收集器能够在应用线程运行的同时并行地对老年代的垃圾进行收集。如果 CPU 的计算能力足以支撑后台垃圾收集线程的运行,该算法能避免应用程序发生 Full GC。

  5. G1 收集器也能在应用线程运行的同时并发地对老年代的垃圾进行收集,在某种程度上能够减少发生 Full GC 的风险。G1 的设计理念使得它比 CMS 更不容易遭遇 Full GC。

CMS 和 G1 在并发模式失效、晋升失败等情况下会回退到 Serial Full GC。

\SerialThroughputCMSG1
分代收集器
新生代GC STW
老年代GC STW是(时间长)是(时间长)否/并发模式失效或晋升失败(碎片化)时是(时间长)Mixed GC是(时间短)/并发模式失效或晋升失败(碎片化)时是(时间长)
是否属于 Concurrent 收集器
Young Thread
Old Thread后台多/回退 Serial单线程后台多/回退 Serial单线程
新生代老年代区域单个区域单个区域单个区域多个区域
压缩整理老年代空间Full GC STW 时Full GC STW 时后台清理不会,回退 Serial Full GC会Mixed GC 将存活的从一个Region复制到另一个Region时会,回退 Serial Full GC会

5.1.3 选择GC算法

GC 算法的选择一方面取决于应用程序的特征,另一方面取决于应用的性能目标

判断依据1. 根据应用程序特征选择(选出 Serial和其他)

如果应用程序的内存使用少于 100 MB,选择Serial 收集器。

否则,需要在 Throughput 和 Concurrent 收集器之间做出抉择(要根据性能目标决定)。

判断依据2. 其中,根据性能目标选择(选出 Throughput 和 Concurrent)
性能目标 2.a. 批处理作业的耗时

如果应用程序线程批处理作业占用绝大部分CPU资源,Throughput比 Concurrent 收集器性能更好。

如果应用程序线程批处理作业没有占用绝大部分CPU资源,Concurrent 收集器性能比 Throughput 更好。

性能目标 2.b. 吞吐量及响应时间
  1. 衡量标准是响应时间或吞吐量,在 Throughput 收集器和 Concurrent 收集器之间做选择的依据主要是有多少空闲 CPU 资源能用于运行后台的并发线程。

  2. Throughput 收集器的平均响应时间通常比 Concurrent 收集器的平均响应时间短,但 Concurrent 收集器通常有更低的90%或99%响应时间。

JVM 不进行 Full GC 的时候,Throughput 收集器依然有优势,因为没有后台进程的消耗

  1. 当 Throughput 收集器执行大量 Full GC 时,切换到 Concurrent 收集器通常能获得更低的响应时间。
判断依据3. CMS收集器和G1收集器的决策

一般情况

堆空间小于 4 GB 时,CMS 收集器的性能比 G1 收集器好。CMS 收集器使用的算法比 G1 更简单,因此在比较简单的环境中(譬如堆的容量很小的情况),它运行得更快。

使用大型堆或巨型堆时,由于 G1 收集器可以分割工作,通常它比 CMS 收集器表现更好。

CMS Concurrent Mode Failure 时,会触发单线程 Full GC,性能损耗严重。 G1将老年代分成多个Region,虽然也会发生 Concurrent Mode Failure,但是概率减少。

CMS 除非发生了耗时的Full GC,否则不会对堆进行整理,堆的碎片化也会触发Full GC。G1 算法在处理过程中伴随着对堆的整理,虽然也会遭遇碎片化问题,但是比 CMS领先一步。

随着堆不断增大(发生Full GC的代价更加昂贵),使用G1收集器更容易避免这些问题发生。(这两种收集器的任何一种避免发生并发模式失效都是不可能的任务)

Throughput 是三个收集器中用了最久远的一个,JVM工程师花了大把时间雕琢它,习性也更为大家熟知。

补充:Oracle 官网的GC算法选择文档

除非您的应用程序有非常严格的暂停时间要求,否则请先运行您的应用程序并允许VM选择收集器。

如有必要,请调整堆大小以提高性能。

如果性能仍然不能满足您的目标,请使用以下准则作为选择收集器的起点:

  • 如果应用程序的数据集较小(最大约为100 MB),则选择带有选项的串行收集器-XX:+UseSerialGC。

  • 如果应用程序将在单个处理器上运行,并且没有暂停时间要求,则选择带有选项的串行收集器-XX:+UseSerialGC。

  • 如果(a)峰值应用性能是第一要务,并且(b)没有暂停时间要求,或者可接受一秒或更长时间的暂停,则让VM选择收集器或使用选择 Throughput(parallel) 收集器-XX:+UseParallelGC。

  • 如果响应时间比整体吞吐量更重要,并且必须将垃圾收集暂停时间保持在大约一秒钟以内,那么请使用-XX:+UseG1GC或选择一个concurrent收集器-XX:+UseConcMarkSweepGC。

这些准则只是选择收集器的起点,因为性能取决于堆的大小,应用程序维护的实时数据量以及可用处理器的数量和速度。暂停时间对这些因素特别敏感,因此前面提到的一秒钟的阈值仅是近似值。在许多堆大小和硬件组合上,并行收集器的暂停时间将超过一秒。反过来,在某些情况下,并发收集器也可能无法将暂停时间保持在一秒以内。

如果推荐的收集器没有达到所需的性能,则首先尝试调整堆和世代大小以满足所需的目标。如果性能仍然不足,请尝试使用其他收集器:使用concurrent收集器来降低暂停时间,并使用parallel收集器来增加多处理器硬件上的总体吞吐量。

Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide

5.2 GC调优基础

5.2.1 调整堆的大小

如果分配的堆过于小,程序的大部分时间可能都消耗在 GC 上,没有足够的时间去运行应用程序的逻辑。

GC 停顿消耗的时间取决于堆的大小,分配的堆太大,停顿频率变少,但是持续的时间变长,让程序的整体性能变慢。

使用超大堆还有另一个风险。操作系统使用虚拟内存机制管理机器的物理内存,JVM不了解分配在磁盘的堆空间,导致严重的性能问题,因为操作系统需要将相当一部分的数据由磁盘交换到内存(这是一个昂贵操作的开始)。在FULL GC时这种内存交换会重演,停顿时间会以正常停顿时间数个量级的方式增长。如果使用 Concurrent 收集器,后台线程在回收堆时,它的速度也可能会被拖慢,因为需要等待从磁盘复制数据到内存,结果导致发生代价昂贵的并发模式失效。

因此,调整堆大小时首要的原则就是永远不要将堆的容量设置得比机器的物理内存还大,另外,如果同一台机器上运行着多个 JVM 实例,这个原则适用于所有堆的总和。除此之外,你还需要为 JVM 自身以及机器上其他的应用程序预留一部分的内存空间:通常情况下,对于普通的操作系统,应该预留至少 1 G 的内存空间。

堆的大小由 2 个参数值控制:分别是初始值(通过 -XmsN 设置)和最大值(通过 -XmxN 设置)(maximum)

堆大小的调节是 JVM 自适应调优的核心。

JVM 会根据其运行的机器,尝试估算合适的最大、最小堆的大小。

表5-4:默认堆的大小

操作系统及JVM类型初始堆的大小(Xms)最大堆的大小(Xmx)
Linux/Solaris,32 位客户端16 MB256 MB
Linux/Soaris,32 位服务器64 MB取 1 GB 和物理内存大小 1/4 二者中的最小值
Linux/Soaris,64 位服务器取 512 MB 和物理内存大小 1/64 二者中的最小值取 32GB 和物理内存大小 1/4 二者中的最小值
MacOS,64 位服务器型 JVM64 MB取 1 GB 和物理内存大小 1/4 二者中的最小值
32 位 Window 系统,客户端型 JVM16 MB256 MB
64 位 Window 系统,服务器型 JVM64 MB1 GB 和物理内存大小 1/4 二者中的最小值

如果机器的物理内存少于 192 MB,最大堆的大小会是物理内存的一半(大约 96 MB,或者更少)。

堆的大小具有初始值和最大值的这种设计让 JVM 能够根据实际的负荷情况更灵活地调整 JVM 的行为。如果 JVM 发现使用初始的堆大小,频繁地发生 GC,它就会尝试增大堆的空间,直到 JVM 的 GC 的频率回归到正常的范围,或者直到堆大小增大到它的上限值。

除非应用程序需要比默认值更大的堆,否则在进行调优时,尽量考虑通过调整 GC 算法的性能目标,而非微调堆的大小来改善程序性能。

5.2.2 代空间的调整

如果新生代分配得比较大,垃圾收集发生的频率就比较低,从新生代晋升到老年代的对象也更少。任何事物都有两面性,采用这种分配方法,老年代就相对比较小,比较容易被填满,会更频繁地触发 Full GC。这里找到一个恰当的平衡点是解决问题的关键。

所有用于调整代空间的命令行标志调整的都是新生代空间;新生代空间剩下的所有空间都被老年代占用。多个标志都能用于新生代空间的调整,它们分别如下所列。

-XX:NewRatio=N

设置新生代与老年代的空间占用比率。

-XX:NewSize=N

设置新生代空间的初始大小。

-XX:MaxNewSize=N

设置新生代空间的最大大小。

-XmnN

将 NewSize 和 MaxNewSize 设定为同一个值的快捷方法。

最初新生代空间大小是由 NewRatio 指定大小,NewRatio 的默认值为 2。影响堆空间大小的参数通常以比率的方式指定;这个值被用于一个计算空间分配的公式之中。下面是使用 NewRatio 计算空间的公式:

Initial Young Gen Size = Initial Heap Size / (1 + NewRatio)

代入堆的初始大小和 NewRatio 的值就能得到新生代的设置值。那么我们很容易得出,默认情况下,新生代空间的大小是初始堆大小的 33%

除此之外,新生代的大小也可以通过 NewSize 标志显式地设定。使用 NewSize 标志设定的新生代大小,其优先级要高于通过 NewRatio 计算出来的新生代大小。NewSize 标志没有默认的设置(虽然使用 Printflagsfinal 标志输出的值为 1 MB)。NewSize 不设置的情况下,初始的新生代大小由 NewRatio 计算出的值决定。

如果堆的大小扩张,新生代的大小也会随之增大,直到由 MaxNewSize 标志设定的最大容量。默认情况下,新生代的最大值也是由 NewRatio 的值设定的,不过它也同时受制于堆的最大容量(注意,不是初始大小)。

试图通过指定新生代的最大及最小值区间的方式来调优新生代最终是十分困难的。如果堆的大小是固定的(可以通过将 -Xms 和 -Xmx 指定为相等的值实现),通常推荐使用 -Xmn 标志将新生代也设定为固定大小。如果应用程序需要动态调整堆的大小,并希望有一个更大(或者更小)的新生代,那就需要关注 NewRatio 值的设定

5.2.3 永久代和元空间的调整

JVM 载入类的时候,它需要记录这些类的元数据。

在 Java 7 里,这部分空间被称为永久代(Permgen 或 Permanent Generation),在 Java 8 中,它们被称为元空间(Metaspace)。

不过永久代和元空间并不完全一样。Java 7 中,永久代还保存了一些与类数据无关的杂项对象(miscellaneous object);这些对象在 Java 8 中被挪到了普通的堆空间内

注意永久代或者元空间内并没有保存类实例的具体信息(即类对象),也没有反射对象(譬如方法对象);这些内容都保存在常规的堆空间内。永久代和元空间内保存的信息只对编译器或者 JVM 的运行时有用,这部分信息被称为“类的元数据”

永久代或者元空间的大小与程序使用的类的数量成比率相关,应用程序越复杂,使用的对象越多,永久代或者元空间就越大。使用元空间替换掉永久代的优势之一是我们不再需要对其进行调整——因为(不像永久代)元空间默认使用尽可能多的空间。表 5-5 列出了永久代和元空间的初始值及最大值。

表5-5:永久代/元空间的默认大小

JVM类型默认的初始大小默认永久代大小的最大值默认元空间大小的最大值
32 位客户端型 JVM12 MB64 MB没有限制
32 位服务器型 JVM16 MB64 MB没有限制
64 位 JVM20.75 MB82 MB没有限制

这些内存区域的行为就像是分隔开的普通堆空间。它们会根据初始的大小动态地调整,需要的时候会增大到最大的堆空间。

对于永久代而言,可以通过 -XX:PermSize=N、-XX:MaxPermSize=N 标志调整大小

而元空间的大小可以通过 -XX:MetaspaceSize=N 和 -XX:MaxMetaspaceSize=N 调整

由于元空间默认的大小是没有作限制的,因此 Java 8(尤其是 32 位系统)的应用可能由于元空间被填满而耗尽内存。如果元空间增长得过大,通过设置 MaxMetaspaceSize 你可以调整元空间的上限,将其限制为一个更小的值,不过这又会导致应用程序最后由于元空间耗尽,发生 OutOfMemoryError 异常。解决这类问题的终极方法还是定位出为什么类的元空间会变得如此巨大。

调整这些区间会触发 Full GC,所以是一种代价昂贵的操作。如果程序在启动时发生大量的 Full GC(因为需要载入数量巨大的类),通常都是由于永久代或者元空间发生了大小调整,因此这种情况下为了改善启动速度,增大初始值是个不错的主意。对于定义了大量类的 Java 7 应用,同时还需要增大永久代空间的最大值。譬如,通常情况下应用服务器永久代的最大值会设置为 128 MB、192 MB 或者更多。

虽然名称叫“永久代”,保存在永久代空间中的数据并不能永久保存(元空间这个名字可能更准确)。开发中的应用服务器(或者任何需要频繁重新载入类的环境)上经常能碰到由于永久代或元空间耗尽触发的 Full GC,这时老的元数据会被丢弃回收。

5.2.4 控制并发

除 Serial 收集器之外几乎所有的垃圾收集器使用的算法都基于多线程。启动的线程数由 -XX:ParallelGCThreads=N 参数控制。

这个参数值会影响以下操作的线程数目:

  • 使用 -XX:+UseParallelGC 收集新生代空间
  • 使用 -XX:+UseParallelOldGC 收集老年代空间
  • 使用 -XX:+UseParNewGC 收集新生代空间
  • 使用 -XX:+UseG1GC 收集新生代空间
  • CMS 收集器的“时空停顿”阶段(phases)(但并非 Full GC)< Stop-the-world phases of CMS (though not full GCs)>
  • G1 收集器的“时空停顿”阶段(phases)(但并非 Full GC)< Stop-the-world phases of G1 (though not full GCs)>

几乎所有的垃圾收集算法中基本的垃圾回收线程数都依据机器上的 CPU 数目计算得出,有一个根据CPU计算线程数的公式。 注意,这个标志不会对 CMS 收集器或者 G1 收集器的后台线程数作设定(虽然它们也会受此设置的影响)。

多个 JVM 运行于同一台物理机上时,依据公式计算出的线程数可能过高,必须进行优化(减少)

5.2.5 自适应调整(UseAdaptiveSizePolicy)

根据调优的策略,JVM 会不断地尝试,寻找优化性能的机会,所以在 JVM 的运行过程中,堆、代以及 Survivor 空间的大小都可能会发生变化。

JVM 在堆的内部如何调整新生代及老年代的百分比是由自适应调整机制控制的。

通常情况下,我们应该开启自适应调整,因为垃圾回收算法依赖于调整后的代的大小来达到它停顿时间的性能目标。

对于已经精细调优过的堆,关闭自适应调整能获得一定的性能提升。

使用 -XX:-UseAdaptiveSizePolicy 标志可以在全局范围内关闭自适应调整功能(默认情况下,这个标志是开启的)。

如果想了解应用程序运行时 JVM 的空间是如何调整的,可以设置 -XX:+PrintAdaptiveSizePolicy 标志。开启该标志后,一旦发生垃圾回收,GC 的日志中会包含垃圾回收时不同的代进行空间调整的细节信息。

5.3 垃圾回收工具

GC 日志是分析 GC 相关问题的重要线索;我们应该开启 GC 日志标志(即使是在生产服务器上)。

使用 PrintGCDetails 标志能获得更详尽的 GC 日志信息。

使用工具能很有效地帮助我们解析和理解 GC 日志的内容,尤其是在对 GC 日志中的数据进行归纳汇总时,它们非常有帮助。

使用 jstat 能动态地观察运行程序的垃圾回收操作。

5.4 小结

对任何一个 Java 应用程序而言,垃圾收集的性能都是其构成整体性能的关键一环。虽然对大多数的应用程序来说,调优的工作仅仅是选择合适的垃圾收集算法,或者在需要的时候,增大应用程序的堆空间。

自适应调整让 JVM 能够自动地调整它的行为,使用给定的堆,提供尽可能好的性能。

更复杂的应用往往需要额外的调优,尤其是针对特定 GC 算法的调优。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值