JVM知识总结

一.什么是JVM

它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。

JRE:Java运行环境,也就是Java平台。所有的Java 程序都要在JRE下才能运行。

JDK:程序开发者用来来编译、调试java程序用的开发工具包,在JDK的安装目录下有一个名为jre的目录

JVM:是JRE的一部分

 

1.内存模型

JVM将内存组织为主内存和工作内存两个部分:主内存主要包括本地方法区和堆。每个线程都有一个工作内存。工作内存中主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)。而虚拟机内存也仅仅是计算机物理内存的一部分(为虚拟机进程分配的那一部分)。

2.重排序

真·重排序:编译器、底层硬件(CPU等)出于“优化”的目的,按照某种规则将指令重新排序(尽管有时候看起来像乱序);伪·重排序:由于缓存同步顺序等问题,看起来指令被重排序了。重排序也是单核时代非常优秀的优化手段,有足够多的措施保证其在单核下的正确性。在多核时代,如果工作线程之间不共享数据或仅共享不可变数据,重排序也是性能优化的利器。然而,如果工作线程之间共享了可变数据,由于两种重排序的结果都不是固定的,会导致工作线程似乎表现出了随机行为。

3.内存屏障

内存屏障可以禁止特定类型处理器的重排序(对于设了屏障的变量不能重排序),从而让程序按我们预想的流程去执行。java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。保证读数据从主内存加载数据,而把最新数据更新写入主内存,让其他线程可见。

 

二.JVM执行过程

 

1. Java类的编译过程

.java源码文件转为 .class二进制字节码文件的过程。词法分析和输入到符号表,注解处理,语义分析和生成字节码。class文件主要有,结构信息:class文件相关信息,元数据:Java源码中的声明和常量信息,方法信息:Java源码语句和表达式对应的字节码

2.类加载机制

虚拟机规范没有指定二进制字节流从哪里读取,可以是class文件,可以是jar,也可以由动态代理在运行时生成,等等,只要是符合规范的字节流即可,由类加载器来决定字节流的来源,通过java本地接口(JNI),找到class文件后并装载进JVM。

1.类加载器

类加载器其实也是Java类。有四大类:

根加载器Bootstrap Class Loader,用c++写的,其他都是java,负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class

扩展加载器Extension Class Loader,负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类

系统应用加载器APP Class Loader,负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CL ASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器

用户自定义加载器Customer Class Loader

 

2.jvm加载机制

主要有3种

全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载


缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因

 

JVM在加载类时默认采用的是双亲委派机制:这样便可以防止核心API库被随意篡改

Java程序在执行前先要检查类是否已经被加载,检查类是否已经被加载,从底层往上层依次检查各个加载器已经加载的类,顺序是系统应用类加载器、扩展加载器、根加载器,一旦发现被某个加载器加载过,则马上使用该类。如果一直找到最顶层的根加载器,发现类还没有被加载进JVM运行数据区的方法区,则接下来就要加载该类

加载过程和检查过程顺序相反,从上层往下层的顺序进行加载。从加载器检查自己的加载路径(某类加载器有一个指定路径去寻找class文件),找要加载的类,一旦找到类就进行加载。越是基础的类,越是被上层的类加载器进行加载,jdk自带的几个jar包肯定是位于最顶级的,再就是我们引用的包,最后是我们自己写的

3. 类执行机制

类在被加载之后,接下来进行连接、初始化,然后才是使用,最后卸载

1.连接

连接(linking)包括三个部分:

验证(verifying):验证类符合Java规范和JVM规范,和编译阶段的语法语义分析不同。

准备(preparing):为类的静态变量分配内存,初始化为系统的初始值。(不初始化静态代码块)。对于final static修饰的变量,直接赋值为用户的定义值。

解析(resolving):将符号引用(字面量描述)转为直接引用(对象和实例的地址指针、实例变量和方法的偏移量)

2.初始化

初始化类的静态变量和静态代码块为用户自定义的值(这里指类的初始化,而不是实例化),也就是说在连接的时候类变量被赋初始值,而类初始化的时候才会对类变量赋用户定义的值,对于实例化的时候才会对成员变量初始化(用户没赋值就系统赋默认值)。一般讲初始化也是实例化,两者没什么区别不分开的,因为对于可以引用一个对象都需要去new一个对象,而这个时候也就包括了类的初始化,平常我们也不可能先去XXX.class先加载类进行初始化在去new对象的,所以两者是一个也不矛盾。

3.内存分配

启动JVM后,操作系统就给JVM分配了内存空间。启动了几个main函数就启动了几个java应用,同时也启动了几个java的虚拟机,一个JVM对应一个堆,当一个线程被创建后,Java栈和PC寄存器就会被创建,Java栈由栈帧组成,调用一个方法,就会生成一个栈帧(可以理解为表示调用一个方法)。栈帧又由局部变量表、操作数栈和动态链接(如:一个常量,并会从常量池里去找),方法出口,其他组成。

当一个类实例化的时候就是在堆内分配内存空间。

4.执行引擎

执行引擎把字节码转为机器码,然后操作系统才可以真正调用,在硬件环境上执行代码。负责执行来自类加载器子系统(class loader subsystem)中被加载类中在方法区包含的指令集,通俗讲就是类加载器子系统把代码逻辑(什么时候该if,什么时候该相加,相减)都以指令的形式加载到了方法区,执行引擎就负责执行这些指令就行了。

解释器:一条一条地读取,解释并且执行字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行的语言的一个缺点。字节码这种“语言”基本来说是解释执行的

即时(Just-In-Time)编译器:即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的

 

三.JVM的体系结构

类装载器(用来装载.class文件),执行引擎(执行字节码,或者执行本地方法),运行时数据区(方法区、堆、java栈、PC寄存器、本地方法栈)。

 

1.PC寄存器(程序计数器)

PC寄存器是用于存储每个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成,线程私有的。

区别于计算机硬件的pc寄存器,两者不略有不同。计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存(一个字长,虚拟机要求字长最小为32位),虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址

2.JVM栈

JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及栈帧,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址

3.堆

它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的

4.方法区域

方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息还有运行时常量池,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。方法区中的类是唯一的。方法区中的类都是运行时的,都是正在使用的,是不能被GC的,所以可以理解成永久代。只加载当前使用的类,也就是边加载边执行

方法区和堆的区别:堆里面放的是这个真正的实例对象,而方法区中存放的是描述类的文字信息;方法区中的类是唯一的,同步的,而在堆中有多个类的实例。

5.运行时常量池

存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配

6.本地方法堆栈

JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。

本地方法库接口:即操作系统所使用的编程语言的方法集,是归属于操作系统的。

本地方法库保存在动态链接库中,即.dll(windows系统)文件中,格式是各个平台专有的,在java代码中会通过System.loadLibrary("")加载c语言库(本地方法库)直接与操作系统平台交互

 

四.JVM垃圾回收

 

1.判断对象是否存活

引用计数算法:给对象中添加一个引用计数器,每当一个地方应用了对象,计数器加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收。但是它很难解决两个对象之间相互循环引用的情况

可达性分析算法:通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即对象到GC Roots不可达),则证明此对象已死、可回收。Java中可以作为GC Roots的对象包括:虚拟机栈中引用的对象、本地方法栈中Native方法引用的对象、方法区静态属性引用的对象、方法区常量引用的对象

总结:在主流的商用程序语言(如我们的Java)的主流实现中,都是通过可达性分析算法来判定对象是否存活的

2.垃圾回收

GC (Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停

对新生代的对象的收集称为minor GC,对旧生代的对象的收集称为Full GC,程序中主动调用System.gc()强制执行的GC为Full GC。

不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:

强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)

软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)

弱引用:在GC时一定会被GC回收

虚引用:由于虚引用只是用来得知对象是否被GC

 

3.垃圾收集算法

1.标记-清除算法

最基础的算法,分标记和清除两个阶段:首先标记处所需要回收的对象,在标记完成后统一回收所有被标记的对象。它有两点不足:一个效率问题,标记和清除过程都效率不高;一个是空间问题,标记清除之后会产生大量不连续的内存碎片(类似于我们电脑的磁盘碎片),空间碎片太多导致需要分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作

2.复制算法

为了解决效率问题,出现了“复制”算法,他将可用内存按容量划分为大小相等的两块,每次只需要使用其中一块。当一块内存用完了,将还存活的对象复制到另一块上面,然后再把刚刚用完的内存空间一次清理掉。这样就解决了内存碎片问题,但是代价就是可以用内容就缩小为原来的一半

3.标记-整理算法

复制算法在对象存活率较高时就会进行频繁的复制操作,效率将降低。因此又有了标记-整理算法,标记过程同标记-清除算法,但是在后续步骤不是直接对对象进行清理,而是让所有存活的对象都向一侧移动,然后直接清理掉端边界以外的内存

4.分代收集算法

当前商业虚拟机的GC都是采用分代收集算法,这种算法并没有什么新的思想,而是根据对象存活周期的不同将堆分为:新生代和老年代,方法区称为永久代(在新的版本中已经将永久代废弃,引入了元空间的概念,永久代使用的是JVM内存而元空间直接使用物理内存)。这样就可以根据各个年代的特点采用不同的收集算法。

新生代又分为Eden区和Survivor区(Survivor from、Survivor to),新生代中的对象“朝生夕死”,每次GC时都会有大量对象死去,少量存活,使用复制算法。老年代中的对象因为对象存活率高、没有额外空间进行分配担保,就使用标记-清除或标记-整理算法。

新产生的对象优先进去Eden区,当Eden区满了之后再使用Survivor from,当Survivor from 也满了之后就进行Minor GC(新生代GC),将Eden和Survivor from中存活的对象copy进入Survivor to,然后清空Eden和Survivor from,这个时候原来的Survivor from成了新的Survivor to,原来的Survivor to成了新的Survivor from。复制的时候,如果Survivor to 无法容纳全部存活的对象,则根据老年代的分配担保(类似于银行的贷款担保)将对象copy进去老年代,如果老年代也无法容纳,则进行Full GC(老年代GC)

4.垃圾收集器

垃圾收集算法是方法论,垃圾收集器是具体实现

1.Serial收集器

Serial收集器是最基本、历史最久的收集器,曾是新生代手机的唯一选择。他是单线程的,只会使用一个CPU或一条收集线程去完成垃圾收集工作,并且它在收集的时候,必须暂停其他所有的工作线程,直到它结束

2.ParNew收集器

ParNew收集器是Serial收集器的多线程版本,除了使用了多线程之外,其他的行为(收集算法、stop the worl d、对象分配规则、回收策略等)同Serial收集器一样。是许多运行在Server模式下的JVM中首选的新生代收集器,其中一个很重还要的原因就是除了Serial之外,只有他能和老年代的CMS收集器配合工作

3.Parallel Scavenge收集器

新生代收集器,并行的多线程收集器。它的目标是达到一个可控的吞吐量(就是CPU运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=行用户代码的时间/[行用户代码的时间+垃圾收集时间]),这样可以高效率的利用CPU时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务

4.Serial Old收集器

Serial 收集器的老年代版本,单线程,“标记整理”算法,主要是给Client模式下的虚拟机使用

5.Parallel Old收集器

Parallel Scavenge的老年代版本,多线程,“标记整理”算法,使“吞吐量优先”收集器终于有了名副其实的组合。在吞吐量和CPU敏感的场合

6.CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。

基于“标记清除”算法,并发收集、低停顿,运作过程复杂,分4步:

1)初始标记:仅仅标记GC Roots能直接关联到的对象,速度快,但是需要“Stop The World”

2)并发标记:就是进行追踪引用链的过程,可以和用户线程并发执行。

3)重新标记:修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要“Stop The World”

4)并发清除:清除标记为可以回收对象,可以和用户线程并发执行

由于整个过程耗时最长的并发标记和并发清除都可以和用户线程一起工作,所以总体上来看,CMS收集器的内存回收过程和用户线程是并发执行的

缺点:并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低;无法处理浮动垃圾(在并发清除时,用户线程新产生的垃圾叫浮动垃圾);产生大量内存碎片,清除后不进行压缩操作产生大量不连续的内存碎片

7.G1收集器

G1是面向服务端应用的垃圾收集器。它的使命是未来可以替换掉CMS收集器。并行与并发:能充分利用多CPU、多核环境的硬件优势,缩短停顿时间;能和用户线程并发执行。

分代收集:G1可以不需要其他GC收集器的配合就能独立管理整个堆,采用不同的方式处理新生对象和已经存活一段时间的对象。

空间整合:整体上看采用标记整理算法,局部看采用复制算法(两个Region之间),不会有内存碎片,不会因为大对象找不到足够的连续空间而提前触发GC,这点优于CMS收集器。

可预测的停顿:除了追求低停顿还能建立可以预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超N毫秒,这点优于CMS收集器

 

5.关于堆内存泄漏

jvm回收机制本质都是判断一个对象是否还被引用,但有些代码让JVM误以为此对象还在引用中,无法回收,造成内存泄漏。所以有了jvm回收机制还是要小心内存泄漏。

内存溢出是指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出,而内存泄漏是指你向系统申请分配内存进行使用,可是使用完了以后却不归还

1.静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

2.各种连接,如数据库连接、网络连接和IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏

3.变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生

4.内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露

5.改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露

 

五.关于堆内存分区

 

1.新生区

新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个:0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园进行垃圾回收(Minor GC),将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1去也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生Major GC(FullGCC),进行养老区的内存清理。若养老区执行Full GC 之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。

如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二: a.Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。 b.代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)

2.养老区

养老区用于保存从新生区筛选出来的 JAVA 对象,一般池对象都在这个区域活跃

3.永久区

永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存

 

 

六.总结

1.编译

创建完源文件之后,程序会先被编译为.class文件,编译后的字节码文件格式主要分为两部分:常量池和方法字节码,常量池记录的是代码出现过的所有token(类名,成员变量名等等)以及符号引用(方法引用,成员变量引用等等);方法字节码放的是类中各个方法的字节码。

2.加载

在应用程序开启的时候,jvm就启动了,然后通过类加载器去寻找main方法所在类.class,找到了并加载到内存里的JVM中的方法区里,方法放进jvm栈,一个方法对应一个栈帧,栈帧里有动态链接就会去方法区里找相应的指令集,再者通过JNI边解释边执行(执行引擎把字节码转为机器码),程序计数器记录所指的指令集位置,计算结果的时候把值入操作数栈。调用本地方法接口,本地方法运行的时候,操纵系统会为本地方法分配本地方法栈,用来储存一些临时变量,然后运行本地方法,调用操作系统API

3.实例

运行到new对象,查看该类是否被加载过,没有就寻找class文件加载到内存,并在堆中分配相应的空间调用构造函数初始化,实例持有着指向方法区的该类的类型信息,如果调用该类的方法x.xxx(),JVM根据该类的引用找到该类对象,然后根据该类对象持有的引用定位到方法区中该类的类型信息的方法表,获得方法()函数的字节码的地址,执行该字节码。

4.回收

当一直创建对象就会有堆空间的不足,就会调用jvm回收机制,在新生区判断哪些对象是否存活,通过垃圾收集器回收无引用对象,清理空间。在垃圾回收机制回收任何对象之前,总会先调用它的finalize()方法,该方法可能使该对象重新复活(让一个引用变量重新引用该对象即obj = this),finalize()方法对于一个对象只会调用一次,finalize()的主要用途是释放一些其他做法开辟的内存空间(垃圾回收器只知道那些显示地经由new分配的内存空间),以及做一些清理工作。finalize()函数是在垃圾回收器准备释放对象占用的存储空间的时候被调用的,绝对不能直接调用finalize(),所以应尽量避免用它(相当于c++的析构函数,只是配合学c++的人好理解,没什么大用)。可以手动调用System.gc执行Full GC,还是希望通过系统判断堆内存不足来调用GC。

 

类加载器加载的类信息放到方法区,执行程序后,方法区的方法压如栈的栈顶,栈执行压入栈顶的方法,遇到new对象的情况就在堆中开辟这个类的实例空间

最后配一张JVM图:

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值