java虚拟机(JVM)

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

一、虚拟机的组成

虚拟机的组成主要有:方法区、堆都为线程共享区域,有线程安全问题。

                                    栈、本地方法栈、程序计数器为线程的独享区域,不存在线程安全问题。

注:JVM的调优主要是针对堆和栈进行的

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

 1.1:运行时数据区域

java程序在运行时,会为JVM单独划出一块内存区域,而这块内存区域又可以再次划分出一块运行时数据区,运行时数据区域大致可以分为五个部分:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_19,color_FFFFFF,t_70,g_se,x_16watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

 

1.2 :堆(Heap)

“推管存储,栈管运行”;虚拟机栈负责运行代码,而虚拟机堆负责存储数据

JAVA堆的特点:

  • 储存的是new出来的对象,不存放基本类型和对象的引用。(eg:new String("hello"))
  • 由于创建了大量的对象,垃圾回收器主要工作在这块区域。
  • 线程共享区域,因此是线程不安全的。
  • 能够发生OutOfMemoryError(内存溢出)。

注:Java堆区还可以划分为新生代和老年代,新生代又可以进一步划分为Eden区、Survivor 1区、Survivor 2区 

1.2.1堆内存:

Java堆区可以划分为新生代和老年代,新生代又可以进一步划分为Eden区、Survivor 1区、Survivor 2区。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

 

解释:

站在垃圾收集器的角度来看,可以把内存分为新生代与老年代。内存的分配规则取决于当前使用的是哪种垃圾收集器的组合,以及内存相关的参数配置。往大的方向说,对象优先分配在新生代的Eden区域,而大对象直接进入老年代。

第一, 新生代的Eden区域,对象优先分配在该区域,同时JVM可以为每个线程分配一个私有的缓存区域,称为TLAB(Thread Local Allocation Buffer),避免多线程同时分配内存时需要使用加锁等机制而影响分配速度。TLAB在堆上分配,位于Eden中。

第二、新生代的Survivor(缓存)区域。当Eden区域内存不足时会触发Minor GC,也称为新生代GC,在Minor GC存活下来的对象,会被复制到Survivor区域中。我认为Survivor区的作用在于避免过早触发Full GC。如果没有Survivor,Eden区每进行一次Minor GC都把对象直接送到老年代,老年代很快便会内存不足引发Full GC。新生代中有两个Survivor区,我认为两个Survivor的作用在于提高性能,避免内存碎片的出现。在任何时候,总有一个Survivor是empty的,在发生Minor GC时,会将Eden及另一个的Survivor的存活对象拷贝到该empty Survivor中,从而避免内存碎片的产生。

第三、老年代。老年代放置长生命周期的对象,通常是从Survivor区域拷贝过来的对象,不过当对象过大的时候,无法在新生代中用连续内存的存放,那么这个大对象就会被直接分配在老年代上。一般来说,普通的对象都是分配在TLAB上,较大的对象,直接分配在Eden区上的其他内存区域,而过大的对象,直接分配在老年代上。

第四、永久代。如前面所说,在早起的Hotspot JVM中有老年代的概念,老年代用于存储Java类的元数据、常量池、Intern字符串等。在JDK8之后,就将老年代移除,而引入元数据区的概念。

第五、Vritual空间。前面说过,可以使用Xms与Xmx来指定堆的最小与最大空间。如果Xms小于Xmx,堆的大小不会直接扩展到上限,而是留着一部分等待内存需求不断增长时,再分配给新生代。Vritual空间便是这部分保留的内存区域。

 1.2.2:GC类型

  • Minor GC/Young GC:针对新生代的垃圾收集;

  • Major GC/Old GC:针对老年代的垃圾收集。

  • Full GC:针对整个Java堆以及方法区的垃圾收集

                                watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16  

 Minor的工作原理:通常情况下,初次被创建的对象存放在新生代的Eden区,当第一次触发Minor GC,Eden区存活的对象被转移到Survivor区的某一块区域。以后再次触发Minor GC的时候,Eden区的对象连同一块Survivor区的对象一起,被转移到了另一块Survivor区。可以看到,这两块Survivor区我们每一次只使用其中的一块,这样也仅仅是浪费了一块Survivor区。

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

Full GC工作原理:老年代是存储长期存活的对象的,占满时就会触发我们最常听说的Full GC,期间会停止所有线程等待GC的完成。所以对于响应要求高的应用应该尽量去减少发生Full GC从而避免响应超时的问题。

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

1.3:虚拟机栈(JVM Stacks) 

JVM Stacks的特点:

  • Java虚拟机栈是线程私有的,每一个线程都有独享一个虚拟机栈,它的生命周期与线程相同。(新建,就绪,运行,阻塞以及死亡)

  • 虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程     watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_16,color_FFFFFF,t_70,g_se,x_16watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_5,color_FFFFFF,t_70,g_se,x_16watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_15,color_FFFFFF,t_70,g_se,x_16

  •  

    存放基本数据类型(boolean、byte、char、short、int、float、long、double)以及对象的引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)
  • 这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。 

1.4:本地方法栈 

本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

1.5:方法区 

方法区的特点:

  • 线程共享区域,因此这是线程不安全的区域。

  • 它用于存储已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据。

  • 主要存放的是 Class,而堆中主要存放的是实例化的对象。

  • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。

  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。

  • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。

  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:ava.lang.OutofMemoryError:PermGen space 或者java.lang.OutOfMemoryError:Metaspace

    • 加载大量的第三方的jar包

    • Tomcat部署的工程过多(30~50个)

    • 大量动态的生成反射类

  • 关闭JVM就会释放这个区域的内存。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

1.6:程序计数器 

程序计数器是用于存放下一条指令所在单元的地址的地方。当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称为“取指令”。与此同时,PC中的地址或自动加1或由转移指针给出下一条指令的地址。此后经过分析指令,执行指令。完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。

二、类加载过程

1.1:类加载介绍

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

以.java结尾的文件是java的源文件,存放的是我们编写的java源代码。.class结尾的文件是java的字节码文件,里面存放的是我们对java源码编译后产生的二进制代码。

1.编译过程:编译器到底是如何将.java文件“玩转“成.class文件_向上的狼的博客-CSDN博客_java文件转class文件

2.如果 JVM 想要执行这个 .class 文件,我们需要将其装进一个类加载器中,它就像一个搬运工一样,会把所有的 .class 文件全部搬进JVM里面来。

1.2:类加载流程

类加载过程包括:加载、验证、准备、解析、初始化。

解释:加载过程中加载、验证、准备、初始化的流程顺序是确定的,但是解析不确定,因为,解析阶段有可能在初始化之前完成,也有可能在初始化之后完成(这是为了支持Java语言的运行时绑定也称为动态绑定或晚期绑定)

注:这五个阶段只是按照此顺序开始,并不是,按此顺序结束或完成(因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段)

1.3:类的生命周期

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

  • 加载:查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象。
  • 连接:包含验证、准备、解析
  1. 验证:文件格式、元数据、字节码、符号引用验证
  2. 准备:为类的静态变量分配内存,并将其初始化为默认值
  3. 解析:把类中的符号引用转换为直接引用
  • 初始化:为类的静态变量赋予正确的初始值
  • 使用:new出对象程序中使用
  • 卸载:执行垃圾回收

1.4:类加载器 

加载一个Class类的顺序也是有优先级的,顺序为:

  1. BootStrap ClassLoader: ri.jar
  2. Extention ClassLoader: 加载扩展的jar
  3. Application ClassLoader:指定的classpath下面的jar包
  4. Custom ClassLoader:自定义的类加载器

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_17,color_FFFFFF,t_70,g_se,x_16

 三、垃圾回收

1.1:如何确定对象已死

判断一个对象是否被销毁有两种方法:

  1. 引用计数算法:为对象添加一个引用计数器,当该对象被引用时,计数器+1,反之-1,当对象的计数器为0时表示该对象没有被引用。
  2. 可达性分析算法:通过一系列被称之为“GC Roots”的根节点开始,沿着引用链进行搜索,凡是在引用链上的对象都不会被回收。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

 绿色部分的对象都在GC Roots的引用链上,就不会被垃圾回收器回收,灰色部分的对象没有在引用链上,自然就被判定为可回收对象。

1.2:垃圾回收算法

  1. 标记--清除算法:对没有引用的对象进行标记,然后清除。  

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16   2.复制算法:把Java堆分成两块,每次垃圾回收时只使用其中一块,然后把存活的对象全部移动到另一块区域。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

   3.整理算法:标记--整理算法算是一种折中的垃圾收集算法,在对象标记的过程,和前面两个执行的是一样步骤。但是,进行标记之后,存活的对象会移动到堆的一端,然后直接清理存活对象以外的区域就可以了。这样,既避免了内存碎片,也不存在堆空间浪费的说法了。但是,每次进行垃圾回收的时候,都要暂停所有的用户线程,特别是对老年代的对象回收,则需要更长的回收时间,这对用户体验是非常不好的。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_19,color_FFFFFF,t_70,g_se,x_16

 1.3:垃圾收集器

 1.Serial收集器:Serial收集器是最基础、历史最悠久的收集器,是一个单线程工作的收集器,使用 Serial收集器,无论是进行 Minor gc 还是 Full GC ,清理堆空间时,所有的应用线程都会被暂停。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

2.ParNew收集器:ParNew 收集器实质上是 Serial 收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括 Serial 收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一致。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

3.Parallel Scavenge收集器:Parallel Scavenge收集器也是一款新生代收集器,基于标记——复制算法实现,能够并行收集的多线程收集器和 ParNew 非常相似。Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

 4.Serial Old收集器:Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

5.Parallel Old收集器:Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16 6.CMS收集器:CMS 收集器设计的初衷是为了消除 Parallel 收集器和 Serial 收集器 Full gc 周期中的长时间停顿。CMS收集器在 Minor gc 时会暂停所有的应用线程,并以多线程的方式进行垃圾回收。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

 垃圾收集器的对比:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

 四、JVM调优

1.1:选择合适的垃圾回收器

  • CPU单核,那么毫无疑问Serial 垃圾收集器是你唯一的选择。

  • CPU多核,关注吞吐量 ,那么选择PS+PO组合。

  • CPU多核,关注用户停顿时间,JDK版本1.6或者1.7,那么选择CMS。

  • CPU多核,关注用户停顿时间,JDK1.8及以上,JVM可用内存6G以上,那么选择G1。

        ​​​​​​​        ​​​​​​​        ​​​​​​​        watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16 

1.2: 调整内存大小

现象:垃圾收集频率非常频繁。

原因:如果内存太小,就会导致频繁的需要进行垃圾收集才能释放出足够的空间来创建新的对象,所以增加堆内存大小的效果是非常显而易见的。

注意:如果垃圾收集次数非常频繁,但是每次能回收的对象非常少,那么这个时候并非内存太小,而可能是内存泄露导致对象无法回收,从而造成频繁GC。

        ​​​​​​​        ​​​​​​​        ​​​​​​​        watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzU0MDk4OTk0,size_20,color_FFFFFF,t_70,g_se,x_16

1.3:设置符合预期的停顿时间

现象:程序间接性的卡顿

原因:如果没有确切的停顿时间设定,垃圾收集器以吞吐量为主,那么垃圾收集时间就会不稳定。

注意:不要设置不切实际的停顿时间,单次时间越短也意味着需要更多的GC次数才能回收完原有数量的垃圾.

参数配置:

 //GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间
 -XX:MaxGCPauseMillis 

1.4:调整内存区域大小比率

现象:某一个区域的GC频繁,其他都正常。

原因:如果对应区域空间不足,导致需要频繁GC来释放空间,在JVM堆内存无法增加的情况下,可以调整对应区域的大小比率。

注意:也许并非空间不足,而是因为内存泄造成内存无法回收。从而导致GC频繁。

参数配置:

 //survivor区和Eden区大小比率
 指令:-XX:SurvivorRatio=6  //S区和Eden区占新生代比率为1:6,两个S区2:6
 
 //新生代和老年代的占比
 -XX:NewRatio=4  //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2

1.5:调整对象升老年代的年龄

现象:老年代频繁GC,每次回收的对象很多。

原因:如果升代年龄小,新生代的对象很快就进入老年代了,导致老年代对象变多,而这些对象其实在随后的很短时间内就可以回收,这时候可以调整对象的升级代年龄,让对象不那么容易进入老年代解决老年代空间不足频繁GC问题。

注意:增加了年龄之后,这些对象在新生代的时间会变长可能导致新生代的GC频率增加,并且频繁复制这些对象新生的GC时间也可能变长。

配置参数:

//进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,默认值7
 -XX:InitialTenuringThreshol=7 

1.6:调整大对象的标准

现象:老年代频繁GC,每次回收的对象很多,而且单个对象的体积都比较大。

原因:如果大量的大对象直接分配到老年代,导致老年代容易被填满而造成频繁GC,可设置对象直接进入老年代的标准。

注意:这些大对象进入新生代后可能会使新生代的GC频率和时间增加。

配置参数:

 //新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。
  -XX:PretenureSizeThreshold=1000000 

1.7:调整GC的触发时机

现象:CMS,G1 经常 Full GC,程序卡顿严重。

原因:G1和CMS  部分GC阶段是并发进行的,业务线程和垃圾收集线程一起工作,也就说明垃圾收集的过程中业务线程会生成新的对象,所以在GC的时候需要预留一部分内存空间来容纳新产生的对象,如果这个时候内存空间不足以容纳新产生的对象,那么JVM就会停止并发收集暂停所有业务线程(STW)来保证垃圾收集的正常运行。这个时候可以调整GC触发的时机(比如在老年代占用60%就触发GC),这样就可以预留足够的空间来让业务线程创建的对象有足够的空间分配。

注意:提早触发GC会增加老年代GC的频率。

配置参数:

 //使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小
 -XX:CMSInitiatingOccupancyFraction
 
 //G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%
 -XX:G1MixedGCLiveThresholdPercent=65 

1.8:调整 JVM本地内存大小

现象:GC的次数、时间和回收的对象都正常,堆内存空间充足,但是报OOM

原因:JVM除了堆内存之外还有一块堆外内存,这片内存也叫本地内存,可是这块内存区域不足了并不会主动触发GC,只有在堆内存区域触发的时候顺带会把本地内存回收了,而一旦本地内存分配不足就会直接报OOM异常。

注意:本地内存异常的时候除了上面的现象之外,异常信息可能是OutOfMemoryError:Direct buffer memory。解决方式除了调整本地内存大小之外,也可以在出现此异常时进行捕获,手动触发GC(System.gc())。

配置参数:

 XX:MaxDirectMemorySize

 

 

  • 7
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要实现一个JVM虚拟,需要深入了解JVM的内部实现原理和Java语言规范。一般来说,JVM虚拟由以下几个模块组成: 1. 类加载器:负责从文件系统、网络或其他来源加载Java类文件,并将其转换为JVM能够理解的格式。 2. 运行时数据区:Java程序运行时需要的内存空间,包括Java堆、方法区、虚拟栈、本地方法栈、程序计数器等。 3. 执行引擎:负责执行Java字节码,将它们转换为器码并执行。 4. 垃圾收集器:负责回收未使用的对象,释放内存空间。 5. 本地方法接口:允许Java代码调用本地方法(C/C++代码)。 下面是一个简单的Java虚拟实现的示例: ```java public class JVM { private ClassLoader classLoader; private RuntimeDataArea runtimeDataArea; private ExecutionEngine executionEngine; private GarbageCollector garbageCollector; private NativeMethodInterface nativeMethodInterface; public JVM() { classLoader = new ClassLoader(); runtimeDataArea = new RuntimeDataArea(); executionEngine = new ExecutionEngine(); garbageCollector = new GarbageCollector(); nativeMethodInterface = new NativeMethodInterface(); } public void run(String className) { // 加载类 Class clazz = classLoader.loadClass(className); // 初始化类 clazz.initialize(runtimeDataArea); // 执行方法 Method mainMethod = clazz.getMethod("main", String[].class); executionEngine.execute(mainMethod); } } ``` 这个简单的JVM实现只包含了类加载器、运行时数据区和执行引擎三个部分。在实现时,还需要考虑Java语言规范中的各种细节,如异常处理、线程安全等。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值