常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一。常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:
被模块导出或者开放的包;
类和接口的全限定名;
字段的名称和描述符;
方法的名称和描述符;
方法句柄和方法类型;
动态调用点和动态常量;
ps:这些常量在常量池中是静态信息,当运行时被加载到内存后,这些符号才有对应的内存地址信息,这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用在程序加载或运行时会被转变为被加载到内存区域的代码的直接引用,也就是我们说的动态链接。
1、八种基本类型的包装类和对象池
java中基本类型的包装类的大部分都实现了常量池技术(严格来说应该叫对象池,在堆上),这些类是 Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外 Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负 责创建和管理大于127的这些类的对象。因为一般这种比较小的数用到的概率相对较大。
示例代码:
/* * 5种整形的包装类Byte,Short,Integer,Long,Character的对象, * 在值小于127时可以使用对象池 */public static void main(String[] args) { //这种调用底层实际是执行的Integer.valueOf(127), //里面用到了IntegerCache对象池 Integer i1 = 127; Integer i2 = 127; System.out.println(i1 == i2);//输出true //值大于127时,不会从对象池中取对象 Integer i3 = 128; Integer i4 = 128; System.out.println(i3 == i4);//输出false //用new关键词新生成对象不会使用对象池 Integer i5 = new Integer(127); Integer i6 = new Integer(127); System.out.println(i5 == i6);//输出false //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}
2、字符串常量池
字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能。JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化为字符串开辟一个字符串常量池,类似于缓存区创建字符串常量时,首先查询字符串常量池是否存在该字符串存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中。
2.1 三种字符串的操作
1、直接赋值
String str = "abc";
这种方式创建的字符串对象,只会在常量池中。因为有"abc"这个字面量,创建对象str的时候,JVM会先去常量池中通过 equals(key) 方法,判断是否有相同的对象 。如果有则直接返回该对象在常量池中的引用;如果没有则会在常量池中创建一个新对象,再返回引用。
2、New 一个对象
String str = new String("abc");
这种方式会保证字符串常量池和堆中都有这个对象,没有就创建,最后返回堆内存中的对象引用。因为有"abc"这个字面量,所以会先检查字符串常量池中是否存在字符串"abc" 。如果不存在,先在字符串常量池里创建一个字符串对象;再去内存中创建一个字符串对象"abc";存在的话,就直接去堆内存中创建一个字符串对象"abc";最后,将内存中的引用返回。
3、 intern方法
String s1 = new String("abc"); String s2 = s1.intern();
String中的intern方法是一个 native 的方法,当调用 intern方法时,如果常量池已经包含一个等于此String对象的字符串,则返回池中的字符串。否则,将intern返回的引用指向当前字符串 s1(jdk1.6版本需要将 s1 复制到字符串常量池里)
2.2 字符串常量池位置
Jdk1.6及之前:有永久代, 运行时常量池在永久代,运行时常量池包含字符串常量池。
Jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里。
Jdk1.8及之后:无永久代,运行时常量池在元空间,字符串常量池里依然在堆里。
/** * jdk6:‐Xms6M ‐Xmx6M ‐XX:PermSize=6M ‐XX:MaxPermSize=6M * jdk8:‐Xms6M ‐Xmx6M ‐XX:MetaspaceSize=6M ‐XX:MaxMetaspaceSize=6M * @param args */public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); for (int i = 0; i < 10000000; i++) { String str = String.valueOf(i).intern(); list.add(str); }}运行结果:jdk7 及以上:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space jdk6:Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
示例代码1:
问:下面的代码创建了多少个 String 对象?
public static void main(String[] args) { String s1 = new String("he") + new String("llo"); String s2 = s1.intern(); System.out.println(s1 == s2);}
在不考虑GC的情况下,JDK 1.6 下输出是false,创建了6个对象。JDK 1.7 及以上的版本输出是true,创建了5个对象。 输出会有这些变化主要还是字符串池从永久代中脱离、移入堆区的原因。在不同的jdk版本中,intern方法发生了一些变化。
1、在 JDK 1.6 中,调用 intern() 首先会在字符串池中寻找 equal() 相等的字符串,假如字符串存在就返回该字符串在字 符串池中的引用;假如字符串不存在,虚拟机会重新在永久代上创建一个实例,将 StringTable 的一个表项指向这个新创建的实例。
2、在 JDK 1.7 (及以上版本)中,由于字符串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的对象。字符 串存在时和 JDK 1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例。
由上面两个图,也不难理解为什么 JDK 1.6 字符串池溢出会抛出 OutOfMemoryError: PermGen space ,而在 JDK 1.7 及以上版本抛出 OutOfMemoryError: Java heap space 。
示例代码2:
String s0="hello"; String s1="hello"; String s2="hel" + "lo"; System.out.println( s0==s1 ); //true System.out.println( s0==s2 ); //true
分析:因为例子中的s0 和s1 中的“hello” 都是字符串常量,它们在编译期就被确定了,所以s0==s1 为true;而“hel” 和“lo” 也都是字符串常量,当一个字 符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2 也同样在编译期就被优化为一个字符串常量"hello",所以s2 也是常量池中”hello” 的一个引用。所以我们得出 s0==s1==s2。
示例代码3:
String s0="hello"; String s1=new String("hello"); String s2="hel" + new String("lo"); System.out.println( s0==s1 ); // false System.out.println( s0==s2 );// false System.out.println( s1==s2 ); // false
分析:用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。s0还是常量池中"hello”的引用,s1因为无法在编译期确定,所以是运行时创建的新对象”hello”的引用,s2因为有后半部分 new String(”hel”)所以也无法在编译期确定,所以也是一个新创建对象”hello”的引用。
示例代码4:
String a = "a1"; String b = "a" + 1; System.out.println(a == b); // true String a = "atrue"; String b = "a" + "true"; System.out.println(a == b); // true String a = "a3.4"; String b = "a" + 3.4; System.out.println(a == b); // true
分析:JVM对于字符串常量的"+"号连接,将在程序编译期,JVM就将常量字符串的"+"连接优化为连接后的值,拿"a" + 1来说,经编译器优化后在class中就已经是a1。在编译期字符串常量的值就确定下来,故上面程序最终的结果都为 true。
示例代码5:
String a = "ab"; String bb = "b"; String b = "a" + bb; System.out.println(a == b); // false
分析:JVM对于字符串引用,由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的, 即"a" + bb无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给b。所以上面程序的结果也就为 false。
示例代码6:
String a = "ab"; final String bb = "b"; String b = "a" + bb; System.out.println(a == b); // true
分析:和示例4中唯一不同的是bb字符串加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷 贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的"a" + bb和"a" + "b"效果是一样的。故上面程序的结果 为true。
示例代码7:
String a = "ab"; final String bb = getBB(); String b = "a" + bb; System.out.println(a == b); // false private static String getBB() { return "b"; }
分析:JVM对于字符串引用bb,它的值在编译期无法确定,只有在程序运行期调用方法后,将方法的返回值和"a"来动态 连接并分配地址为b,故上面程序的结果为false。
示例代码8:
//就等价于String s = "abc"; String s = "a" + "b" + "c"; String a = "a"; String b = "b"; String c = "c"; String s1 = a + b + c;System.out.println(s == s1); // false
分析:通过s1的jvm指令码,可以发现s1 的操作会转变成如下操作:
StringBuilder temp = new StringBuilder(); temp.append(a).append(b).append(c); String s = temp.toString();
我们在来看一下StringBuilder 的toString 方法,故上面程序的结果为false。
public synchronized String toString() { if (toStringCache == null) { toStringCache = Arrays.copyOfRange(value, 0, count); } return new String(toStringCache, true);}