Java GC的疑问解答(面试点归纳)

23 篇文章 0 订阅

可能面试中会问到有关GC的提问,那么这里就将我的收集和总结归纳成这篇博客!

1.JVM的内存回收过程是怎样的?

对象在Eden Space创建,当Eden Space满了的时候,gc就把所有在Eden Space中的对象扫描一次,把所有有效的对象复制到第一个Survivor Space,同时把无效的对象所占用的空间释放。当Eden Space再次变满了的时候,就启动移动程序把Eden Space中有效的对象复制到第二个Survivor Space,同时,也将第一个Survivor Space中的有效对象复制到第二个Survivor Space。如果填充到第二个Survivor Space中的有效对象被第一个Survivor Space或Eden Space中的对象引用,那么这些对象就是长期存在的,此时这些对象将被复制到Permanent Generation。若垃圾收集器依据这种小幅度的调整收集不能腾出足够的空间,就会运行Full GC,此时JVM GC停止所有在堆中运行的线程并执行清除动作。

2.为什么需要把堆分代?不分代不能完成他所做的事情么?

其实不分代完全可以,分代的唯一理由就是优化GC性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

3.分代区域比例划分具体是什么样的?

默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
  默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
  JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
  因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

4.对象在年轻代中如何存活和回收的?具体是怎样?

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中

5.Survivor的From和To区为什么需要进行“角色切换”使用?

主要是为了减少内存碎片的产生。
  Young Gen垃圾回收时,采用将存活对象复制到到空的Suvivor Space的方式来确保尽量不存在内存碎片,采用空间换时间的方式来加速内存中不再被持有的对象尽快能够得到回收

6.HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例?

一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

7.年老代是如何工作的?

年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。

8.形象描述HotSpot虚拟机GC算法采用分代收集算法:

1、一个人(对象)出来(new 出来)后会在Eden Space(伊甸园)无忧无虑的生活,直到GC到来打破了他们平静的生活。GC会逐一问清楚每个对象的情况,有没有钱(此对象的引用)啊,因为GC想赚钱呀,有钱的才可以敲诈嘛。然后富人就会进入Survivor Space(幸存者区),穷人的就直接kill掉。
2、并不是进入Survivor Space(幸存者区)后就保证人身是安全的,但至少可以活段时间。GC会定期(可以自定义)会对这些人进行敲诈,亿万富翁每次都给钱,GC很满意,就让其进入了Genured Gen(养老区)。万元户经不住几次敲诈就没钱了,GC看没有啥价值啦,就直接kill掉了。
3、进入到养老区的人基本就可以保证人身安全啦,但是亿万富豪有的也会挥霍成穷光蛋,只要钱没了,GC还是kill掉。

9.分区(代)的目的?

新生区由于对象产生的比较多并且大都是朝生夕灭的,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而养老区生命力很强,没有额外空间对其进行分配担保,必须使用**“标记-清理”或者“标记-整理”算法**来进行回收。

10.垃圾回收是如何回收的?

三种经典垃圾回收算法(标记清除算法、复制算法、标记整理算法)及分代收集算法 和 七种垃圾收集器。
一、垃圾回收算法
(1)标记-清除(Mark-Sweep)算法

(2)标记-复制(Mark-Copy)算法

(3)标记-整理(Mark-Compact)算法

(4)分代收集(Generational Collection)
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在下面的分代假说之上:
1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭。
2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
3)跨代引用假说(Intergeneration Generational Hypothesis):跨代引用相对于同代引用来说仅占极少数。

二、垃圾收集器
  如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
  用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。
(1)Serial收集器(复制算法)
  新生代单线程收集器,标记和清理都是单线程,优点是简单高效;

(2)Serial Old收集器 (标记-整理算法)
  老年代单线程收集器,Serial收集器的老年代版本;

(3)ParNew收集器 (复制算法)
  新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;

(4)Parallel Scavenge收集器 (复制算法)
   新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;

(5)Parallel Old收集器 (标记-整理算法)
  老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;

(6)CMS(Concurrent Mark Sweep)收集器(标记-清除算法)
  老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。

(7)G1(Garbage First)收集器 (标记-整理算法)
  Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

11.如何确定一个对象是否可以被回收?

(1)引用计数算法:判断对象的引用数量
1)基本思想
  在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
2)为什么Java领域主流的Java虚拟机里面都没有用引用技术算法来管理内存?
  客观地说,引用技术算法虽然占用了一些额外地内存空间来进行计数,但它的原理简单,判断效率也很高,在大多数情况下它都是一个不错的算法。但是这个看似简单的算法也有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯地引用计数就很难解决对象之间循环引用的问题

(2)可达性分析算法:判断对象的引用链是否可达
1)基本思想
  通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reerence Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,证明此对象是不可能再被使用的
  
2)GC Roots根节点有哪些
a.虚拟机栈(栈桢中的本地变量表)中的引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
b.方法区中的类静态属性引用的对象,譬如Java类的应用类型静态变量。
c.方法区中的常量引用的对象,譬如字符串常量池(String Table)里的引用。
d.本地方法栈中JNI(即通常所说的Native方法)引用的对象
e.Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器(也就是应用类加载器,Application Class Loader)
f.所有被同步锁(synchronized关键字)持有的对象
g.反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

12.垃圾回收类型?

垃圾回收有两种类型,Minor GC和Full GC。
(1)Minor GC:
  对新生代进行回收,不会影响到年老代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。
  导致Minor GC的原因:Eden被写满
  
(2)Full GC:
  也叫 Major GC,对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数。
  导致Full GC的原因包括:老年代被写满永久代(Perm)被写满System.gc()被显式调用等。

13.什么时候触发Minor GC和Full GC?

(1)Minor GC
Eden区满了就会触发Minor GC,细分如下:
虚拟机在进行minorGC之前会判断老年代最大的可用连续空间是否大于新生代的所有对象总空间
1)如果大于的话,直接执行minorGC
2)如果小于,判断是否开启HandlerPromotionFailure,没有开启直接FullGC
3)如果开启了HanlerPromotionFailure, JVM会判断老年代的最大连续内存空间是否大于历次晋升的大小,如果小于直接执行FullGC
4)如果大于的话,执行minorGC

(2)Full GC
1)老年代空间不足
  如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发Full GC。为了避免这种情况,最好就是不要创建太大的对象。
2)持久代空间不足
  如果有持久代空间的话,系统当中需要加载的类,调用的方法很多,同时持久代当中没有足够的空间,就出触发一次Full GC
3)YGC(Minor GC)出现promotion failure
  promotion failure发生在Young GC, 如果Survivor区当中存活对象的年龄达到了设定值,会就将Survivor区当中的对象拷贝到老年代,如果老年代的空间不足,就会发生promotion failure, 接下去就会发生Full GC.
4)统计YGC发生时晋升到老年代的平均总大小大于老年代的空闲空间
  在发生YGC是会判断,是否安全,这里的安全指的是,当前老年代空间可以容纳YGC晋升的对象的平均大小,如果不安全,就不会执行YGC,转而执行Full GC。
5)显示调用System.gc

14.对象进入老年代的策略有哪些?

(1)迭代年龄判断
  在对象的对象头信息中存储着对象的迭代年龄,迭代年龄会在每次Minor GC之后对象的移区操作(也就是前面说的对象采用复制算法从Survivor的From区移到To区的操作)中自增1,当这个年龄值达到15(默认,CMS中是6)之后,这个对象将会被一如老年代。
年龄阈值的设置:- XX:MaxTenuringThreshold
(2)大对象直接进入老年代
  虚拟机提供了一个阈值参数,令大于这个设置值的对象直接进入老年代。最典型的大对象就是那种很长的字符串以及数组,大对象对虚拟机的内存分配就是坏消息,尤其是一些朝生夕灭的短命大对象,写程序时应避免。
大对象阈值的设置:- XX:PretenureSizeThreshold
(3)对象动态年龄判断
  如果在 Survivor 空间中所有相同年龄的对象,大小总和大于 Survivor 空间的一半,那么年龄大于或等于该年龄的对象就直接进入老年代,无须等到阈值中要求的年龄。
  例如:假设我这里按照年龄划分了10批对象,对象年龄依次为1-10,现在年龄1到3这批对象的总大小大于Survivor空间一半,则对象为4-10的所有对象会被放入老年代。
(4)空间分配担保机制
  如果老年代中最大可用的连续空间大于新生代所有对象的总空间,那么 Minor GC 是安全的。如果老年代中最大可用的连续空间大于历代晋升到老年代的对象的平均大小,就进行一次有风险的 Minor GC,如果小于平均值,就进行 Full GC 来让老年代腾出更多的空间。
  因为新生代使用的是复制算法,为了内存利用率,只使用其中一个 Survivor 空间来做轮换备份,因此如果大量对象在 Minor GC 后仍然存活,导致 Survivor 空间不够用,就会通过分配担保机制,将多出来的对象提前转到老年代,但老年代要进行担保的前提是自己本身还有容纳这些对象的剩余空间,由于无法提前知道会有多少对象存活下来,所以取之前每次晋升到老年代的对象的平均大小作为经验值,与老年代的剩余空间做比较。

15.OOM结合GC的理解?

OOM并不是发现直接抛出的,而是在经过Minor GC和Full GC后,老年代以及永久代都还是写满的状态,导致无空间可用才抛出的OOM的。

16.Java调优的目的是什么?

其实主要目的就是为了减少GC,特别是性能消耗大的Full GC。
那么如何减少呢?
  这时候就要深入理解什么情况下会调用Full GC了,既然知道老年代写满了就会调用Full GC,那也就需要深入理解什么情况下对象会进入老年代,针对避免这些策略,配置相关参数在一些情况下是可以完全避免调用Full GC的,从而达到Java调优的目的。

17.GC结构图

在这里插入图片描述

18.什么是STW(Stop-The-World)机制?为什么要设计这个机制?

(1)什么是STW机制?
  STW是Java中Stop-The-World机制的简称,现阶段所有垃圾收集器以及垃圾收集算法都没有避免该机制。
  STW是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。是Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互。
  我们日常所说的GC优化其实就是优化缩减STW的时间。
  
(2)为什么要设计这个机制?
  逆向思维,如果没有STW机制,即我们的GC线程与Java线程是并行的,那么当我们执行GC时,Java线程任务以及执行完成,其对应的引用链中的变量已经被删除,那么GC就会再次运用“可达性分析算法”计算得到“引用链”,这种全局操作会很耗费性能,所以才会想将GC线程Java线程设置成单线程的形式,GC运行完成再继续运行Java线程任务。

19.System.gc方法详解?

1.JDK里的System.gc的实现

/**
 * Runs the garbage collector.
 * <p>
 * Calling the <code>gc</code> method suggests that the Java Virtual
 * Machine expend effort toward recycling unused objects in order to
 * make the memory they currently occupy available for quick reuse.
 * When control returns from the method call, the Java Virtual
 * Machine has made a best effort to reclaim space from all discarded
 * objects.
 * <p>
 * The call <code>System.gc()</code> is effectively equivalent to the
 * call:
 * <blockquote><pre>
 * Runtime.getRuntime().gc()
 * </pre></blockquote>
 *
 * @see     java.lang.Runtime#gc()
 */
public static void gc() {
    Runtime.getRuntime().gc();
}

其实发现System.gc方法其实是调用的Runtime.getRuntime.gc(),接着往下看。

/*
  运行垃圾收集器。
调用此方法表明,java虚拟机扩展
努力回收未使用的对象,以便内存可以快速复用,
当控制从方法调用返回的时候,虚拟机尽力回收被丢弃的对象
*/
public native void gc();

这里看到gc方法是native的,在java层面只能到此结束了,代码只有这么多,要了解更多,可以看方法上面的注释,不过我们需要更深层次地来了解其实现,那还是准备好进入到jvm里去看看。

2.System.gc的作用有哪些?
(1)做一次full gc
(2)执行后会暂停整个进程。
(3)System.gc我们可以禁掉,使用-XX:+DisableExplicitGC,
其实一般在cms gc下我们通过-XX:+ExplicitGCInvokesConcurrent也可以做稍微高效一点的gc,也就是并行gc。
(4)最常见的场景是RMI/NIO下的堆外内存分配
注:
  如果我们使用了堆外内存,并且用了DisableExplicitGC设置为true,那么就是禁止使用System.gc,这样堆外内存将无从触发极有可能造成内存溢出错误,在这种情况下可以考虑使用ExplicitGCInvokesConcurrent参数。

20.GC主要针对哪些内存区域?为什么?

GC(Garbage Collection)主要针对堆和方法区,着两个区域有着显著的不确定性,只有处于运行期间,才能知道程序究竟会创建哪些 对象,创建多少个对象,这部分内存的分配和回收时动态的。垃圾收集器所关注的正是这部分内存如何管理。
  程序计数器、虚拟机栈和本地方法栈3个区域是随线程而生,随线程而灭(每一个线程任务都会划分出一小块的对应区域以供当前线程使用),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由即时编译器进行一些优化,但在基于概念模型的讨论里,大体上可以认为时编译器可知的),因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑如何回收的问题,当方法结束或线程结束时,内存自然就跟随着回收了。

21.垃圾收集分类?

1.部分收集(Partial GC):
指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
(1)新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
(2)老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在优点混淆,再不同的资料上常有不同的所指,读者需按上下文区分到底是指老年代的收集还是整堆收集。
(3)混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
2.整堆收集(Full GC):
收集整个Java堆和方法区的垃圾收集。

22.方法区的垃圾回收?

《Java虚拟机规范》中提到过可以不要求虚拟机在方法区中实现垃圾收集,事实上也确实有为实现或未能完整实现方法区类型卸载的收集器存在(如JDK11时期的ZGC收集器就不支持类卸载),方法区垃圾收集的“性价比”通常也是比较低的:在Java堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间,相比之下,方法区回收由于苛刻的判定条件,其区域垃圾收集的回收成果远低于此。
  方法区的垃圾收集主要回收两部分内容:废弃的常量不再使用的类型
(1)如何判定一个常量是否“废弃”?
回收废弃常量与回收Java堆中的对象非常类似。简单理解就是没有被任何对象引用的常量就是“废弃的常量”。
(2)如何判定一个类型是否属于“不在被使用的类”?
比较苛刻,需同时满足三个条件:
1)该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
2)加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
  Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而不是和对象一样,没有引用了就必然回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:=TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持。

23.为什么会说即使在可达性分析算法中判定为不可达的对象,也不是“非死不可的”?

要真正宣告一个对象死亡,至少要经历两次标记过程:
第一次标记:
  如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记
第二次标记:
  第一次标记后,进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。如果有必要执行,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的Finalize()方法。这里所说的“执行”是指虚拟机会触发这个方法开始允许,但并不承诺一定会等待它运行结束。这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端的发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记。如果对象要在finalize()中成功拯救自己—只要重新与引用链上的任何一个对象建立关联即可,则在第二次标记时它将会被移出“即将回收”的集合,反之就是基本上它就真的要被回收了。

24.GC有关的JVM参数

(1)–XX:NewRatio
年轻代和老年代的内存空间大小的比例,默认1:2。
(2)–XX:SurvivorRatio
Eden:From:To的内存空间大小的比例,默认8:1:1。
(3)-XX:NewSize和-XX:MaxNewSize
用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。
(4)-XX:SurvivorRatio
用于设置Eden和其中一个Survivor的比值,这个值也比较重要。
(5)-XX:+PrintTenuringDistribution
这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。
(6)-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。
(7)–XX:MaxTenuringThreshold
From区中的年龄阈值。指定使用CMS,默认值是6;不指定垃圾收集器,默认值是15。
虚拟机参数的修改可查看我的博客《小码农的十万个为什么!!!(持续更新)》中的第56条内容,也可查看《Java虚拟机参数配置》学习JVM参数配置。

参考自:
https://blog.csdn.net/asleepysheep/article/details/82180284
https://blog.csdn.net/luzhensmart/article/details/82563734
https://blog.csdn.net/justloveyou_/article/details/71216049

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值