String类在虚拟机中的存储及intern方法

首先,有一些需要说明的地方,常量池分为静态常量池(class文件常量池)和运行时常量池。静态常量池在 .class 中,运行时常量池在方法区中。

字符串池

string pool也有叫做string literal pool:

字符串池实际上是一个 HashTable。
JVM为了提升性能和减少内存开销,避免字符串的重复创建,其维护了一块特殊的内存空间,即字符串池(String Pool)。字符串池由String类私有的维护。

在 JDK 1.6 以及以前的版本中,字符串池是放在 Perm 区(Permanent Generation,永久代)。Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,容量是固定的,默认在 32 M 到 96 M 间,我们可以通过 -XX:MaxPermSize = N 来配置永久代的大小,但是在运行过程中它仍然还是固定大小的。也有说 Perm 区实际上就是 HotSpot 下的方法区,HotSpot 的开发人员更愿意将方法区称为 Permanent Generation,这里我们不做过多的探讨。

在 JDK 1.7 的版本中,字符串池移到Java Heap。在 JDK 1.8 中永久代的说法被废弃,元空间成为方法区的替代品。

字符串池存的是实例还是引用?

根据《Java 虚拟机规范》,堆是供对象实例化分配的区域,Java 程序中的对象实例都应该分配在堆上,我们通过引用对这些实例进行访问。
在 HotSpot 下的 reference 类型使用的都是直接指针的访问形式,也就是直接指向堆上的实例对象。

字符串池这个 HashTable 保存的本质上是 reference:

  • 在jdk1.6中,字符串池中保存的是对象。
  • 在jdk1.7之后,字符串池中保存的是对象的引用。

“字面量"和"常量”

在开始介绍String类之前,先来了解"字面量"和"常量"。

  • 字面量:
    • 先来看看百度百科的叙述:字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。
    • 看不懂没关系,我们接着往下看。“字面量”,顾名思义就是字面的意思。如Java中的数据定义大可分为两种,字面定义和对象定义(个人理解),例如int a=5和Integer a=new Integer(5) 这个5就是整数字面量。当然还有特殊的一种,String s=“hello”,这个"hello"就是一个字符串字面量。
  • 常量:
    • 不单单指 final 变量,任何具有不变性的东西我们都将它称为常量。
    • 而着重关注String类就可以发现,经常在很多地方看到这样的描述“字符串是常量,不可修改”,这就不得不看看String类的设计了:不仅类定义使用 final 修饰,关键的字符数组同样声明为 private final。但这并不能保证字符串的不可修改性,final修饰类定义只能使类不被继承,字符数组被 final 修饰只能保证 value 不能指向其他内存,但我们仍然可以通过 value[0] = ‘V’ 的方式直接修改 value 的内容。
public final class String 
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    ...
}

String 是不可变,关键是因为 SUN 公司的工程师,在后面所有 String 的方法里很小心的没有去动数组里的元素,没有暴露内部成员字段。private final char value[] 这一句里,private的私有访问权限的作用都比 final 大。而且设计师还很小心地把整个 String 设成 final 禁止继承,避免被其他人继承后破坏。所以 String 是不可变的关键都在底层的实现,而不是一个 final。考验的是工程师构造数据类型,封装数据的功力。

String类的创建方式

在Java中有两种创建字符串对象的方式:

采用字面量的方式赋值、采用new关键字新建一个字符串对象。

1.采用字面值的方式赋值

String s1="abc";
String s2="abc";
System.out.println(s1==s2);

采用字面值的方式创建一个字符串,JVM首先会去字符串池中查找是否存在"abc"这个字符串的引用,如果不存在,则在池中创建"abc"这个对象。然后将池中"abc”对象的地址返回给字符串常量s1,这样s1会指向池中"abc"这个字符串对象;如果存在,则不创建任何对象,直接将池中"abc"这个对象或引用返回,赋给字符串常量s2。

如图是s1和s2的内存示意图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hYK4TNKi-1611831631761)

图中并未标注字符串池在方法区,是因为字符串池逻辑上属于方法区,而此处我们从底层看起,都是在物理层面的堆上

2.采用new关键字创建对象

例如:

String s1=new String("abc");
String s2=new String("abc");
System.out.println(s1==s2);

采用new关键字新建一个字符串对象时,先去字符串常量池中查找有没有这个字符串的引用,没有的话在字符串常量池和堆中创建两个对象。如果字符串常量池中有这个字符串就不会在常量池中创建,但依然还是会在堆中创建。并且都会返回堆中字符串的地址。这样上述代码在内存中的示意图如图:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IqWVGxS4-1611831631762)
字符串池在方法区,是因为字符串池逻辑上属于方法区,而此处我们从底层看起,都是在物理层面的堆上==。

3.字符串拼接操作

有一种特殊的字符串创建方式,那就是字符串拼接。

  • 常量与常量的拼接结果在常量池,原理是编译期优化
  • 常量池中不会存在相同内容的变量
  • 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
  • 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象引用放入池中,并返回此对象的引用地址

    例一:
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = s1 + s2;

执行细节:

  • StringBuilder s = new StringBuilder();
  • s.append(s1);
  • s.append(s2);
  • s.toString(); ->调用new String(“ab”);
  • StringBuilder调用toString方法,不会在常量池中生成ab

也就是说每次拼接时都会创建一个StringBuilder对象,会大大降低性能。

String的intern()方法

intern是一个native方法,调用的是底层C的方法。

20200615101358632

大致意思就是调用时会返回一个String对象:当intern()方法被调用的时候,如果在字符串常量池中已经包含了一个使用equals()方法判断相等的String对象,那么就返回该String对象。否则这个String对象就会被添加到常量池中,然后返回该对象的一个引用。并且保证常量池中不会重复。

String对象添加到常量池中到底是什么方式?有如下两种方式:

①在常量池中创建一个String对象并返回该对象的引用。

②在常量池中保存常量池外String对象的引用,并返回该引用。

在jdk6及以前使用的是方法①,jdk7及以后使用的是方法②。明显方法②是方法①的优化版。

体验两个案例:

案例一
String s1=new String("ab");
String s2= s1.intern();
System.out.println(s1==s2);

由于在调用intern()方法时"ab"在字符串常量池中已经
存在,所以intern()方法直接返回该字符串常量池中对象的引用。

20200615135738302

案例二
 String s=new String("a")+new String("b");
 s.intern();
 String s1="ab";
 System.out.println(s==s1);

对象s调用intern()方法时,字符串常量池中并没有对象"ab",所以我们就需要执行将"ab"添加到字符串常量池的的操作。而这时在不同的jdk版本可能就有不同的操作。

①jdk6及之前在常量池中创建一个String对象并返回该对象的引用。

所以很明显这时候数据是这样存储的(需要注意的是这时候字符串常量池是在方法区中,jdk1.7才将字符串常量池移到堆中),所以结果就是false。
20200615143207711

②jdk7及之后在常量池中保存常量池外String对象的引用,并返回该引用。
这时候数据是这样存储的,所以这时候就是true。
20200615142617861

参考:字节码层面解析String到底创建了几个对象以及String扩展之intern()方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值