JVM类加载机制

该文章中的文字部分摘自于周志明版的《深入理解Java虚拟机》

类加载的五大阶段

虚拟机把描述类的数据从.class文件加载(Loading)到内存,并对数据进行验证(Verification)准备(Preparation)解析(Resolution)初始化(Initialization),最终形成可以被虚拟机直接使用的Java类型,这就是类加载(Class Loading)的五大阶段,其中验证、准备和解析三个部分统称为连接(Linking),如下图:

注意:加载、验证、准备和初始化这四个阶段的顺序是确定的,而解析阶段则不一定:它在某些特殊情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定)。

补充:一个类的生命周期在上述五个阶段的基础上再加上使用(Using)和卸载(Unloading)。

下面对五个阶段进行详细的介绍

1、加载

在加载阶段,虚拟机需要完成如下三件事情:

1)、通过一个类的全限定名来获取定义此类的二进制字节流
2)、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)、在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

简单来说,就是:将字节码文件从硬盘加载到方法区,并在堆中创建Class类的对象

相对于类加载过程的其他阶段,加载阶段(准确地说,是上述的1)阶段,放在Java虚拟机外去实现)是开发期可控性最强的阶段,因为加载阶段既可以使用系统提供的类加载器来完成,也可以由用户自定义的类加载器去完成。

当在代码中使用反射代码:Class clazz = Object.class,clazz引用指向的对象就是此时在加载阶段生成的,并且多次调用获取到的引用都是指向堆中同一块内存空间

public class Test001 {
    public static void main(String[] args) throws Exception {
        Class clazz1 = Object.class;
        Class clazz2 = new Object().getClass();
        Class clazz3 = Class.forName("java.lang.Object");
        System.out.println(clazz1); //class java.lang.Object
        System.out.println(clazz2); //class java.lang.Object
        System.out.println(clazz3); //class java.lang.Object
        System.out.println(clazz1 == clazz2); //true
        System.out.println(clazz2 == clazz3); //true
    }
}

Class类的注释也可以说明这一点:类Class没有公共的构造方法,取而代替的是,当类被类加载器加载时,Java虚拟机会自动生成该对象。

类加载器的种类:

  • 启动类加载器(BootstrapClassLoader)负责加载位于JRE的lib目录下的核心类库,如resources.jar、rt.jar,charsets.jar等;
  • 扩展类加载器(ExtClassLoader)负责加载位于JRE\lib\ext目录下的类库;
  • 系统类加载器(AppClassLoader)负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类,以及除了上面两个类加载器负责的类以外的类;
  • 用户自定义加载器(User ClassLoader)负责加载用户自定义路径下的类包;

ExtClassLoader和AppClassLoader都继承自URLClassLoader

位于rt.jar中的sun.misc.Launch类很好的说明了这一点,如下是部分代码:

package sun.misc;

public class Launcher {
    private static String bootClassPath = System.getProperty("sun.boot.class.path");

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            //注意此处,//var1为ExtClassLoader的对象,传入AppClassLoader的构造方法,最终赋值给其parent
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

    }

    static class ExtClassLoader extends URLClassLoader {

        private static File[] getExtDirs() {
            String var0 = System.getProperty("java.ext.dirs");
        }

    }

    static class AppClassLoader extends URLClassLoader {

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
        }

    }

}

第4行,Bootstrap负责加载通过System.getProperty("sun.boot.class.path")方法获取到的目录下的类;
第25行,ExtClassLoader负责加载通过System.getProperty("java.ext.dirs")方法获取到的目录下的类;
第33行,AppClassLoader负责加载通过System.getProperty("java.class.path")方法获取到的目录下的类;

并且还可以从Launcher的构造方法可以看出,ExtClassLoader先于AppClassLoader被加载。如下是一个测试类:

package cn.yxz.classload;

import com.sun.java.accessibility.AccessBridge;

public class ClassLoaderTest {

    public static void main(String[] args) throws Exception {
        //=====================获取某个类对应的Class对象的三种方法-start=====================
        //Class类没有public构造方法,而是当x.class字节码文件被类加载器加载到虚拟机并调用defineClass方法后,x类对应的Class对象由JVM自动创建,位于堆。
        //获取对应类的Class对象的三种方法
        getClass1();
        getClass2();
        getClass3();
        //=====================获取某个类对应的Class对象的三种方法-end=====================

        //=====================获取类的类加载器-start=====================
        //BootstrapClassLoader、ExtClassLoader、AppClassLoader
        testBootstrapClassLoader();
        testExtClassLoader();
        testAppClassLoader();
        //=====================获取类的类加载器-end=====================
    }

    public static void getClass1() {
        Class clazz = Object.class;
        System.out.println(clazz);
    }

    public static void getClass2() throws ClassNotFoundException {
        Class clazz = Class.forName("java.lang.Object");
        System.out.println(clazz);
    }

    public static void getClass3() {
        Object object = new Object();
        Class clazz = object.getClass();
        System.out.println(clazz);
    }

    /**
     * BootstrapClassLoader由C++编写,是最上层的类加载器
     * 通过查看sun.misc.Launcher类中的代码:private static String bootClassPath = System.getProperty("sun.boot.class.path");
     * 它负责加载通过System.getProperty("sun.boot.class.path")方法获取到的目录下的类,
     * 对应本机的目录是:D:\Java\jdk1.8.0_181\jre\lib\resources.jar;D:\Java\jdk1.8.0_181\jre\lib\rt.jar;D:\Java\jdk1.8.0_181\jre\lib\sunrsasign.jar;D:\Java\jdk1.8.0_181\jre\lib\jsse.jar;D:\Java\jdk1.8.0_181\jre\lib\jce.jar;D:\Java\jdk1.8.0_181\jre\lib\charsets.jar;D:\Java\jdk1.8.0_181\jre\lib\jfr.jar;D:\Java\jdk1.8.0_181\jre\classes
     *
     * ExtClassLoader、AppClassLoader位于rt.jar中的sun.misc.Launcher,因此这两个类加载器是由BootstrapClassLoader加载。
     */
    public static void testBootstrapClassLoader() {
        System.out.println("\n========testBootstrapClassLoader-start========");
        System.out.println("负责加载的jar:" + System.getProperty("sun.boot.class.path"));
        System.out.println("Object类加载器:" + Object.class.getClassLoader());
        System.out.println("String类加载器" + String.class.getClassLoader());
        System.out.println("========testBootstrapClassLoader-end========");
    }

    /**
     * ExtClassLoader由Java编写
     * 通过查看源码(sun.misc.Launcher的内部类),它负责加载通过System.getProperty("java.ext.dirs")方法获取到的目录下的类,
     * 对应本机的目录是:D:\Java\jdk1.8.0_181\jre\lib\ext
     *
     * sun.misc.Launcher$ExtClassLoader中的parent为null
     */
    public static void testExtClassLoader() {
        System.out.println("\n========testExtClassLoader-start========");
        System.out.println("负责加载的jar:" + System.getProperty("java.ext.dirs"));
        ClassLoader extClassLoader = AccessBridge.class.getClassLoader();
        System.out.println("AccessBridge类加载器:" + extClassLoader);
        System.out.println("sun.misc.Launcher$ExtClassLoader类加载器:" + extClassLoader.getClass().getClassLoader());
        System.out.println("========testExtClassLoader-end========");
    }

    /**
     * AppClassLoader由Java编写
     * 通过查看源码(sun.misc.Launcher的内部类),它负责加载通过System.getProperty("java.class.path")方法获取到的目录下的类,
     * 除了上面的,其余的都是由AppClassLoader加载
     *
     * sun.misc.Launcher$AppClassLoader中的parent为sun.misc.Launcher$ExtClassLoader,根据如下代码:
     *
     * public Launcher() {
     *     Launcher.ExtClassLoader var1;
     *     try {
     *         var1 = Launcher.ExtClassLoader.getExtClassLoader();
     *     } catch (IOException var10) {
     *         throw new InternalError("Could not create extension class loader", var10);
     *     }
     *     try {
     *         //var1为ExtClassLoader的对象,传入AppClassLoader的构造方法,最终赋值给其parent
     *         this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
     *     } catch (IOException var9) {
     *         throw new InternalError("Could not create application class loader", var9);
     *     }
     * }
     */
    public static void testAppClassLoader() {
        System.out.println("\n========testAppClassLoader-start========");
        System.out.println(System.getProperty("java.class.path"));
        ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println("TestAfter类加载器:" + appClassLoader);
        System.out.println("sun.misc.Launcher$AppClassLoader的parent:" + appClassLoader.getParent());
        System.out.println("sun.misc.Launcher$AppClassLoader类加载器:" + appClassLoader.getClass().getClassLoader());
        System.out.println("========testAppClassLoader-end========");
    }

}

输出结果:

class java.lang.Object
class java.lang.Object
class java.lang.Object

========testBootstrapClassLoader-start========
负责加载的jar:D:\Java\jdk1.8.0_181\jre\lib\resources.jar;D:\Java\jdk1.8.0_181\jre\lib\rt.jar;D:\Java\jdk1.8.0_181\jre\lib\sunrsasign.jar;D:\Java\jdk1.8.0_181\jre\lib\jsse.jar;D:\Java\jdk1.8.0_181\jre\lib\jce.jar;D:\Java\jdk1.8.0_181\jre\lib\charsets.jar;D:\Java\jdk1.8.0_181\jre\lib\jfr.jar;D:\Java\jdk1.8.0_181\jre\classes
Object类加载器:null
String类加载器null
========testBootstrapClassLoader-end========

========testExtClassLoader-start========
负责加载的jar:D:\Java\jdk1.8.0_181\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
AccessBridge类加载器:sun.misc.Launcher$ExtClassLoader@6a8065f
sun.misc.Launcher$ExtClassLoader类加载器:null
========testExtClassLoader-end========

========testAppClassLoader-start========
D:\Java\jdk1.8.0_181\jre\lib\charsets.jar;D:\Java\jdk1.8.0_181\jre\lib\deploy.jar;D:\Java\jdk1.8.0_181\jre\lib\ext\access-bridge-64.jar;D:\Java\jdk1.8.0_181\jre\lib\ext\cldrdata.jar;D:\Java\jdk1.8.0_181\jre\lib\ext\dnsns.jar;D:\Java\jdk1.8.0_181\jre\lib\ext\jaccess.jar;D:\Java\jdk1.8.0_181\jre\lib\ext\jfxrt.jar;D:\Java\jdk1.8.0_181\jre\lib\ext\localedata.jar;D:\Java\jdk1.8.0_181\jre\lib\ext\nashorn.jar;D:\Java\jdk1.8.0_181\jre\lib\ext\sunec.jar;D:\Java\jdk1.8.0_181\jre\lib\ext\sunjce_provider.jar;D:\Java\jdk1.8.0_181\jre\lib\ext\sunmscapi.jar;D:\Java\jdk1.8.0_181\jre\lib\ext\sunpkcs11.jar;D:\Java\jdk1.8.0_181\jre\lib\ext\zipfs.jar;D:\Java\jdk1.8.0_181\jre\lib\javaws.jar;D:\Java\jdk1.8.0_181\jre\lib\jce.jar;D:\Java\jdk1.8.0_181\jre\lib\jfr.jar;D:\Java\jdk1.8.0_181\jre\lib\jfxswt.jar;D:\Java\jdk1.8.0_181\jre\lib\jsse.jar;D:\Java\jdk1.8.0_181\jre\lib\management-agent.jar;D:\Java\jdk1.8.0_181\jre\lib\plugin.jar;D:\Java\jdk1.8.0_181\jre\lib\resources.jar;D:\Java\jdk1.8.0_181\jre\lib\rt.jar;E:\Projects\GIT\java\robot-test\target\classes;D:\Java\IntelliJ IDEA 2019.3.1\lib\idea_rt.jar
TestAfter类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader的parent:sun.misc.Launcher$ExtClassLoader@6a8065f
sun.misc.Launcher$AppClassLoader类加载器:null
========testAppClassLoader-end========

类加载机制

  • 全盘负责委托机制:当一个ClassLoader加载一个类的时候,除非显示的使用另一个ClassLoader,否则该类所依赖和引用的类也由这个ClassLoader载入
  • 父类委派机制:当前类加载器先判断是否加载过该类,是则返回,不是则委托给上级类加载器。同样,上级类加载器先判断是否加载过该类,是则返回,不是则继续委托给上级类加载器。当所有类加载器都未加载过该类时,从上往下依次判断该类是否是自己负责加载,是则加载并返回给下一级,不是则交由下级去判断并加载

当要加载一个类,例如new A(),JVM会调用ClassLoader中的loadClass(加载类)方法,如下是关键代码,下面进行详细介绍

  1. AppClassLoader间接继承ClassLoader,首先调用它的loadClass方法,参数name为类的全限定名(如:cn.yxz.A)。调用findLoadedClass方法,判断AppClassLoader是否加载过A。加载过则loadClass直接返回A的Class对象,未加载过执行下一步(此时是AppClassLoader在工作)
  2. 获取AppClassLoader的父加载器ExtClassLoader,也是先调用它的findLoadedClass方法,判断ExtClassLoader是否加载过A。加载过返回给AppClassLoader,未加载过执行下一步(此时是ExtClassLoader在工作)
  3. 调用ExtClassLoader的findBootstrapClassOrNull方法,判断是否被BootstrapClassLoader加载过以及尝试去加载,代码继续向下(此时是ExtClassLoader在工作)
  4. ExtClassLoader根据步骤3返回的结果,如果Class对象不为空,直接返回给AppClassLoader;为空则调用它的findClass方法,该方法会判断A是否是它负责加载,是则去加载,不是则直接返回null(此时是ExtClassLoader在工作)
  5. 根据步骤4返回的结果,如果Class对象不为空则loadClass方法直接返回,为空则调用它的findClass方法获取Class对象,再最终返回
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            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) {
                c = findClass(name);
            }
        }
        return c;
    }
}

父类委派机制的优势

  • 沙箱安全机制:比如自己写的String.class类不会被加载,这样可以防止核心库被随意篡改
  • 避免类的重复加载:当父ClassLoader已经加载了该类的时候,就不需要子ClassLoader再加载一次

那么沙箱安全机制具体是如何实现的呢?下面自己编写了一个String类,并且包名为java.lang,在里面定义一个main方法并执行,这个时候会出现报错。因为在使用这个类时,会将类加载的动作向上传递,传到启动类加载器时,它发现该类位于rt.jar中,于是从rt.jar中进行加载(注意:这时加载的是系统提供的String类,而不是自定义的),但是发现String类中没有main方法,因此报错,这样就防止了恶意注入。

如下实现一个自定义类加载器,需要继承ClassLoader类,然后提供一个方法,用来将字节码文件从硬盘加载到方法区,并在堆中创建Class类的对象

package cn.yxz.classload.memory.one;

import java.io.FileInputStream;

/**
 * @Author xzyang2
 * @Date 2020/11/3 18:28
 * @Description
 */
public class MyClassLoader extends ClassLoader {

    /**
     * 方法名任意起,一般都命名为defineClass。如下是一种比较粗略的写法
     *
     * 将字节码文件从硬盘加载到方法区,并在堆中创建Class类的对象
     * @param filePath 字节码文件所在的硬盘路径
     * @param className 要加载的类的全限定名
     * @return
     */

    public Class getMyClass(String filePath,String className) {
        try {
            FileInputStream fis = new FileInputStream(filePath);
            int available = fis.available();
            byte[] bytes = new byte[available];
            fis.read(bytes);
            fis.close();
            return defineClass(className,bytes,0,available);
        } catch (Exception e) {
            return null;
        }
    }

}

测试类如下,前提是将编译后的MyClassLoaderBean.class放在硬盘的目录下

package cn.yxz.classload.memory.one;

import cn.yxz.classload.MyClassLoaderBean;
import java.lang.reflect.Constructor;

public class MyClassLoaderTest {

    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader();
        Class myClass = classLoader.getMyClass("F:\\1\\MyClassLoaderBean.class","cn.yxz.classload.MyClassLoaderBean");
        System.out.println(myClass);//输出结果:class cn.yxz.classload.MyClassLoaderBean

        Object bean1 = myClass.newInstance();
        //输出结果为false,因为bean1是由自定义的MyClassLoader类加载器加载,而MyClassLoaderBean是由AppClassLoader加载
        System.out.println(bean1 instanceof MyClassLoaderBean);

        //强制转换会报错,因为括号里的MyClassLoaderBean是由AppClassLoader加载,而bean是由MyClassLoader加载
        //MyClassLoaderBean bean2 = (MyClassLoaderBean) bean;
        //通过构造器的方法赋值
        Constructor constructor = myClass.getConstructor(int.class);
        Object bean2 = constructor.newInstance(1);
        System.out.println(bean2);//输出结果:MyClassLoaderBean{value=1}

    }

}

2、验证

验证是连接阶段的第一步,这一阶段的目的是为了确保.class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。一般情况下分四个阶段的验证过程:文件格式验证、元数据验证、字节码验证和符号引用验证,这个只需简单了解,下面以文件格式验证中的“是否以魔数0xCAFEBABE开头”验证点进行证明。

首先用文本编辑器如Notepad编写一段简单的Java源码,文件名为Test.java,注意不需要包名(这里之所以不用IDEA ,是由于不方便使用Java自带的javac和java命令)

public class Test {
    public static void main(String[] args) {
        System.out.println(1);
    }
}

将该文件放在任意目录下,使用命令“javac Test.java”命令进行编译成.class文件,然后再使用“java Test”运行,输出结果为:1,可以看出这时的.class文件是符合虚拟机规范的,内容如下:

注意看红色框中的内容就是该.class文件的魔数,如果手动删掉该魔数后,再执行“java Test”命令的话就会报如下错误:

可以看出,虚拟机首先会检查.class文件中的前四个字节是否是“CAFEBABE”,如果不是则会报错

3、准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。在这里需要注意:进行内存分配和设置初始值的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

假设有一个类变量的定义为:

public static int value = 1 ;

那么变量value在准备阶段过后的初始值为0而不是1,把value赋值为1的操作存放在类构造器<clinit>()方法中,发生在初始化阶段。

但如果加上final修饰符:

public static final int value = 1 ;

那么编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机将会根据ConstantValue的设置将value直接赋值为1。

因此在平常开发过程中,如果一个静态变量是永久不变的,建议加上final修饰符,这样可以省去在初始化阶段又被赋值一次。

4、解析

待理解

5、初始化

类初始化阶段是类加载过程的最后一步,在前面的类加载过程中,除了在加载阶段用户可以通过自定义类加载器参与之外,其余动作完全由虚拟机主动和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(如static代码块)。

在准备阶段,变量已经赋值过一次系统要求的初始值,而在初始化阶段,会执行类构造器<clinit>()方法,那么<clinit>()方法是怎么生成的呢?

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的。

下面通过一段代码进行验证

public class Test {
    public static int value = 1 ;
    static {
        System.out.println(value);
    }
}

将该Test.class文件通过jad命令进行反编译,结果如下:

可以看出,编译器将value赋值为1的操作放到了static中

对于类加载过程的第一个阶段:加载,虚拟机规范中并没有进行强制约束,可以交给具体的虚拟机自由实现,但对于阶段:初始化,虚拟机规范则是严格规定了有且只有四种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)

  • 使用new关键字实例化对象、读取或者设置一个类的静态字段(被final修饰的除外)、调用一个类的静态方法的时候
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  • 使用java.lang.reflect包的方法对类进行反射调用的时候(最简单的如:Object.class)
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个主类

先说明一点,初始化阶段,是执行类构造器<clinit>()方法的过程,即用static修饰的静态代码块

接下来通过几个简单的事例,对第一和第二种情况进行证明,后两种情况类似

  • 读取一个类的静态字段
public class Dog {
    public static String name = "旺财";
    static {
        System.out.println("Dog init");
    }
}

public class DogTest {
    public static void main(String[] args) {
        System.out.println(Dog.name);
    }
}

输出结果为:
Dog init
旺财

通过输出结果可以看出,在DogTest 类中调用Dog类的静态变量name时,会首先执行DogTest类中的静态代码块。

如果将静态变量name加上final修饰符,那么静态代码块则不会被执行,这是因为虽然引用了Dog类中的常量name,但是在编译阶段将此常量的值“旺财”存储到了DogTest类的常量池中,对常量Dog.name的引用实际都转换为DogTest类对自身常量池的引用了。也就是说实际上DogTes.class文件中并没有Dog类的符号引用入口,这两个类在编译成.class文件后就不存在任何联系了。(其实也好理解,Dog类中的name被定义成了一个常量,那么它的值就不会再改变,当在DogTest中使用时,为了避免引用Dog类带来的损耗,直接将值放在引用它的类中)。不过另外也可以这样理解,静态常量在准备阶段就已经完成了赋值操作,不会放在初始化阶段赋值,因此不会执行static代码块。 

  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化

现在新建一个Animal类,让Dog继承该类,再查看输出结果

public class Animal {
    //是否能飞,默认不能
    public static boolean canFly = false;
    static {
        System.out.println("Animal init");
    }
}

public class Dog extends Animal {
    public static String name = "旺财";
    static {
        System.out.println("Dog init");
    }
}

public class DogTest {
    public static void main(String[] args) {
        System.out.println(Dog.name);
    }
}

这时输出结果为:
Animal init
Dog init
旺财

可以看出当访问Dog类的静态变量name时,如果发现其父类Animal没有被初始化,则进行初始化,然后再执行本身的初始化。

现在将DogTest代码改成如下,通过Dog类访问Animal类的静态变量canFly

public class DogTest {
    public static void main(String[] args) {
        System.out.println(Dog.canFly);
    }
}

输出结果
Animal init
false
请注意,没有输出“Dog init”,对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中的静态字段,只会触发父类的初始化而不会触发子类的初始化

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值