JAVA中的垃圾回收器(2)------G1

G1垃圾回收器:-XX:+UseG1GC:使用G1收集器

1)垃圾收集器迭代停顿时间越少越好,但是垃圾回收的总时间会增多,默认暂停时间默认是200ms,G1的内部底层算法非常复杂比CMS复杂,如果大内存,G1还比较有效果,但是如果堆内存不大,G1性能还不如CMS,在小内存效率依然不高;

2)注重于停顿时间和回收regin的效益比,适用于堆内存比较大的内存,最大停顿时间的优势就凸显出来了,停顿时间比较短,对于用户体验非常好,适用于高并发大内存的情况下,很好的解决大内存STW时间更长的问题;

3)空白的regin使用链表来进行连接

一)G1垃圾收集器介绍

1)G1垃圾回收器是一款面向服务器的垃圾收集器,主要针对的是配备多个处理器以及大容量内存的机器,以极高的频率来满足GC停顿时间的要求的同时来尽量的提升吞吐量

2)虽然在物理上分代已经不连续了,但是在逻辑上还是存在着分代的概念的

G1在大内存中对整个用户的停顿时间可以控制的优势是非常明显的,如果内存特别大,那么可控停顿时间的优势就会非常明显

但是在小内存中G1的优势就不算是太明显了,因为G1内部有很多很难的内部算法细节,整个算法时间复杂度非常高,又要估计regin的回收价值,又要分配卡表和记忆集,本来CMS和parNew的垃圾回收也是可以的了;

3)G1堆将JAVA的堆划分成多个大小相等的独立区域,JVM最多有2048个regin,一般的regin大小等于堆内存大小除以2048,比如说堆的大小是4096M那么regin的大小就是2M,当然也是可以使用参数-XX:G1HeapReginSize来手动指定Regin的大小,但是推荐默认的计算方式

4)G1保留了年轻代和老年代的概念,但是不在是物理隔阂了,都是不连续的Regin的集合,默认年轻代对于堆内存的占比是5%,如果堆内存大小是4096M,那么年轻代大概占据200M的内存,大概是100个regin,可以使用-XX:G1NewSizePercent来设置新生代初始占比,在系统运行过程中JVM会不断给年轻代增加更多的regin,但是最多新生代的占比也没有超过60%,可以通过-XX:G1MaxNewSizePercent调整,年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1,假设年轻代现在有1000个region,eden区对应800个,s0对应100个,s1对应100个,一开始regin内存区域都是空的,随着空间不断被使用,年轻代不在扩容,程序运行后期这些不同的regin会被赋予不同的含义;

5)一个regin可能之前是年轻代,如果regin进行了垃圾回收,可能就变成了老年代,也就是说regin的功能区域可能会动态发生变化

6)G1垃圾收集器对于对象什么时候会转移到老年代和之前说过的原则一模一样,唯一不同的是针对于大对象的处理,G1有一个专门分配大对象的区域叫做Humongous区域,而不是让大对象直接进入到老年代的Regin中,在G1中有专门分配大对象的区域叫做Humongous区,大对象的判断规则就是一个对象超过了Regin区域的一半,按照上面的计算规则,每一个Regin的大小是2M,只要对象的大小超过了1M,就会被存放到Humongous,如果对象特别大,还有可能使用多个连续的Regin存放Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销FullGC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收;

1)G1垃圾收集器会让初始标记和最终标记和筛选回收的总的STW时间控制在一个设置的参数范围内,在筛选回收中G1垃圾回收器不一定会把所有的堆中的垃圾全部回收掉,可能在筛选回收之前并发标记过程中标记了很多非垃圾对象还有很多的垃圾对象要进行清理,但是在G1筛选回收阶段,因为要考虑到停顿时间,可能要把整个堆空间回收完成要400ms,但是程序员指定的STW时间是200ms,G1会有一个算法来预估回收多少块区域(预估每一块regin大概要回收多长时间)能够达到用户设置的最大停顿时间,剩余的区域下一次GC回收;

2)一共的GC总时间比CMS时间还长,ZGC的垃圾回收一次完整过程的时间可能会更久;

3)可不可以把GC的停顿时间指定的非常短呢,不要乱指定,既然需要停顿时间短一些,就指定为10ms,G1一次性清理的垃圾regin个数非常非常少,垃圾积累多了,可能会触发大级别的FullGC,直接STW,直接单线程垃圾收集,本来设置时间是200ms,但是你设置10ms,最终可能导致G1停顿时间失效,甚至于GC垃圾收集的速度明显赶不上内存垃圾产生的速度,最后还有可能出现FullGC,是独占式的;

G1可能会触发GC,设置最小暂停时间,比如说20ms,要是设置成20ms,那就说明垃圾回收器必须要在20ms时间内垃圾回收完成,如果此时垃圾回收的速度赶不上用户线程造垃圾的速度,那么此时就会产生FullGC

4)G1垃圾回收器在后台维护了一个优先级列表,每一次根据允许的收集时间,优先进行选择回收加载最大的regin,比如说一个regin花费200ms能够回收10M垃圾,另外一个regin花费50ms能够回收20M垃圾,在总回收时间有限的情况下,G1当然会优先回收后面这个;

5)一块区域存活的对象越多,垃圾回收的效益比越低,又要移动对象更新引用啥的,应该优先回收垃圾比较多的regin区域;

二)G1垃圾回收分类: 

2.1)MinorGC:

伊甸园区默认占用整个堆空间的5%,假设现在伊甸园区放满了,之前的垃圾回收器Parallel NEW,伊甸园区满了之后会触发young GC,但是对于G1来说并不是这样,原始的伊甸园区放满以后并不会触发Young GC,G1有一个最大默认停顿时间默认值是200ms,G1垃圾回收器会进行计算,假设这5%的伊甸园区如果要是做young GC的回收接近200ms,会触发minorGC,但是如果垃圾回收器计算下来整个伊甸园区计算下来只需要50ms,远远小于200ms,那么就说明此时伊甸园区太小了,垃圾其实并不多,但是G1是分区算法5%的伊甸园区满了以后,G1垃圾回收器会把这些新new出来的对象放在其他新的空闲的空的regin格子中,那么此时这个regin的格子就是一个新的伊甸园区,此时如果隔一段时间之后发现系统评估的伊甸园区总的回收时间接近于200ms,此时就会触发youngGC,G1垃圾回收器在进行触发young GC之前会评估一下计算一下当下伊甸园区的回收时间是否接近于最大停顿时间,如果接近了,就做minor GC,如如果最大停顿时间远远大于伊甸园区的总的回收时间,那么此时伊甸园区会扩容,把这些新对象丢到新的空区域,这些新的空区域就又变成了伊甸园区,直到G1垃圾回收器判断整个伊甸园区的垃圾回收时间接近于最大停顿时间,此时才会触发minor GC;

应用程序分配内存,当年轻代的Eden园区即将用尽的时候开始年轻代回收过程,G1的年轻代收集仍然是一个独占式并行的垃圾回收过程,在年轻代回收过程中,G1 GC将停止所有的应用程序线程,启动多线程执行年轻代回收,然后从年轻代区间移动存活对象到幸存者区或者是老年代区间,也有可能是两个区间都会涉及;

2.2)年轻代GC+并发标记过程

老年代的堆空间的占有率达到参-XX:IniatiattingHeapingOccupanyPercent设定的值来触发,默认情况下是45%,标记完成以后马上执行混合回收

2.3)MixedGC:

根据最大停顿时间来计算回收效益比,它会回收所有的Young和部分Old区域(根据GC的停顿时间确定Old区域垃圾收集的先后顺序)以及大对象区域正常情况下G1的垃圾收集是先做MixedGC,主要采用的是复制算法,需要把各个Regin中的存活的对象拷贝到别的Regin区域里面去,拷贝过程中如果发现没有空的Regin能够承载拷贝对象就会触发一次FullGC,会进行回收所有的新生代和部分老年代,因为它还是要根据停顿时间来进行计算最大回收效益比,对于大对象区域的HS也会回收,

2.4)FullGC:

如果说触发MixedGC的占用老年代的参数设置的比较小比如说45%,那么55%都是新生代,如果新生代太多,说明此时新生代占用regin过多,此时就没有额外的空闲regin供老年代对象来进行复制了,此时就会触发Full gc,因为复制算法是对剩余的空间要求是比较高的,如果剩余空间太小没有充足的空间进行复制,此时没空间存放来年代对象,要对停顿时间做一个好的把控,此时直接单线程收集,使用单线程垃圾回收器进行回收的,STW时间非常长;

之前的垃圾回收器,只是需要在年轻代记录一个卡表就可以了,但是在G1来说,每一个regin区域都有一个卡表,记录着老年代区域对于新生代的引用关系;

停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的,Shenandoah优化成多线程收集了;

针对于-XX:MxGCPauseMillis这个参数来说,如果说这个值设置的特别大,可能会导致系统运行很久,年轻代可能都已经占用到了堆内存的60%了,才会触发YGC,那么此时存活的对象很多,此时就有可能导致幸存区存不下那么多的对象,就会进入到老年代,或者是说年轻代GC以后,存活下来的对象过大导致进入到幸存区触发了动态年龄判断机制,达到了幸存者区的50%,也会是得一些对象快速进入到了老年代

XX:MaxGCPauseMills这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixedgc

1)CMS并发清理的过程中回收垃圾是和用户线程并发执行的,G1的筛选回收过程是会造成STW的;

2)CMS并发清理过程中会产生内存碎片,但是G1时使用的复制算法,不会产生内存碎片;

3)G1使用了Regin划分内存空间以及有优先级的区域回收方式,保证了G1在有限的垃圾回收时间内尽可能保证比较高的回收效率)

4)G1相比于CMS要占用更多的内存空间,比如说要使用记忆集和卡表,G1的垃圾回收器额外占用10%-20%的内存空间,主要用于存放记忆集

并行:垃圾回收线程是并行的,独占式:会产生STW

1)年轻代GC:当Eden园区不足的时候,就会触发复制算法执行YGC,将存活的对象复制到Surivor区域或者是Old区域,此事是独占式GC,需要STW

2)年轻代GC和老年代并发标记:当内存使用率达到一定值的时候默认是45%,开始执行老年代的并发标记

3)混合回收:老年代并发标记完成以后就会执行混合回收流程,老年代将活得对象移动到空闲区域,只会回收一部分老年代的regin,此时新生代和老年代是一起回收的

4)全局垃圾回收:也叫做全堆垃圾回收,当GC设置的回收时间过低,垃圾回收频率过低,增长垃圾的速度比较快的时候,可能会触发FGC,他是独占式的,单线程的垃圾回收

三)G1的使用场景:

1)假设现在在每秒几十万数据量的高并发的系统的环境下,KFK一秒钟可以处理几十万单,肯定要使用到大内存,此时就是需要使用GC,小内存是肯定不符合要求的,假设一个订单一KB,几十万的消息,每一秒钟有5 6个G放到新生代,这个时候有大量数据对象直接晋升到老年代,等了几秒以后发现老年代也放不下,可能每几秒钟就需要做一次FullGC,但是触发fullGC可能,消息还没有执行完,此时还是放不出来空间,此时直接报OOM;

2)所以为了避免系统触发FullGC或者导致OOM,针对于这种高并发场景肯定是用大内存的机器,因为大部分高并发对象都是朝生夕死,经过一次minorGC有可能就被回收,所以将新生代的空间设置的比较大,5 6g都可以,但是此时minorGC的执行是非常慢的,会造成卡顿,这个时候做GCroot,遍历整个年轻代的对象速度很慢,尽管存活对象不多,但是要遍历几十个G的新生代还是比较浪费时间,所以young GC的回收速度不一定比老年代回收速度快,因为如果新生代特别大,就是一个反例,还有就是新生代的垃圾回收非常慢,如果不使用G1,那么此时的用户会非常卡顿,如果不使用G1那么STW时间非常长

3)如果用G1,使用固定停顿时间200ms,每一次GC也可以收集好几个G,可以回收内存空间,虽然整个GC时间比较多,但是不会导致用户等待时间非常长,虽然总的吞吐量还不错,对于用户体验还不错;

4)G1边处理业务,边收集垃圾,STW降到非常短的时间

为什么要分代?因为很多对象的生命周期不一致,有的对象朝生夕死new出来之后需要马上进行销毁掉,有的老对象,生命周期就是很短,如果将这些对象放在一起,每一次都需要扫描整个堆的区域,是很麻烦的,那堆时时刻刻都需要做GC的;

堆空间越大,垃圾回收时间会变长,用户基本无感知,那么震哥哥系统就可以在卡顿几乎无感知的情况下一便处理业务一边收集垃圾;

四)G1为什么使用增量更新?

1)增量更新效率更低,因为要从增量节点数据结构的根节点继续向下扫描,G1要是采取扫描的话,你会涉及到很多regin区域的跨代扫描,就会比较麻烦,这个时候跨很多代,遍历的复杂度会更高,增量更新不需要跨很多代,老年代有很多卡表;

2)但是CMS只有一个卡表,STAB只需要标记一下,不需要重新扫描,只需要下一轮GC再次进行回收即可,下一次再去重新去回收垃圾即可,对于G1来说,多一些浮动垃圾也没啥事,因为本身算法很多垃圾对象都不会回收,多一些浮动垃圾也没啥事;

原来的时候是新生代老年代一整块的空间,称之为是逻辑上是连续的,现在把G1打成一块一块的regin,化整为0,使用G1收集器的时候,他将整个JAVA堆空间划分成约2048个大小相同的独立的Regin,每一个Regin块大小根据堆空间的实际大小确定,整体被控制在1MB到32MB之间,况且是2的N次幂,即1MB,2MB,4MB.可以通过-XX:G1HeapReginSize设定,所有的Regin大小相同,且在JVM生命周期内不会被改变,虽然还保留着新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分Regin的集合,通过Regin的动态分配方式实现逻辑上的隔离; 

假设此时yongGC正在执行将伊甸园区的数据全部进行回收,这个时候就把这个伊甸园区的regin放到空闲列表中,从而记录这些空闲的regin,下一刻可能这个regin可能被从空闲列表取出来,变成老年代

设置H的原因:对于堆中的大对象,默认会直接被分配到老年代,但是如果他是一个断其存在的大对象,就会对垃圾收集器造成负面影响,为了解决这样一个问题,G1划分了一个Humongous区,它是用来专门存放大对象,如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储,为了可以找到连续的H区,有时候不得不启动Full GC,G1的大多数行为都会把H区当作是老年代的一部分来看待

五)G1垃圾回收器的回收过程:

1)初始标记:暂停所有的其他线程,并记录下gcroots直接能引用的对象,速度很快

2)并发标记:如果在并发标记过程中发现某些regin对象的存活率很小甚至于说基本没有对象存活,那么G1就会在这个阶段将整个regin回收

3)重新标记:

4)筛选回收:筛选回收阶段实现针对每一个的Regin的回收价值和成本来进行排序,根据用户所期望的GC停顿时间来制定回收计划,比如说此时老年代有1000个regin都满了,但是因为根据本次停顿时间,本次来及回收可能只是停顿200ms,那么根据之前回收成本计算可知回收其中的800个regin可能只是需要200ms,那么就只会回收800个regin,尽量把GC的停顿时间控制在我们想要的范围内

其实这个过程也是可以和用户线程一起并发执行,但是因为只是回收一部分regin,时间是用户可控制的,而且停顿用户线程可以提升垃圾回收的效率,不管是年轻代还是老年代,回收算法主要采用的是复制算法,将一个regin的存活对象存放到另一个regin中,这种不会像CMS一样回收玩因为还有很多内存碎片需要重新整理一次,G1采用复制算法几乎不会有太多的内存碎片

(注意:CMS回收阶段是跟用户线程一起并发执行的,G1因为内部实现太复杂暂时没实现并发回收,不过到了Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本)

六)G1垃圾回收器的特点:

一)G1垃圾回收器兼具并行和并发:

并行性:G1在回收期间,可以存在多个GC线程同时工作,有效地利用CPU的多核能力,此时筛选回收阶段用户线程STW

并发性:G1具有和应用线程交替执行的能力,并发标记可以和应用线程同时执行

二)分代收集:

1)从分代角度来看G1仍然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然存在伊甸区和幸存区,但是从对的结构上来看,他不要求整个伊甸区,年轻代和老年代都是连续的,也不再坚持固定大小和固定数量;

2)将堆空间划分成若干个区域,这些区域中包含了逻辑上的年轻代和老年代,实际工作不要求连续;

3)和之前的各类回收期不同,他同时兼顾年轻代和老年代,对比与其他回收器,或者工作在年轻代或者工作在老年代;

三)空间整合:

CMS:采用标记清除算法,内存碎片,若干次GC以后再做一次碎片整理,G1将内存划分成一个个的regin,内存的回收是以regin作为基本单位的,regin之间采用的是复制算法,但是整体上来看使用的是标记压缩算法,这两种算法都是可以避免内存碎片,这种特性有利于程序长时间运行,分配大对象的时候不会因为无法找到连续内存空间而提前出发下一次GC,尤其当JAVA堆空间比较大的时候,G1的优势更加明显;

四)可预测的时间模型:

1)这是G1相对于CMS的一大优势,G1除了追求低停顿以外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度是M毫秒的时间片段内,消耗在垃圾收集上面的时间不得超过N毫秒;

2)由于分区的原因,G1可以只是进行选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到很好的控制,G1会跟踪各个Regin里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个哟溴铵列表,每一次根据允许的收集时间,优先回收价值最大的Regin,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率;

3)相比于CMS GC,G1未必能够做到CMS在最好情况下的延时停顿但是最差情况下要好很多

当堆的空间比较大的时候,在指定的暂停时间之内就去找那些回收价值比较高的regin去回收,其他堆地方还可以分配内存,空间越大越好,况且还可以建减少GC时间;

下面可以指定停顿时间:M-N/M是吞吐量,以前的垃圾回收器要么新生代全部进行回收,要么老年代全部进行回收,通过缩小回收的范围,用不着全局停顿了,根据用户限定的停顿时间从价值大的从高往低进行回收

五)避免内存碎片+分代收集+避免全局回收

缺点:

1)占用更多的内存(卡表)和CPU资源,因为本身需要卡表和记忆集来记录新生代和老年代之间的引用关系,需要更多的CPU资源是因为G1里面肯定有更多复杂的算法;

2)不适用于低配置的机器;

FullGC:如果发现清理过程中没有足够的空Regin来存放转移的对象,就会出现FullGC,单线程执行标记整理算法,此时会导致用户线程的暂停,所以要尽量保证堆内存有一定的空间

 G1:适合于大堆,有10%和%20额外空间占用

 

七)记忆集和卡表:

 现在给每一个regin都配上一个remberSet,使用一个记忆集来进行记录有哪些引用指向了这个区域集中的这个对象,在后续想要回收这个regin2,就用不着去回收全堆的遍历,直接看记忆集就可以了,用记忆集来记录当前有哪些regin区域指向当前regin区域的对象,那么下一次想要回收regin2区域的对象,直接看记忆集就可以了

 记忆集是记录是否存在外部引用的,卡表是老年代记录是否自己引用了新生代的

局部回收,GCroots加入remberSet,给定预测时间还要进行评估分析

1)一个对象被不同区域引用的问题

2)一个Regin不可能是独立的,一个Regin中的对象可能被其他任意的Regin中的对象引用,在进行判断对象存活的时候,是否也是需要扫描整个JAVA堆才能保证准确?

3)在其他的分代垃圾收集器中,也是存在着这样的问题,但是G1更突出,回收新生代的同时不得不扫描老年代,如果这样子做的话会降低minor GC的效率;

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

5)每一个regin都是对应着一个Remembered Set

6)每一次Reference类型进行写操作的时候,都会产生一个Write Barrier暂时中断操作

7)然后进行检查将要写入的引用所指向的对象是否和该Reference类型数据在不同的Regin

如果不同,通过卡表将相关的引用信息记录到引用所指向对象所在的Regin对应的RemberedSet里面;

8)再进行全局垃圾扫描的时候,在进行枚举根节点的时候将枚举范围加入到Rembered Set,就可以保证不进行全局扫描,也不会有纰漏;

八)总结:
一)年轻代GC:

JVM在进行启动的时候,G1先准备好Eden园区,程序在运行过程中会不断创建对象到Eden园区,当Eden园区空间耗尽的时候,G1会启动年轻代垃圾收集过程,年轻代垃圾收集会回收Eden园区和幸存者区域,YGC的时候,首先G1会停止应用线程的执行,G1创建会收集,回收集是需要被回收的分段集合,年轻代垃圾收集包含伊甸园区和幸存区的所有分段集合

1)扫描根区域:根指的是GCROOTS+记忆集中记录的外部引用作为扫描存活对象的入口

2)更新RSET:处理脏卡表中的页,更新Rset,此阶段完成以后,Rset可以真实的反馈老年代所在的内存分段中对于新生代的对象的引用

年轻代的GC是一定要使用到记忆集的,新生代的回收是很频繁的,有可能新生代的对象还被老年代对象中的引用在指向着,所以有的时候新生代的对象就不能够随便的被回收,就为了扫描新生代的时候做minorGC的时候不需要扫描老年代

对于老年代来说会不会出现新生代的对象中的引用还指向老年代的对象的引用,此时就不需要特别担心了,因为老年代混合回收的时候也是需要进行回收新生代的,正常的来说也要扫描新生代,主要担心的是回收新生代的时候是否被老年代中的对象的引用所指向

画圆圈的时到达直接进入到老年代

对于应用程序的赋值语句来说,object.field=field,JVM会在之前和之后执行特殊的操作以在藏卡表队列中保存了引用关系,在进行年轻代回收的过程中,G1会对整个脏卡表中的card进行处理,来更新Rset,确保Rset能够反映实时的引用关系,那么为什么不在引用赋值操作直接来更新Rset呢,这是为了性能的需要,REST的处理如果进行线程同步,开销会很大,使用队列进行异步处理会好很多,经常要操作Rset对应的regin,直接更新Rset,同步开销比较大

3)处理RSET:识别被老年代对象指向的Eden园区的对象,这些被指向的Eden园区的对象被认为是存活的对象;

4)复制对象:

5)处理引用:处理软引用等等,最终Eden园区的区域被置为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程中可以达到内存整理的效果,减少碎片

二)年轻代GC+并发标记:

1)初始标记阶段:标记从根节点直接可达的对象,这个阶段是STW的

2)根区域扫描:G1会扫描新村去直接可大的老年代的区域的对象,并标记被引用的对象,这一个过程必须在YGC之前完成;

3)并发标记:在整个堆中进行并发标记:和应用程序并发执行,那么这个过程中可能被YGC中断,在并发标记阶段,如果发现区域对象的所有对象都是垃圾,那么这块区域会被立即回收,同时并发标记过程中,会进行计算每一个区域的对象活性(区域中存活对象的比例)

4)最终标记:处理漏标的对象,原始快照

5)独占式清理:计算各个区域的存活对象和GC回收比例,并进行排序,识别可以进行混合回收的区域,为下一个阶段做铺垫,是STW的,不会做垃圾的收集,先回收哪些;

三)混合回收: ​​​​​​

四)FullGC:

设置暂停时间比较短也会造成FGC,

假设说设置暂停时间,如果比较短,导致每一次进行垃圾回收的时候regin比较少,释放空间比较少,设置暂停时间比较短,频繁进行GC,但是用户线程不断进行垃圾

G1是在可控的暂停时间最大限度的提高吞吐量

年轻代GC是独占式清理的

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值