枯燥的JVM - 字符串常量池

字符串常量池的设计思想
  1. 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能。
  2. JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。
    • 为字符串开辟一个字符串常量池,类似于缓存区。
    • 创建字符串常量时,首先查询字符串常量池是否存在该字符串。
    • 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中。
三种字符串操作(Jdk1.7 及以上版本)
  • 直接赋值字符串
String name = "lilei"; // name 指向常量池中的引用

这种方式创建的字符串对象,只会在常量池中。

因为有 "lilei" 这个字面量,创建对象 name 的时候,JVM 会先去常量池中通过 equals(key) 方法,判断是否存在相同的对象。
不存在:会在常量池中创建一个新对象,再返回引用。
存在:直接返回该对象在常量池中的引用。

  • new String();
1 String name = new String("meimei"); // name 指向内存中的对象引用

这种方式会保证字符串常量池和堆中都有这个对象,没有就创建,最后返回堆内存中的对象引用。

因为有 "meimei" 这个字面量,所以会先检查字符串常量池中是否存在字符串 "meimei"。
不存在:先在字符串常量池里创建一个字符串对象;再去内存中创建一个字符串对象 "meimei";
存在:直接去堆内存中创建一个字符串对象 "meimei";
最后,将内存中的引用返回。

  • intern方法
String s1 = new String("hanxin"); 
String s2 = s1.intern(); 
System.out.println(s1 == s2);  // false

String 中的 intern 方法是一个 native 的方法,当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(用 equals(oject) 方法确定),则返回池中的字符串。否则,将intern 返回的引用指向当前字符串 s1 (jdk1.6版本需要将 s1 复制到字符串常量池里)。

字符串常量池位置
Jdk1.6及之前:有永久代,运行时常量池在永久代,运行时常量池包含字符串常量池。
Jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里。
Jdk1.8及之后:无永久代,运行时常量池在元空间,字符串常量池里依然在堆里。

字符串常量池设计原理

字符串常量池底层是 hotspot 的 C++ 实现的,底层类似一个HashTable,保存的本质上是字符串对象的引用。 看一道比较常见的面试题,下面的代码创建了多少个 String 对象?

String s1 = new String("he") + new String("llo"); 
String s2 = s1.intern(); 
System.out.println(s1 == s2); 
// 在 JDK 1.6 下输出是 false,创建了 6 个对象 
// 在 JDK 1.7 及以上的版本输出是 true,创建了 5 个对象 
// 当然我们这里没有考虑GC,但这些对象确实存在或存在过

为什么输出会有这些变化呢?主要还是字符串池从永久代中脱离、移入堆区的原因,intern() 方法也相应发生了变化:
1、在 JDK 1.6 中,调用 intern() 首先会在字符串池中寻找 equal() 相等的字符串,假如字符串存在就返回该字符串在字 符串池中的引用;假如字符串不存在,虚拟机会重新在永久代上创建一个实例,将 StringTable 的一个表项指向这个新创建的实例。

image.png

2、在 JDK 1.7 (及以上版本)中,由于字符串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的对象。字符串存在时和 JDK 1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例。


image.png

由上面两个图,可以得出:
JDK 1.6 字符串池溢出会抛出 OutOfMemoryError: PermGen space ,而在 JDK 1.7 及以上版本抛出 OutOfMemoryError: Java heap space 。

String常量池问题的几个例子

示例1:

String s1 = "lilei"; 
String s2 = "li" + "lei"; 
String s3 = "meimei2";
String s4 = "meimei" + 2;
System.out.println(s1 == s2);  // true
System.out.println(s3 == s4);  // true

分析:当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以 s2 也同样在编译期就被优化为一个字符串常量 "lilei",所以 s2 也是常量池中 ” lilei” 的一个引用,s3与s4同理。

示例2:

String s0 = "lilei"; 
String s1 = new String("lilei");  
String s2 = "li" + new String("lei"); 
System.out.println(s0 == s1); // false 
System.out.println(s0 == s2); // false 
System.out.println(s1 == s2); // false

分析:用new String() 创建的字符串不是常量,不能在编译期就确定,所以 new String() 创建的字符串不放入常量池中,它们有自己的地址空间。 s0 还是常量池 中 "lilei” 的引用,s1 因为无法在编译期确定,所以是运行时创建的新对象 ”lilei” 的引用,s2 因为有后半部分 new String(”lei”) 所以也无法在编译期确定,所以也是一个新创建对象 ”lilei” 的引用。

示例3:

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。

关于String是不可变的
String s = "a" + "b" + "c"; //就等价于String s = "abc"; 
String a = "a"; 
String b = "b"; 
String c = "c"; 
String s1 = a + b + c;
System.out.println(s == s1); //false

s1 这个就不一样了,可以通过观察其JVM指令码发现s1的 "+" 操作会变成如下操作:

0: ldc           #2                  // String abc
2: astore_1
3: ldc           #3                  // String a
5: astore_2
6: ldc           #4                  // String b
8: astore_3
9: ldc           #5                  // String c
11: astore        4
13: new           #6                  // class java/lang/StringBuilder
16: dup
17: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
20: aload_2
21: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: aload_3
25: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: aload         4
30: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
36: astore        5
38: return

最后再看一个例子:

//字符串常量池:"计算机"和"技术" 堆内存:str1引用的对象"计算机技术" 
//堆内存中还有个StringBuilder的对象,但是会被gc回收,StringBuilder的toString方法会new String(),这个String才是真正返回的对 象引用 
String str2 = new StringBuilder("计算机").append("技术").toString(); //没有出现"计算机技术"字面量,所以不会在常量池里生 成"计算机技术"对象
System.out.println(str2 == str2.intern()); //true 
//计算机技术 在池中没有,但是在heap中存在,则intern时,会直接返回该heap中的引用 
//字符串常量池:"ja"和"va" 堆内存:str1引用的对象"java" 
//堆内存中还有个StringBuilder的对象,但是会被gc回收,StringBuilder的toString方法会new String(),这个String才是真正返回的对 象引用 
String str1 = new StringBuilder("ja").append("va").toString(); //没有出现"java"字面量,所以不会在常量池里生成"java"对象 
System.out.println(str1 == str1.intern()); //false 
//java是关键字,在JVM初始化的相关类里肯定早就放进字符串常量池了
String s1 = new String("test"); 
System.out.println(s1 == s1.intern()); //false 
//"test"作为字面量,放入了池中,而new时s1指向的是heap中新生成的string对象,s1.intern()指向的是"test"字面量之前在池中生成的 字符串对象
String s2 = new StringBuilder("abc").toString(); 
System.out.println(s2 == s2.intern()); //false 
//同上

八种基本类型的包装类和对象池

java 中基本类型的包装类的大部分都实现了常量池技术(严格来说应该叫对象池,在堆上),这些类是 Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外 Byte,Short,Integer,Long,Character 这 5 种整型的包装类也只是在对应值小于等于 127 时才可使用对象池,也即对象不负责创建和管理大于 127 的这些类的对象。因为一般这种比较小的数用到的概率相对较大。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348