JVM之【运行时数据区2——堆】

三、堆(Heap)

在这里插入图片描述

1、什么是堆

  • 在Java虚拟机(JVM)中,堆(Heap)是用于动态分配内存的区域。在Java程序运行时,所有对象和数组都是在堆中分配内存的。堆是Java内存模型的重要组成部分,允许程序在运行时动态地分配和释放内存。
  • 一个JVM实例通常只有一个堆区域,整个应用程序中的所有线程共享这个堆。这个堆是由JVM在启动时根据配置参数(如-Xms-Xmx)来初始化和管理的。
  • 堆的大小JVM启动时就确定,并且创建了。
  • -Xms用于表示堆区的起始内存,等价于-XX:InitialHeapSize
  • -Xmx则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
  • 一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常。
  • 通常会将-Xms 和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能频繁的堆大小变更,并不是好事。堆空间的抖动也会耗费很多资源
  • 默认情况下,初始内存大小:物理电脑内存大小/64最大内存大小:物理电脑内存大小/4

2、堆的分代策略

JVM将堆内存分为三代:年轻代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation,Java 8之后称为元空间(Metaspace))。

(1)年轻代(Young Generation)

年轻代主要存放新创建的对象,大部分对象在这里很快变得不可达。年轻代又细分为三个区域:

  • Eden区: 大部分新对象在这里分配。
  • 两个Survivor区(S0和S1): 在Eden区进行垃圾回收时,存活的对象会被移到Survivor区。这两个Survivor区会轮流使用,一个用作复制的目标,另一个空闲。

(2)老年代(Old Generation)

生命周期较长的对象会从年轻代晋升到老年代。老年代存放的对象相对稳定,垃圾回收频率较低,但回收时通常会进行全堆扫描,代价较高。

(3)永久代(Permanent Generation)/元空间(Metaspace)

永久代用于存储类的元数据(如类信息、方法信息等)。在Java 8及之后,永久代被元空间取代,元空间在本地内存中分配,而不再是堆的一部分。

Java虚拟机(JVM)中的分代垃圾回收策略是为了提高内存管理的效率和性能。通过将堆内存划分为不同的区域,并根据对象的生命周期对其进行管理,分代策略优化了垃圾回收的频率和速度。


3、堆的内存分配策略

在Java堆内存管理中,有两种主要的内存分配策略:指针碰撞(Bump-the-pointer)和空闲列表(Free List)。这两种策略用于不同的垃圾回收器和内存布局方式。下面将详细解释这两种策略,以及它们的优缺点和相关配置。

(1)指针碰撞(Bump-the-pointer)

指针碰撞是一种非常高效的内存分配方式,适用于内存连续分配的情形。堆内存被划分为两部分:已使用的部分和未使用的部分。JVM维护一个指针,该指针指向当前未使用部分的开始位置。当需要分配内存时,只需将指针向前移动所需的大小,分配过程非常简单高效。

工作原理
  1. 初始化: 在堆的开始位置设置一个指针,指向可用内存的起始位置。
  2. 分配内存: 当需要分配对象时,将指针向前移动对象大小的距离,并返回原指针位置作为分配地址。
  3. 回收内存: 垃圾回收时,通过压缩和整理,使存活对象连续存放在堆的一端,然后重置指针到存活对象的末尾。
优点
  • 分配速度快: 内存分配只需移动指针,时间复杂度为O(1)。
  • 低碎片: 通过压缩整理,可以减少内存碎片。
缺点
  • 适用范围有限: 适用于对象存活时间较短、需要频繁分配和回收的情况(如年轻代)。
  • 整理开销: 垃圾回收时需要对存活对象进行整理,开销较大。
相关配置参数

指针碰撞通常用于垃圾回收器G1和Parallel Scavenge的年轻代。具体配置参数取决于使用的垃圾回收器。例如:

  • -XX:+UseG1GC:启用G1垃圾回收器。
  • -XX:+UseParallelGC:启用Parallel垃圾回收器。

(2)空闲列表(Free List)

空闲列表是一种较为灵活的内存分配方式,适用于内存块大小不固定的情形。JVM维护一个已回收内存块的列表,每次分配内存时,从空闲列表中找到适当大小的内存块进行分配。

工作原理
  1. 初始化: 创建一个空闲列表,记录所有可用的内存块。
  2. 分配内存: 当需要分配对象时,从空闲列表中找到适当大小的内存块,并将其从列表中移除。
  3. 回收内存: 垃圾回收时,将回收的内存块加入空闲列表。
优点
  • 灵活性高: 可以处理不同大小的内存分配请求,适用于对象大小和生命周期不确定的情况(如老年代)。
  • 无须整理: 不需要像指针碰撞那样频繁整理内存。
缺点
  • 分配速度慢: 分配内存时需要遍历空闲列表找到适当的内存块,时间复杂度为O(n)。
  • 内存碎片: 回收和分配过程中可能产生内存碎片,降低内存利用率。
相关配置参数

空闲列表通常用于垃圾回收器CMS(Concurrent Mark-Sweep)和老年代。具体配置参数包括:

  • -XX:+UseConcMarkSweepGC:启用CMS垃圾回收器。
  • -XX:+UseParallelOldGC:启用Parallel Old垃圾回收器(老年代使用空闲列表)。

总结对比

特性指针碰撞(Bump-the-pointer)空闲列表(Free List)
分配速度快(O(1))慢(O(n))
内存碎片少,通过整理减少多,需管理和合并碎片
适用场景年轻代,短生命周期对象老年代,大小不定的对象
整理开销高,需要压缩和整理无需整理,但需管理空闲块
相关垃圾回收器G1, Parallel ScavengeCMS, Parallel Old

4、对象在各个代之间的转移过程

一个对象从Eden区创建开始,到老年代,最后涉及元空间的过程如下:

  1. 对象在Eden区创建:

    • 当使用new关键字或其他方式创建对象时,首先在Eden区分配内存。
    • 大对象会直接进入老年代(如超过了新生代大小的对象)
  2. Minor GC(小型垃圾回收):

    • 当Eden区满时,会触发Minor GC。
    • 在Minor GC期间,Eden区的存活对象会被复制到一个空闲的Survivor区(S0或S1)。
    • Eden区的内存会被清空。
  3. Survivor区的对象晋升:

    • 存活下来的对象继续留在Survivor区,如果在多次Minor GC后仍存活(达到一定的年龄阈值,一般为15),对象会从Survivor区晋升到老年代。
    • 大对象会直接进入老年代(如超过了新生代大小的对象)
  4. 老年代的对象:

    • 在老年代的对象生命周期较长,通常只有在Major GC(也称为Full GC)时才会被回收。
    • 当老年代的内存使用达到一定阈值时,会触发Major GC,清理老年代中的不可达对象。
  5. 永久代/元空间:

    • 类的元数据、方法元数据等会存放在永久代(Java 7及以前)或元空间(Java 8及以后)。
    • 元空间在本地内存中分配,不属于堆的一部分。
    • 类加载器加载类时,类的元数据会存放到元空间中,这部分数据在类卸载时会被回收。

示例:对象从Eden到老年代的转移过程

假设我们创建一个新的对象:

public class Test {
    public static void main(String[] args) {
        Object obj = new Object(); // 对象在Eden区分配
    }
}
  1. 创建对象:

    • new Object()会在Eden区分配内存,创建obj对象。
  2. Eden区满时触发Minor GC:

    • 如果Eden区满了,会触发Minor GC。
    • 存活的obj对象会被复制到一个Survivor区(假设是S0)。
  3. 对象在Survivor区之间复制:

    • 如果obj在下一次Minor GC时仍然存活,会从S0复制到S1。
    • 每次Minor GC,存活对象在S0和S1之间复制。
  4. 对象晋升到老年代:

    • obj对象达到晋升年龄(如15次Minor GC),它会被移动到老年代。
    • 大对象会直接进入老年代(如超过了新生代大小的对象)
  5. Major GC回收老年代对象:

    • 当老年代内存不足时,会触发Major GC,清理不可达的老年代对象。

总结

通过分代策略,JVM能够高效地管理内存,减少垃圾回收的开销。年轻代频繁进行Minor GC,快速回收短生命周期对象,而老年代的Major GC则更少进行,但处理存活时间长的对象。元空间管理类元数据,独立于堆内存。通过这些机制,JVM能够在性能和内存管理之间取得平衡。


5、Minor GC、Major GC、Full GC

在Java虚拟机(JVM)中,垃圾回收(Garbage Collection, GC)是管理内存的关键机制。垃圾回收器通过自动回收不再使用的对象来释放内存,避免内存泄漏和内存溢出。JVM中的垃圾回收可以分为三种主要类型:Minor GC、Major GC 和 Full GC。
在这里插入图片描述

(1)Minor GC

作用

Minor GC专门用于清理年轻代(Young Generation)的垃圾对象。年轻代中的对象生命周期通常较短,频繁创建和销毁,因此Minor GC发生频率较高。

触发条件

当Eden区满时,JVM会触发Minor GC。这种情况通常发生在新对象被频繁创建的情况下。

过程
  1. 复制存活对象: 在Minor GC期间,Eden区中的存活对象会被复制到一个空闲的Survivor区(S0或S1,这俩会交替空)。
  2. 清空Eden区: Eden区的所有内存会被清空,所有不可达的对象都会被回收。
  3. Survivor区轮换: 存活的对象在两个Survivor区之间轮换,最后达到一定年龄的对象会被晋升到老年代。

(2) Major GC

作用

Major GC,也称为Old GC,主要用于清理老年代(Old Generation)的垃圾对象。老年代存放生命周期较长的对象,Major GC的发生频率较低,但回收过程较慢。所需时间一般为Minor GC的十倍以上

触发条件

当老年代的内存使用达到一定的阈值时,JVM会触发Major GC。这通常发生在老年代中的对象越来越多,导致内存不足的情况下。

过程
  1. 标记存活对象: Major GC会首先标记所有存活的对象。
  2. 清理垃圾对象: 清理不可达的对象,释放老年代中的内存。
  3. 整理内存: 一些垃圾收集器(如CMS)可能会对内存进行压缩和整理,以减少内存碎片。

(3)Full GC

作用

Full GC是一次全面的垃圾回收操作,包括清理年轻代、老年代和永久代/元空间中的所有垃圾对象。Full GC的开销最大,因为它需要暂停所有应用线程(Stop-the-World,STW)进行全堆扫描和回收。

触发条件

Full GC可以由多种情况触发,包括:

  1. System.gc() 调用: 显式调用System.gc()会建议JVM执行Full GC。
  2. 老年代或永久代/元空间内存不足: 当老年代或元空间的内存不足时,可能会触发Full GC。
  3. JVM自适应调整: 某些情况下,JVM的自适应调整策略可能会触发Full GC。
过程
  1. 标记所有存活对象: Full GC会标记整个堆中的所有存活对象,包括年轻代、老年代和永久代/元空间。
  2. 清理垃圾对象: 回收所有不可达的对象,释放内存。
  3. 整理内存: 对内存进行压缩和整理,减少内存碎片。

垃圾回收器的种类

JVM提供了多种垃圾回收器,每种回收器在处理Minor GC、Major GC和Full GC时有不同的策略。常见的垃圾回收器包括:

  1. Serial GC: 单线程垃圾回收器,适用于单处理器机器。
  2. Parallel GC: 多线程垃圾回收器,适用于多处理器机器。
  3. CMS(Concurrent Mark-Sweep) GC: 低暂停时间的垃圾回收器,适用于需要响应时间的应用。
  4. G1(Garbage-First) GC: 适用于大堆内存和低暂停时间要求的应用,结合了并行和并发回收技术。

总结

  • Minor GC: 清理年轻代的垃圾对象,触发频率高,回收速度快。
  • Major GC: 清理老年代的垃圾对象,触发频率低,回收速度慢。
  • Full GC: 全堆垃圾回收,包括年轻代、老年代和永久代/元空间,触发代价最高,通常是最后的手段。

6、TLAB-本地线程分配缓冲(Thread Local Allocation Buffer)

为什么

  • 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
  • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
  • 因为堆是线程共享的,那就会出现正在给A分配内存,还没有完成,B又来使用原先的内存状态分配内存的情况。

是什么

  • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
  • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略

再说明

  • 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。
  • 在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间。默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项“-XX:TLABwasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。
  • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

在这里插入图片描述


7、对象在堆里的创建过程

对象的创建方式

  • new: new Object();
  • 反射:Class.forName(“java.lang.Object”).newInstance();
  • clone:Object.clone();
  • 反序列化: ObjectInputStream.readObject();

对象在内存中的结构可分为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。以下是整体的创建过程。

(1)前提条件:类加载

类加载

根据new的参数,在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析、初始;如果没有,先执行相应的类加载过程。

  • 当JVM遇到一个类的首次使用(通常是通过new关键字),会触发类加载过程。
  • 类加载分为加载(Loading)、连接(Linking)和初始化(Initialization)三个阶段。
  • 加载阶段:JVM通过类加载器(ClassLoader)读取类的字节码。
  • 连接阶段:包括验证(Verification)、准备(Preparation)和解析(Resolution)。
    • 验证:确保字节码符合JVM规范。
    • 准备:为类的静态变量分配内存并设置初始值。
    • 解析:将符号引用转换为直接引用。
  • 初始化阶段:执行类的静态初始化块和静态变量的赋值。
    对象初始化是对象在堆内存中创建的关键步骤,包括内存清零、对象头填充、字段初始化和构造方法调用等过程。对齐填充则是为了优化内存访问效率,对对象的大小进行调整。下面详细描述对象初始化的过程和对齐填充。

(2)初始化

  1. 内存分配

    • 一个对象所需的内存大小,在类加载完成后就可以确定下来,所以只需要把一块确定大小的内存从堆中划分出来。
    • 在JVM的堆内存中为对象分配一个连续的内存块,这个内存块的大小由对象的类定义,包括对象头和实例数据。
    • 内存分配可能采用指针碰撞或空闲列表的方式,还有可能使用TLAB,具体取决于垃圾收集器的实现。
    • 对于小对象,线程优先在堆中自己的“本地线程分配缓冲区 TLAB”上分配内存
    • 对于大对象,可以选择直接放入老年代
  2. 内存清零

    • 分配的内存块通常会被清零(非必须但常见),以确保对象的默认值是零值。
    • 清零的目的是防止使用未初始化的内存,减少潜在的错误。
  3. 字段初始化

    • JVM将对象的字段按照类定义进行初始化,基本类型字段初始化为默认值(例如int为0,float为0.0,boolean为false等),引用类型字段初始化为null。
    • 如果类有显示的字段初始值(例如private int age = 25;),JVM会在这一步将这些字段初始化为指定的值。
  4. 对象头填充

    • 将对象头的内容填充到内存块的前几字节,具体包括:
      • Mark Word(4字节):初始化对象的hashcode值、GC年龄、锁状态标志、偏向线程ID、偏向时间戳等。等。
      • 类型指针(Klass Pointer)(4字节):指向对象所属的类元数据,JVM通过这个指针知道对象的类信息。也可以理解为,当前对象是谁/哪个类创建出来的。
      • 数组长度(如果是数组对象)(4字节):存储数组的长度。
  5. 调用构造方法

    • JVM调用类的构造方法(<init>),执行用户定义的初始化逻辑。
    • 构造方法可能调用父类的构造方法(通过super),确保整个类继承链上的初始化顺序正确。
    • 构造方法中可以进一步修改字段的初始值,设置对象的初始状态。

(3)对齐填充(Padding)

对齐填充的目的是优化内存访问效率,确保对象在内存中的对齐符合硬件要求。对齐填充通常发生在以下几个方面:

  1. 对象大小对齐

    • JVM要求对象的大小是特定字节数的倍数(通常是8字节)。如果对象的实际大小不是8字节的倍数,JVM会在对象末尾添加填充字节,以满足对齐要求。
    • 例如,一个对象实际大小是14字节,那么JVM会添加2字节的填充,使对象大小达到16字节。
  2. 字段对齐

    • JVM可能会调整对象内部字段的排列顺序,以确保每个字段都对齐到其自然边界(例如,4字节的int字段对齐到4字节边界)。
    • 字段对齐可以提高内存访问速度,因为硬件在读取或写入未对齐的数据时可能需要额外的操作。

(4)示例分析

假设有一个简单的Java类:

public class Person {
    private int age;
    private boolean isEmployed;
}

当创建一个Person对象时:

Person person = new Person();
内存布局分析
  1. 内存分配

    • JVM为Person对象分配一个内存块,包括对象头和实例数据。
  2. 内存清零

    • 内存块被清零,确保初始值为0。
  3. 对象头填充

    • 对象头(12字节,假设为32位系统):包含Mark Word(8字节)和类型指针(4字节)。
  4. 字段初始化

    • age(4字节,初始值为0)
    • isEmployed(1字节,初始值为false)
  5. 对齐填充

    • 为了对齐对象大小到8字节,可能会在isEmployed后面添加3字节的填充,使对象大小达到16字节。
对象的内存布局示例(32位系统)
| 对象头 (Mark Word, 8字节) |
| 对象头 (类型指针, 4字节) |
| int age (4字节)          |
| boolean isEmployed (1字节)|
| 填充 (3字节)             |

在64位系统中,对象头可能是16字节,因此需要调整相应的对齐填充:

| 对象头 (Mark Word, 8字节)   |
| 对象头 (类型指针, 8字节)    |
| int age (4字节)             |
| boolean isEmployed (1字节)  |
| 填充 (3字节)               |

(5)对象访问过程(对象如何定位)

在Java虚拟机(JVM)中,对象定位方式主要有两种:句柄(Handle)和直接指针(Direct Pointer)。下图为书中原图:
在这里插入图片描述

1. 句柄(Handle)访问方式
过程和原理
  • 句柄池:在堆中维护一个句柄池(Handle Pool),每个对象在堆中分配一个句柄
  • 句柄结构:句柄包含两个指针,一个指向对象实例数据(对象本身),另一个指向对象类型信息(类元数据)
  • 引用过程:对象引用保存的是句柄的地址,通过句柄间接访问对象数据和类型信息。
优劣分析

优点

  • 对象移动灵活:由于对象引用指向的是句柄,所以对象在堆中移动(如垃圾收集时的压缩)时,只需要更新句柄中的指针,无需修改引用。
  • 内存碎片管理:通过句柄池,JVM可以更有效地管理内存碎片。

缺点

  • 访问效率较低:每次访问对象都需要通过句柄进行一次间接访问,增加了访问的开销。
2. 直接指针(Direct Pointer)访问方式

在这里插入图片描述

过程和原理
  • 对象引用:对象引用直接保存对象实例的地址。
  • 访问过程:通过引用直接访问对象实例数据和类型信息。
优劣分析

优点

  • 访问速度快:由于引用直接指向对象实例,省去了句柄的间接访问,访问效率较高。
  • 简单实现:实现简单,节省了维护句柄池的开销。

缺点

  • 对象移动成本高:对象在堆中移动时,必须更新所有引用该对象的指针,增加了垃圾收集时的开销。
  • 内存碎片问题:没有句柄池统一管理,内存碎片问题可能更加严重。### 句柄(Handle)和句柄池(Handle Pool)
3.什么是句柄(Handle)

在JVM中,句柄是一种用于间接访问对象的机制。句柄本质上是一个包含对象数据指针和类型信息指针的数据结构。通过句柄,可以在对象实例在内存中移动时,保持引用的不变性。

4.什么是句柄池(Handle Pool)

句柄池是一块专门用来存储句柄的内存区域。每个新创建的对象都会在句柄池中分配一个句柄。对象引用指向句柄池中的句柄,而不是直接指向对象的内存地址。这种设计使得对象在内存中的移动对引用透明,只需更新句柄池中的指针即可。

总结

句柄访问方式和直接指针访问方式各有优劣。

  • 句柄方式的主要优点在于对象移动时引用不变,适合需要频繁移动对象的场景,如垃圾收集。
  • 直接指针方式则在访问速度上有优势,适合对访问效率要求较高的场景。
  • Java中,通过句柄机制,可以在管理内存时提供更大的灵活性和可维护性,尽管它在访问效率上稍逊一筹。
  • 选择哪种访问方式,通常取决于JVM的具体实现和所需的优化目标。

8、对象一定在堆上创建嘛?

在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。因为局部变量将随着方法结束而随之销毁。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。

如何将堆上的对象分配到栈,需要使用逃逸分析手段。程序中同步负载和内存堆分配压力的跨函数这是一种可以有效减少Java全局数据流分析算法。

通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。

在这里插入图片描述

9、根据逃逸分析,进行代码优化

使用逃逸分析,编译器可以对代码做如下优化:

  • 栈上分配

    • 将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
    • JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
    • 常见的栈上分配的场景,分别是:给成员变量赋值、方法返回值、实例引用传递。
  • 同步省略

    • 如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
    • 线程同步的代价是相当高的,同步的后果是降低并发性和性能
    • 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除,
      在这里插入图片描述
  • 分离对象或标量替换

    • 有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
    • 标量(scalar)是指一个无法再分解成更小的数据的数据。
    • Java中的原始数据类型就是标量。
    • 相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
    • 在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JI优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值