Java GC详解

目录

需要GC的内存区域

GC的标记算法

什么时候触发GC

GC常用算法

三色标计算法

GC垃圾收集器


需要GC的内存区域

        jvm 中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。

主动触发fullGC

  • system.gc
    • 不确定什么时候生效
  • 主动生产垃圾触发fullGc
    • byte[] 1M
  • jmap指令手动触发

GC的标记算法

  • 需要进行回收的对象就是已经没有存活的对象,判断一个对象是否存活常用的有两种办法:引用计数和可达分析。
    • 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
    • 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
      • 可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。
      • 当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象"复活”
      • 每个对象只能触发一次finalize(方法,由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘它。
    • GC Roots:栈帧中局部变量表中的数据。包括:
      • 本地变量
      • 静态变量、常量
      • 本地方法栈变量

什么时候触发GC

  • 程序调用System.gc时可以触发
  • 系统自身来决定GC触发的时机(根据Eden区和From Space区的内存大小来决定。当内存大小不足时,则会启动GC线程并停止应用线程)
  • GC又分为 minor GC 和 Full GC (也称为 Major GC )
    • Minor GC触发条件:当Eden区满时,触发Minor GC。
    • Full GC触发条件:
      • 调用System.gc时,系统建议执行Full GC,但是不必然执行
      • 老年代空间不足
      • 方法区空间不足
      • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
      • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

GC常用算法

  • GC常用算法有:标记-清除算法,标记-压缩算法,复制算法,分代收集算法。
    • 标记-清除算法
      • 为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。
      • 优点:最大的优点是,标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。此外,更重要的是,这个算法并不移动对象的位置。
      • 缺点:它的缺点就是效率比较低(递归与全堆对象遍历)。每个活着的对象都要在标记阶段遍历一遍;所有对象都要在清除阶段扫描一遍,因此算法复杂度较高。没有移动对象,导致可能出现很多碎片空间无法利用的情况。
    • 标记-整理算法
      • 标记-压缩法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。
      • 优点:该算法不会像标记-清除算法那样产生大量的碎片空间。
      • 缺点:如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。
    • 复制算法
      • 该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。
      • 优点:实现简单;不产生内存碎片
      • 缺点:每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半。
    • 分代收集算法
      • 现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。
      • 新生代(Young)分为Eden区,From区与To区
        • 当系统创建一个对象的时候,总是在Eden区操作,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回收。一般来说这时候不是所有的对象都没用了,所以就会把还能用的对象复制到From区。 
        • 这样整个Eden区就被清理干净了,可以继续创建新的对象,当Eden区再次被用完,就再触发一次YoungGC,然后呢,注意,这个时候跟刚才稍稍有点区别。这次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区, 
        • 再下一次YoungGC的时候,则是将Eden区与To区中的还在被使用的对象复制到From区。
        • 经过若干次YoungGC后,有些对象在From与To之间来回游荡,这时候From区与To区亮出了底线(阈值),这些家伙要是到现在还没挂掉,对不起,一起滚到(复制)老年代吧。 
        • 老年代经过这么几次折腾,也就扛不住了(空间被用完),好,那就来次集体大扫除(Full GC),也就是全量回收。如果Full GC使用太频繁的话,无疑会对系统性能产生很大的影响。所以要合理设置年轻代与老年代的大小,尽量减少Full GC的操作。

三色标计算法

  • 「白色」:该对象没有被标记过。(对象垃圾)
  • 「灰色」:该对象已经被标记过了,但该对象下的属性没有全被标记完。(GC需要从此对象中去寻找垃圾)
  • 「黑色」:该对象已经被标记过了,且该对象下的属性也全部都被标记过了。(程序所需要的对象)
  • 流程
    • 首先创建三个集合:白、灰、黑。
    • 将所有对象放入白色集合中。
    • 然后从根节点开始遍历所有对象(注意这里并不「递归遍历」),把遍历到的对象从白色集合放入灰色集合。
    • 之后遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
    • 重复 4 直到灰色中无任何对象
    • 通过write-barrier检测对象有变化,重复以上操作
    • 收集所有白色对象(垃圾)

三色标记存在问题

  • 浮动垃圾
    • 并发标记的过程中,若一个已经被标记成黑色或者灰色的对象,突然变成了垃圾,由于不会再对黑色标记过的对象重新扫描,所以不会被发现,那么这个对象不是白色的但是不会被清除,重新标记也不能从GC Root中去找到,所以成为了浮动垃圾,「浮动垃圾对系统的影响不大,留给下一次GC进行处理即可」。
  • 对象漏标问题(需要的对象被回收)
    • 并发标记的过程中,一个业务线程将一个未被扫描过的白色对象断开引用成为垃圾(删除引用),同时黑色对象引用了该对象(增加引用)(这两部可以不分先后顺序);因为黑色对象的含义为其属性都已经被标记过了,重新标记也不会从黑色对象中去找,导致该对象被程序所需要,却又要被GC回收,此问题会导致系统出现问题,而CMS与G1,两种回收器在使用三色标记法时,都采取了一些措施来应对这些问题,「CMS对增加引用环节进行处理(Increment Update),G1则对删除引用环节进行处理(SATB)。」
  • 解决方案
    • Increment Update
      • 在一个未被标记的对象(白色对象)被重新引用后,「引用它的对象若为黑色则要变成灰色,在重新标记阶段时让GC线程继续标记它的属性对象」。
    • SATB
      • 在GC开始时对内存进行一个对象图的逻辑快照(snapshot),通过GC Roots参照并发标记的过程,只要被快照到对象是活的,那在整个GC的过程中对象就被认定的是活的,即使该对象的引用稍后被修改或者删除。同时新分配的对象也会被认为是活的,除此之外其它不可达的对象就被认为是死掉了。这样SATB就保证了真正存活的对象不会被GC误回收,但同时也造成了某些可以被回收的对象逃过了GC,导致了内存里面存在浮动的垃圾
      • 在开始标记的时候生成一个快照图标记存活对象,在一个引用断开后,要将此引用推到GC的堆栈里,保证白色对象(垃圾)还能被GC线程扫描到。
      • 可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
  • 对比
    • SATB 算法是关注引用的删除。(B->C 的引用),而Incremental Update 算法关注引用的增加。(A->C 的引用)。
    • G1 如果使用 Incremental Update 算法,因为变成灰色的成员还要重新扫,重新再来一遍,效率太低了。 所以 G1 在处理并发标记的过程比 CMS 效率要高,但是可能会产生浮动垃圾。这个主要是解决漏标的算法决定的。

        为什么要设计STW:为了确保数据安全,如果在进行full gc的过程中不阻塞所有用户线程的话,可达性算法锁标记的非垃圾对象可能会因为用户的操作结束而销毁,所对应的内存空间也会释放,这时引用链中被标记为非垃圾的对象就无法回收可能会出现问题。 

GC垃圾收集器

  • Serial收集器(年轻代、单线程、复制算法)
    • 是最古老的垃圾收集器,使用复制算法,曾经是JDK1.3.1 之前新生代唯一的垃圾收集器。Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Serial垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。
  • Serial Old 收集器(老年代、单线程、标记整理算法 )
    • Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法
  • ParNew收集器(年轻代、多线程、复制算法)
    • ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。
  • Parallel Scavenge 收集器(年轻代、多线程、复制算法)
    • Parallel Scavenge收集器也是一个并行的多线程新生代收集器,它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。
    • 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为GC自适应的调节策略(GC Ergonomics)。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
    • JDK8年轻代默认使用Parallel Scavenge进行垃圾回收
  • Parallel Old 收集器(老年代、多线程、标记整理算法)
    • Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在 JDK1.6才开始提供。在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。
    • JDK8老年代默认使用Parallel old进行垃圾回收
  • CMS 收集器(老年代、多线程、标记清除算法)
    • 从CMS开始出现了并发收集器:指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。
    • CMS(Concurrent mark sweep)是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
    • CMS的垃圾回收分为4个阶段:
      • 初始标记:仅仅是标记一下GC Roots能直接关联的对象,速度快;需要STW
      • 并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
      • 重新标记:是修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,停顿时间比初始标记长,比并发标记短;需要STW
      • 并发清除:清除标记的对象,由于使用标记清楚算法可能会在收集结束时产生大量空间碎片,有可能导致没有足够大的连续空间来分配当前对象而触发一次Full GC。
    • 优点
      • 并发收集、低停顿,因此CMS收集器也被称为并发低停顿收集器。
    • 缺点
      • 会产生大量空间碎片
      • 无法处理浮动垃圾
        • 伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”

G1 收集器

  • 通过把Java堆分成大小相等的多个独立区域,回收时计算出每个区域回收所获得的空间以及所需时间的经验值,根据记录两个值来判断哪个区域最具有回收价值,所以叫Garbage First(垃圾优先)。
  • G1是一种分代收集器,只有逻辑上的分代概念,物理上不分代
    • 年轻代:采用复制算法
    • 年老代:标记清除算法
    • 通过命令行参数
      • -XX:NewRatio=n来配置新生代与老年代的比例,默认为2,即比例为2:1;
      • -XX:SurvivorRatio=n则可以配置Eden与Survivor的比例,默认为8。
    • G1最大的优势就在于可预测的停顿时间模型,我们可以自己通过参数-XX:MaxGCPauseMillis来设置允许的停顿时间(默认200ms),G1会收集每个Region的回收之后的空间大小、回收需要的时间,根据评估得到的价值,在后台维护一个优先级列表,然后基于我们设置的停顿时间优先回收价值收益最大的Region。
    • 重要概念
      • Region(区域)
        • G1收集器通过把Java堆分成一个个大小相等的Region,Region是G1回收的最小单元。
        • 默认将整堆划分为2048个分区,可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂)。如果不设定,那么G1会根据Heap大小自动决定。
        • Region分为
          • Eden Region
          • Survivor Region
          • Humongous Region
            • 当一个对象大于Region大小的50%,称为巨型对象;它就会独占一个或多个Region,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区-Humongous Region
            • G1 不会对巨 型对象进行拷贝,并且回收时也会优先回收这个巨型对象
          • Old Region
  • Collection Set(收集集合)
    • 它记录了GC要收集的Region集合
    • 在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲Region中。
    • Young GC: CSet就是所有年轻代里面的Region;
    • Mixed GC: CSet是所有年轻代里的Region加上在全局并发标记阶段标记出来的收益高的老年代Region;
  • Remembered Set
    • 每一个Region都有自己的RSet
    • RSet里面记录了引用——就是其他Region中指向本Region中所有对象的所有引用,也就是谁引用了我的对象
    • RSet里面记录的是被引用的信息,Region2中的Rset记录的是Region1和Region3中的对象引用信息
    • RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index
    • G1 GC每次都会对年轻代进行整体收集,因此young->old和young->young也不需要在RSet中记录。而对于old->young和old->old的跨代对象引用,需要拥有RSet
    • 所以G1中YGC不需要扫描整个老年代,只需要扫描Rset就可以知道老年代引用了哪些新生代中的对象。
    • 在做YGC的时候,只需要选定young generation region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。 而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。所以RSet的引入大大减少了GC的工作量。
  • Card Table
    • 每个Region被分成了多个Card(一般为512byte),Card Table维护着所有的Card
    • Card Table的结构是一个连续的字节数组,Card Table用这个数组映射着每一个Card
    • Card中对象的引用发生改变时,Card在Card Table数组中对应的值被标记为dirty,就称这个Card被脏化了
    • 所以Card Table其实就是映射着内存中的对象,Young GC的时候只需要扫描状态是dirty的card
  • 垃圾收集阶段
    • Young GC
      • 年轻代收集不会进行并发标记,所以它全程都是STW
      • 应用线程不断活动后,年轻代空间会被逐渐填满。当JVM分配对象到Eden区域失败(Eden区已满)时,便会触发一次STW式的年轻代收集
      • 过程
        • 首先扫描GC ROOT
        • 对card table的dirty card的分区进行扫描,来更新RSet
        • 构建CSet
        • 将CSet分区存活对象的转移到新survivor或old Region,回收CSet内垃圾对象
        • 记录每个阶段的时间用于自动调优
    • Mixed GC
      • 在进行正常的年轻代垃圾收集,也会回收一部分老年代分区。会优先选取垃圾多(垃圾占用大于85%,复制算法存活对象越少效率越高)的Regions,一共1/8的年老代Regions加入Cset中
      • 全局并发标记
        • 初始标记(initial-mark)
          • 在这个阶段应用会经历STW,通常初始标记阶段会跟一次新生代收集一起进行,换句话说既然这两个阶段都需要暂停应用,G1 GC就重用了新生代收集来完成初始标记的工作。
        • 根分区扫描(root-region-scan)
          • 这个过程不需要暂停应用,在初始标记或新生代收集中被拷贝到survivor分区的对象,都需要被看做是根,这个阶段G1开始扫描survivor分区,所有被survivor分区所引用的对象都会被扫描到并将被标记。
          • survivor分区就是根分区,正因为这个,该阶段不能发生新生代收集,如果扫描根分区时,新生代的空间恰好用尽,新生代垃圾收集必须等待根分区扫描结束才能完成。如果在日志中发现根分区扫描和新生代收集的日志交替出现,就说明当前应用需要调优。
        • 并发标记(Concurrent Marking)
          • G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断。
        • 重新标记(Remark,STW)
          • 该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
        • 清除垃圾(Cleanup)
          • 在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
    • Old GC
      • 当堆内存占用空间超过整堆比IHOP阈值-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1就会进行年老代收集
      • 在年轻代收集之后或巨型对象分配之后,会去检查这个空间占比
      • Old GC回收过程和Mixed GC相同
    • Full GC
      • 当G1无法在堆空间中申请新的分区时,G1便会触发担保机制,执行一次STW式的、单线程的Full GC(serial old)。Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。
      • G1在以下场景中会触发Full GC,同时会在日志中记录to-space-exhausted以及Evacuation Failure
      • 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
        • 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
        • 分配巨型对象时在老年代无法找到足够的连续分区
        • 从老年代分区转移存活对象时,无法找到可用的空闲分区
      • 由于G1的应用场合往往堆内存都比较大,所以Full GC的收集代价非常昂贵,应该避免Full GC的发生。

  • 18
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: jstat -gc 命令用于查看Java进程的垃圾回收情况。下面是各个参数的详细解释: S0C:第一个幸存区的大小,即Survivor0的大小。 S1C:第二个幸存区的大小,即Survivor1的大小。 S0U:第一个幸存区的使用大小,即Survivor0的使用大小。 S1U:第二个幸存区的使用大小***即Eden区的使用大小。 OC:老年代大小,即Old区的大小。 OU:老年代使用大小,即Old区的使用大小。 MC:元数据区大小,即Metaspace的大小。 MU:元数据区使用大小,即Metaspace的使用大小。 CCSC:压缩类空间大小,即Compressed Class Space的大小。 CCSU:压缩类空间使用大小,即Compressed Class Space的使用大小。 YGC:年轻代垃圾回收次数,即Young Generation GC的次数。 YGCT:年轻代垃圾回收消耗时间,即Young Generation GC的消耗时间。 FGC:老年代垃圾回收次数,即Full GC的次数。 FGCT:老年代垃圾回收消耗时间,即Full GC的消耗时间。 GCT:总垃圾回收消耗时间,即总的GC消耗时间。 [1 [2 [3<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [jstat -gc pid数据详解](https://blog.csdn.net/dhj199181/article/details/108415771)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [jvm jstat -gcutil 参数详解](https://blog.csdn.net/weixin_44371237/article/details/129546682)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [jstat -gc pid参数](https://blog.csdn.net/weixin_43923436/article/details/128240747)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值