JVM——GC策略及经典垃圾收集器

第3章 垃圾收集器与内存分配策略

3.1 motivation

  • 垃圾收集需要完成的三件事情: 哪些内存需要回收? 什么时候回收? 如何回收?
  • 不需要回收的区域:程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭
  • 需要回收的区域:Java堆和方法区

3.2 对象已死?

要确定这些对象之中哪些还“存活”着,哪些已经“死去”

3.2.1 引用计数法

  • 在对象中添加一个引用计数器,被引用一次,计数器就+1
  • 优点:简单、效率高
  • 缺点:特例太多,例如对象之间的循环引用

3.2.2 可达性分析算法

  • 从GC Roots根对象根据引用关系往下查找,搜索路径称为引用链,不可达的对象就是可回收的对象

  • 可作为GC Roots的对象:
    (1)在虚拟机栈(栈帧中的本地变量表)中引⽤的对象,譬如各个线程被调⽤的⽅法堆栈中使⽤到的参数、局部变量、临时变量等
    (2)在⽅法区中类静态属性引⽤的对象,譬如Java类的引⽤类型静态变量
    (3)在⽅法区中常量引⽤的对象,譬如字符串常量池(String Table)⾥的引⽤
    (4)在本地⽅法栈中JNI(即通常所说的Native⽅法)引⽤的对象
    (5)Java虚拟机内部的引⽤,如基本数据类型对应的Class对象,⼀些常驻的异常对象(⽐如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器
    (6)所有被同步锁(synchronized关键字)持有的对象
    (7)反映Java虚拟机内部情况的JM XBean、JVM TI中注册的回调、本地代 码缓存等
    (8)除了以上可以固定作为GC Roots的集合外,由于所选⽤的垃圾收集器 以及当前回收的内存区域不同,还可以有其它对象“临时性”加⼊

3.2.3 引用类型

  • 引用类型:当内存空间还足够时,能保留在内存之中,如果内存空间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象——很多系统的缓存功能都符合这样的应用场景。

  • 强引用:传统引用的定义(reference类型的数据中存储的数值代表的是另外一块内存的起始地址), GC永远不会回收被强引用的对象

  • 软引用:还有用,但非必须。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。

  • 弱引用:垃圾回收的目标。被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

  • 虚引用:无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知

3.2.4 对象死亡过程

判定对象已死,至少进行两次标记:
1)可达性分析判定不可达,标记第一次
2)筛选该对象是否有必要执行finalize()方法:

  • 没必要执行:对象没有覆盖 finalize()方法、 finalize()方法已经被虚拟机调用过( finalize()方法最多只会被系统自动调用一次)
  • 有必要执行:把对象放入F-Queue队列,由finalizer线程执行它们的finalize()方法,之后收集器将对F-Queue中的对象再次进行标记
    (对象自救:在 finalize()方法中建立引用)
    finalize()运行代价高,不确定大,不推荐使用

3.2.5 回收方法区

  • 性价比低,回收条件苛刻,能释放的空间少
  • 方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。
  • 废弃常量:没有任何字符串对象引用的常量池中的常量,且虚拟机中也没有其他地方引用这个
  • 不再使用的类:三个条件:
    1)该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
    2)加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP的重加载等,否则通常是很难达成的。
    3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    jvm允许对满足以上三点的无用类进行回收,关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX: +TraceClassUnLoading查看类加载和卸载信息

3.3 垃圾收集算法(三个追踪式垃圾收集算法)

3.3.1 分代收集理论

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
    收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储
    有个问题:对象不是孤立的,对象之间会存在跨代引用。
  3. 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。
    存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。
    在新生代上建立一个全局的数据结构(记忆集),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。
  • 垃圾收集类型:
    新生代收集(Minor GC/Young GC)
    老年代收集(Major GC/Old GC)
    混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。(G1收集器)
    整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

3.3.2 标记-清除算法

  • 先标记出所有要回收的对象,之后统一回收所有被标记的对象
  • 缺点:
    1)大量的标记和清除动作,执行效率不稳定;
    2)内存空间的碎片化问题

3.3.3 标记复制算法(主要用于新生代)

  • 将可用内存平分成两块,每次只使用其中的一块,这一块满了,标记其中存活的对象,复制到另一块上面,然后把已使用这块全清理掉
  • 优点:无内存碎片问题,复制过程只需移动堆顶指针,顺序分配
  • 缺点:空间缩小为一半
  • 优化:
    Appel式回收:把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。
    HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1
    当Survivor空间不足以容纳一次Minor GC之后存活的对象时,需要老年代进行分配担保

3.3.4 标记-整理算法(用于老年代)

  • 让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
  • 老年代有大量存活对象,移动操作代价高,而且需要暂停用户应用
  • 移动->内存回收时会更复杂;不移动->内存分配时会更复杂
  • 不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量(用户程序与收集器的效率总和)来看,移动对象会更划算。
  • CMS收集器:多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。

3.4 HotSpot的算法细节实现

3.4.1 根节点枚举

  • 所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的
  • 如果处于根节点集合的对象引用关系还在不断变化的情况,分析结果准确性无法保证
  • ⽬前主流的JVM使⽤的都是准确式收集==> 当⽤户线程停顿下来之后并不需要⼀个不漏地检查完所有执⾏上下⽂和全局的引⽤位置,虚拟机应当有办法直接得到哪些地⽅存放着对象引⽤;
  • HotSpot使用OopMap数据结构,直接来得到哪些位置存放着对象引用
    OopMap存储两种引⽤:
    1)栈⾥和寄存器内的引⽤:在即时编译中,在特定的位置记录下栈⾥和寄存器⾥哪些位置是引⽤;
    2)对象内的引⽤:类加载动作完成时,HotSpot 就会计算出对象内什么偏移量上是什么类型的数据;

3.4.2 安全点

  • 到安全点记录一次OopMap
  • 让所有线程跑到最近的安全点,停顿下来,发生垃圾收集
  • 中断方案:
    • 抢先式中断:系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上
    • 主动式中断:设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。
      轮询标志的地方:安全点、创建对象、需要在堆上分配内存的地方
  • HotSpot使用内存保护陷阱:当需要暂停用户线程时,虚拟机把0x160100的内存页设置为不可读,那线程执行到test指令时就会产生一个自陷异常信号,然后在预先注册的异常处理器中挂起线程实现等待,这样仅通过一条汇编指令便完成安全点轮询和触发线程中断了

3.4.3 安全区域

  • 确保在某一段代码片段之中,引用关系不会发生变化
  • 用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,垃圾收集就不用管安全区里的线程
  • 当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以离开安全区域的信号为止。

3.4.4 记忆集与卡表

  • 解决对象跨代引用带来的问题,用记忆集以避免把整个老年代加进GC Roots扫描范围
  • 记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
    记录精度:字长精度(跨代指针的地址)、对象精度(该对象里有字段含跨代指针)、卡精度(一块内存区域,该区域内有对象含有跨代指针)
  • 卡表:记忆集的具体实现
    字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块(卡页)
    一个卡页的内存里只要有一个对象的字段存在跨代引用对应卡表数组位就置1
    CARD_TABLE [this address >> 9] = 0;//HotSpot实现
    //地址右移9位,相当于用地址除以512–> 卡页是512字节
    在这里插入图片描述

3.4.5 写屏障:维护卡表状态

  • 在引用类型字段赋值操作之前或之后附加更新card的指令
  • 问题:
    每次只要对引用进行更新,就会产生额外的开销;
    高并发场景下的“伪共享”问题:多线程修改位于一个缓存行的变量时会彼此影响
  • 解决:
    先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏

3.4.6 并发的可达性分析

  • 必须在一个能保障一致性的快照上才能进行对象图的遍历
  • 三色理论:当且仅当以下两个条件同时满足时,会产生“对象消失”:
    1)赋值器插入了一条或多条从黑色对象到白色对象的新引用;
    2)赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
  • 解决:
    增量更新:破坏1),黑色对象一旦新插入了指向白色对象的引用之后,将其变成灰色重新扫描
    原始快照:破坏2),按照刚刚开始扫描那一刻的对象图快照来进行搜索

3.5 经典垃圾收集器

在这里插入图片描述

3.5.1 Serial收集器

  • 单线程:它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束
  • 算法:标记-复制
  • 优点:简单高效,内存消耗小,最高的单线程收集效率
  • 适合对于运行在客户端模式下的虚拟机
    在这里插入图片描述

3.5.2 ParNew收集器(Serial收集器的多线程并行版本)

  • 算法:标记-复制

  • 默认与CMS 收集器配合工作
    在这里插入图片描述

  • 收集器语境下的“并发”和“并行”概念:
    并行描述的是多条垃圾收集器线程之间的关系
    并发描述的是垃圾收集器线程与用户线程之间的关系

3.5.3 Parallel Scavenge收集器

  • 算法:标记-复制
  • Parallel Scavenge收集器的目标:达到一个可控制的吞吐量
    吞吐量:运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)
    控制吞吐量参数:
    -XX:MaxGCPauseMillis :最大垃圾收集停顿时间
    -XX:GCTimeRatio :直接设置吞吐量大小
    自适应调节策略:
    参数:-XX:+UseAdaptiveSizePolicy
    虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时 间或者最大的吞吐量

3.5.4 Serial Old收集器

  • 单线程
  • 算法:标记-整理
  • 主要供客户端使用
  • 服务端:在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用;或者作为CMS 收集器发生失败时的后备预案

3.5.5 Parallel Old收集器

Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法

3.5.6 CMS收集器

  • 算法:标记-清除

  • 以获取最短回收停顿时间为目标

  • 运行步骤:
    1)初始标记(CMS initial mark) STW
    只标记GC Roots能直接关联到的对象,速度很快
    2)并发标记(CMS concurrent mark)
    从GC Roots的直接关联对象开始遍历整个对象图的过程,耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
    3)重新标记(CMS remark) STW
    了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
    4)并发清除(CMS concurrent sweep)
    并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
    在这里插入图片描述

  • CMS缺点:
    对处理器资源非常敏感,并发导致应用程序变慢,降低总吞吐量;
    无法处理“浮动垃圾”(并发用户线程产生的新对象),有可能出现“Con-current Mode Failure”失败(CMS运行期间预留的内存无法满足程序分配新对象的需要)进而导致另一次完全“Stop The World”的Full GC的产生;
    标记清除算法,有空间碎片问题,触发Full GC

3.5.7 G1收集器:优先处理回收价值收益最大的那些Region

  • 面向服务端,Mixed GC

  • G1把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间,收集器能够对扮演不同角色的 Region采用不同的策略去处理

  • Humongous区域,专门用来存储大对象

  • 新生代和老年代不再是固定的了,都是一系列区域(不需要连续)的动态集合

  • 维护一个优先级列表,回收价值即回收所获得的空间大小以及回收所需时间的经验值

  • 跨Region引用对象问题?:
    使用记忆集避免全堆作为GC Roots扫描
    每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region 指向自己的指针,并标记这些指针分别在哪些卡页的范围之内
    内存占用负担高

  • 并发标记阶段如何保证收集线程与用户线程互不干扰地运行?
    1)使用原始快照(SATB)
    2)使用TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来存并发回收时新分配的对象地址

  • 怎样建立起可靠的停顿预测模型?
    衰减均值理论: Region的统计状态越新越能决定其回收的价值

  • G1收集器运作过程:整体基于标记-整理
    1)初始标记:标记GC Roots能直接关联到的对象,修改TAMS 指针的值,保证并发阶段能在可用region中分配新对象。停顿线程在进行Minor GC的时候同步完成,实际没有额外停顿
    2)并发标记:从GC Root开始对堆中对象进行可达性分析。扫描完成后,还要重新处理SATB记录下的在并发时有引用变动的对象。
    3)最终标记:用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。此处有短暂停顿。
    4)筛选回收:更新Region的统计数据,根据价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集。
    局部上是标记-复制:将最后决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间
    存活对象的移动,必须暂停用户线程,由多条收集器线程并行完成
    在这里插入图片描述

  • 停顿时间设置:
    如果太低,每次选出来的回收集只占堆内存很小的一部分,收集器收集的速度逐渐跟不上分配器分配的速度,导致垃圾慢慢堆积,最终引发Full GC
    通常设置为一两百毫秒或者两三百毫秒

  • G1的优势:
    可以指定最大停顿时间、分Region的内存布局、按收益动态确定回收集
    不会产生内存空间碎片

  • G1的劣势:
    垃圾收集产生的内存占用和程序运行时的额外执行负载都要比CMS高
    内存占用:G1的卡表实现复杂,每个region都必须有一个卡表

  • 执行负载:
    CMS和G1都用写后屏障更新卡表,但G1的卡表维护操作更繁琐,G1还需要使用写前屏障来跟踪并发时的指针变化情况

    • 小内存应用–>CMS表现好
    • 大内存应用–>G1

3.6 低延迟垃圾收集器

  • 衡量垃圾收集器的三项最重要的指标(不可能三角):内存占用(Footprint)、吞吐量(Throughput)和延迟
  • Shenandoah和ZGC,几乎整个工作过程全部都是并发的,只有初始标记、最终标记这些阶段有短暂的停顿,这部分停顿的时间基本上是固定的,与堆的容量、堆中对象的数量没有正比例关系

3.6.1 Shenandoah收集器

  • 支持并发的整理算法

  • 不使用分代收集

  • 弃用记忆集,改用“连接矩阵”

  • 运行步骤:
    初始标记(Initial Marking):与G1一样
    并发标记(Concurrent Marking):与G1一样
    最终标记(Final Marking):与G1一样
    并发清理(Concurrent Cleanup):清理那些整个区域内连一个存活对象都没有找到 的Region(这类Region被称为Immediate Garbage Region)。
    并发回收(Concurrent Evacuation):把回收集里面的存活对象先复制一份到其他未被使用的Region之中
    初始引用更新(Initial Update Reference):建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务
    并发引用更新(Concurrent Update Reference):真正开始进行引用更新操作,这个阶段是与用户线程一起并发的。按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值。
    最终引用更新(Final Update Reference):解决了堆中的引用更新后,还要修正存在于GC Roots 中的引用。这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。
    并发清理(Concurrent Cleanup):经过并发回收和引用更新之后,整个回收集中所有的Region已再无存活对象,最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使用

  • 并行整理核心概念Brooks Pointer:
    在原有对象布局结构的最前面统一增加一个新的引用字段,在正常不处于并发移动的情况下,该引用指向对象自己
    当对象拥有了一份新的副本时,只需要修改一处指针的值,即旧对象上转发指针的引用位置,使其指向新对象,便可将所有对该对象的访问转发到新的副本上。
    在这里插入图片描述

  • 存在多线程竞争问题,通过比较并交换(Compare And Swap,CAS)操作来保证并发时对象的访问正确性

  • 缺点:读屏障数量多,代价高

3.6.2 ZGC收集器

  • 在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟
  • ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
  • ZGC的Region(也被称为Page或者ZPage)具有动态性——动态创建和销毁,以及动态的区域容量大小。
  • 并发整理算法的实现:染色指针技术

3.7 垃圾收集器的选择

3.7.1 Epsilon收集器

不进行垃圾收集,复责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等

3.7.2 收集器的权衡

应用程序的主要关注点
运行应用的基础设施
使用JDK的发行商、版本号

3.7.3 虚拟机及垃圾收集器日志

  • HotSpot所有功能的日志都收归到了“-Xlog”参数
    命令⾏:-Xlog[:[selector][:[output][:[decorators][:output-options]]]]
    最关键的参数是选择器(Selector),由标签(Tag)和⽇志级别(Level)共同组成。
    垃圾收集器的标签名称为“gc”,
    ⽇志级别从低到⾼,共有Trace, Debug, Info, Warning, Error, Off六种级别,⽇志级别决定了输出信息的详细程度,默认级别为Info;
    还可以使⽤修饰器(Decorator)来要求每⾏⽇志输出都附加上额外的内容,⽀持附加在⽇志⾏上的信息包括:
    time:当前⽇期和时间;
    uptime:虚拟机启动到现在经过的时间,以秒为单位;
    timemillis:当前时间的毫秒数,相当于System.currentTimeMillis()的输
    出。
    uptimemillis:虚拟机启动到现在经过的毫秒数。
    timenanos:当前时间的纳秒数,相当于System.nanoTime()的输出。
    uptimenanos:虚拟机启动到现在经过的纳秒数。
    pid:进程ID。
    tid:线程ID。
    level:⽇志级别
    如果不指定, 默认值是uptime、level、 tags这三个;

3.8 实战:内存分配与回收策略

(1)对象优先在Eden区分配
⼤多数情况下,对象在新⽣代Eden区中分配。当Eden区没有⾜够空间进⾏分配
时,虚拟机将发起⼀次Minor GC;
(2)⼤对象直接进⼊⽼年代
⼤对象就是指需要⼤量连续内存空间的Java对象,HotSpot虚拟机提供了-
XX:PretenureSizeThreshold参数,指定⼤于该设置值的对象直接在⽼年代分配,
⽬的在于避免在Eden区及两个Survivor区之间来回复制,产⽣⼤量的内存复制操
作。
(3)⻓期存活的对象将进⼊⽼年代
虚拟机给每个对象定义了⼀个对象年龄(Age)计数器,存储在对象头中。对象通
常在Eden区⾥诞⽣,如果经过第⼀次Minor GC后仍然存活,并且能被Survivor容
纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在
Survivor区中每熬过⼀次Minor GC,年龄就增加1岁,当它的年龄增加到⼀定程度
(默认为15),就会被晋升到⽼年代中。对象晋升⽼年代的年龄阈值,可以通过参
数-XX:MaxTenuringThreshold设置。
(4)动态对象年龄判定
为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的
年龄必须达到-XX:MaxTenuringThreshold才能晋升⽼年代,如果在Survivor空间中
相同年龄所有对象⼤⼩的总和⼤于Survivor空间的⼀半,年龄⼤于或等于该年龄
的对象就可以直接进⼊⽼年代,⽆须等到-XX:MaxTenuringThreshold中要求的年

(5)空间分配担保
发⽣Minor GC之前,检查➀⽼年代最⼤可⽤的连续空间是否⼤于新⽣代所有对象
总空间➁-XX:HandlePromotionFailure参数的设置值是否允许担保失败➂检查⽼年
代最⼤可⽤的连续空间是否⼤于历次晋升到⽼年代对象的平均⼤⼩;
==》➀不满⾜时进⾏➁,➁允许进⾏➂,➂满⾜时尝试⼀次MinorGC,➁不允许或者➂不满⾜则进⾏Full GC;

JDK 6 Update 24之后的规则变为只要⽼年代的连续空间⼤于新⽣代对象总⼤⼩或
者历次晋升的平均⼤⼩,就会进⾏Minor GC,否则将进⾏Full GC;

Reference:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) (华章原创精品) - 周志明

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值