JVM内存管理和垃圾回收

1,JVM内存组成结构
   这里说一下堆和栈的区别:栈是运行时的单位,而堆是存储的单元;栈解决程序的运行问题,即程序如何执行,或者说如何处理数据,堆解决的是数据存储的问题,即数据怎么放,放在哪儿。在java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关的信息。包括局部变量、程序运行状态、方法返回值等等,而堆只负责存储对象信息。
   堆和栈中,栈是程序运行最根本的东西。程序运行可以没有堆,但是不能没有栈。而堆是为栈进行数据存储服务的,说白了堆就是一块共享的内存。不过,正是因为堆和栈的分离的思想,才使得java的垃圾回收成为可能。 堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说是可以动态变化的,但是在栈中,一个对象只对应了一个4byte的引用(堆栈分离的好处)。

             JVM栈由堆、栈、本地方法栈、方法区等部分组成,结构图如下所示:


       整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小
           为什么要分代?
              分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同声明周期的对象可以采取不同的收集方  式,以便提高回收效率。 在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是它们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
           堆:1)所有new出来的对象的内存都是由堆分配的,堆的大小可以在配置文件中合理的配置-Xmx和-Xms这两个参数指定。
                      案例:java -Xmx3550m -Xms3550m 
                             -Xmx3550m:设置JVM最大可用内存为3550M。
                             -Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
                  2)堆被划分为:年轻代和老年代;新生代又进一步划分为:Eden(青年代)和Survivor(幸存区),Survivor由From Space和To                              Space组成。-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直                          接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行                        多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
                       案例:java  -XX:MaxTenuringThreshold=0
                  3)新new出来的对象的内存是由年轻代(属于堆)分配的,当Eden空间不足的时候,会把存活的对象转移到Survivor区(幸存区)                             中,年轻代大小可以在配置文件中合理配置-Xmn或者配置-XX:SurvivorRatio(控制Eden和Survivor的比例)参数指定
                       案例:java  -Xmn2g 
                                        -Xmn2g:设置年轻代大小为2G。
                                 或者 java  XX:SurvivorRatio=4
                                 -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的                                   比值为2:4,一个Survivor区占整个年轻代的1/6
                  4)老年代:用于存放新生代中经过多次垃圾回收仍然存活的对象,持久代一般固定大小为64m,所以增大年轻代后,将会减小年                         老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
                             -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代                         所占比值为1:4,年轻代占整个堆栈的1/5
                        案例:java  -XX:NewRatio=4 


                  5)结构图:
                       
           栈:每个线程执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临                   时变量、参数和中间结果。
                      -xss:设置每个线程的堆栈大小. JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。
              本地方法栈:用于支持native方法的执行,存储了每个native方法调用的状态
              方法区:存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息。JVM用持久代(Permanet Generation)来存放方                          法区,持久代(Permanent Space)实现方法区,主要存放所有已加载的类信息,方法信息,常量池,静态变量等等。可通                          过-XX:PermSize和-XX:MaxPermSize来指定持久带初始化值和最大值。Permanent Space并不等同于方法区,只不过是                              Hotspot JVM用PermanentSpace来实现方法区而已,有些虚拟机没有Permanent Space而用其他机制来实现方法区。可通                         过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值
                   案例:java  -XX:MaxPermSize=64m 
                            -XX:MaxPermSize=64m:设置持久代大小为64m。
                        持久代主要存放的是java类的类信息,与垃圾收集要收集的java对象关系不大,年轻代和年老代的划分是对垃圾收集影响比较大的。
           堆中存放的对象的大小计算:
                在java中,一个空Object对象的大小是8byte,这个大小只是保存堆中一个没有任何属性的对象的大小。看看下面语句:
           Object  ob = new  Object();      一个Object对象的大小是8byte
           这样在程序中完成了一个java对象的声明,但是它所占的空间为:4byte+8byte。4byte是上面部分所说的java栈中保存引用的所需要             空间(存放引用地址的空间)。
          而那8byte则是java堆中对象的信息。因为所有的java非基本类型的对象都需要默认继承Object对象,因此不论什么样的java对象,其大           小都必须是大于8byte。有了Object对象的大小,我们就可以计算其他对象的大小了。一个NewObject对象在堆中占大小为:24byte
       其大小为:空对象大小(8byte)+int大小(4byte)+Boolea大小(1byte)+空Object引用的大小(4byte)=17byte。但是因为java在对对象内存         分配时都是以8的整数倍来分的,因此大于17byte的最接近8的整数倍的是24,因此此对象的大小为24byte。
           JVM常见配置
           1)堆设置
                -Xms:初始堆大小
                -Xmx:最大堆大小
                -XX:NewSize=n:设置年轻代大小
                -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
                -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一                 个Survivor区占整个年轻代的1/5
                -XX:MaxPermSize=n:设置持久代大小
            2)收集器设置
                -XX:+UseSerialGC:设置串行收集器
                -XX:+UseParallelGC:设置并行收集器
                -XX:+UseParalledlOldGC:设置并行年老代收集器
                -XX:+UseConcMarkSweepGC:设置并发收集器
            3)垃圾回收统计信息
               -XX:+PrintGC
               -XX:+PrintGCDetails
               -XX:+PrintGCTimeStamps
               -Xloggc:filename
             4)并行收集器设置
               -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
               -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
               -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
             5)并发收集器设置
               -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
               -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。


 2,垃圾回收器选择
            1)JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对                  并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。                    JDK5.0以后,JVM会根据当前系统配置进行判断。
             JVM提供两种较为简单的GC策略的设置方式:
             第一种:吞吐量优先的并行收集器,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等,同时启动多个线程              进行垃圾回收,回收的效率大大提高。
                  案例: java  -XX:+UseParallelGC
                  -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效(因为垃圾回收主要正对的是对new出的对象的内存                    不被使用时的回收,当然jdk1.6之后也出现了老年代的并行收集器)。即上述配置下,年轻代使用并发收集,而年老代                                     仍旧使用串行收集。
                   java   -XX:ParallelGCThreads=20
                   -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相                    等。
                   java  -XX:+UseParallelGC -XX:+UseAdaptiveSizePolicy
                    -XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规                    定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
                   java     -XX:+UseParallelOldGC
                      -XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
                       java   -XX:+UseParallelOldGC   -XX:MaxGCPauseMillis=100
                       -XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以                          满足此值。
               第二种:响应时间优先的并发收集器:并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电               信领域等。
              java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX                                  +UseParNewGC
              -XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所                  以,此时年轻代大小最好用-Xmn设置。
             -XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设              置此值。
             java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:                +UseCMSCompactAtFullCollection
             -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使               得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
             -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片


JVM内存调优
               1,首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。
               对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数,过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量。特别要关注Full GC,因为它会对整个堆进行整理,导致Full GC一般由于以下几种情况:
            1)调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对  象 ,所以要控制好年轻代和老年代的比例,垃圾回收不要手动触发,尽量依靠JVM自身的机制 
                2)调优手段主要是通过控制堆内存的各个部分的比例和GC策略来实现,下面来看看各部分比例不良设置会导致什么后果
                 11)新生代设置过小
                 一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入老年代,占据了老年代剩余空间,诱发Full GC
                 12)新生代设置过大
                 一是新生代设置过大会导致旧生代过小(堆总量一定),从而诱发Full GC;二是新生代GC耗时大幅度增加一般说来新生代占整个                  堆1/3比较合适
                 3)Survivor设置过小
                 导致对象从eden直接到达老年代,降低了在年轻代的存活时间
                 4)Survivor设置过大
                 导致eden过小,增加了GC频率,另外,通过-XX:MaxTenuringThreshold=n来控制新生代存活时间,尽量让对象在新生代被回收

2,内存泄漏检查
         内存泄漏是比较常见的问题,而且解决方法也比较通用,这里可以重点说一下,而线程、热点方面的问题则是具体问题具体分析了。
 内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。内存泄漏对系统危害比较大,因为他可以直接导致系统的崩溃。需要区别一下,内存泄漏和系统超负荷两者是有区别的,虽然可能导致的最终结果是一样的。内存泄漏是用完的资源没有回收引起错误,而系统超负荷则是系统确实没有那么多资源可以分配了(其他的资源都在使用)。
3,堆栈溢出
 异常:java.lang.StackOverflowError
说明:这个就不多说了,一般就是递归没返回,或者循环调用造成
线程堆栈满
异常:Fatal: Stack size too small
说明:java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。
解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。
系统内存被占满
异常:java.lang.OutOfMemoryError: unable to create new native thread
说明:
    这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在Java堆中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。
分配给Java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给Java虚拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。
解决:
    1. 重新设计系统减少线程数量。
    2. 线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能生产更多的线程。
持久代被占满
异常:java.lang.OutOfMemoryError: PermGen space
说明:
    Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。
    更可怕的是,不同的classLoader即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有N个classLoader那么他将会被加载N次。因此,某些情况下,这个问题基本视为无解。当然,存在大量classLoader和大量反射类的情况其实也不多。
解决:
    1. -XX:MaxPermSize=16m
    2. 换用JDK。比如JRocket。
年老代堆空间被占满
异常: java.lang.OutOfMemoryError: Java heap space
参考:http://blog.csdn.net/ochangwen/article/details/52971913
JDK自带的JVM内存调优的工具:
      JDK本身提供了很多方便的JVM性能调优监控工具,除了集成式的VisualVM和jConsole外,还有jps、jstack、jmap、jhat、jstat等小巧的工具。比如:  1)jps主要用来输出JVM中运行的进程状态信息
                     2) jstack主要用来查看某个Java进程内的线程栈信息
                     3) jmap(Memory Map)和jhat(Java Heap Analysis Tool)
                         jmap用来查看堆内存使用状况,一般结合jhat使用。  打印进程的类加载器和类加载器加载的持久代对象信息,输出:类加载器名称、对象是否存活(不可靠)、对象地址、父类加载器、已加载的类大小等信息
                      4)jstat:JVM Statistics Minitoring Tool,用于收集HotSpot虚拟机各方面的运行数据
                           
                           这是我监控到我的eclipse的内存状况。查询结果表明:新生代Eden区(E,表示Eden)使用了32.54%的空间,两个Survivor区(S0是空的、S1占满了100%,表示Survivor0、Survivor1),老年代(O,表示Old)和永久代(P,表示Permanent)则分别使用了74.81%和?的空间。程序运行以来共发生Minor GC(YGC,Young GC)5次,总耗时(YGCT,Young GC Time)1.956秒,发生Full GC(FGC)5次,总耗时(FGCT)2.204秒,所有GC总耗时(GCT)4.160秒。
                      5)jinfo:Configuration Info for Java,显示虚拟机配置信息
现实企业级Java开发中,有时候我们会碰到下面这些问题:
OutOfMemoryError,内存不足
内存泄露
线程死锁
锁争用(Lock Contention)
Java进程消耗CPU过高
    这些问题在日常开发中可能被很多人忽视(比如有的人遇到上面的问题只是重启服务器或者调大内存,而不会深究问题根源),但能够理解并解决这些问题是Java程序员进阶的必备要求。本文将对一些常用的JVM性能调优监控工具进行介绍,希望能起抛砖引玉之用。本文参考了网上很多资料,难以一一列举,在此对这些资料的作者表示感谢!关于JVM性能调优相关的资料。
jdk可视化工具
       1,JConsole   jdk/bin下的jconsole命令
 JConsole工具在JDK/bin目录下,启动JConsole后,将自动搜索本机运行的jvm进程,不需要jps命令来查询指定。双击其中一个jvm进程即可开始监控,也可使用“远程进程”来连接远程服务器。
        2,VisualVM     jdk/bin下的jvisualvm命令
VisualVM是一个集成多个JDK命令行工具的可视化工具。VisualVM基于NetBeans平台开发,它具备了插件扩展功能的特性,通过插件的扩展,可用于显示虚拟机进程及进程的配置和环境信息(jps,jinfo),监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack)等。VisualVM在JDK/bin目录下。



参考:https://www.cnblogs.com/xingzc/p/5756119.html    java执行原理的详解
          https://www.cnblogs.com/jay36/p/7680008.html    JVM调优
             


VisualVM工具的使用
VisualVM的profile功能:可以提供两方面的性能跟踪功能。
一个是:CPU,可以跟踪每个方法占用CPU的时长;比如你在发现CPU持续走高的时候可以通过Profile的CPU跟踪来确定是哪些函数耗费了性能;
然后在对耗时的方法进行代码优化,从而提升性能。






另一个是内存,内存的Profile通常可以检测到现存的对象都有哪些,占用了多少内存,对象存在多久等信息;通过内存Profile可以发现可疑的内存泄漏对象,进行分析;还可以针对某一个具体的对象进行内存跟踪,只要右键类名称列,有一个“在实例视图中显示”,点击即可切换到跟踪指定对象的内存使用情况。




选中类,筛选出指定的类名称

鼠标右击类,显示在实例视图中显示,就可以查看这个类的都有哪些实例,每个实例的对象情况更等等



点击查找按钮,查出内存中保留的最大对象,觉得那个对象可疑,就点击链接








调试大对象
映入眼帘的左侧菜单的选择,是该对象有多少个实例,


排查JAVA应用程序线程锁
启动 VisualVM,在应用程序窗口,选择对应的JAVA应用,在详情窗口》线程标签(勾选线程可视化),查看线程生命周期状态,主要留意线程生命周期中红色部分,它会监控线程的运行状态(对应RUNNABLE)、休眠(对应NEW)、等待(WAITING+TIMED_WAITING)、驻留(BLOCKED)、监视状态、已完成(TERMINATED)六种状态。
NEW 
A thread that has not yet started is in this state. 
一个还没有开始的线程就处于这种状态。
表明线程尚未开始。
RUNNABLE 
A thread executing in the Java virtual machine is in this state. 
一个在JVM中执行的线程处于这种状态。
可运行线程。一个线程在Java虚拟机处于可运行状态,即它在等待其他资源或操作系统的处理,一旦获取到CPU资源进行了执行,则进入人们所说的子状态RUNNING状态(这个状态JVM并不关心,我们也不是特别关注,一般的JVM监控工具都不会统计这种状态)。
BLOCKED 
A thread that is blocked waiting for a monitor lock is in this state. 
一个被阻塞的线程就处于这种状态,它正在等待监视器锁。 
出于某种原因,比如等待用户输入等而让出当前的CPU给其他的线程执行时,会进入BLOCKED状态。 
BLOCKED状态的线程会一直等待监视器锁,而后执行synchronized代码块/方法。或者在调用Object.wait()后,执行synchronized代码块/方法。
WAITING 
A thread that is waiting indefinitely for another thread to perform a particular action is in this state. 
一个线程正在无限期地等待另一个线程来唤醒是在这种状态下。
通过以下方法进入WAITING状态:
调用Object.wait()且没有超时
调用Thread.join()且没有超时
调用LockSupport.park(Object)
一个线程处于WAITING状态需要由于另一个线程激活。例如,一个线程执行Object.wait()后会等待另一个线程调用对象Object.notify()或Object.notifyAll()。
一个线程调用了另一个线程的Thread.join()方法,则在另一个线程执行后才会继续执行(join方法可以指定延迟执行时间)。
TIMED_WAITING 
A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state. 
一个线程在一个时间阈值内等待另一个线程来唤醒就处于这种状态,达到阈值则回到RUNNABLE状态。
通过以下方法进入TIMED_WAITING状态:
调用Thread.sleep()
调用Object.wait()超过时间阈值
调用Thread.join()超过时间阈值
调用LockSupport.parkNanos(Object,long)
调用LockSupport.parkUntil(Object,long)
TERMINATED 
A thread that has exited is in this state. 
一个线程退出就处于这种状态。
线程执行完成就会变为这个状态。

(1)绿色:代表运行状态。一般属于正常情况。如果是多线程环境,生产者消费者模式下,消费者一直处于运行状态,说明消费者处理性能低,跟不上生产者的节奏,需要优化对应的代码,如果不处理,就可能导致消费者队列阻塞的现象。对应线程的【RUNNABLE】状态。
(2)蓝色:代表线程休眠。线程中调用Thread.sleep()函数的线程状态时,就是蓝色。对应线程的【TIMED_WAITING】状态。
(3)黄色:代表线程等待。调用线程的wait()函数就会出现黄色状态。对应线程的【WAITING】状态。
(4)红色:代码线程锁定。对应线程的【BLOCKED】状态。红色也就是驻留状态。

分析解决JAVA应用程序线程锁
        发生线程锁的原因有很多,我所遇到比较多的情况是多线程同时访问同一资源,且此资源使用synchronized关键字,导致一个线程要等另外一个线程使用完资源后才能运行。例如再没有连接池的情况下,同时访问数据库接口。这种情况会导致性能的极具下降,解决的方案是增加连接池,或者修改访问方式。或者将资源粒度细化,类似ConCurrentHashMap中的处理方式,将资源分为多个更小粒度的资源,在更小粒度资源上来处理锁,就可以解决资源竞争激烈的问题。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值