一、静态常量池和运行时常量池
我们通过javap -v MyTest.class
解析字节码文件
public class com.example.spring.jvmTest.MyTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#23 // java/lang/Object."<init>":()V
#2 = Methodref #5.#24 // com/example/spring/jvmTest/MyTest.math:()V
#3 = Class #25 // com/example/spring/jvmTest/A
#4 = Methodref #3.#23 // com/example/spring/jvmTest/A."<init>":()V
#5 = Class #26 // com/example/spring/jvmTest/MyTest
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/example/spring/jvmTest/MyTest;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 math
#19 = Utf8 a
#20 = Utf8 Lcom/example/spring/jvmTest/A;
#21 = Utf8 SourceFile
#22 = Utf8 MyTest.java
#23 = NameAndType #7:#8 // "<init>":()V
#24 = NameAndType #18:#8 // math:()V
#25 = Utf8 com/example/spring/jvmTest/A
#26 = Utf8 com/example/spring/jvmTest/MyTest
#27 = Utf8 java/lang/Object
{
public com.example.spring.jvmTest.MyTest();
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/spring/jvmTest/MyTest;
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: invokestatic #2 // Method math:()V
3: return
LineNumberTable:
line 6: 0
line 7: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
public static void math();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=0
0: new #3 // class com/example/spring/jvmTest/A
3: dup
4: invokespecial #4 // Method com/example/spring/jvmTest/A."<init>":()V
7: astore_0
8: return
LineNumberTable:
line 10: 0
line 11: 8
LocalVariableTable:
Start Length Slot Name Signature
8 1 0 a Lcom/example/spring/jvmTest/A;
}
SourceFile: "MyTest.java"
其中的Constant pool就是我们的静态常量池,即在加载期间class文件内部的常量池
而我们代码在运行的过程中,需要把这些静态常量池中的数据加载到内存中,一旦加载进内存,就叫做运行时常量池
二、字符串常量池
JDK1.6之前字符串常量池位于方法区(永久代),1.6之后从方法区脱离出来,放于堆区
示例1(JDK1.8的环境):
public static void main(String[] args) {
String s1 = "hello";
String s2 = new String("hello");
String s3 = s2.internn();
System.err.println(s1==s2);
System.err.println(s2==s3);
System.err.println(s1==s3);
StringBuilder sb = new StringBuilder();
sb.append("llo");
String s4 = sb.toString().intern();
System.err.println("llo".equals(s4));
}
输出:
false
false
true
true
-
String s1 = "hello"
会去字符串常量池中通过equals()找是否有"hello"的对象,如果有,则直接返回该字符串。如果没有,则在常量池中创建一个"hello"的对象 -
String s2 = new String("hello")"
,它分为两步,
- 先去字符串常量池中通过equals()找是否有"hello"的对象,如果没有,则在常量池中创建一个"hello"的对象,如果有,就进行第二步。
- 再去堆区创建一个"hello"的字符串对象,返回堆区对象的引用
String s3 = s2.internn()
会去字符串常量池中通过equals()找是否有s2的对象,如果有,则直接返回该对象。如果没有:
- 如果是JDK1.6,则在字符串常量池中创建一个新的对象,并且在字符串常量池中新建一个指针,指向字符串常量池中的新对象,返回新对象的引用
- 如果是JDK1.6以上,则在常量池中新建一个指针,指向堆中的对象,返回堆中对象的引用
sb.append("llo")
会直接在堆区创建一个"llo"的对象,不会在字符串常量池中创建,返回堆区对象的引用。
示例2(JDK1.8的环境):
public static void main(String[] args) {
String s1 = new String("he")+new String("llo");
String s3 = s1.intern();
System.err.println(s1==s3);
}
JDK6输出:false
JDK6以上输出:true
- new String(“he”),先去字符串常量池中通过equals()找是否有"he"的字符串,没有,在字符串常量池中创建一个"he"的字符串,再去堆区创建一个"he"的字符串对象
- new String(“llo”),先去字符串常量池中通过equals()找是否有"llo"的字符串,没有,在字符串常量池中创建一个"llo"的字符串,再去堆区创建一个"llo"的字符串对象
- “he”+“llo”,代码在编译后变成了
sb.append("he").append("llo")
,既然是StringBuider,则只会在堆区生成一个"hello"对象 - s1.intern(),先去字符串常量池中通过equals()找是否有s1的字符串,没有,则返回堆中的对象
示例3(JDK1.8的环境):
public static void main(String[] args) {
String ab = "ab";
String k = "a"+"b";
System.err.println(ab==k);
}
输出:true
"a"+"b"
这种常量之间的"+“号会在编译期间优化为"ab"
,它们都指向字符串常量池中的"ab”,所以为true
示例4(JDK1.8的环境):
public static void main(String[] args) {
String ab = "ab";
String b = "b";
String c = "a"+b;
System.err.println(ab==c);
}
输出:false
"a"+b
这种因为引用类型b在编译期间是无法确定具体的值的, 编译之后变成了StringBuider(“a”).append(b),即会在堆中生成"ab"对象,c指向这个对象,所以为false
示例5(JDK1.8的环境):
public static void main(String[] args) {
String ab = "ab";
final String b = "b";
String c = "a"+b;
System.err.println(ab==c);
}
输出:true
和上面的例子就一个final的区别,final修饰的变量在编译期间就能确定下来,即"a"+b
就是"a"+"b"
,所以为true
三、基本类型包装类的对象池
我们来看一段代码 :
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
Integer a1 = 128;
Integer b1 = 128;
Integer a2 = new Integer(127);
Integer b2 = new Integer(127);
System.err.println(a==b);
System.err.println(a1==b1);
System.err.println(a2==b2);
}
输出:
true
false
false
那么为什么呢?
- 其实是Integer内部维护了一个本地缓存,缓存范围是-128到127,
Integer a = 127
在编译期间变成了Integer.valueOf(127)
,会直接走缓存逻辑,在缓存内的数字,直接从缓存拿即可 - 像
new Integer(127)
这种不会走缓存逻辑的,而是直接去堆区创建对象
注意:
8大包装类中除了Float和Double没有实现缓存以外(因为Float和Double的范围太广了,没有缓存的必要),其他的包装类都实现了