static final String 与类的加载与初始化问题

题目

  • 下面的代码会不会导致Student类的加载初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class ClassLoadTest {
    public static void main(String[] args) {
    System.out.println(Student.final_str);
    }
    }

    class Student {
    //在编译阶段编译阶段,加入到了类的常量池中,没有直接引用到类,不会触发类的初始化
    static final String final_str = "final world";
    static String str = "str world";
    }
  • 答案是不一定会加载类,但不会初始化,会加载类可以通过 JVM参数 -XX:+TraceClassLoading 得到验证

  • 之前也有刷过这样的题目,run一下代码,还真是,然后看解释,嗯,然后就没有然后了,之前看过的解释是说在编译阶段编译阶段,类的常量池中,没有直接引用到类,不会触发类的初始化。后面也就忘了

  • 所以这次来深入验证一下 这句话 “在编译阶段编译阶段,加入到了类的常量池中”,主要对比 static final String 与 static String


验证

  • 你通过-XX:+TraceClassLoading打印了类的加载信息,居然没有导致加载类?没有加载?类的都没有加载,你怎么访问类的成员呢???

  • 更加有力的说明其实就是在调用方法里面看到

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // access flags 0x9
    public static main([Ljava/lang/String;)V
    L0
    LINENUMBER 17 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "final world" //这里直接加载了一个常量,并没有调用某一个对象,所以灭有加载类
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
    L1
    LINENUMBER 18 L1
    RETURN
    L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
    }
  • 如果是static string的话,会有调用类的信息出来

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // access flags 0x9
    public static main([Ljava/lang/String;)V
    L0
    LINENUMBER 17 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    GETSTATIC com/test/classload/Student.str : Ljava/lang/String; //这里指的那个类
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
    L1
    LINENUMBER 18 L1
    RETURN
    L2
  • 其实上面已经说明了类的初始化问题,但是我们继续看一下区别

  • 先编译Student

  • 直接查看二进制class

  • 通过反编译 查看字节码,有注释

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // class version 52.0 (52)
    // access flags 0x20
    class com/test/classload/Student {

    // compiled from: ClassLoadTest.java

    // access flags 0x18 注意区别 这里直接给了值
    final static Ljava/lang/String; final_str = "final world"

    // access flags 0x8 注意区别 这里在类的初始化的时候给值
    static Ljava/lang/String; str

    // access flags 0x0 对象的init方法
    <init>()V
    L0
    LINENUMBER 21 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
    L1
    LOCALVARIABLE this Lcom/test/classload/Student; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

    // access flags 0x8 类的init方法,其实这里
    static <clinit>()V
    L0
    LINENUMBER 24 L0
    LDC "str world" //看到没有,这里只有对str的赋值,并没有对 final_str 进行赋值
    PUTSTATIC com/test/classload/Student.str : Ljava/lang/String;
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 0
    }
  • 只看这里,只能说明区别,没有完全,说明问题,直接看一下原始的二进制

  • 这就涉及到,class文件的格式的的

  • 可以简单的看到,16进制对应的String信息,有我们写的str world,其实就是按照一定的TLV(type length value)格式,进行数据的标识。

image.png

  • 具体的格式信息,我们可以借用jclasslib工具来可视化
  • 这就class文件的信息排列顺序
  • 可以看到我们定义字段信息,比如str和final_str,以及自动生成init的方法
  • image.png
  • 注意到了区别,发现final_str是直接指向了常量池的信息的,而str就是空的,只有属性,没有value信息,需要在class init的时候进行赋值,这个也符合上面看到的字节码
  • 可以看到final_str的属性信息和value,就是我们自定义的 final world
  • image.png
  • image.png
  • 以上说明在编译后,final_str在还没有进行类的初始化的时候,就已经有了值,指向了常量池的中的字符串。

    总结

    编译的时候,编译器知道 既然是final的,直接在调用放加入了,其实这是一种编译器的优化,不用去调用类的方法,所以才没有导致类的加载,并不是说只要调用了类的final static 就一定没有导致类的初始化,比如通过反射。