JVM 常量的含义、初识助记符以及创建数组的本质

419 阅读5分钟

前言

  • 由于本博客JVM的知识是连续,如有不明白的名词,请观看前面的篇章。

常量

  • 就是被 static final 修饰的就是常量

用代码演示一番常量到底咋回事

代码

public class ConstantTest {
    public static void main(String[] args) {
        System.out.println(Parent2.STR);//访问类的静态成员
    }
}
class Parent2{
    public static final String STR = "hello world";//常量
    static {//静态代码块
        System.out.println("Parent2 static block");
    }
}
  • 运行结果

image.png

  • 可以发现静态代码块没有执行!!!
  • 按理说访问类的成员变量会进行初始化,导致静态代码块的执行。但是这里并没有执行静态代码块,也就是类没有初始化。

常量的特殊之处

  • 常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中。
  • 本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。

注意:以上面举的样例来说,就是将常量存放到了ConstantTest的常量池中,因为是ConstantTest类中的main方法调用了Parent2的常量,之后ConstantTest与Parent2就没有任何关系了。甚至,我们可以将MyParent2的class文件删除。其结果和准确性也不会受影响。

助记符

什么是助记符?

  • Java代码会被编译成class文件,但class文件本身不是给人类看的,它是给JVM看的,即它是一个二进制文件(字节码文件)。

    • 就像这样

    image.png

  • 程序执行起来就是让cpu识别操作(指令)和被操作数(都是二进制,class文件的内容就是指令加操作数),为了让人类可以看懂指令,遂引入助记符。

生成助记符

  • 到class的路径使用终端输入 javap -c ***.class,反编译生成助记符。

回到刚开始的那个代码样例

从字节码层面解读一下,为什么访问一个类的常量,不会导致其初始化

  • 反编译

image.png

image.png

这意味着?

  • 首先要明确,JVM运行的是class文件,也就是其文件内容是什么,运行时就是什么。
  • 然后编译后,println 语句中 Parent2.STR 被替换成了 hello world,也就是 Constant 类根本就没有使用 Parent2 , 即编译之后这两个类没有任何关系了,运行期间以及类加载期间根本没有主动使用 Parent2 这个类。也就不会初始化它,就进一步导致其静态代码块没有执行。

多学些助记符

  • 代码如下:
public class ConstantTest {
    public static void main(String[] args) {
        String str;
        short a;
        int b,c,d,e;
        str = Parent2.STR;
        a = Parent2.a;
        b = Parent2.b;
        c = Parent2.c;
        d = Parent2.d;
        e = Parent2.e;
    }
}
class Parent2{
    public static final String STR = "hello world";
    public static final short a = 127;
    public static final int b = 127;
    public static final int c = 128;
    public static final int d = 1;
    public static final int e = -1;

    static {
        System.out.println("Parent2 static block");
    }
}
  • 反编译 image.png

  • ldc 表示将int, float或是String类型的常量值从常量池中推送至栈顶。栈是JVM内存模型中的栈,推送至栈顶就说明该常量即将被使用。
  • bipush 表示将单字节(-128 ~ 127)的常量值推送至栈顶。
    • 并没有区分short还是int,只看数值。
  • sipush 表示将一个短整型常量值(-32768 ~ 32767)推送至栈顶。
  • iconst_1 表示将int类型1推送至栈顶(iconst_m1 ~ iconst_5 表示 -1 ~ 5)。

其实助记符也是类

  • 助记符类在rt.jar里面。

image.png

更进一步

将本篇开头用的代码改一改

  • 代码如下:
public class ConstantTest2 {
    public static void main(String[] args) {
        System.out.println(Parent3.STR);
    }
}

class Parent3{
    public static final String STR = UUID.randomUUID().toString();
    static {
        System.out.println("Parent3 static block");
    }
}

Parent3的静态代码块能运行吗?

  • 结果

image.png


  • 可以发现静态代码块执行了!!!

原因何在?

  • 当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类被初始化。
    • 可以设想一下,因为UUID每次运行的结果都不一样,也就是每次编译结果都不一样,就不能将其放入常量池中了。

数组的创建

先巩固一下首次主动使用的概念

  • 代码
public class ArrayTest {
    public static void main(String[] args) {
        Parent4 parent4 = new Parent4();
        System.out.println("============");
        Parent4 parent41 = new Parent4();
    }
}
class Parent4{
    public static final String STR = "hello world";
    static {
        System.out.println("Parent4 static block");
    }
}
  • 结果

image.png

new 是主动使用,上面的代码,静态代码只执行了一次,因为只有首次主动使用才会导致其初始化,第二次主动使用并不会导致其初始化。

再来说说数组

  • 老样子,上代码,猜静态代码块会不会执行
public class ArrayTest {
    public static void main(String[] args) {
        Parent4[] parent4 = new Parent4[1];
    }
}
class Parent4{
    public static final String STR = "hello world";
    static {
        System.out.println("Parent4 static block");
    }
}
  • 结果

image.png

  • 静态代码块并没有执行!!!

原因

  • 可以猜测,既然静态代码没有执行,就说明 Parent4 没有被主动使用,虽然表面上像 new 了它。
  • 看看到底到 new 了谁?
    • 加一行代码,获取它的类名
     System.out.println(parent4.getClass().getName());
    
    • 结果

    image.png

再用反编看一看

image.png


  • anewarray 表示创建一个引用类型的(如类、接口、数组)数组,并将其引用值压入栈顶。
    • 看来引用并不是使用。
  • 在原来的基础上,添加一些代码
    public class ArrayTest {
     public static void main(String[] args) {
         Parent4[] parent4 = new Parent4[1];
         int[] arr = new int[10];
         boolean[] f =new boolean[10];
         System.out.println(arr.getClass().getName());
         System.out.println(f.getClass().getName());
       }
    }
    
  • 结果

image.png

  • 反编译

image.png


  • newarray 表示创建一个指定的原始类型 (如int、float、char等) 的数组,并将其引用值压入栈顶。

概念

  • 对于数组实例来说,其类型是由JVM在运行期动态生成的,表示为 [Lcom.jvmstudy.classloading.Parent4 这种形式。是动态生成的类型。
  • 而对于非引用类型,数组类型就是直接是 [ 开头,没有 L
  • 当创建一个数组类型的实例时,并不表示对其元素类的主动使用,数组类型是JVM在运行期动态创建出来的。

  • 再者,对于助记符的学习应该是边用边学,而不是一整个列表全列出来,意义并不大。