String的内存模型,为什么String被设计成不可变的
String是Java中最常用的类,是不可变的(Immutable), 那么String是如何实现Immutable呢,String为什么要设计成不可变呢?
前言
关于String,收集一波基础,来源标明最后,不确定是否权威, 希望有问题可以得到纠正。
0. String的内存模型
- Java8以及以后的字符串新建时,直接在堆中生成对象,而字符创常量池位于Metaspace。必要的时候,会把堆中的指针存入Metaspace, 而不是复制。
- Metaspace位于虚拟机以外的直接内存,因此大小和外部直接内存有关,但也可以通过指定参数设置
-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m

0.1 一些真实测试,以及某些推测
很难直接从百度出的中文资料中得到确切的答案,因为大多以讹传讹,未经验证。这里且做测试,先记住,因为很不情愿啃官方文档。
前期准备
首先,要有字符串常量池的概念。然后知道String是怎么和常量池打交道的。这里的武器就是intern(),看一下javadoc:
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
即常量池存在,返回常量池中的那个对象,常量池不存在,则放入常量池,并返回本身。由此推断两个公式:
str.intern() == str //证明返回this本身,证明常量池不存在。
str.intern() != str //证明返回常量池中已存在的对象,不等于新建的对象。
这两个公式有什么用?
面试题虽然被很多牛人说low(请别再拿“String s = new String("xyz");创建了多少个String实例”来面试了吧),但确实经常出现new String以及几个对象之类的问题。而这个问题主要是考察String的内存模型,连带可以引出对Java中对象的内存模型的理解。
通过判断上述两个公式,我们可以知道对象究竟是新建的,还是来自常量池,如此就可以坦然面对谁等于谁的问题。
约定
- 为了准确表达,这里为伪地址表示指针位置,比如
0xab表示"ab"这个对象的地址 - 测试基于jdk1.8.0_131.jdk
- 操作系统: MacOS 10.12.6
- 内存: 16G
- CPU: 2.2 GHz Intel Core i7
Java Visual VM
JDK提供一个可视化内存查看工具jvisualvm。Mac由于安装Java后已经设置了环境变量,所以打开命令行,直接输入jvisualvm, 即可打开。Windows下应该是在bin目录下找到对应的exe文件,双击打开。
OQL语言
在Java VisualVM中可以使用OQL来查找对象。具体可以查看Oracle博客。百度出来的结果都是摘抄的[深入理解Java虚拟机]这本书附录里的内容。但我表示用来使用行不通。一些用法不一样。简单的归纳一些用的语法。
查询一个内容为RyanMiao的字符串:
select {instance:s} from java.lang.String s where s.toString() == "RyanMiao"
查询前缀为Ryan的字符串:
select {instance:s} from java.lang.String s where s.toString().substring(0,4) =="Ryan"
遍历
filter(
sort(
map(heap.objects("java.lang.String"),
function(heapString){
if( ! counts[heapString.toString()]){
counts[heapString.toString()] = 1;
} else {
counts[heapString.toString()] = counts[heapString.toString()] + 1;
}
return { string:heapString.toString(), count:counts[heapString.toString()]};
}),
'lhs.count < rhs.count'),
function(countObject) {
if( countObject.string ){
alreadyReturned[countObject.string] = true;
return true;
} else {
return false;
}
}
);
没找到匹配前缀的做法,这里使用最笨的遍历
filter(
heap.objects("java.lang.String"),
function(str){
if(str != "Ryan" && str !="Miao" && str != "RyanMiao"){
return false;
}
return true;
}
);
0.1.1 通过=创建字符串
通过=号创建对象,运行时只有一个对象存在。
/**
* @author Ryan Miao
* 等号赋值,注意字面量的存在
*/
@Test
public void testNewStr() throws InterruptedException {
//str.intern(): 若常量池存在,返回常量池中的对象;若常量池不存在,放入常量池,并返回this。
//=号赋值,若常量池存在,直接返回常量池中的对象0xs1,如果常量池不存在,则放入常量池,常量池中的对象也是0xs1
String s1 = "RyanMiao";//0xs1
Assert.assertTrue(s1.intern() == s1);//0xs1 == 0xs1 > true
Thread.sleep(1000*60*60);
}
通过Java自带的工具Java VisualVM来查询内存中的String实例,可以看出s1只有一个对象。操作方法如下。
为了动态查看内存,选择休眠1h,run testNewStr(),然后打开jvisualvm, 可以看到几个vm列表,找到我们的vm,右键heamp dump.

然后,选择右侧的OQL,在查询内容编辑框里输入:
select {instance:s} from java.lang.String s where s.toString() == "RyanMiao"
可以发现,只有一个对象。

0.1.2 通过new创建字符串
通过new创建对象时,参数RyanMiao作为字面量会生成一个对象,并存入字符创常量池。而后,new的时候又将创建另一个String对象,所以,最好不要采用这种方式使用String, 不然就是双倍消耗内存。
/**
* @author Ryan Miao
*
* 暴露的字面量(literal)也会生成对象,放入Metaspace
*/
@Test
public void testNew(){
//new赋值,直接堆中创建0xs2, 常量池中All literal strings and string-valued constant expressions are interned,
// "RyanMiao"本身就是一个字符串,并放入常量池,故intern()返回0xab
String s2 = new String("RyanMiao");
Assert.assertFalse(s2.intern() == s2);//0xRyanMiao == 0xs2 > false
}

0.1.3 通过拼接创造字符串
当字符创常量池不存在此对象的的时候,返回本身。
/**
* @author Ryan Miao
* 上栗中,由于字面量(literal)会生成对象,并放入常量池,因此可以直接从常量池中取出(前提是此行代码运行之前没有其他代码运行,常量池是干净的)
*
* 本次,测试非暴露字面量的str
*/
@Test
public void testConcat(){
//没有任何字面量为"RyanMiao"暴露给编译器,所以常量池没有创建"RyanMiao",所以,intern返回this
String s3 = new StringBuilder("Ryan").append("Miao").toString();
Assert.assertTrue(s3.intern() == s3);
}
在Java Visual VM中,查询以"Ryan"开头的变量:
select {instance:s} from java.lang.String s where s.toString().substring(0,4) =="Ryan"
但,根据以上几个例子,可以明显看出来,字符串字面量(literal)都是对象,于是上栗中应该有三个对象:Ryan,Miao,RyanMiao。验证如下:

此时的内存模型:

0.1.4 针对常量池中已存在的字符串
/**
* @author Ryan Miao
* 上栗中,只要不暴露我们最终的字符串,常量池基本不会存在,则每次新建(new)的时候,都会放入常量池,intern并返回本身。即常量池的对象即新建的对象本身。
*
* 本次,测试某些常量池已存在的字符串
*/
@Test
public void testExist(){
//为毛常量池存在java这个单词
//s4 == 0xs4, intern发现常量池存在,返回0xexistjava
String s4 = new StringBuilder("ja").append("va").toString();
Assert.assertFalse(s4.intern() == s4); //0xexistjava == 0xs4 > false
//int也一开始就存在于常量池中了, intern返回0xexistint
String s5 = new StringBuilder().append("in").append("t").toString();
Assert.assertFalse(s5.intern()==s5); // 0xexistint == 0xs5 > false
//由于字面量"abc"加载时,已放入常量池,故s6 intern返回0xexistabc, 而s6是新建的0xs6
String a = "abc";
String s6 = new StringBuilder().append("ab").append("c").toString();
Assert.assertFalse(s6.intern() == s6); //0xexistabc == 0xs6 > false
}
验证如下:

使用命令行工具javap -c TestString可以反编译class,看到指令执行的过程。
% javap -c TestString
Warning: Binary file TestString contains com.test.java.string.TestString
Compiled from "TestString.java"
public class com.test.java.string.TestString {
public com.test.java.string.TestString();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void testNewStr() throws java.lang.InterruptedException;
Code:
0: ldc #2 // String RyanMiao
2: astore_1
3: aload_1
4: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String;
7: aload_1
8: if_acmpne 15
11: iconst_1
12: goto 16
15: iconst_0
16: invokestatic #4 // Method org/junit/Assert.assertTrue:(Z)V
19: return
public void testNew() throws java.lang.InterruptedException;
Code:
0: new #5 // class java/lang/String
3: dup
4: ldc #2 // String RyanMiao
6: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: aload_1
11: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String;
14: aload_1
15: if_acmpne 22
18: iconst_1
19: goto 23
22: iconst_0
23: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V
26: return
public void testConcat() throws java.lang.InterruptedException;
Code:
0: new #8 // class java/lang/StringBuilder
3: dup
4: ldc #9 // String Ryan
6: invokespecial #10 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
9: ldc #11 // String Miao
11: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
17: astore_1
18: aload_1
19: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String;
22: aload_1
23: if_acmpne 30
26: iconst_1
27: goto 31
30: iconst_0
31: invokestatic #4 // Method org/junit/Assert.assertTrue:(Z)V
34: return
public void testExist() throws java.lang.InterruptedException;
Code:
0: new #8 // class java/lang/StringBuilder
3: dup
4: ldc #14 // String ja
6: invokespecial #10 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
9: ldc #15 // String va
11: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
17: astore_1
18: aload_1
19: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String;
22: aload_1
23: if_acmpne 30
26: iconst_1
27: goto 31
30: iconst_0
31: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V
34: new #8 // class java/lang/StringBuilder
37: dup
38: invokespecial #16 // Method java/lang/StringBuilder."<init>":()V
41: ldc #17 // String in
43: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
46: ldc #18 // String t
48: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
51: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
54: astore_2
55: aload_2
56: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String;
59: aload_2
60: if_acmpne 67
63: iconst_1
64: goto 68
67: iconst_0
68: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V
71: ldc #19 // String abc
73: astore_3
74: new #8 // class java/lang/StringBuilder
77: dup
78: invokespecial #16 // Method java/lang/StringBuilder."<init>":()V
81: ldc #20 // String ab
83: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
86: ldc #21 // String c
88: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
91: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
94: astore 4
96: aload 4
98: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String;
101: aload 4
103: if_acmpne 110
106: iconst_1
107: goto 111
110: iconst_0
111: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V
114: ldc2_w #22 // long 3600000l
117: invokestatic #24 // Method java/lang/Thread.sleep:(J)V
120: return
}
Java在compile的时候优化了执行逻辑
我以为使用了StringBuilder可以减少性能损耗啊,然而,编译后的文件直接说no,直接给替换成拼接了:

1. String是如何实现Immutable的?
Immutable是指String的对象实例生成后就不可以改变。相反,加入一个user类,你可以修改name,那么就不叫做Immutable。所以,String的内部属性必须是不可修改的。
1.1 私有成员变量
String的内部很简单,有两个私有成员变量:
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
而后并没有对外提供可以修改这两个属性的方法,没有set,没有build。
1.2 Public的方法都是复制一份数据
String有很多public方法,要想维护这么多方法下的不可变需要付出代价。每次都将创建新的String对象。比如,这里讲一个很有迷惑性的concat方法:
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
从方法名上看,是拼接字符串。这样下意识以为是原对象修改了内容,所以对于str2 = str.concat("abc"),会认为是str2==str。然后熟记String不可变定律的你肯定会反对。确实不是原对象,确实new了新String。同样的道理,在其他String的public方法里,都将new一个新的String。因此就保证了原对象的不可变。说到这里,下面的结果是什么?
String str2 = str.concat("");
Assert.assertFalse(str2 == str);
按照String不可变的特性来理解,这里str2应该是生成的新对象,那么肯定不等于str.所以是对的,是false。面试考这种题目也是醉了,为了考验大家对String API的熟悉程度吗?看源码才知道,当拼接的内容为空的时候直接返回原对象。因此,str2==str是true。
1.3 String是final的
由于String被声明式final的,则我们不可以继承String,因此就不能通过继承来复写一些关于hashcode和value的方法。
2. String为什么要设计成Immutable?
一下内容来自http://www.kogonuso.com/2015/03/why-string-is-immutable-or-final-class.html#sthash.VgLU1mDY.dpuf. 发现百度的中文版本基本也是此文的翻译版。
缓存的需要
String是不可变的。因为String会被String pool缓存。因为缓存String字面量要在多个线程之间共享,一个客户端的行为会影响其他所有的客户端,所以会产生风险。如果其中一个客户端修改了内容"Test"为“TEST”, 其他客户端也会得到这个结果,但显然并想要这个结果。因为缓存字符串对性能来说至关重要,因此为了移除这种风险,String被设计成Immutable。
HashMap的需要
HashMap在Java里太重要了,而它的key通常是String类型的。如果String是mutable,那么修改属性后,其hashcode也将改变。这样导致在HashMap中找不到原来的value。
多线程中需要
string的subString方法如下:
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
如果String是可变的,即修改String的内容后,地址不变。那么当多个线程同时修改的时候,value的length是不确定的,造成不安全因素,无法得到正确的截取结果。而为了保证顺序正确,需要加synchronzied,但这会得到难以想象的性能问题。
保证hashcode
这和上条中HashMap的需要一样,不可变的好处就是hashcode不会变,可以缓存而不用计算。
classloader中需要
The absolutely most important reason that String is immutable is that it is used by the class loading mechanism, and thus have profound and fundamental security aspects. Had String been mutable, a request to load "java.io.Writer" could have been changed to load "mil.vogoon.DiskErasingWriter"
String会在加载class的时候需要,如果String可变,那么可能会修改加载中的类。
总之,安全性和String字符串常量池缓存是String被设计成不可变的主要原因。
参考
- https://stackoverflow.com/questions/3052442/what-is-the-difference-between-text-and-new-stringtext/3052456
- http://www.kogonuso.com/2015/03/why-string-is-immutable-or-final-class.html#sthash.VgLU1mDY.dpuf
- http://rednaxelafx.iteye.com/blog/774673
- http://www.jianshu.com/p/4ee6aec39c89?from=groupmessage
- http://www.cnblogs.com/yulei126/p/6777323.html
- https://blogs.oracle.com/sundararajan/querying-java-heap-with-oql
String的内存模型,为什么String被设计成不可变的的更多相关文章
- String类为什么被设计成不可变类
1.享元模式: 1.共享元素模式,也就是说:一个系统中如果有多处用到了相同的一个元素,那么我们应该只存储一份此元素,而让所有地方都引用这一个元素. 2.Java中String就是根据享元模式设计的,而 ...
- 为什么String要设计成不可变的?
英文原:http://www.programcreek.com/2013/04/why-string-is-immutable-in-java/ 转自:http://blog.csdn.net/ren ...
- String类为什么设计成不可变的
在Java中将String设计成不可变的是综合考虑到各种因素的结果,需要综合考虑内存.同步.数据结构以安全方面的考虑. String被设计成不可变的主要目的是为了安全和高效. 1)字符串常量池的需要 ...
- 为什么Java中的String是设计成不可变的?(Why String is immutable in java)
There are many reasons due to the string class has been made immutable in Java. These reasons in vie ...
- 深刻理解Java中final的作用(一):从final的作用剖析String被设计成不可变类的深层原因
声明:本博客为原创博客,未经同意,不得转载!小伙伴们假设是在别的地方看到的话,建议还是来csdn上看吧(原文链接为http://blog.csdn.net/bettarwang/article/det ...
- [2017-09-04]Abp系列——为什么值对象必须设计成不可变的
本系列目录:Abp介绍和经验分享-目录 这篇是之前翻备忘录发现漏了的,前阵子刚好同事又提及过这个问题,这里补上. 本文重点在于理解什么是值对象的不可变性. Abp的ValueObject以及EF的Co ...
- C++ 头文件系列(string)----分析string初始化内存模型
测大小 这里我们比较4种版本的basic_string模版类,分别是:string, wstring, u16string, u32string. 虽然他们是不同的字符串类型,但是其sizeof的结果 ...
- Java为什么把String设计成不可变的(immutable)
在java中,String是字符串常量,可以从内存,同步机制,数据结构等方面分析 1:字符串中常量池的需要 String不同于普通基础变量类型的地方在于对象.java中的字符串对象都保存在字符串常量池 ...
- String被设计成不可变和不能被继承的原因
String是所有语言中最常用的一个类.我们知道在Java中,String是不可变的.final的.Java在运行时也保存了一个字符串池(String pool),这使得String成为了一个特别的类 ...
随机推荐
- RedHat 7 常用命令总结
Linux RedHat 7常用命令总结... ----------------------- 征服Linux从终端开始 ------------------------------------- 在 ...
- 论文写作office实用技巧
最近在写论文,然后要按照模板来写,其中office排版有很多技巧;先前一直没有弄透彻,今晚上终于完美收工! 主要问题如下 MathType破解版 Mathtype试用版,到期后要卸载干净,才能再次下载 ...
- 大学写作期中作业--霸天黄小o
霸天黄小o 百无聊赖之中,小o又回想起了它当初的辉煌时刻. 那是凤儿凰自行车厂的又一次大规模交货.虽然其实在500万的订单下,一批又一批的交货几乎是连着的. 但这并不影响小o和与它一届的同学们的意气风 ...
- 7_linux下PHP、Apache、Mysql服务的安装
1.首先安装之前,要确保你的虚拟机能连上外网. Mysql: 1.yum -y install mysql 连接数据库命令行模式 2.yum install mysql-server 安装mys ...
- Apache与Tomcat的关系和区别 -个人比较
我们经常在用apache和tomcat等这些服务器,可是总感觉还是不清楚他们之间有什么关系,在用tomcat的时候总出现apache,总感到迷惑,到底谁是主谁是次,因此特意在网上查询了一些这方面的资料 ...
- linux下用OCI库访问oracle数据库返回错误Cannot create OCI environment!;
linux下链接oracle数据库,直接用OCI库函数OCI_Initialize初始化返回Oracle ErrorString:Cannot create OCI environment! 原因是缺 ...
- FreeMarker简介
什么是 FreeMarker? FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具. 它不是面向最终用 ...
- Spring之声明式事务
在讲声明式事务之前,先回顾一下基本的编程式事务 编程式事务: //1.获取Connection对象 Connection conn = JDBCUtils.getConnection(); try { ...
- android TranslateAnimation 顶部segment分段移动动画
这里实现的功能是从主页布局的fragment点击跳转到一个acitivity,然后顶部是一个切换的segment顶部是一个listview,点击segment分段让listview加载不同的内容.我这 ...
- IT团队之非正式沟通
沟通能力是一种能证明和让对方发现你具有社会工作能力的能力.从表面上看来,它只是一种能说会道的能力,可实际上它却包罗了一个人从穿衣打扮到言谈举止等一切行为的能力. 从大体上,我将沟通分为正式沟通 ...