LAB5

目录
1 实验目标概述 ········································································································································· 1
2 实验环境配置 ········································································································································· 1
3 实验过程 ·················································································································································· 1
3.1 Static Program Analysis ················································································································· 2
3.1.1 人工代码走查(walkthrough) ························································································· 2
3.1.2 使用CheckStyle和SpotBugs进行静态代码分析 ······················································ 2
3.2 Java I/O Optimization ····················································································································· 5
3.2.1 多种I/O实现方式················································································································· 5
3.2.2 多种I/O实现方式的效率对比分析 ················································································ 5
3.3 Java Memory Management and Garbage Collection (GC) ···················································· 7 3.3.1 使用-verbose:gc参数 ······································································································ 7 3.3.2 用jstat命令行工具的-gc和-gcutil参数 ···························································· 8 3.3.3 使用jmap -heap命令行工具 ························································································· 9 3.3.4 使用jmap -clstats命令行工具 ··············································································· 10 3.3.5 使用jmap -permstat命令行工具 ············································································· 11 3.3.6 使用JMC/JFR、jconsole或VisualVM工具 ························································ 12
3.3.7 分析垃圾回收过程 ·············································································································· 13
3.3.8 配置JVM参数并发现优化的参数配置 ······································································· 13
3.4 Dynamic Program Profiling ········································································································ 14
3.4.1 使用JMC或VisualVM进行CPU Profiling ······························································ 14
3.4.2 使用VisualVM进行Memory profiling········································································ 15
3.5 Memory Dump Analysis and Performance Optimization ···················································· 16
3.5.1 内存导出 ································································································································ 16
3.5.2 使用MAT分析内存导出文件 ························································································ 16
3.5.3 发现热点/瓶颈并改进、改进前后的性能对比分析 ················································· 18
3.5.4 在MAT内使用OQL查询内存导出 ············································································ 18
3.5.5 观察jstack/jcmd导出程序运行时的调用栈 ························································· 20
3.5.6 使用设计模式进行代码性能优化 ·················································································· 21
4 实验进度记录 ······································································································································· 21
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
5 实验过程中遇到的困难与解决途径 ······························································································ 21
6 实验过程中收获的经验、教训、感想 ························································································· 22
6.1 实验过程中收获的经验和教训 ······························································································· 22
6.2 针对以下方面的感受 ················································································································· 22
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
1
1 实验目标概述
2 实验环境配置
简要陈述你配置本次实验所需环境的过程,必要时可以给出屏幕截图。
特别是要记录配置过程中遇到的问题和困难,以及如何解决的。
在这里给出你的GitHub Lab5仓库的URL地址(Lab5-学号)。
3 实验过程
请仔细对照实验手册,针对每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
2
3.1 Static Program Analysis
3.1.1 人工代码走查(walkthrough)
1.修改所有包名。
2.修改if格式。
3.控制一行字符个数。超过了100个字符,具体的代码如下:
private static String typeString = "^(GraphType|VertexType|EdgeType|GraphName) *(\\\"[\\w]+\\\")(, *\\\"[\\w]+\\\")* *$";
(由于在word中一行的代码显示问题此处不能显示出长度,此处的长度为127字符组成)
由于过长的代码对于程序员直接去看会非常累,所以我们采用了合理划分长度的办法,在等号之后划分,最终的效果如下:
private static String typeString = "^(GraphType|VertexType|EdgeType|GraphName) + "= *(\\\"[\\w]+\\\")(, *\\\"[\\w]+\\\")* *$";
4.变量定义独占一行。
3.1.2 使用CheckStyle和SpotBugs进行静态代码分析
1.字符串数组可能为空,应该先判断是否为空再操作。
2.创建了没有用的对象,应该删去。
3. 判断两个浮点数是否相等,要做差,判断差值是否小于某个数。
4. 对于一个方法,应先判断传进来的参数是否为空。
5.导入包的时候需要按照字典序。
6.导入包时要求分别导入子包。如下:
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
3
import static org.junit.Assert.*;
如果我使用了相应的类应该仅仅导入我所需要的类文件,比如我使用了assertEquals函数,那么
import static org.junit.Assert.assertEquals;
7.注释不规范。注释的缩进应与最近的代码缩进类似,即不能出现注释的双斜线在正常代码的前面,比如下面这种情况:
public class Main { public static void main(String[] args) { System.out.println("Hello world!"); // System.err.println("hello world"); } }
而应该将注释的位置向后移动,转为如下形式:
public class Main { public static void main(String[] args) { System.out.println("Hello world!"); //System.err.println("hello world"); } }
在Google中要求Javadoc的第一个描述的最后必须有结束的符号,如下所示
public class Main{ /** * For some test. */ public static void main(String [] args){ System.out.println("Hello, world!"); } }
8.缩进不规范一个。在Google Style的要求中,必须使用两个空格代替tab键作为行前缩进。此处的遵循这种代码规范的原因,是因为Google公司在使用Java的过程中发现,Java是一个多层缩进的代码,举个例子
这是Java的很常见的一种格式,在真正的函数体的前方的缩进已有8个空格,另外最长的代码一行最好不要超过100个字符,这样一下就减少了近十分之一。
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
4
public class Main{ public static void main(String [] args){ System.out.println("Hello, world!"); } }
所以谷歌就建议使用两个空格进行缩进,如下:
public class Main{ public static void main(String [] args){ System.out.println("Hello, world!"); } }
9.文件流没有关闭。
findbugs主要发现的问题就是赘余的代码和字符编码的问题。
(1)代码赘余的问题,就是所有定义过的变量均需要有地方使用,也就是我们不能有的变量定义了但是却没有使用。解决的办法,就是将所有的不必要的变量没有用到的变量均删除。
(2)IO依赖于平台的编码,也就是我们IO策略没有考虑编码的问题,在findbugs中表现出的问题“Found reliance on default encoding”。解决办法如下:
InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(fileName));
对于上述的代码,其实在使用的时候就默认使用了平台代码,其实非常简单,只需要在new对象的时候传入另一个参数作为默认使用的编码形式如下
InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(fileName), "utf-8")
就可以避免这种潜在bug的引入。
对比分析两种工具发现问题的能力和发现问题的类型上有何差异。
在checkstyle中关注的就是格式问题,比如行前缩进应该有几个空格、Javadoc应该怎么写,这些的内容不是必须的。而是像第4章中说的可理解性,即使我们的代码写的“奇丑无比”,也是可用的。但是在与同行之间的交流就会显得非常困难,因为缩进没有一点也不美的代码会给人极大的厌恶感。
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
5
findbugs更关注的是代码较深的层次里的问题,我这样写代码是不是有引入潜在bug的风险,比如我依赖于平台的编码方式,那么我的可移植性是不是受到了极大的阻碍,并且在移植后的正确性是不是还可以保证,这些问题是findbugs所关注的。
3.2 Java I/O Optimization
3.2.1 多种I/O实现方式
实现了哪些I/O方式来读写文件,具体如何实现的。
本次项目中中采用了Stream,Writer/Reader,Bufffer来读写文件。
Stream读:
使用java.io.FileInputStream中的FileInputStream类创建一个实例inputStream,用文件路径作为参数。然后把 inputStream实例作为参数创建一个InputStreamReader对象,最后传递给BufferedReader的构造方法,创建一个BufferedReader对象,并且从文件中读取,返回一个字符串,传递给相应的工厂类来解析并将相应信息传递入图中,直到文件末尾,读文件建图结束。
Stream写:
先利用文件路径创建一个File对象,然后与Stream读相对应的,传进FileOutputStream的构造方法,创建一个对象。然后遍历图中的信息,按行写入目标文件。
Reader读:
根据文件路径创建一个文件对象,然后用这个对象作为参数创建一个FileReader对象,并且按行读取文件中的信息,调用parse方法解析字符串,将信息加入图中。文件读到末尾后,文件读取和建图同时结束。
Writer写:
根据要写入的文件的路径,创建一个File对象,传入FileWriter的构造函数中创建一个FileWriter对象,然后再传入BufferedWriter的构造方法中创建一个BuffeeredWriter对象bwriter,遍历图中的物体和边,将相应的信息写入目标文件。
对于每个IO策略,在读入的时候都是基于readLine的策略进行的,也就是我们每读一行就处理一行;而在输出的策略都是将已有的图转换为String之后才利用输出的策略进行输出。
3.2.2 多种I/O实现方式的效率对比分析
如何收集你的程序I/O语法文件的时间。
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
6
为了降低单次读入文件建图的时间,所以之前将所有需要读入的文件(file1-file4)均做过处理,保证其中所有的物体和边都是符合语法并且符合相应的不变量要求。仅仅将读图建图的时间作为所测量的时间,在此期间不会做对于文件内容合法性的检测。
表格方式对比不同I/O的性能。
表格对比不同IO的性能
图形对比不同I/O的性能。
用EXCEL绘图得到。
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
7
3.3 Java Memory Management and Garbage Collection (GC)
3.3.1 使用-verbose:gc参数
在测试类中使用-verbose:gc参数将GC的情况输出可以得到如下的信息。
所有使用的命令行参数如下:
-XX:InitialHeapSize=1073741824 -XX:MaxHeapSize=2147483648 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:ReservedCodeCacheSize=1073741824 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
(a)在全过程中一共进行了757 次Minor GC,平均每两次进行Minor GC的时间间隔是 0.5853725231175694s,每进行一次Minor GC使用的平均用时 0.045822134478203394s
(b)在全过程中一共进行了21次 Full GC,平均每两次进行Full GC的时间间隔是20.24266666666667s,每进行一次Full GC的平均用时 0.5618594285714285s
(c)在每次Minor GC前,年轻代的内存区域平均会占到最大年轻代的
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
8
95.48%,在经过一次Minor GC之后,就会降到3.9%左右,与此对应的是每一次MinorGC之后通过对比Heap的变化可以平均有28M的年轻代会被拷贝到Old generation的区域。
(d)在每次进行Full GC之前,Old Gen占比都可以达到最大Old Gen的94%以上,然后每一次Full GC之后的就会将其占比降到11%左右。
(e)综合数据可以看到,Old Gen和Young Gen之间的内存分配比例是65%对比35%。
下图是在T运行一次APP时产生的基本信息:
3.3.2 用jstat命令行工具的-gc和-gcutil参数
Windows下获得当前进程PID:
根据jstat -gcutil进行分析。下面只摘取前两次的Minor GC数据。
采用的采样的时间间隔为250ms。
在进行第40次Minor GC所用的时间为0.044s(1.706-1.662),并且以后的每
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
9
进行一次Minor GC的时间也不会超过0.1s,也就是进行Minor GC的时间适中,Minor GC回收时间正常。Eden 区域的占用率经过一个 Minor GC后从89.99%下降到22%。
对于Full GC的分析,针对以下数据进行。
可以看到在进行一次Full GC的时间所用为0.288s(0.61-0.322由于取样的间隔为250ms, 所以两次取样才得到一次Full GC的时间),进行一次Full GC的时间也合理。并且经过一次,可以回收相当部分的垃圾,比如从这里Old Gen的占用比例的93.48%下降到8.94%。
并且Metaspace的空间利用率和压缩类的空间利用率都是95%左右,表示JVM的垃圾回收正常。
3.3.3 使用jmap -heap命令行工具
使用jmap -heap输出的信息可以看到:
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
10
3.3.4 使用jmap -clstats命令行工具
使用jmap -histo命令行可以看到当前装载进内存区域的各类的实例数目和占用的内存的情况,具体的情况如下:
仅列出前十位大小的类如下:
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
11
其中[C表示char类型;[I表示Integer类型;由于使用了过多的HashSet导致在内存中有关HashMap Node的使用数量过多,这将是后面进行优化的时候的内存瓶颈所在。
3.3.5 使用jmap -permstat命令行工具
使用jmap -clstats可以看到以上的结果。
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
12
3.3.6 使用JMC/JFR、jconsole或VisualVM工具
可以看到在使用当前参数的情况下:
Old Gen进行Full GC的次数过多,占用的时间也比较多,证明在Old Gen区域的大小偏小,可能需要增大。
另外对于Metaspace的区域在刚刚开始运行的时候有一个明显的升高,证明对于元数据区域的大小也偏小,这里也是一个优化的地方。
所以根据以上的信息进行重新配置JVM参数进行优化。
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
13
3.3.7 分析垃圾回收过程
根据visual GC中的显示,并且结合JVM GC知识可以总结Java执行垃圾回收的过程为:
1. Eden:当对象创建后,会被存储在新生代的Eden区中
2.Survivor(S0):当Eden满了,就会进行一个Minor GC,将存活的对象复制到S0,然后清空Eden
3.Survivor(S1):当再次发生Minor GC时,就会对Edon和S0一起进行垃圾回收,将存活的对象复制到S1,然后清空Eden和S0
4.年轻代垃圾回收就是上面的三步,不断地复制清除
5.老年代:在年轻代中进行垃圾回收存活的对象有一个岁数,垃圾回收一次就会加一,默认值是15,当到达临界年龄,对象就会被复制到老年代
6.当老年代的被占满,无法再进入对象时,就会进行一次Full GC,这个垃圾回收的时间比较长
3.3.8 配置JVM参数并发现优化的参数配置
-Xmx:JVM最大可用内存
-Xms:JVM最小可用内存
-Xmn:年轻代大小。
-XX:PermSize:表示非堆区初始内存分配大小。
-XX:MaxPermSize:表示对非堆区分配的内存的最大上限。
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
14
经过我的不断修正,这是一个比较合理的参数。简单的说,GC时间和GC频率不可兼得,需要找到一个比较平衡的点。想找到这样的参数,需要大量的运行数据,以及长时间的运行。
-XX:+Use使用的垃圾回收是并行还是串行
-XX:NewSize=128m
-XX:MaxNewSize=512m
-Xms1024m
-Xmx1500m
3.4 Dynamic Program Profiling
3.4.1 使用JMC或VisualVM进行CPU Profiling
在最前面用时最长的是sleep函数,此处是防止Visual VM程序打开较慢不能及时打开Profiler进行监控。
我们的测试文件先是建图,然后分别运行在接口中定义的所有函数,监控使用CPU的时间。
最终得到的测试结果可以看到,建图所用的时间最长,这点显而易见由于其中的边和点的数量非常多。在建图这个函数中,又多次解析来自输入文件的输入信息。
(1)在建图过程中,更多的时间用在了构造中,也就是用于解析输入信息的函数耗时最长,所以如有进一步的优化,应该在函数内部将处理输入信息的方式做一些优化,如更好的使用正则表达式等等。但总体来说运行时间已经比较合理。
(2)再对比多个操作之间运行时间其中对于移除耗时最长,这是由于删除的物体是有多条边邻接的,那么为了找到所有邻接该物体的边,必然涉及到一次对边的遍历,所以运行时间耗时合理。
(3)然后时间运行较长的是构造边函数,即返回整个图的所有边的集合,由于此处为了方式表示泄露,使用的是将返回的集合进行一次拷贝,所以用时较长
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
15
合理。
(4)对于剩余的两个函数可以一起来看,查找函数来遍历所有的边并将符合要求的边加入map,所以运行时间合理。
3.4.2 使用VisualVM进行Memory profiling
对于Memory Profiler的监控使用的是仍然是与CPU监控相同的方式,可以得到下图:
此处只截出来了所占内存较大的一些类。
可以看到最多的是HashMap$Node,这是由于大量使用了HashSet并且在测试最后测试了targets和sources函数,他们的返回值均为HashMap。
另外查看物体的个数,可以看到:
computer的数量恰好为从文件中读入的1000个对象和在测试中新加入的两个对象相符,server对象的数量与文件中读入1000个对象和测试中新加入1个对象相符。
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
16
3.5 Memory Dump Analysis and Performance Optimization
3.5.1 内存导出
为了避免在程序运行结束后立即退出,使用Thread.sleep()方法将程序在建图完毕的时刻停下。并利用Visual VM对CPU使用率和堆的大小进行监控,当CPU的使用率接近于0并且堆的大小几乎不变的时刻,即为程序进行“休眠”。此时导出堆文件,恰好为刚刚建图完毕的堆文件。
3.5.2 使用MAT分析内存导出文件
OverView:
Histogram:
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
17
(3)查看Dominator tree视图
(4)Top consumers视图
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
18
3.5.3 发现热点/瓶颈并改进、改进前后的性能对比分析
分析代码可以发现所有的边加入其中的物体的时候,并没有开放给用户直接使用关系向里面传递参数的功能。
重新利用Visual VM导出堆文件,然后利用MAT分析。
我们可以看到,经过修改后的物体数目已经明显减少物体实例的数目同时缩短了读图所需的时间。并将这个结果与之前没有做过优化的情况进行对比。
实际上改进之后,我们观察之后会发现,代码的运行结果差别并不大。主要原因是:
1. 代码冗杂度过高;
2. 代码重构之后仍然不够合理;
3. 基础数据结构需要完善。
3.5.4 在MAT内使用OQL查询内存导出
此处使用的是利用MAT中的OQL查询功能
(1)查询长度大于100的String对象
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
19
查询结果见上图。可以看到一共有243个String的对象的长度大于100.
(2)查询所有包含关系的实例数量
首先用文本编辑器在1.txt中直接查询。
转而用MAT进行搜索,使用的搜索条件如下:
SELECT * FROM edge.NetworkConnection s WHERE s.label.toString().contains("S")
说明查询结果正确。
(3)查询所有的HashMap的节点,由于大量使用了Hashmap作为存储信息的工具,所以此处查询一下到底有多少的Hashmap的节点被使用。
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
20
3.5.5 观察jstack/jcmd导出程序运行时的调用栈
(1)删除某个物体的时候的调用栈
由于单独执行删除物体的时间非常短,所以为了查看删除某个物体时的调用栈,只能调用sleep函数将程序暂停,运行后的结果如下:
可以看到处理最里面函数之外,调用关系是正确的。
(2)增加某条边在社交关系网络中,同样由于执行速度的问题,此处仍然选择使用sleep的方式进行查看其中的调用栈,具体如下:
此处的调用关系是函数重写了其父类中的相关函数,但是在最终的使用时仍然用到了其中的逻辑,调用关系正确。
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
21
3.5.6 使用设计模式进行代码性能优化
目前可以想到的优化策略如下:
①改进前,读文档遇到关系的端点的标签,要把相应的顶点加入关系的list属性是通过遍历一遍所有的顶点,匹配相应的顶点,加入关系的list中,这样太耗费时间,每次向关系中加入端点耗费的时间都是O(n)的操作。改进策略:在图中加入一个map属性,map的key是顶点的label,map的value是顶点对象。在读文档遇到关系的端点的label时,可以直接根据map中的映射利用label得到结点,花费时间是O(1)的,大大节省了时间。
②在向图中加入关系时,需要判断关系的顶点是否在图中已经存在,之前采用的方法是遍历一遍所有的顶点看关系的顶点是否已经存在,消耗是时间是O(n)的。改进方法:将顶点提前加入一个set中,要判断的时候直接调用contains()方法,因为set是存储特点,所以查找消耗时间的复杂度是O(1),大大节省了时间。
③在对图中的list操作时,做了防御性拷贝\深拷贝,导致消耗了90%的时间,由于在整个过程中不会对ADT中的属性做违法修改,所以直接返回部分可变属性,节省了90%的时间。
3.6 Git仓库结构
已修改完成。
4 实验进度记录
日期
时间段
计划任务
实际完成情况
5/27
12:00-23:24
完成3.1
完成
5/28
8:00-22:00
完成3.2-3.4
完成
5/29
10:00-23:00
完成3.5
完成
5 实验过程中遇到的困难与解决途径
遇到的难点
解决途径
各种工具的使用
Google搜索。
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
22
6 实验过程中收获的经验、教训、感想
6.1 实验过程中收获的经验和教训
6.2 针对以下方面的感受
(1) 代码“看起来很美”和“运行起来很美”,二者之间有何必然的联系或冲突?哪个比另一个更重要些吗?在有限的编程时间里,你更倾向于把精力放在哪个上?
没有必然联系;两者都应该追求;放在运行起来美上。
(2) 诸如SpotBugs和CheckStyle这样的代码静态分析工具,会提示你的代码里有无数不符合规范或有潜在bug的地方,结合你在本次实验中的体会,你认为它们是否会真的帮助你改善代码质量?
会。
(3) 为什么Java提供了这么多种I/O的实现方式?从Java自身的发展路线上看,这其实也体现了JDK自身代码的逐渐优化过程。你是否能够梳理清楚Java I/O的逐步优化和扩展的过程,并能够搞清楚每种I/O技术最适合的应用场景?
大多数情况下,我觉得我们不需要记忆这些内容,直接借鉴前人的知识即可,例如使用搜索工具等。
(4) JVM的内存管理机制,与你在《计算机系统》课程里所学的内存管理基本原理相比,有何差异?有何新意?你认为它是否足够好?
没有弹性,使用不够便利。
(5) JVM自动进行垃圾回收,从而避免了程序员手工进行垃圾回收的麻烦(例如在C++中)。你怎么看待这两种垃圾回收机制?你认为JVM目前所采用的这些垃圾回收机制还有改进的空间吗?
在进行手动垃圾回收时我们会遇到更多的不确定。自动回收省去手动操作的不便利,但是回收不够及时,容易堆积垃圾。网上搜索之后发现应该有改善方法。
(6) 基于你在实验中的体会,你认为“通过配置JVM内存分配和GC参数来提高程序运行性能”是否有足够的回报?
以我目前的水平,尚未感觉到。
(7) 通过Memory Dump进行程序性能的分析,JMC/JFR、VisualVM和MAT这几个工具提供了很强大的分析功能。你是否已经体验到了使用它们发现程序热点以进行程序性能优化的好处?
软件构造课程实验报告 实验 5:静态代码分析、动态性能分析与优化
23
体会到了,针对热点进行优化可以大幅度降低程序的时间占用/空间占用。
(8) 使用各种代码调优技术进行性能优化,考验的是程序员的细心,依赖的是程序员日积月累的编程中养成的“对性能的敏感程度”。你是否有足够的耐心,从每一条语句、每一个类做起,“积跬步,以至千里”,一点一点累积出整体性能的较大提升?
必然会。
(9) 关于本实验的工作量、难度、deadline。
工作量比较大,而且比较麻烦,因为涉及到很多没用过的工具。
(10) 到目前为止,你对《软件构造》课程的意见与建议。
希望老师上课时的指向性更强,实验的目标性更强。

转载于:https://www.cnblogs.com/richardodliu/p/11061677.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值