文章目录
- 编译期常量
- 运行时常量
- 相关助记符
- 附:常量入栈指令
- References
关于常量,在《Java 核心技术 卷 I》中是这么定义的:
在 Java 中,利用关键字 final
指示常量。一旦被赋值之后,就不能够再更改了。
在 Java 中,经常希望某个常量可以在一个类中的多个方法中使用,通常将这些常量称为类常量。可以使用关键字 static final
设置一个类常量。
而常量又分为编译期常量和运行时常量。
编译期常量
在 小议 Java 类的初始化 中也提到过,对于 HotSpot 虚拟机来说,可以通过 -XX:+TraceClassLoading
参数查看有哪些类被加载;根据《Java 虚拟机规范》,验证一个类是否被初始化就是通过查看静态代码块是否执行。
看一个例子:
package dongguabai.demo.testing.classloader;
/**
* @author Dongguabai
* @Description
* @Date 创建于 2020-11-22 01:38
*/
public class Main2 {
public static void main(String[] args) {
System.out.println(Person.str);
}
}
class Person{
public static String str = "User_Str";
static {
System.out.println("User static....");
}
}
输出:
[Loaded dongguabai.demo.testing.classloader.Main2 from file:/Users/dongguabai/IdeaProjects/thinkingTest/testing/target/classes/]
[Loaded dongguabai.demo.testing.classloader.Person from file:/Users/dongguabai/IdeaProjects/thinkingTest/testing/target/classes/]
User static....
User_Str
可以发现,Person
类被加载和初始化了。接下来将 Person
中的 str
加上 final
修饰,使之成为一个类常量:
public static final String str = "User_Str";
重新运行输出结果:
[Loaded dongguabai.demo.testing.classloader.Main2 from file:/Users/dongguabai/IdeaProjects/thinkingTest/testing/target/classes/]
User_Str
发现 Person
类既没有被加载,也没有被初始化。
实际上,在编译期间,常量会被存入调用常量的方法所在的类的常量池中。在上面的例子中调用类 Main2
并没有直接引用到定义常量 str
的 Person
类,所以这里并没有触发 Person 类的加载和初始化,所以编译之后其实 Main2 类和 Person 类没啥关系了。
再看一个例子:
➜ java git:(master) ✗ cd /Users/dongguabai/IdeaProjects/thinkingTest/testing/src/main/java
➜ java git:(master) ✗ javac dongguabai/demo/testing/classloader/Main2.java
➜ java git:(master) ✗ ls dongguabai/demo/testing/classloader
Main.java Main2.class Main2.java Person.class
➜ java git:(master) ✗ rm dongguabai/demo/testing/classloader/Person.class
➜ java git:(master) ✗ java dongguabai/demo/testing/classloader/Main2
User_Str
在这个例子中我使用 javac
将 Main2.java
编译后,删除了 Person.class
,但是此时 Main2.class
仍然可以正常运行,下面是 Main2.class
反编译后的结果:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package dongguabai.demo.testing.classloader;
public class Main2 {
public Main2() {
}
public static void main(String[] var0) {
System.out.println("User_Str");
}
}
的确是跟 Person
没有任何关联了。
接下来使用 javap
查看一下 Main2.class
的字节码指令:
➜ java git:(master) ✗ javap -c dongguabai/demo/testing/classloader/Main2.class
Compiled from "Main2.java"
public class dongguabai.demo.testing.classloader.Main2 {
public dongguabai.demo.testing.classloader.Main2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String User_Str
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
可以看到 Main2.java
中的 Person.str
已经被替换成了 User_Str
,ldc
是 Java 中操作常量的指令助记符。
运行时常量
在《深入理解 Java 虚拟机》中关于触发类的初始化的行为有这么一段描述:
遇到 new
、getstatic
、putstatic
或 invokeStatic
这 4 条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型 Java 代码场景有:
- 读取或设置一个类型的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候。(补充:分别会触发
getstatic
和 putstatic
)。
也就是说在编译器就能够确定结果的会直接放入常量池,不会触发常量所在类的初始化。再看一个例子:
package dongguabai.demo.testing.classloader;
import java.util.Date;
/**
* @author Dongguabai
* @Description
* @Date 创建于 2020-11-22 10:36
*/
public class Main4 {
public static void main(String[] args) {
System.out.println(Person2.date);
}
}
class Person2{
public static final Date date = new Date();
static {
System.out.println("User2 static....");
}
}
运行结果:
[Loaded dongguabai.demo.testing.classloader.Main4 from file:/Users/dongguabai/IdeaProjects/thinkingTest/testing/target/classes/]
[Loaded dongguabai.demo.testing.classloader.Person2 from file:/Users/dongguabai/IdeaProjects/thinkingTest/testing/target/classes/]
User2 static....
Sun Nov 22 10:49:38 CST 2020
这时候发现 Person2
被加载也被初始化了,因为此时的常量 date
是无法在编译期确定的。此时在 Main4
中调用 Person2.date
就相当于是主动使用了 Person2
,会触发 Person2
的初始化。
相关助记符
在 com.sun.org.apache.bcel.internal.generic
包下有很字节码相关的类。
/**
* LDC - Push item from constant pool.
*
* <PRE>Stack: ... -> ..., item</PRE>
*
* @author <A HREF="mailto:markus.dahm@berlin.de">M. Dahm</A>
*/
public class LDC extends CPInstruction
implements PushInstruction, ExceptionThrower, TypedInstruction {
/**
* Empty constructor needed for the Class.newInstance() statement in
* Instruction.readInstruction(). Not to be used otherwise.
*/
LDC() {}
public LDC(int index) {
super(com.sun.org.apache.bcel.internal.Constants.LDC_W, index);
setSize();
}
// Adjust to proper size
protected final void setSize() {
if(index <= com.sun.org.apache.bcel.internal.Constants.MAX_BYTE) { // Fits in one byte?
opcode = com.sun.org.apache.bcel.internal.Constants.LDC;
length = 2;
} else {
opcode = com.sun.org.apache.bcel.internal.Constants.LDC_W;
length = 3;
}
}
...
...
}
这里的 index
来自父类 CPInstruction
:
protected int index; // index to constant pool
通过 com.sun.org.apache.bcel.internal.generic.LDC#setSize
方法可以大概猜出 ldc
与 ldc_w
的区别,这里测试一下:
package dongguabai.demo.testing.classloader;
/**
* @author Dongguabai
* @Description
* @Date 创建于 2020-11-22 03:21
*/
public class Main3 {
public static void main(String[] args) {
System.out.println("0");
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
...
...
System.out.println("253");
System.out.println("254");
System.out.println("255");
}
}
javap
查看:
➜ java git:(master) ✗ javap -c -verbose dongguabai/demo/testing/classloader/Main3.class
Classfile /Users/dongguabai/IdeaProjects/thinkingTest/testing/src/main/java/dongguabai/demo/testing/classloader/Main3.class
Last modified 2020-11-22; size 5692 bytes
MD5 checksum 414b5be640030c3f3b05118b4fb376ad
Compiled from "Main3.java"
public class dongguabai.demo.testing.classloader.Main3
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #261.#270 // java/lang/Object."<init>":()V
#2 = Fieldref #271.#272 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #273 // 0
#4 = Methodref #274.#275 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = String #276 // 1
#6 = String #277 // 2
...
#382 = Utf8 107
#383 = Utf8 108
...
#526 = Utf8 251
#527 = Utf8 252
#528 = Utf8 253
#529 = Utf8 254
#530 = Utf8 255
#531 = Utf8 dongguabai/demo/testing/classloader/Main3
#532 = Utf8 java/lang/Object
#533 = Utf8 java/lang/System
#534 = Utf8 out
#535 = Utf8 Ljava/io/PrintStream;
#536 = Utf8 java/io/PrintStream
#537 = Utf8 println
#538 = Utf8 (Ljava/lang/String;)V
{
public dongguabai.demo.testing.classloader.Main3();
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 8: 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: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String 0
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #5 // String 1
13: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
19: ldc #6 // String 2
...
2000: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
2003: ldc #254 // String 250
2005: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
2008: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
2011: ldc #255 // String 251
2013: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
2016: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
2019: ldc_w #256 // String 252
...
2031: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
2034: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
2037: ldc_w #258 // String 254
2040: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
2043: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
2046: ldc_w #259 // String 255
2049: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
2052: return
LineNumberTable:
...
}
SourceFile: "Main3.java"
可以看到 index
大于 255 的常量使用的是 ldc_w
。
同样地,还有比如 ICONST
:
public ICONST(int i) {
super(com.sun.org.apache.bcel.internal.Constants.ICONST_0, (short)1);
if((i >= -1) && (i <= 5))
opcode = (short)(com.sun.org.apache.bcel.internal.Constants.ICONST_0 + i); // Even works for i == -1
else
throw new ClassGenException("ICONST can be used only for value between -1 and 5: " +
i);
value = i;
}
看一个例子:
package dongguabai.demo.testing.classloader;
/**
* @author Dongguabai
* @Description
* @Date 创建于 2020-11-22 15:32
*/
public class Main5 {
public static void main(String[] args) {
System.out.println(0);
System.out.println(10);
}
}
javap
查看:
➜ java git:(master) ✗ javap -c -verbose dongguabai/demo/testing/classloader/Main5.class
Classfile /Users/dongguabai/IdeaProjects/thinkingTest/testing/src/main/java/dongguabai/demo/testing/classloader/Main5.class
Last modified 2020-11-22; size 428 bytes
MD5 checksum e5ab95769f8d7d9f91305d67c0434419
Compiled from "Main5.java"
public class dongguabai.demo.testing.classloader.Main5
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 // dongguabai/demo/testing/classloader/Main5
#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 Main5.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 dongguabai/demo/testing/classloader/Main5
#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 dongguabai.demo.testing.classloader.Main5();
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 8: 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: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iconst_0
4: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
10: bipush 10
12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
15: return
LineNumberTable:
line 11: 0
line 12: 7
line 13: 15
}
SourceFile: "Main5.java"
可以看到 0 使用的是 iconst_0
,而 10 使用的是 bipush
。
附:常量入栈指令
指令码 | 操作码(助记符) | 操作数 | 描述(栈指操作数栈) |
0x01 | aconst_null | null值入栈。 | |
0x02 | iconst_m1 | -1(int)值入栈。 | |
0x03 | iconst_0 | 0(int)值入栈。 | |
0x04 | iconst_1 | 1(int)值入栈。 | |
0x05 | iconst_2 | 2(int)值入栈。 | |
0x06 | iconst_3 | 3(int)值入栈。 | |
0x07 | iconst_4 | 4(int)值入栈。 | |
0x08 | iconst_5 | 5(int)值入栈。 | |
0x09 | lconst_0 | 0(long)值入栈。 | |
0x0a | lconst_1 | 1(long)值入栈。 | |
0x0b | fconst_0 | 0(float)值入栈。 | |
0x0c | fconst_1 | 1(float)值入栈。 | |
0x0d | fconst_2 | 2(float)值入栈。 | |
0x0e | dconst_0 | 0(double)值入栈。 | |
0x0f | dconst_1 | 1(double)值入栈。 | |
0x10 | bipush | valuebyte | valuebyte值带符号扩展成int值入栈。 |
0x11 | sipush | valuebyte1valuebyte2 | (valuebyte1 << 8) | valuebyte2 值带符号扩展成int值入栈。 |
0x12 | ldc | indexbyte1 | 常量池中的常量值(int, float, string reference, object reference)入栈。 |
0x13 | ldc_w | indexbyte1indexbyte2 | 常量池中常量(int, float, string reference, object reference)入栈。 |
0x14 | ldc2_w | indexbyte1indexbyte2 | 常量池中常量(long, double)入栈。 |
References
- 《The Java Virtual Machine Specification Java SE 8 Edition》
- 《Java 核心技术 卷 I》
- https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.ldc_w
- 《深入理解 Java 虚拟机》