java class文件的加载和双亲委派

java class文件加载过程:jvm把描述的数据从class文件加载(loading)到内存(java方法区)中,中间对数据进行校验(verification)、转换解析(resolution)和初始化(initialization),最终形成可以被jvm直接使用的Java类,这就是class文件的加载。

例如:Student.class,通过class文件的加载,就可以直接通过newInstance创建对象,供jvm使用。

同时jvm把class文件加载到内存中,在jvm中就形成一份描述Class结构的元信息对象(Class对象,存在java堆中),通过该元信息对象就可以获知Class的结构信息,例如:构造函数、属性、方法等,Java也允许用户借由这个元信息对象间接调用Class对象的功能。

例如:
1 Class.forName(“classLoader.ClassLoaderTest”).getClassName();//获取class的名称
2 Class.forName(“classLoader.ClassLoaderTest”).getClassLoader();//获取类加载器
3 Class.forName(“classLoader.ClassLoaderTest”).getMethod();//获取方法
4 User.class.getClassLoader().loadClass(“classLoader.ClassLoaderTest”).getField();//获取属性

class的生命周期

class的生命周期

class的生命周期

类加载的过程包括了:加载、验证、准备、解析、初始化五个阶段。

  1. 加载、验证、准备和初始化这四个阶段发生的顺序是确定的。
  2. 解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。
  3. 另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段,这就有可能出现对象虽然不为null,但是仍存在部分字段没有初始化完全,因此单利模式的double-check-lock的对象需要加volatile修饰。

加载(loading)

主要做了3件事:

  1. 通过一个类的全限定名来获取其定义的二进制字节流。就是常见的Class.forName(className)中的className,一般都是包名类名。
  2. 把该字节流所代表的静态存储结构转化为方法取的运行数据结构,此时相关的class类的相关信息就存储到方法区。把该字节流所代表的静态存储结构转化为方法取的运行数据结构,此时相关的class类的相关信息就存储到方法区。
  3. 根据class文件,在java堆中创建一个该class文件对应的Class对象,作为方法取中数据的访问入口。根据class文件,在java堆中创建一个该class文件对应的Class对象,作为方法取中数据的访问入口。

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载,主要是通过自定义类加载器进行控制。

连接(linking)

连接分为以下几步:验证、准备、解析。

  1. 验证:验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  2. 准备:为类的静态变量分配内存,并将其初始化为默认值。,注意仅对静态变量进行内存分配和初始化值,这个值是系统默认的初始化值,例如:0,0L,"",null等,而不是代码中赋的值。

    示例:public static int value = 3;
    此时 value的值就是系统默认0,而不是代码中赋的值3,3需要到初始化的时候进行复制。当然如果被修饰为final static int value = 3,那么准备结束后,value就是3了,static final常量在编译期就将其结果放入了调用它的类的常量池中。
    还需要注意如下几点:

    1. 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。例如:方法内定义 int i;会提示initialize variable 初始化变量。
    2. 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
    3. 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
    4. 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
  3. 解析:把类中的符号引用转换为直接引用。解析阶段是虚拟机将常量池内符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化(initialization)

为类的静态变量赋予正确的初始值,JVM负责对类进行初始化。例如:static int a = 3;解析后a的值为0,初始化之后a的值为3。

初始化的方式主要有2种:

1. 指定初始值,例如:static int a = 3;直接声明。
2. 通过静态代码块,进行赋值。

例如:static int a;static{ a =3;},初始化之后a的值为3。相当于static代码块就是给static变量初始化值使用,并且只执行一次。

jvm的初始化步骤:

  1. 该类是否被加载过,如果没有,就进行loading,linking后,再初始化。

  2. 如果该类的直接父类没有进行初始化,就先初始化直接父类。

  3. 如果类中有初始化语句(static代码块,或者赋值语句),一次执行初始化语句。

    记住:优先初始化该类的直接父类。

jvm初始化的时机:

  1. 直接创建类的实例,通过new的方式,这是会类初始化,当然除了初始化,还有后续的实例化。

  2. 访问某个类\接口的静态变量(get/set值),此时会直接初始化该静态变量所在的类。注意:即使通过子类访问父类的静态变量,那么也只会初始化父类。

    package classLoader.dynamic;
    
    public class Parent {
    	static String name;
    	static{
    		System.out.println("parent init--befor:"+name);
    		name="123";
    		System.out.println("parent init--after:"+name);
    	}
    	public static void main(String[] args) {
    		System.out.println("print:"+Son.name);
    	}
    }
    
    class Son extends Parent{
    	static{
    		System.out.println("son init");
    	}
    	
    }
    
    输出结果-------
    parent init--befor:null
    parent init--after:123
    print:123
    
    并没有执行Son中的static代码块,这是因为:main方法在Parent中,虽然通过Son进行访问,但是访问的是父类的属性。
    如果main方法在Son中,Son的static就会执行,这是因为Son是main方法的入口,
    需要初始化Son:输出如下:
    parent init--befor:null
    parent init--after:123
    son init
    print:123
    
    

    示例代码二:

    public class ClassLoadTest {
        public static void main(String[] args) {
            System.out.println(Son.name);
            System.out.println("-----");
            System.out.println(Son.sonName);
        }
    }
    
    class Parent {
        static String name;
        static{
            System.out.println("parent init--befor:"+name);
            name="123";
            System.out.println("parent init--after:"+name);
        }
    }
    
    class Son extends Parent{
        static String sonName="1231";
        static{
            System.out.println("son init");
        }
    
    }
    输出内容:
    
    parent init--befor:null
    parent init--after:123
    123
    -----
    son init
    1231
    
    虽然第一次是Son.name,但是是访问父类的参数。Son.sonName就是访问子类了。
    
  3. 调用类的静态方法。

  4. 反射(如Class.forName(“com.shengsiyuan.Test”))。

  5. 初始化某个类的子类,则其父类也会被初始化,这是隐式的初始化。

  6. Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类。

类加载器

把class文件,加载进jvm内存中,需要通过类加载器,jvm的类加载器分为3种(从加载内容的位置分):

  1. 启动类加载器(bootstrap classloader):jvm核心的类加载器,是虚拟机自身的类加载器,无法被Java程序直接引用。负责加载%JAVA_HOME%\jre\lib下的所有jar包,例如:String类的核心jar包,就是有bootstrap classLoader加载的。

  2. 扩展类加载器(extention classLoader):继承自ClassLoader对象,需要由bootstrap classLoader加载后,才能加载其他类,同时该类的父亲就是bootstrap classLoader,负责加载:%JAVA_HOME%\jre\lib\ext可以被Java程序调用,用来加载其他class,注意:两者的父子关系并不是通过继承实现的,而是通过组合实现的,即通过类中的parent属性获取父类。

  3. 应用程序加载器(application classLoader):负责加载classPath指定的类库。

  4. 自定义类加载器(custom classLoader):如果上述类加载器不满足需要,可以自定义classLoader,从指定的位置加载class,同时可以进行一些加载前和加载后的处理,例如:加载前进行解密、不进行class文件的缓存等。自定义类加载器需要继承自ClassLoader,或者继承自一些系统提供给的classLoader,例如:URLClassLoader,只需重写里面的findClass方法即可。

  5. 图例说明:
    不同ClassLoader的加载文件位置

    一般自定义classLoader有如下应用:

    (1)在执行非置信代码之前,自动验证数字签名,比如:那么为了安全需要对class文件加密,那么就可以自定义classLoader进行解密和转化。

    (2) 动态地创建符合用户特定需要的定制化构建类。这个主要是用来动态加载class文件,保证文件的修改可以立刻生效。

    (3) 从特定的场所取得java class,例如数据库中和网络中。

JVM类加载机制

  1. 全盘负责制:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。例如:application classLoader加载User class文件,那么其中User通过继承的父类、实现的接口类、导入的jar包等,加载都是由application classLoader负责。

  2. 父类委托:当前加载器会先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类,这就是类加载的双亲委派。通过父类委托,当appliation加载User class文件时,会优先询问父类是否加载,如果父类没有加载,那么application classLoader就会尝试在classPath路径下加载该类,这同样适用于User继承的父类、实现的接口类等。

类加载机制

  1. 命令行启动应用时候由JVM初始化加载。
  2. 通过Class.forName()方法动态加载,除了加载进内存,同时会进行初始化。
  3. 通过ClassLoader.loadClass()方法动态加载,这种加载只是把class文件加载进内存,并不进行初始化。
    示例:
package classLoader;

public class ClassInit {
	public static void main(String[] args) throws ClassNotFoundException {
		Class.forName("classLoader.Son");//方法一
		ClassLoader.getSystemClassLoader().loadClass("classLoader.Son");//方法二
	}
}

class Son{
	static String name;
	static{
		System.out.println("init-before:"+name);
		name="123";
		System.out.println("init-after:"+name);
	}
}



-------输出结果:
方法一:
	init-before:null
	init-after:123
方法二:
	
方法二并不会初始化,只是把class文件加载,因此没有输出。
当然Class.forName(),也可以通过参数配置,不进行初始化。

双亲委派模型

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

示例:

  1. 当用户加载一个class文件,获取系统的class loader,默认的是Application classLoader,此时Application classLoader先去缓存中,查看该class是否被jvm记录了,有记录就返回,如果没有就请求Extention calssLoader,如果extention classLoader没有加载成功,那么Application classLoader就会去classpath路径下加载,仍未加载成功抛出ClassNotFindException。

  2. Extention classLoader 也不会直接加载class文件,而是交给父加载器bootstrap classLoader去加载,如果bootstrap classLoader没有加载成功,那么extention classLoader就会去%JAVA_HOME%\jre\lib\ext下面查找,成功就加载,否则就返回null,交给Application classLoader加载。

  3. 因为bootstrap classLoader加载器没有父加载器,因此bootstrap classLoader直接在%JAVA_HOME%\jre\lib下,检索该class文件,如果没有就返回null,交给extention classLoader去加载。

双亲委派模型意义:
-系统类防止内存中出现多份同样的字节码
-保证Java程序安全稳定运行

ClassLoader.java的loadClass源码

	public Class<?> loadClass(String name)throws ClassNotFoundException {
            return loadClass(name, false);
    }
    
    protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
            // 首先判断该类型是否已经被加载
            Class c = findLoadedClass(name);
            if (c == null) {
                //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
                try {
                    if (parent != null) {
                         //如果存在父类加载器,就委派给父类加载器加载
                        c = parent.loadClass(name, false);
                    } else {
                    //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
                        c = findBootstrapClass0(name);
                    }
                } catch (ClassNotFoundException e) {
                 // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

自定义类加载器

通常我们使用系统的类加载器(默认为Application classLoader)。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java 类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自 ClassLoader 类,从上面对 loadClass 方法来分析来看,我们只需要重写 findClass 方法即可。
为什么只需要重写findClass方法即可?

ClassLoader源码

protected ClassLoader() {
	this(checkCreateClassLoader(), getSystemClassLoader());
}
//有参构造函数,指定classLoader作为ClassLoader的父加载器。
protected ClassLoader(ClassLoader parent) {
	this(checkCreateClassLoader(), parent);
}
private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    if (ParallelLoaders.isRegistered(this.getClass())) {
        parallelLockMap = new ConcurrentHashMap<>();
        package2certs = new ConcurrentHashMap<>();
        domains =
            Collections.synchronizedSet(new HashSet<ProtectionDomain>());
        assertionLock = new Object();
    } else {
        // no finer-grained lock; lock on the classloader instance
        parallelLockMap = null;
        package2certs = new Hashtable<>();
        domains = new HashSet<>();
        assertionLock = this;
    }
}

自定义ClassLoader

从ClassLoader的loadClass方法中,发现当Bootstrap classLoader、ExtentionClassLoader、Application classLoader、ClassLoader都无法成功加载class文件后,会抛出ClassNotFindException,此时catch后,调用了findClass,因此我们只需要重新findClass,把我们的class文件返回即可,这种写法,并没有破坏双亲委派模型。

自定义ClassLoader示例:

```
	//继承ClassLoader
	 class NetworkClassLoader extends ClassLoader {
	    String host;
	    int port;
		//重写findClass
	    public Class findClass(String name) {
	        byte[] b = loadClassData(name);
	        return defineClass(name, b, 0, b.length);
	    }
		
		//加载Class文件,生成字节数组即可,因此可以在loadClassData()方法中,添加自定义方法即可,例如解密,class文件的获取等。
	    private byte[] loadClassData(String name) {
	        // load the class data from the connection
	         . . .
	    }
	}
```

注意:

  1. 如果是自定义classLoader,尽量不要重写ClassLoader的loadClass方法,因为这会破坏双亲委派。
  2. 如果是加载本地class,该class文件不要放在classpath中,因为双亲委派,application classLoader会提前加载。
  3. findClass(String namee)方法的name尽量按照全路径(包名+类名),因为defineClass方法是按照这种方式处理,同时全路径也能解决class文件的缓存的唯一性。

说明:

  1. 如果想更好的了解自定义classLoader,可以参考URLClassLoader,根据url地址,加载指定名称的class文件。
  2. class文件加载进jvm中,判断Class对象的唯一性,就依靠,loadClass(String name)和使用的类加载器,两者全部相同时,才认为是同一个Class,例如:com.classLoader.A.class和com.classLoader.B.class就不是同一个Class对象;由Application classLoader加载的com.classLoader.A.class和Extention classLoader加载的com.classLoader.A.class同样不是同一个Class对象,当然实际情况应该是不同的自定义加载器,加载同一个class文件,但是Class对象却不相同。

破坏双亲指派模型

正常的自定义ClassLoader也是符合双亲委派模型,但是如果需要真的需要破坏双亲委派模型,我们就需要重写loadClass,取消parent的加载,这样就破坏了双亲委派模型。为什么要破坏双亲委派模型:

双亲委派模型的第二次“被破坏”是这个模型自身的缺陷所导致的,双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢。
这并非是不可能的事情,一个典型的例子便是JNDI服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?
为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。了有线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。

理解一下为什么JDBC需要破坏双亲委派模式,原因是原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。例如,MySQL的mysql-connector-java.jar中的Driver类具体实现的。 原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的,在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-java.jar中的Driver类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载。于是乎,这个时候就引入线程上下文件类加载器(Thread Context ClassLoader)。有了这个东西之后,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。下面看看JDBC中是怎么去应用的呢。

DriverManger的源码:DriverManage是rt.jar包中的,因此需要启动加载器加载(bootstrapt classLoader)加载,但是发现在DriverManger中的getConnection中,获取不同的Driver的Class.for指定了加载器。

	
//  Worker method called by the public getConnection() methods.
private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
    /*
     * When callerCl is null, we should check the application's
     * (which is invoking this class indirectly)
     * classloader, so that the JDBC driver class outside rt.jar
     * can be loaded from here.
     */
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized(DriverManager.class) {
        // synchronize loading of the correct classloader.
        if (callerCL == null) {
        	// 传说中的上下文加载器,默认取父线程的,父线程为null,默认为应用加载器(application classloader)
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }

    if(url == null) {
        throw new SQLException("The url cannot be null", "08001");
    }

    println("DriverManager.getConnection(\"" + url + "\")");

    // Walk through the loaded registeredDrivers attempting to make a connection.
    // Remember the first exception that gets raised so we can reraise it.
    SQLException reason = null;

    for(DriverInfo aDriver : registeredDrivers) {
        // If the caller does not have permission to load the driver then
        // skip it.
        if(isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                println("    trying " + aDriver.driver.getClass().getName());
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
                    // Success!
                    println("getConnection returning " + aDriver.driver.getClass().getName());
                    return (con);
                }
            } catch (SQLException ex) {
                if (reason == null) {
                    reason = ex;
                }
            }

        } else {
            println("    skipping: " + aDriver.getClass().getName());
        }

    }

    // if we got here nobody could connect.
    if (reason != null)    {
        println("getConnection failed: " + reason);
        throw reason;
    }

    println("getConnection: no suitable driver found for "+ url);
    throw new SQLException("No suitable driver found for "+ url, "08001");
}

private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
    boolean result = false;
    if(driver != null) {
        Class<?> aClass = null;
        try {
        	// 指定加载器,此时只是启动加载器加载该class文件,但是启动加载器并没有直接加载,而是交给了一个classLoader,通过查看classLoader并不是一个启动加载器
            aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
        } catch (Exception ex) {
            result = false;
        }

         result = ( aClass == driver.getClass() ) ? true : false;
    }

    return result;
}

其实在DriverManager的静态方法中

/**
 * Load the initial JDBC drivers by checking the System property
 * jdbc.properties and then use the {@code ServiceLoader} mechanism
 */
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

// 加载用户配置的driver
private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    // If the driver is packaged as a Service Provider, load it.
    // Get all the drivers through the classloader
    // exposed as a java.sql.Driver.class service.
    // ServiceLoader.load() replaces the sun.misc.Providers()

    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            /* Load these drivers, so that they can be instantiated.
             * It may be the case that the driver class may not be there
             * i.e. there may be a packaged driver with the service class
             * as implementation of java.sql.Driver but the actual class
             * may be missing. In that case a java.util.ServiceConfigurationError
             * will be thrown at runtime by the VM trying to locate
             * and load the service.
             *
             * Adding a try catch block to catch those runtime errors
             * if driver not available in classpath but it's
             * packaged as service and that service is there in classpath.
             */
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
        }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            // 加载,还是通过系统加载器进行加载,bootstrapt classloader 加载的东西,还需要application classloader加载,这就破坏了双亲委派
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

参考文章:

  1. http://www.cnblogs.com/ityouknow/p/5603287.html。
  2. http://www.importnew.com/18548.html
  3. http://blog.csdn.net/u013256816/article/details/50837863
    2和3的参考文章,其中关于子父类的static代码块、构造代码块、代码块的执行顺序有很好的解说。
  4. https://www.jianshu.com/p/60dbd8009c64 关于双亲委派的破坏,文章说的非常好。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值