JVM - 垃圾回收

垃圾回收

如何判断对象可以回收?

引用计数法

给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加一;相反的,当引用失效的时候,计数器值就减一;任何时刻计数器为0的对象就是不可能再被使用的。

当一个对象被引用时,该对象的引用计数器值 +1 ,当引用计数器值 为0时,表示该对象不再被引用,可以被垃圾回收器回收。

当有一个弊端,当循环引用时,它两的引用计数器值 为1 ,导致两个对象都无法被回收

可达性分析法

  • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象

  • 扫描堆中的对象,看是否能够沿着 GC Root 对象为起点的引用链找到该对象,找不到,表示可以回收

  • 从上图我们可以看到,对象Object5、Object6、Object7、虽然相互间有关联,但是它们到GC Roots是不可达的,因此他们将会被判定为可回收的对象。

哪些对象可以作为GC Root ?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。

  • 方法区中类静态属性引用的对象。

  • 方法区中常量引用的对象。

  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

可达性分析算法中不可达的对象就是可以回收的,但是它一定会被回收吗?

对于可达性分析算法中不可达的对象,它们也不会立刻就被回收,这个时候它们暂时处于“嫌疑人”状态,到真正宣告一个对象死亡,至少要经历两次标记过程。

如果对象在进行可达性分析之后被发现没有与GC Roots相连的引用链,那么它将会被第一次标记,并且进行一次*筛选*,筛选的条件就是此对象是否有必要执行*finalize()*方法。

以下两种情况虚拟机将视为没有必要执行finalize()方法:

当对象没有覆盖finalize()方法 finalize()方法已经被虚拟机调用过 如果这个对象被判定为有必要执行finalize()方法,那么这个对象就会被放置在一个叫做F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。

这里所说的“执行”是指虚拟机会触发这个方法,但是并不承诺会等待它执行完毕。为什么呢?

你想,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(甚至其它更加极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待状态,甚至可能导致整个内存回收系统奔溃。

finalize()方法是对象逃脱死亡命运的最后一次机会。因为在执行了finalize()方法之后,GC将会对F-Queue队列中的对象进行第二次小规模的标记,如果对象能够在finalize()中成功地拯救自己,即只要重新与引用链上的任一对象建立关联即可,比如将自己(this关键字)赋值给某一个类变量或者对象的成员变量,那么在第二次标记的时候,它就会被移出“即将回收”的集合,即移出F-Queue队列。如果这个时候,对象还没有逃脱,那么它就基本上就要被回收了。

———————————————— 原文链接:JVM——引用计数算法与可达性分析算法_引用计数法和可达性分析_一只野生饭卡丘的博客-CSDN博客

四种引用

强引用

只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收

把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到, JVM也不会回收。因此强引用是造成 Java内存泄漏的主要原因之一。

软引用

1.仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 可以配合引用队列来释放软引用自身

2.软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中

弱引用

1.仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 可以配合引用队列来释放弱引用自身

2.引用需要用WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM的内存空间是否足够,总会回收该对象占用的内存。

虚引用

1.必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存

2.虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

终结器引用

无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象。

垃圾回收算法

标记清除

定义:Mark Sweep

  1. 速度较快

  2. 会产生内存碎片

清除之后它的内存是碎片化的,这时候如果有一个比较大的数组,因为数组的存储是连续的,虽然整个的容量是够的,但是无法放入,就会导致内存的浪费。

标记整理

Mark Compact

  1. 速度慢【缺点,因为涉及到的对象的移动】

  2. 没有内存碎片【优点】

复制

Copy

  1. 不会有内存碎片

  2. 需要占用两倍内存空间

分代垃圾回收

  • 新创建的对象首先分配在 eden 区

  • 新生代空间不足时,触发 minor gc ,eden 区 和 from 区存活的对象使用 - copy 复制到 to 中,存活的对象年龄加一,然后交换 from to

  • minor gc 会引发 stop the world,暂停其他用户线程【时间较短】,等垃圾回收结束后,恢复用户线程运行

  • 当幸存区对象的寿命超过阈值时,会晋升到老年代,最大的寿命是 15(4bit)

  • 当老年代空间不足时,会先触发 minor gc,如果空间仍然不足,那么就触发 full gc ,STW的时间更长!

一、年轻代

也叫新生代,顾名思义,主要是用来存放新生的对象。新生代又细分为 Eden区(伊甸园)、SurvivorFrom区(幸存区From)、SurvivorTo区(幸存区To)

如果新生对象在Eden区无法分配空间时,此时发生Minor GC。发生MinorGC,对象会从Eden区进入Survivor区,如果Survivor区放不下从Eden区过来的对象时,此时会使用分配担保机制将对象直接移动到老年代。

在Minor GC开始的时候,对象只会存在于Eden区和Survivor from区,Survivor to区是空的。

Minor GC操作后,Eden区如果仍然存活(判断的标准是被引用了,通过GC root进行可达性判断)的对象,将会被移到Survivor To区。而From区中,对象在Survivor区中每熬过一次Minor GC,年龄就会+1岁,当年龄达到一定值(年龄阈值,默认是15,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,否则对象会被复制到“To”区。经过这次MinorGC后,Eden区和From区已经被清空,所有对象都在to区连续存储

“From”区和“To”区互换角色,原Survivor To成为下一次GC时的Survivor From区, 总之,GC后,都会保证Survivor To区是空的。

奇怪为什么有 From和To,2块区域?这就要说到新生代Minor GC的算法了:复制算法,把内存区域分为两块,每次使用一块,GC的时候把一块中的内容移动到另一块中,原始内存中的对象就可以被回收了,优点是避免内存碎片

1.1survivor区解释

具体详见:survivor区解释

二、老年代

随着Minor GC的持续进行,老年代中对象也会持续增长,导致老年代的空间也会不够用,最终会执行Major GC(MajorGC 的速度比 Minor GC 慢很多很多,据说10倍左右)。Major GC使用的算法是:标记清除(回收)算法或者标记压缩算法

标记清除(回收):

\1. 首先会从GC root进行遍历,把可达对象(存过的对象)打标记;

\2. 再从GC root二次遍历,将没有被打上标记的对象清除掉。

优点:老年代对象一般是比较稳定的,相比复制算法,不需要复制大量对象。之所以将所有对象扫描2次,看似比较消耗时间,其实不然,是节省了时间。举个栗子,数组 1,2,3,4,5,6。删除2,3,4,如果每次删除一个数字,那么5,6要移动3次,如果删除1次,那么5,6只需移动1次。

缺点:这种方式需要中断其他线程(STW),相比复制算法,可能产生内存碎片。

标记压缩:和标记清除算法基本相同,不同的就是,在清除完成之后,会把存活的对象向内存的一边进行压缩,这样就可以解决内存碎片问题。

当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。

三、Full GC

Major GC和Full GC是不一样的,前者只清理老年代,后者会清理年轻代+老年代什么时候触发:

1. 调用System.gc

2. 方法区空间不足

3.老年代空间不足,包括:

  • 新创建的对象都会被分配到Eden区,如果该对象占用内存非常大,则直接分配到老年代区,此时老年代空间不足;

  • 做Minor GC操作前,发现要TO区中移动的对象需要的空间(Eden区、From区向To区复制时,To区的内存空间不足或者年龄到达阈值)比老年代剩余空间要大,则触发Full GC,而不是Minor GC;

  • 等等。

GC优化的本质,也是为什么分代的原因:减少GC次数和GC时间,避免全区扫描。

———————————————— 原文链接:关于新生代和老年代_timelvke的博客-CSDN博客

相关JVM参数

含义参数
堆初始大小-Xms
堆最大大小-Xmx 或 -XX:MaxHeapSize=size
新生代大小-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态)-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例-XX:SurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=threshold
晋升详情-XX:+PrintTenuringDistribution
GC详情-XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC-XX:+ScavengeBeforeFullGC

GC 分析

public class Code_10_GCTest {
 ​
     private static final int _512KB = 512 * 1024;
     private static final int _1MB = 1024 * 1024;
     private static final int _6MB = 6 * 1024 * 1024;
     private static final int _7MB = 7 * 1024 * 1024;
     private static final int _8MB = 8 * 1024 * 1024;
 ​
     // -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
     public static void main(String[] args) {
         List<byte[]> list = new ArrayList<>();
         list.add(new byte[_6MB]);
         list.add(new byte[_512KB]);
         list.add(new byte[_6MB]);
         list.add(new byte[_512KB]);
         list.add(new byte[_6MB]);
     }
 }

通过上面的代码,给 list 分配内存,来观察 新生代和老年代的情况,什么时候触发 minor gc,什么时候触发 full gc 等情况,使用前需要设置 jvm 参数。

垃圾回收器

相关概念:

  • 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。

  • 并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个 CPU 上

  • 吞吐量:即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99% 。

串行

  • 单线程

  • 堆内存较小,适合个人电脑

 -XX:+UseSerialGC=serial + serialOld

安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象 因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

Serial 收集器 Serial 收集器是最基本的、发展历史最悠久的收集器 特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)!

ParNew 收集器 ParNew 收集器其实就是 Serial 收集器的多线程版本 特点:多线程、ParNew 收集器默认开启的收集线程数与CPU的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题

Serial Old 收集器 Serial Old 是 Serial 收集器的老年代版本 特点:同样是单线程收集器,采用标记-整理算法

吞吐量优先

  • 多线程

  • 堆内存较大,多核CPU

  • 让单位时间内,STW的时间最短

 -XX:+UseParallelGC ~ -XX:+UsePrallerOldGC
 -XX:+UseAdaptiveSizePolicy
 -XX:GCTimeRatio=ratio // 1/(1+radio)
 -XX:MaxGCPauseMillis=ms // 200ms
 -XX:ParallelGCThreads=n

Parallel Scavenge 收集器 与吞吐量关系密切,故也称为吞吐量优先收集器 特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与 ParNew 收集器类似)

该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与 ParNew 收集器最重要的一个区别)

GC自适应调节策略: Parallel Scavenge 收集器可设置 -XX:+UseAdptiveSizePolicy 参数。 当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、 晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为 GC 的自适应调节策略。

Parallel Scavenge 收集器使用两个参数控制吞吐量:

XX:MaxGCPauseMillis=ms 控制最大的垃圾收集停顿时间(默认200ms) XX:GCTimeRatio=rario 直接设置吞吐量的大小 Parallel Old 收集器 是 Parallel Scavenge 收集器的老年代版本 特点:多线程,采用标记-整理算法(老年代没有幸存区)

响应时间优先

  • 多线程

  • 堆内存较大,多核CPU

  • 尽可能让单词STW的时间最短

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
 -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
 -XX:CMSInitiatingOccupancyFraction=percent
 -XX:+CMSScavengeBeforeRemark //在重新标记发生之前对新生代进行一次垃圾回收

CMS 收集器 Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器 特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片 应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务 CMS 收集器的运行过程分为下列4步:

  1. 初始标记:标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。

  2. 并发标记:进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。

  3. 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题

  4. 并发清除:对标记的对象进行清除回收,清除的过程中,可能任然会有新的垃圾产生,这些垃圾就叫浮动垃圾,如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会退化为 serial Old 收集器,将老年代垃圾进行标记-整理,当然这也是很耗费时间的!

CMS 收集器的内存回收过程是与用户线程一起并发执行的,可以搭配 ParNew 收集器(多线程,新生代,复制算法)与 Serial Old 收集器(单线程,老年代,标记-整理算法)使用。

G1 收集器

定义: Garbage First 适用场景:

  • 同时注重吞吐量和低延迟(响应时间)

  • 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域

  • 整体上是标记-整理算法,两个区域之间是复制算法

相关参数: JDK8 并不是默认开启的,所需要参数开启

 -XX:+UseG1GC
 -XX:G1HeapRegionSize=size
 -XX:MaxGCPauseMillis=time
G1 垃圾回收阶段

Young Collection:对新生代垃圾收集 Young Collection + Concurrent Mark:如果老年代内存到达一定的阈值了,新生代垃圾收集同时会执行一些并发的标记。 Mixed Collection:会对新生代 + 老年代 + 幸存区等进行混合收集,然后收集结束,会重新进入新生代收集。

Young Collection

新生代存在 STW: 分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间! E:eden,S:幸存区,O:老年代 新生代收集会产生 STW !

Young Collection + CM 在 Young GC 时会进行 GC Root 的初始化标记 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的 JVM 参数决定 -XX:InitiatingHeapOccupancyPercent=percent (默认45%)

Mixed Collection 会对 E S O 进行全面的回收

最终标记会 STW 拷贝存活会 STW -XX:MaxGCPauseMills=xxms 用于指定最长的停顿时间! 问:为什么有的老年代被拷贝了,有的没拷贝? 因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)

Full GC

G1 在老年代内存不足时(老年代所占内存超过阈值) 如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理 如果垃圾产生速度快于垃圾回收速度,便会触发 Full GC,然后退化成 serial Old 收集器串行的收集,就会导致停顿的时候长。

Young Collection 跨代引用 新生代回收的跨代引用(老年代引用新生代)问题

  • 卡表 与 Remembered Set

    • Remembered Set 存在于E中,用于保存新生代对象对应的脏卡

      • 脏卡:O 被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡

  • 在引用变更时通过 post-write barried + dirty card queue

  • concurrent refinement threads 更新 Remembered Set

Remark

重新标记阶段 在垃圾回收时,收集器处理对象的过程中

  • 黑色:已被处理,需要保留的

  • 灰色:正在处理中的

  • 白色:还未处理的

但是在并发标记过程中,有可能 A 被处理了以后未引用 C ,但该处理过程还未结束,在处理过程结束之前 A 引用了 C ,这时就会用到 remark 。 过程如下

  1. 之前 C 未被引用,这时 A 引用了 C ,就会给 C 加一个写屏障,写屏障的指令会被执行,将 C 放入一个队列当中,并将 C 变为 处理中状态

  2. 在并发标记阶段结束以后,重新标记阶段会 STW ,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它,由灰色变成黑色。

DK 8u20 字符串去重

过程

  • 将所有新分配的字符串(底层是 char[] )放入一个队列

  • 当新生代回收时,G1 并发检查是否有重复的字符串

  • 如果字符串的值一样,就让他们引用同一个字符串对象

  • 注意,其与 String.intern() 的区别

    • String.intern() 关注的是字符串对象

    • 字符串去重关注的是 char[]

    • 在 JVM 内部,使用了不同的字符串标

优点与缺点

  • 节省了大量内存

  • 新生代回收时间略微增加,导致略微多占用 CPU

 -XX:+UseStringDeduplication

JDK 8u40 并发标记类卸载

在并发标记阶段结束以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类

JDK 8u60 回收巨型对象

  • 一个对象大于region的一半时,就称为巨型对象

  • G1不会对巨型对象进行拷贝

  • 回收时被优先考虑

  • G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

JDK 9 并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为 FulGC

  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent

  • JDK 9 可以动态调整

    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值

    • 进行数据采样并动态调整

    • 总会添加一个安全的空挡空间

垃圾回收调优

查看虚拟机参数命令,可根据参数去查询具体的信息

 D:\JavaJDK1.8\bin\java  -XX:+PrintFlagsFinal -version | findstr "GC"

调优领域

  • 内存

  • 锁竞争

  • cpu 占用

  • io

  • gc

确定目标

  • 低延迟/高吞吐量? 选择合适的GC

  • CMS G1 ZGC

  • ParallelGC

  • Zing

最快的 GC

首先排除减少因为自身编写的代码而引发的内存问题

  • 查看 Full GC 前后的内存占用,考虑以下几个问题

    • 数据是不是太多?

      • resultSet = statement.executeQuery(“select * from 大表 limit n”)

    • 数据表示是否太臃肿

      • 对象图

      • 对象大小 16 Integer 24 int 4

  • 是否存在内存泄漏

    • static Map map …

    • 第三方缓存实现

新生代调优

-Xmn 建议新生代大于内存25%,且总的内存小于堆内存的50%

  • 新生代的特点

    • 所有的 new 操作分配内存都是非常廉价的

      • TLAB thread-lcoal allocation buffer

    • 死亡对象回收零代价

    • 大部分对象用过即死(朝生夕死)

    • Minor GC 所用时间远小于 Full GC

  • 新生代内存越大越好么?

    • 不是

      • 新生代内存太小:频繁触发 Minor GC ,会 STW ,会使得吞吐量下降

      • 新生代内存太大:老年代内存占比有所降低,会更频繁地触发 Full GC。而且触发 Minor GC 时,清理新生代所花费的时间会更长

    • 新生代内存设置为内容纳【并发量*(请求-响应)】的数据为宜

  • 幸存区需要能够保存 【当前活跃对象+需要晋升的对象】

  • 晋升阈值配置得当,让长时间存活的对象尽快晋升

 -XX:MaxTenuringThreshold=threshold //调整最大晋升阈值
 -XX:+PrintTenuringDistrubution //是上面的参数
 Desired survivor size 48286924 bytes, new threshold 10 (max 10)
 对象年龄 : 占用空间大小 , 空间占用累计总和
 age 1: 28992024 bytes,28992024 total
 age 2: 1366864 bytes, 30358888 total
 age 3: 1425912 bytes,  31784800 total
 ...

老年代调优

CMS 是一个低响应时间的,并发的垃圾回收器

以 CMS 为例:

  • CMS 的老年代内存越大越好

  • 先尝试不做调优,如果没有 Full GC 那么已经,否者先尝试调优新生代。

  • 观察发现 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3

 -XX:CMSInitiatingOccupancyFraction=percent

案例

案例1:Full GC 和 Minor GC 频繁

 增大了新生代的内存导致minor gc更少出发,并且survivor区增大,就不会让本不是生命周期那么长的对象进入老年区,从而给老年区节省空间,进一步就减少了老年区出发fullGC

案例2:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)

 在重新标记发生之前对新生代进行一次垃圾回收

案例3:老年代充裕情况下,发生 Full GC(jdk1.7)

————————————————

上述部分内容借鉴了

JVM 学习笔记(二)垃圾回收_CodeAli的博客-CSDN博客

 如需删除与我联系

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

叫一只啦啦

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

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

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

打赏作者

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

抵扣说明:

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

余额充值