1 – the introduction

A code problem is presented while reading “Understanding the Java Virtual Machine in Depth (version 3).” I think of several variations for this topic expansion, the results are different, in order to find the cause of the difference and in-depth understanding of some.

2 – Class initialization time

2.1 the original problem

Listing 7-1 in section 7.2, “Class Loading Timing,” in “Understanding the Java Virtual Machine (version 3)” has this code:

public class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }

    public static int VALUE = 1;
}

public class SubClass extends SuperClass {
    static {
        System.out.println(Ttf_subclass init! ""); }}public class Main {

    public static void main(String[] args) { System.out.println(SubClass.VALUE); }}Copy the code

The output is:

The answer to this result is given in the book:

After the above code runs, it simply prints “SuperClass init!” Instead of printing “SubClass init!” . For static fields, only the class that directly defines the field is initialized, so referring to a static field defined in a parent class by its subclass triggers initialization of the parent class but not the subclass. 1

So a call to subclass. VALUE in main() actually calls superclass.value. When the SuperClass is initialized, the SuperClass static block is called.

2.2 – Variant one

I’m going to modify the above code a little bit.

public class SuperClass {
    static {
        System.out.println("SuperClass init");
    }

    // public static int VALUE = 1;
    public final static int VALUE = 1; // Add a final modifier
}    
Copy the code

Adding a final modifier to superclass. VALUE without changing the rest of the code produces the following output:

Instead of the original result, “SuperClass init! And “SubClass init! None of that came out.

My initial guess was that because the VALUE field is final and is a basic data type, the JVM made some optimizations to refer directly to the VALUE of the field without passing superclass.value.

Later look at the IDEA decompress main. class source:

// Main.class
public class Main {
    public Main(a) {}public static void main(String[] args) {
        System.out.println(1); }}Copy the code

The Main class optimizes subclass. VALUE directly to “1” at compile time. Contrary to initial speculation, the Main class is optimized not by the JVM at runtime, but directly by the compiler.

In this case, the compiler is optimized according to what principle, in the future in depth expansion, first continue to look at the next variant.

2.3 – Variation two

public class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }

    // public static int VALUE = 1;
    // public final static int VALUE = 1;
    public final static Integer VALUE = 1; // Change VALUE to Integer wrapper class
}   
Copy the code

This time change the VALUE of int to the wrapper Integer and see what happens.

This time the result is “SuperClass init!” . Indeed, a wrapper class is just a normal class that is modified by final and cannot be optimized by the compiler like a basic data type, so it initializes the SuperClass by calling subclass.value.

2.4 – Variation three

public class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }

    // public static int VALUE = 1;
    // public final static int VALUE = 1;
    // public final static Integer VALUE = 1;
    public final static String VALUE = "1"; // Change VALUE to String
}  
Copy the code

This time change subclass. VALUE from Integer to String, and look at the result:

Now I get the same result as the one I got before, which makes me a little confused. String and Integer are both wrapped classes, and can be used as primitive data types without triggering SuperClass initialization.

IDEA decomcompiled main.class main.class main.class main.class main.class main.class

// Main.class
public class Main {
    public Main(a) {}public static void main(String[] args) {
        System.out.println("1"); }}Copy the code

As in the case of variant one, the compiler optimizes the String VALUE directly at compile time.

Compiler optimization techniques – Conditional constant propagation

For the variations 1 and 3 above, only the VALUE of VALUE is printed and “SuperClass init! “is not printed. The number one reason is compiler optimization.

Although the goal of a compiler is to translate program code into local machine code, the difficulty is not whether it can successfully translate machine code, but whether it can optimize the output code is the key to determine whether a compiler is good or not. On the official OpenJDK Wiki, the HotSpot VIRTUAL Machine design team lists a relatively comprehensive list of optimization techniques used in the just-in-time compiler. Address: wiki.openjdk.java.net/display/Hot… 1

There are many compiler optimization techniques listed in the official list, among which conditional Constant Propagation is the reason for the output results of variant 1 and variant 3 above.

Constant propagation is one of the most widely used optimization methods in modern compilers, and it is usually applied to advanced intermediate representation (IR). It solves the problem of statically detecting at run time whether an expression is always evaluated to a unique constant, and the compiler can simplify constants at compile time if it is known at call time which variables will have constant values and what those values will be. 2

3.1 – Optimization constants

In simple terms, the compiler uses an algorithm to find a constant in the code, and then directly replaces the value of the variable pointing to it. Such as:

public class Main {
    public static final int a = 1; // Global static constants

    public static void main(String[] args) {
        final int b = 2; // Local constant
        System.out.println(a);
        System.out.println(2); }}Copy the code

After the compiler compiles:

// Main.class
public class Main {
    public static final String A = "1";

    public static void main(String[] args) {
        String b = "2";
        System.out.println("1");
        System.out.println("2"); }}Copy the code

3.2 – Optimizing constant expressions

Even some constant expressions can be compiled directly in advance:

public class Main {
    public static void main(String[] args) {
        final int a = 3 * 4 + 5 - 6;
        int b = 10;
        if(a > b) { System.out.println(a); }}}Copy the code

After compiling:

// Main.class
public class Main {
    public static void main(String[] args) {
        int a = true;
        int b = 10;
        if (11 > b) {
            System.out.println(11); }}}Copy the code

3.3 – Optimized string concatenation

You can also compile String concatenation. There are often some questions on the web asking how many strings are generated. In the JVM virtual machine level analysis, it is not correct, the compiler directly at compile time optimization out of the memory pool, can not reach the runtime.

public class Main {
    public static void main(String[] args) {
        final String str = "hel" + "lo";
        System.out.println(str);
        System.out.println("hello"== str); }}Copy the code

After compiling the source code, we can see that STR is replaced directly with “hello” String, and “hello” == STR is true, so the entire String is generated.

// Main.class
public class Main {
    public static void main(String[] args) {
        String str = "hello";
        System.out.println("hello");
        System.out.println(true); }}Copy the code

String concatenation: You can’t concatenate multiple strings with “+”. Use StringBuilder. In fact, even using “+” will be optimized by the compiler to StringBuilder, so you can try it out for yourself.

3.4 – Risks associated with compiler conditional constant propagation

While compiler optimization can improve runtime efficiency, it also carries risks

3.4.1 – Constant reflection failure

Although some final-modified fields are optimized by the compiler as constants, Java has a reflection mechanism that allows you to change these values through some bizarre tricks. However, because it is optimized by the compiler, the modified value may not take effect as expected. Such as:

public class Main {
    public static final String VALUE = "A";

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Class<Main> mainClass = Main.class;
        Field value = mainClass.getField("VALUE");
        value.setAccessible(true);

        // Remove the final modifier of A
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(value, value.getModifiers() & ~Modifier.FINAL);

        value.set(null."B");
        System.out.println(VALUE); // 实际还是输出 "A"}}Copy the code

System.out.println(VALUE); system.out.println (VALUE); Replace System. Out. Println (” A “); , the final result will be different from the expected.

3.4.2 – Partial compilation

If the constant and the object referenced are not in the same file, and only the file in which the constant is recompiled is modified, the unrecompiled file will use the old value. Such as:

// Constant.java
public class Constant {
    public final static String VALUE = "A";
}

// Main.java
public class Main {
    public static void main(String[] args) { System.out.println(Constant.VALUE); }}Copy the code

If you change the VALUE of constant. VALUE to “B” and compile the constant. Java file separately through javac constant. Java, the output in Main will still be “A”.

4 – Constant, static constant pool, dynamic constant pool

4.1 constant

A constant is a quantity whose value remains constant throughout the run of a program. In Java development, this usually refers to a variable that is modified by final. But “constant” is defined differently from a virtual machine’s point of view.

In the virtual machine, constants are stored in a constant pool, which contains two major types of constants: literals and Symbolic References. Literals are close to the Java language level concepts of constants, such as text strings, constant values declared final, and so on. 1 Symbolic reference is a concept of compilation principle, mainly including class, field, method information, etc., which will not be described here.

4.2 – Static constant pool

A (static) constant pool can be likened to a resource repository in a Class file. It is the data item that is most associated with other items in a Class file structure, and is usually one of the data items that occupy the largest space in a Class file. It is also the first table type data item to appear in a Class file.

The data item types stored in the (static) constant pool are as follows:

1

Static constant pools are compiled and written in class files. You can view the bytecode directly to see the structure, as shown in the following code:

public class Main {
    final static String A = "A";
    final static int B = 1;
    final static Integer C = 2;

    public static void main(String[] args) { System.out.println(A); System.out.println(B); System.out.println(C); }}Copy the code

Run the javap-verbose main. class command to view the decompiled bytecode:

You can see that strings and ints in the code are stored in the static constant pool, but integers are not. Because the former corresponds to the “CONSTANT_String_info” and “CONSTANT_Integer_info” types in the constant pool, the latter corresponds to ordinary objects, where only object information is stored.

This explains the difference in the results of variations 1, 3 and 2 above.

4.3 – Dynamic constant Pool

Another important feature of the runtime constant pool (dynamic constant pool) over the static constant pool of the Class file is that it is dynamic. The Java language does not require constants to be generated only at compile time. That is, the contents of the constant pool in the Class file are not preloaded into the method area run-time constant pool. New constants can also be pooled at run time, a feature most used by developers is the Intern () method of the String class.

5 – conclusion

“Deep understanding of the Java virtual machine” is indeed a very good book, read several times, each time there is a new harvest, this is due to their own in the source code when the “careless error” had unexpected harvest.

reference

  • [1] Understanding the Java Virtual Machine in Depth (version 3)

  • [2] Compiler Optimization – Introduction to constant propagation

  • [3] Confusing concepts in Java: constant pool, string pool, wrapped object pool in Java 8

  • [4] Compile-time constants and runtime constants


Reflections from a question in “Understanding the Java Virtual Machine”