JVM之类加载篇

类加载

1. 类加载的过程

1.1 加载

  • 将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

    • _java_mirror:java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用
    • _super:父类
    • _fields:成员变量
    • _methods:方法
    • _constants:常量池
    • _class_loader:类加载器
    • _vtable:虚方法表
    • _itable:接口方法
  • 如果这个类还有父类没有加载,先加载父类

  • 加载和链接可能是交替运行

    img
  • instanceKlass保存在方法区。JDK 8以后,方法区位于元空间中,而元空间又位于本地内存中

  • _java_mirror则是保存在堆内存

  • InstanceKlass和*.class(JAVA镜像类)互相保存了对方的地址

  • 类的对象在对象头中保存了*.class的地址。让对象可以通过其找到方法区中的instanceKlass,从而获取类的各种信息

  • 加载Class文件的方式:

    • 从本地系统中直接加载
    • 通过网络获取,典型场景:Web Applet
    • 从zip压缩包中读取,成为日后jar、war格式的基础
    • 运行时计算生成,使用最多的是:动态代理技术
    • 由其他文件生成,典型场景:JSP应用从专有数据库中提取.class文件,比较少见
    • 从加密文件中获取,典型的防Class文件被反编译的保护措施

1.2 链接

1.2.1 验证
  • 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。如果出现不合法的字节码文件,那么将会验证不通过。
  • 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
1.2.2 准备
  • 为 static 变量分配空间,设置默认值
    • static变量在JDK 7以前是存储于instanceKlass末尾。但在JDK 7以后就存储在_java_mirror末尾了
    • static变量在分配空间和赋值是在两个阶段完成的。分配空间在准备阶段完成,赋值在初始化阶段完成
    • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
    • 如果 static 变量是 final 的,但属于引用类型,那么赋值会在初始化阶段完成
1.2.3 解析
  • 解析就是将常量池内的符号引用转换为直接引用的过程。
  • 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
  • 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等

1.3 初始化

  • 初始化阶段就是执行类构造器clinit()方法的过程,虚拟机会保证这个类的**"构造方法"的线程安全**

    • clinit()方法不需定义,是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。
      • 也就是说,当我们代码中包含static变量的时候,就会有clinit方法
  • 注意:编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

    //案例
    public class Test(){
        static {
            i = 0;//给变量赋值可以正常编译通过
            System.out.println(i);//这句编译器会提示"非法向前引用"
        }
        static int i = 1;
    }
    
  • 初始化发生的时机:

    • 类的初始化的懒惰的,以下情况会初始化:

      • main 方法所在的类,总会被首先初始化

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

      • 首次访问这个类的静态变量或静态方法时

        public class Test {
            public static void main(String[] args) {
                System.out.println(A.i);
            }
        }
        class A{
            static{
                System.out.println("A init...");
            }
            static int i = 10;
        }
        

        在这里插入图片描述

      • 子类初始化,如果父类还没初始化,会引发父类的初始化

        public class Test {
        
            public static void main(String[] args) {
                int a = B.j;
            }
        }
        class A{
            static{
                System.out.println("A init...");
            }
            static int i = 10;
        }
        class B extends A{
            static {
                System.out.println("B init...");
            }
            static int j = 20;
        }
        

        在这里插入图片描述

      • 子类访问父类的静态变量,只会触发父类的初始化

        public class Test {
        
            public static void main(String[] args) {
                int a = B.i;//子类访问父类的静态变量
            }
        }
        class A{
            static{
                System.out.println("A init...");
            }
            static int i = 10;
        }
        class B extends A{
            static {
                System.out.println("B init...");
            }
            static int j = 20;
        }
        

        在这里插入图片描述

      • Class.forName

        public class Test {
        
            public static void main(String[] args) throws ClassNotFoundException {
                Class.forName("com.suqu.test.A");//全限定类名
            }
        }
        class A{
            static{
                System.out.println("A init...");
            }
            static int i = 10;
        }
        class B extends A{
            static {
                System.out.println("B init...");
            }
            static int j = 20;
        }
        

        在这里插入图片描述

      • new 会导致初始化

        public class Test {
        
            public static void main(String[] args) {
                A a = new A();
            }
        }
        class A{
            static{
                System.out.println("A init...");
            }
            static int i = 10;
        }
        

        在这里插入图片描述

    • 以下情况不会初始化:

      • 访问类的 static final 静态常量(基本类型和字符串)

        public class Test {
        
            public static void main(String[] args) {
                int a = A.j;
            }
        }
        class A{
            static{
                System.out.println("A init...");
            }
            static final int j = 20;
        }
        
      • 类对象.class 不会触发初始化

        public class Test {
        
            public static void main(String[] args) {
                Class clazz = A.class;
            }
        }
        class A{
            static{
                System.out.println("A init...");
            }
        }
        
      • 创建该类对象的数组

      • 类加载器的.loadClass方法

      • Class.forNamed的第二个参数为false时

        public class Test {
            public static void main(String[] args) throws ClassNotFoundException {
                ClassLoader classLoader = ClassLoader.getSystemClassLoader();
                Class clazz = Class.forName("com.suqu.test.A",false,classLoader);
            }
        }
        class A{
            static{
                System.out.println("A init...");
            }
            static int i = 10;
            static final int j = 20;
        }
        
    • 验证类是否被初始化,可以看改类的静态代码块是否被执行

2. 类加载器

2.1 概述

  • Java虚拟机设计团队有意把类加载阶段中的**“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”**(ClassLoader)
  • 类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段
  • 对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等

2.2 类加载器的分类

  • JVM支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。

  • 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。

  • 以JDK 8为例(这里的四者之间是包含关系,不是上层和下层,也不是子系统的继承关系):

  • 通过一个类,获取它不同的加载器
public class ClassLoaderTest {
    public static void main(String[] args) {
        // 获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        // 获取其上层的:扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@135fbaa4

        // 试图获取 根加载器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);//null

        // 获取自定义加载器
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
        
        // 获取String类型的加载器
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);//null
    }
}
  • 从结果可以看出根加载器无法直接通过代码获取,同时目前用户代码所使用的加载器为系统类加载器。同时我们通过获取String类型的加载器,发现是null,那么说明String类型是通过根加载器进行加载的,也就是说Java的核心类库都是使用根加载器进行加载的。
2.2.1 启动类加载器(Bootstrap ClassLoader)
  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
  • 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
  • 并不继承自java.lang.ClassLoader,没有父加载器。
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
2.2.2 扩展类加载器(Extension ClassLoader)
  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 派生于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
2.2.3 应用程序类加载器(AppClassLoader)
  • java语言编写,由sun.misc.LaunchersAppClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
  • 通过classLoader#getSystemclassLoader()方法可以获取到该类加载器
2.2.4 用户自定义类加载器
  • 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。

  • 为什么要自定义类加载器?

    • 隔离加载类
    • 修改类加载的方式
    • 扩展加载源
    • 防止源码泄漏
  • 用户自定义类加载器实现步骤:

    1. 继承ClassLoader父类
    2. 要遵从双亲委派机制,重写 findClass 方法
      • 不是重写loadClass方法,否则不会走双亲委派机制
    3. 读取类文件的字节码
    4. 调用父类的 defineClass 方法来加载类
    5. 使用者调用该类加载器的 loadClass 方法

2.3 关于ClassLoader

  • ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
  • 常用方法:
    • getParent():返回类加载器的超类加载器
    • loadClass(String name):加载名称为name的类,返回结果为java.lang.Class类的实例
    • findClass(String name):查找名称为name的类,返回结果为java.lang.Class类的实例
    • findLoadedClass(String name):查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例
    • defineClass(String name, byte[] b,int off,int len):把字节数组b中的内容转换为一个Java类,返回结果为java.lang.Class类的实例
    • resolveClass(Class<?> c):连接指定的一个Java类
  • 获取ClassLoader的途径
    • 获取当前ClassLoader:clazz.getClassLoader()
    • 获取当前线程上下文的ClassLoader:Thread.currentThread().getContextClassLoader()
    • 获取系统的ClassLoader:ClassLoader.getSystemClassLoader()
    • 获取调用者的ClassLoader:DriverManager.getCallerClassLoader()

2.4 双亲委派机制

  • Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
2.4.1 工作原理
  • 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行

  • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器

  • 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

2.4.2 沙箱安全机制
  • 自定义string类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
2.4.3 破坏双亲委派模式
  • 双亲委派模型的第一次"被破坏"其实发生在双亲委派模型出现之前——即JDK1.2面世以前的远古时代
    • 建议用户重写findClass()方法,在类加载器中的loadClass()方法中也会调用该方法
  • 双亲委派模型的第二次"被破坏"是由这个模型自身的缺陷导致的
    • 如果有基础类型又要调用回用户的代码,此时也会破坏双亲委派模式
  • 双亲委派模型的第三次"被破坏"是由于用户对程序动态性的追求而导致的
    • 这里所说的"动态性"指的是一些非常"热门"的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等
2.4.4 双亲委派机制的优势
  • 避免类的重复加载
  • 保护程序安全,防止核心API被随意篡改
    • 自定义类:java.lang.String
    • 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类

2.5 类的拓展知识

  • 如何判断两个class对象是否相同

    • 在JVM中表示两个class对象是否为同一个类存在两个必要条件:

      • 类的完整类名必须一致,包括包名。
      • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
    • 换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。

    • JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

  • 类的主动使用和被动使用

    Java程序对类的使用方式分为:主动使用和被动使用。 主动使用,又分为七种情况:

    • 创建类的实例
    • 访问某个类或接口的静态变量,或者对该静态变量赋值
    • 调用类的静态方法
    • 反射
    • 初始化一个类的子类
    • Java虚拟机启动时被标明为启动类的类
    • JDK7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF getStatic、REF putStatic、REF invokeStatic句柄对应的类没有初始化,则初始化。

    除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。

3. 运行期优化

3.1 分层编译

  • JVM 将执行状态分成了 5 个层次:

    • 0层:解释执行,用解释器将字节码翻译为机器码
    • 1层:使用 C1 即时编译器编译执行(不带 profiling)
    • 2层:使用 C1 即时编译器编译执行(带基本的profiling)
    • 3层:使用 C1 即时编译器编译执行(带完全的profiling)
    • 4层:使用 C2 即时编译器编译执行
  • profiling 是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数循环的回边次数

  • 即时编译器(JIT)与解释器的区别

    • 解释器
      • 将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
      • 是将字节码解释为针对所有平台都通用的机器码
    • 即时编译器
      • 将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
      • 根据平台类型,生成平台特定的机器码
  • 对于大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由 来),并优化这些热点代码

3.2 逃逸分析

  • 逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术
  • 逃逸分析的 JVM 参数如下:
    • 开启逃逸分析:-XX:+DoEscapeAnalysis
    • 关闭逃逸分析:-XX:-DoEscapeAnalysis
    • 显示分析结果:-XX:+PrintEscapeAnalysis
  • 逃逸分析技术在 Java SE 6u23+ 开始支持,并默认设置为启用状态,可以不用额外加这个参数
3.2.1 对象逃逸状态
  • 全局逃逸(GlobalEscape)
    • 即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:
      • 对象是一个静态变量
      • 对象是一个已经发生逃逸的对象
      • 对象作为当前方法的返回值
  • 参数逃逸(ArgEscape)
    • 即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的
  • 没有逃逸
    • 即方法中的对象没有发生逃逸
3.2.2 逃逸分析优化
  • 锁消除

    我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁

    例如,StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作

    锁消除的 JVM 参数如下:

    • 开启锁消除:-XX:+EliminateLocks
    • 关闭锁消除:-XX:-EliminateLocks

    锁消除在 JDK8 中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上

  • 标量替换

    首先要明白标量和聚合量,基础类型对象的引用可以理解为标量,它们不能被进一步分解。而能被进一步分解的量就是聚合量,比如:对象

    对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量,这就叫做标量替换

    这样,如果一个对象没有发生逃逸,那压根就不用创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能

    标量替换的 JVM 参数如下:

    • 开启标量替换:-XX:+EliminateAllocations
    • 关闭标量替换:-XX:-EliminateAllocations
    • 显示标量替换详情:-XX:+PrintEliminateAllocations

    标量替换同样在 JDK8 中都是默认开启的,并且都要建立在逃逸分析的基础上

  • 栈上分配

    当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能

3.3 方法内联

  • 内联函数:内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来直接进行替换

  • JVM内联函数

    • C++是否为内联函数由自己决定,Java由编译器决定。Java不支持直接声明为内联函数的,如果想让他内联,你只能够向编译器提出请求: 关键字final修饰 用来指明那个函数是希望被JVM内联的

    • 总的来说,一般的函数都不会被当做内联函数,只有声明了final后,编译器才会考虑是不是要把函数变成内联函数

    • JVM内联有许多运行时优化

      • 首先:短方法更利于JVM推断。流程更明显,作用域更短;副作用也更明显,如果遇到长方法,就不利于JVM处理。

      • 其次:方法内联,如果JVM监测到一些小方法被频繁的执行,它会把方法的调用替换成方法体本身

        private int add4(int x1, int x2, int x3, int x4) { 
        		//这里调用了add2方法
                return add2(x1, x2) + add2(x3, x4);  
            }  
        
            private int add2(int x1, int x2) {  
                return x1 + x2;  
            }
        

        方法调用被替换后:

        private int add4(int x1, int x2, int x3, int x4) {  
            	//被替换为了方法本身
                return x1 + x2 + x3 + x4;  
            }
        

3.4 反射优化

public class Reflect1 {
   public static void foo() {
      System.out.println("foo...");
   }

   public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
      Method foo = Reflect1.class.getMethod("foo");
      for(int i = 0; i<=16; i++) {
         foo.invoke(null);
      }
   }
}
  • foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现
//invoke方法源码
@CallerSensitive
public Object invoke(Object obj, Object... args)
    throws IllegalAccessException, IllegalArgumentException,
       InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, obj, modifiers);
        }
    }
    //MethodAccessor是一个接口,有3个实现类,其中有一个是抽象类
    MethodAccessor ma = methodAccessor;             // read volatile
    if (ma == null) {
        ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
}
img
  • 会由DelegatingMehodAccessorImpl去调用NativeMethodAccessorImpl
//NativeMethodAccessorImpl源码
class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method var1) {
        this.method = var1;
    }
	
	//每次进行反射调用,会让numInvocation与ReflectionFactory.inflationThreshold的值(15)进行比较,并使使得numInvocation的值加一
	//如果numInvocation>ReflectionFactory.inflationThreshold,则会调用本地方法invoke0方法
    public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
        if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
            MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
            this.parent.setDelegate(var3);
        }

        return invoke0(this.method, var1, var2);
    }

    void setParent(DelegatingMethodAccessorImpl var1) {
        this.parent = var1;
    }

    private static native Object invoke0(Method var0, Object var1, Object[] var2);
}
  • 一开始if条件不满足,就会调用本地方法invoke()
  • 随着numInvocation的增大,当它大于ReflectionFactory.inflationThreshold的值16时,就会本地方法访问器替换为一个运行时动态生成的访问器,来提高效率
    • 这时会从反射调用变为正常调用,即直接调用 Reflect1.foo()
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Daylan Du

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值