java性能优化

http://blog.csdn.net/kid_2412/article/details/52490757


性能参考指标:

  • 执行时间: 程序从开始到结束的执行时间。
  • CPU时间: 函数或者线程占用的cpu时间。
  • 内存分配: 程序运行期间所占内存。
  • 磁盘吞吐量: 硬盘I/O使用情况。
  • 网络吞吐量: 网络使用情况。
  • 响应时间:用户行为做出的响应时间,越短性能越好。

短板原理:

一直木桶能装多少水取决于桶壁最短的那个木板。
产生短板的资源有:

  • 磁盘I/O:很多应用大部分瓶颈在硬盘的I/O,硬盘比内存慢很多。如果等待磁盘读写完成会浪费大量CPU时间,拖累整个系统。
  • 网络请求:由于网络不稳定或拥塞控制等都会降低网络请求效率,如果等待网络请求结束或者超时时间设置过长会浪费大量CPU时间,拖累整个系统。
  • CPU:一些运算对CPU直接或者不间断占用很大,CPU密集型应用会在CPU上产生瓶颈。
  • 程序异常:对于java应用,异常产生构造异常栈并加以处理是很耗费资源的,高频率出现异常会拖垮整个系统。
  • 数据库:关系型数据库在存盘读盘操作(即磁盘I/O操作),更新索引、锁等待和竞争都会产生瓶颈。
  • 锁竞争:高并发应用线程之间互相锁的竞争,线程上下文切换都是很大的开销。
  • 内存:内存使用nand flash读写速度很快,但如果应用程序和操作系统把内存占满,会导致交换分区的使用,内存数据与硬盘交换分区交换数据会产生(即磁盘I/O)瓶颈。

上述瓶颈基本解决方案:

  • 磁盘I/O:使用NIO、零拷贝、mmap映射等技术,尽量异步读写硬盘,使用缓存和缓冲区技术。使用SSD硬盘,使用RAID技术提高写入性能。
  • 网络请求:CDN或PCDN加速、缓存、合并请求、压缩报文,尽量异步网络请求(消息队列服务、master-worker等),使用NIO。加大带宽、升级网卡。
  • CPU:持续优化代码,利用好多线程或多进程,设计合理架构,拆分业务,减少单机复杂运算,避免复杂运算和业务请求掺杂在一起。
  • 异常:处理好边界、缓冲区等,尤其需要注意避免出现NullPointException,测试时间大于开发时间减少bug出现几率。
  • 数据库:优化sql语句,注意执行计划,使用缓存代替数据库部分功能,根据情景使用合理的存储引擎,注意锁,使用连接池,使用PrepareStatment。
  • 锁竞争:尽量减少锁竞争,可以使用无锁的copy-on-write、重入锁等技术。
  • 内存:优化代码,分析内存占用情况,合理配置gc。增大物理内存。

Amdahl定律:

加速比公式:加速比=优化前耗时/优化后耗时
Speedup ≤ 1/(F+(1-F)/N)
- Speedup:加速比
- F:串行化比重
- N:CPU数量
根据Amdahl定律,优化的效果取决于CPU数量以及系统中串行化程序比重,CPU数量越多,串行比越低,优化效果越好。

性能调优层次

  • 设计调优:对整体架构进行梳理,找出短板进行优化,适当使用设计模式和以往经验。详细进行系统设计。
  • 代码调优:熟悉基本API、第三方类库的原理,选择最适当和最优秀的算法,精简实现,面向接口。
  • JVM调优:熟悉jvm内存模型,字节码等深层技术,使用常用监控工具如:visualVM、Jconsole等。进行压测,调节gc和堆栈大小。
  • 数据库调优:对sql语句进行调优,分析执行计划,对数据库系统配置调优(缓冲区、共享区、连接池等),对数据库设计进行调优(冗余、索引、分库分表、读写分离等)
  • 操作系统调优:调节句柄数、关闭没用服务和端口、调节共享内存、调节交换分区大小等。

最后需要注意:不要因为优化而优化!

设计优化手段:

  • 设计模式:使用常用设计模式改善代码,优化运行时产生的对象、对象生命周期等。
  • Value-Object:合并网络请求,减少网络请求。
  • 业务代理:对业务模块封装代理层,代理层缓存远程调用的请求数据。
  • 缓存和缓冲区:缓存热区数据,对延时比较大的功能使用缓冲技术。
  • 池技术:使用线程池、连接池、对象池等技术减少对象、连接的重新创建。
  • 并行化:充分利用cpu资源,采用多进程、多线程、协程等技术。
  • 负载均衡:分发请求,减小单机压力。
  • 时间和空间:适当情况下可以以内存或硬盘空间换取程序执行效率,同样也可以降低程序执行效率来节省内存或硬盘空间。

性能优化可采用的设计模式:

  • 单例模式:保证应用中只存在一份对象实例,减少对象产生,节省内存空间,gc回收压力减轻。
  • 享元模式:多业务模块共享对象实例,减少对象产生,节省内存空间,gc回收压力减轻。
  • 工厂模式:采用工厂生产对象实例,保证对象实例在适当的情况下创建,间接性优化对象生命周期。
  • 代理模式:封装代理模块,可对被代理对象进行安全检查、缓存、对象实延迟实例化、延迟建立网络连接或磁盘I/O操作等。
  • 装饰者模式:在对象构建时包装对象,最常见的是Java的流操作BufferedInputStream包装FileInputStream进行缓冲操作。
  • 观察者模式:在业务中,若A依赖B的处理状态时,可用观察者模式替代A对B的状态轮询,B会以回调的方式通知A。

Value-Object:

常见的Value-Object场景:循环中查询数据库,可改成集中一次性查询(如:把id用逗号分隔,使用in或exists去查询),然后通过Map(id作为key关联)以Value-Object的形式对应取出查询结果。

业务代理模式:

在RPC调用中我们可以使用代理对模块拆分,将代理提供的接口暴露给客户端调用,代理中可对客户端的重复调用进行缓存来减少网络请求,或将多个RPC调用进行合并,也可以将PRC直接的调用依赖拆分,以事件驱动方式处理返回结果。同样也可以在代理中做适当的安全性校验。

缓存和缓冲区:

  • 缓存:最常用的缓存技术有
    • 应用内缓存:jvm或容器内的缓存,通常可以用application实现,当然也有专业的缓存框架,如oscache、ehcache等,当然也可以使用HashMap自己实现。
    • 全局缓存:全局性的缓存服务,如redis、memchached等,本类型的缓存基本都是独立的缓存服务器,应用通过api访问缓存。由于单独的缓存服务,所以不受应用内存或jvm内存大小的限制,也可以方便动弹的横向扩容。
  • 缓冲区:常见的缓冲区技术如java的文件流读写缓冲BufferedInputStream或BufferedOutpuStream,将字节流缓冲到内存中逐渐写入硬盘。

池技术:

  • 池分类:线程池、对象池、连接池。
  • 池实现:

    • 线程池:java的Executors提供四种线程池:

      • newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
      • newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。
      • newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。
      • newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
    • 对象池:一般可自己使用Vector实现,或使用apache的common-pools实现。也可以通过全局缓存实现对象池。

    • 连接池:数据库的连接池主要有c3p0、dbcp等。

并行化:

  • 多进程:通过操作系统提供的fork和exec系统调用产生子进程,主进程与子进程之间采用共享内存、消息队列、管道、信号、unix域内套接字等方式通信。多进程虽然占用内存比较大(子进程会复制父进程的堆栈段、数据空间),但处理速度比较快。注意java中没有多进程概念。
  • 多线程:充分利用cpu时间片,将进程划分更细小的线程(子线程只得到了父线程的代码段),由于只得到了代码段,所以要比多进程节省空间,但由于得到的少自然在线程之间要共享数据。由此锁是必不可少的,同时线程间通信还有:信号量、信号、条件。
  • 协程:将线程再进行更细小的切分,协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈(状态寄存器)。

负载均衡:

  • 常用负载技术:
    • 硬负载:F5、思科、梭子鱼、AppDirector系列。
    • 软负载:LVS(4层负载)、nginx(7层负载)、apache(7层负载)、HAProxy(4-7层负载)等。这里主要说明软负载技术。
  • 负载策略:基于权重(weight)、轮询(round robin)、hash ip算法、主从(master-slave)。
  • 软负载方案:nginx+tomcat、apache+tomcat,需要注意session问题:
    • 黏性session:session通过hash算法绑定在一台tomcat上。一台宕机会丢失session,出现单机故障。
    • session复制:tomcat之间广播同步session,并发大以后容易产生广播风暴。
    • 缓存共享session:可使用nginx的memcached session缓存模块或Terracotta服务。也可以自己通过token/jsessionid+缓存/数据库实现session管理。

时间和空间:

  • 时间换空间:最常见的例子是在冒泡排序中不适用第三个临时变量交换前后两个变量,请参考如下代码:

    • 使用第三个临时变量:
      a=tmp;
      a=b;
      b=tmp;

    • 不使用第三个变量:
      a=a+b;
      b=a-b;
      a=a-b;

  • 空间换时间:最典型的例子是通过缓存来加快热数据的读取速度,或使用对象池减小对象创建和销毁的开销。

程序代码优化要点:

  • 字符串优化:分析String源码,了解String常用方法,使用StringBuffer、StringBuilder。
  • List、Map、Set优化:分析常用ArrayList、LinkedList、HashMap、TreeMap、LinkedHashMap、Set接口、集合常用方法优化。
  • 使用NIO:Buffered、Channel操作和原理,使用零拷贝。
  • 引用优化:强引用、弱引用、软引用、虚引用、WeekHashMap。
  • 优化技巧:常用代码优化技巧。这里不一一罗列,请参考下面的详解。

字符串优化:

  • String对象特点:

    • 终态:String类被声明为final,不可被继承重写,保护了String类和对象的安全。在jdk1.5之前final声明会被inline编译,性能大幅度提高,jdk1.5之后性能提升不大。
    • 常量池:String在编译期间会直接分配在方法区的常量池中,当我们写了多个相同值的String对象时,它们实际是指向了同一空间的不同引用罢了。这样对于String这样经常使用的对象访问代价和创建代价是十分低的。需要注意的是当使用String a="123";String b=new String("123");的时候,编译器虽然会创建一个新的String实例,但是实际值依然是指向常量池中的已有的123。我们可以使用a.intern(),String的intern方法返回常量池中的引用,intern是一个native本地方法。
    • 不变性:String对象生成后内存空间永久不会变化,好处是在多线程的情况下不用加锁同步操作。需要注意如下代码:String a="123";a="456";只是改变了对象的引用所指向的位置,实际的”123”是不变的。
  • 关于内存泄漏:

    • 存在内存泄漏的方法:
      String:

      • substring(int,int):

      这里写图片描述

      可以看到substring方法中使用了 return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen)构建截取的新字符串,来看看new String三参的构造函数,
      这里写图片描述
      最后String使用了数组拷贝this.value = Arrays.copyOfRange(value, offset, offset+count);这样做的好处是以空间换取了时间,快速的实现了新字符串的产生。但是当我们构造一个大的字符串进行截取时,并且进行批量截取时,可以想到字节拷贝将会耗费很大内存,存在内存泄漏的问题。这是因为substring使用的三参构造函数返回的字符串被外界调用者保持着强引用,而内存拷贝量大,gc无法回收,所以会产生OutOfMemory异常。

      • new String(char[],int,int):根据上述分析,这个三参的构造函数是罪魁祸首,所以建议不要使用。注意在jdk1.7之前该构造函数是只可以在包内使用的,但是1.7以后变成了公有方法。
      • substring(int):与两参的substring一样,单参的也是调用了new String(char[],int,int),参考如下代码:
        这里写图片描述
        注意jdk1.7之前该函数调用的是两参的substring。
      • concat(String):与substring一样,在jdk1.7之前使用的是new String(char[],int,int)返回的新字符串会出现内存泄漏问题,jdk1.7以后算法改进不会出现该问题。
      • replace(char,char):与substring一样,在jdk1.7之前使用的是new String(char[],int,int)返回的新字符串会出现内存泄漏问题,jdk1.7以后算法改进不会出现该问题。
      • valueOf(char[],int,int):与substring一样,但在jdk1.7之前和之后使用的是new String(char[],int,int)返回的新字符串会出现内存泄漏问题,并没有什么改进。
      • copyValueOf(char[],int,int):同上。。。
      • toLowerCase(Locale):同上。。。
      • toUpperCase(Locale):同上。。。

      Integer:

      • toString(int):与substring一样,但在jdk1.7之前和之后使用的是new String(char[],int,int)返回的新字符串会出现内存泄漏问题,并没有什么改进。

      Long:

      • toString(long):与substring一样,但在jdk1.7之前和之后使用的是new String(char[],int,int)返回的新字符串会出现内存泄漏问题,并没有什么改进。
    • 修复内存泄漏:解决这个内存泄漏的方法是可以new String(str.substring(0,100));,构造一个新字符串接触了substring原来的强引用,让gc可以正常回收,就不会出现OutOfMemory异常了。至于其他都调用了三参构造函数的,也可以使用new String对返回值重新创建实例解除强引用,也可以自己实现这些函数的功能避免调用String的三参构造函数。
  • 关于字符串分割和查找:

    • String的split:
      split实现中使用了正则表达式,在大量字符串分割时正则表达式会贪婪匹配,效率会降低,不推荐使用。
    • StringTokenizer的使用:
      StringTokenizer是jdk自带的字符串分割工具,由于没有使用正则匹配,所以速度更快,可以参看如下源码:
      这里写图片描述

      这里写图片描述

      StringTokenizer只是使用字符串本身的属性进行了切分。

StringBuffer和StringBuilder:

  • 区别:StringBuffer是线程安全的,所有操作字符串的方法都做了synchronized操作,而StringBuilder没有,是线程不安全的,所以StringBuffer性能低于StringBuilder。
  • 注意事项:StringBuffer和StringBuilder都提供了带有capacity参数的构造函数,主要作用是指定初始化容量(保存字符串缓冲区)的大小,当容量超过capacity时,会进行扩容,扩容为原来大小的2倍,创建新内存空间,同时把原来空间的内存拷贝到新内存空间,然后释放原内存空间。由于内存拷贝很耗时,所以最好指定适当的capacity。
  • 与String的+号对比:当使用+号拼接字符串时,编译器会把+号替换成new StringBuilder().append(),提高拼接效率,但是在大量循环拼接时,编译器不够智能,每次都生成新的StringBuilder,产生大量gc,所以性能不高,最好在循环中使用conact或自己构建StringBuffer或StringBuilder。

List接口: 由于篇幅过长,故拆分,请参考《java性能优化笔记-List接口分析》//TODO
Map接口: 由于篇幅过长,故拆分,请参考《java性能优化笔记-Mapt接口分析》//TODO
Set接口: 由于篇幅过长,故拆分,请参考《java性能优化笔记-Set接口分析》//TODO
RadnomAccess接口: 由于篇幅过长,故拆分,请参考《java性能优化笔记-RadnomAccess接口分析》//TODO

优化集合操作:

  • 分离循环中重复代码:最常见的是循环中调用集合的size()方法,如果集合容量不变,一定要把size提前求出来,int size=list.size();for(int i=0;i<size;i++){...},虽然size()方法返回的是集合的内部变量size,但是由于size()是方法,存在函数的入栈出栈,会耗时。
  • 减少方法调用:与上面一样,方法调用存在函数入栈出栈,所以最好不要在遍历集合中调用方法。
  • 省略相同操作:在遍历集合时我们经常会通过get方法获取集合中的对象再对其操作,如果在遍历中多次使用get也是耗时的,所以可以在循环体内先get出对象存到局部变量中,然后操作局部变量。类似这样的重复操作可以提取出来。
  • 使用迭代器:遍历集合的方法有很多,通过for随机访问,foreach迭代,迭代器迭代等。
    • for随机访问:在对ArrayList时迭代相当快,LinkedList基于链表实现随机访问非常差。
    • 迭代器迭代:迭代器访问集合的速度是最快的,每个集合都实现了Ietrator迭代器接口,每个实现都会根据集合本身特性优化访问数据。
    • foreach迭代:由于foreach会被编译成迭代器,正常理应访问速度快,但是编译后会存在一次对迭代器next()返回变量的多余赋值,所以速度有所减缓。

使用NIO:

  • NIO与传统I/O区别:
  • Buffer和Channel:
  • Buffer原理:
  • API:
  • 零拷贝:

引用类型:

  • 强引用:直接new出的对象都是强引用的,强引用gc回收很少,除非与gc root彻底断开,否则gc宁可抛出OutOfMemory异常。
  • 软引用:软引用会在堆接近阈值的时候被gc回收,只要有足够的内存就会保持引用。使用java.lang.SoftReference构造软引用。
  • 弱引用:软引用的引用级别最低,只要gc线程运行时发现软引用的存在就会回收弱引用,不过gc线程优先级很低,所以也会存活一段时间。使用java.lang.WeekReference构造弱引用。
  • 虚引用:虚引用是无法直接引用的,当使用java.lang.PhantomReference构造虚引用后用get()方法取出原来的强引用时,会直接得到null,因为虚引用get()方法实现直接返回的null。虚引用的唯一作用是配合引用队列回收资源,在gc回收强引用时进入引用队列,在引用队列中通过引用队列的remove()或poll()方法的返回值判断是否被回收,如果回收的话清理其他资源。
  • WeekHashMap:WeakHashMap是HashMap的弱引用版本,里面每个Key的元素都是弱引用的。WeakHashMap继承WeekReference用于把Key放入弱引用中,在get或者put时也会直接或间接调用内部方法expungeStaleEntries(),该方法会检测弱引用是否被回收,如果被回收会释放Key的资源。
  • 引用队列:当对象改变其可达性状态时,对该对象的引用就可能会被置于引用队列(reference queue)中。这些队列被垃圾回收器用来与我们的代码沟通有关对象可达性变化的情况。java.lang.ReferenceQueue,在软引用、弱引用、虚引用构造函数中传入,当gc线程回收时,会把对象放入引用队列,但是它们不会被清除。一旦引用对象被垃圾回收器插人到队列中,其get方法的返回值就肯定会是null,因此该对象就再也不能复活了。
    • public Reference < ? extends下>poll ():用于移除并返回该队列中的下一个引用对象,如果队列为空,则返回null.
    • public Referenceremove ()throws InterruptedException:用于移除并返回该队列中的下一个引用对象,该方法会在队列返回可用引用对象之前一直阻塞。
    • public Referenceremove (long timeout) throws interrupte-dException:用于移除并返回队列中的下一个引用对象。该方法会在队列返回可用引用对象之前一直阻塞,或者在超出指定超时后结束。如果超出指定超时,则返回null.如果指定超时为0,意味着将无限期地

    代码优化技巧:

    • 异常优化:永远不要在循环中处理异常,循环构造异常栈会十分耗时,把异常捕获放循环外面。
    • 使用局部变量:局部变量存放在虚拟机栈的本地变量表中,本地变量表会随着方法销毁(出栈)而销毁,所以不需要gc。new出的对象存放在堆中,需要gc回收。而static变量存放于方法区,在编译时通过cinit构造生成,所以生命周期与类相同,方法区gc几乎不去回收(永久代),所以static多了会很耗费内存。
    • 位运算:位运算速度是最快的,经常使用的除法可替换成>>,乘法可替换成<<,右移一位等同于除以2,左移一位等同于乘以2。
    • 替换switch:if和switch性能区别并不大,但是有时使用if性能会更高,比如:
    <code class="hljs cs has-numbering"><span class="hljs-keyword">switch</span>(num):
    <span class="hljs-keyword">case</span> <span class="hljs-number">1</span>:<span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;
    <span class="hljs-keyword">case</span> <span class="hljs-number">2</span>:<span class="hljs-keyword">return</span> <span class="hljs-number">2</span>;
    <span class="hljs-keyword">case</span> <span class="hljs-number">3</span>:<span class="hljs-keyword">return</span> <span class="hljs-number">3</span>
    <span class="hljs-keyword">default</span>:<span class="hljs-keyword">return</span> -1<span class="hljs-number"></span></code>

    使用if优化后:

    <code class="hljs cs has-numbering"><span class="hljs-keyword">int</span> swArr[<span class="hljs-number">3</span>]={<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>};
    <span class="hljs-keyword">if</span>(num<<span class="hljs-number">1</span>||num><span class="hljs-number">3</span>){
        <span class="hljs-keyword">return</span> -<span class="hljs-number">1</span>;
    }<span class="hljs-keyword">else</span>{
        <span class="hljs-keyword">return</span> swArr[num];
    }</code>

    由于对数组随机访问非常快,所以使用if要比switch快。这需要根据不同业务选择性优化。另外,使用策略或者工厂模式都可以优化swtich和if判断,方便解耦。

    • 表达式:表达式运算是耗时的,可以在不影响业务的情况下把一些循环内的重复性的表达式提取到循环外用变量保存,然后再在循环内部使用。另外我们经常使用24*60*60这样的方式计算一天的秒数,其实可以在变量中直接写好计算结果。
    • 展开循环:展开循环可参考如下代码:

    未展开前:

    <code class="hljs cs has-numbering"><span class="hljs-keyword">int</span> num[]=<span class="hljs-keyword">new</span> <span class="hljs-keyword">int</span>[<span class="hljs-number">10000</span>];
    <span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> i=<span class="hljs-number">0</span>;i<<span class="hljs-number">10000</span>;i++){
        num[i]=i;
    }</code>

    展开后:

    <code class="hljs cs has-numbering"><span class="hljs-keyword">int</span> num[]=<span class="hljs-keyword">new</span> <span class="hljs-keyword">int</span>[<span class="hljs-number">10000</span>];
    <span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> i=<span class="hljs-number">0</span>;i<<span class="hljs-number">10000</span>;i+=<span class="hljs-number">3</span>){
        num[i]=i;
        num[i+<span class="hljs-number">1</span>]=i+<span class="hljs-number">1</span>;
        num[i+<span class="hljs-number">2</span>]=i+<span class="hljs-number">2</span>;
    }</code>

    这种情况展开后要比展开前运算速度快,因为循环时减少了步进的判断。

    • 使用布尔运算代替位运算:位运算虽然快,也存在位逻辑,但是在判断时使用位运算和其他逻辑运算一起时,java的if会完成位运算的判断执行后再继续判断条件中的其他逻辑运算。而布尔运算在条件满足后会直接跳转到if块中执行,省略后续的逻辑运算。不过通常我们只用布尔运算。
    • 优化数组拷贝:使用System.arrayCopy(),因为他是native的,调用操作系统实现的拷贝,效率非常高。
    • 使用缓冲区:BufferedInput和BufferedOutput在上面的文章中已经介绍过了,同样BufferedWrtier和BufferedReader效率也非常高。优先使用缓冲区。
    • 使用静态方法:静态方法不需要构建实例就可以直接使用,并且由于方法区gc很少回收,且jvm会缓存常用的类,所以一些常用工具类封装成static的性能会更高。而且要比函数重载更具有表达意义。
    • 使用设计模式:在对象比较大时可以使用原型模式替换new操作,尤其对象构造函数比较耗时时,可以直接使用原型模式clone对象,也可以使用apache的commons下的BeanUtil中的clone方法。同样在一些业务下,可以使用单例模式、享元模式、代理模式、工厂模式等常用的设计模式优化对象生成过程,提升性能。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值