jvm总结

一、JVM结构

JVM是可运行Java代码的假想计算机

1.1 类加载器

1.2 执行引擎:执行包在装载类的方法中的指令,也就是方法,class文件是否可运行,由执行引擎决定

1.3 本地库接口:融合不同的编程语言为Java所用

1.4 运行数据区

      1.4.1 本地方法栈:登记native方法,在Execution Engine执行时加载native libraies

       1.4.2  程序计数器:每个线程都有一个程序计算器,就是一个指针,指向下一个将要执行的指令代码,由执行引擎读取下一条指令

       1.4.3 方法区:线程共享,存储静态变量、常量、类信息、编译后的字节码

       1.4.4 :线程私有。主管Java程序的运行。生命周期和线程一致,随线程创建时创建,线程结束栈内存释放,不存在垃圾回收问题。

            1)存储:基本类型的变量和对象的引用变量

           2)原理:栈中的数据都是以栈帧(Stack Frame)的格式存在,帧用来存储方法的参数、局部变量和运算过程中的临时数据。当一个方法A被调用时就产生了一 个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入 栈…… 依次执行完毕后,先弹出后进......F3栈帧,再弹出F2栈帧,再弹出F1栈帧。遵循“先进后出”/“后进先出”原则。

       1.4.5 堆:线程共享。jvm区域最大。一个jvm实例只存在一个堆类存,存储初始化对象,成员变量

              1.4.5.1 新生区:类的诞生、成长、消亡的区域。分为伊甸区和幸存区(分为from区、to区)

              1.4.5.2  养老区:保存从新生区筛选出来的 JAVA 对象

              1.4.5.3  永久区:存放JDK自身所携带的 Class,Interface 的元数据。垃圾回收主要两部分内容:废弃的常量和无用的类          

      注意 :Jdk1.6及之前:常量池分配在永久代 。 

                     Jdk1.7:有,但已经逐步“去永久代” 。 

                     Jdk1.8及之后:无(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中),采用了Metaspace     

二、类加载机制

    2.1 类加载过程

         2.2.1 加载:通过全限定名来加载生成class对象到内存中

         2.2.2 验证:验证这个class文件,包括文件格式校验、元数据验证,字节码校验等

        2.2.3 准备:分配内存,为类变量分配(被static修饰的变量,不包括实例变量,实例变量在对象实例化阶段分配)并设置类变量初始值的阶段,假如public static int value = 123;在准备阶段过后的初始值为0而不是123

        2.2.4 解析:将符号引用转化为直接引用(指针引用)

        2.2.5 初始化:为类的静态变量赋予正确的初始值,上述的准备阶段为静态变量赋予的是虚拟机默认的初始值,此处赋予的才是程序编写者为变量分配的真正的初始值

         主动引用(会发生初始化):

  1.  遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。比如以下几种场景:1)、new对象       2)、调用类中的静态成员,除final         3)、调用类中的静态方法   
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,例如通过调用java.lang.Class.forName(String className)
  3. 当初始化一个类的时候,如果发现其父类还没进行过初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。 其实就是public static void main(String[] args)所在的那个类 。

         被动应用(看上去会,其实不会发生初始化):

  1. 通过子类引用父类的静态字段,不会导致子类初始化;
  2. 通过数组定义类引用类,不会触发此类的初始化;
  3. 静态常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

    2.2 双亲委派模型

           2.2.1 概念:一个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载

           2.2.2 优点:例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱

          2.2.3 类加载器

  1. Bootstrap ClassLoader:启动类加载器,负责将$ Java_Home/lib下面的类库加载到内存中(比如rt.jar)
  2. Extension ClassLoader:标准扩展(Extension)类加载器,它负责将$Java_Home /lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中
  3. ApplicationClassLoader:它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器          

   2.3 java反射中,Class.forName和classloader的区别

      java中class.forName()和classLoader都可用来对类进行加载。class.forName()前者除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。而classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static代码块和static方法

三、垃圾回收

     在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中

  3.1  GC判定方法

         1)引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为0就会回收但是JVM没有用这种方式,因为无法判定相互循环引用(A引用B,B引用A)的情况

         2)根搜索算法:通过一种GC ROOT的对象来判断,如果有一条链能够到达GC ROOT就说明,不能到达GC ROOT就说明可以回收

         可以作为GC ROOT的对象有:

         1、栈(栈帧中的本地变量表)中引用的对象。

         2、方法区中的静态成员。

         3、方法区中的常量引用的对象(全局变量)

         4、本地方法栈中JNI(一般说的Native方法)引用的对象。

  3.2 GC收集方法

        1)标记清除:效率不高,会产生碎片

        2)复制算法:解决标记清除效率问题,采用8:1的Eden区和survivor区(伊甸区98%对象会死去,没必要1:1),其中Eden和一块Survivor空间用来存放新生代的对象,还有一块用来复制垃圾回收时Eden和另外一块Survivor的存活对象

        3)标记整理:解决标记清除算法的内存碎片问题,清理完可回收的对象后会对该内存块进行整理

        4)分代收集算法:新生代采用复制算法,老年代采用“标记-清除”或者“标记-整理”算法

 

 它们的区别如下:(>表示前者要优于后者,=表示两者效果一样)

(1)效率:复制算法>标记/整理算法>标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。

(2)内存整齐度:复制算法=标记/整理算法>标记/清除算法。

(3)内存利用率:标记/整理算法=标记/清除算法>复制算法。

  3.3 垃圾收集器

      3.3.1 新生代的垃圾收集器

          1) Serial收集器(复制算法):缺点是垃圾收集器回暂停用户的所有线程来进行单线程的垃圾收集

          2) ParNew收集器(复制算法):Serial的多线程版本,默认开启的线程=CPU核心数,通过使用-XX:ParallelGCThreads参数可以来限制垃圾收集的线程数

         3)Parallel Scavenge收集器(复制算法):跟parNew收集器差不多,但与上面2个收集器的关注点不同,前面的2个更多的关注用户的停顿时间,而此收集器更关注CPU的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。其中有2个参数可以控制这个吞吐量,1、-XX:MaxGCPauseMillis xxxx,控制最大垃圾收集停顿时间(x>0的毫秒数)。2、-XX:GCTimeRatio XXX,设置吞吐量大小(设置一个0<x<100的整数,表示垃圾收集器时间占总时间的比率,相当于吞吐量的倒数)。 另外,此收集器还有一个参数-XX:+UseAdaptiveSizePolicy,如果打开这个开发,则新生代的大小(-Xmn)、Eden、Survivor(-XX:SurvivorRatio)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等参数就不需要手工指定了,虚拟机会根据系统的运行情况来动态调整。

     3.3.2 老年代收集器

    1、Serial Old收集器(标记-整理算法):新生代serial的老年代版本,主要有如下2个用途:1、在JDK1.5以及之前的版本搭配新生代的Parallel Scavenge收集器使用。2、作为CMS收集器发生Concurrent Mode Failure的一个后备方案

    2.Parallel Old收集器(标记-整理算法):新生代Parallel Scavenge收集器的老年代版本,在JDK1.6的版本中才提供,只能和新生代的Parallel Scavenge收集器搭配使用

    3.CMS收集器(标记-清除算法):响应优先。分为初始标记、并发标记、重新标记、并发清除四个过程

       优点:以获取最短回收停顿时间为目标的收集器,具有并发低顿挫的特点

       缺点:1)对CPU资源很敏感(需要跟用户线程并发执行)

               2)无法处理浮动垃圾(Floating Garbage,在标记过程后产生的垃圾),CMS需要预留足够的内存空间给用户线程使用,所以CMS收集器在老年代使用了68%的空间后被激活(可以通过-XX:CMSInitiatingOccupancyFranction参数来配置触发百分比),如果在运行过程中预留的内存无法满足程序需要,则会出现一次“Concurrent Mode Failure”失败并启用后备预案(Serial Old收集器)进行Full GC

               3)采用标记-清除算法,会产生内存碎片,不过可以配置-XX:+UseCMSCompactAtFullCollection开关让Full GC后进行一次碎片整理,也可以使用-XX:CMSFullGCsBeforeCompaction来设置执行多少次Full GC自动来一次碎片整理。

   4.G1收集器:吞吐量优先。在JDK1.6_update14提供试用,是一款面向服务器应用的垃圾收集器

     特点:1)并发运行    2)基于标记-整理算法,不会产生空间碎片

             3)非常精确的控制停顿,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在收集上的时间不超过N毫秒

             4)将整个堆(新生代、老年代)划分为多个大小固定的独立区域,并与这些区域为单位进行垃圾回收,避免对整个堆进行全量回收操作,并对这些区域进行优先级维护和回收

四、JVM调优

  4.1 常用参数设置:

    4.1.1 堆设置

  • -Xmx3550m:设置JVM最大堆内存为3550M。
  • -Xms3550m:设置JVM初始堆内存为3550M。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
  • -Xss128k:设置每个线程的栈大小。JDK5.0以后每个线程栈大小为1M,之前每个线程栈大小为256K。应当根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。需要注意的是:当这个值被设置的较大(例如>2MB)时将会在很大程度上降低系统的性能。
  • -Xmn2g:设置年轻代大小为2G。在整个堆内存大小确定的情况下,增大年轻代将会减小年老代,反之亦然。此值关系到JVM垃圾回收,对系统性能影响较大,官方推荐配置为整个堆大小的3/8。
  • -XX:NewSize=1024m:设置年轻代初始值为1024M。
  • -XX:MaxNewSize=1024m:设置年轻代最大值为1024M。
  • -XX:PermSize=256m:设置持久代初始值为256M。
  • -XX:MaxPermSize=256m:设置持久代最大值为256M。
  • -XX:NewRatio=4:设置年轻代(包括1个Eden和2个Survivor区)与年老代的比值。表示年轻代比年老代为1:4。
  • -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的比值。表示2个Survivor区(JVM堆内存年轻代中默认有2个大小相等的Survivor区)与1个Eden区的比值为2:4,即1个Survivor区占整个年轻代大小的1/6。
  • -XX:MaxTenuringThreshold=7:表示一个对象如果在Survivor区(救助空间)移动了7次还没有被垃圾回收就进入年老代。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代,对于需要大量常驻内存的应用,这样做可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代存活时间,增加对象在年轻代被垃圾回收的概率,减少Full GC的频率,这样做可以在某种程度上提高服务稳定性。

疑问解答:-Xmn,-XX:NewSize/-XX:MaxNewSize,-XX:NewRatio 3组参数都可以影响年轻代的大小,混合使用的情况下,优先级是什么?
如下:

  1. 高优先级:-XX:NewSize/-XX:MaxNewSize 
  2. 中优先级:-Xmn(默认等效  -Xmn=-XX:NewSize=-XX:MaxNewSize=?) 
  3. 低优先级:-XX:NewRatio 

推荐使用-Xmn参数,原因是这个参数简洁,相当于一次设定 NewSize/MaxNewSIze,而且两者相等,适用于生产环境。-Xmn 配合 -Xms/-Xmx,即可将堆内存布局完成。

-Xmn参数是在JDK 1.4 开始支持。

  4.1.2 收集器设置

   -XX:+UseSerialGC:设置串行收集器

  -XX:+UseParallelGC:设置并行收集器

  -XX:+UseParalledlOldGC:设置并行年老代收集器

  -XX:+UseConcMarkSweepGC:设置并发收集器

并行收集器设置

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。

-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间

-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

并发收集器设置

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。

-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

   4.1.3 垃圾回收统计信息

  -XX:+PrintGC:输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]

  -XX:+PrintGCDetails:输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

  -XX:+PrintGCTimeStamps:可与上面两个混合使用,输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

  -Xloggc:filename:把相关日志信息记录到文件以便分

4.2 调优总结

年轻代大小选择

响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。

吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

养老代大小选择

响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

1. 并发垃圾收集信息

2. 持久代并发收集次数

3. 传统GC信息

4. 花在年轻代和年老代回收上的时间比例

减少年轻代和年老代花费的时间,一般会提高应用的效率

吞吐量优先的应用

一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

较小堆引起的碎片问题

因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

1. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。

2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

 

案例一:大型网站服务器案例

承受海量访问的动态Web应用

服务器配置:8 CPU, 8G MEM, JDK 1.6.X

参数方案:

-server -Xmx3550m -Xms3550m -Xmn1256m -Xss128k -XX:SurvivorRatio=6 -XX:MaxPermSize=256m -XX:ParallelGCThreads=8 -XX:MaxTenuringThreshold=0 -XX:+UseConcMarkSweepGC

案例二:内部集成构建服务器案例

高性能数据处理的工具应用

服务器配置:1 CPU, 4G MEM, JDK 1.6.X

参数方案:

-server -XX:PermSize=196m -XX:MaxPermSize=196m -Xmn320m -Xms768m -Xmx1024m

4.3 调试工具

常用的几种内存调试工具(jdk自带,位于bin目录下):jmap、jstack、jconsole、jhat

 详情访问 https://blog.youkuaiyun.com/qq_33314107/article/details/81045318 

五、问题总结

1、进入老年代的几种途径

    1)、在新生代GC过程中survivor空间不够,通过老年代的分配担保机制提前转入老年代。

    2)、超过参数PretenureSizThreshold设置的值的大对象直接进入老年代,所谓大对象就是需要大量连续内存空间的java对象(例如很长的字符串和大数组)。  

    3)、长期存活的新生代对象进入老年代,如果对象在一次Minor GC(新生代GC)后仍存活并且能被survivor容纳后被移动到Survivor中后将该对象的年龄+1,当它的年龄达到一定程     度(默认15)后就会晋升到老年代,这个阀值可以通过-XX:MaxTenuringThreshold来设置。

    4)、虚拟机并不总要求对象达到XX:MaxTenuringThreshold的值后才晋升老年代,如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或者等于      该年龄的对象就可以直接进入老年代。

2、 如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:

              a.Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。

              b.代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。  

     如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。原因有二:

              a. 程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。

              b. 大量动态反射生成的类不断被加载,最终导致Perm区被占满。

3、几种常用的内存调试工具:jmap、jstack、jconsole、jhat;例如当出现了内存溢出时,可以用jmap看内存情况,然后用 jstack主要用来查看某个Java进程内的线程堆栈信息

4.分派:分派是多态性的体现。分为静态分派、动态分派。重载属于静态分派,重写属于动态分派

5. g1和cms区别,吞吐量优先和响应优先的垃圾收集器选择 

    CMS收集器:一款以获取最短回收停顿时间为目标的收集器,是基于“标记-清除”算法实现的,分为4个步骤:初始标记、并发标记、重新标记、并发清除。 

    G1收集器:面向服务端应用的垃圾收集器,过程:初始标记;并发标记;最终标记;筛选回收。整体上看是“标记-整理”,局部看是“复制”,不会产生内存碎片。 

    吞吐量优先的并行收集器:以到达一定的吞吐量为目标,适用于科学技术和后台处理等。

     响应时间优先的并发收集器:保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。 

6. 说一说你对环境变量classpath的理解?如果一个类不在classpath下,为什么会抛出ClassNotFoundException异常,如果在不改变这个类路径的前期下,怎样才能正确加载这个类? 

    classpath是javac编译器的一个环境变量。它的作用与import、package关键字有关。package的所在位置,就是设置CLASSPATH当编译器面对import packag这个语句时,它先会查找CLASSPATH所指定的目录,并检视子目录java/util是否存在,然后找出名称吻合的已编译文件(.class文件)。如果没有找到就会报错! 动态加载包

7.内存溢出和内存泄漏的区别:

  内存溢出:是指内存不足      内存泄漏:是指无法释放已申请的内存空间

8.java对象的四种引用:

    1)强引用:创建一个对象并把这个对象赋给一个引用变量, 只要强引用还在,永远不会回收 

public class Main {  
    public static void main(String[] args) {  
        new Main().fun1();  
    }  
    //语句1、2、3都属于强引用,当程序运行到语句3,内存不过时,jvm会抛出oom异常,不会垃圾回收,只有fun1()方法运行完,才可以回收。
      可以设置值为null,jvm会在合适的时间回收
    public void fun1() {                       
        Object object = new Object();  //语句1
        Object object = "fdfdfd";           //语句2
        Object[] objArr = new Object[1000];  //语句3
 } 

    2)软引用:非必需对象。内存不足时,进行回收 。主要用于缓存

MyObject aRef = new  MyObject(); //强引用
ReferenceQueue queue = new  ReferenceQueue(); //用来管理SoftReference对象,避免大量SoftReference对象回收之后带来的内存泄漏
SoftReference aSoftRef=new SoftReference(aRef,queue); //软引用
aRef =null;
aRef =aSoftRef.get();//在垃圾回收aSoftRef对象时,SoftReference类所提供的get()方法返回Java对象的强引用。回收之后,返回null

    3)弱引用:非必须对象。只要jvm进行垃圾回收,就会回收被弱引用的对象。 

public class test {  
    public static void main(String[] args) {  
        WeakReference<People>reference=new WeakReference<People>(new Strinng("zhouqian"));//这里也可以跟ReferenceQueue结合使用
        //String str =new Strinng("zhouqian");    //语句1
        //WeakReference<People>reference=new WeakReference<People>(str); //语句2
        System.out.println(reference.get());  //返回zhouqian
        System.gc();//通知GVM回收资源  
        System.out.println(reference.get());  //返回null
    }  
    假设是语句1和语句2这种写法,弱引用和强引用结合,被引用对象不会被回收,返回都是zhouqian
}

    4)虚引用:在任何时候都可能被垃圾回收器回收。

import java.lang.ref.PhantomReference;  
import java.lang.ref.ReferenceQueue;  
public class Main {  
    public static void main(String[] args) {  
        ReferenceQueue<String> queue = new ReferenceQueue<String>();  
        PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);  
        System.out.println(pr.get());  //通过get()方法,得到的永远是null
    }  
    //虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值