垃圾收集三个经典问题:哪些内存需要回收?什么时候回收?如何回收
jvm gc算法有哪些?目前jdk版本采用什么回收算法?
g1回收期讲下回收过程
gc是什么?为什么要有gc?
gc的两种判定方法?cms收集器与g1收集器特点
gc算法,分代回收是什么
垃圾收集策略与算法
jvm gc原理,jvm怎么回收内存
cms特点,垃圾回收算法有哪些?各自优缺点,共同缺点是什么
java垃圾收集器有哪些,g1应用场景,如何使用垃圾回收器
知道哪几种垃圾收集器,各自优缺点,重点讲下cms和g1,包括原理、流程、优缺点,垃圾回收算法的实现原理
垃圾回收算法
什么情况下触发垃圾回收
如何选择合适的垃圾收集算法
jvm有哪三种垃圾回收算法
常见的垃圾回收器算法有哪些,各有什么优劣
system.gc和runtime.gc会做哪些事情
java gc机制,gc roots有哪些
java对象回收方式,回收算法
cms和g1,解决什么问题,回收过程
cms回收停顿几次,为什么要停顿两次
### 整理后的面试题及解答:
---
#### **1. JVM GC算法有哪些?目前JDK版本采用什么回收算法?**
- **常见GC算法**:
- **标记-清除(Mark-Sweep)**:标记存活对象,清除未标记对象。**缺点**:内存碎片。
- **复制(Copying)**:将存活对象复制到另一块内存。**缺点**:空间利用率低(需保留一半内存)。
- **标记-整理(Mark-Compact)**:标记后整理存活对象到内存一端。**缺点**:整理耗时。
- **分代收集(Generational)**:基于对象生命周期划分内存区域(年轻代、老年代),不同区域使用不同算法(如年轻代用复制算法,老年代用标记-清除或标记-整理)。
- **G1(Garbage-First)**:分区(Region)模型,可预测停顿时间,结合标记-整理和复制算法。
- **JDK默认算法**:
---
#### **2. G1回收器的回收过程**
1. **初始标记(Initial Mark)**:STW(Stop-The-World)标记GC Roots直接关联的对象。
2. **并发标记(Concurrent Mark)**:与用户线程并发,遍历对象图。
3. **最终标记(Remark)**:STW处理并发期间变动的引用(用SATB算法)。
4. **筛选回收(Cleanup)**:选择回收价值高的Region,存活对象复制到空Region(复制算法),清空旧Region。
- **特点**:分Region回收,可预测停顿时间(通过`-XX:MaxGCPauseMillis`设置)。
---
#### **3. GC是什么?为什么要有GC?**
- **GC(垃圾回收)**:自动管理内存,回收无用的对象。
- **必要性**:
- 避免内存泄漏和手动释放内存的错误。
- 提升开发效率,专注于业务逻辑。
---
#### **4. GC的两种判定方法?CMS与G1的特点**
- **判定方法**:
1. **引用计数法**:对象被引用数归零时回收。**缺点**:循环引用问题。
2. **可达性分析**:从GC Roots出发,不可达的对象判定为垃圾。
- **CMS(Concurrent Mark-Sweep)**:
- **特点**:并发收集,低停顿。**缺点**:内存碎片,CPU敏感。
- **算法**:标记-清除。
- **G1**:
- **特点**:分Region回收,可预测停顿,标记-整理减少碎片。**适用场景**:大堆内存、低延迟。
5. CMS与G1的特点
CMS(Concurrent Mark-Sweep) | G1(Garbage-First) |
---|---|
- 目标:低停顿时间(老年代回收)。 - 过程:初始标记→并发标记→重新标记→并发清除。 - 缺点:内存碎片、并发失败风险。 | - 目标:平衡吞吐与低延迟,适合大堆。 - 特点:分Region回收、可预测停顿模型、标记-整理为主。 - 缺点:内存占用稍高。 |
#### **5. 分代回收**
- **分代理论**:对象生命周期不同(年轻代对象朝生夕死,老年代长期存活)。
- **分代策略**:
- **年轻代**:复制算法(Survivor区)。
- **老年代**:标记-清除或标记-整理(如CMS、G1)。
---
#### **6. 垃圾收集策略与算法**
- **策略**:
- **Minor GC**:回收年轻代。
- **Major GC/Full GC**:回收老年代或整个堆。
- **算法选择**:根据分代和收集器特性(如G1统一管理,CMS专注老年代)。
---
#### **7. CMS回收过程及停顿次数**
- **回收阶段**:
1. **初始标记(STW)**:标记GC Roots直接关联对象。
2. **并发标记**:与用户线程并发。
3. **重新标记(STW)**:修正并发期间的引用变化。
4. **并发清除**:清理垃圾对象。
- **停顿次数**:两次STW(初始标记、重新标记),确保标记准确性。
---
#### **8. CMS与G1解决的问题**
- **CMS**:减少老年代回收的停顿时间(低延迟)。
- **G1**:解决大堆内存的停顿问题,提供可预测的停顿模型。
---
#### **9. 垃圾回收器分类及选择**
- **常见收集器**:
- **Serial**:单线程,适合客户端应用。
- **Parallel Scavenge/Old**:吞吐量优先。
- **CMS**:低停顿,适合Web应用。
- **G1**:平衡吞吐量和延迟。
- **ZGC/Shenandoah**:超低停顿(JDK 11+)。
- **选择依据**:
- **吞吐量**:Parallel Scavenge。
- **低延迟**:G1、ZGC。
- **堆大小**:G1适合大堆(>4GB)。
---
#### **10. GC Roots有哪些?**
- 栈帧中的局部变量、静态变量、JNI引用的对象、活跃线程对象、Class元数据等。
---
#### **11. 触发GC的条件**
- **年轻代**:Eden区满时触发Minor GC。
- **老年代**:空间不足时触发Full GC。
- **手动触发**:`System.gc()`(不保证立即执行)。
---
#### **12. 垃圾回收算法优缺点**
- **标记-清除**:快但碎片多。
- **复制算法**:无碎片但浪费空间。
- **标记-整理**:无碎片但耗时。
- **共同缺点**:STW停顿(影响实时性)。
---
#### **13. System.gc()与Runtime.gc()**
- 提示JVM执行Full GC,但具体行为由JVM决定(可能被忽略)。
---通常伴随长时间STW,生产环境慎用。
### 总结:
- **核心算法**:标记-清除、复制、标记-整理,分代收集是策略。
- **收集器选择**:CMS(低延迟老年代)、G1(平衡大堆和停顿)。
- **趋势**:G1逐渐成为主流,ZGC/Shenandoah适用于极致低延迟场景。
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。如果不及时对内存中垃圾进行清理,则这些垃圾对象所占的内存空间会一直保留到应用程序结束,被结束的空间无法被其他对象使用。
如果不进行垃圾回收,内存迟早被消耗完。除了释放没用的对象,垃圾回收也可以清楚内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便jvm整理出的内存分配给新的对象。3随着应用程序所应付的业务越来越庞大,复杂,用户越来越多,没有gc就不能保证应用程序的正常运行。经常造成stw的gc又跟不上实际的需求,所以需要不断的对gc进行优化。
早期垃圾回收
早期的c/c++,垃圾回收基本上是手工进行。开发人员可以使用new关键字进行内存申请,并使用delete关键字进行内存释放。2这种方式可以灵活控制内存释放时间,但会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码问题忘记被回收了,就会产生内存泄漏,垃圾对象永远无法被清楚。随着系统运行时间的不断增长,垃圾对象所耗内存持续上升,出现内存溢出并造成应用程序崩溃。
java垃圾回收机制
1自动内存管理,无需手动分配和回收,降低内存泄漏和内存溢出的风险;2自动内存管理机制,只需关注业务开发;3弱化内存溢出时的定位问题和解决问题能力;
java堆是垃圾收集器的工作重点,频繁收集年轻代,较少收集老年代,基本不对元数据区
标记阶段(收集):引用计数法、可达性分析算法
对象阶段:标记-清除、复制、标记-压缩
分代收集算法、增量收集算法、分区算法
垃圾标记阶段:堆中存放着几乎所有java对象实例,gc进行垃圾回收之前,需要区分出内存中哪些是存活对象,哪些是已经死亡对象。只有被标记为已经死亡的,gc才会在执行垃圾回收时,释放掉其所占用的内存空间
如何标记?当一个对象不再被任何存活对象继续引用时,可以宣判已经死亡
对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象A,只要任何一个对象引用了对象A,则A的引用计数器加1,引用失效后,减1,值为0时,表示对象不可能再被引用,可进行回收
优点:实现简单,垃圾对象便于辨识,判定效率高,回收没有延迟性
缺点:1需要单独的字段存储计数器,增加了存储空间的开销;2每次赋值都需要更新计数器,伴随着加法和减法操作,增加了时间开销;3引用计数器有一个严重问题,无法处理循环引用的情况(导致内存泄漏),致命缺陷,导致java垃圾回收期没有使用这类算法
总结:1引用计数算法,是很多语言的资源回收选择,如python同时支持引用计数垃圾收集机制;2具体哪种最优看场景,业界有大规模实践中保留引用计数机制,以提高吞吐量的尝试;3java没选择引用计数,在于难解决循环引用问题;4python解决循环引用办法,手动解除,使用弱引用
循环引用
采用标记 - 清除算法,先从根对象开始标记可达对象,相互引用但从根不可达的对象会被标记为可回收。解决内部相互引用问题
可以有效解决引用计数算法中循环引用的问题,防止内存泄露发生;2也叫做追踪性垃圾收集;2所谓gc root根集合是一组必须活跃的引用
基本思路:1可达性算法是以根对象集合gc roots为起始点,按照从上至下的方式搜索被根对象集合所链接的目标对象是否可达;2使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接,搜索所走过的路径,称为引用链;3如果目标对象没有任何引用链相连,则是不可达的,意味着该对象已经死亡,可以标记为垃圾对象;4可达性分析算法中,只有能够被根对象直接或间接连接的对象才是存活对象
java语言中,gc root包含以下几类元素
虚拟机栈中引用的对象:1各个线程被调用的方法中使用到的参数,局部变量等;2本地方法栈内JNI本地方法引用的对象;3方法区内静态属性引用的对象;4方法区中常量引用的对象,如字符串常量池中的引用;5所有被同步锁synchronized持有的对象;6java虚拟机内部的引用,基本数据类型对应的class对象,一些常驻的异常对象,如nullpointerexception,outofmemory,系统类加载器;7反映java虚拟机内部情况的jmxbean,jvmti中注册的回调、本地代码缓存等
除这些固定的gc roots集合外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象临时性的加入,共同构成gc root集合,如分代收集和局部回收。
如果只针对java堆中某一块区域进行垃圾回收,如典型的新生代,必须考虑到内存区域是虚拟机自己实现细节,不是孤立封闭的,这个区域的对象完全有可能被其他区域对象所引用,需要一并关联的区域对象也加入gc root集合中考虑,才能保证可达性分析的准确性。
技巧:由于root采用栈方式存放变量和指针,如果一个指针,保存了堆内存的对象,但自己又不存在于堆内存中,为一个root
注意:如果使用可达性分析算法来判断内存是否可回收,分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性无法保证;这点事导致gc进行时必须stw的原因,即时号称不会发生停顿的cms收集器,枚举根节点是也必须要停顿的
对象的finalization机制
JAVa语言提供了对象终止机制来允许开发人员提供对象被销毁之前的自定义处理逻辑;2当垃圾收集器发现没有引用指向一个对象,即,即垃圾回收对象之前,总会先调用这个对象的finalize ()方法; 3finalize()方法允许在子类中被重写,,用于在对象被回收时进行资源释放.。通常在这个方法中进行一些资源释放和清理的工作,比,比如关闭文件套,接字和数据库连接。
永远不要主动调用某对象的finalize()方法,应该交给垃圾回收机制调用。1在finalize()时,可能会导致对象复活;2。finalize ()方法执行时间是没有保障的,完全由gc线程决定,极端情况下若不发生gc,则finalize ()方法没有执行机会。3一个糟糕的finalize ()方法会严重影响gc性能。
从功能上来说,finalize ()方法与C++中的c构函数比较类似,但JAVA采用的是基于垃圾回收器的自动内存管理机制,所以本质上不同于C++中的析构函数。由于finalize方法的存在,虚拟机中的对象一般出于三种可能的状态。可触及\可复活\不可触及。
垃圾回收之前会调用 finalize 方法,这个方法是用来检测,要回收对象是否被引用吗?
finalize方法并非主要用于检测要回收对象是否被引用。它是 Object类的一个受保护方法,当垃圾回收器确定对象可被回收时,会在回收该对象内存之前调用此方法。设计初衷是提供一个对象自我拯救的机会,不过它更多是用于执行一些资源清理操作,比如关闭文件、释放网络连接等,因为垃圾回收本身主要处理内存回收,不会自动处理这些外部资源。
但要注意,finalize方法有诸多弊端,比如调用时机不确定、执行效率低等,不建议依赖它进行资源管理,在 Java 9 中已被标记为弃用
生存还是死亡
如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了。一般来说,此对象需要被回收,但事实上也并非死不可的,这时候他们暂时处于缓刑阶段,一个无法触及的对象有可能在某一条件下复活自己,如果这样,那么对他的回收就是不合理的,为此定义虚拟机中的对象可能的三种状态如下:
1可触及的,从根节点开始,可以到达这个对象。2可复活的对象的,所有引用都被释放,但是对象有可能在finalize中复活。3不可触及的,对象的finalize被调用,并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为finalize只会被调用一次。
以上三种状态中,由于finalize方法的存在进行区分,只有在对象不可触达时才会被回收。
具体过程
判定一个对象是否可回收至,至少要经历两次标记过程。
1如果对象到roots没有引用链,则进行第一次标记。
2进行筛选判断。此对象是否有必要进行finalize方法?1如果对象没有重写方法或者方法,已经被虚拟机调用过了,则虚拟机视为没有必要执行,被判定为不可触及的。2如果对象重写了方法且还未执行过,那么会被插入到队列中,由一个虚拟机自动创建的低优先级的线程触发其方法执行。3finalize方法是对象逃脱死亡的最后机会。稍后gc会对f队列中的对象进行第二次标记,如果对象在该方法中与引用链上中的任何一个对象建立了联系,那么在第二次标记时会被移出即将回收集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态。也就是说,一个对象的finalize方法只会被调用一次。
MAT与JProfiuler的GC roots溯源
MAT是memory annalyzer的简称,一款功能强大的java堆内存分析器,便于查找内存泄漏以及查看内存消耗情况,有eclipse开发,一款免费的性能分析工具
jmap是 JDK 自带的一个工具,用于生成 Java 堆转储快照(heap dump)文件。dump 文件是 Java 堆在某个特定时刻的内存镜像,它以二进制格式保存了堆中所有对象的详细信息,包括对象的类型、数量、大小以及对象之间的引用关系等。可用于内存泄漏检测、性能调优、问题排查
当成功区分出内存中存活对象和死亡对象后,Gc接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
目前在gvm中比较常见的三种垃圾收集算法是标记-清除 mark-sweep,复制 copying、标记压缩 mark-compact。
一种非常基础和常见的垃圾收集算法,1960年提出。
执行过程:当堆中的有效空间被耗尽的时候,就会停止整个程序,然后进行两项工作,第一项是标记,第二项是清除。1标记,collector从引用根节点开始遍历标记,所有被引用的对象。一般是在对象的header中记录为可达对象。2清除,collector对堆内存从头到尾进行线性的便利,如果发现某个对象在其header中没有标记为可达对象,则将其回收。
缺点:一,效率不算高。二,在进行gc的时候需要停止整个应用程序,导致用户体验差。三,这种方式清理出来的空闲内存是不连续的,产生内存碎片需要维护一个空闲列表。
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里,下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够就存放。
为解决标记清除算法,在垃圾收集效率方面的缺陷。1963年发表的著名论文,使用双存储区的LISP语言垃圾收集器。
核心思想:将活着的内存空间分为两块,每次只使用其中的一块,在垃圾回收时将正在使用的内存中的存活对象,复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
优点:没有标记和清除过程,实现简单运行高效;复制过去以后,保证空间的连续性不会出现碎片问题。
缺点:需要两倍的内存空间。对于g1这种分拆成大量region的gc,复制而不是移动意味着gc需要维护region之间对象引用关系。不管是内存占用或者时间开销也不小。
如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。
应用场景:在新生代对常规应用的垃圾回收,一次通常可以回收百分之70到99%的内存空间,回收性价比很高。所以现在的商业虚拟机都是用这种垃圾收集算法。
标记-压缩(整理)算法
背景:复制算法的高效性是建立在存活对象少,垃圾对象多的前提下。这种情况在新生代经常发生,但是在老年代更常见的情况是,大部分对象都是存活对象,如果依然使用复制算法,由于存活的对象较多,复制的成本也较高,因此基于老年代垃圾回收的特性,需要使用其他的算法。
标记清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以gvm的设计者需要在此基础上进行改进标记-压缩算法由此诞生。
1970年前后发布了标记压缩算法。
执行过程:第一阶段,和标记清除算法一样,从根节点开始标记所有被引用对象。第二阶段,将所有的存活对象压缩到内存的一端,按顺序存放。之后清理边界外所有的空间。
标记压缩算法的最终效果等同于,标记-清除算法执行完成后,再进行一次内存碎片整理,因此可以把它称为标记-清除-压缩算法。
二者的本质差异在于标记清除算法是一种非移动式的回收算法,标记压缩是移动式的,是否移动回收后的存活对象是一项优缺点并存的风险决策。
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,我们需要给新对象分配内存时,gvm只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
指针碰撞 bump the pointer
如果内存空间以规整和有序的方式分布,即已用和未用的内存,都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量,将新对象分配在第一个空内存位置上,这种分配方式就叫做指针碰撞。
优点:消除了标记-清除算法当中内存区域分散的缺点,我们需要给新对象分配内存时,gvm只需要持有一个内存的起止地址即可;消除了复制算法当中内存减半的高额代价。
缺点:1从效率上来说,标记整理算法要低于复制算法;2移动对象的同时,如果对象被其他对象引用,则还需要调整引用地址。3移动过程中需要全程暂停用户应用程序,即stw。
对比
效率上讲,复制算法最高,但浪费了太多内存。
前面所有这些算法中并没有一种算法可以完全替代其他算法,他们都具有自己独特的优势和特点,分带收集算法应运而生。
分代收集算法,是基于这样一个事实,不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采用不同的收集方式,以便提高回收效率。一般是把JAVA堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
在JAVA程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如HTTP请求中的session对象,线程、socket连接,这些对象跟业务直接挂钩,因此生命周期比较长,但是还有一些对象,主要是程序运行过程中的生成的临时变量,这些对象生命周期会比较短,比如string对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
目前,几乎所有的gc都是采用分代收集算法执行垃圾回收的。在hotspot中,基于分代的概念,gc所使用的内存回收算法必须结合年轻代和老年代各自的特点。
年轻代
特点:区域相对老年代较小,对象生命周期短,存活率低,回收频繁
这种情况复制算法的回收整理,速度是最快的,复制算法的效率只和当前存活对象大小有关,因此很适合用于年轻代的回收,而复制算法内存利用率不高的问题,通过hotspot中两个survivor的设计得到缓解。
(整个伊甸园区和幸存者区的划分,对应标记复制算法,对伊甸园区的对象进行标记,存活的复制到幸存者区,死亡的直接清除)新生代:老年代=1:2 默认
老年代
特点:区域较大对象生命周期长,存活率高,回收不及年轻代频繁
这种情况存在大量存活率高的对象。复制算法明显变得不合适,一般由标记清除或标记除整理的混合实现。Mark阶段的开销与存活对象的数量成正比,sweep阶段的开销与所管理区的大小成正比,compact阶段的开销与存活对象的数据成正比。
以hotspot中的CMS回收器为例,CMS是基于mark-sweep实现的,对于对象的回收效率很高,而对于碎片问题,CMS采用的是mark-compact算法的serial old回收器作为补偿措施。当内存回收不佳,将采用sirial old执行full gc达到对老年代内存的整理。
CMS 收集器
CMS 是 Concurrent Mark Sweep 的缩写,即并发标记清除收集器。它是一种以获取最短回收停顿时间为目标的垃圾收集器,比较适合对响应时间要求较高的 Web 应用等场景。
G1 收集器
G1 是 Garbage - First 的缩写,即垃圾优先收集器。它面向服务端应用,旨在满足大内存、多处理器机器的需求,能在实现高吞吐量的同时,尽可能满足垃圾收集暂停时间的目标。
CMS 收集器
G1 收集器
HotSpot JVM默认不使用CMS收集器,原因主要包括以下几点:
因此,CMS在JDK 9中被标记为废弃,并计划在未来版本中移除。
上述现有的算法,在垃圾回收过程中,应用软件将处于一种stop the word的状态,在stop the word状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成,如果垃圾回收时间过长,应用挂起很久,将严重影响用户体验或者系统的稳定性。为解决这个问题,即对实时垃圾收集算法的研究,直接导致了增量收集算法的诞生。
基本思想
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程依次反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍然是传统的标记清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记清理或复制的工作。
缺点:使用这种方式,由于在垃圾回收过程中,间断性的还执行了应用程序代码,所以能减少系统的停顿时间。但是因为线程切换和上下文转换的消耗,会使垃圾回收的总体成本上升,造成系统吞吐量的下降。
一般来说,在相同条件下堆空间越大,一次gc时所需要的时间就越长,有关gc产生的停顿也越长。为了更好的控制gc产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间很是合理的。回收若干个小区间,而不是总堆区间,从而减少一次gc所产生的停顿。
分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个对空间划分成连续的不同小区间,每个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
这些只是基本的算法思路,实际gc实现过程要复杂的多,目前还在发展中的前沿gc都是复合算法并且并行和并发兼备。
参考资料
康师傅jvm
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。