x 字节码与类的加载中篇

字节码与类的加载
  成员变量(非静态)的赋值过程:
  			1.默认初始化
  			2.显示初始化/代码块中初始化(并列看写的顺序)
  			3.构造器初始化 
  			4.有了对象之后可以对象.属性或对象.方法赋值
  class文件构成
     1.魔术
     2.版本号
     3.常量池表
     4.访问标识
     5.类索引,父类索引,接口索引
     6.字段表集合
     7.方法表集合
     8.属性表集合:源文件名等等。。

 前端编译:
     1.javac xxx.java没有局部变量表
     2.javac -g xxx.java具有局部变量表  eclipse idea都是这个
 javap的用法
   javap <options>	<classes>
         -version                 当前javap所在的jdk的版本信息,不是class在哪个jdk生成的	
         -public     xxxx.class   只显示公共类和成员
         -protected  xxxx.class   显示受保护的/公共类成员
         -p/-private xxxx.class   大于private都会显示
         -package    xxxx.class   显示非私有的信息   
         -sysinfo    xxxx.class   显示正在处理的类的系统信息(路径,大小,日期。。。。。)
         -constants  xxxx.class   显示静态常量
         -s    					  输出内部类型签名(变量的类型boolean Z,char C,类L,方法返回值 参数等 )
         -l 					  输出行号和本地变量表(局部变量表	)
         -c                       代码进行反汇编(主要看方法字节码指令,没有版本,常量池)
         -v /-verbose             行号,本地变量表,反汇编等信息【常用】
         -v -p                    可以看到私有的
 行号表(LineNumberTable):指明字节码指令的偏移量与java源程序中代码的行号的一一对应关系

***java虚拟机指令:操作码+操作数
局部变量压栈指令:
   xload_n(x为i,l,f,d,a)将局部变量表中索引为n的位置上的数据压入操作数栈中n为0-3 n大于四个 xload n【n代表的是本地变量表的索    引】
   【栈中存放的是具体的值,如果是对象就是直接地址,还要注意槽位的复用】
常量入栈指令:根据数据类型不同可分为const系列,push系列和ldc系列
 	 const:用于对特定的常量入栈,入栈的常量隐含在指令本身里
           iconst_<i>(i从-1到5)【i就是代表数值1】iconst_m1将-1压入栈
           lconst_<l>(l从0到1)
           fconst_<f>(f从0到2)
           dconst_<d>(d从0到1)
           aconst_null【引用类型的空值赋值,如果不是就需要特定区new了,这个只能赋null值】
     push:包括bipush和sipush。区别在于接收数据类型的不同,bipush接收8位整数作为参数,sipush接收16位整数,他们将参数压入栈
           【push就是在const指令超过范围的用push bipush 6    bipush范围(	-128,127)   sipush(-32768,32767)】
     ldc:超过以上范围就用ldc,可以接收一个8位的参数,该参数指向常量池中的int、float或者string的【索引】,将指定的内容压入栈中
         ldc_w,它接收两个8位参数,能支持的索引范围大于ldc。可以压入long或者double类型的、
         
出栈装入局部变量表的指令:用于将操作数中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值
		xstore(x为i,l,f,d,a)、xstore_n(n为0至3 是索引)

算术指令:
    用于对两个操作数栈上的值进行某种特定运算,并把结果重新压入操作数栈、分为对整型进行运算与对浮点型进行运算的指令、
    运算模式:4舍5入,向零舍入模式
    NaN:当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义的话,将会使用NaN值来表示,而且所有		  使用NaN值作为操作数的算术操作,结果都会返回NaN【int i=10; double j=i/0.0 Infinity double d1=0.0 double 		  d2=d1/0.0 NaN】
    加法指令:iadd,ladd,fadd,dadd
    减法指令:isub,lsub,fsub,dsub
    乘法指令:imul,lmul,fmul,dmul
    除法指令:idiv,ldiv,fdiv,ddiv
    求余指令:irem,lrem,frem,drem   //remainder:余数
    取反指令:ineg,lneg,fneg,dneg  //negation:取反
    自增指令:iinc
    位运算指令:
          位移指令   :ishl(左移),ishr(右移),iushr(无符号右移),lshl,lshr,lushr
          按位或指令 :ior,lor
          按位与指令 :iand,land
          按位异或指令:ixor,lxor
    比较指令:dcmpg(double compare greter),dcmpl(double compare less),fcmpg,fcmpl,lcmp  比较栈顶两个元素,并将结果			入栈,大于压入1,等于压入0,小于压入-1,遇到NaN时fcmpg会压入1,而fcmpl会压入-1
            数值类型才可以比较大小(byte,short,char,int;long,float,double)

   
操作数栈:(xload操作)
   每当为java方法分配栈帧时,java虚拟机需要开辟一块额外的空间作为操作数栈,来存放计算的操作数及返回结果
   执行每一条指令之前,java虚拟机要求该指令的操作数已被压入操作数栈中,在执行指令时,java虚拟机会将该指令所需要的操作数弹出,并	且将指令的结果重新压入栈中
局部变量表:
   字节码程序可以将计算的结果缓存在局部变量区之中,java虚拟机将局部变量区当成一个数组,依次存放this(仅非静态方法),所传入的参	数,以及字节码中的局部变量4个字节及小于4个字节的(byte,short,int,float,reference)占用1个slot8个字节(long,double)的占    用2个槽位
 
++运算符
    变量不参与运算的时候++i和i++在字节码上是一致的
    参与运算的时候:
      			a=i++ 是先iload把值压入栈,在改变局部变量表中i的值,然后再把栈中的数据赋值到局部变量表a
      			a=++i 是先运算在把i压入栈,然后在赋值到局部变量表
 
类型转换指令:
    1.宽化类型转换(int->float)
       int->long,float,double 指令为:i2l,i2f,i2d
       long->float,double     指令为:l2f,l2d
       float->double          指令为:f2d
       可能会有精度损失,存储数据是也不存在byte short他们都是以int类型存储的
    2.窄化类型转换(强制类型转换)
       从int类型至byte,short,char  指令为:i2b,i2c,i2s
       从long类型到int              指令为:l2i
       从float->int,long           指令为:f2i,f2l
       double->int,long,float      指令为:d2i,d2l,d2f
       没有float,long->byte指令需要借助int去转换float->int->byte
      也会有精度损失、float和double无穷大为Infinity表示,int、long有确定的值;Double.NaN转化为int时为0 
      
        
对象创建与访问指令:
   一、创建指令
      1.创建类实例指令:new 操作数   将索引地址入操作数栈
      2.创建数组指令: newarray(基本类型数组) anewarray(引用类型数组)multianewarray(创建多维数组)
   二、字段访问指令
      1.(static)类字段访问 getstatic putstatic
      2.(非static)类实例字段 getfield,putfield
   三、数组操作指令
      1.xastore:  bastore、castore、sastore、iastore、lastore、fastore、dastore、aastore
          修改堆内的值不是本地变量表中的,执行该命令时操作数栈要弹出:值、索引、数组引用三个
      2.xaload:baload(boolean和byte都用这个)、caload、saload、iaload、laload、faload、daload、aaload
          要求栈顶元素为数组索引i,栈顶顺位第2个元素为数组引用a,该指令弹出两个元素并将a[i]重新入栈	 
      3.取数组长度指令
         arraylength:弹出栈顶数组元素获取数组长度,将长度压入栈
    四、类型检查指令
       检查类实例或数组类型指令:
       instanceof :判断对象是否是某一个类的实例,将结果压入操作数栈
       checkcast  :检查类型强制转换是否可以可以进行,可以进行不会改变操作数栈,否则抛classCastException
方法调用与返回指令  
    1.方法调用指令
       invokevirtual    :调用对象的实例方法,最常见的,动态,支持多态
       invokeinterface  :调用接口方法	
       invokespecial    :调用一些需要特殊处理的实例方法,包括实例初始化方法(构造)、私有方法和父类方法,静态绑定的,不能重写
                           弹出栈顶两个地址值
       invokestatic     :调用命名类中的类方法(static),静态绑定
       invokedynamic    :调用动态绑定方法
    2.方法返回指令:根据返回值类型区分
       ireturn(boolean,byte,short,int,char)
       lreturn
       freturn
       dreturn
       areturn
       return(无返回值)
 操作数栈管理指令
     弹栈:pop,pop2(弹出两个slot)
     复制:dup,dup2(复制2个slot)
     复制插入 :dup_x1(插入位置:1+1=2,即栈顶2个Slot下面),dup2_x1(2+1),dup_x2(1+2),dup2_x2(2+2)
     交换:swap 交换两个slot位置
     占位nop 没有什么特殊作用
 控制转移指令
     条件跳转指令:条件指令通常和比较指令结合使用,在条件跳转指令执行前,一般可以先用比较指令进行栈顶元素的准备然后进行条件跳转
                ifeq(等于0跳转),iflt(小于0跳转),ifle(小于等于0),ifne,ifgt,ifge,ifnull,ifnonnull,这些指令都接收两个				 字节的操作数,用于计算跳转的位置
     比较跳转指令:类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一
                if_icmeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne
                以字符"i"开头的指令对int整数操作(也包括short和byte),以字符a开头表示引用
     多条件分支跳转指令:主要为switch-case涉及到的,主要有tableswitch和loookupswitch
             	tableswitch:case值连续,效率高
             	lookupswitch:case值不连续,会按照case值大小排序,效率比较低;【判断字符串时用hashcode和equals】
      抛出异常指令:
            异常及异常处理:
                 过程一:异常对象的生成过程-->throw(手动/自动)	 --->指令:athrow 
                 过程二:异常的处理:抓抛模型。try-catch-finally --->指令:使用异常表
                       异常表保存信息:
                            起始位置
                            结束位置
                            程序计数器记录的代码处理的偏移地址
                            被捕获的异常类在常量池中的索引
             抛出异常时将结果压入调用者的操作数栈中。
      同步控制指令:方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的
               1.方法级同步:是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作中,ACC_SYNCHRONIZED访问标志得				  知一个方法是否声明为同步方法。
               2.方法内部同步(同步代码块)通过monitorenter和monitorexit两条指令来支持synchronized关键字的语义,当一个线					程进入代码块时使用monitorenter指令请求进入,看计数器是否为0【改变对象头header中的计数器】
                    
*类的加载过程
  类的生命周期
    1.概念:java中数据类型分为基本数据类型和引用数据类型。基本类型由虚拟机预先定义,引用数据类型则需要类的加载
    生命周期:加载->验证->准备->解析->初始化->使用->卸载
    2.Loading(加载):将java类的字节码文件加载到内存中,生成实例,并在内存中构建出java类的模型-类模板对象,反射就是通过类模板					  对象。
     				加载类时虚拟机必须完成3件事:
     				    通过类的全名,获取类的二进制数据流:
     				           1.通过文件系统读入一个class后缀文件
     				           2.读入jar、zip等归档数据包,提取类文件
     				           3.事先存放在数据库中的类的二进制数据
     				           4.使用类似http之类的协议通过网络进行加载
     				           5.在运行时生成一段Class的二进制信息等
     				    解析类的二进制数据流为方法区内的数据结构(java类模型) :存放在方法区中,1.8后在元空间
     				    创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口:在堆中指向方法区
     		数组类的加载:数组类本身并不是由类的加载器负责创建的,而是由jvm在运行时根据需要而直接创建的,但数组的元素类型仍然需						  要依靠类加载器去创建。基本类型数组jvm预先定义的,引用类型加载引用的类
     3.Linking(链接阶段):
           验证(verification):一般与加载同时进行
               格式检查:魔术检查(cafebabe)、版本检查(1.9编译没办法用1.8jre解释运行)、长度检查
               语义检查:是否继承final、是否有父类、抽象方法是否有实现(下面这三个是有了方法区之后进行的)
               字节码验证:跳转指令是否指向正确的位置、操作数类型是否合理 
                         栈映射帧(StackMapTable)就是在这个阶段,用于检测在特定的字节码处,其局部变量和操作数栈是否有着正确						  的数据类型。但不是100%能判断。
               符号引用验证:符号引用的直接引用是否存在。Class文件在其常量池会通过字符串记录自己将要使用的类和方法,因此在验证							阶段,虚拟机就会检查这些类或者方法确实是存在的,此阶段会在解析时执行               
           准备(preparation):为类的静态变量分配内存并将其初始化为默认值(byte/short/int->0,long->0L,float->0.0f								    double->0.0,char->\u0000,boolean->false,reference->null)
             				注意:这里不包括基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配,准									备阶段会显式赋值,准备阶段不会执行代码
           解析(Resolution):将类、接口、字段和方法的符号引用转为直接引用(一般在初始化阶段之后进行的)
     4.Initalization初始化阶段:
           为类的静态变量赋予正确的初始值,即到了初始化阶段,才真正开始执行类中定义的Java程序代码
           初始化阶段的重要工作是执行类的初始化方法:<clinit>()方法,该方法由编译器自动生成并由jvm调用,他是由类静态成员的赋			 值语句以及static语句块合并产生的,父类的clinit总是在子类clinit之前被调用;
          不会产生clinit:
              1.非静态字段不会生成
              2.静态字段没有显式赋值也不会生成
              3.有sttaic final也不会生成,final的在准备阶段显示就赋值了
              4.static final String s0=“helloworld0“在准备阶段赋值    static final String s1=new 							String("helloworld1")是在clinit赋值
            总结:对于基本类型的数据来说,如果使用static final 修饰,则通常是在链接阶段准备环节赋值,对于引用(String)类型来					说static final 要看有没有new关键字,有new是在clinit显式赋值,使用字面量时在准备环节
          clinit的线程安全性:
             clinit方法在多线程环境中被正确地加锁、同步,一个线程调用clinit方法其他线程会阻塞等待
          主动使用与被动使用:
		           主动使用clinit会被调用,被动使用不会被调用 
		           主动:
     				   1.创建实例,new关键字、反射、克隆、反序列化
     				   2.调用类的静态方法,即当使用了字节码invokestatic指令
     				   3.使用类、接口的静态字段时(final修饰特殊考虑)
     				   4.使用java.lang.reflect包中的方法反射类的方法时
     				   5.当初始化子类时,如果父类还没初始化则先初始化父类
                    注意:初始化一个类时要求它的所有父类都已被初始化,当初始化一个类时不会初始化它实现的接口,当初始化一个接口						  时也不会初始化它的父接口
                       6.如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化
                       7.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个类
                       8.当初次调用MethodHandle实例时,初始化该MethodHandle指向的所在的类。(invoke包下的)
                   被动:
                       1.当访问一个静态字段时,只有真正声明这个字段的类才会被初始化
                           当通过子类调用父类的静态变量时,不会导致子类初始化;但是没有初始化的类不意味着没有加载
                       2. 通过数组定义类引用,不会触发此类的初始化
                       3.引用常量不会触发此类或接口的初始化,因为常量在链接阶段准备环节已经被显式赋值了
                       4.调用ClassLoader类的LoadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化
                           ClassLoader.getSystemClassLoader().loadClass("com.atguid.java.Person")
      5.类的Using(使用) 
           调用方法
      6.类的Unloading(卸载)
           一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期
           方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型(类模板)
             常量池回收:
                 1.常量池中的常量没有被任何地方引用,就可以被回收
             类模板回收:
                 1.该类所有的实例已经被回收。
                 2.加载该类的类加载器已经被回收,很难达成
                 3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
           注意:很难卸载
               1.启动类加载器(BootStrap)加载的类型在整个运行期间是不可能被卸载的
               2.被系统类记载器和扩展类加载器加载的类型在运行期也不太可能被卸载
               3.被开发者自定义的类加载器加载的类型	只有在简单的上下文环境中才能被卸载
*类的加载器
  ClassLoader作用:
      ClassLoader是java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进		制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例,然后交给Java虚拟机进行链接、初始化等操作;所以	   ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于他是否可以运行,则	   由Execution Engine决定
   类的显式加载和隐式加载 
      显式:Class.forName();   ClassLoader.getSystemClassLoader.onload();
      隐式:new关键字
   类的唯一性和命名空间
      一个Class文件由两个不同的ClassLoader加载会产生两个不同的类
      每个加载器都有自己的命名空间,命名空间由该类加载器及所有父类加载器所加载的类组成,在同一命名空间中,不会出现两个相同的类,
      在不同的命名空间中,有可能出现两个相同的类
   类加载器分类:
      引导类加载器(Bootstrap 用c/C++实现的)和自定义类加载器(都继承ClassLoader 扩展类加载器,系统类加载器,用户自定义加载器       用java语言实现的)
   启动类加载器:
         1.c/C++实现的,嵌套在JVM内部
         2.加载核心类库  (JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)
         3.并不继承与java.lang.ClassLoder,没有父类加载器
         4.处于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
         5.加载扩展类和应用程序类加载器,并指定为他们的父类加载器
    扩展类加载器:
         1.java语言编写,由sun.misc.Launcher$ExtClassLoader实现
         2.继承ClassLoader类
         3.父类为启动类加载器
         4.从java.ext.dirs系统属性所指定的目录中加载类库,或从jdk的安装目录的jre/lib/ext子目录下载类库。
    应用程序类加载器:
         1.java语言编写,由sun.misc.Launcher$ApplicationClassLoader实现
         2.继承ClassLoader类
         3.父类加载器为扩展类加载器
         4.他负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
         5.应用程序中的类加载器默认是系统类加载器
         6.他是用户自定义类加载器的默认父类加载器
         7.通过ClassLoader的getSsystemClassLoader()方法可以获取到该类加载器
    用户自定义类加载器:
         1.java开发者可以自定义类加载器来实现类库的动态加载,加载资源可以是本地的JAR包,也可以是网络上的远程资源
         2.通过类加载器可以实现非常绝妙的插件机制
         3.自定义加载器能够实现应用隔离,
      
    测试不同的类加载器:
         1.获取当前类的ClassLoader  clazz.getClassLoader()
         2.获取当前线程上下文的ClassLoader clazz.currentThread().getContextClassLoader()
         3.获取系统的ClassLoader  ClassLoader.getSystemClassLoader()
   ClassLoader与现有类加载器的关系:
         abstract ClassLoader -> SecureClassLoader -> URLClassLoader ->ExtClassLoader->AppClassLoader->自定义
   Class.forName和ClassLoader.loadClass()区别
         Class.forName:是一个静态方法,该方法在将Class文件加载到内存的同时,会执行类的初始化。
         ClassLoader.loadClass:实例方法,需要一个ClassLoader对象来调用该方法。该方法将Class文件加载到内存时,并不会执行类   					           初始化,直到这个类第一次使用时才进行初始化 
         
双亲委派机制
   一、优势
       1.避免类的重复加载,确保一个类的全局唯一性
       2.保护程序安全,防止核心API被随意篡改
   二、代码支持
       1.当前类加载器的缓冲中查找有无目标类,如果有直接返回
       2.当前加载器的父类加载器是否为空,如果不为空调用parent.loadClass()接口进行加载
       3.反之,如果当前父类加载器为空,则调用findBootSrapClassOrNull()接口,让引导类加载器进行加载
       4.以上3条路径都没能成功加载,则调用findClass接口进行加载,该接口最终会调用java.lang.ClassLoader 接口的defineClass		  系列的native接口加载目标java类。 
   三、弊端:
       检查类是否加载的委托过程是单向的,但顶层的classLoader无法访问底层的ClassLoader所加载的类
   四、破坏双亲委派机制
      1.第一次破坏
         双亲委派机制在jdk1.2之后引入,所以1.2之前会有子加载器覆盖重写load方法
      2.第二次破坏:线程上下文加载器
        双亲委派机制越基础的类越由上层的加载器进行加载,如果基础类型又要调用回用户的代码,那该怎么办呢? 
        线程上下文加载器就是应用程序类加载器(系统类加载器),这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是		 打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派机制一般原则
      3.第三次破坏
         是由于用户对程序动态性的追求而导致的,如:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等
         
沙箱安全机制
    ·保护程序安全
    ·保护java原生的JDK代码
    java安全模型的核心就是java沙箱(sandbox),就是一个限制程序运行的环境,主要限制资源的访问
    
    jdk1.0本地代码直接调用操作系统和本地资源,远程代码是要依靠沙箱
    jdk1.1改进了 受信任的远程代码可以不用沙箱,不受信任的用沙箱
    jdk1.2代码签名,权限组,不论本地还是远程按照用户设定的权限用沙箱
    jdk1.6引入域的概念,虚拟机会把所有代码加载到不同的系统域和应用域。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通		   过系统域的部分代理来对各种需要的资源进行访问
自定义类的加载器
    1.隔离加载类
       在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如;阿里内某容器框架通过自定义类加载器确保应用中依赖的jar		包不会影响到中间件运行时使用的jar包(类仲裁->类冲突(避免类的冲突))       
    2.修改类加载的方式
       类的加载模型并非强制,除Bootstrap外,其他类的加载并非一定引入,可以动态加载。
    3.扩展加载源
       比如从数据库、网络、甚至是电视机顶盒进行加载。
    4.防止源码泄露
       java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。
  
 实现方式:
    1.重写loadClass()方法
    2.重写findClass()方法[建议使用这个]
 说明:
   自定义的类加载器父类是系统类加载器,jvm中的所有类加载都会使用java.lang.ClassLoader.loadClass(String)接口
java9新特性
   1.扩展机制被移除
      扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform classLoader),可以通过					  ClassLoader.getPlatformClassLoader()获取
   2.平台类加载器和引用程序类加载器都不再继承自java.net.URLClassLoader
     ClassLoader->SecureClassLoader->BuiltinClassLoader->AppClassLoader,PlatformClassLoader,BootClassLoader
   3.java9中,类加载器有了名称
   4.启动类加载器现在是在jvm内部和java类库共同协作实现的类加载器(以前是c++),但为了与之前代码兼容,在获取启动类加载器的场景中仍 	  然会返回null而不会得到BootClassLoader实例
   5.类加载的委派关系也发生了变动
      分模块加载,不同模块加载不同的类
      以前:启动类加载器<-扩展类加载器<-应用程序类加载器<-自定义类加载器
           只有当父类反馈自己无法完成加载时,子加载器才会尝试去加载
      现在:启动类加载器<-平台类加载器<-->应用程序类加载器<-自定义类加载器
            在委派给父类加载器加载前,先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那  		    个模块加载器完成加载
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值