常量池(constant_pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量和符号引用。运行时常量池是方法区的一部分。
在Class文件结构中,最头的4个字节用于存储魔数Magic Number,用于确定一个文件是否能被JVM接受,再接着4个字节用于存储版本号,前2个字节存储次版本号,后2个存储主版本号,再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个U2类型的数据(constant_pool_count)存储常量池容量计数值。常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
类和接口的全限定名
字段名称和描述符
方法名称和描述符
Java中八种基本类型的包装类的大部分都实现了常量池技术,它们是Byte、Short、Integer、Long、Character、Boolean,另外两种浮点数类型的包装类(Float、Double)则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值在-128到127时才可使用对象池。
看下面代码:
/** * Huisou.com Inc. * Copyright (c) 2011-2012 All Rights Reserved. */ /** * @description * @package * @title Test.java * @author chenzehe * @email * @version * @updateUser * @create 2011-12-21 上午11:27:48 * @update 2011-12-21 上午11:27:48 */ public class Test { public static void main(String[] args) throws Exception { Integer a = 127; Integer b = 127; Integer c = 128; Integer d = 128; System.out.println(a == b);// 输出true System.out.println(c == d);// 输出false } }
使用javap查看生成的字节码:
E:\chenzehe\workspace_b2b_3th\test\src>javap -verbose Test Compiled from "Test.java" public class Test extends java.lang.Object SourceFile: "Test.java" minor version: 0 major version: 50 Constant pool: const #1 = Method #6.#21; // java/lang/Object."<init>":()V const #2 = Method #22.#23; // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; const #3 = Field #24.#25; // java/lang/System.out:Ljava/io/PrintStream; const #4 = Method #26.#27; // java/io/PrintStream.println:(Z)V const #5 = class #28; // Test const #6 = class #29; // java/lang/Object const #7 = Asciz <init>; const #8 = Asciz ()V; const #9 = Asciz Code; const #10 = Asciz LineNumberTable; const #11 = Asciz main; const #12 = Asciz ([Ljava/lang/String;)V; const #13 = Asciz StackMapTable; const #14 = class #30; // "[Ljava/lang/String;" const #15 = class #31; // java/lang/Integer const #16 = class #32; // java/io/PrintStream const #17 = Asciz Exceptions; const #18 = class #33; // java/lang/Exception const #19 = Asciz SourceFile; const #20 = Asciz Test.java; const #21 = NameAndType #7:#8;// "<init>":()V const #22 = class #31; // java/lang/Integer const #23 = NameAndType #34:#35;// valueOf:(I)Ljava/lang/Integer; const #24 = class #36; // java/lang/System const #25 = NameAndType #37:#38;// out:Ljava/io/PrintStream; const #26 = class #32; // java/io/PrintStream const #27 = NameAndType #39:#40;// println:(Z)V const #28 = Asciz Test; const #29 = Asciz java/lang/Object; const #30 = Asciz [Ljava/lang/String;; const #31 = Asciz java/lang/Integer; const #32 = Asciz java/io/PrintStream; const #33 = Asciz java/lang/Exception; const #34 = Asciz valueOf; const #35 = Asciz (I)Ljava/lang/Integer;; const #36 = Asciz java/lang/System; const #37 = Asciz out; const #38 = Asciz Ljava/io/PrintStream;; const #39 = Asciz println; const #40 = Asciz (Z)V; { public Test(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 18: 0 public static void main(java.lang.String[]) throws java.lang.Exception; Code: Stack=3, Locals=5, Args_size=1 0: bipush 127 2: invokestatic #2; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 5: astore_1 6: bipush 127 8: invokestatic #2; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 11: astore_2 12: sipush 128 15: invokestatic #2; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 18: astore_3 19: sipush 128 22: invokestatic #2; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 25: astore 4 27: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream; 30: aload_1 31: aload_2 32: if_acmpne 39 35: iconst_1 36: goto 40 39: iconst_0 40: invokevirtual #4; //Method java/io/PrintStream.println:(Z)V 43: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream; 46: aload_3 47: aload 4 49: if_acmpne 56 52: iconst_1 53: goto 57 56: iconst_0 57: invokevirtual #4; //Method java/io/PrintStream.println:(Z)V 60: return LineNumberTable: line 20: 0 line 21: 6 line 22: 12 line 23: 19 line 24: 27 line 25: 43 line 26: 60 StackMapTable: number_of_entries = 4 frame_type = 255 /* full_frame */ offset_delta = 39 locals = [ class "[Ljava/lang/String;", class java/lang/Integer, class java/lang/Integer, class java/lang/Inte ger, class java/lang/Integer ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/lang/Integer, class java/lang/Integer, class java/lang/Inte ger, class java/lang/Integer ] stack = [ class java/io/PrintStream, int ] frame_type = 79 /* same_locals_1_stack_item */ stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/lang/Integer, class java/lang/Integer, class java/lang/Inte ger, class java/lang/Integer ] stack = [ class java/io/PrintStream, int ] Exceptions: throws java.lang.Exception }
Integer a = 127;对应的指令为:
0: bipush 127 2: invokestatic #2; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
Integer c = 128;对应的指令为:
12: sipush 128 15: invokestatic #2; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
bipush指令意思是将单字节的常量值(-128~127)推送到栈顶
sipush指令意思是将短型的常量值(-32768~32767)推送到栈顶
invokestatic指令意思是调用静态方法,这里调用的是常量池中#2指向的方法java/lang/Integer.valueOf,查看Integer.valueOf方法:
public static Integer valueOf(int i) { final int offset = 128; if (i >= -128 && i <= 127) { // must cache return IntegerCache.cache[i + offset]; } return new Integer(i); }
IntegerCache代码:
private static class IntegerCache { private IntegerCache(){} static final Integer cache[] = new Integer[-(-128) + 127 + 1]; static { for(int i = 0; i < cache.length; i++) cache[i] = new Integer(i - 128); } }
其它封装类如下:
//Boolean类也实现了常量池技术 Boolean bool1=true; Boolean bool2=true; System.out.println(bool1==bool2); //输出true //浮点类型的包装类没有实现常量池技术 Double d1=1.0; Double d2=1.0; System.out.println(d1==d2); //输出false
String s = new String( "xyz" ); 在运行时涉及 几个String实例?
两个,一个是字符串字面量"xyz"所对应的、驻留(intern)在一个全局共享的字符串常量池中的实例,另一个是通过new String(String)创建并初始化的、内容与"xyz"相同的实例。
String中的final用法和理解:
final只对引用的"值"(即内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。
final StringBuffer a = new StringBuffer("111"); final StringBuffer b = new StringBuffer("222"); a=b;//此句编译不通过 final StringBuffer a = new StringBuffer("111"); a.append("222");//编译通过
String a = "a1"; String b = "a" + 1; System.out.println((a == b)); //result = true String a = "atrue"; String b = "a" + "true"; System.out.println((a == b)); //result = true String a = "a3.4"; String b = "a" + 3.4; System.out.println((a == b)); //result = true
JVM对于字符串常量的"+"号连接,将程序编译期,JVM就将常量字符串的"+"连接优化为连接后的值,拿"a" + 1来说,经编译器优化后在class中就已经是a1。在编译期其字符串常量的值就确定下来,故上面程序最终的结果都为true。
String a = "ab"; String bb = "b"; String b = "a" + bb; System.out.println((a == b)); //result = false
JVM对于字符串引用,由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a" + bb无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给b。所以上面程序的结果也就为false。
String a = "ab"; final String bb = "b"; String b = "a" + bb; System.out.println((a == b)); //result = true
和上面唯一不同的是bb字符串加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的"a" + bb和"a" + "b"效果是一样的。故上面程序的结果为true。
String a = "ab"; final String bb = getBB(); String b = "a" + bb; System.out.println((a == b)); //result = false private static String getBB() { return "b"; }
JVM对于字符串引用bb,它的值在编译期无法确定,只有在程序运行期调用方法后,将方法的返回值和"a"来动态连接并分配地址为b,故上面程序的结果为false。
通过上面4个例子可以得出得知:
String s = "a" + "b" + "c";
就等价于String s = "abc"; int i = 1+2+3;也等价于int i = 6;
String a = "a"; String b = "b"; String c = "c"; String s = a + b + c;
这个就不一样了,最终结果等于:
StringBuffer temp = new StringBuffer(); temp.append(a).append(b).append(c); String s = temp.toString();
由上面的分析结果,可就不难推断出String 采用连接运算符(+)效率低下原因分析,形如这样的代码:
public class Test { public static void main(String args[]) { String s = null; for(int i = 0; i < 100; i++) { s += "a"; } } }
每做一次 + 就产生个StringBuilder对象,然后append后就扔掉。下次循环再到达时重新产生个StringBuilder对象,然后 append 字符串,如此循环直至结束。 如果我们直接采用 StringBuilder 对象进行 append 的话,我们可以节省 N - 1 次创建和销毁对象的时间。所以对于在循环中要进行字符串连接的应用,一般都是用StringBuffer或StringBulider对象来进行 append操作。
String.intern()解析
Java语言并不要求常量一定只能在编译期产生,运行时也可能将新的常量放入常量池中,这种特性用的最多的就是String.intern()方法。
String的intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加 一个Unicode等于str的字符串并返回它的引用。
String s0= "xyz"; String s1=new String("xyz"); String s2=new String("xyz"); System.out.println(s0==s1); s1.intern(); s2=s2.intern(); //把常量池中“pku”的引用赋给s2 System.out.println( s0==s1); System.out.println( s0==s1.intern() ); System.out.println( s0==s2 );
输出为:
false
false //虽然执行了s1.intern(),但它的返回值没有赋给s1
true //说明s1.intern()返回的是常量池中”pku”的引用
true
有人说,“使用String.intern()方法则可以将一个String类的保存到一个全局String表中,如果具有相同值的Unicode字符串 已经在这个表中,那么该方法返回表中已有字符串的地址,如果在表中没有相同值的字符串,则将自己的地址注册到表中“如果我把他说的这个全局的String 表理解为常量池的话,他的最后一句话,“如果在表中没有相同值的字符串,则将自己的地址注册到表中”是错的:
String s1=new String("xyz"); String s2=s1.intern(); System.out.println( s1==s1.intern() ); System.out.println( s1+" "+s2 ); System.out.println( s2==s1.intern() );
输出为:
false
xyz xyz
true