前言
- 由于本博客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");
}
}
- 运行结果
- 可以发现静态代码块没有执行!!!
- 按理说访问类的成员变量会进行初始化,导致静态代码块的执行。但是这里并没有执行静态代码块,也就是类没有初始化。
常量的特殊之处
- 常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中。
- 本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。
注意:以上面举的样例来说,就是将常量存放到了ConstantTest的常量池中,因为是ConstantTest类中的main方法调用了Parent2的常量,之后ConstantTest与Parent2就没有任何关系了。甚至,我们可以将MyParent2的class文件删除。其结果和准确性也不会受影响。
助记符
什么是助记符?
-
Java代码会被编译成class文件,但class文件本身不是给人类看的,它是给JVM看的,即它是一个二进制文件(字节码文件)。
- 就像这样
-
程序执行起来就是让cpu识别操作(指令)和被操作数(都是二进制,class文件的内容就是指令加操作数),为了让人类可以看懂指令,遂引入助记符。
生成助记符
- 到class的路径使用终端输入
javap -c ***.class,反编译生成助记符。
回到刚开始的那个代码样例
从字节码层面解读一下,为什么访问一个类的常量,不会导致其初始化
- 反编译
这意味着?
- 首先要明确,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");
}
}
- 反编译
- 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里面。
更进一步
将本篇开头用的代码改一改
- 代码如下:
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的静态代码块能运行吗?
- 结果
- 可以发现静态代码块执行了!!!
原因何在?
- 当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类被初始化。
- 可以设想一下,因为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");
}
}
- 结果
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");
}
}
- 结果
- 静态代码块并没有执行!!!
原因
- 可以猜测,既然静态代码没有执行,就说明
Parent4没有被主动使用,虽然表面上像new了它。 - 看看到底到
new了谁?- 加一行代码,获取它的类名
System.out.println(parent4.getClass().getName());- 结果
再用反编看一看
- 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()); } } - 结果
- 反编译
- newarray 表示创建一个指定的原始类型 (如int、float、char等) 的数组,并将其引用值压入栈顶。
概念
- 对于数组实例来说,其类型是由JVM在运行期动态生成的,表示为
[Lcom.jvmstudy.classloading.Parent4这种形式。是动态生成的类型。 - 而对于非引用类型,数组类型就是直接是
[开头,没有L。 - 当创建一个数组类型的实例时,并不表示对其元素类的主动使用,数组类型是JVM在运行期动态创建出来的。
- 再者,对于助记符的学习应该是边用边学,而不是一整个列表全列出来,意义并不大。