12. JVM内存管理的深度剖析

.JVM中对象的创建过程

1.对象的分配

虚拟机遇到一条new指令时,首先检查是否被类加载器加载,如果没有就必须先执行相应的类加载过程,

类加载就是把class加载到JVM的运行时数据区的过程。

  1. 检查加载

首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用(符号引用:符号引用以一组符号来描述所引用的目标),并检查类是否被加载,解析和初始化过;

  1. 分配内存

接下来虚拟机将为新生对象分配内存,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

1>指针碰撞

java推中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲那边挪动一段与对象大小相等的距离,该分配方式称为"指针碰撞"

选择哪种分配方式由Java堆是否规整决定,而java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定,如果是serial,ParNew等带有压缩整理的垃圾回收器的话,系统采用的是指针碰撞,既简单又高效。如果是使用CMS这种不带压缩(整理)的垃圾回收器的话,理论上只能采取会啊复杂的空间列;

2>空闲列表

如果java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错.那就没法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为"空闲列表"。

3>并发安全

除如何划分可用空间之外,对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没修改,对象B又同时使用了原理指针分配内存。

4>CAS机制(并发问题解决方案1)

一种是对分配内存空间的动作进行同步处理---实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。

5>分配缓冲(并发问题解决方案2)

把内存分配的动作按线程划分在不同的空间中进行,即每个线程在Java堆中运行分配一小块私有内存,即本地线程分配缓(Local Allocation Buffer, TLAB),JVM在线程初始化的同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,若需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可大大提升分配效率,当Buffer容量不够时,再重新从Eden区域申请一块继续使用。

  • TLAB的目的是在为新对象分配内存空间时,让每个Java应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。

  • TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其他线程无法在这个区域分配而已;

  • 当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB。

  1. 内存空间初始化

注意不是构造方法,内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(如 int 值为0,boolean值为flase等等),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用程序能访问到这些字段的数据类型所对应的零值。

  1. 设置

接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息(Java classes在Java hotspot VM内部表示为类元数据)、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

  1. 对象初始化

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始,所有的字段都还为零值。所以,一般来说,执行new指令之后会接着把对象按照程序员的意愿进行初始化(构造方法),这样一个真正可用的对象才算完全产生出来。

2.对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data) 和 对齐填充;

1)对象头包括两部分信息

第一部分存储对象自身的运行时数据:如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳;

第二部分是类型指针,即对象指向它的类元数据指针,虚拟机通过这个指针来确定这个对象是哪一个类的实例,

如果是java数组,在对象头中还有一块用于基类数组长度的数据;

2)对齐填充

它并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对对象的大小必须是8字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全。

3.对象的访问定位

建立对象是为了使用对象,我们的java程序需要通过栈上的reference数据来操作堆上的具体对象,

目前主流的访问方式有使用句柄和直接指针两种;

句柄

若使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;

直接指针

若使用直接指针访问, reference中存储的直接就是对象地址;

这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集移动对象是非常普遍的行为)是只会改变句柄中的实例数据指针,而reference本身不需要修改。

使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在java中非常频繁,因此这类开销积少成多后也是一项非常客观的执行成本,对sun hotspot而言,它是使用直接指针访问方式进行对象访问的;

.对象的分配策略

所谓自动内存管理,最终要解决的是内存分配和内存回收两个问题。前面我们介绍了内存回收,这里我们再来聊聊内存分配。

对象的内存分配通常是在 Java 堆上分配(随着虚拟机优化技术的诞生,某些场景下也会在栈上分配,后面会详细介绍),

对象主要分配在新生代的 Eden 区,如果启动了本地线程缓冲,将按照线程优先在 TLAB 上分配。少数情况下也会直接在

老年代上分配。总的来说分配规则不是百分百固定的,虚拟机对于内存的分配还是会遵循以下几种「普世」规则:

对象的分配原则 栈中分配对象

1)对象优先在Eden分配 逃逸分析

2)空间分配担保

3)大对象直接进入老年代 堆中的优化技术

4)长期存活的对象进入老年代 本地线程分配缓冲(TLAB)

5)动态对象年龄判定

1. 对象优先在 Eden 区分配

多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。

如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。

这里我们提到 Minor GC,如果你仔细观察过 GC 日常,通常我们还能从日志中发现 Major GC/Full GC。

Minor GC 是指发生在新生代的GC,因为Java对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;

Major GC/Full GC 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。

Major GC 的速度通常会比 Minor GC 慢 10 倍以上。

2. 大对象直接进入老年代

所谓大对象是指需要大量连续内存空间的对象(如数组),频繁出现大对象是致命的,会导致在内存还有不少空间的情况下

提前触发 GC 以获取足够的连续空间来安置新对象,前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,

如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。

因此对于大对象都会直接在老年代进行分配。

3. 长期存活对象将进入老年代

虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。

因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,

将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,

当年龄达到一定程度(默认 15) 就会被晋升到老年代。

4. 动态对象年龄判定

为了更好的适应不同程序的内存情况,虚拟机并不是永远要求对象的年龄必需达到某个固定的值(比如前面说的 15)才会

被晋升到老年代,而是会去动态的判断对象年龄,若在 Survivor 区中相同年龄所有对象大小的总和大于Survivor空间的一半,

年龄大于等于该年龄的对象就可以直接进入老年代。

5. 空间分配担保

在新生代触发 Minor GC 后,如果 Survivor 中任然有大量的对象存活就需要老年队来进行分配担保,

让 Survivor 区中无法容纳的对象直接进入到老年代。

.对象的引用分析

0.GC需要完成的3件事情

哪些内存需要回收? 什么时候回收? 如何回收? JVM内部原理

垃圾收集器在做垃圾回收的时候,首先需要判断的就是那些内存是需要被回收的,

那些对象是存活的,是不可以被回收的;哪些对象已经死掉了,需要被回收;

1.引用计数法

在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1,当引用失效时,计数器减1.

Python在用,但主流虚拟机没有使用,因为存在对象相互引用的情况,这个时候需要引入额外的机制来处理,这样做影响效率,在代码中看到,只保留相互引用的对象还是被回收掉了,说明JVM中采用的不是引用计数法

1)优点:

引用计数收集器执行简单,判定效率高,交织在程序运行中,对程序不被长时间打断的实施环境比较有利;

2)缺点:

难以检测出对象之间的循环引用,同时引用计数器增加了程序执行的开销,所以java语言并没有现在这种算法进行垃圾回收;

2.可达性分析(根可达)

可达性分析算法又叫根搜索算法,该算法的基本思想就是通过该一系列称为[GC Roots]的对象作为起始点,从这些起始点开始

往下搜索,搜索所走过的路径称为引用链(References Chain),当一个对象到GC Roots 对象之间没有任何引用链式(不可达),

证明该对象是不可用的,于是就会判定可回收对象;

如下图所示:object5、 object6、 object7 虽然互有关联,但他们到GC Roots是不可达的,因此也会被判为可回收对象;

来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的;

作为GC Roots 的对象包括以下几种

1)虚拟机栈(栈帧中的本地变量表)中引用的对象;

2)方法区中类静态属性引用的对象

3)本地方法栈中JNI(即一般说的Native方法)引用的对象;

4)JVM的内部引用(class对象,异常对象NullPointException、OutMemoryError,系统类加载器)

5)所有被同步锁(synchronized关键字)持有的对象;

6)JVM内部的JMXBean、JVMTI中注册的回调、本地代码缓存等;

7)JVM实现中的"临时性"对象,跨代引用的对象(在使用分代模型回收只回收部分代时)

JVM中用到的所有现代GC算法在回收前都会先找出所有仍存活的对象。可达性分析算法是从离散数学中的图论引入的,

程序把所有的引用关系看作一张图。下图展示的JVM中的内存布局可以用来很好地阐释这一概念:

以上回收的都是对象.类的回收条件如下:

注意Class要被回收,条件比较苛刻(仅仅是可以,不代表必然,因为还有一些参数可以进行控制)

1) 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。

2) 加载该类的ClassLoader已经被回收。

3) 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

4) 参数控制:

还有一个废弃的常量,这个是对象的回收非常相似,比如:假如有一个字符串“king”进入常量池。

3.Finalize方法-生存还是死亡

即使通过可达性分析判断不可达的对象,也不是“非死不可”,它还会处于“缓刑”阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是没有找到与GCRoots的引用链,它将被第一次标记。随后进行一次筛选(如果对象覆盖了finalize),我们可以在finalize中去拯救。

一个对象是否应该在垃圾回收器在GC时回收,至少要经历两次 标记的过程;

1. 第一次标记

如果对象在进行可达性分析后被判断为不可达对象,那么他将被第一次标记,并进行一次筛选,筛选条件是此对象是否有必要

执行finalize()方法,对象没有覆盖finalize()方法或者该对象的finalize()方法曾经被虚拟机调用过,则判定为没必要;

2.第二次标记 finalize()

如果被判定为有必要执行 finalize() 方法,那么这个对象会被放置到一个 F-Queue 队列中,并在稍后由虚拟机

自动创建的、低优先级的 Finalizer 线程去执行该对象的 finalize() 方法。但是虚拟机并不承诺会等待该方法结束,

这样做是因为,如果一个对象的 finalize() 方法比较耗时或者发生了死循环,就可能导致 F-Queue 队列中的其他对象

永远处于等待状态,甚至导致整个内存回收系统崩溃。finalize() 方法是对象逃脱死亡命运的最后一次机会,如果对象

要在finalize()中挽救自己,只要重新与 GC Roots 引用链关联上就可以了,这样在第二次标记时它将被移除「即将回收」

的集合,如果对象在这个时候还没有逃脱,那么它基本上就真的被回收了。

4.对象的引用类型

通过可达性分析来判断对象是否可被回收涉及到「引用」的概念。在java中,根据引用关系的强弱不一,将引用类型划分为:

强引用(Strong Reference)、软引用(soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference);

1. 强引用

Object obj=new Object()这种方式就是强引用,只要这种强引用存在,垃圾收集器就永远不会回收被引用的对象;

2.软引用

用来描述有用但非必须的对象,在OOM前GC会把被软引用的对象进行回收,若回收后内存不足才会触发OOM,

使用 SoftReference<User> userSoft = new SoftReference<>(new User())这种方式实现软引用;

3.弱引用

它类同软引用但强度比软引用更弱,被弱引用关联的对象只能生存到下次GC发生前,当GC工作时,无论当前内存是否

足够,都会回收掉被弱引用关联的对象,WeakReference<User> userWeak=new WeakReference<>(new User);

4.虚引用

是最弱的一种引用关系,对象是否使用虚引用完全不影响对象的生存时间,也无法通过虚引用获取一个对象的实例,

使用虚引用的唯一目的是为在被垃圾收集器回收时收到一个系统通知,在Java中使用PhantomReference类来实现;

四.垃圾收集算法

1.标记-清除算法

标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段;

·标记阶段:标记出可以回收的对象;

·清除阶段:回收被标记的对象所占用的空间

标记-清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法的基础上进行改进的;

优点:实现简单,不需要对象进行移动;

缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率;

标记-清除算法的执行的过程如下图所示:

2.复制算法(新生代)

为了解决标记-清除算法的效率不高的问题,产生了复制算法,它把内存空间划分为两个相等的区域,每次只使用其中一个区域

垃圾收集时,遍历当前使用的区域,把存活对象复制到另一个区域中,最后将当前使用的区域的可回收的对象进行回收;

优点:按顺序分配内存即可,实现简单,运行高效,不用考虑内存碎片;

缺点L可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制;

Appel式回收(新生代Eden:From:To 8:1:1)

现在的商业虚拟机都采用这种算法来回收新生代,新生代的内存划分比例比不是1:1而是将内存分为一块较大的Eden/ˈiːdn/

空间和两块较小的Survivor/sərˈvaɪvər/空间,每次使用Eden和其中一块Survivor,当回收时,将Eden和Survivor中还

存活的对象一次复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间;JVM默认Eden和Survivor的

大小比例是8:1:1,也就是每次新生代中可用的内存为整个新时代容量的90%(80%+10%),只有10%会浪费,当然,98% 的对象

可回收只是一般场景下的数据,我们没办法保证每次回收后都只有不多于 10% 的对象存活,当Survivor空间不够用时,

需要依赖其他内存(这里指老年代)进行分配担保,如果另一块Survivor空间没有足够空间存放上一次新生代收集下来存活

的对象,这些对象将直接通过分配担保机制进入老年代(提高空间利用率 和 空间分配担保);

3.标记-整理算法(老年代)

在新生代中可使用复制算法,但在老年代就不能选择复制算法了,因为老年代的对象存活率比较高,这样会有较多的复制操作,

导致效率变低,标记-清除算法可以应用在老年代中,但效率不高,在内存回收后容易产生大量内存碎片,因此就出现了标记-

整理算法(Mark-Compact)算法,与标记-清除算法不同的是,标记-整理算法在标记可回收的对象后将所有存活的对象压缩

到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收,回收后,已用和未用的内存都各自一边;

优点:解决了标记-清理算法存在的内存碎片问题

缺点:仍需要进行局部对象移动,一定程度上降低了效率,直接引用, 暂停用户线程,因为对象地址已经变更;

标记-整理算法的执行过程如下图所示

4.分代收集算法

堆用于在运行时分配类实例和数组,数组和对象可能永远不会存储在栈上,因为一个栈帧并不是设计为在创建后会随时改变

大小,栈帧仅保存引用,这个引用指向对象或数组在堆中的位置,与局部变量表(每个栈帧里)中的基本数据类型和引用不同,

对象总是被存储在堆里,所以他们在方法结束后不会被移除,仅仅在垃圾收集的时候才会被移除;

当前商业虚拟机都采用分代收集的垃圾收集算法,顾名思义是根据对象的存活周期将内存划分为几块,

一般包括年轻代、老年代 和 永久代,如图所示:

1> 新生代(Young generation)

绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可达,

所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程我们称之为 minor GC /ˈmaɪnər/

新生代 中存在一个Eden区和两个Survivor区。新对象会首先分配在Eden中(如果新对象过大,会直接分配在老年代中),

在GC中,Eden中的对象会被移动到Survivor中,直至对象满足一定的年纪(定义为熬过GC的次数),会被移动到老年代;

可以设置新生代和老年代的相对大小。这种方式的优点是新生代大小会随着整个堆大小动态扩展。数 -XX:NewRatio

设置老年代与新生代的比例,参例如 -XX:NewRatio=8 指定 老年代/新生代 为8/1.老年代 占堆大小的 7/8 ,

新生代 占堆大小的 1/8(默认即是 1/8)。示例:-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8

2> 老年代(Old generation)

对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其所占空间

较大,发生在老年代上的GC要比新生代要少得多。对象从老年代中消失的过程,可以称之为major GC(或者full GC)。

3> GC工作流程:

(存活的进入下一代,无引用的直接置空)(分代回收)

*1.新生成的对象在Eden区完成内存分配;

    *2.当Eden区满了,再创建对象,会因为申请不到空间,触发minor GC,进行young(eden+From survivor)区的垃圾回收。

(为什么是eden+From survivor:两个survivor中始终有一个survivor是空的,空的那个被标记成To Survivor)

  minorGC时,Eden不能被回收的对象被 复制 到空的survivor(也就是放到ToSurvivor,同时Eden肯定会被清空)

另一个survivor(From Survivor)里不能被GC回收的对象也会被 复制 到这个survivor(To Survivor),

始终保证一个survivor是空的。(MinorGC完成之后,To Survivor 和 From Survivor的标记互换)(复制算法)

  *3.如果发现存放对象的那个survivor满了,则这些对象被复制到old区,或者survivor区没有满,但是有些对象已经

足够Old(通过XX:MaxTenuringThreshold参数来设置)默认是 15 岁,这些对象就会成为老年代。 

但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。

   

*4.Old区被放满的之后,进行完整的垃圾回收,即 Full GC

    FullGC时,整理的是Old Generation里的对象,把存活的对象放入到Permanent Generation里。

4> Minor GC、Major GC 和 Full GC之间的区别:

   *1.Minor GC(分配率越高,越频繁执行):

年轻代的回收过程称为 Minor GC,当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。

所以分配率越高,越频繁执行 Minor GC,内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。

Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记,扫描,压缩,清理操作,所以Eden 和 Survivor

区不存在内存碎片。写指针总是停留在所使用内存池的顶部。执行 Minor GC 操作时,不会影响到永久代;

从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。

所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,

停顿导致的延迟都是可以忽略不计的。其中的真相就 是,大部分 Eden 区中的对象都能被认为是垃圾,

永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,

Minor GC 执行时暂停的时间将会长很多

*2.Major GC vs Full GC (收集频率较低,耗时较长)

Major GC 是清理老年代。

Full GC 是清理整个堆空间—包括年轻代和老年代。

由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数。

首先,许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的。

另一方面,许多现代垃圾收集机制会清理部分永久代空间,所以使用“cleaning”一词只是部分正确。

5.垃圾收集算法总结

0)可达性分析算法|GC Roots 引用链

1)标记-清除|效率高但有碎片问题 二次标记finalize();

2)复制-清除|分区复制回收;适用于对象存活率较低场景(新生代)

3)标记-整理|标记-清除-整理:适用于对象存活率较高的场景(老年代)

4)分代收集 |根据对象存活周期划分:1.新生代(复制算法) 2.老年代(标记清除算法);

6.JVM中常见的垃圾收集器

7.简单的垃圾回收器工作示意图

8.CMS垃圾回收器工作示意图

9.G1图示

五.回收方法区

方法区在JVM中被划分为永久代,在java虚拟机规范中没有要求方法区实现垃圾收集,且方法区垃圾收集性价比较低;

方法区(永久代)的垃圾收集主要回收两部分内容:废弃常量和无用的类;

废弃常量的回收和java堆中对象的回收非常类似,这里就不做过多解释;

类的回收条件就比较苛刻,判断一个类是否可以被回收,要满足以下三个条件:

1)该类的所有实例已经被回收;

2)加载该类的classLoader已经被回收;

3)该类的java.lang.Class对象没有被引用,无法再任何地方通过该反射访问该类的方法;

虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。

是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc 参数进行控制,

还可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值