JVM垃圾回收篇(经典垃圾回收器讲解)

1.Serial垃圾收集器

  • 概述
    请添加图片描述

    • Serial收集器是最基本、历史最悠久的垃圾收集器了
    • Serial收集器作为HotSpot VM中Client模式下的默认新生代垃圾收集器
    • Serial收集器采用复制算法、串行回收、STW机制的方式执行内存回收
    • Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器,Serial Old收集器同样也采用了串行回收、STW机制,只不过内存回收算法使用的是标记压缩算法
    • Serial收集器是一个单线程的收集器,但它的“单线程”意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束
  • 优点

    • 与其他收集器的单线程比,简单而高效
    • 对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率
  • 设置

    • 在HotSpot虛拟机中,使用参数 -XX: +UseSerialGC 可以指定年轻代和老年代都使用串行收集器
  • 现状

    • 一般在交互较强的应用程序中是不会采用串行垃圾收集器的
    • 一般在JavaWeb应用程序中是不会采用串行垃圾收集器的

2.ParNew垃圾收集器

  • 概述
    请添加图片描述

    • 如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本
    • ParNew收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别
    • ParNew收集器在年轻代中同样也是采用复制算法、STW机制
    • ParNew收集器是很多JVM运行在Server模式下新生代的默认垃圾收集器
    • ParNew收集器运行在多CPU的环境下,由于可以充分利用多CPU、 多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量
  • 组合

    • 对于新生代,回收次数频繁,使用并行方式的ParNew收集器执行高效
    • 对于老年代,回收次数少,使用串行方式的Serial Old收集器节省资源
  • 设置

    • 我们可以使用参数 -XX: +UseParNewGC 可以指定年轻代使用ParNew垃圾收集器执行内存回收任务,不影响老年代
    • 我们可以使用参数 -XX: ParallelGCThreads 限制ParNew收集器启用的线程数量,默认为主机CPU的总线程数

3.Parallel垃圾收集器

  • 概述
    请添加图片描述

    • Parallel Scavenge收集器采用了复制算法、并行回收、STW机制
    • Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量,它也被称为吞吐量优先的垃圾收集器
    • Parallel Scavenge收集器拥有自适应调节策略,根据程序运行情况,动态进行调整
    • Parallel Scavenge收集器在JDK1.6时提供了用于执行老年代垃圾收集的 Parallel Old 收集器,用来代替老年代的Serial Old收集器,它采用了标记压缩算法,并行回收、STW机制
    • Parallel Scavenge收集器是JDK8中的默认垃圾收集器
  • 组合

    • 在程序吞吐量优先的应用场景中,Parallel收集器和 Parallel Old 收集器的组合,在Server模式下的内存回收性能很不错
  • 场景

    • 高吞吐量则可以高效率地利用CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务,因此经常在服务器环境中使用
  • 设置

    • 通过参数 -XX: +UseParallelGC 设置年轻代使用Parallel并行收集器执行内存回收任务
    • 通过参数 -XX: +UseParallelOldGc 设置老年代使用Parallel Old并行收集器执行内存回收任务
    • 通过参数 -XX: ParallelGCThreads 设置年轻代并行收集器的线程数
      • 默认情况下,当CPU数量小于等于8个的时候,ParallelGCThreads的值等于当前CPU数量
      • 默认情况下,当CPU数量大于8个的时候,ParallelGCThreads的值等于 3 + 5*当前CPU数量/8
    • 通过参数 -XX: MaxGCPauseMillis 设置垃圾收集器最大停顿时间( STW 时间 ),单位是毫秒
      • 为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整Java堆大小或者其他一些参数
    • 通过参数 -XX: GCTimeRatio 设置垃圾收集时间占总时间的比例,用于衡量吞吐量的大小
      • 取值范围(0,100),默认值99,也就是垃圾回收时间占总时间的比例为 1:99
    • 通过参数 -XX: +UseAdaptiveSizePolicy 设置垃圾收集器是否具有自适应调节策略
      • 在这种模式下,年轻代的大小、Eden区和Survivor区的比例、晋升老年代对象的年龄等参数都会被自动调整,找到在堆大小、吞吐量、停顿时间之间的平衡点
      • 在手动调优比较困难的场合,可以直接使用这种自适应的方式,让虚拟机自己完成调优工作

4.CMS垃圾收集器

  • 概述

    • CMS收集器是在JDK1.5的时候提出的,是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作
    • CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短就越适合与用户交互的程序,良好的响应速度能提升用户体验
    • CMS收集器采用标记清除算法、 STW机制
    • CMS收集器作为老年代的收集器,无法与JDK1.4中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK1. 5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器
  • 过程
    请添加图片描述

    • 初始标记阶段

      在这个阶段中,程序中所有的工作线程都将会因为STW机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象,一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的查找的速度会非常快

    • 并发标记阶段

      从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与用户程序线程一起并发运行

    • 重新标记阶段

      由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短

    • 并发清除阶段

      此阶段清理删除标记阶段找出的该被清除的对象,然后释放内存空间,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

  • 注意

    • 尽管CMS收集器采用的是并发回收,但是在其初始化标记和再次标记这两个阶段中仍然需要执行STW机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要STW,只能是尽可能地缩短暂停时间

    • 由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收过程是低停顿的

    • 由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满时再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。

    • 如果CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备方案,也就是临时启用Serial Old收集器来重新进行老年代的垃圾收集,只不过这样停顿时间就很长了

    • CMS收集器的垃圾收集算法采用的是标记清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。 那么CMS在为新对象分配内存空间时,将无法使用指针碰撞技术,只能够选择空闲列表执行内存分配

      既然标记清除算法会造成内存碎片,那么为什么不把算法换成标记压缩呢?

      因为当在并发清除阶段的时候,用户线程和垃圾收集线程在一并执行,此时如果使用标记压缩算法进行垃圾收集然后整理的话,必然要将对象进行整理移动,这样对象的地址就要发生变化。所以为了保证用户线程能继续执行,保证在它运行期间资源不受影响,所以只能使用标记清除算法

  • 优点

    • 采用并发收集
    • 低延迟
  • 缺点

    • CMS收集器会产生内存碎片,导致并发清除后,用户线程可用的空间不足、无法分配大对象、不得不提前触发Full GC
    • CMS收集器对CPU资源非常敏感,在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低
    • CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure" 失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间
  • 设置

    • 通过参数 -XX: +UseConcMarkSweepGC 设置老年代使用CMS收集器执行内存回收任务,同时也会设置年轻代使用ParNew收集器执行内存回收任务
    • 通过参数 -XX: CMSInitiatingOccupanyFraction 设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收
      • JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS垃圾回收
      • JDK6及以上版本默认值为92,即当老年代的空间使用率达到92%时,会执行一次CMS垃圾回收
      • 如果内存增长缓慢,则可以设置一个稍大的值。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值
    • 通过参数 -XX: +UseCMSCompactAtFullCollection 设置在执行完Full GC后,是否对内存空间进行压缩整理,以此避免内存碎片的产生
    • 通过参数 -XX: CMSFullGCsBeforeCompaction 设置在执行多少次Full GC后对内存空间进行压缩整理
    • 通过参数 -XX: ParallelCMSThreads 设置CMS收集器执行过程中使用的线程数量,默认启动的线程数是(主机线程数 + 3) / 4

5.G1垃圾收集器

1.基本介绍

  • 由于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化,最终出现了G1收集器
  • G1收集器是在Java7之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一
  • G1收集器为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低了暂停时间,同时还兼顾良好的吞吐量
  • G1收集器的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担起“全功能收集器”的重任与期望
  • G1收集器是一个并行垃圾回收器,它把堆内存分割为很多不相关的区域,即Region区域,然后使用不同的Region来表示Eden区、幸存者0区,幸存者1区,老年代区
  • G1收集器有计划地避免了在整个Java 堆中进行全区域的垃圾收集,G1会跟踪各个 Region 里面的垃圾堆积的价值大小,也就是回收所获得的空间大小以及回收所需时间的综合衡量标准。根据这个标准,优先回收价值最大的Region区域,并把它加入到一个优先列表中进行维护
  • G1收集器是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还能够兼具高吞吐量的性能特征
  • G1收集器在JDK7版本正式启用,是JDK9以后版本的默认垃圾回收器,被Oracle官方称为“全功能的垃圾收集器”

2.四大特性

  • 并行与并发

    • 并行性: G1在回收期间,可以有多个GC线程同时工作,有效的利用了多核计算能力。同时除了G1以外,其他的垃圾收集器使用的都是内置的JVM线程来执行GC的多线程操作,而G1可以利用应用程序的线程运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程
    • 并发性: G1拥有与应用程序交替执行的能力,部分垃圾收集工作可以和应用程序同时执行
  • 分代收集

    • G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的整体结构上看,它不要求整个年轻代或者老年代都是连续的,也不必要让它们使用固定大小和固定数量的空间。而是将整个堆空间分为若干个区域 ,这些区域中包含了逻辑上的年轻代和老年代
      请添加图片描述

    • G1和之前的各类垃圾回收器不同,它同时兼顾年轻代和老年代的回收

  • 空间整合

    • G1将内存划分为一个个的Region区域,内存回收的基本单位也就是Region
    • G1的Region之间采用的是复制算法,但整体上可看作是标记压缩算法,不过不管哪种算法,都可以避免产生内存碎片
    • G1的这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC,尤其是当Java堆非常大的时候,G1的优势更加明显
  • 可预测的停顿时间

    • G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
    • G1可以只选取部分区域进行内存回收,这样就可以缩小回收的范围,因此对于全局停顿也能得到较好的控制
    • G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次优先回收价值最大的Region,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率

3.不足之处

  • G1还不具备全方位、压倒性优势,相较于CMS收集器而言。因为在用户程序运行过程中,G1对于垃圾收集产生的内存占用和运行时的额外负载都要比CMS高很多
  • G1不适合在小内存应用上进行使用

4.参数设置

  • 通过参数 -XX: +UseG1GC 设置使用G1收集器执行内存回收任务
  • 通过参数 -XX: G1HeapRegionSize 设置每个Region的大小,值必须是2的幂次方,范围是1MB~32MB之间,目标是根据最小的Java堆大小划分出约2048个区域,默认是堆内存的1/2000
  • 通过参数 -XX: MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标,默认值是200ms
  • 通过参数 -XX: ParallelGCThread 设置并行执行过程中使用的线程数量,最大值为8
  • 通过参数 -XX: ConcGCThreads 设置并发执行过程中使用的线程数量,值是并行执行过程中使用的线程数量的四分之一
  • 通过参数 -XX: InitiatingHeapOccupancyPercent 设置触发并发垃圾回收的条件,即Java堆占用率的阈值,超过阈值则触发GC,默认值是45

5.适用场景

  • 面向服务端应用,针对具有大内存、多处理器的机器
  • 最主要应用在,需要低GC延迟并具有大堆的应用程序,比如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒
  • 当超过50%的Java堆被活动数据占用、对象分配频率或年代提升频率变化很大、GC停顿时间过长的时候,可以用来替换CMS垃圾收集器

6.什么是Region?

  • 使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块
  • 每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次
  • 每个Region块大小相同,且在JVM生命周期内不会被改变
  • 虽然还保留有新生代和老年代的概念,但由于Region的出现,新生代和老年代不再是物理隔离的了,它们都是一个个Region的集合,可以通过Region的动态分配实现逻辑上的连续
  • 一个Region有可能属于Eden, Survivor、Old内存区域中的一个,但是一个Region在指定时间段内只可能属于一个角色,当该Region被回收清理后可以充当另一种角色
  • Region中还增加了一种新的内存区域,叫做Humongous内存区域或者H区,它专门用于存储大对象。如果一个H区装不下某个大对象,那么G1会寻找连续的H区来存储,所以为了能找到连续的H区,有时候不得不启动Full GC

7.什么是记忆集?

  • 记忆集( Remembered Set ),是为了解决一个对象被不同区域引用时回收的问题

  • 我们知道一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,好比是老年代中的对象引用了年轻代中的对象,那么在进行某个Region回收的时候,就需要判断其中的对象是否被其它Region区域引用着

  • 无论G1还是其他分代收集器,JVM都是使用 Remembered Set 来避免全局扫描

  • 每个Region都有 一个对应着一个Remembered Set

  • 每次引用类型数据写操作时,都会产生一个Write Barrier( 写屏障 )的事件产生,进而产生中断操作。在中断的过程中,检查将要写入的引用指向的对象是否和该引用类型数据在不同的Region区域,如果不同,则通过Card Table把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中
    请添加图片描述

  • 当对某个Region进行垃圾收集时,查看Region对应的Remembered Set中是否存有其它Region区域的引用信息,如果有则不进行回收,反之则回收

8.G1回收过程

请添加图片描述

  • 年轻代的回收( Young GC )
    • 应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程
    • G1的年轻代收集阶段是一个并行的独占式收集器,在年轻代回收期,G1暂停所有应用程序线程,启动多线程执行年轻代回收
    • 然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及
  • 老年代并发标记过程( Concurrent Marking )
    • 当堆内存使用达到一定值( 默认45% )时,开始老年代并发标记过程
  • 混合回收( Mixed GC )
    • 标记完成后,马上开始混合回收过程
    • 在混合回收中,G1从老年区移动存活对象到空闲区,这些空闲区间也就成为了老年代的一部分
    • 和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描并回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的
  • 全回收( Full GC )
    • 如果需要单线程、独占式、高强度的应用场景,Full GC还是继续存在的
    • 它也是针对GC的评估失败提供的一种失败保护机制

9.G1回收详细步骤

  • 年轻代的回收( Young GC )

    JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1就会启动一次年轻代垃圾回收。年轻代垃圾回收只会回收Eden区和Survivor区,回收时,首先停止所有应用程序的执行,然后创建一个回收集( 需要被回收的内存分段的集合,包括Eden区分段和Survivor区分段 )

    1. 扫描根

      根是指static变量指向的对象、正在执行的方法调用链条上的局部变量等。根引用和Remembered Set中记录的外部引用一同作为扫描存活对象的入口

    2. 更新记忆集

      处理 dirty card queue ( 由于直接更新RememberSet需要线程同步,开销比较大,所以JVM在进行对象引用赋值的时候,将该引用信息保存在dirty card队列中,之后更新RememberedSet时使用 ) 中的 card,然后更新Remembered Set,此时Remembered Set就可以准确的反映老年代对所在的内存分段中对象的引用情况

    3. 处理记忆集

      识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象

    4. 复制对象

      将对象树进行遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段。而Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到Old区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接复制到Old区中空的内存分段

    5. 处理引用

      处理Soft,Weak,Phantom,Final,JNI Weak等引用,最终Eden空间的数据为空且目标内存中的对象都是连续存储的,没有内存碎片,所以复制过程可以达到内存整理的效果

  • 老年代并发标记过程( Concurrent Marking )

    1. 初始标记

      标记从根节点直接可达的对象,这个阶段是STW的,并且会触发一次年轻代GC

    2. 根区域扫描

      G1扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象,并且这一过程必须在年轻代GC之前完成

    3. 并发标记

      在整个堆中进行并发标记,此过程可能被年轻代GC中断。在并发标记过程中,若发现某个区域中的所有对象都是垃圾,那这个区域会被立即回收。标记时,也会计算每个区域的对象活性( 区域中存活对象的比例 )

    4. 再次标记

      由于在上述过程中应用程序持续进行,所以需要修正上一次的标记结果,这里G1采用了比CMS更快的初始快照算法:SATB算法

    5. 独占清理

      计算各个区域的存活对象和GC回收的比例,并进行排序,用于在混合回收阶段识别可以回收的区域

    6. 并发清理

      识别并清理完全空闲的区域

  • 混合回收( Mixed GC )

    • 当越来越多的对象晋升到老年代区域时,为了避免堆内存被耗尽,JVM会触发一个混合的垃圾收集器( Mixed GC )。回收过程中,除了回收整个年轻代区域,还会回收一部分的老年代区域
    • 并发标记结束以后,老年代中全部为垃圾对象的内存分段被立即回收了,部分为垃圾的内存分段会被计算活性。默认情况下,这些老年代的内存分段会分8次被回收,可以通过参数 -XX: G1MixedGCCountTarget 设置
    • 混合回收的回收集包括八分之一的老年代内存分段、Eden区内存分段、Survivor区内存分段。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段
    • 由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段,也就是垃圾占内存分段比例越高的,越先被回收。并且有一个阈值会决定内存分段是否被回收,默认值是65%,意思是垃圾占内存分段比例要达到65%才会被回收,这个值可以通过参数 -XX:G1MixedGCLiveThresholdPercent 进行设置
    • 其实混合回收也并不一定要进行8次。默认情况下G1允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则可以不再进行混合回收,这个值可以通过参数 -XX:G1HeapWastePercent 进行设置
  • 全回收( Full GC )

    • G1的初衷就是要避免Full GC的出现,但是如果上述过程不能正常工作,G1就会停止应用程序的执行。然后使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长,所以要避免Full GC的发生
    • 当堆内存太小、当G1在复制存活对象的时候没有空的内存分段可用、当并发处理过程完成之前空间耗尽的时候,都会进行Full GC

6.垃圾收集器总结

请添加图片描述

  • Java垃圾收集器的配置对于JVM优化来说是一个很重要的过程,选择合适的垃圾收集器可以让JVM的性能有一个很大的提升
  • 在进行选择配置时,优先调整堆的大小让JVM自适应完成。如果内存小于100M,那就使用串行收集器。如果是单核、单机程序,并且没有停顿时间的要求,那就使用串行收集器。如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,那就使用并行收集器。如果是多CPU、追求低停顿时间、需快速响应,那就使用并发收集器。现在互联网的项目,基本都是使用G1垃圾收集器
  • 没有最好的收集器,更没有万能的收集,调优也永远是针对特定场景、特定需求而不断改变的,所以不存在一劳永逸的收集器
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

编程小吉

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

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

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

打赏作者

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

抵扣说明:

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

余额充值