Java class类文件和类加载器详解以及代码优化

JVM就是Java虚拟机,它是Java程序运行的载体。
计算机只识别0和1。Java是⾼级语⾔。⾼级语⾔编写的程序要想被计算机执⾏,需要变成⼆进制形式的本地机器码。能直接变成机器码的语义是C++,它的缺点是不同操作系统,需要准备多份。Java需要先变成Java字节码(class⽂件)。然后再变成机器码。JVM可以实现Java的⼀次编译,到处运⾏。
这个就是区别于类似于C语⾔的⽅式。机器码是电脑CPU直接读取运行的机器指令,运行速度最快,但是非常晦涩难懂,也比较难编写,一般从业人员接触不到。字节码是一种中间状态(中间码)的二进制代码(文件)。需要直译器转译后才能成为机器码。

JVM字节码(class文件)

对于程序本身的优化,可以借鉴很多前辈们的经验,但是有些时候,在从源码角度方面分析的话,不好鉴别出哪个效率高,如对字符串拼接的操作,是直接“+”号拼接效率高还是使用StringBuilder效率高?这个时候,就需要通过查看编译好的class文件中字节码,就可以找到答案。
我们都知道,java编写应用,需要先通过javac命令编译成class文件,再通过jvm执行,jvm执行时是需要将class文件中的字节码载入到jvm进行运行的。

通过javap命令查看class文件的字节码内容

首先,看一个简单的Test1类的代码:

public class Test1 {

    public static void main(String[] args) {
        int a = 2;
        int b = 5;
        int c = b - a;
        System.out.println(c);
    }
}

通过javap命令查看class文件中的字节码内容:

javap ‐v Test1.class > Test1.txt

用法: javap <options> <classes>
其中, 可能的选项包括:
  -help  --help  -?        输出此用法消息
  -version                 版本信息
  -v  -verbose             输出附加信息
  -l                       输出行号和本地变量表
  -public                  仅显示公共类和成员
  -protected               显示受保护的/公共类和成员
  -package                 显示程序包/受保护的/公共类
                           和成员 (默认)
  -p  -private             显示所有类和成员
  -c                       对代码进行反汇编
  -s                       输出内部类型签名
  -sysinfo                 显示正在处理的类的
                           系统信息 (路径, 大小, 日期, MD5 散列)
  -constants               显示最终常量
  -classpath <path>        指定查找用户类文件的位置
  -cp <path>               指定查找用户类文件的位置
  -bootclasspath <path>    覆盖引导类文件的位置

查看Test1.txt文件,内容如下:

Classfile /E:cn/zjq/jvm/Test1.class
  Last modified 2021-7-28; size 411 bytes
  MD5 checksum 50e6a3429b5ebc3fa0ad40dcb54ebf03
  Compiled from "Test1.java"
public class cn.zjq.jvm.Test1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#14         // java/lang/Object."<init>":()V
   #2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #17.#18        // java/io/PrintStream.println:(I)V
   #4 = Class              #19            // cn/zjq/jvm/Test1
   #5 = Class              #20            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               main
  #11 = Utf8               ([Ljava/lang/String;)V
  #12 = Utf8               SourceFile
  #13 = Utf8               Test1.java
  #14 = NameAndType        #6:#7          // "<init>":()V
  #15 = Class              #21            // java/lang/System
  #16 = NameAndType        #22:#23        // out:Ljava/io/PrintStream;
  #17 = Class              #24            // java/io/PrintStream
  #18 = NameAndType        #25:#26        // println:(I)V
  #19 = Utf8               cn/zjq/jvm/Test1
  #20 = Utf8               java/lang/Object
  #21 = Utf8               java/lang/System
  #22 = Utf8               out
  #23 = Utf8               Ljava/io/PrintStream;
  #24 = Utf8               java/io/PrintStream
  #25 = Utf8               println
  #26 = Utf8               (I)V
{
  public cn.zjq.jvm.Test1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_2
         1: istore_1
         2: iconst_5
         3: istore_2
         4: iload_2
         5: iload_1
         6: isub
         7: istore_3
         8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_3
        12: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        15: return
      LineNumberTable:
        line 6: 0
        line 7: 2
        line 8: 4
        line 9: 8
        line 10: 15
}
SourceFile: "Test1.java"

内容大致分为4个部分:
第一部分:显示了生成这个class的java源文件、版本信息、生成时间等。
第二部分:显示了该类中所涉及到常量池,共26个常量。
第三部分:显示该类的构造器,编译器自动插入的。
第四部分:显示了main方的信息。(这个是需要我们重点关注的)

常量池

官网文档:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4-140

Constant TypeValue说明
CONSTANT_Class7类或接口的符号引用
CONSTANT_Fieldref9字段的符号引用
CONSTANT_Methodref10类中方法的符号引用
CONSTANT_InterfaceMethodref11接口中方法的符号引用
CONSTANT_String8字符串类型常量
CONSTANT_Integer3整形常量
CONSTANT_Float4浮点型常量
CONSTANT_Long5长整型常量
CONSTANT_Double6双精度浮点型常量
CONSTANT_NameAndType12字段或方法的符号引用
CONSTANT_Utf81UTF-8编码的字符串
CONSTANT_MethodHandle15表示方法句柄
CONSTANT_MethodType16标志方法类型
CONSTANT_InvokeDynamic18表示一个动态方法调用点

描述符

字段描述符

官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2
在这里插入图片描述

方法描述符

官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.3
示例:
The method descriptor for the method:

Object m(int i, double d, Thread t) {...}

is:

(IDLjava/lang/Thread;)Ljava/lang/Object;

解读方法字节码

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V //方法描述,V表示该方法的放回值为void
flags: ACC_PUBLIC, ACC_STATIC // 方法修饰符,public、static的
Code:
// stack=2,操作栈的大小为2、locals=4,本地变量表大小,args_size=1, 参数的个数
stack=2, locals=4, args_size=1
0: iconst_2 //将数字2值压入操作栈,位于栈的最上面
1: istore_1 //从操作栈中弹出一个元素(数字2),放入到本地变量表中,位于下标为1的位置(下标为0的是this)
2: iconst_5 //将数字5值压入操作栈,位于栈的最上面
3: istore_2 //从操作栈中弹出一个元素(5),放入到本地变量表中,位于第下标为2个位置
4: iload_2 //将本地变量表中下标为2的位置元素压入操作栈(5)
5: iload_1 //将本地变量表中下标为1的位置元素压入操作栈(2)
6: isub //操作栈中的2个数字相减
7: istore_3 // 将相减的结果压入到本地本地变量表中,位于下标为3的位置
// 通过#2号找到对应的常量,即可找到对应的引用
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iload_3 //将本地变量表中下标为3的位置元素压入操作栈(3)
// 通过#3号找到对应的常量,即可找到对应的引用,进行方法调用
12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
15: return //返回
LineNumberTable: //行号的列表
line 10: 15 LocalVariableTable: // 本地变量表
SourceFile: “Test1.java”

图解

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

研究i++ 与++i 的不同

我们都知道,i++表示,先返回再+1,++i表示,先+1再返回。它的底层是怎么样的呢? 我们一起探究下。
编写测试代码:

public class Test2 {
    public static void main(String[] args) {
        new Test2().method1(); 
        new Test2().method2();
	}
    public void method1(){
        int i = 1; 
        int a = i++;
        System.out.println(a); //打印1
    }

     public void method2(){
        int i = 1;
        int a = ++i;
        System.out.println(a);//打印2
    }
}

2.5.1 、查看class字节码

Classfile /E:/cn/zjq/jvm/Test2.class
  Last modified 2021-7-28; size 572 bytes
  MD5 checksum 3a7509b80d2f5064b662c1912a716e1b
  Compiled from "Test2.java"
public class Test2
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#19         // java/lang/Object."<init>":()V
   #2 = Class              #20            // Test2
   #3 = Methodref          #2.#19         // Test2."<init>":()V
   #4 = Methodref          #2.#21         // Test2.method1:()V
   #5 = Methodref          #2.#22         // Test2.method2:()V
   #6 = Fieldref           #23.#24        // java/lang/System.out:Ljava/io/PrintStream;
   #7 = Methodref          #25.#26        // java/io/PrintStream.println:(I)V
   #8 = Class              #27            // java/lang/Object
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               method1
  #16 = Utf8               method2
  #17 = Utf8               SourceFile
  #18 = Utf8               Test2.java
  #19 = NameAndType        #9:#10         // "<init>":()V
  #20 = Utf8               Test2
  #21 = NameAndType        #15:#10        // method1:()V
  #22 = NameAndType        #16:#10        // method2:()V
  #23 = Class              #28            // java/lang/System
  #24 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #25 = Class              #31            // java/io/PrintStream
  #26 = NameAndType        #32:#33        // println:(I)V
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (I)V
{
  public Test2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: new           #2                  // class Test2
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: invokevirtual #4                  // Method method1:()V
        10: new           #2                  // class Test2
        13: dup
        14: invokespecial #3                  // Method "<init>":()V
        17: invokevirtual #5                  // Method method2:()V
        20: return
      LineNumberTable:
        line 3: 0
        line 4: 10
        line 5: 20

  public void method1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_1
         1: istore_1
         2: iload_1
         3: iinc          1, 1
         6: istore_2
         7: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_2
        11: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
        14: return
      LineNumberTable:
        line 7: 0
        line 8: 2
        line 9: 7
        line 10: 14

  public void method2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_1
         1: istore_1
         2: iinc          1, 1
         5: iload_1
         6: istore_2
         7: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_2
        11: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
        14: return
      LineNumberTable:
        line 13: 0
        line 14: 2
        line 15: 7
        line 16: 14
}
SourceFile: "Test2.java"

对比

i++:

0: iconst_1 //将数字1压入到操作栈
1: istore_1 //将数字1从操作栈弹出,压入到本地变量表中,下标为1
2: iload_1 //从本地变量表中获取下标为1的数据,压入到操作栈中
3: iinc 1, 1 // 将本地变量中的1,再+1
6: istore_2 // 将数字1从操作栈弹出,压入到本地变量表中,下标为2
7: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_2 //从本地变量表中获取下标为2的数据,压入到操作栈中
11: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
14: return

++i:

0: iconst_1 //将数字1压入到操作栈
1: istore_1 //将数字1从操作栈弹出,压入到本地变量表中,下标为1
2: iinc 1, 1// 将本地变量中的1,再+1
5: iload_1 //从本地变量表中获取下标为1的数据(2),压入到操作栈中
6: istore_2 //将数字2从操作栈弹出,压入到本地变量表中,下标为2
7: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_2 //从本地变量表中获取下标为2的数据(2),压入到操作栈中
11: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
14: return

区别:

  • i++
    • 只是在本地变量中对数字做了相加,并没有将数据压入到操作栈
    • 将前面拿到的数字1,再次从操作栈中拿到,压入到本地变量中
  • ++i
    • 将本地变量中的数字做了相加,并且将数据压入到操作栈
    • 将操作栈中的数据,再次压入到本地变量中

小结:可以通过查看字节码的方式对代码的底层做研究,探究其原理。

字符串拼接

字符串的拼接在开发过程中使用是非常频繁的,常用的方式有三种:

  • +号拼接:str+“456”
  • StringBuilder拼接
  • StringBuffer拼接

StringBuffer是保证线程安全的,效率是比较低的,我们更多的是使用场景是不会涉及到线程安全的问题的,所以更多的时候会选择StringBuilder,效率会高一些。
那么,问题来了,StringBuilder和“+”号拼接,哪个效率高呢?接下来我们通过字节码的 方式进行探究。
首先,编写个示例:

package cn.zjq.jvm;

public class Test3 {

    public static void main(String[] args) {
        new Test3().m1();
        new Test3().m2();
    }

    public void m1(){
        String s1 = "123";
        String s2 = "456";
        String s3 = s1 + s2;
        System.out.println(s3);
    }

    public void m2(){
        String s1 = "123";
        String s2 = "456";
        StringBuilder sb = new StringBuilder();
        sb.append(s1);
        sb.append(s2);
        String s3 = sb.toString();
        System.out.println(s3);
    }
}

查看Test3.class的字节码

Classfile /E:/jvm-test/src/main/java/cn/zjq/jvm/Test3.class
  Last modified 2021-7-28; size 817 bytes
  MD5 checksum 51561188551689cee4e66aff5f7eb966
  Compiled from "Test3.java"
public class cn.zjq.jvm.Test3
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #14.#25        // java/lang/Object."<init>":()V
   #2 = Class              #26            // cn/zjq/jvm/Test3
   #3 = Methodref          #2.#25         // cn/zjq/jvm/Test3."<init>":()V
   #4 = Methodref          #2.#27         // cn/zjq/jvm/Test3.m1:()V
   #5 = Methodref          #2.#28         // cn/zjq/jvm/Test3.m2:()V
   #6 = String             #29            // 123
   #7 = String             #30            // 456
   #8 = Class              #31            // java/lang/StringBuilder
   #9 = Methodref          #8.#25         // java/lang/StringBuilder."<init>":()V
  #10 = Methodref          #8.#32         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #11 = Methodref          #8.#33         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #12 = Fieldref           #34.#35        // java/lang/System.out:Ljava/io/PrintStream;
  #13 = Methodref          #36.#37        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #14 = Class              #38            // java/lang/Object
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               main
  #20 = Utf8               ([Ljava/lang/String;)V
  #21 = Utf8               m1
  #22 = Utf8               m2
  #23 = Utf8               SourceFile
  #24 = Utf8               Test3.java
  #25 = NameAndType        #15:#16        // "<init>":()V
  #26 = Utf8               cn/zjq/jvm/Test3
  #27 = NameAndType        #21:#16        // m1:()V
  #28 = NameAndType        #22:#16        // m2:()V
  #29 = Utf8               123
  #30 = Utf8               456
  #31 = Utf8               java/lang/StringBuilder
  #32 = NameAndType        #39:#40        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #33 = NameAndType        #41:#42        // toString:()Ljava/lang/String;
  #34 = Class              #43            // java/lang/System
  #35 = NameAndType        #44:#45        // out:Ljava/io/PrintStream;
  #36 = Class              #46            // java/io/PrintStream
  #37 = NameAndType        #47:#48        // println:(Ljava/lang/String;)V
  #38 = Utf8               java/lang/Object
  #39 = Utf8               append
  #40 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #41 = Utf8               toString
  #42 = Utf8               ()Ljava/lang/String;
  #43 = Utf8               java/lang/System
  #44 = Utf8               out
  #45 = Utf8               Ljava/io/PrintStream;
  #46 = Utf8               java/io/PrintStream
  #47 = Utf8               println
  #48 = Utf8               (Ljava/lang/String;)V
{
  public cn.zjq.jvm.Test3();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: new           #2                  // class cn/zjq/jvm/Test3
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: invokevirtual #4                  // Method m1:()V
        10: new           #2                  // class cn/zjq/jvm/Test3
        13: dup
        14: invokespecial #3                  // Method "<init>":()V
        17: invokevirtual #5                  // Method m2:()V
        20: return
      LineNumberTable:
        line 6: 0
        line 7: 10
        line 8: 20

  public void m1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: ldc           #6                  // String 123
         2: astore_1
         3: ldc           #7                  // String 456
         5: astore_2
         6: new           #8                  // class java/lang/StringBuilder
         9: dup
        10: invokespecial #9                  // Method java/lang/StringBuilder."<init>":()V
        13: aload_1
        14: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        17: aload_2
        18: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        24: astore_3
        25: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        28: aload_3
        29: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        32: return
      LineNumberTable:
        line 11: 0
        line 12: 3
        line 13: 6
        line 14: 25
        line 15: 32

  public void m2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=5, args_size=1
         0: ldc           #6                  // String 123
         2: astore_1
         3: ldc           #7                  // String 456
         5: astore_2
         6: new           #8                  // class java/lang/StringBuilder
         9: dup
        10: invokespecial #9                  // Method java/lang/StringBuilder."<init>":()V
        13: astore_3
        14: aload_3
        15: aload_1
        16: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: pop
        20: aload_3
        21: aload_2
        22: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        25: pop
        26: aload_3
        27: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        30: astore        4
        32: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        35: aload         4
        37: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        40: return
      LineNumberTable:
        line 18: 0
        line 19: 3
        line 20: 6
        line 21: 14
        line 22: 20
        line 23: 26
        line 24: 32
        line 25: 40
}
SourceFile: "Test3.java"

从解字节码中可以看出,m1()方法源码中是使用+号拼接,但是在字节码中也被编译成了StringBuilder方式。所以,可以得出结论,字符串拼接,+号和StringBuilder是相等的,效率一样。 接下来,我们再看一个案例:

package cn.zjq.jvm;

public class Test4 {

    public static void main(String[] args) {
        new Test4().m1();
        new Test4().m2();
    }

    public void m1(){
        String str = "";
        for (int i = 0; i < 5; i++) {
            str = str + i;
        }
        System.out.println(str);
    }

    public void m2(){
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 5; i++) {
            sb.append(i);
        }
        System.out.println(sb.toString());
    }
}

m1() 与m2() 哪个方法的效率高?依然是通过字节码的方式进行探究。

Classfile /E:jvm/jvm-test/src/main/java/cn/zjq/jvm/Test4.class
  Last modified 2021-7-28; size 926 bytes
  MD5 checksum 7e80afebcbdc278af26a12995acfda40
  Compiled from "Test4.java"
public class cn.zjq.jvm.Test4
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #14.#28        // java/lang/Object."<init>":()V
   #2 = Class              #29            // cn/zjq/jvm/Test4
   #3 = Methodref          #2.#28         // cn/zjq/jvm/Test4."<init>":()V
   #4 = Methodref          #2.#30         // cn/zjq/jvm/Test4.m1:()V
   #5 = Methodref          #2.#31         // cn/zjq/jvm/Test4.m2:()V
   #6 = String             #32            //
   #7 = Class              #33            // java/lang/StringBuilder
   #8 = Methodref          #7.#28         // java/lang/StringBuilder."<init>":()V
   #9 = Methodref          #7.#34         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #10 = Methodref          #7.#35         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  #11 = Methodref          #7.#36         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #12 = Fieldref           #37.#38        // java/lang/System.out:Ljava/io/PrintStream;
  #13 = Methodref          #39.#40        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #14 = Class              #41            // java/lang/Object
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               main
  #20 = Utf8               ([Ljava/lang/String;)V
  #21 = Utf8               m1
  #22 = Utf8               StackMapTable
  #23 = Class              #42            // java/lang/String
  #24 = Utf8               m2
  #25 = Class              #33            // java/lang/StringBuilder
  #26 = Utf8               SourceFile
  #27 = Utf8               Test4.java
  #28 = NameAndType        #15:#16        // "<init>":()V
  #29 = Utf8               cn/zjq/jvm/Test4
  #30 = NameAndType        #21:#16        // m1:()V
  #31 = NameAndType        #24:#16        // m2:()V
  #32 = Utf8
  #33 = Utf8               java/lang/StringBuilder
  #34 = NameAndType        #43:#44        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #35 = NameAndType        #43:#45        // append:(I)Ljava/lang/StringBuilder;
  #36 = NameAndType        #46:#47        // toString:()Ljava/lang/String;
  #37 = Class              #48            // java/lang/System
  #38 = NameAndType        #49:#50        // out:Ljava/io/PrintStream;
  #39 = Class              #51            // java/io/PrintStream
  #40 = NameAndType        #52:#53        // println:(Ljava/lang/String;)V
  #41 = Utf8               java/lang/Object
  #42 = Utf8               java/lang/String
  #43 = Utf8               append
  #44 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #45 = Utf8               (I)Ljava/lang/StringBuilder;
  #46 = Utf8               toString
  #47 = Utf8               ()Ljava/lang/String;
  #48 = Utf8               java/lang/System
  #49 = Utf8               out
  #50 = Utf8               Ljava/io/PrintStream;
  #51 = Utf8               java/io/PrintStream
  #52 = Utf8               println
  #53 = Utf8               (Ljava/lang/String;)V
{
  public cn.zjq.jvm.Test4();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: new           #2                  // class cn/zjq/jvm/Test4
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: invokevirtual #4                  // Method m1:()V
        10: new           #2                  // class cn/zjq/jvm/Test4
        13: dup
        14: invokespecial #3                  // Method "<init>":()V
        17: invokevirtual #5                  // Method m2:()V
        20: return
      LineNumberTable:
        line 6: 0
        line 7: 10
        line 8: 20

  public void m1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #6                  // String
         2: astore_1	// 将空字符串压入到本地变量表中的下标为1的位置
         3: iconst_0	// 将数字0压入操作栈顶
         4: istore_2	// 将栈顶数字0压入到本地变量表中的下标为2的位置
         5: iload_2	  // 将本地变量中下标为2的数字0压入操作栈顶
         6: iconst_5  // 将数字5压入操作栈顶
         7: if_icmpge	35	//比较栈顶两int型数值大小,当结果大于等于0时跳转到35
        10: new           #7                  // class java/lang/StringBuilder
        13: dup       //复制栈顶数值并将复制值压入栈顶(数字5)
        14: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
        17: aload_1
        18: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: iload_2     //将本地变量中下标为2的数字0压入操作栈顶
        22: invokevirtual #10                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        25: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        28: astore_1
        29: iinc          2, 1
        32: goto          5
        35: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        38: aload_1
        39: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        42: return
      LineNumberTable:
        line 11: 0
        line 12: 3
        line 13: 10
        line 12: 29
        line 15: 35
        line 16: 42
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 5
          locals = [ class java/lang/String, int ]
        frame_type = 250 /* chop */
          offset_delta = 29

  public void m2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #7                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
         7: astore_1
         8: iconst_0
         9: istore_2
        10: iload_2
        11: iconst_5
        12: if_icmpge     27
        15: aload_1
        16: iload_2
        17: invokevirtual #10                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        20: pop
        21: iinc          2, 1
        24: goto          10
        27: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        30: aload_1
        31: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        37: return
      LineNumberTable:
        line 19: 0
        line 20: 8
        line 21: 15
        line 20: 21
        line 23: 27
        line 24: 37
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 10
          locals = [ class java/lang/StringBuilder, int ]
        frame_type = 250 /* chop */
          offset_delta = 16
}
SourceFile: "Test4.java"

可以看到,m1()方法中的循环体内,每一次循环都会创建StringBuilder对象,效率低于m2()方法。

小结

使用字节码的方式可以很好查看代码底层的执行,从而可以看出哪些实现效率高,哪些实现效率低。可以更好的对我们的代码做优化。让程序执行效率更高。

类加载详解

类加载的时机

  1. 遇到new、getstatic、putstatic、或invokestatic这四条字节码指令时,如果类没有进行初始化,先出发初始化。
public class Student{
	private static int age ;
	public static void method() {
	}
// Student.age
//Student. method() ;
//new Student( ) ;
  1. 使用java.lang.reflect包的方法反射调用的时候。
Classc=Class.forname("com.zjq.Student");
  1. 初始化类时,父类尚未初始化,先初始化父类。
  2. 虚拟机启动时指定要执行的主类,虚拟机先初始化这个主类。

类加载过程

在这里插入图片描述

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载7个阶段。其中验证、准备、解析三个阶段统称为连接。
其中解析的阶段的顺序可能会发生变化,某些情况下可能会在初始化后再开始,另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

加载(class文件–>Class对象)

在这里插入图片描述

加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口

加载源
  • 本地class文件
  • zip包

Jar、 War、Ear等

  • 其它文件生成

由JSP文件中生成对应的Class类.

  • 数据库中

将二进制字节流存储至数据库中,然后在加载时从数据库中读取。有些中间件会这么做,用来实现代码在集群间分发

  • 网络

从网络中获取二进制字节流。典型就是Applet.

  • 运行时计算生成

动态代理技术,用ProxyGenerator .generateProxyClass为特定接口生成形式为"$Proxy"的代理类的二进制字节流.

类和数组加载的区别

数组也有类型,称为“数组类型”。如:

String[] str=newString[10];

这个数组的数组类型是java. lang. String,而String只是这个数组的元素类型。
数组类和非数组类的类加载是不同的,具体情况如下:

  • 非数组类:是由类加载器来定成。
  • 数组类:数组类本身不通过类加载器创建,它是由java虚拟机直接创建,但数组类与类加载器有很密切的关系,因为数组类的元素类型最终要靠类加载器创建。
类加载过程的注意点

加载阶段和链接阶段是交叉的
类加载的过程中每个步骤的开始顺序都有严格限制,但每个步骤的结束顺序没有限制。也就是说,类加载过程中,必须按照如下顺序开始:

** **加载-> 链接-> 初始化

但结束顺序无所谓,因此由于每个步骤处理时间的长短不一就会导致有些步骤会出现交叉。

校验(各种检查)

验证阶段比较耗时,它非常重要但不一定必需(因为对程序运行期没有影响",如果所运行的代码已经被反复使用和验证过,那么可以使用-Xverify:none 参数关闭,以缩短类加载时间。
**验证目的:**保证二进制字节流中的信息符合虚拟机规范,并没有安全问题。
验证的过程:

  • 文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前的虚拟机处理.
本验证阶段是基于二进制字节流进行的,只有通过本阶段验证,才被允许存到方法区
后面的三个验证阶段都是基于方法区的存储结构进行,不会再直接操作字节流。
印证【加载和验证】是交叉进行的:

1.加载开始前,⼆进制字节流还没进⽅法区,⽽加载完成后,⼆进制字节流
已经存⼊⽅法区
2.⽽在⽂件格式验证前,⼆进制字节流尚未进⼊⽅法区,⽂件格式验证通过
之后才进⼊⽅法区

  • 元数据验证

对字节码描述信息进行语义分析,确保符合Java语法规范.

  • 字节码验证

本阶段是验证过程的最复杂的一个阶段。对方法体进行语义分析,保证方法在运行时不会出现危害虚拟机的事件。
字节码验证将对类的方法进行校验分析,保证被校验的方法在运行时不 会做出危害虚拟机的事,一个类方法体的字节码没有通过字节码验证,那一定有问题,但若一个方法通过了验证,也不能说明它一定安全。

  • 符号引用验证

发生在JVM将符号引用转化为直接引用的时候,这个转化动作发生在解析阶段,对类自身以外的信息进行匹配校验,确保解析能正常执行。

准备(为静态成员变量分配内存并初始化0值)

准备阶段主要完成两件事情:

  • **为己在内存的类的静态成员变量分配内存 **
  • 为静态成员变量设置初始值,初始值为0,false,null等
    在这里插入图片描述

仅仅[为类变量(即static修饰的字段变量)分配内存]并且[设置该类变量的初始值,即零值],这⾥不包含⽤final修饰的static,因为final在编译的时候就会分配了(编译器的优化),同时这⾥也[不会为实例变量分配初始化]。
[类变量(静态变量)]会分配在⽅法区中,⽽[实例变量]是会随着对象⼀起分配到[Java堆]中。
比如:
public static int x = 1000;
注意:

实际上变量x在准备阶段过后的初始值为0,而不是1000
将x赋值为1000的putstatic指令是程序被编译后,存放于类构造器方法之中

但是如果声明为:
public static final int x = 1000;
在编译阶段会为x⽣成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将x赋值为1000。

解析(将符号引用替换为直接引用)

解析是虚拟机将常量池的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行 , 分 别 对 应 于 常量 池 中 的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四种常量类型。

  1. 类或接口的解析:

判断所要转化成的直接引用是数组类型,还是普通的对象类型的引用,从而进行不同的解析。

  1. 字段解析:

会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段
​如果有,则查找结束;
如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个 接口和它们的父接口,
还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束。(优先从接口来,然后是继承的父类。理论上是按照上述顺序进行搜索解析,但在实际应用中,虚拟机的编译器实现可能要比上述规范要求的更严格一些。如果有一个同名字段同时出现在该类的接口和父类中,或同时在自己或父类的接口中出现,编译器可能会拒绝编译)

  1. 类方法解析:

对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。

  1. 接口方法解析:

与类方法解析步骤类似,只是搜索的是接口不会有父类,因此,只递归向上搜索父接口就行了。

初始化(调用方法)

初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码(初始化成为代码设定的默认值)。
在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器()方法的过程。
其实初始化过程就是调用类初始化方法的过程,完成对static有修饰的类变最的手动赋值还有主动调用静态代码块。

初始化过程的注意点
  • 方法是编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语旬在源文件中出现的顺序所决定的.
  • 静态代码块只能访问到出现在静态代码块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问.
public class Test {
     static {
         i=0;
         System.out.println(i); //编译失败:"⾮法向前引⽤"
     } 
     static int i = 1; 
}
  • 实例构造器需要显式调用父类构造函数,而类的不需要调用父类的类构造函数,虚拟机会确保子类的方法执行前已经执行完毕父类的方法.因此在JVM中第一个被执行的方法的类肯定是java.lang.Object.
  • 如果一个类或接口中没有静态代码块,也没有静态成员变量的赋值操作,那么编译器就不会为此类生成方法.
  • 接口也需要通过方法为接口中定义的静态成员变量显示初始化。

接口中不能使用静态代码块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成方法。不同的是,执行接口的方法不需要先执行父接口的方法.只有当父接口中的静态成员变量被使用到时才会执行父接口的方法.

  • 虚拟机会保证在多线程环境中一个类的方法被正确地加锁,同步.当多条线程同时去初始化一个类时,只会有一个线程去执行该类的方法,其它 线程都被阻塞等待,直到活动线程执行方法完毕.其他线程虽会被阻塞,只要有一个方法执行完,其它线程唤醒后不会再进入方法.同一个类加载器下,一个类型只会初始化一次.
使用静态内部类的单例实现:
public class Student {
     private Student() {
     }
     /*
     * 此处使⽤⼀个内部类来维护单例 JVM在类加载的时候,是互斥的,所
    以可以由此保证线程安全问题
     */
     private static class SingletonFactory {
     	private static Student student = new Student();
     }
     /* 获取实例 */
     public static Student getSingletonInstance() {
     	return SingletonFactory.student;
     } 
}

类加载器介绍

名称加载哪的类说明
BootStrap ClassLoader(启动类加载器)JAVA_HOME/jre/lib无法直接访问(C++代码书写的)
Extension ClassLoader(扩展类加载器)JAVA_HOME/jre/lib/ext上级为BookStrap,显示为null
Application ClassLoader(应用程序类加载器)classpath上级为Extension
自定义类加载自定义上级为Application

启动类加载器

  • -Xbootclasspath表示设置bootclasspath
  • 其中/a:.将当前目录追加至bootclasspath之后
  • 可以用这个方法替换核心类
    • java -Xbootclasspath:{解释:用新路径完全替换掉了原来路径JAVA_HOME/jre/lib}
    • java-Xbootclasspath/a:<追加路径>{解释:不替换原有路径,在原有路径追加路径a}
    • java- Xbootclasspath/p:<追加路径 >{解释:在原有路径上追加}
    1. 启动类加载非java实现,使用的 c/c++实现,嵌套在jvm内部

2.用于加载java的核心类库,用于提供jvm自身需要的类,比如rt.jar里面的类
3.不继承java.lang.ClassLoader,没有父加载器
4.负责加载扩展类加载器和系统应用类加载器,并为他们指定父加载器
5.出于安全考虑,该加载器只负责加载java,javax,sun等开头的类

扩展类加载器

  1. 是java语言编写,派生于ClassLoader类
  2. 父类加载器是启动类加载器
  3. 从java.ext.dirs系统属性所指定的目录中加载类库,或从jdk的安装目录下的jre/lib/ext子目录下加载类库,如果用户创建的 jar放在此目录下,也会有扩展类加载器来加载

应用程序类加载器

  1. 是java语言编写,派生于ClassLoader类
  2. 父类加载器是扩展类加载器
  3. 负责加载 环境变量classpath或者系统属性java.class.path路径下的类库
  4. 是程序中的默认加载器,一般来说,基本上java的类都是由它加载

自定义类加载器

为什么要定义自已的类加载器呢?

因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,就只能自定义一个ClassLoader类了。

比如:我要加载网络.上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader

如何定义类加载器?

继承ClassLoader类,重写findClass( )方法,loadClass( )方法
自定义类加载器注意事项:
自定义类加载器需要去继承ClassLoader类。

JDK1.2之前是重写ClassLoader类中的loadClass方法
JDK1.2以后 是重写ClassLader类中的findClass方法

自定义类加载器的实现,不要去覆盖ClassLoader类的loadClass方法,去实现findClass方法,为什么呢?

双亲委派机制

在这里插入图片描述

为什么要使用双亲委托这种模型呢?

考虑到安全因素,因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次 。
比如加载位于rt,jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
另外我们还可以试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一一个 自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

如何判定两个class是相同?

JVM在判定两个Class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。
Proxy.newProxyInstance(ClassI oader)

双亲委派机制源码
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) {
     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
     // to find the class.
     long t1 = System.nanoTime();



     c = findClass(name);

     // this is the defining class loader;
    record the stats

    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1
    - t0);

    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFro
    m(t1);

    sun.misc.PerfCounter.getFindClasses().increment();
     }
     }
     if (resolve) {
     resolveClass(c);
     }
     return c;
     }
 }

代码优化

优化,不仅仅是在运行环境进行优化,还需要在代码本身做优化,如果代码本身存在性能问题,那么在其他方面再怎么优化也不可能达到效果最优的。

尽可能使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。

尽量减少对变量的重复计算

明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的。所以例如下面的操作:

for (int i = 0; i < list.size(); i++)
{...}

建议替换为:

int length = list.size();
for (int i = 0,	i < length; i++)
{...}

这样,在list.size()很大的时候,就减少了很多的消耗。

尽量采用懒加载的策略,即在需要的时候才创建

String str = "aaa";
if (i == 1){
	list.add(str);
}

//建议替换成

if (i == 1){
	String str = "aaa";
    list.add(str);
}

异常不应该用来控制程序流程

异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用名为fillInStackTrace()的本地同步方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建 了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。

不要将数组声明为public static final

因为这毫无意义,这样只是定义了引用为static final,数组的内容还是可以随意改变的,将数组声明为public更是一个安全漏洞,这意味着这个数组可以被外部类所改变。

不要创建一些不使用的对象,不要导入一些不使用的类

这毫无意义,如果代码中出现"The value of the local variable i is not used"、“The import java.util is never used”,那么请删除这些无用的内容

程序运行过程中避免使用反射

反射是Java提供给用户一个很强大的功能,功能强大往往意味着效率不高。不建议在程序运行过程中使用尤其是频繁使用反射机制,特别是Method的invoke方法。
如果确实有必要,一种建议性的做法是将那些需要通过反射加载的类在项目启动的时候通过反射实例化出一个对象并放入内存。

使用数据库连接池和线程池

这两个池都是用于重用对象的,前者可以避免频繁地打开和关闭连接,后者可以避免频繁地创建和销毁线程。

容器初始化时尽可能指定长度

容器初始化时尽可能指定长度,如:new ArrayList<>(10); new HashMap<>(32); 避免容器长度不足时,扩容带来的性能损耗。

ArrayList随机遍历快,LinkedList添加删除快

使用Entry遍历Map

Map<String,String> map = new HashMap<>();
for (Map.Entry<String,String> entry : map.entrySet()) {
    String key = entry.getKey();
	String value = entry.getValue();
}

避免使用这种方式:

Map<String,String> map = new HashMap<>();
for (String key : map.keySet()) {
	String value = map.get(key);
}

不要手动调用System.gc();

String尽量少用正则表达式

正则表达式虽然功能强大,但是其效率较低,除非是有需要,否则尽可能少用。
replace() 不支持正则
replaceAll() 支持正则
如果仅仅是字符的替换建议使用replace()。

日志的输出要注意级别

// 当 前 的 日 志 级 别 是 error
LOGGER.info("保存出错!" + user);

对资源的close()建议分开操作

try{
    XXX.close();
    YYY.close();
}
catch (Exception e){
...
}

//建议改为
try{
	XXX.close();
}
catch (Exception e){
...
}
try{
    YYY.close();
}
catch (Exception e){
...
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

共饮一杯无

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

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

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

打赏作者

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

抵扣说明:

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

余额充值