Java底层——GC

Java垃圾回收机制基础——标记算法

当一个对象没有被任何对象引用的情况下,对于系统而言就是垃圾,占据的内存就要被释放,此对象也会被销毁

引用计数算法

  • 堆中每一个对象实例都有一个引用计数器,被引用则 +1,完成引用则 -1
  • 当一个对象被创建的时候,若该对象实例分配一个引用变量,该对象实例的引用计数就设置为 1,若该对象又被另外一个对象所引用,则该对象的引用计数器继续 +1
  • 而当该对象实例的某个引用超过了生命周期,或者被设置为一个新值的时候,该对象实例的引用计算便 -1
  • 任何引用计数为 0 的对象实例都会被当做垃圾收集

优点:

  • 执行效率高,程序执行受影响小
  • 只需过滤出引用计数器为 0 的对象,将其内存回收即可,可以交织在程序运行中
  • 垃圾回收时可以做到几乎不打断程序的执行,对程序需要不被长时间打断的实时环境比较有利

缺点:

  • 实现过于简单,无法检测出循环引用的情况,导致内存泄露
  • 如父对象引用子对象,子对象引用父对象
  • 循环引用问题代码示例
public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
         
        object1.object = object2;
        object2.object = object1;
         
        object1 = null;
        object2 = null;
    }
}
 
class MyObject{
    public Object object = null;
}

最后面两句将 object1 和 object2 赋值为null,也就是说 object1 和 object2 指向的对象已经不可能再被访问,但是由于他们互相引用对方,导致他们的引用计数都不为 0,那么垃圾收集器永远不会回收他们。

主流的Java垃圾收集器不采用该机制

可达性分析算法

  • 通过判断对象的引用链是否可达来决定对象是否可以被回收
  • 可达性算法是从离散数学的图论引入的,程序把所有的引入关系看做一张图,通过一系列名为 GC root作为起始点,从这些结点开始向下搜索,搜索所走过的路径被称为引用链
  • 当一个对象从 GC root 没有任何引用链相连,从图论上来说就是 GC root 到这个对象是不可达的,这个时候就证明了该对象是不可用的,他就被标记为垃圾了

在这里插入图片描述
可以作为 GC Root 的对象

  • 虚拟机栈中引用的对象(栈帧中的本地变量表中引用的对象)
  • 方法区中的常量引用的对象
    如,类里面定义的常量,该常量保存的某个对象的地址,那么被保存的对象也称为 GC 的根对象,当别的对象引用到他的时候就会形成关系链
  • 方法区中的类静态属性引用的对象
  • 本地方法栈中 JNI (Native 方法)的引用对象
  • 活跃线程的引用对象

常见可以将对象判定为可回收对象的情况

  • 显示地将某个引用赋值为 null 或者将已经指向某个对象的引用指向新的对象
Object obj = new Object();
obj = null;
Object obj1 = new Object();
Object obj2 = new Object();
obj1 = obj2;
  • 局部引用所指向的对象
void fun() {
 
.....
//循环每执行完一次
//生成的Object对象都会成为可回收的对象
    for(int i=0;i<10;i++) {
        Object obj = new Object();
        System.out.println(obj.getClass());
    }   
}
  • 只有弱引用与其关联的对象
WeakReference<String> wr = new WeakReference<String>(new String("world"));

追加:Java中的强引用、软引用、弱引用、虚引用

  • 强引用(Strong Reference):最普遍的引用,例如 Object obj = new Object;这里 new 一个对象实例,其中的 obj 就是一个强引用

    • 如果一个对象具有强引用,当内存空间不足的时候,Java虚拟机宁可抛出 OutOfMemoryError 终止应用程序,也不会回收具有强引用的对象
    • 通过将对象设置为 null 来弱化引用,使其被回收。如果我们不使用这个对象了,需要通过将对象的引用设置为 null 方法来弱化引用,使其被回收,及将刚才的 obj 设置为 null,或者等待他超过对象的生命周期,这个时候 GC 就认为该对象不存在引用了,就可以回收这个对象了。具体什么时候收集,取决于系统
  • 软引用(Soft Reference):表示一个对象处在有用但非必须的状态

    • 当内存空间充足的时候,GC 就不会回收该对象
    • 当内存空间不足的时候,GC 会回收该引用的对象的内存
    • 软引用可以实现内存敏感的高速缓存。不用太担心 OutOfMemoryError 的问题,因为软引用的对象内存会在内存不足的时候进行回收,同时由于一般情况下内存空间是充足的,相关对象就一直存在便于复用。软引用也可以和引用队列配合使用
// 强引用,创建的对象实例赋值给强引用str
String str = new String("abc");
// 软引用,使用SoftReference类型,泛型类型是String的
//然后将强引用str包装起来,此时softReference就是软引用了
SoftReference<String> softReference = new SoftReference<String>(str);
  • 弱引用(Weak Reference):用来描述非必须对象,类似软引用,强度比软引用更弱
    • 弱引用具有更短的声明。GC 在扫描的过程中,一旦发现有弱引用关联的对象,就会将他回收。
    • 即无论内存是否紧缺,GC 都会回收被弱引用关联的对象
    • 其被回收的概率也不大,GC 线程优先级比较低。由于垃圾回收是一个优先级很低的线程,因此不一定会很快发现哪些子句有弱引用的对象
    • 适用于引用偶尔别使用且不影响垃圾收集的对象。用法和软引用一样,弱引用也可以和引用队列搭配使用
// 强引用,创建的对象实例赋值给强引用str
String str = new String("abc");
 弱引用,使用WeakReference类型,泛型类型是String的
//然后将强引用str包装起来,此时weakReference就是弱引用了
WeakReference<String> weakReference = new WeakReference<String>(str);
  • 虚引用(Phantom Reference):形同虚设,与其他几种引用不同,虚引用不会决定对象的生命周期
    • 如果一个对象仅持有虚引用,那么他就和没有任何引用一样。任何时候都可能被垃圾收集器回收
    • 跟踪对象被垃圾收集器回收的活动,其哨兵作用
    • 虚引用和软引用和弱引用的一个区别,就是必须和引用队列 ReferenceQueue 联合使用。GC 在回收一个对象的时候,如果发现该对象具有虚引用,那么在回收之前会首先将该对象的虚引用加入到与之关联的引用队列中,程序可以判断引用队列是否加入虚引用来了解被引用的对象是否被 GC 回收,因此起到一个哨兵的作用
// 强引用,创建的对象实例赋值给强引用str
String str = new String("abc");
// ReferenceQueue对象
ReferenceQueue queue = new ReferenceQueue();
// 虚引用
PhantomReference ref = new PhantomReference(str,queue);
  • 引用等级
    强引用 > 软引用 > 弱引用 > 虚引用
    在这里插入图片描述
  • ReferenceQueue 引用队列
    • ReferenceQueue 名义上是一个队列,但是无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达。Queue 类似一个链表的结构,这里的节点其实就是 Reference 本身。链表的容器,其自己只存储当前的 head 节点,而后面的节点由每个 Reference 节点自己通过 next 来保存即可
    • 存储关联的且被 GC 的软引用、弱引用、虚引用,这三个引用都可以保存到引用队列里面,如果在创建一个引用对象的时候,指定了 ReferenceQueue,那么该引用对象指向的对象达到合适的状态的时候,GC 会把引用对象本身添加到这个队列里面,方便我们处理

Java垃圾回收机制基础——回收算法

标记-清除算法(Mark and Sweep)

  • 标记:从根集合进行扫描,对存活的对象进行标记,使用的是 可达性算法 来找到垃圾对象
  • 清除:标记完成后,对堆内存从头到尾进行线性遍历,如果发现对象没有被标识为可达对象,就将此对象占用的内存回收,并且将之前标记为可达的标识清除掉,以便进行下一次垃圾回收,回收不可达对象内存
  • 图示
    图一在这里插入图片描述
    图二
    在这里插入图片描述
    • 缺点:产生大量碎片化。由于标记清除不需要对象的移动,并且仅对不存活的对象进行处理,因此标记清除之后,会产生大量不连续的内存碎片,空间碎片太多,可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续内存,而不得不提前触发另一次垃圾收集动作

复制算法(copying)

  • 分为对象面和空闲面。
    复制算法将可用的内存按照一定比例划分为两块或者多个块,并选择其中一块或两块作为对象面,其余作为空闲面
  • 对象则是在对象面上创建的。
    当被定义为对象面的块的内存使用完之后,就将还存活着的对象复制到其中一块空闲面上。将对象面所有对象内存清除。将已使用过的内存空间一次清理掉
  • 优点:复制算法解决碎片化的问题。顺序分配内存,简单高效。适用于对象存活率低的场景——年轻代
  • 缺点:对内存空间的使用做出了昂贵的代价,因为能够使用的内存缩减到原来的一半或以下
  • 图示
    图一
    在这里插入图片描述
    图二
    在这里插入图片描述

标记-整理算法(Compacting)

  • 适用于老年代的对象回收。采用标记-清除算法引用的对象标记,但在清除时有所不同
  • 标记:从根集合进行扫描,对存活的对象进行标记
  • 清除:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收
  • 优点:在标记-清除算法的基础上进行了对象的移动(成本更高),解决了内存碎片问题、避免内存的不连续性。不用设置两块内存互换。适用于对象存活率极高的场景——老年代的回收
  • 图示
    图一
    在这里插入图片描述
    图二
    在这里插入图片描述

分代收集算法(Generational Collection)

主流的垃圾回收算法

  • 按照对象生命周期的不同划分区域以采用不同的垃圾回收算法(将堆内存进行进一步划分,不同的对象的生命周期以及存活情况不同,将不同生命周期的对象分配到堆中不同的区域,并对堆内存不同区域采用不同的策略进行回收)——一般情况下将堆划分为 老年代(Tenured Generation)和 年轻代/新生代(Young Generation)
    • 老年代特点:每次垃圾收集时只有 少量 对象需要被回收
    • 年轻代特点:每次垃圾回收时都有 大量 对象需要被回收
    • 分代收集算法目的:提高 JVM 垃圾回收执行效率

了解:

  • JDK 6,JDK 7 的堆内存分为 年轻代、老年代、永久代
  • JDK 8及其之后的版本,堆内存分为 年轻代、老年代
  • 年轻代对象存活率低,采用 复制算法
  • 老年代对象存活率高,采用 标记-清除标记-整理 算法

分代收集算法的 GC 分类

  • Minor GC:发生在年轻代中的垃圾收集动作,采用的是复制算法。

    年轻代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在年轻代。Java 中大部分的对象不需要长久的存活,具有 朝生夕灭 的性质。当一个对象被判定为死亡的时候,GC 就有责任回收掉这部分对象的内存空间

    新生代是 GC 收集垃圾的频繁区域

  • Full GC:这种 GC 和 老年代 相关,由于对老年代的回收一般会伴随着年轻代的垃圾收集,因此,此种方式被称为 Full GC .

分代收集算法-年轻代

尽可能快速地收集生命周期较短的对象

  • Eden 区:对象刚被创建出来时,其内存空间首先是被分配在 Eden 区的,若 Eden 区放不下新创建的对象,则对象有可能会被直接放到 Survivor 区,甚至是老年代中

  • 两个 Survivor 区:分别被定义为 from 区和 to 区,这两个区不固定随着垃圾回收的进行而相互转换。年轻代的目标是尽可能快速地收集那些生命周期较短的对象,一般情况下所有新生成的对象首先都是放在年轻代中

  • 年轻代内存按照 8:1:1 的默认比例分为 Eden 区和 Survivor 区,绝大部分对象在 Eden 区生成。
    年轻代中 98% 的对象都是朝生夕死的,不需要按照 1:1:1 的比例来划分空间,而是将年轻代内存划分为一块较大的 Eden 区 和两块较小的 Survivor区

  • 参考其他资料图示过程
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    Minor GC采用复制算法,使得进行内存分配时,不需要考虑内存碎片等复杂情况,只需要移动堆顶指针按顺序分配即可。回收时一次性将某个区域清空。

对象如何晋升到老年代

  • 经历一定Minor次数依然存活的对象
    长期存活的对象会进入老年代,对象在新生代经历一次Minor GC依然存活则年龄+1,年龄超过一定限制默认是15岁时就晋升到老年代

  • Survivor区中存放不下的对象
    如果是Eden区或者Survivor区放不下的对象会直接进入老年代,对象优先在Eden区分配,当Eden区没有足够的空间分配的时候,会触发一次Minor GC,每次Minor GC结束Eden区就会被清空,因为它会把Eden区还依然存活的对象放到Survivor区中,当Survivor区中放不下的时候,则有分派担保进入到老年代中

  • 新生成的大对象直接进入到老年代当中
    -XX:+PretenuerSizeThreshold:可以通过这个参数控制大对象的大小,如果超过这个参数的对象,一经生成直接放入到老年代中。

    • -XX:SurvivorRatio : Eden和一个Survivor的比值,默认是8比1
    • -XX:NewRatio : 老年代和年轻代内存大小的比例。新生代和老年代的总内存大小由-Xms、-Xmx参数决定的
    • -XX:MaxTenuringThreshold : 对象从年轻代晋升到老生代经过GC次数的最大阈值
  • 小结:
    目前大部分垃圾收集器对于年轻代都采取 复制算法,因为年轻代中每次垃圾回收都要回收大部分对象,需要复制的操作次数少。一般将年轻代划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将 Eden 和 Survivor 中还存活的对象复制到另一块 Survivor 空间中,然后清理掉 Eden 和刚才使用过的 Survivor 空间。
    当 Survivor 区空间不够用的时候,则需要依赖老年代,进行分配的担保。

分代收集算法-老年代

老年代是存放生命周期较长的对象。在年轻代中经过了 n 次垃圾回收依然存活的对象就会被放到老年代中(年纪大了)

  • 老年代的内存比年轻代的内存大,大概比例是 2:1。
    年轻代使用的是复制算法,复制成本低
    老年代对象存活率较高,没有额外空间分配担保,使用的算法是标记-清除算法、标记-整理算法进行回收
  • 触发老年代的垃圾回收的时候,通常伴随着新生代堆内存的回收,即对整个堆进行垃圾回收——Full GC。
    Minor GC 通常是和 Full GC 等价的,即收集整个 GC 堆。Full GC 比 Minor GC 慢,一般慢十倍以上,执行效率低
    当 Eden 区空间不足的时候,会触发 Minor GC 回收年轻代的内存空间

分代收集算法关键词

  • Stop-the-World:JVM 由于要执行GC而停止了应用程序的执行。

    • 在任何一种GC算法中都会发生。当 Stop-the-World发生时,除了GC所需的线程,所有线程都处于等待状态,直到GC任务完成。
    • 多数GC优化通过减少 Stop-the-World 发生的时间来提高持续性能,从而使系统具有高吞吐、低停顿的效果
  • Safepoint:垃圾收集器里面的安全点。

    • 分析过程中对象引用关系不会发生变化的点。在可达性分析中,要分析那个对象没有引用的时候,必须在一个快照的状态点进行,在这个点所有的线程都被冻结了,不可以出现分析过程中对象引用关系还在不停变化的情况,因此分析结果需要在某个节点具备确定性,该节点便叫做安全点。程序不是那个点就停顿下来的,而是到达安全点才会停顿下来
    • 产生Safepoint的地方是方法调用、循环跳转、异常跳转等等。一旦GC发生,所有的线程都跑到最新的安全点才会停顿下来,如果发现线程不在安全点,就恢复线程,等其跑到安全点再说
    • 安全点数量得适中,安全点的数量不能太少,太少就会让GC等待太长时间;也不能太多,因为太多会增加程序运行的负荷

常见垃圾收集器

JVM的运行模式:Server和Client

  • Client启动速度较快,采用的是轻量级的虚拟机。
  • Server启动速度较慢,启动进入稳定期,长期运行之后,Server模式程序运行比Client快,这是因为Server模式采用的是重量级的虚拟机,对程序采用了更多的优化。

java -version可以查看当前虚拟机使用的哪种运行模式。

  • 垃圾收集器之间的联系
    垃圾收集器和JVM实现紧密相关的,虚拟机所处的区域,说明它是属于新生代的收集器还是老年代的收集器,如果两个收集器之间有连线,就说明它们可以搭配使用。
    在这里插入图片描述

年轻代常见垃圾收集器

Serial收集器(-XX:+UseSerialGC,复制算法)

  • 在程序启动的时候,通过设置UseSerialGC参数使得年轻代使用该垃圾收集器回收。Serial收集器是java最基本、历史最悠久的收集器,jdk1.3版本之前,年轻代收集器的唯一选择。
  • 单线程收集,进行垃圾收集时,必须暂停所有工作线程。单线程的意义不仅仅是说明只会使用一个CPU或者一条收集线程去完成垃圾收集工作。更重要的是,在它进行垃圾收集的时候,必须暂停其它所有工作线程,直到它收集结束。
  • 简单高效,Client模式下默认的年轻代收集器。用户桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆,年轻代停顿时间会在几十毫秒到最多一百毫秒之间。

ParNew收集器(-XX:+UseParNewGC,复制算法)

  • 在程序启动的时候,通过设置UseParNewGC参数使得年轻代使用该垃圾收集器回收。
  • 多线程收集,其余的行为,特点和Serial收集器一样。是Server模式下虚拟机年轻代首选的收集器。
  • 单核执行效率不如Serial,因为存在线程交互开销,在多核下执行才有优势。默认开启的收集线程数和CPU数量相同,在CPU数量非常多的情况下,可以使用参数限制垃圾收集的线程数。
  • Server模式下ParNew收集器是一个非常重要的收集器,因为除Serial收集器外,目前只有它能与CMS收集器配合工作。

Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)

  • 在程序启动的时候,通过设置UseParallelGC参数使得年轻代使用该垃圾收集器回收。

  • 系统的吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。系统的吞吐量等于运行用户代码时间除以CPU总消耗时间的比值。

  • 比起关注用户线程停顿时间,更关注系统的吞吐量。Parallel Scavenge收集器类似ParNew收集器,使用多线程进行垃圾回收。停顿时间短适合用于用户交互的程序,良好的相应速度,可以提升用户的体验;高吞吐量则可以高效率利用CPU时间,尽可能快的完成运算任务,主要适合在后台运算,而不需要太多交互任务的情况。

  • 在多核下执行才有优势,Server模式下默认的年轻代收集器。如果对垃圾收集器运作原理不熟悉,在优化过程中遇到困难了,可以使用Parallel Scavenge收集器,配合自适应调节策略,即在启动参数中加入-XX:+UseAdaptiveSizePolicy这个参数会把内存管理调优任务交给虚拟机去完成。

老年代常见的垃圾收集器

Serial Old(MSC)收集器(-XX:+UseSerialOldGC,标记-整理算法)

  • 在程序启动的时候,通过设置UseSerialOldGC参数使得老年代使用该垃圾收集器回收,是Serial GC的老年代版本。

  • 单线程收集,进行垃圾收集的适合,必须暂停所有工作线程。

  • 简单高效,Client模式下默认的老年代收集器。

Parallel Old收集器(-XX:+UseParallelOldGC,标记-整理算法)

  • 在程序启动的时候,通过设置UseParallelOldGC参数使得老年代使用该垃圾收集器回收。

  • 多线程,吞吐量优先。在jdk1.6之后才开始提供的。

  • 在注重吞吐量和CPU资源敏感的场合,都可以优先考虑Parallel Old收集器加Parallel Scavenge收集器。

CMS收集器(-XX:+UseConcMarkSweepGC,标记-清除算法)

  • 在程序启动的时候,通过设置UseConcMarkSweepGC参数使得老年代使用该垃圾收集器回收。

  • CMS收集器占据了老年代垃圾收集器的半壁江山,划时代的意义就是几乎可以做到垃圾回收线程集合可以和用户线程做到同时工作,“几乎”是因为还不能做到完全不需要停止用户线程的,只是尽可能的缩短了停顿时间。如果应用程序对停顿比较敏感,并且在应用程序运行的时候,可以提供更大的内存和更多的CPU,也就是更厉害的硬件,使用CMS来收集会带来好处。如果在JVM中有相对较多存活时间较长的对象,会更适合使用CMS。

CMS垃圾收集器的整个过程(六步)

  • 初始化标记:stop-the-world。在这个阶段需要虚拟机停顿正在执行的任务。这个过程从垃圾收集器的根对象开始,只扫描到能和根对象关联的对象并做标记,所以这个过程虽然暂停了整个JVM,但是很快就完成了

  • 并发标记:并发追溯标记,程序不会停顿。紧随初始标记阶段,在初始标记的基础上继续向下追溯标记,并发标记阶段,并发标记的线程和用户执行的线程并发执行,所以程序不会停顿。

  • 并发预清理:查找执行并并发标记阶段从年轻代晋升到老年代的对象。通过重新扫描,减少下个阶段重新标记的工作,因为下个阶段会stop-the-world。

  • 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象。这个过程从垃圾收集器的根对象开始向下追溯,并处理对象关联。需要stop-the-world。

  • 并发清理:清除垃圾对象,程序不会停顿。

  • 并发重置:重置CMS收集器的数据结构。等待下一次垃圾回收。

并发标记,也就是和用户线程同时工作,就是一边丢垃圾,一边打扫,这样就会带来如果垃圾的产生是在标记后发生的,那么这次垃圾就只能等待下次再回收了,当然等待垃圾标记了过后,垃圾自然不会和用户线程产生冲突,而清理过程就能和用户线程同时处理了。对于此类垃圾回收器,有一个比较显著不可避免的一个问题,就是它所采用的是标记-清除算法,也就是说它不会压缩存活的对象,这样就会带来内存空间碎片化的问题,如果出现需要分配一个连续的较大的内存空间,则只能触发一次GC。

既用于年轻代又用于老年代的收集器

G1收集器(-XX:+UseG1GC,复制 + 标记-整理算法)

G1收集器的使命是未来替换掉JDK1.5发布的CMS收集器。

  • 在程序启动的时候,通过设置UseG1GC参数使得年轻代使用该垃圾收集器回收。

Garbage First收集器的特点

  • 并行和并发:使用多个CPU来缩短stop-the-world的停顿时间,与用户线程并发执行。

  • 分代收集,独立管理整个堆,能够采用不同的方式,去处理新创建的对象,和以及存在一段时间熬过多次GC旧对象,以获得更好的收集效果。

  • 空间整合,基于标记-整理算法,这样就解决了内存碎片的问题。

  • 可预测的停顿,可以建立可以预测的停顿时间模型,能让使用者明确指定在一个长度为m毫秒时间片段内,消耗在垃圾收集器上的时间不得超过m毫秒。

Garbage First收集器之前收集器收集的范围都是整个年轻代的,或者老年代的,Garbage First收集器Java堆的内存布局与其他收集器有很大的差别,将整个Java堆内存划分成多个大小相等的独立区域Region。

虽然保留了年轻代和老年代的概念,年轻代和老年代不再是物理隔离了。它们是一部分不再连续的Region的集合,这就意味着在分配空间的时候不需要连续的内存空间,即不需要在JVM启动的时候决定哪些Region是属于老年代,哪些属于年轻代。随着时间推移,年轻代Region被回收以后,就会变为可用状态,这个时候可以把它分配成老年代。Garbage First年轻代收集器是并行stop-the-world收集器,和其它的hotspot GC一样,当一个年轻代GC发生的时候,整个年轻代会被回收。G1的老年代收集器有所不同,它在老年代不需要整个老年代进行回收,只有一部分Region被调用。Garbage First GC的年轻代由Eden Region、Survivor Region组成。JVM分配Eden Region失败之后就会触发一个年轻代回收,这意味着Eden区间满了,GC开始释放空间,第一个年轻代收集器会移动所有的存储对象,从Eden Region到Survivor Region,这就是Copy-to-Survivor的过程。

CMS 和 G1 的区别

参考文章:CMS 与 G1 的区别

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值