JVM

Java跨平台的原理:
一次编译,到处运行,在虚拟机层面隐藏了底层技术的复杂性以及机器和操作系统的差异性
运行时区域:
1.程序计数器
当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令。
2.Java虚拟机栈
线程私有,每个方法被执行的时候,Java虚拟机会同步创建一个栈帧用来存储局部变量表、操作数栈、动态连接、方法出口等信息。一个方法被调用直至执行完毕的过程,就对应者一个栈帧在虚拟机中入栈到出栈的过程。局部变量表存放了编译期可知的各种Java虚拟机基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址)。这些数据类型在局部变量表中以局部变量槽来表示,其中long和double类型占用两个变量槽,其他的只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法在栈帧中分配多大的局部变量空间是确定的,运行期间不会改变。
3.本地方法栈
发挥作用与虚拟机栈类似,区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
4.堆
线程共享,是虚拟机所管理的内存中最大的一块,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例(and 数组),它是垃圾收集器管理的内存区域,大小可以通过参数-Xmx和-Xms设定。
5.方法区
线程共享,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
6.运行时常量池
是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息之外,还有一项信息是常量池表,用来存放编译器生成的各种字面量和符号引用,这部分内容将在类加载之后存放到方法区的运行时常量池中。并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以把新的常量放入池中,比如String类的intern方法。

对象的创建过程:

  • 当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用所代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
  • 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上就是把一块确定大小的内存块从Java堆中划分出来。此时有两种情况:1.假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,这种分配方式称为“指针碰撞”;2.但如果Java堆中的内存并不是规整的,虚拟机必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”。选择哪种分配方式由Java堆是否规整而定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理的能力决定。
  • 内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值。
  • 接下来,JVM还要对对象进行必要设置,例如这个对象是哪个类的实例、对象哈希吗、对象GC分代年龄等信息。这些信息存放在对象的对象头之中。至此一个新的对象产生了。
  • 对象的创建才刚刚开始–构造函数,即Class文件中的《init》()方法还没执行,所有字段都是默认的零值。一般来说new指令之后会执行《init》方法,这样一个真正可用的对象才算完全被构造出来。

对象的内存布局:
对象头、实例数据、对齐填充
对象头部分包括两类信息:

  • 存储对象自身运行时数据,如哈希吗、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。官方称它为“Mark Word”
  • 类型指针,即对象指向它的类型元数据的指针,JVM以此确定对象是哪个类的实例。

实例数据
是对象真正存储的有效信息,即我们在代码里定义的各种类型的字段内容。HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(普通对象指针),可见相同宽度的字段总是被分配到一起存放,在满足这个的前提下,父类中定义的变量会出现在子类之前。
对齐填充
他不是必然存在的,HotSpot的自动内存管理要求任何对象的大小都必须是8字节的整数倍,如果对象实例数据部分没有对齐的话,就通过对齐填充来补全。
OOM异常:
堆溢出:
只要不断创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除,总容量触及最大堆的容量限制后就会OOM。
虚拟机栈和本地方法栈溢出:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  • 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。

方法区和运行时常量池溢出:

  • String::intern()是一个本地方法,他的作用是如果字符串常量池中已经包含一个等于此String对象的对象,则返回这个字符串的String对象的引用,否则会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。JDK6环境下,产生大量字符串一直intern(),就可以看到OOM: PermGen space(JDK6HotSpot虚拟机用永久代实现方法区)
  • 当产生大量的类时

对象已死?

  • 引用计数算法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。但是无法解决两个对象相互引用的问题。
  • 可达性分析算法:通过一系列称为“GC Root”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用的。

固定作为GC Root的对象包括:
1.在虚拟机栈(栈帧中的本地变量表)中引用的对象
2.在本地方法栈JNI(通常所说的Native方法)引用的对象
3.在方法区中类静态属性引用的对象
4.在方法区中常量引用的对象
5.Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象比如NullPointException、OutOfMemoryError等,还有系统类加载器。
6.所有被同步锁(synchronized关键字)持有的对象。
7.反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地缓存代码等。

引用类型:

  • 强引用是最传统的“引用”的定义,是指在代码中普遍存在的引用赋值,即类似“Object obj = new Object()”这种引用关系,无论任何情况下,只要强引用在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用是用来描述一些还有用,但非必须的对象。“只”被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围进行二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用比软引用强度更弱一点,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉“只”被弱引用关联的对象。
  • 虚引用是最弱的引用关系,一个对象是否有虚引用,对其生存时间完全没有影响,也无法通过虚引用获取一个对象实例。为一个对象设置虚引用的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

生存还是死亡?
即使在可达性分析算法中判定为不可达的对象,也不是非死不可的,要真正宣告一个对象的死亡,至少要经历两次标记过程:

  • 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,则被第一次标记。
  • 随后进行筛选,筛选条件是此对象是否有必要执行finalize()方法,假如对象没有覆盖finalize()方法,或者该方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
    finalize是对象逃脱死亡命运的最后一次机会,如果对象要成功拯救自己,只需与引用链上的任何一个对象建立关联即可,比如把自己(this)赋值给某个类变量或者对象的成员变量,那它在第二次标记时将被移出“即将回收”的集合,但这种自救的机会只有一次。

回收方法区:
方法区的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“java”曾经进入常量池,但是没有任何String对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量,若这时内存回收,且垃圾收集器判断有必要的话,就会被清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也类似。
判定一个常量是否是“废弃常量”简单,而类需要同时满足下面3个条件才能算是“无用的类”

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收,但仅仅是“可以”,不是必然会回收,是否回收由虚拟机的参数决定。在大量使用反射、动态代理、CGLib等字节码框架、动态生成JSP以及OSGi这类频繁自定义类加载器的场景都需要虚拟机具备类卸载的功能,以保证不会对方法区造成过大压力。

分代收集理论:
收集器将Java堆划分出不同的区域,然后将回收对象根据其年龄(熬过垃圾收集过程的次数)分配到不同的区域中存储,垃圾收集器可以每次只回收其中某一个或某些区域,因此才有了“Minor GC”,“Major GC”,“Full GC”的划分。把分代收集理论具体放到现在的商用虚拟机里,设计者一般把堆划分为新生代和老年代两个区域。

  • 跨代引用问题:Minor GC时,新生代中的对象可能被老年代引用,为了找出该区域中存活的对象,不得不在GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性。
  • 解决办法:在新生代建立全局数据结构记忆集Remembered Set,标识出老年代的哪一块内存存在跨带引用,此后当发生Minor GC时,只有包含了跨带引用的小块内存里的对象才会加入GC Roots进行扫描。

标记清除算法:
首先标记处所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,标记过程就是对象是否属于垃圾的判定过程(引用计数、可达性分析),它是最早出现也是最基础的垃圾收集算法。
主要缺点:
1.执行效率不稳定,如果Java堆中包含大量的对象,而且其中大部分是需要被回收的,此时需要进行大量的标记和清除动作,导致这两个过程的执行效率随着对象数量的增长而降低。
2.内存空间的碎片化问题。
标记复制算法:
1969年Fenichel提出“半区复制”的垃圾收集算法,他将可用内存按容量划分为大小相等的两块,每次只使用一块,当这一块的内存用完了,就把存活的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将产生大量内存复制的开销,但对于多数对象都是可回收的情况,实现简单,运行高效,但是缺陷显而易见,这种算法代价是将可用内存缩小了一半,空间浪费太多。
改进版本:1989年,Andrew Appel提出了一种更优化的复制策略,现称为“Appel式回收”。
具体做法:把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存时只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中存活的对象一次性复制到另外一块Survivor上,然后清理掉Eden和那块已使用过的Survivor空间。Eden和Survivor默认比例大小为8比1,也即每次新生代可用内存空间为整个新生代容量的90%。
HotSpot虚拟机的Serial、PartNew等新生代收集器均采用此策略
对象通常在Eden区产生,如果经过第一次MinorGC后仍然存活,并且能被Survivor区容纳的话,对象就被移到Survivor区,并且将年龄设置为一岁。在Survivor区每熬过一次MinorGC,年龄就增加一岁,年龄到一定程度,默认15,就被被晋升到老年代。
空间分配担保
在发生MinorGC之前,虚拟机必须先检查老年代最大的可用的连续空间是否大于新生代所有对象空间,如果这个条件成立,那这次MinorGC是安全的。如果条件不成立,则:
虚拟机会先查看-XX:HandlePromotionFailure参数的值是否允许担保失败,如果允许,那会继续检查老年代的最大可用的连续空间是否大于历次晋升到老年代的平均大小(赌概率的做法),如果大于,将尝试进行一次MinorGC,尽管是有风险的,如果小于或者参数设置不允许冒险,那就进行一次FullGC。
标记整理算法:
针对老年代对象的存亡特征,其标记过程依然和“标记清除算法”一样,但后续不是直接对垃圾对象进行清理,而是让所有存活的对象往内存空间一侧移动,然后清理掉边界以外的内存。
他和标记清除的本质差异是,是否移动回收后的存活对象。
缺点:移动存活对象并更新所有引用这些对象的地方是极为负重的操作,而且移动存活对象必须全程暂停用户线程,需要"Stop The World“。
并发的可达性分析:
可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能进行分析,这意味着必须全程冻结用户线程的执行。但用户线程与收集器怎么并发工作?即收集器在标记,同时用户线程在修改引用关系。
由此产生了两种解决方案:增量更新和原始快照。
CMS是基于增量更新做并发标记的,G1、Shenandoah是用原始快照来实现。
七种经典垃圾回收器:
在这里插入图片描述
%%%%%%%新生代%%%%%%%
Serial收集器:
它是最基础,历史最悠久的基于标记复制的收集器,也是一个单线程工作的收集器,“单线程”说明它只会使用一个处理器或一条线程去完成垃圾收集,并且更重要的是强调它在垃圾收集时必须暂停其他所有工作线程,直至收集结束。
优点:简单高效,对于内存资源受限的环境,它是所有收集器里额外消耗内存最小的;对于单核处理器或处理器核心数较少的环境,Serial由于没有线程交互的开销,专心做垃圾收集可获得更高的收集效率。
ParNew收集器:
ParNew是Serial的并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial完全一致。
它是很多运行在服务端模式下的HotSpot虚拟机,尤其是JDK7之前的遗留系统中首选的新生代收集器,有个很重要的原因是,除了Serial,只有它能和CMS收集器配合使用。
Parallel Scavenge收集器:
同样是基于标记-复制算法实现的并行收集器,它的特点是关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短用户线程的停顿时间,而Parallel Scanvenge是关注吞吐量的收集器。
吞吐量:处理器用于运行用户代码的时间和处理器总消耗时间的比值,它=运行用户代码的时间/(运行用户代码的时间+运行垃圾收集的时间)。
停顿时间越短就越适合需要与用户交互或保证服务响应的程序,如此可提升用户体验;而高吞吐量可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。
自适应调节也是Parallel Scanvenge相较于ParNew的一个重要特性,它可以通过开关参数-XX:+UseAdaptiveSizePolicy开启,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或者最大的吞吐量。

%%%%%%%老年代%%%%%%%
Serial Old收集器:
Serial的老年代版本,单线程收集器,使用标记-整理算法。
Parallel Old收集器:
Parallel Scanvenge收集器的老年代版本,支持多线程并发收集,基于标记-整理实现,同样也是吞吐量优先。
CMS收集器:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现。整个过程分为四个步骤:初始标记,并发标记,重新标记,并发清除。其中初始标记和重新标记仍然需要“Stop The World”。

  • 初始标记:仅仅只是标记一下RC Roots能直接关联到的对象,速度很快。
  • 并发标记:从RC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但不需要停顿用户线程。
  • 重新标记:修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段停顿时间比初始标记稍长,但也远比并发标记时间段。
  • 并发清除:由于不需要移动存活对象,所以可以与用户线程并发。

三个缺点:

  • 对处理器资源非常敏感。并发阶段会因为占用了一部分线程,导致程序变慢,降低总吞吐量。
  • 无法清理“浮动垃圾”。在并发标记和并发清除阶段,用户线程还在运行,程序就会有新的垃圾不断产生,但这部分垃圾是出现在标记过程以后,CMS只能留待下一次垃圾收集再清理掉。
  • 基于标记-清除实现,无法避免大量空间碎片的产生。

G1收集器:
G1是一款主要面向服务端应用的垃圾收集器。作为CMS的替代者,设计者们希望能做出一款建立起“停顿时间模型”的收集器,停顿时间模型是指能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间概览不超过N毫秒这样的目标。
做法:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要扮演新生代的Eden空间、Survivor空间或者老年代空间。G1之所以能建立起可预测的停顿时间模型,是因为它将Region作为单词回收的最小单元,即每次手机到的内存空间都是Region大小的整数倍,这样可以避免在整个Java堆中进行全区域的垃圾收集。更具体的思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后后台维护一个优先级列表,每次根据用户设定的收集停顿时间,优先处理回收价值最大的那些Region。

  • 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短。
  • 并发标记:从GC Roots开始对堆中的对象进行可达性分析,递归扫描对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。对象图扫描完成以后,还要重新处理SATB(原始快照)记录下的在并发时有引用变动的对象。
  • 最终标记:对用户线程做一个短暂的暂停,用于处理并发阶段结束后扔遗留下来的最后那少量的SATB记录。
  • 筛选回收:更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的空间。这里的操作设计对象的移动,必须暂停用户线程,由多条收集器线程并行完成。

可见,除了并发标记外,其余阶段也是要暂停用户线程的。
和CMS的对比:
1.可以指定最大停顿时间、分Region的内存布局,按收益动态确定回收集。
2.与CMS的标记清除不同,G1从整体上来看是基于标记-整理的收集器,但从局部(两个Region之间)看又是基于标记-复制的收集器,这意味着G1不会产生内存空间碎片。
3.弱项是:G1为了垃圾收集产生的内存占用和程序运行时的额外执行负载都比CMS高,虽然G1和CMS都是用卡表来处理跨代指针,但G1的卡表更为复杂。
衡量垃圾收集器的三个指标
内存占用、吞吐量、时间延迟

类文件常量池中主要存放两大类常量:
字面量和符号引用。
字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:

  • 被模块导出或开放的包
  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符
  • 方法句柄和方法类型
  • 动态调用点和动态常量

Java代码在进行编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接,也就是说Class文件不会保存各个方法、字段最终在内存中的布局信息,当虚拟机加载类的时候,将会从常量池中获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中。

类的生命周期:
在这里插入图片描述
类初始化的时机:
在什么情况下需要开始类加载的过程的第一个阶段“加载”,《Java虚拟机规范》没有强行约束,这点可以由虚拟机自由把握。但是对于“初始化”阶段,《Java虚拟机规范》规定了以下六种情况必须进行类的初始化(而加载、验证、准备自然需要在此前开始)。

  • 使用new关键字实例化对象的时候
  • 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
  • 调用一个类型的静态方法的时候
  • 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则先触发其初始化。
  • 初始化类的时候,如果发现其父类还没有进行过初始化,则先触发其父类的初始化
  • 虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个类
  • 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
  • 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化

类加载的过程:
加载:
通过一个类的全限定名来获取定义此类的二进制字节流;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。
验证:
这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机的安全。验证阶段大致上会完成下面四个阶段的检验工作:文件格式验证、元数据验证、字节码验证和符号引用验证。
准备:
正式为类中定义的变量(被static修饰的变量)分配内存并设置类变量初始值(零值)。
解析:
将常量池中的符号引用替换为直接引用。
初始化:
执行类构造器《client》()方法。《client》方法是编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。
类加载器:
“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到JVM外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码称为类加载器。
对于任何一个类,都必须由加载他的类加载器和这个类本身共同确立其在JVM中的唯一性。比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个JVM加载,只要加载它们的类加载器不同,这两个类必定不相等。
双亲委派模型
在这里插入图片描述
引导类加载器:负责加载存放在’<JAVA_HOME’>\lib目录,或者被-Xbootclasspath参数所指定的路径存放的,而且是JVM能够识别的类库加载到虚拟机的内存中。
扩展类加载器:负责加载’<JAVA_HOME’>\lib\ext目录中,或者被java.ext.dirs系统变量指定的路径中所有的类库。
系统类加载器:负责加载用户类路径(Classpath)上所有的类库,如果应用程序没有自定义过自己的类加载器,这个就是默认加载器。
工作过程:
如果一个类加载器收到了类加载器的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成。每个层次的类加载器都是如此,因此所有的加载请求最终都会传送到Bootstrap类加载器(启动类加载器)中,只有父类加载反馈自己无法加载这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
优点:
java类随着它的加载器一起具备了一种带有优先级的层次关系。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值