AdaptiveSizePolicy简介

 

今天也遇到类似问题,项目jvm采用默认设置,偶得好文

原文链接:https://www.jianshu.com/p/7414fd6862c5

一、AdaptiveSizePolicy简介

AdaptiveSizePolicy(自适应大小策略) 是 JVM GC Ergonomics(自适应调节策略) 的一部分。

如果开启 AdaptiveSizePolicy,则每次 GC 后会重新计算 Eden、From 和 To 区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量

开启 AdaptiveSizePolicy 的参数为:

-XX:+UseAdaptiveSizePolicy

JDK 1.8 默认使用 UseParallelGC 垃圾回收器,该垃圾回收器默认启动了 AdaptiveSizePolicy。

AdaptiveSizePolicy 有三个目标:

  1. Pause goal:应用达到预期的 GC 暂停时间。
  2. Throughput goal:应用达到预期的吞吐量,即应用正常运行时间 / (正常运行时间 + GC 耗时)。
  3. Minimum footprint:尽可能小的内存占用量。

AdaptiveSizePolicy 为了达到三个预期目标,涉及以下操作:

  1. 如果 GC 停顿时间超过了预期值,会减小内存大小。理论上,减小内存,可以减少垃圾标记等操作的耗时,以此达到预期停顿时间。
  2. 如果应用吞吐量小于预期,会增加内存大小。理论上,增大内存,可以降低 GC 的频率,以此达到预期吞吐量。
  3. 如果应用达到了前两个目标,则尝试减小内存,以减少内存消耗。

注:AdaptiveSizePolicy 涉及的内容比较广,本文主要关注 AdaptiveSizePolicy 对年轻代大小的影响,以及随之产生的问题。

AdaptiveSizePolicy 看上去很智能,但有时它也很调皮,会引发 GC 问题。


二、由 AdaptiveSizePolicy 引发的 GC 问题

某一天,有一位群友在群里发来一张 jmap -heap 内存使用情况图。

说 Survivor 区占比总是在 98% 以上。

jmap -heap 内存情况

仔细观察这张图,其中包含几个重要信息:

  1. From 和 To 区都比较小,只有 10M。容量比较小,才显得占比高。
  2. Old 区的占比和使用量(两个多 G)都比较高。

此外,还可以看到 Eden、From、To 之间的比例不是默认的 8:1:1。

于是,立马就想到 AdaptiveSizePolicy。

经群友的确认,使用的是 JDK 1.8 的默认回收算法。

JVM 参数配置如下:

JVM 参数配置

参数中没有对 GC 算法进行配置,即使用默认的 UseParallelGC。

用默认参数启动一个基于 JDK 1.8 的应用,然后使用 jinfo -flags pid 即可查看默认配置的 GC 算法。

默认使用 UseParallelGC

上文提到,该算法默认开启 AdaptiveSizePolicy。

即使 SurvivorRatio 的默认值是 8,但年轻代三个区域之间的比例仍会变动。

这个问题,可以参考来自R大的回答:

http://hllvm.group.iteye.com/group/topic/35468

HotSpot VM里,ParallelScavenge系的GC(UseParallelGC / UseParallelOldGC)默认行为是SurvivorRatio如果不显式设置就没啥用。显式设置到跟默认值一样的值则会有效果。

因为ParallelScavenge系的GC最初设计就是默认打开AdaptiveSizePolicy的,它会自动、自适应的调整各种参数。

在群友的截图中,From 区只有 10M,Eden 区占用了却超过年轻代八成的空间。

其原因是 AdaptiveSizePolicy 为了达到期望的目标而进行了调整。


大概定位了 Survivor 区小的原因,还有一个问题:

为什么老年代的占比和使用量都比较高?

于是群友使用 jmap -histo 查看堆中的实例。

jmap -histo 结果

可以看出,其中有两个类的实例比较多,分别是:

  1. LinkedHashMap$Entry
  2. ExpiringCache$Entry

于是,搜索关键类 ExpiringCache。

可以看出在 ExpiringCache 的构造函数中,初始化了一个 LinkedHashMap。

怀疑 LinkedHashMap$Entry 数量多的原因和 ExpiringCache$Entry 直接有关。

 

ExpiringCache(long millisUntilExpiration) {
    this.millisUntilExpiration = millisUntilExpiration;
    map = new LinkedHashMap<String,Entry>() {
        protected boolean removeEldestEntry(Map.Entry<String,Entry> eldest) {
          return size() > MAX_ENTRIES;
        }
      };
}

注:该 map 用于保存缓存数据,设置了淘汰机制。当 map 大小超过 MAX_ENTRIES = 200 时,会开始淘汰。

接着查看 ExpiringCache$Entry 类。

这个类的主要属性是「时间戳」和「值」,时间戳用于超时淘汰(缓存常用手法)。

 

static class Entry {
    private long   timestamp;
    private String val;
    ……
}

接着查看哪里使用到了这个缓存。

于是找到 get 方法,定位到只有一个类的一个方法使用到了这个缓存。

缓存 get 方法

使用到缓存的函数

接着往上层找,看到了一个熟悉的类:File,它的 getCanonicalPath() 方法使用到了这个缓存。

File 类的 getCanonicalPath 方法

该方法用于获取文件路径。

于是,询问群友,是否在项目中使用了 getCanonicalPath() 方法。

得到的回答是肯定的。

当项目中使用 getCanonicalPath() 方法获取文件路径时,会发生以下的事情:

  1. 首先从缓存中读取,取不到则需要生成缓存。
  2. 生成缓存需要新建 ExpiringCache$Entry 对象用于保存缓存值,这些新建的对象都会被分配到 Eden 区
  3. 大量使用 getCanonicalPath() 方法时,缓存数量超过 MAX_ENTRIES = 200 开启淘汰策略。原来 map 中的 ExpiringCache$Entry 对象变成垃圾对象,真正存活的 Entry 只有 200 个。
  4. 当发生 YGC 时,理论上存活的 200 个 Entry 会去往 To 区,其他被淘汰的垃圾 Entry 对象会被回收。
  5. 但由于 AdaptiveSizePolicy 将 To 区调整到只有 10MB,装不下本该移动到 To 区的对象,只能直接移动到老年代
  6. 于是,在每次 YGC 时,会有接近 200 个存活的 ExpiringCache$Entry 对象进入到老年代。随着缓存淘汰机制的运行,这些 Entry 对象立马又变成垃圾。
  7. 当对象进入老年代,即使变成了垃圾,也需要等到老年代 GC 或者 FGC 才能将其回收。由于老年代容量较大,可以承受多次 YGC 给予的 200 个 ExpiringCache$Entry 对象。
  8. 于是,老年代使用量逐渐变高。

老年代内存占用量高的问题也定位到了。

因为每次 YGC 只有 200 个实例进入到老年代,问题显得比较温和。

只是隔一段时间触发 FGC,应用运行看似正常。


接着使用 jstat -gcutil 查看 GC 情况。

可以看到从应用启动,一共发生了 15654 次 YGC。

jstat -gcutil 结果

推算每次 YGC 有 200 个 ExpiringCache$Entry 对象进入老年代。

那么,老年代中大约存在 3130800 个 ExpiringCache$Entry 对象。

从之前的 jmap -histo 结果中看到,ExpiringCache$Entry 对象的数量是 6118824 个。

两个数目都为百万级。其余约 300W 个实例应该都在 Eden 区。

每一次 YGC 后,都会有大量的 ExpiringCache$Entry 对象被回收。

从群友截取的 GC log 中可以看出,YGC 的频率大概为 23 秒一次。

GC log

假设运行的 jmap -histo 命令是在即将触发 YGC 之前。

那么,应用大概在 20s 的事件内产生了 300W 个 ExpiringCache$Entry 实例,1s 内产生约 15W 个。

假设单机 QPS = 300,一次请求产生的 ExpiringCache$Entry 实例数约为 500 个。

猜测是在循环体中使用了 getCanonicalPath() 方法。

至此可以得出 Survior 区变小,老年代占比变高的原因:

  1. 在默认 SurvivorRatio = 8 的情况下,没有达到吞吐量的期望,AdaptiveSizePolicy 加大了 Eden 区的大小。From 和To 区被压缩到只有 10M。
  2. 在项目中大量使用 getCanonicalPath() 方法,产生大量ExpiringCache$Entry 实例。
  3. 当 YGC 发生时候,由于 To 区太小,存活的 Entry 对象直接进入到老年代。老年代占用量逐渐变大。

从群友的 jstat -gcutil 截图中还可以看出,应用从启动到使用该命令,触发了 19 次 FGC,一共耗时 9.933s,平均每次 FGC 耗时为 520ms。

这样的停顿时间,对于一个高 QPS 的应用是无法忍受的。


定位到了问题的原因,解决方案比较简单。

解决的思路有两个:

  1. 不使用缓存,就不会生成大量 ExpiringCache$Entry 实例。
  2. 阻止 AdaptiveSizePolicy 缩小 To 区。让 YGC 时存活的 ExpiringCache$Entry 对象都能顺利进入 To 区,保留在年轻代,而不是进入老年代。

解决方案一:

不使用缓存。

使用 -Dsun.io.useCanonCaches = false 参数即可关闭缓存。

sun.io.useCanonCaches 参数

这种方案解决比较方便,但这个参数并非常规参数,慎用。

解决方案二:

保持使用 UseParallelGC,显式设置 -XX:SurvivorRatio=8。

配置参数进行测试:

默认配置

看到默认配置下,三者之间的比例不是 8:1:1。

加上参数 -Xmn100m -XX:SurvivorRatio=8

可以看到,加上参数 -Xmn100m -XX:SurvivorRatio=8 参数后,固定了 Eden 和 Survivor 之间的比例。

解决方案三:

使用 CMS 垃圾回收器。

CMS 默认关闭 AdaptiveSizePolicy。

配置参数 -XX:+UseConcMarkSweepGC,通过 jinfo 命令查看,可以看到 CMS 默认减去/不使用 AdaptiveSizePolicy。

jinfo 结果

群友也是采用了这个方法:

使用 CMS 之后的 jmap -heap 结果

可以看出,Eden 和 Survivor 之间的比例被固定,To 区没有被缩小。老年代的使用量和使用率也都很正常。


三、源码层面了解 AdaptiveSizePolicy

注:以下源码均主要基于 openjdk 8,不同 jdk 版本之间会有区别。

对源码的理解程度有限,对源码的理解也一直在路上。

有任何错误,还请各位指正,谢谢。

首先解释,为什么在 UseParallelGC 回收器的前提下,显式配置 SurvivorRatio 即可固定年轻代三个区域之间的比例。

在 arguments.cpp 类中有一个 set_parallel_gc_flags() 方法。

从方法命名来看,是为了设置并行回收器的参数。

 

// If InitialSurvivorRatio or MinSurvivorRatio were not specified, but the
  // SurvivorRatio has been set, reset their default values to SurvivorRatio +
  // 2.  By doing this we make SurvivorRatio also work for Parallel Scavenger.
  // See CR 6362902 for details.
  if (!FLAG_IS_DEFAULT(SurvivorRatio)) {
    if (FLAG_IS_DEFAULT(InitialSurvivorRatio)) {
       FLAG_SET_DEFAULT(InitialSurvivorRatio, SurvivorRatio + 2);
    }
    if (FLAG_IS_DEFAULT(MinSurvivorRatio)) {
      FLAG_SET_DEFAULT(MinSurvivorRatio, SurvivorRatio + 2);
    }
  }

当显式设置 SurvivorRatio,即 !FLAG_IS_DEFAULT(SurvivorRatio),该方法会设置别的参数。

方法注释上写着:

make SurvivorRatio also work for Parallel Scavenger
通过显式设置 SurvivorRatio 参数,SurvivorRatio 就会在 Parallel Scavenge 回收器中生效。

至于为何会生效,还有待进一步学习。

而默认是会被 AdaptiveSizePolicy 调整的。


接着查看 AdaptiveSizePolicy 动态调整内存大小的代码。

JDK 1.8 默认的 UseParallelGC 回收器,其对应的年轻代回收算法是 Parallel Scavenge。

触发 GC 的原因有多种,最普通的一种是在年轻代分配内存失败。

UseParallelGC 分配内存失败引发 GC 的入口位于
vmPSOperations.cpp 类的 VM_ParallelGCFailedAllocation::doit() 方法。

之后依次调用了以下方法:

parallelScavengeHeap.cpp 类的 failed_mem_allocate(size_t size) 方法。

psScavenge.cpp 类的 invoke()、invoke_no_policy() 方法。

invoke_no_policy() 方法中有一段代码涉及 AdaptiveSizePolicy。

 

if (UseAdaptiveSizePolicy) {
  ……
  size_policy->compute_eden_space_size(young_live,
                                               eden_live,
                                               cur_eden,
                                               max_eden_size,
                                               false /* not full gc*/);
  ……
}

在 GC 主过程完成后,如果开启 UseAdaptiveSizePolicy 则会重新计算 Eden 区的大小。

在 compute_eden_space_size 方法中,有几个判断。

对应 AdaptiveSizePolicy 的三个目标:

  1. 与预期 GC 停顿时间对比。
  2. 与预期吞吐量对比。
  3. 如果达到预期,则调整内存容量。

 

if ((_avg_minor_pause->padded_average() > gc_pause_goal_sec()) ||
      (_avg_major_pause->padded_average() > gc_pause_goal_sec())) {
    adjust_eden_for_pause_time(is_full_gc, &desired_promo_size, &desired_eden_size);
  } else if (_avg_minor_pause->padded_average() > gc_minor_pause_goal_sec()) {
    adjust_eden_for_minor_pause_time(is_full_gc, &desired_eden_size);
  } else if(adjusted_mutator_cost() < _throughput_goal) {
    assert(major_cost >= 0.0, "major cost is < 0.0");
    assert(minor_cost >= 0.0, "minor cost is < 0.0");
    adjust_eden_for_throughput(is_full_gc, &desired_eden_size);
  } else {
    if (UseAdaptiveSizePolicyFootprintGoal &&
        young_gen_policy_is_ready() &&
        avg_major_gc_cost()->average() >= 0.0 &&
        avg_minor_gc_cost()->average() >= 0.0) {
      size_t desired_sum = desired_eden_size + desired_promo_size;
      desired_eden_size = adjust_eden_for_footprint(desired_eden_size, desired_sum);
    }
  }

详细看其中一个判断。

 

if ((_avg_minor_pause->padded_average() > gc_pause_goal_sec()) ||
      (_avg_major_pause->padded_average() > gc_pause_goal_sec()))

如果统计的 YGC 或者 Old GC 时间超过了目标停顿时间,则会调用 adjust_eden_for_pause_time 调整 Eden 区大小。

gc_pause_goal_sec() 方法获取预期停顿时间,在 ParallelScavengeHeap::initialize() 方法中,通过读取 JVM 参数 MaxGCPauseMillis 获取。

gc_pause_goal_sec() 来自 JVM 参数


接下来,再看 CMS 回收器。

CMS 初始化分代位于 cmsCollectorPolicy.cpp 类的 initialize_generations() 方法。

 

if (UseParNewGC) {
  if (UseAdaptiveSizePolicy) {
    _generations[0] = new GenerationSpec(Generation::ASParNew,
                                         _initial_gen0_size, _max_gen0_size);
  } else {
    _generations[0] = new GenerationSpec(Generation::ParNew,
                                         _initial_gen0_size, _max_gen0_size);
  }
} else {
  _generations[0] = new GenerationSpec(Generation::DefNew,
                                       _initial_gen0_size, _max_gen0_size);
}
if (UseAdaptiveSizePolicy) {
  _generations[1] = new GenerationSpec(Generation::ASConcurrentMarkSweep,
                          _initial_gen1_size, _max_gen1_size);
} else {
  _generations[1] = new GenerationSpec(Generation::ConcurrentMarkSweep,
                          _initial_gen1_size, _max_gen1_size);
}

其中 _generations[0] 代表年轻代特征,_generations[1] 代表老年代特征。

如果设置不同的 UseParNewGC 、UseAdaptiveSizePolicy 参数,会对年轻代和老年代使用不同的策略。

CMS 垃圾回收入口位于 genCollectedHeap.cpp 类的 do_collection 方法。

在 do_collection 方法中,GC 主过程完成后,会对每个分代进行大小调整。

 

for (int j = max_level_collected; j >= 0; j -= 1) {
  // Adjust generation sizes.
  _gens[j]->compute_new_size();
}

使用 compute_new_size() 方法

本文主要讨论 AdaptiveSizePolicy 对年轻代的影响,主要看 ASParNewGeneration 类,其中的 AS 前缀就是 AdaptiveSizePolicy 的意思。

如果设置 -XX:+UseAdaptiveSizePolicy 则年轻代对应 ASParNewGeneration 类,否则对应 ParNewGeneration 类。

在 ASParNewGeneration 类中 compute_new_size() 方法中,调用了另一个方法调整 Eden 区大小。

 

size_policy->compute_eden_space_size(eden()->capacity(), max_gen_size());

该方法与 Parallel Scavenge 的 compute_eden_space_size 方法类似,也从三个方面对内存大小进行调整,分别是:

  • adjust_eden_for_pause_time
  • adjust_eden_for_throughput
  • adjust_eden_for_footprint

接着进行测试,设置参数 -XX:+UseAdaptiveSizePolicy、
-XX:+UseConcMarkSweepGC。

期望 CMS 会启用 AdaptiveSizePolicy,但根据 jmap -heap 结果查看,并没有启动,年轻代三个区域之间的比例为 8:1:1。

从 jinfo 命令结果也可以看出,即使设置了 -XX:+UseAdaptiveSizePolicy,仍然关闭了 AdaptiveSizePolicy。

jinfo 结果

因为在 JDK 1.8 中,如果使用 CMS,无论 UseAdaptiveSizePolicy 如何设置,都会将 UseAdaptiveSizePolicy 设置为 false。

查看 arguments.cpp 类中的 set_cms_and_parnew_gc_flags 方法,其调用了 disable_adaptive_size_policy 方法将 UseAdaptiveSizePolicy 设置成 false。

 

static void disable_adaptive_size_policy(const char* collector_name) {
  if (UseAdaptiveSizePolicy) {
    if (FLAG_IS_CMDLINE(UseAdaptiveSizePolicy)) {
      warning("disabling UseAdaptiveSizePolicy; it is incompatible with %s.",
              collector_name);
    }
    FLAG_SET_DEFAULT(UseAdaptiveSizePolicy, false);
  }
}

如果是在启动参数中设置了,则会打出提醒。

提醒 UseAdaptiveSizePolicy 参数和 CMS 不搭

但在 JDK 1.6 和 1.7 中,set_cms_and_parnew_gc_flags 方法的逻辑和 1.8 中的不同。

如果 UseAdaptiveSizePolicy 参数是默认的,则强制设置成 false。

如果显式设置(complete),则不做改变。

 

// Turn off AdaptiveSizePolicy by default for cms until it is
// complete.
if (FLAG_IS_DEFAULT(UseAdaptiveSizePolicy)) {
  FLAG_SET_DEFAULT(UseAdaptiveSizePolicy, false);
}

于是尝试使用 JDK 1.6 搭建 web 应用,加上 -XX:+UseAdaptiveSizePolicy、-XX:+UseConcMarkSweepGC 两个参数。

再用 jinfo -flag 查看,看到两个参数都被置为 true。

jinfo -flag 结果

接着,使用 jmap -heap 查看堆内存使用情况,发现展示不了信息。

jmap -heap 结果

这其实是 JDK 低版本的一个 Bug。

1.6.30以上到1.7的全部版本已经确认有该问题,jdk8修复。

参考:UseAdaptiveSizePolicy与CMS垃圾回收同时使用导致的JVM报错 https://www.cnblogs.com/moonandstar08/p/5751175.html


四、问题小结

  1. 现阶段大多数应用使用 JDK 1.8,其默认回收器是 Parallel Scavenge,并且默认开启了 AdaptiveSizePolicy。
  2. AdaptiveSizePolicy 动态调整 Eden、Survivor 区的大小,存在将 Survivor 区调小的可能。当 Survivor 区被调小后,部分 YGC 后存活的对象直接进入老年代。老年代占用量逐渐上升从而触发 FGC,导致较长时间的 STW。
  3. 建议使用 CMS 垃圾回收器,默认关闭 AdaptiveSizePolicy。
  4. 建议在 JVM 参数中加上 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution,让 GC log 更加详细,方便定位问题。
  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: UseAdaptiveSizePolicyJVM 的一个参数,用于启用或禁用 Java 堆大小自适应调整策略。当该参数设置为 true 时,JVM 将会根据当前应用程序的内存使用情况自动调整 Java 堆大小。这可以有效地减少因为 Java 堆过小或过大而导致的性能问题。 具体来说,如果 JVM 发现当前应用程序的内存使用率过高,它会自动增加 Java 堆的大小,以提供更多的可用内存。反之,如果 JVM 发现当前应用程序的内存使用率过低,它会自动减小 Java 堆的大小,以节省内存资源。 需要注意的是,启用 UseAdaptiveSizePolicy 参数并不一定能够解决所有的内存问题。在某些情况下,需要手动调整 Java 堆的大小或者优化应用程序的内存使用方式。 ### 回答2: UseAdaptiveSizePolicy是一种在Java虚拟机(JVM)中用于管理堆大小和垃圾回收性能的选项。 在JVM中,堆是用于存储对象实例的内存区域。堆的大小对于应用程序的性能和稳定性非常重要。如果堆的大小设置太小,可能导致OutOfMemoryError的发生。而如果堆的大小设置太大,可能会浪费内存资源。 AdaptiveSizePolicy选项允许JVM根据应用程序的实际需求动态调整堆的大小。JVM会根据应用程序在运行过程中的性能指标和垃圾回收的表现来自动调整堆的大小。 具体来说,AdaptiveSizePolicy会根据应用程序的工作负载动态调整以下参数: - 年轻代堆大小(Young Generation Heap Size):年轻代堆用于存储新创建的对象。如果频繁发生垃圾回收,说明年轻代堆较小,JVM会适当增大年轻代堆大小。 - 年老代堆大小(Old Generation Heap Size):年老代堆用于存储生存时间较长的对象。如果年老代堆频繁发生垃圾回收,说明年老代堆较小,JVM会适当增大年老代堆大小。 - 年轻代和年老代的比例(Young Generation and Old Generation Ratio):如果应用程序生成的新对象较多,JVM会适当增大年轻代堆的大小,以减少频繁发生垃圾回收的次数。 通过使用AdaptiveSizePolicyJVM可以根据应用程序的实际情况自动调整堆的大小,最大限度地提高垃圾回收的性能,以提供更好的应用程序性能和稳定性。 ### 回答3: UseAdaptiveSizePolicy是一个在Java虚拟机(JVM)中的参数,用于控制垃圾收集(GC)的自适应策略。它的作用是根据应用程序的需求和系统资源的情况来动态地调整GC的策略,以便在不同的情况下提供最佳的性能和内存利用率。 当启用UseAdaptiveSizePolicy时,JVM会根据应用程序运行的情况来自动调整各种垃圾收集相关的参数,包括新生代和老年代的内存大小、各代的比例、垃圾收集的线程数量等。通过这种方式,JVM能够根据实际情况来优化内存的使用和垃圾回收的效率,提供更好的性能和响应速度。 使用UseAdaptiveSizePolicy的好处是它减少了对手动调整GC参数的依赖,开发人员不需要深入了解各个参数的含义和调整方法,也不需要针对不同的场景和硬件配置进行调优。JVM会根据应用程序的需求和系统的资源情况自动选择合适的策略和参数,简化了应用程序的开发和维护过程。 然而,使用UseAdaptiveSizePolicy也存在一些潜在的问题。由于JVM自动调整GC参数是基于实时监测和反馈的,它需要一定的时间来收集足够的数据并进行调整。因此,在应用程序刚启动或者垃圾收集器发生变化的时候,可能会出现一些性能波动和延迟。另外,JVM的自动调整可能无法满足特定场景下的需求,此时仍然需要手动调整GC参数来达到最佳性能。 综上所述,UseAdaptiveSizePolicy是一个Java虚拟机中的参数,它通过自动调整垃圾回收的策略和参数来优化内存使用和垃圾回收的效率。它简化了应用程序的开发和维护过程,但也可能带来一些潜在的性能问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值