先来了解一下常量池,常量池分为方法区中的运行时常量池和class文件中的常量池,class文件中的常量池在编译时确定,其中包括符号引用和字面量(文本字符串,被声明为final的变量的值),运行时,JVM从中读取数据到方法区的运行时常量池,运行时常量池可以在运行时添加常量,常量可以在运行时或编译时被放入常量池,编译期放入到类文件的常量池中,运行时放入到方法区的运行时常量池中,JDK1.7后运行时常量池位于堆中,访问类中的常量不一定会加载类,如下代码:
class deal
{ static {System.out.println("加载了deal类");}
static final int x=10000000;
}
public class Try {
public static void main(String[] args) {
System.out.println(deal.x);
}
}
输出结果为
可见并未加载类deal,这是一种优化,因为访问常量只是为了获取其中的值,我们无法改变它的值,所以不需要加载deal类。当我们的代码访问了类中的常量时,该常量才会在编译时被放入对应类class文件的常量池,否则不会被放入,看看Try.class文件的反编译结果:
Constant pool:
#1 = Methodref #7.#16 // java/lang/Object."<init>":()V
#2 = Fieldref #17.#18 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #19 // deal
#4 = Integer 10000000
#5 = Methodref #20.#21 // java/io/PrintStream.println:(I)V
#6 = Class #22 // Try
#7 = Class #23 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 SourceFile
#15 = Utf8 Try.java
#16 = NameAndType #8:#9 // "<init>":()V
#17 = Class #24 // java/lang/System
#18 = NameAndType #25:#26 // out:Ljava/io/PrintStream;
#19 = Utf8 deal
#20 = Class #27 // java/io/PrintStream
#21 = NameAndType #28:#29 // println:(I)V
#22 = Utf8 Try
#23 = Utf8 java/lang/Object
#24 = Utf8 java/lang/System
#25 = Utf8 out
#26 = Utf8 Ljava/io/PrintStream;
#27 = Utf8 java/io/PrintStream
#28 = Utf8 println
#29 = Utf8 (I)V
{
public Try();
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 5: 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 #4 // int 10000000
5: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
8: return
LineNumberTable:
line 7: 0
line 8: 8
}
SourceFile: "Try.java"
在常量池索引4处,我们看到了10000000,,接下来我们反编译如下代码:
class deal
{ static {System.out.println("加载了deal类");}
static final int x=10000000;
}
public class Try {
public static void main(String[] args) {
}
}
反编译Try.class文件后:
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // Try
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 main
#9 = Utf8 ([Ljava/lang/String;)V
#10 = Utf8 SourceFile
#11 = Utf8 Try.java
#12 = NameAndType #4:#5 // "<init>":()V
#13 = Utf8 Try
#14 = Utf8 java/lang/Object
{
public Try();
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 5: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0
}
SourceFile: "Try.java"
在常量池中并未看到常量10000000
下面讲讲我个人的一些理解:
在编译下列代码(或者说在编译访问常量的代码时)时
System.out.println(deal.x);
编译器做了一个等价处理,编译器从源文件上文得知deal.x是一个常量,所以会将该常量的值10000000放入常量池,同时利用ldc指令进行访问,所以我们实际上是直接访问常量池中的10000000,此时JVM并未分配内存给x,我们可以理解为static final常量在编译期就将值放入到访问了该常量的类文件的常量池中。
下面我们来看看常量如何在运行时加入常量池,由于目前我不知道如何查看运行时常量池,所以无法给出直观的例子。
在运行时常量加入常量池最典型的就是String的intern方法。来看看下面的代码:
public class Try {
public static void main(String[] args) {
String m=new String("12")+new String("34");
String x=m.intern();
}
}
intern方法的作用如下:
1、若m的字符串值在常量池中,则返回m的引用。
2、若m的字符串值不在常量池中(在堆内存中),但常量池中含有该字符串值,则返回常量池中该字符串值的引用。
3、若m的字符串值不在常量池中(在堆内存中),且常量池中不含有该字符串值,则在常量池中创建一个与m的字符序列相同的字符串值,然后返回新创建字符串值的引用。
先来看看Try.class文件中的常量池
虽然我们调用了intern,但在Try.class中并未看到字符串"1234",因为在编译时并未运行intern函数,编译器只会收集源文件中标识出并使用的常量或是常量相加后的常量(例如int n=1+2,则3会在编译期被放入常量池),例如上述代码中的“12”和“34”。在代码中调用intern满足情况3,在运行时会将“1234”添加到运行时常量池中。
以上。