字符串常量池

字符串常量池的设计思想

字符串常量池是一个存储字符串常量的池子,它的设计思想是为了减少重复的字符串对象,从而节约内存空间和提高程序性能。在JDK1.7以前,字符串常量池位于方法区,在此之后挪到了堆中,但是在说明是,还是将常量池与堆分开来说。

设计字符串常量池的思想主要包括以下几点:

  • 重用字符串对象:通过将相同的字符串对象存储在同一个位置,可以避免创建重复的字符串对象。这可以减少内存占用,提高程序的性能。

  • 字符串常量池是全局共享的:在一个Java虚拟机中,所有类共享同一个字符串常量池。这意味着如果一个字符串对象已经存在于常量池中,它可以在多个类中共享,从而减少内存占用。

  • 字符串常量池的不变性:字符串常量池中的字符串对象是不可变的,详细的请看博客:(8条消息) 可变数据类型和不可变数据类型、String的不可变?_Hello CC7的博客-CSDN博客这也是字符串常量池设计的基础,如果一个字符串对象被创建之后发生变化,那么它将无法重用,从而失去了字符串常量池的优势。

  • 字符串常量池的实现方式:一种是直接使用字符串字面值创建字符串对象并存储在常量池中;另一种是使用String.intern()方法将字符串对象的引用添加到常量池中。

“字面量”何时加入到字符串常量池

字符串常量池在JDK1.7之前存的是实例,JDK1.7之后移到了堆中,存的是引用,真正的实例存储在堆中。本文主要介绍JDK1.7之后的字符串常量池。JVM中字符串常量池是利用StringTable类实现的,本质上是一个Hash表 ,表里存着字符串实例的引用。

字符串字面量指的是双引号“”创建的字符串,在堆中创建了对象后其引用插入到字符串常量池中(jdk1.7后),可以全局使用,遇到相同内容的字面量,就不需要再次创建。

String s=new String("abc");//语句1

对于语句1中的“abc”,在class文件的常量池里就存有该字面量的符号引用,在类加载过程的解析阶段,将该class文件常量池的内容加载到方法区的运行时常量池,然后在堆中创建“abc”实例(这个实例是为了字符串驻留引用创建的,和new关键字创建的不是一个),并将该实例的引用驻留到字符串常量池(在StringTable记录下这个引用),然后将运行时常量池中的符号引用转为直接引用。由于JVM规范指定解析阶段可以是lazy的,所以粗体部分在解析阶段可能不会发生。那么什么时候发生呢?

关于类加载相关内容见博客:关于Java类加载、双亲委派机制_Hello CC7的博客-CSDN博客

String s=new String("abc");

 0 new #7 <java/lang/String>//创建一个对象,并将其类的引用值压入栈顶,引用值对应常量池索引为7的项
 3 dup//复制栈顶数值,并将复制值压入栈顶
 4 ldc #9 <abc> //把常量池中第9个常量池项引用推到操作数栈栈顶,表示“abc”字符
 6 invokespecial #11 <java/lang/String.<init> : (Ljava/lang/String;)V>//实例初始化方法
 9 astore_1//操作数栈的栈顶元素出栈,将栈顶元素的值赋给局部变量表上索引为1的变量s
 10 return

这里插一下关于new对象的执行过程,new指令负责创建实例(包括分配空间、设定类型、所有字段设置默认值等工作,但是并未完成实例的初始化,即s表示的内容还不为“abc”),然后将引用s压到操作数栈栈顶,此时引用还不能使用,处于未初始化状态。但是可以使用它调用实例构造器(invokespecial指令),即<init>()方法。使用该方法可以初始化实例,那初始化数据必然要在此之前准备好。即通过dup和ldc指令将参数压到操作数栈,dup将栈顶的实例引用s复制一份并推至栈顶,ldc将“abc”常量从常量池推至栈顶。至此实例已被初始化,s也可使用了。

ldc 字节码在这里的执行语义是:到当前类的运行时常量池去查找该 index 对应的项,如果该项尚未解析则解析,也就是真正运行上面粗体部分内容。如果已解析即StringTable 已经记录了一个对应内容的 String 的引用则直接返回引用。

以上介绍的情况是class文件已有的字面量何时加到字符串常量池,也存在在运行代码过程中新创建class文件里没有的字面量的情况。

String s=new String("ab")+new String("c");

在该语句执行完,此时常量池只有“ab”、“c”这两个引用。但是可以通过intern()方法,将“abc”这个字符串的引用加到运行时常量池和字符串常量池。需要注意的是,由于此时堆内因为使用StringBuilder拼接“ab”和“c”时已经创建了一个实例“abc”,所以不需要再次创建新实例,直接将该实例的引用驻留到字符串常量池即可。

字符串示例解析

当JVM将class文件加载至内存时,会创建一个字符串常量池,将编译时期确定的字符串常量("xx"或"x"+"x")加入到字符串常量池。

String s1 = new String("abc");//语句1    
String s2 = "abc";//语句2    
String s3 = new String("abc");//语句3   

类加载阶段在常量池中创建了一个对象"abc"引用(真正对象在堆,跟代码执行阶段创建的不一样)。在代码执行阶段,语句1,语句3分别在堆中创建了一个类对象,并初始化为"abc",所以以上三个语句共创建3个对象。语句1,2,3分别创建了三个引用s1(指向语句1的堆中对象),s2(指向字符串常量池的"abc"对象(实际在堆中)),s3(指向语句3的堆中对象)。

"=="比较的是变量(存于栈中)是否指向同一个对象(存于堆中),变量是堆中对象的内存地址。

"equals()"最初也是比较对象的内存地址是否相等,由于String重写了equals()方法,所以比较的是字符串的内容。

System.out.println(s1 == s2);//语句4    
System.out.println(s1 == s3);//语句5    
System.out.println(s2 == s3);//语句6  
结果:false false false

由于s1、s2、s3分别指向三个不同的对象,所以全部false。 

System.out.println(s1 == s1.intern());//语句7    
System.out.println(s2 == s2.intern());//语句8    
System.out.println(s1.intern() == s2.intern());//语句9   
结果:false true true

String类有一个intern()方法,返回字符串在字符串常量池中的引用。在调用"s".intern()方法时,若此时字符串常量池含有"s"那么直接返回该字符串在常量池中的引用。对于语句7,由于在调用"s1".intern()时,此时字符串常量池里面有"abc"引用,所以直接返回引用,即0x02。如果字符串常量池不含"s"引用那么将这个字符串的引用添加到字符串常量池,并返回引用。也就是说在字符串调用intern()这个方法之前,常量池可能不含这个字符串引用,但调用该方法之后常量池一定就有了这个字符串引用。

String hello = "hello";//语句10    
String hel = "hel";//语句11    
String lo = "lo";//语句12    
     
System.out.println(hello == "hello");//语句13
System.out.println(hello == "hel" + "lo");//语句14
System.out.println(hello == "hel" + lo);//语句15
System.out.println(hello == hel + lo);//语句16
System.out.println("hel" + lo == hel + lo);//语句17
结果:true true false false false    

执行完语句10、11、12字符串常量池又增加了“hello”、“hel”、“lo”对象引用,所以语句13指向的都是常量池的“hello”对象。语句14“hel”+“lo”在编译时期就已经变为“hello”了,由于此时常量池已经存在该对象引用了,所以“hel”+“lo”的内存地址和常量池“hello”的一样。语句15需要在代码执行时才能确定,由于lo是变量,所以本质上是新建了一个StringBuilder对象,将“hel”append“lo”,在toString时在堆中新建了一个“hello”。语句16类似,且语句15与语句16在堆中创建的不是一个对象。

String s1=new String("he")+new String("llo");//语句1
String s2=new String("h")+new String("ello");//语句2
String s3=s1.intern(); //语句3
String s4=s2.intern();  //语句4
System.out.println(s1==s3); //语句5
System.out.println(s1==s4); //语句6
System.out.println(s2 == s3);//语句7
System.out.println(s2 == s4); //语句8
结果:true true false false

以上代码是新java程序,在类加载阶段完成后,字符串常量池新加了四个对象分别是“he”、“llo”、“h”、“ello”(引用)。在代码执行过程中,语句1由于new关键字在堆中创建了“he”、“llo”两个对象,并且创建了一个StringBuilder对象拼接字符串,在toString时在堆中创建了“hello”对象。注意,此时字符串常量池还没有“hello”引用。语句2类似。在执行语句3时,由于字符串常量池没有“hello”,于是会将堆中的s1指向的对象的引用加入到常量池。在执行语句4时由于此时常量池已经有“hello”了,所以直接返回引用。所以s4、s3和s1指向堆中的同一个对象。

 参考博客:

Java 基础:String——常量池与 intern - 知乎 (zhihu.com)

 

(7 封私信 / 80 条消息) Java 中new String("字面量") 中 "字面量" 是何时进入字符串常量池的? - 知乎 (zhihu.com)(19条消息) String字符串进入常量池的时机以及intern()方法_forever_together的博客-CSDN博客(8条消息) Java 中方法区与常量池_方法区中发常量池也有常量对象吗_hresh的博客-CSDN博客

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值