JVM学习笔记1- java类的加载机制

JVM学习笔记1-java类的加载机制

1.什么是类加载(类的初始化)
当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。
类的加载(类初始化):
1、在java代码中,类型的加载、连接、与初始化过程都是在程序运行期间完成的(类从磁盘加载到内存中经历的三个阶段)
2、提供了更大的灵活性,增加了更多的可能性
类加载的注意事项
1、类加载器并不需要等到某个类被 “首次主动使用” 时再加载它关于首次主动使用这个重要概念下文将讲解

2、JVM规范允许类加载器在预料某个类将要被使用时就预先加载它

3、如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
2.类的生命周期
在这里插入图片描述
2、1.加载
在上面已经提到过,加载阶段是类加载的第一个阶段!类的加载过程就是从加载阶段开始~
加载阶段指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象(JVM规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在方法区中),用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class对象, Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

Class对象位置【HotSpot虚拟机】:

在JDK1.7是在方法区中或者说永久代中

在JDK1.8放在方法区或者说元空间中中

JDK1.8移除了永久代,转而使用元空间来实现方法区

永久代和元空间最大的区别: JKD7的永久代放在堆中并且独立于堆,JKD8的元空间完全剥离虚拟机,存在于直接内存中

加载阶段简单来说就是:

.class文件(二进制数据)——>读取到内存——>数据放进方法区——>堆中创建对应Class对象——>并提供访问方法区的接口

2.2验证(了解)
验证是连接阶段的第一阶段,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object之外。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
2、3.准备【重点】
当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。

这里需要注意两个关键点,即内存分配的对象以及初始化的类型。

**内存分配的对象:**要明白首先要知道Java 中的变量有类变量以及类成员变量两种类型,类变量指的是被 static 修饰的变量,而其他所有类型的变量都属于类成员变量。在准备阶段,JVM 只会为类变量分配内存,而不会为类成员变量分配内存。类成员变量的内存分配需要等到初始化阶段才开始(初始化阶段下面会讲到)。
**初始化的类型:**在准备阶段,JVM 会为类变量分配内存,并为其初始化(JVM 只会为类变量分配内存,而不会为类成员变量分配内存,类成员变量自然这个时候也不能被初始化)。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的默认值,而不是用户代码里初始化的值。
2、4.解析
当通过准备阶段之后,进入解析阶段。解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
其实这个阶段对于我们来说也是几乎透明的,了解一下就好。
2、5.初始化【重点】
到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。
Java程序对类的使用方式可分为两种:主动使用与被动使用。一般来说只有当对类的首次主动使用的时候才会导致类的初始化,所以主动使用又叫做类加载过程中“初始化”开始的时机。那啥是主动使用呢?类的主动使用包括以下六种【超级重点】:

1、 创建类的实例,也就是new的方式
2.访问某个类或接口的静态变量,或者对该静态变量赋值(凡是被final修饰不不不其实更准确的说是在编译器把结果放入常量池的静态字段除外)
3、 调用类的静态方法
4、 反射(如 Class.forName(“com.gx.yichun”))
5.初]始化某个类的子类,则其父类也会被初始化
6、 Java虚拟机启动时被标明为启动类的类( JavaTest ),还有就是Main方法的类会首先被初始化
最后注意一点对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),这句话在继承、多态中最为明显!为了方便理解下文会陆续通过例子讲解

类的初始化顺序
类初始化方法:编译器会按照其出现顺序,收集:类变量(static变量)的赋值语句、静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行。
**对象初始化方法:**编译器会按照其出现顺序,收集:成员变量的赋值语句、普通代码块,最后收集构造函数的代码,最终组成对象初始化方法,值得特别注意的是,如果没有监测或者收集到构造函数的代码,则将不会执行对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。
2、6.使用
当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。这个使用阶段也只是了解一下就可以了。
2、7.卸载
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。这个卸载阶段也只是了解一下就可以了。
2、8.结束生命周期
在如下几种情况下,Java虚拟机将结束生命周期

1、 执行了 System.exit()方法 2、 程序正常执行结束 3、 程序在执行过程中遇到了异常或错误而异常终止 4、
由于操作系统出现错误而导致Java虚拟机进程终止

3、接口的加载过程

接口加载过程与类加载过程稍有不同。
当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,当真正用到父接口的时候才会初始化。
4.类加载器
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载入JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。

关于唯一标识符:

在Java中,一个类用其全限定类名(包括包名和类名)作为标识;

但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。

类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例,在虚拟机提供了3种类加载器,启动(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器),如下:在这里插入图片描述
站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:
启动类加载器: BootstrapClassLoader,启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,负责加载存放在 JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader加载)。启动类加载器是无法被Java程序直接引用的。总结一句话:启动类加载器加载java运行过程中的核心类库JRE\lib\rt.jar, sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar 以及存放在JRE\classes里的类,也就是JDK提供的类等常见的比如:Object、Stirng、List…
扩展类加载器: ExtensionClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载 JDK\jre\lib\ext目录中,或者由 java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
应用程序类加载器: ApplicationClassLoader,该类加载器由 sun.misc.LauncherAppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。总结一句话:应用程序类加载器加载CLASSPATH变量指定路径下的类 即指你自已在项目工程中编写的类
==线程上下文类加载器:==除了以上列举的三种类加载器,其实还有一种比较特殊的类型就是线程上下文类加载器。类似Thread.currentThread().getContextClassLoader()获取线程上下文类加载器,线程上下文加载器其实很重要,它违背(破坏)双亲委派模型,很好地打破了双亲委派模型的局限性,尽管我们在开发中很少用到,但是框架组件开发绝对要频繁使用到线程上下文类加载器,如Tomcat等等…
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,因为JVM自带的类加载器(ClassLoader)只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

1、在执行非置信代码之前,自动验证数字签名。 2、动态地创建符合用户特定需要的定制化构建类。 3、从特定的场所取得java
class,例如数据库中和网络中。

需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机默认采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式.

5.java虚拟机入口应用:sun.misc.Launcher

类加载器首先会去到应用程序类加载器 ApplicationClassLoader
小总结:
1、Launcher初始化了ExtClassLoader和AppClassLoader,首先是创建了Extcl扩展类加载器
2、之后的App应用类【系统类】加载器作为Launcher中的一个成员变量,至于为啥不把Extcl扩展类加载器也做为成员变量的原因,大家可以仔细想一想,是为啥呢?其实很简单,因为没必要,因为直接把App系统加载器.parent()方法即可得到Extcl扩展类加载器!
3、Launcher中并没有看见BootstrapClassLoader,但通过System.getProperty(“sun.boot.class.path”)得到了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径。我们可以用输出语句代码测试一下sun.boot.class.path是什么内容,其实就是JRE目录下的jar包或者是class文件。

6.命名空间

命名空间概念: 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。 特别注意:
在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

由子加载器加载的类能看见父加载器的类,由父亲加载器加载的类不能看见子加载器加载的类

7.Java类的加载机制

JVM的类加载机制主要有如下3种。
**全盘负责:**当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
**父类委托:**先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类,通俗讲就是儿子们都他么是懒猪,自己不管能不能做,就算能加载也先不干,先给自己的父亲做,一个一个往上抛,直到抛到启动类加载器也就是最顶级父类,只有父亲做不了的时候再没办法由下一个子类做,直到能某一个子类能做才做,之后的子类就直接返回,实力坑爹!
**缓存机制:**缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

8.双亲委派模型

**双亲委派模型的工作流程是:**如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
双亲委派机制:

1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2、当
ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
3、如果 BootStrapClassLoader加载失败(例如在 $JAVA_HOME/jre/lib里未查找到该class),会使用
ExtClassLoader来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果 AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

在这里插入图片描述
从代码层面了解几个Java中定义的类加载器及其双亲委派模式的实现,它们类图关系如下:
在这里插入图片描述

从图可以看出顶层的类加载器是抽象类ClassLoader类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器),为了更好理解双亲委派模型,ClassLoader源码中的loadClass(String)方法该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作。
既然存在这个双亲委派模型,那么就一定有着存在的意义,其意义主要是: Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
双亲委派模型意义总结来讲就是:

1、系统类防止内存中出现多份同样的字节码

2、保证Java程序安全稳定运行

特别注意数组类型:

1、 数组类的类对象不是由类加载器创建的,而是根据Java运行时的需要自动创建的。
2、 数组类的类加载器getClassLoader()与它的元素类型的类加载器相同;如果元素类型是基本类型,则数组类没有类加载器也就是null,而这个null不同于根类加载器返回的null,它是单纯的null。

9.、ClassLoader

ClassLoader类是一个抽象类,所有的类加载器都继承自ClassLoader(不包括启动类加载器),因此它显得格外重要,分析ClassLoader抽象类也是非常重要的!
1.loadClass
该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作:

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先从缓存查找该class对象,找到就不用重新加载
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      //如果找不到,则委托给父类加载器去加载
                      c = parent.loadClass(name, false);
                  } else {
                  //如果没有父类,则委托给启动加载器去加载
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }

              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // 如果都没有找到,则通过自定义实现的findClass去查找并加载
                  c = findClass(name);

                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              }
          }
          if (resolve) {//是否需要在加载时进行解析
              resolveClass(c);
          }
          return c;
      }
  }

正如loadClass方法所展示的,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载(关于findClass()稍后会进一步介绍)。从loadClass实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass(“className”),这样就可以直接调用ClassLoader的loadClass方法获取到class对象。
2.findClass
在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的(稍后会分析),ClassLoader类中findClass()方法源码如下:
3.defineClass(byte[] b, int off, int len)
defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。
总结
检查完父类加载器之后loadClass会去默认调用findClass方法,父类(ClassLoader)中的findClass方法主要是抛出一个异常。
findClass根据二进制名字找到对应的class文件,返回值为Class对象Class<?>
defineClass这个方法主要是将一个字节数组转换成Class实例,会抛三个异常,但只是threws一个,因为其他两个是运行时异常。
loadClass方法是一个加载一个指定名字的class文件,调用findLoadedClass (String)检查类是否已经加载…如果已经加装就不再加载而是直接返回第一次加载结果 所以一个类只会加载一次
11.自定义类加载器
自定义核心目的是扩展java虚拟机的动态加载类的机制,JVM默认情况是使用双亲委托机制,虽然双亲委托机制很安全极高但是有些情况我们需要自己的一种方式加载,比如应用是通过网络来传输 Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。因此自定义类加载器也是很有必要的。
自定义类加载器一般都是继承自 ClassLoader类,从上面对 loadClass方法来分析来看,我们只需要重写 findClass 方法即可。自定义加载器中点:重写findClass。
12.加载类的三种方式

1、静态加载,也就是通过new关键字来创建实例对象。
2、动态加载,也就是通过Class.forName()方法动态加载(反射加载类型),然后调用类的newInstance()方法实例化对象。
3、动态加载,通过类加载器的loadClass()方法来加载类,然后调用类的newInstance()方法实例化对象

三种方式的区别:
1、第一种和第二种方式使用的类加载器是相同的,都是当前类加载器。(this.getClass.getClassLoader)。而3由用户指定类加载器。
2、如果需要在当前类路径以外寻找类,则只能采用第3种方式。第3种方式加载的类与当前类分属不同的命名空间。
3、第一种是静态加载,而第二、三种是动态加载。
.Class.forName与ClassLoader.loadClass区别

首先,我们必须先明确类加载机制的三个过程主要是:加载 --> 连接 --> 初始化。

Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;

ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

Class.forName(name, initialize,
loader):带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

13.总结

类的加载、连接与初始化:

1、加载:查找并加载类的二进制数据到java虚拟机中
2、 连接:
验证: 确保被加载的类的正确性

准备:为类的静态变量分配内存,并将其初始化为默认值,但是到达初始化之前类变量都没有初始化为真正的初始值(如果是被 final
修饰的类变量,则直接会被初始成用户想要的值。)

解析:把类中的符号引用转换为直接引用,就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程

3、 初始化:为类的静态变量赋予正确的初始值 类从磁盘上加载到内存中要经历五个阶段:加载、连接、初始化、使用、卸载

Java程序对类的使用方式可分为两种

(1)主动使用

(2)被动使用

所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才能初始化他们

主动使用

(1)创建类的实例

(2)访问某个类或接口的静态变量 getstatic(助记符),或者对该静态变量赋值 putstatic

(3)调用类的静态方法 invokestatic

(4)反射(Class.forName(“com.test.Test”))

(5)初始化一个类的子类

(6)Java虚拟机启动时被标明启动类的类以及包含Main方法的类

(7)JDK1.7开始提供的动态语言支持(了解)
被动使用

除了上面七种情况外,其他使用java类的方式都被看做是对类的被动使用,都不会导致类的初始化
初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器(),之后初始化对象构造器()。
初始化类构造器:JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。

初始化对象构造器:JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。值得特别注意的是,如果没有监测或者收集到构造函数的代码,则将不会执行对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。
如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回。如此反复循环,最终返回 main 方法所在类。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值