解密JVM(三)垃圾回收

前言:

正文

1.如何判断对象可以回收

  • 引用计数
  • 可达性分析算法

1.1 引用计数法

  • 引用计数算法( reference- counting):每个对象有一个引用计数器,当对象被引用一次则计数器加1,当对象引用失效一次则计数器减1,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。
  • 弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法作为垃圾进行回收,造成内存泄露

image-20201225165404514

1.2 可达性分析算法

  • 可达性算法(GC Roots Tracing):从GC Roots作为起点开始搜索,那么整个连通图中的对象便都是活对象,对于GC Roots无法到达的对象便成了垃圾回收的对象,随时可被GC回收。

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

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

  • 哪些对象可以作为GC Root

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(即一般说的Native方法)引用的对象

1.3 四种引用

img

1.强引用

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

2.软引用(SoftReference)

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

软引用的使用

public class Demo1 {
	public static void main(String[] args) {
		final int _4M = 4*1024*1024;
		//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
		List<SoftReference<byte[]>> list = new ArrayList<>();
		SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
	}
}

如果在垃圾回收时发现内存不足,在回收软引用所指向的对象时,软引用本身不会被清理

如果想要清理软引用,需要使用引用队列

public class Demo1 {
	public static void main(String[] args) {
		final int _4M = 4*1024*1024;
		//使用引用队列,用于移除引用为空的软引用对象
		ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
		//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
		List<SoftReference<byte[]>> list = new ArrayList<>();
		SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);

		//遍历引用队列,如果有元素,则移除
		Reference<? extends byte[]> poll = queue.poll();
		while(poll != null) {
			//引用队列不为空,则从集合中移除该元素
			list.remove(poll);
			//移动到引用队列中的下一个元素
			poll = queue.poll();
		}
	}
}

**大概思路为:**查看引用队列中有无软引用,如果有,则将该软引用从存放它的集合中移除(这里为一个list集合)

3.弱引用(WeakReference)

  • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象

  • 可以配合引用队列来释放弱引用自身

弱引用的使用和软引用类似,只是将 SoftReference 换为了 WeakReference

4.虚引用(PhantomReference)

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

5.终结器引用(FinalReference)

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

引用队列(RefernceQueue)

  • 软引用和弱引用可以配合引用队列
    • 弱引用虚引用所引用的对象被回收以后,会将这些引用放入引用队列中,方便一起回收这些软/弱引用对象
  • 虚引用和终结器引用必须配合引用队列
    • 虚引用和终结器引用在使用时会关联一个引用队列

2. 垃圾回收算法

2.1 标记清除

定义: Mark Sweep

  • 速度较快
  • 会造成内存碎片

图解:

image-20201226142747378

image-20201226142612433

2.2 标记整理

定义:Mark Compact

  • 没有内存碎片
  • 速度慢
    • 原因:内存区块拷贝移动,所有内存引用地址加以改变

图解:

image-20201226143119865

2.3 复制

定义:Copy

  • 不会有内存碎片

  • 需要占用双倍内存空间

图解:

image-20201226144454660

image-20201226143427700

img

img

3.分代垃圾回收

image-20201226143725602

3.1 回收流程

新创建的对象都被放在了新生代的伊甸园

img

当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做 Minor GC

Minor GC 会将伊甸园和幸存区FROM存活的对象复制到 幸存区 TO中, 并让其寿命加1,再交换两个幸存区

img

img

img

再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1

img

如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代

img

如果新生代老年代中的内存都满了,就会先触发Minor Gc,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收

总结:

  • 对象首先分配在伊甸园区域

  • 新生代空间不足时,触发 minor gc,伊甸园和from 存活的对象使用copy 复制到 to中,存活的对象年龄加 1并且交换from to

  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行

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

  • 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gcstop the world的时间更长

3.2 相关VM参数

含义参数
堆初始大小-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

3.3 GC 分析

大对象处理策略

  • 当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代

线程内存溢出

  • 某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行

  • 这是因为当一个线程抛出OOM异常后它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常

4.垃圾回收器

相关概念

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

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

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

1.串行

  • 单线程

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

2.吞吐量优先

  • 多线程

  • 堆内存较大,多核 cpu

  • 让单位时间内,STWstop the world)的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高

  • JDK1.8默认使用的垃圾回收器

3.响应时间优先

  • 多线程

  • 堆内存较大,多核 cpu

  • 尽可能让单次STW的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5

4.1 串行

img

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

-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收集器的老年代版本

  • 特点:同样是单线程收集器,采用标记-整理算法

4.2 吞吐量优先

img

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC

-XX:+UseAdaptiveSizePolicy

-XX:GCTimeRatio=ratio

-XX:MaxGCPauseMillis=ms

-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控制最大的垃圾收集停顿时间
    • XX:GCRatio 直接设置吞吐量的大小
Parallel Old 收集器

是Parallel Scavenge收集器的老年代版本

  • 特点:多线程,采用标记-整理算法(老年代没有幸存区)

4.3 响应时间优先

image-20201226154731927

-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步:

    • 初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题
    • 并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行
    • 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题
    • 并发清除:对标记的对象进行清除回收
  • CMS收集器的内存回收过程是与用户线程一起并发执行的。

4.4 G1

定义:Garbage First

  • 2004 论文发布

  • 2009 JDK 6u14 体验

  • 2012 JDK 7u4 官方支持

  • 2017 JDK 9 默认

img

适用场景

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms

  • 超大堆内存,会将堆划分为多个大小相等的 Region

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

相关 JVM 参数

  • JDK8 并不是默认开启的,所需要参数开启

-XX:+UseG1GC

-XX:G1HeapRegionSize=size

`-XX:MaxGCPauseMillis=time

1)G1 垃圾回收阶段

image-20201226160504731

新生代伊甸园垃圾回收—–>内存不足,新生代回收+并发标记—–>回收新生代伊甸园、幸存区、老年代内存——>新生代伊甸园垃圾回收(重新开始)

2) Young Collection
  • 会STW(stop the world

白色:空闲区域,E:伊甸园 ,S:幸存区, O:老年代

image-20201226160724825

image-20201226160904320

image-20201226161103032

3) Young Collection + CM
  • 在 Young GC 时会进行 GC Root 的初始标记

  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定

    -XX:InitiatingHeapOccupancyPercent(默认45%)

白色:空闲区域,E:伊甸园 ,S:幸存区, O:老年代

image-20201226161132587

4) Mixed Collection

会对 E、S、O 进行全面垃圾回收

  • 最终标记(Remark)会 STW

  • 拷贝存活(Evacuation)会 STW

-XX:MaxGCPauseMillis=ms 用于指定最长的停顿时间

白色:空闲区域,E:伊甸园 ,S:幸存区, O:老年代

image-20201226161507300

:为什么有的老年代被拷贝了,有的没拷贝?

因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)

5) Full GC
  • SerialGC (串行)

    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC(并行)

    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS

    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足
      • 如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理
      • 如果垃圾产生速度快于垃圾回收速度,便会触发Full GC
  • G1

    • 新生代内存不足发生的垃圾收集 - minor gc

    • 老年代内存不足(老年代所占内存超过阈值)

      • 如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理
      • 如果垃圾产生速度快于垃圾回收速度,便会触发Full GC
6) Young Collection跨代引用
  • 新生代回收的跨代引用(老年代引用新生代)问题

白色:空闲区域,E:伊甸园 ,S:幸存区, O:老年代

image-20201226162507896

  • 卡表与Remembered Set
    • Remembered Set存在于E中,用于保存新生代对象对应的脏卡
      • 脏卡:O被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡
  • 在引用变更时通过post-write barried + dirty card queue
  • concurrent refinement threads 更新 Remembered Set

img

7) Remark

重标记阶段

  • pre-write barrier + satb_mark_queue

  • 在垃圾回收时,收集器处理对象的过程中

黑色:已被处理,需要保留的, 灰色:正在处理中的 , 白色:还未处理的

image-20201226163018908

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

过程如下

  • 之前C未被引用,这时A引用了C,就会给C加一个写屏障(下图中红线),写屏障的指令会被执行,将C放入一个队列当中,并将C变为 处理中 状态
  • 并发标记阶段结束以后,重新标记阶段会STW,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它(C变为黑色)

img

8) JDK 8u20 字符串去重
  • 优点:节省大量内存

  • 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加

-XX:+UseStringDeduplication

String s1 = new String("hello"); // char[]{'h','e','l','l','o'} 
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
  • 将所有新分配的字符串放入一个队列

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

  • 如果它们值一样,让它们引用同一个 char[]

  • 注意,与String.intern()不一样

    • String.intern()关注的是字符串对象
    • 而字符串去重关注的是 char[]
    • 在 JVM 内部,使用了不同的字符串表
9) JDK 8u40 并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类

-XX:+ClassUnloadingWithConcurrentMark默认启用

10)JDK 8u60 回收巨型对象
  • 一个对象大于 region 的一半时,称之为巨型对象

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

  • 回收时被优先考虑

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

白色:空闲区域,E:伊甸园 ,S:幸存区, O:老年代(有卡表), H:巨型对象

img

11) JDK 9并发标记起始时间的调整
  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC

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

  • JDK 9 可以动态调整

    • -XX:InitiatingHeapOccupancyPercent用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间
12) JDK 9更高效的回收
  • 250+增强

  • 180+bug修复

  • https://docs.oracle.com/en/java/javase/12/gctuning

image-20201226165327092

5. 垃圾回收调优

  • 工具参考:点击我

  • 查看虚拟机参数命令

"C:\Program Files\Java\jdk1.8.0_271\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"

可以根据参数去查询具体的信息

image-20201226170212645

预备知识

  • 掌握 GC 相关的 VM 参数,会基本的空间调整

    • 工具参考:点击我

      • image-20201226165940429
    • 查看虚拟机参数命令

      • "C:\Program Files\Java\jdk1.8.0_271\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"
        
      • image-20201226170212645

  • 掌握相关工具

  • 明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则

5.1 调优领域

  • 内存

  • 锁竞争

  • cpu 占用

  • io

5.2 确定目标

  • 【低延迟】还是【高吞吐量】,选择合适的回收器

  • CMS,G1,ZGC

  • ParallelGC

  • Zing

5.3 最快的GC是不发生 GC

  • 查看 FullGC 前后的内存占用,考虑下面几个问题

    • 数据是不是太多?
      • resultSet = statement.executeQuery("select * from 大表 limit n")
    • 数据表示是否太臃肿?
      • 对象图
      • 对象大小 16byte, Integer 24byte, int 4byte
  • 是否存在内存泄漏?

    • static Map map =

      • 第三方缓存实现,eg:redis…

5.4 新生代调优

  • 新生代的特点

    • 所有的new操作分配内存都是非常廉价的
      • TLAB thread-local allocation buffffer
    • 死亡对象回收零代价
    • 大部分对象用过即死
    • Minor GC 所用时间远低于Full GC
  • 新生代内存越大越好吗?

    -Xmn Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery).GC is performed in this region more often than in other regions. If the size for the younggeneration is too small, then a lot of minor garbage collections are performed. If the size is too large, then only full garbage collections are performed, which can take a long time to complete.Oracle recommends that you keep the size for the young generation greater than 25% and less than 50% of the overall heap size.
    谷歌翻译:
    -Xmn设置年轻代(nursery)的堆的初始大小和最大大小(以字节为单位)。在此区域中执行GC的频率高于其他区域。如果年轻一代的大小太小,则会执行许多次要的垃圾回收。如果大小太大,则仅执行完整的垃圾回收,这可能需要很长时间才能完成。Oracle建议您使年轻代的大小保持大于堆总大小的25%,并且小于总堆大小的50%。
    
    • 新生代能容纳所有【并发量 * (请求-响应)】的数据

    • 幸存区大到能保留【当前活跃对象+需要晋升对象】

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

      -XX:MaxTenuringThreshold=threshold

      -XX:+PrintTenuringDistribution

      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 
      ...
      
  • 新生代内存越大越好吗?

    • 不是
      • 新生代内存太小:频繁触发Minor GC,会STW,会使得吞吐量下降
      • 新生代内存太大:老年代内存占比有所降低,会更频繁地触发Full GC。而且触发Minor GC时,清理新生代所花费的时间会更长
    • 新生代内存设置为能容纳 [并发量*(请求-响应)] 的数据为宜

5.5 老年代调优

以 CMS 为例

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

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

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

    • -XX:CMSInitiatingOccupancyFraction=percent

5.6 案例

  • 案例1 Full GC 和 Minor GC频繁

    • **分析:**GC特别频发,代表空间紧张。
      • 新生代空间紧张,当业务高峰期到来,大量对象被创建,新生代空间会被占满,造成幸存区空间紧张,晋升阈值随之降低,导致很多生存周期很短的对象也会被晋升到老年代,造成老年代存在大量生存周期很短的对象,老年代空间紧张。
      • 老年代空间紧张,造成老年代的Full GC频繁出现
    • **解决方法:**试着先增大新生代内存,新生代内存充裕了,新生代垃圾回收就会变得不那么频繁。同时增大幸存区空间以及晋升的阈值,这样可以让生命周期较短的对象尽可能留在新生代,而不要晋升到老年代。从而让老年代的Full GC不再频繁出现。
  • 案例2 请求高峰期发生 Full GC,单次暂停时间特别长 (CMS)

    • **分析:**业务需求要求低延迟,垃圾回收器上选择CMS。单次暂停时间特别长,要点在分析哪一部分耗费时间较长。

      • 看下图,初始标记和并发标记运行比较快,比较慢的在于重新标记
      • image-20201226183803628
      • CMS,查看GC日志,看每阶段耗费的时间 —> 重新标记阶段耗时接近1s~2s,问题出现在重新标记阶段
      • CMS重新标记时,会扫描全部堆内存(包含老年代及新生代),如果业务高峰,老年代及新生代对象剧增,遍历算法耗时太多。
    • **解决方法:**在重新标记之前先把新生代对象先做一次垃圾回收,减少新生代对象的数量,从而减少在重新标记阶段的时间,降低单次暂停时间特别长问题。

      • 参数:-XX:+CMSScavengeBeforeRemark
  • 案例3 老年代充裕情况下,发生 Full GC (CMS jdk1.7)

    • **分析:**jdk1.7以前是使用永久代的,永久代空间不足,也会导致Full GC发生,jdk1.8后使用元空间,是使用操作系统的空间的,所以内存充足。
      所以是由于永久代空间不足导致Full GC
    • **解决方法:**可以增加永久代的初始值和最大值。
      • 参数:永久带的初始值-XX:PermSize及最大值-XX:MaxPermSize
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值