文章目录

  • 1. 常量池概述
    • 1.1 静态常量池、运行时常量池与字符串常量池的区别
  • 2. 字符串常量池的设计初衷
  • 3. 字符串的几种创建方式及原理
    • ①:直接赋值
    • ②:new String()
    • ③:intern()方法
  • 3. 面试题:字符串比较
  • 4. 从字节码层面分析字符串相加"+"的原理
  • 5. 其他的常量池

1. 常量池概述

java代码经过编译后会生成.class字节码文件,随便打开一个target目录下的.class文件,可以看到cafe babe魔数等其他的二进制数据!


        可以看到.class文件中除了魔数、主次版本等,其他的都属于常量池。常量池可以看做存放java代码的,主要存放两大类常量:字面量和符号引用。

  • 字面量:字面量就是指由字母、数字等构成的字符串或者数值常量。字面量只可以右值出现,所谓右值是指等号右边的值,如:int a=1 这里的a为左值,1为右值。在这个例子中1就是字面量。
int a = 1;
int b = 2;
int c = "abcdefg";
int d = "abcdefg";
  • 符号引用:上面的a、b就是字段名称,就是一种符号引用,符号引用可以是:

    • 类和接口的全限定名 com.xx.User
    • 字段的名称和描述符 name
    • 方法的名称和描述符 set()

1.1 静态常量池、运行时常量池与字符串常量池的区别

像这些静态的、未加载的.class文件的数据被称为静态常量池,但经过jvm.class文件装入内存、加载到方法区后,常量池就会变为运行时常量池!对应的符号引用在程序加载或运行时会被转变为被加载到方法区的代码的直接引用,在jvm调用这个方法时,就可以根据这个直接引用找到这个方法在方法区的位置,然后去执行。

字符串常量池又是运行时常量池中的一小部分,字符串常量池的位置在jdk不同版本下,有一定区别!

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

验证:不同jdk版本中,往ArrayList<String>中添加字符串

/*** jdk6:-Xms6M -Xmx6M -XX:PermSize=6M -XX:MaxPermSize=6M  * jdk8:-Xms6M -Xmx6M -XX:MetaspaceSize=6M -XX:MaxMetaspaceSize=6M*/
public class RuntimeConstantPoolOOM{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);}}
}

运行结果:可以看到jdk7jdk6抛出的异常是在不同的区域

  • jdk7及以上抛的异常:Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
  • jdk6抛的异常:Exception in thread “main” java.lang.OutOfMemoryError: PermGen space

2. 字符串常量池的设计初衷

字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能。

JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化:为字符串开辟一个字符串常量池,类似于缓存区。在创建字符串常量时:

  • 首先查询字符串常量池是否存在该字符串
  • 如果存在该字符串,返回引用实例
  • 如果不存在,先实例化该字符串并放入池中(),然后再把这个字符串返回。
  • 下次再调用时,直接从字符串常量池中取值!

字符串常量池底层是hotspot的C++实现的,底层类似一个哈希表k-v结构的, 保存的本质上是字符串对象的引用。

3. 字符串的几种创建方式及原理

字符串的创建分为以下三种:

  • 直接赋值
  • new String()
  • intern方法

①:直接赋值

这种方式创建的字符串对象,只会在常量池中。返回的也只是字符串常量池中的对象引用!

String s = "aaa";  // s指向常量池中的引用

步骤如下:

  • 因为有aaa这个字面量,在创建对象s的时候,JVM会先去常量池中通过 equals(key) 方法,判断是否有相同的对象
  • 如果有,则直接返回该对象在常量池中的引用
  • 如果没有,则会在常量池中创建一个新对象,再返回常量池中aaa的对象引用。

②:new String()

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

String s1 = new String("aaa");  // s1指向内存中的对象引用

步骤如下:

  • 同上,看到有"aaa"这个字面量,就会先去字符串常量池中检查是否存在字符串"aaa"
  • 如果不存在,先在字符串常量池里创建一个字符串对象"aaa";再去堆内存中创建一个字符串对象"aaa"
  • 如果存在,就直接去堆内存中创建一个字符串对象"aaa"
  • 无论存不存在,都只返回堆内存中的字符串对象"aaa"的引用

③:intern()方法

String s1 = new String("aaa");
String s2 = s1.intern();System.out.println(s1 == s2);  //false

intern方法是一个 native 的方法,当调用 intern方法时:

  • 如果字符串常量池中已经包含一个等于"aaa"的字符串(用equals(oject)方法确定),则返回字符串常量池中的字符串"aaa"
  • 如果字符串常量池中没有"aaa"这样一个字符串,则会将intern返回的引用指向当前字符串 s1,也就说会返回堆中的"aaa"

注意:在jdk1.6版本及以前,如果字符串常量池中没有"aaa" 这样一个字符串 ,还需要将"aaa" 复制到字符串常量池里,然后返回字符串常量池中的这个新创建的字符串"aaa"

3. 面试题:字符串比较

①:下面的代码创建了多少个 String 对象(intern方法的理解)?

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 个对象

原理如下:

  • String s1 = new String("he") + new String("llo")这段代码首先会先根据字面量在字符串常量池中创建hello这两个对象
  • 然后根据new String又会在堆中创建hello这两个对象,此时一共四个对象
  • 然后再根据+号,合成一个新的对象hello,这个对象是存在堆中的,一共五个对象

上述过程无论是jdk哪个版本都是一致的,但在调用intern()方法时,需要区分jdk版本:
        
在 jdk1.6 中调用 intern() 方法时:

  • 首先会在字符串常量池中寻找与hello(s1的值)equal()方法 相等的字符串
  • 假如字符串存在,就返回该字符串在字符串常量池中的引用
  • 假如字符串不存在,jvm会重新在永久代上创建一个实例,并返回这个字符串常量池中新建的实例hello
  • 这道题显然是不存在的,所以s2的引用是字符串常量池中新建的实例hello,而s1的引用则是堆中的实例hellos1 == s2又比较的是引用地址,所以在jdk1.6创建了 6 个对象,输出是 false

在 jdk1.8 中调用 intern() 方法时:

  • 在 JDK 1.7 (及以上版本)中,由于字符串常量池不在永久代了,放在了堆中,刚好字符串对象s1也是存在于堆中的,所以intern() 做了一些修改,为了更方便地利用堆中的对象,省去了字符串常量池的复制操作!可以直接指向堆上的实例hello
  • 所以在 JDK 1.7 (及以上版本)中,只需要创建5个对象,且intern()返回的对象s2也是堆中的对象hellos1s2同为堆中对象hello的引用,所以s1 == s2,返回true

②:字符串比较

示例1:

String s0="ab";
String s1="ab";
String s2="a" + "b";
System.out.println( s0==s1 ); //true
System.out.println( s0==s2 ); //truea

解析:

  • 例子中的 s0和s1中的ab都是字符串常量,它们在编译期就被确定了,所以s0 == s1为 true;
  • String s2="a" + "b"字面量a和字面量b直接相加,在编译期就被优化为一个字符串常量"ab",所以s0 、s1 、s2都可以看作是sx = "ab",返回的都是字符串常量池中的ab,无论怎么比较都是相等的!

示例2:

String s0="ab";
String s1=new String("ab");
String s2="a" + new String("b");
System.out.println( s0==s1 );  // false
System.out.println( s0==s2 );  // false
System.out.println( s1==s2 );  // false

解析:

  • s0 指向字符串常量池中的ab,s1指向堆中的ab,两者不相等
  • s2因为有后半部分 new String(”b”),所以jvm无法在编译期确定,所以也是一个新创建对象”ab”的引用,s2也相当于new String("ab"),与其他的都不想等!

示例3:

  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

示例4:

String a = "ab";
String bb = "b";
String b = "a" + bb;System.out.println(a == b); // false

分析:

  • JVM对于字符串引用,由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a" + bb无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给b。所以上面程序的结果也就为false

示例5:

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

示例6:

String a = "ab";
final String bb = getBB();
String b = "a" + bb;System.out.println(a == b); // falseprivate static String getBB()
{  return "b";  }

分析:

  • JVM对于字符串引用bb,它的值在编译期无法确定,只有在程序运行期调用方法后,将方法的返回值和"a"来动态连接并分配地址为b,故上面程序的结果为false

示例7:

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

4. 从字节码层面分析字符串相加"+"的原理

先看如下代码:分析为什么s0s1不相等!

public class Stringtest {public static void main(String[] args) {String  s0  =  "a" + "b" + "c";  //就等价于String s = "abc";String  a  =  "a";String  b  =  "b";String  c  =  "c";String  s1  =   a  +  b  +  c;}
}

运行程序后,在IDEA里会生成Stringtest.class,打开后发现:

//编译后的 Stringtest.class 文件
public class Stringtest {public Stringtest() {}public static void main(String[] args) {//s0编译后已经被优化成abcString s0 = "abc";    String a = "a";String b = "b";String c = "c";//s1则不会直接被编译器优化,而是使用StringBuilder.append().toString()生成(new StringBuilder()).append(a).append(b).append(c).toString();}
}

可以看到:

  • 对于字符串常量的相加操作:s0 = "a" + "b" + "c"经过编译后已经被优化成s0 = "abc",这种返回的是字符串常量池中的对象引用
  • 对于字符串引用的相加操作:String s1 = a + b + c;编译器无法对其进行优化,在底层会调用StringBuilder.append().toString()生成对象引用

那么两者不相等的原因只能出现在StringBuilder.append().toString()中!我们进一步查看StringBuilder的源码发现其toString方法居然是new String()

    @Overridepublic String toString() {// Create a copy, don't share the arrayreturn new String(value, 0, count);}

new String返回的是堆中的对象引用,与s0 = "abc"返回常量池的对象引用是肯定的不相等的!

注意:Stringtest.class是字节码文件的易读版本,如果不信,可以使用javap -c Stringtest.class来直接查看字节码文件!

 public static void main(java.lang.String[]);Code:0: ldc           #2                  // String abc s0直接被优化成abc2: astore_13: ldc           #3                  // String a5: astore_26: ldc           #4                  // String b8: astore_39: ldc           #5                  // String c11: astore        413: new           #6                  // class java/lang/StringBuilder16: dup17: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V20: aload_221: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;24: aload_325: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;28: aload         430: 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        538: return

5. 其他的常量池

java中基本类型的包装类的大部分都实现了常量池技术(严格来说应该叫对象池,在堆上),这些类是ByteShortIntegerLongCharacterBoolean。另外两种浮点数类型的包装类FloatDouble则没有实现。

ByteShortIntegerLongCharacter5种整型的包装类也只是在值为【-128 ,127】之间才可使用对象池, 因为一般这种比较小的数用到的概率相对较大。

public class IntegerTest {public static void main(String[] args) {//5种整形的包装类Byte,Short,Integer,Long,Character的对象,//在值小于127时可以使用对象池Integer i1 = 127;  //这种调用底层实际是执行的Integer.valueOf(127),里面用到了IntegerCache对象池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}
}

上述代码,使用javap -c IntegerTest.class查看字节码文件如下:

 public static void main(java.lang.String[]);Code:0: bipush        1272: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;5: astore_16: bipush        1278: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;11: astore_212: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;15: aload_116: aload_217: if_acmpne     2420: iconst_121: goto          2524: iconst_025: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V28: sipush        12831: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;34: astore_335: sipush        12838: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;41: astore        443: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;46: aload_347: aload         449: if_acmpne     5652: iconst_153: goto          5756: iconst_057: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V60: new           #5                  // class java/lang/Integer63: dup64: bipush        12766: invokespecial #6                  // Method java/lang/Integer."<init>":(I)V69: astore        571: new           #5                  // class java/lang/Integer74: dup75: bipush        12777: invokespecial #6                  // Method java/lang/Integer."<init>":(I)V80: astore        682: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;85: aload         587: aload         689: if_acmpne     9692: iconst_193: goto          9796: iconst_097: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V100: iconst_1101: invokestatic  #7                  // Method java/lang/Boolean.valueOf:(Z)Ljava/lang/Boolean;104: astore        7106: iconst_1107: invokestatic  #7                  // Method java/lang/Boolean.valueOf:(Z)Ljava/lang/Boolean;110: astore        8112: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;115: aload         7117: aload         8119: if_acmpne     126122: iconst_1123: goto          127126: iconst_0127: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V130: dconst_1131: invokestatic  #8                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;134: astore        9136: dconst_1137: invokestatic  #8                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;140: astore        10142: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;145: aload         9147: aload         10149: if_acmpne     156152: iconst_1153: goto          157156: iconst_0157: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V160: return
}

可以看到,当比较【-128,127】以内的数时,会调用ByteShortIntegerLongCharacterBoolean这五种包装类的valueOf()方法,以Integer为例:

    public static Integer valueOf(int i) {//如果在【low,hign】区间内,则统一使用IntegerCache缓存池中的对象if (i >= IntegerCache.low && i <= IntegerCache.high)return IntegerCache.cache[i + (-IntegerCache.low)];//如果不在【low,hign】区间内,则 new 一个对象,生成一个新的地址引用return new Integer(i);}

其中【low,hign】区间如下:

      static final int low = -128;static final int high;...int h = 127;high = h;

一文彻底搞懂字符串、字符串常量池原理相关推荐

  1. layer output 激活函数_一文彻底搞懂BP算法:原理推导+数据演示+项目实战(下篇)...

    在"一文彻底搞懂BP算法:原理推导+数据演示+项目实战(上篇)"中我们详细介绍了BP算法的原理和推导过程,并且用实际的数据进行了计算演练.在下篇中,我们将自己实现BP算法(不使用第 ...

  2. 一文彻底搞懂BP算法:原理推导+数据演示+项目实战(下篇)

    在"一文彻底搞懂BP算法:原理推导+数据演示+项目实战(上篇)"中我们详细介绍了BP算法的原理和推导过程,并且用实际的数据进行了计算演练.在下篇中,我们将自己实现BP算法(不使用第 ...

  3. java字符串的常量池

    1.对于引用类型来说,==进行的是地址值的比较. 2.双引号直接写的字符串在常量池当中,new的不在池当中.

  4. 一文彻底搞懂前端监控 等推荐

    大家好,我是若川.话不多说,这一次花了几个小时精心为大家挑选了20余篇好文,供大家阅读学习.本文阅读技巧,先粗看标题,感兴趣可以都关注一波,一起共同进步. 前端点线面 前端点线面 百度前端研发工程师, ...

  5. opc服务器是硬件吗,opc是什么(一文彻底搞懂什么是OPC)

    原标题:(opc是什么(一文彻底搞懂什么是OPC)) opc是什么(一文完全搞懂什么是OPC)从2000年终以来,我们就一直在运用OPC软件互操纵性范例,而那些正准备踏入和想要踏入工业自动化范畴的人们 ...

  6. 一文彻底搞懂Mybatis系列(十六)之MyBatis集成EhCache

    MyBatis集成EhCache 一.MyBatis集成EhCache 1.引入mybatis整合ehcache的依赖 2.类根路径下新建ehcache.xml,并配置 3.POJO类 Clazz 4 ...

  7. 一文彻底搞懂ROC曲线与AUC的概念

    一文彻底搞懂ROC曲线与AUC的概念 1. ROC曲线的初级含义 1.1 精确率和召回率 1.2 ROC曲线的含义 2. ROC曲线如何绘制 3. ROC曲线和排序有什么关联? 4. AUC和基尼系数 ...

  8. 一文快速搞懂Kudu到底是什么

    文章目录 引言 文章传送门: Kudu 介绍 背景介绍 新的硬件设备 Kudu 是什么 Kudu 应用场景 Kudu 架构 数据模型 分区策略 列式存储 整体架构 Kudu Client 交互 Kud ...

  9. 一文快速搞懂对95%置信区间的理解

    一文快速搞懂对95%置信区间的理解 综合知乎上各大神的解答和网络资料得到本文对95%置信区间的理解 先给出结论 最常出现的对置信区间的错误理解: 在95%置信区间内,有95%的概率包括真实参数  (错 ...

  10. 奇舞周刊第 440 期:一文彻底搞懂前端沙箱

    记得点击文章末尾的" 阅读原文 "查看哟~ 下面先一起看下本期周刊 摘要 吧~ 奇舞推荐 ■ ■ ■ 一文彻底搞懂前端沙箱‍ 沙箱是一种安全机制,为运行中‍的程序提供隔离环境.通常 ...

最新文章

  1. RabbitMQ(二):RabbitMQ高级特性
  2. php正则截取富文本编辑器中路径字符串_php使用正则表达式获取字符串中的URL
  3. linux I/O--I/O多路复用--详解(四)
  4. 集群、分布式、负载均衡区别与联系
  5. how to render AET extension field as code list
  6. .net core 整洁架构入门
  7. JAVA通过JCO连接SAP例子
  8. myeclipse-common 找不到
  9. oracle数据库卡住了无法保存,Oracle数据库使用NFS存储,启动报错提示无法锁定文件...
  10. python编程语言-初学者最容易学的六种编程语言
  11. Latex 资料汇总(持续更新)
  12. ubuntu 压缩率最高的软件_国外这款不知名的视频压缩软件,100M视频秒变2M,网友:太好用了...
  13. SQL的bit列名转换成access是/否数据类型,True无效的问题
  14. [转]vmlinuz
  15. 通达信 缠论分笔、分段DLL插件使用说明
  16. Linux信号量详解
  17. wangluorenzhengmoban
  18. TOYOTA MOTOR CORPORATION Programming Contest 2022(AtCoder Beginner Contest 270) AB题解
  19. 爬虫--Scrapy之Downloader Middleware
  20. APICloud实战--每日生鲜App

热门文章

  1. 天梯赛L2-010 排座位(并查集和\set)
  2. html5邀请函在线制作,很炫的html5年会邀请函
  3. linux11g导入10g 怎么改版本,Oracle 11g导入到10g引起的错误
  4. winsock使用java编写_利用Socket进行Java网络编程(一)
  5. laravel 中request
  6. Android屏幕禁止休眠的方法
  7. 使用IDEA+MVN 编译Spark 1.5.2 without hive
  8. 移动应用可以通过微信沟通接口连接公众号 微信涨粉多了一个新通道
  9. css @语法,@规则 @import @charset @font-face @fontdef @media @page
  10. netbsd配置gnome桌面