G1垃圾收集器的应用调研和实践- JNI Weak Reference Cost Too Long

以下的一些理论主要用于个人的回顾使用,如需看案例,可以直接跳到下面的应用部分。

G1收集器的处理方式也不是一蹴而就的,从它身上还是可以看到很多之前收集器的身影在上面。比如:逻辑分代、晋升、复制、并行清理、并发分段标记(CMS)等思想中借鉴而来的,较适合大内存的机器应用,比如8c16G。之所以要用大内存,那是因为它的主要核心还是空间换时间的做法。核心主要有以下几点

  • Region区域的划分搭配CSET使得单次gc关注的容量变小
  • RSet和Cardtable使得标记过程扫描空间变小
  • 三色标记和SATB实现增量标记
  • 在MIXED GC中对于垃圾空间清理的取舍,单次清理的垃圾变小,垃圾占比小的region不清理(Region活跃度)

基本概念


  • Cardtable: Cardtable是为了减少扫描区域而设计的数据结构,早在G1出现之前就有应用,在分代的思想中,就有老年代担保分配空间这种机制,那么问题就来了,如果对象分配到老年代,而YGC只扫描新生代,是否可行呢 ? 老年代中的对象对年轻代的对象有引用,岂不是会发生误杀新生代对象这种事情么(只扫描新生代不可达,扫描了老年代发现可达。)? 但是,全部扫描整个堆空间无疑是性能非常差的,此时,CardTable就派上了用场。**它是一种记录了引用关系的数据结构,在分代中记录的是老年代对新生代的引用,而在G1中则是记录了Region的对象之间的引用关系。以这种方式减少了扫描的范围,提高性能,解决可能的误杀问题。**详情请查看 https://segmentfault.com/a/1190000004682407
    在这里插入图片描述

  • Region: Region是G1垃圾收集器堆内存中的内存分配单位,确切的说,它是一种数据结构。因为在它的内部还有子结构Rset和一些Region的属性。根据分代分为Eden、Sur、Old3种Region分类,根据大小又单独定义了Humongous,当对象大小>=所设定的Region的一半,Region会被标记为Humongous对象,并且直接在老年代分配,避免了晋升copy的耗时。相关图示如下:

在这里插入图片描述

  • RSet: Rset全称为Remenbered Set,它是Region的一个子结构,它记录了当前Region的对象被其他哪些Region引用了,**本质结构是一个hashtable, key是对当前Region有引用关系的其他Region,而Value则是一个CardTable的描述结构。**Cardtable可以查看上面的概念描述.为了便于理解,我将我理解的java伪代码和对应贴图贴在下面.
    在这里插入图片描述

    //- 为新对象创建一个Region的伪代码
    Region RegionA = new Region();
    Table rset = new HashTable<Region,Cardtable>();
    RegionA.setRSet(rest)
    
    //- 分配对象时,如有跨区域引用,则有对应的cardtable和rset的设置
    CardtableB.setdirtyCard(y)
    rset.put(regionB,CardtableB);
    CardtableC.setdirtyCard(y)
    rset.put(regionC,CardtableC);
    
    //- 设置对象,发生引用关系变更时则可能有
    Cardtable.setDirtyCard(n);
    
    • RegionB和RegionC中有对象引用了RegionA的对象。
    • Cardtable B描述了RegionB中哪些空间可能对RegionA有引用关系.
  • CSet: CSet是单次GC中的中间结构,它保存单次GC(包含YOUNG GC和MIXED GC)要处理的Region集合.在GC之后,这些Region中存活的对象会copy到Sur Region中或晋升至Old Region中。值得注意的是CSet中的Region会随着用户所设定的预期停顿时间而变更。预期停顿时间越短,cset集合中的Region则可能越少,反之正相反。相关图解,请在下面的GC过程图解中关注

  • 三色标记算法:GC 开始运行前所有的对象都是白色。当GC 开始运行,所有从根能到达的对象都会被标记。GC 只是发现了这样的对象,但还没有搜索完它们,所以这些对象就 成了灰色对象。当其所有的子对象(也就是字段)都被涂成 灰色时,对象就会被涂成黑色。 当 GC 结束时已经不存在灰色对象了,活动对象全部为黑色,垃圾则为白色。 这就是三色标记算法的概念。摘抄出自:https://segmentfault.com/a/1190000039300766,该博主总结的很棒,在此不再赘述了

  • SATB: (SNAPSHOT AT THE BEGINNING.): **增量式GC算法,服务于最终标记阶段,其本质是记录并发标记阶段开始到并发标记阶段结束这段时间内所产生的引用关系变更。**由于三色标记的算法。使得黑色的对象(认为已经完成了搜索),不会再次去搜索其引用关系。SATB是引用写屏障的方式解决该问题,既在字节码中插入对应写屏障,在栈上分配对象时,也在GC上进行相应标记。

GC过程


在这里插入图片描述

  • 初始标记:只是标记GC ROOT所直接引用的对象。比如栈中引用到的对象和静态引用对象。这些对象所在的Region放入CSET中,这些REGION我们又称为ROOT Region,这个阶段是要STW的,但是所扫描的范围较小,其实,这个阶段我们就已经享受到了G1给我们动态调整Eden区域的福利。要知道Eden越小、stw越短

  • 并发标记:这个阶段是三色标记的主要标记期,它的执行过程是通过ROOT REGION的RSet去关联其他所有可以关联REGION中的对象,以此来提高效率。本阶段所扫描标记到的Region也累加性的放入CSET中。这个阶段是非STW的,会和用户线程一起运行。但是,这个阶段不会太长,过长的并行会导致下一阶段需要增量标记的容量过高。要知道下一阶段是STW的,这等于是变相损耗

  • 重新标记:这个阶段主要是处理并发阶段所产生的增量引用,这个增量引用,需要通过SATB来弥补三色标记法的漏洞,具体推荐大家看这篇文章,通俗易懂https://segmentfault.com/a/1190000039300766,增量标记虽然不是第一次提出,但是这也是G1的优化点,因为它和CMS的算法不同,同样,这个阶段也需要STW

  • 复制清理:复制清理这个阶段主要的优化点是“G1为了避免清理复制所产生的耗时,在Region的垃圾占用率上的做了取舍。即:当OLD区单个REGION达到清理阈值才会去清理,否则,留到下一次进行清理。但是YOUNG和SUR区域是会全部清理不可达Region的

G1应用


相关参数解析
  • -XX:MaxGCPauseMillis=200:预期停顿其实是一种动态设置机制,它其实是通过动态调整对应区域大小来获取stw和吞吐的算法,需要注意的是,如果你显示的指定了 -Xmn 选项或 -XX:NewRatio,那么,这个预期将失效!,比如新生代的Eden和Sur大小的过程。其实GC的本质没有变更,扫描越大的空间,必然导致STW变长,而扫描越小的空间,会增加对应的频率,从而影响吞吐。G1的这种做法虽然屏蔽了相应的参数设置复杂度。**但是,实际使用的使用不建议将该值设置过小或过大,比如:设置100ms,那也不是每次的GC时间都在这个时间段。此值设置过小,引起的频率飙升,在并发高时,甚至导致应用不可用。请谨慎设置!!**在stw停顿时长和扫描空间大小之间做取舍。-XX:GCTimeRatio参数,同样是一个预期吞吐的参数。我想,其原理MaxGCPauseMillis大致相同,本篇不过多说明。感兴趣的同学可私下了解。
  • -XX:+G1SummarizeRSetStats :如果CSets包含许多携带coarsened RSets的Region(注意,“coarsening of RSets”是根据RSets贯穿不同级别颗粒度的过渡期定义的),然后你会看到扫描RSets消耗时间的增长。GC阶段这些扫描时间就是GC日志里面的“Scan RS (ms)”。如果RSets扫描时间相当于GC阶段总时间很高,或者你的应用中它们表现很高,然后通过使用诊断选项**-XX:+G1SummarizeRSetStats请观察你的 Young GC 日志输出的“Did xyz coarsenings”(你可以通过设置-XX:G1SummarizeRSetStatsPeriod=period**指定周期频率报告(GCs的数量))。from https://segmentfault.com/a/1190000007815623
  • -XX:InitiatingHeapOccupancyPercent:简单来说,是老年代达到堆空间占用的多少开始MIXED GC。该值如果设置过小,则会一直循环触发MIXED GC。引用说明:通过-XX:G1HeapWastePercent指定触发Mixed GC的堆垃圾占比,默认值5%,也就是在全局标记结束后能够统计出所有Cset内可被回收的垃圾占整对的比例值,如果超过5%,那么就会触发之后的多轮Mixed GC,如果不超过,那么会在之后的某次Young GC中重新执行全局并发标记。可以尝试适当的调高此阈值,能够适当的降低Mixed GC的频率。 https://zhuanlan.zhihu.com/p/181305087
  • -XX:G1HeapWastePercent:如果ROOT REGION标记结束后,垃圾占比超过堆的X%,那么,此时,启用mixed gc。引用原文通过-XX:G1HeapWastePercent指定触发Mixed GC的堆垃圾占比,默认值5%,也就是在全局标记结束后能够统计出所有Cset内可被回收的垃圾占整对的比例值,如果超过5%,那么就会触发之后的多轮Mixed GC,如果不超过,那么会在之后的某次Young GC中重新执行全局并发标记。可以尝试适当的调高此阈值,能够适当的降低Mixed GC的频率。https://zhuanlan.zhihu.com/p/181305087
  • -XX:SoftRefLRUPolicyMSPerMB:当软引用占比达到{X} M,就开始回收掉这些软引用,大部分在于业务code中有反射生成的多个软引用占用内存空间。多表现在META SPACE的占比。这个值不能设置为0,也不能太小。它是让JVM提前优化反射生成的类。个人不推荐设置。由JVM自己控制。出自:https://blog.csdn.net/qiang_zi_/article/details/100700784
日志解析
2021-08-23T15:14:35.787-0800: [GC pause (G1 Evacuation Pause) (young) 134.253: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 0, predicted base time: 10.39 ms, remaining time: 139.61 ms, target pause time: 150.00 ms]
 134.253: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 574 regions, survivors: 0 regions, predicted young region time: 0.59 ms]
 134.253: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 574 regions, survivors: 0 regions, old: 0 regions, predicted pause time: 10.97 ms, target pause time: 150.00 ms]
, 0.0937042 secs]
   [Parallel Time: 20.4 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 134254.7, Avg: 134257.3, Max: 134259.1, Diff: 4.4]
      [Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.7, Diff: 0.7, Sum: 0.8]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.1, Max: 1, Diff: 1, Sum: 1]
      [Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.5]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.2]
      [Termination (ms): Min: 0.0, Avg: 4.2, Max: 11.3, Diff: 11.3, Sum: 34.0]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 8]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.5, Max: 3.7, Diff: 3.7, Sum: 4.0]
      [GC Worker Total (ms): Min: 0.1, Avg: 4.9, Max: 11.4, Diff: 11.3, Sum: 39.5]
      [GC Worker End (ms): Min: 134259.2, Avg: 134262.3, Max: 134269.2, Diff: 10.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 1.1 ms]
   [Other: 72.2 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 70.3 ms]
      [Ref Enq: 0.1 ms]
      [Redirty Cards: 0.2 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.6 ms]
   [Eden: 9184.0M(9184.0M)->0.0B(9184.0M) Survivors: 0.0B->0.0B Heap: 9185.6M(10.0G)->1682.0K(10.0G)]
 [Times: user=0.05 sys=0.01, real=0.10 secs] 
  • G1自动调整分区大小,展开的cardtable:0,预计需要时间10.39ms,134.253单位为秒,是jvm启动到现在持续的时间。
  • 将GC ROOTS关联的相关REGION添加到CSET中,整个EDEN区共574个REGION,预计持续0.59ms
  • 结束CSET收集阶段,预计耗时10.97ms
  • Parallel Time: 20.4 ms, GC Workers: 8:并行阶段,worker线程8个,以下表示的min,max和avg都是以线程为单位进行描述的。因为每个线程的操作时间都不一样。
    • [GC Worker Start (ms): Min: 134254.7, Avg: 134257.3, Max: 134259.1, Diff: 4.4]:gc worker并行阶段开始的时间,这里的时间是说距离jvm启动到现在距离的时间,其中,min是线程最早开始时间,max是线程最晚开始时间avg是平均开始时间,这里的diff是max-min
    • Ext Root Scanning:扫描root集合所花费的时间(线程栈、jni、全局变量、系统表等等),花费的时间,其中sum是cpu总共花费的时间
    • Update RS:根据上文描述,每个分区都有自己的rset,用来记录其他region对当前region的引用。但是,在并行收集的过程中,region的rset是一个互斥的区域,由此,rset的更新情况只能借助另外一个日志来存储对应的post-write barrier变更,新的应用变更会标记为dirty card,对应的dirty card处理有另外一个线程,在重新标记阶段会处理这些log buffer以达到更新rset为最新的记录
      • Processed Buffers:表示并行扫描阶段产生了多少个写屏障的dirty card
    • Scan RS:找出rset的用时,找出region中有关联当前cset的region
    • Code Root Scanning:code root指的是jit编译后代码里面引用了heap的对象,扫描该区域花费的时间。
    • Object Copy:拷贝gc完成后,存活对象到新的region的耗时
    • Termination:gc结束时,已经处理完任务的线程尝试查看有没有待处理的任务(其他线程没有执行完的),此时,去“窃取”其他线程未完成的任务,这个阶段有点类似fork/join线程池的窃取机制
      • Termination Attempts:如果一个线程成功窃取了其他线程的工作任务,每次处理完之后,它会重新尝试窃取或者终止。每一次尝试终止,这个时间就会增加。
    • GC Worker Other:其他未能列出的耗时
    • GC Worker Total:每个垃圾收集线程的最小、最大、平均、差值总共时间。
    • GC Worker End:min表示最早结束的垃圾收集线程结束时该JVM启动后的时间;max表示最晚结束的垃圾收集线程结束时该JVM启动后的时间。理想情况下,你希望它们快速结束,并且最好是同一时间结束。
  • Code Root Fixup:清理GC期间产生的数据结构耗时
  • Code Root Purge:清理更多的数据结构
  • Clear CT:清理GC完成之后清理无效的Card Table.
  • Other:
    • Choose CSet:选择放入CSET的Region所消耗的时间,该选择是筛选Region活跃度的过程
    • Ref Proc :引用入ReferenceQueues
    • Ref Enq:遍历所有的引用,将不能回收的放入pending列表
    • Redirty Cards:在回收过程中,被修改的card重新设置为dirty脏卡,以在下次GC之前,重新标记该card
    • Humongous Register:大对象分配耗时,JDK8u60提供了一个特性,巨型对象可以在新生代收集的时候被回收——通过G1ReclaimDeadHumongousObjectsAtYoungGC设置,默认为true。
    • Humongous Reclaim:做下列任务的时间:确保巨型对象可以被回收、释放该巨型对象所占的分区,重置分区类型,并将分区还到free列表,并且更新空闲空间大小。
    • Free CSet:释放CSET中的region到freelist
预发布应用的实践效果

  • 预发布参数设置

    • 之前
    -Xms2048m
    -Xmx2048m
    -Xmn768m
    -Xss512K
    -XX:PermSize=256m
    -XX:MaxPermSize=256m
    -XX:SurvivorRatio=8
    -XX:InitialTenuringThreshold=8
    -XX:MaxTenuringThreshold=8
    -XX:ParallelGCThreads=4
    -XX:TargetSurvivorRatio=70
    -XX:+UseBiasedLocking
    -XX:-UseAdaptiveSizePolicy
    -verbose:gc 
    -Xloggc:../gc.log
    -XX:+PrintGCDetails
    -XX:+PrintGCDateStamps
    -XX:+PrintAdaptiveSizePolicy
    -XX:+PrintTenuringDistribution
    -XX:+PrintReferenceGC
    -Djava.awt.headless=true
    -Dsun.net.client.defaultConnectTimeout=60000
    -Dsun.net.client.defaultReadTimeout=60000
    -Djmagick.systemclassloader=no
    -Dnetworkaddress.cache.ttl=300
    -Dsun.net.inetaddr.ttl=300
    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=./java_HeapDump.hprof"
    
    • 之后
    -Xms2G 
    -Xmx2G 
    -XX:MetaspaceSize=128M 
    -XX:MaxMetaspaceSize=128M 
    -XX:+UseG1GC 
    -XX:G1HeapRegionSize=1M 
    -XX:G1MaxNewSizePercent=90 
    -XX:MaxGCPauseMillis=150 
    -XX:+G1SummarizeRSetStats 
    -XX:+ParallelRefProcEnabled 
    -XX:ParallelGCThreads=2
    -XX:+UnlockExperimentalVMOptions 
    -XX:+UnlockDiagnosticVMOptions 
    -XX:InitiatingHeapOccupancyPercent=3 
    -XX:G1HeapWastePercent=1 
    -XX:SoftRefLRUPolicyMSPerMB=1 
    -XX:+PrintGCDetails
    -XX:+PrintAdaptiveSizePolicy
    -XX:+PrintGCDateStamps
    -Dcom.sun.management.jmxremote 
    -Dcom.sun.management.jmxremote.port=52001 
    -Dcom.sun.management.jmxremote.authenticate=false 
    -Dcom.sun.management.jmxremote.ssl=false
    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=./java_HeapDump.hprof
    -verbose:gc -Xloggc:../gc.log"
    
  • 更改之前的参数效果
    在这里插入图片描述

  • 更改后的效果
    在这里插入图片描述

  • 结论:参数可用,但在2C4G小内存的表现下,G1没有parallel表现好。

  • 单次YGC时间变长,频率变低,但是,没有了FULL GC。

生产环境实践G1的效果

  • 更改前的参数
-Xms2048m
-Xmx2048m
-Xmn768m
-Xss512K
-XX:PermSize=256m
-XX:MaxPermSize=256m
-XX:SurvivorRatio=8
-XX:InitialTenuringThreshold=8
-XX:MaxTenuringThreshold=8
-XX:ParallelGCThreads=4
-XX:TargetSurvivorRatio=70
-XX:+UseBiasedLocking
-XX:-UseAdaptiveSizePolicy
-verbose:gc 
-Xloggc:../gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintAdaptiveSizePolicy
-XX:+PrintTenuringDistribution
-XX:+PrintReferenceGC
-Djava.awt.headless=true
-Dsun.net.client.defaultConnectTimeout=60000
-Dsun.net.client.defaultReadTimeout=60000
-Djmagick.systemclassloader=no
-Dnetworkaddress.cache.ttl=300
-Dsun.net.inetaddr.ttl=300
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./java_HeapDump.hprof"
  • 更改后的参数
-Xms10G
-Xmx10G
-XX:+UseG1GC
-XX:MetaspaceSize=512M
-XX:MaxMetaspaceSize=512M
-XX:+UnlockExperimentalVMOptions
-XX:+UnlockDiagnosticVMOptions
-XX:G1HeapRegionSize=16M
-XX:G1MaxNewSizePercent=70
-XX:MaxGCPauseMillis=150
-XX:+G1SummarizeRSetStats
-XX:+ParallelRefProcEnabled
-XX:ParallelGCThreads=8
-XX:InitiatingHeapOccupancyPercent=3
-XX:G1HeapWastePercent=1
-XX:+PrintGCDetails
-XX:+PrintAdaptiveSizePolicy
-XX:+PrintGCDateStamps
-XX:+PrintReferenceGC
-XX:SoftRefLRUPolicyMSPerMB=500
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./java_HeapDump.hprof
-verbose:gc
-Xloggc:../gc.log
  • 更改前的GC效果
    在这里插入图片描述

  • 第一阶段更改后的效果(STW 60-140MS)
    在这里插入图片描述

  • 最终更改后的效果图(STW 10-20ms)
    在这里插入图片描述

遇到问题和解决过程 JNI Weak Reference


从上图中,可以看到,第一阶段换G1GC以后,效果并没有那么明显。优化前单次GC的时间是10-20ms. 而优化后,居然STW到了60-140ms,这显然是不能接受的,很多帖子说增加-XX:+ParallelRefProcEnabled参数来并行处理引用,但是,我的参数中是有这个的,显然不是这个问题。于是查看GC LOG,如下图:
在这里插入图片描述

这个Ref Proc是引用入队,这其中JNI Weak Reference耗时最长,这个JNI Weak Reference是什么东西 ? 果断jmap -histo
在这里插入图片描述

  • 看到这里,我只能黔驴技穷了。问题被搁置,但是,经验告诉我不能放弃。因为,之前我有过实践G1,YGC的STW不会这么长时间!!

  • 不能去查看C语言编写的jvm内部到底分配了什么对象,翻看网上的一些帖子,看到"你假笨"骚操作,居然更改jvm实现,打印出来这东西是什么。对应的帖子:https://www.heapdump.cn/article/62972,文章中说,用java调用了js,我赶紧去看项目,但是,项目中并没有这样的实现。

  • 没有办法,线索断了,就连笨神都只能用修改jvm这样的办法来找问题。在这样的情况下,我只能好好的去看看JNI Reference这东西到底是什么?试着去联想。然而,我是幸运的。项目中接入了公司自研的agent监控程序,其实现用C语言调用jvmti来实现相关监控。想到这里,我基本上就定位了问题。尝试去卸载一台监控。问题得到了解决。在8C16G的G1环境中继续保持10-20ms的stw时间,但是堆增大到10G空间,减少了对应的GC频率,并提高了整体吞吐!

参考文献


https://tech.meituan.com/2016/09/23/g1.html 美团技术团队

https://www.oracle.com/cn/technical-resources/articles/java/g1gc.html Oracle

https://blog.csdn.net/u012586326/article/details/112343691 东北小狐狸-Hellxz 2020-11-29 15:23:00

https://zhuanlan.zhihu.com/p/43010596 知乎

https://blog.csdn.net/weixin_38007185/article/details/108093716 一直Tom猫 2020-08-19 10:14:01

https://segmentfault.com/a/1190000004682407 cardtable

https://cloud.tencent.com/developer/article/1847053 cset

https://segmentfault.com/a/1190000039300766 三色标记SATB(SNAPSHOT AT THE BEGINNING)

https://zhuanlan.zhihu.com/p/181305087 G1参数解析

https://zhuanlan.zhihu.com/p/74517142 G1日志解析

https://blog.csdn.net/qiang_zi_/article/details/100700784 SoftRefLRUPolicyMSPerMB

https://blog.csdn.net/u010833547/article/details/90289325 SoftRefLRUPolicyMSPerMB

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值