极客时间已完结课程限时免费阅读

03 | 字符串性能优化不容小觑,百M内存轻松存储几十G数据

03 | 字符串性能优化不容小觑,百M内存轻松存储几十G数据-极客时间

03 | 字符串性能优化不容小觑,百M内存轻松存储几十G数据

讲述:李良

时长15:08大小13.84M

你好,我是刘超。
从第二个模块开始,我将带你学习 Java 编程的性能优化。今天我们就从最基础的 String 字符串优化讲起。
String 对象是我们使用最频繁的一个对象类型,但它的性能问题却是最容易被忽略的。String 对象作为 Java 语言中重要的数据类型,是内存中占据空间最大的一个对象。高效地使用字符串,可以提升系统的整体性能。
接下来我们就从 String 对象的实现、特性以及实际使用中的优化这三个方面入手,深入了解。
在开始之前,我想先问你一个小问题,也是我在招聘时,经常会问到面试者的一道题。虽是老生常谈了,但错误率依然很高,当然也有一些面试者答对了,但能解释清楚答案背后原理的人少之又少。问题如下:
通过三种不同的方式创建了三个对象,再依次两两匹配,每组被匹配的两个对象是否相等?代码如下:
String str1= "abc";
String str2= new String("abc");
String str3= str2.intern();
assertSame(str1==str2);
assertSame(str2==str3);
assertSame(str1==str3)
你可以先想想答案,以及这样回答的原因。希望通过今天的学习,你能拿到满分。

String 对象是如何实现的?

在 Java 语言中,Sun 公司的工程师们对 String 对象做了大量的优化,来节约内存空间,提升 String 对象在系统中的性能。一起来看看优化过程,如下图所示:
1. 在 Java6 以及之前的版本中,String 对象是对 char 数组进行了封装实现的对象,主要有四个成员变量:char 数组、偏移量 offset、字符数量 count、哈希值 hash。
String 对象是通过 offset 和 count 两个属性来定位 char[]数组,获取字符串。这么做可以高效、快速地共享数组对象,同时节省内存空间,但这种方式很有可能会导致内存泄漏。
2. 从 Java7 版本开始到 Java8 版本,Java 对 String 类做了一些改变。String 类中不再有 offset 和 count 两个变量了。这样的好处是 String 对象占用的内存稍微少了些,同时,String.substring 方法也不再共享 char[],从而解决了使用该方法可能导致的内存泄漏问题。
3. 从 Java9 版本开始,工程师将 char[]字段改为了 byte[]字段,又维护了一个新的属性 coder,它是一个编码格式的标识。
工程师为什么这样修改呢?
我们知道一个 char 字符占 16 位,2 个字节。这个情况下,存储单字节编码内的字符(占一个字节的字符)就显得非常浪费。JDK1.9 的 String 类为了节约内存空间,于是使用了占 8 位,1 个字节的 byte 数组来存放字符串。
而新属性 coder 的作用是,在计算字符串长度或者使用 indexOf()函数时,我们需要根据这个字段,判断如何计算字符串长度。coder 属性默认有 0 和 1 两个值,0 代表 Latin-1(单字节编码),1 代表 UTF-16。如果 String 判断字符串只包含了 Latin-1,则 coder 属性值为 0,反之则为 1。

String 对象的不可变性

了解了 String 对象的实现后,你有没有发现在实现代码中 String 类被 final 关键字修饰了,而且变量 char 数组也被 final 修饰了。
我们知道类被 final 修饰代表该类不可继承,而 char[]被 final+private 修饰,代表了 String 对象不可被更改。Java 实现的这个特性叫作 String 对象的不可变性,即 String 对象一旦创建成功,就不能再对它进行改变。
Java 这样做的好处在哪里呢?
第一,保证 String 对象的安全性。假设 String 对象是可变的,那么 String 对象将可能被恶意修改。
第二,保证 hash 属性值不会频繁变更,确保了唯一性,使得类似 HashMap 容器才能实现相应的 key-value 缓存功能。
第三,可以实现字符串常量池。在 Java 中,通常有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,如 String str=“abc”;另一种是字符串变量通过 new 形式的创建,如 String str = new String(“abc”)。
当代码中使用第一种方式创建字符串对象时,JVM 首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。
String str = new String(“abc”) 这种方式,首先在编译类文件时,"abc"常量字符串将会放入到常量结构中,在类加载时,“abc"将会在常量池中创建;其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的"abc” 字符串,在堆内存中创建一个 String 对象;最后,str 将引用 String 对象。
这里附上一个你可能会想到的经典反例。
平常编程时,对一个 String 对象 str 赋值“hello”,然后又让 str 值为“world”,这个时候 str 的值变成了“world”。那么 str 值确实改变了,为什么我还说 String 对象不可变呢?
首先,我来解释下什么是对象和对象引用。Java 初学者往往对此存在误区,特别是一些从 PHP 转 Java 的同学。在 Java 中要比较两个对象是否相等,往往是用 ==,而要判断两个对象的值是否相等,则需要用 equals 方法来判断。
这是因为 str 只是 String 对象的引用,并不是对象本身。对象在内存中是一块内存地址,str 则是一个指向该内存地址的引用。所以在刚刚我们说的这个例子中,第一次赋值的时候,创建了一个“hello”对象,str 引用指向“hello”地址;第二次赋值的时候,又重新创建了一个对象“world”,str 引用指向了“world”,但“hello”对象依然存在于内存中。
也就是说 str 并不是对象,而只是一个对象引用。真正的对象依然还在内存中,没有被改变。

String 对象的优化

了解了 String 对象的实现原理和特性,接下来我们就结合实际场景,看看如何优化 String 对象的使用,优化的过程中又有哪些需要注意的地方。

1. 如何构建超大字符串?

编程过程中,字符串的拼接很常见。前面我讲过 String 对象是不可变的,如果我们使用 String 对象相加,拼接我们想要的字符串,是不是就会产生多个对象呢?例如以下代码:
String str= "ab" + "cd" + "ef";
分析代码可知:首先会生成 ab 对象,再生成 abcd 对象,最后生成 abcdef 对象,从理论上来说,这段代码是低效的。
但实际运行中,我们发现只有一个对象生成,这是为什么呢?难道我们的理论判断错了?我们再来看编译后的代码,你会发现编译器自动优化了这行代码,如下:
String str= "abcdef";
上面我介绍的是字符串常量的累计,我们再来看看字符串变量的累计又是怎样的呢?
String str = "abcdef";
for(int i=0; i<1000; i++) {
str = str + i;
}
上面的代码编译后,你可以看到编译器同样对这段代码进行了优化。不难发现,Java 在进行字符串的拼接时,偏向使用 StringBuilder,这样可以提高程序的效率。
String str = "abcdef";
for(int i=0; i<1000; i++) {
str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}
综上已知:即使使用 + 号作为字符串的拼接,也一样可以被编译器优化成 StringBuilder 的方式。但再细致些,你会发现在编译器优化的代码中,每次循环都会生成一个新的 StringBuilder 实例,同样也会降低系统的性能。
所以平时做字符串拼接的时候,我建议你还是要显示地使用 String Builder 来提升系统性能。
如果在多线程编程中,String 对象的拼接涉及到线程安全,你可以使用 StringBuffer。但是要注意,由于 StringBuffer 是线程安全的,涉及到锁竞争,所以从性能上来说,要比 StringBuilder 差一些。

2. 如何使用 String.intern 节省内存?

讲完了构建字符串,我们再来讨论下 String 对象的存储问题。先看一个案例。
Twitter 每次发布消息状态的时候,都会产生一个地址信息,以当时 Twitter 用户的规模预估,服务器需要 32G 的内存来存储地址信息。
public class Location {
private String city;
private String region;
private String countryCode;
private double longitude;
private double latitude;
}
考虑到其中有很多用户在地址信息上是有重合的,比如,国家、省份、城市等,这时就可以将这部分信息单独列出一个类,以减少重复,代码如下:
public class SharedLocation {
private String city;
private String region;
private String countryCode;
}
public class Location {
private SharedLocation sharedLocation;
double longitude;
double latitude;
}
通过优化,数据存储大小减到了 20G 左右。但对于内存存储这个数据来说,依然很大,怎么办呢?
这个案例来自一位 Twitter 工程师在 QCon 全球软件开发大会上的演讲,他们想到的解决方法,就是使用 String.intern 来节省内存空间,从而优化 String 对象的存储。
具体做法就是,在每次赋值的时候使用 String 的 intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用,这样一开始的对象就可以被回收掉。这种方式可以使重复性非常高的地址信息存储大小从 20G 降到几百兆。
SharedLocation sharedLocation = new SharedLocation();
sharedLocation.setCity(messageInfo.getCity().intern()); sharedLocation.setCountryCode(messageInfo.getRegion().intern());
sharedLocation.setRegion(messageInfo.getCountryCode().intern());
Location location = new Location();
location.set(sharedLocation);
location.set(messageInfo.getLongitude());
location.set(messageInfo.getLatitude());
为了更好地理解,我们再来通过一个简单的例子,回顾下其中的原理:
String a =new String("abc").intern();
String b = new String("abc").intern();
if(a==b) {
System.out.print("a==b");
}
输出结果:
a==b
在字符串常量中,默认会将对象放入常量池;在字符串变量中,对象是会创建在堆内存中,同时也会在常量池中创建一个字符串对象,String 对象中的 char 数组将会引用常量池中的 char 数组,并返回堆内存对象引用。
如果调用 intern 方法,会去查看字符串常量池中是否有等于该对象的字符串的引用,如果没有,在 JDK1.6 版本中会复制堆中的字符串到常量池中,并返回该字符串引用,堆内存中原有的字符串由于没有引用指向它,将会通过垃圾回收器回收。
在 JDK1.7 版本以后,由于常量池已经合并到了堆中,所以不会再复制具体字符串了,只是会把首次遇到的字符串的引用添加到常量池中;如果有,就返回常量池中的字符串引用。
了解了原理,我们再一起看下上边的例子。
在一开始字符串"abc"会在加载类时,在常量池中创建一个字符串对象。
创建 a 变量时,调用 new Sting() 会在堆内存中创建一个 String 对象,String 对象中的 char 数组将会引用常量池中字符串。在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。
创建 b 变量时,调用 new Sting() 会在堆内存中创建一个 String 对象,String 对象中的 char 数组将会引用常量池中字符串。在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。
而在堆内存中的两个对象,由于没有引用指向它,将会被垃圾回收。所以 a 和 b 引用的是同一个对象。
如果在运行时,创建字符串对象,将会直接在堆内存中创建,不会在常量池中创建。所以动态创建的字符串对象,调用 intern 方法,在 JDK1.6 版本中会去常量池中创建运行时常量以及返回字符串引用,在 JDK1.7 版本之后,会将堆中的字符串常量的引用放入到常量池中,当其它堆中的字符串对象通过 intern 方法获取字符串对象引用时,则会去常量池中判断是否有相同值的字符串的引用,此时有,则返回该常量池中字符串引用,跟之前的字符串指向同一地址的字符串对象。
以一张图来总结 String 字符串的创建分配内存地址情况:
使用 intern 方法需要注意的一点是,一定要结合实际场景。因为常量池的实现是类似于一个 HashTable 的实现方式,HashTable 存储的数据越大,遍历的时间复杂度就会增加。如果数据过大,会增加整个字符串常量池的负担。

3. 如何使用字符串的分割方法?

最后我想跟你聊聊字符串的分割,这种方法在编码中也很最常见。Split() 方法使用了正则表达式实现了其强大的分割功能,而正则表达式的性能是非常不稳定的,使用不恰当会引起回溯问题,很可能导致 CPU 居高不下。
所以我们应该慎重使用 Split() 方法,我们可以用 String.indexOf() 方法代替 Split() 方法完成字符串的分割。如果实在无法满足需求,你就在使用 Split() 方法时,对回溯问题加以重视就可以了。

总结

这一讲中,我们认识到做好 String 字符串性能优化,可以提高系统的整体性能。在这个理论基础上,Java 版本在迭代中通过不断地更改成员变量,节约内存空间,对 String 对象进行优化。
我们还特别提到了 String 对象的不可变性,正是这个特性实现了字符串常量池,通过减少同一个值的字符串对象的重复创建,进一步节约内存。
但也是因为这个特性,我们在做长字符串拼接时,需要显示使用 StringBuilder,以提高字符串的拼接性能。最后,在优化方面,我们还可以使用 intern 方法,让变量字符串对象重复使用常量池中相同值的对象,进而节约内存。
最后再分享一个个人观点。那就是千里之堤,溃于蚁穴。日常编程中,我们往往可能就是对一个小小的字符串了解不够深入,使用不够恰当,从而引发线上事故。
比如,在我之前的工作经历中,就曾因为使用正则表达式对字符串进行匹配,导致并发瓶颈,这里也可以将其归纳为字符串使用的性能问题。具体实战分析,我将在 04 讲中为你详解。

思考题

通过今天的学习,你知道文章开头那道面试题的答案了吗?背后的原理是什么?

互动时刻

今天除了思考题,我还想和你做一个简短的交流。
上两讲中,我收到了很多留言,在此非常感谢你的支持。由于前两讲是概述内容,主要是帮你建立对性能调优的整体认识,所以相对来说重理论、偏基础。但我发现,很多同学都有这样迫切的愿望,那就是赶紧学会使用排查工具,监测分析性能,解决当下的一些问题。
我这里特别想分享一点,其实性能调优不仅仅是学会使用排查监测工具,更重要的是掌握背后的调优原理,这样你不仅能够独立解决同一类的性能问题,还能写出高性能代码,所以我希望给你的学习路径是:夯实基础 - 结合实战 - 实现进阶。
最后,欢迎你积极发言,讨论思考题或是你遇到的性能问题都可以,我会知无不尽。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 43

提建议

上一篇
02 | 如何制定性能调优策略?
下一篇
04 | 慎重使用正则表达式
unpreview
 写留言

精选留言(130)

  • KL3
    2019-05-25
    老师,能解释下, “String.substring 方法也不再共享 char[],从而解决了使用该方法可能导致的内存泄漏问题。” 共享char数组可能导致内存泄露问题?

    作者回复: 你好 KL3,在Java6中substring方法会调用new string构造函数,此时会复用原来的char数组,而如果我们仅仅是用substring获取一小段字符,而原本string字符串非常大的情况下,substring的对象如果一直被引用,由于substring的里面的char数组仍然指向原字符串,此时string字符串也无法回收,从而导致内存泄露。 试想下,如果有大量这种通过substring获取超大字符串中一小段字符串的操作,会因为内存泄露而导致内存溢出。

    共 2 条评论
    131
  • 扫地僧
    2019-05-25
    答案是false,false,true。背后的原理是: 1、String str1 = "abc";通过字面量的方式创建,abc存储于字符串常量池中; 2、String str2 = new String("abc");通过new对象的方式创建字符串对象,引用地址存放在堆内存中,abc则存放在字符串常量池中;所以str1 == str2?显然是false 3、String str3 = str2.intern();由于str2调用了intern()方法,会返回常量池中的数据,地址直接指向常量池,所以str1 == str3;而str2和str3地址值不等所以也是false(str2指向堆空间,str3直接指向字符串常量池)。不知道这样理解有木有问题
    展开

    作者回复: 答案非常正确,理解了这个题目基本理解了string的特性了。

    共 11 条评论
    96
  • 快乐的五五开
    2019-05-25
    自学一年居然不知道有String.intern这个方法😓😓 不过从Java8开始(大概) String.split() 传入长度为1字符串的时候并不会使用正则,这种情况还是可以用

    作者回复: 非常感谢Geek的补充,我在这里也再补充一个小点,split有两种情况不会使用正则表达式: 第一种为传入的参数长度为1,且不包含“.$|()[{^?*+\\”regex元字符的情况下,不会使用正则表达式; 第二种为传入的参数长度为2,第一个字符是反斜杠,并且第二个字符不是ASCII数字或ASCII字母的情况下,不会使用正则表达式。

    共 3 条评论
    71
  • 风轻扬
    2019-08-01
    老师好,诚心请教一个问题 string s1 = new string(“1”)+new string(“1”); s1.intern; string s2=“11”; s1==s2为什么是true呢,我理解s1指向的对象,s2指向的常量池地址才对啊? 然后 string s1 = new string(“1”); s1.intern; string s2=“11”; s1==s2又是false了,区别在哪? 老师,周董提的这个问题,我都琢磨一晚上了。您的回答看了好多遍,确实是看不懂,您能再解释一下吗?目前的回答,咋看也看不懂。。。。。。
    展开

    作者回复: 如果看不太懂,建议先熟悉下JVM这块的知识点。我们知道,JVM从逻辑分区可以分为堆、JVM栈、本地方法栈、方法区、程序计数器,方法区中,在JDK1.8之后,包含了元空间、静态常量池、运行时常量池。 对于字符串常量,在类加载时,会将字符串放入方法区中的静态常量池,包括字符串的字面量和字符引用。而在初始化或运行时,会将字符引用转为直接引用,存放在运行时常量池。 如果是运行时动态生成的字符串对象调用intern方法,如果字符串的引用在运行时常量池不存在,则会在常量池中创建一个引用。 所以第一个通过加动态生成的“11”字符串由于在运行时常量中没有该字符串的引用,所以会在调用s1.intern时,在运行时常量池中生成一个s1的引用,当s2再次引用该字符串时,发现运行时常量池中存在相同值的字符串的引用,就直接返回s1的引用。所以s1==s2是返回的true。这也仅限于JDK1.7之后的版本。 而第二种,用于"11"在类加载时,已经存在静态常量池中,在new string(“11”)时,会在运行时常量池中创建一个“11”字符串的直接引用。而s1指向的并不是该引用,而是new string这个对象的引用。当s2=“11”时,返回的是运行时常量池中的引用。所以s1==s2返回false。

    共 16 条评论
    38
  • 周董
    2019-08-01
    老师,还有一个问题网上众说纷纭,jdk1.8版本,字符串常量池和运行时常量池分别在内存哪个区?您文中的常量池是什么常量池?调用intern后字符串是在哪个常量池生成引用或者对象?麻烦老师抽空解答下,这个困扰很久了。

    作者回复: 严格来说,是静态常量池和运行时常量池,静态常量池是存放字符串字面量、符号引用以及类和方法的信息,而运行时常量池存放的是运行时一些直接引用。 运行时常量池是在类加载完成之后,将静态常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用。 这两个常量池在JDK1.7版本之后,就移到堆内存中了,这里指的是物理空间,而逻辑上还是属于方法区(方法区是逻辑分区)。 我文中说的是两个常量池,没有具体区分,在初次加载时,是字面量是加载到了静态常量池中,解析之后会将引用加载到运行时常量池。 intern方法生成的引用或对象是在运行时常量池中。

    共 2 条评论
    29
  • Teanmy
    2019-06-02
    老师好,有一点始终想不明白,请老师解惑,非常感谢! 老师先帮忙看看关于这两行代码,我的分析是否正确: str1 = "abc"; str2 = new String("abc") str1 = "abc"; 1.str1,首先是在字符串常量池中寻找"abc",找到则取其地址,找不到则创建并返回其地址 2.将该地址赋值给栈中的str1 str2 = new String("abc") 1.在堆中创建String对象,我查阅了String构造方法源码,实际值取的是"abc"的(此时"abc"已经存在字符串常量池中)引用,也就是说,str2还是指向常量池,并没有创建新的"abc"。 public String(String original) { this.value = original.value; this.hash = original.hash; } 2.堆中创建完String对象,将该对象的地址赋值给栈变量str2 疑问: 既然不管是以上哪种方式,最终实际引用的还是常量池中的"abc",str2 = new String("abc")只是增加了一个堆中String的“空壳”对象而已(因为实际上char[]指向的还是常量池中的"abc"),这个空壳对象并不会占用过多内存。而.intern的实质只是减少了这个中间的String空壳对象,那何来twitter通过.intern减少大量内存?
    展开

    作者回复: 你好 teanmy。运行时创建的字符串对象只会在堆中创建一个对象。在这个前提下,如果有相同值的对象创建,使用intern可以减少重复字符串的创建。例如,有广东省/深圳市/南山区,如果有千万个人发布消息,创建了地址对象,这样导致千万个“广东省”对象在堆内存中创建,如果长时间引用,这些对象都没法释放,使用intern将“广东省”放到常量池中,其他对象引用常量池中的同一个“广东省”字符串,而堆中的千万个对象将被回收。 如果有疑问,请继续留言。

    共 4 条评论
    22
  • 失火的夏天
    2019-05-25
    开头题目答案是false false true str1是建立在常量池中的“abc”,str2是new出来,在堆内存里的,所以str1!=str2, str3是通过str2..intern()出来的,str1在常量池中已经建立了"abc",这个时候str3是从常量池里取出来的,和str1指向的是同一个对象,自然也就有了st1==str3,str3!=str2了

    作者回复: 这里我纠正下,str3是intern返回的引用,intern而不是创建出来的。 你的答案是正确的!

    21
  • Only now
    2019-05-29
    看了本篇几乎全部留言, 感觉包括老师在内, 对于 "字符串常量池" 和 "常量池", 这俩概念用的很混。 对于jdk7 以及之前的jvm版本不再去深究了, 它的字符串常量池存在于方法区, 但是jdk8以后, 它存在于Java堆中, 唯一, 且由java.lang.String类维护, 它和类文件常量池, 运行时常量池没有半毛钱的关系。 最后我有个疑问问老师, 字符串常量池中的对象, 在失去了所有外部引用之后, 会被gc掉吗?
    展开

    作者回复: 非常感谢only now的总结,这一讲中没有详细去区分常量池,而是在强调字符串的使用,后面我们在JVM中可以再一起研究下常量池。 JVM文献中提到方法区是存在垃圾回收。我们可以通过intern方法来验证这个gc问题,通过大量请求请求某个接口,传入参数创建字符串对象,之后通过intern方法在常量池中生成字符串对象,之后失去引用,观察gc情况。

    16
  • Zend
    2019-05-26
    “在字符串变量中,对象是会创建在堆内存中,同时也会在常量池中创建一个字符串对象,复制到堆内存对象中,并返回堆内存对象引用。” 比如: 是从常量池中复制到堆内存,这时常量池中字符串与堆内存字符串是完全独立的,内部也不存在引用关系?
    展开

    作者回复: 你好 Zend,具体的复制过程是先将常量池中的字符串压入栈中,在使用string的构造方法时,会拿到栈中的字符串作为构造方法的参数。这里我纠正一点,今天我查看了下这个构造函数,String的构造函数是一个char数组赋值过程,不是new char[]重新创建,所以是引用了常量池中的字符串对象,存在引用关系。

    14
  • 六维
    2019-05-25
    使用 intern 方法需要注意的一点是,一定要结合实际场景。因为常量池的实现是类似于一个 HashTable 的实现方式,HashTable 存储的数据越大,遍历的时间复杂度就会增加。如果数据过大,会增加整个字符串常量池的负担。 像国家地区是有边界的。像其他情况,怎么把握这个度呢?

    作者回复: 如果对空间要求高于时间要求,且存在大量重复字符串时,可以考虑使用常量池存储。 如果对查询速度要求很高,且存储字符串数量很大,重复率很低的情况下,不建议存储在常量池中。 具体可以通过模拟测试自己的场景,对比两种存储方式的性能,通过数据来给自己答案。

    共 3 条评论
    13
  • Eric
    2019-05-25
    对于您文中 “在一开始创建 a 变量时,会在堆内存中创建一个对象,同时在常量池中创建一个字符串对象” 这句话 我认为前部分没有问题 分歧点在后面那部分 我觉得abc常量早就在运行时常量池就存在了 可以理解使用这个类之前 就已经构造好了运行时常量池 而运行时常量池中就包括“abc”常量 至于使用new String(“abc”) 我觉得它应该只会在堆中创建String对象 并将运行时常量池中已经存在的“abc”常量的引用作为构造函数的参数而已

    作者回复: 你理解的分歧点是对的,这个构造是在加载类时,就已经在常量池中构造好常量。

    共 2 条评论
    11
  • 周董
    2019-07-26
    老师好,诚心请教一个问题 string s1 = new string(“1”)+new string(“1”); s1.intern; string s2=“11”; s1==s2为什么是true呢,我理解s1指向的对象,s2指向的常量池地址才对啊? 然后 string s1 = new string(“1”); s1.intern; string s2=“11”; s1==s2又是false了,区别在哪?
    展开

    作者回复: String s1 = new String("1") + new String("1")会在堆中组合一个新的字符串对象"11",在s1.intern()之后,由于常量池中没有该字符串的引用(只有字符串常量"11"),所以常量池中生成一个堆中字符串"11"的引用,此时String s2= "11"返回的是堆字符串"11"的引用,所以s1==s2。 在JDK1.7版本以及之后的版本运行以下代码,你会发现结果为true,在JDK1.6版本运行的结果却为false: String s1 = new String("1") + new String("1"); System.out.println( s1.intern()==s1); 而String s1 = new String("11")首先会在常量池中创建字符串"11"的引用,而s1则是返回的堆中的new String("11")对象的引用,此时s1.intern()返回的是常量池字符串常量"11"的引用,而非堆中的。而String s2="11"又是返回的常量池中常量"11"的引用。所以s1==s2为false。 总结:常量池中同时存在字符串常量和字符串引用,在JDK1.7版本之后的intern()方法只会尝试对象的引用放入常量池,而在之前的版本中,intern()方法会复制字符串常量到常量池中,并返回字符串引用。

    共 4 条评论
    7
  • -W.LI-
    2019-05-26
    老师好!第一个问题没有描述清楚。String a = ”abc”, String b =new String("abc"),String c=new String(new char[]{‘a’,‘b’,‘c’})。创建的String对象。我debug时发现这三个String对象的value指向的那个char数组地址值都是一样的。他们是复用了一个char数组么?还是工具显示问题?我用的idea。

    作者回复: 你好 W.LI,刚我debug了下,a和b的value是同一个地址,因为a在常量池中创建了"abc",而new String("abc")时,发现常量池存在"abc"字符串对象,不会创建了。这时通过构造函数String(String original)将常量池中的"abc"复制给value,这里的复制是引用,不是创建新的char[]数组,所以是同一个value地址。 而c中的构造函数,是新开辟了一个char[]数组: public String(char value[]) { this.value = Arrays.copyOf(value, value.length); } 所以value的地址不一样。 可以再试试,有问题留言。

    7
  • Eric
    2019-05-25
    String s1 = new String("abc").intern() Code: 0: new #2 // class java/lang/String 3: dup 4: ldc #3 // String abc 6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 9: invokevirtual #5 // Method java/lang/String.intern:()Ljava/lang/String; 12: astore_1 13: return 9:invokevirtual的时候 常量池里面应该早就有了”abc“这个字符串常量了吧 为什么文中说的是先去堆中创建一个String对象 然后再去常量池创建一个字符串常量? 我理解错误了吗?
    展开

    作者回复: 我们可以看到0 new,即是生成了一个对象,这个对象是在堆内存用创建的,之后4 Idc则是将常量池中创建的字符串abc压入栈中,invokespecial调用构造方法复制abc字符串到对象中,invokevirtual调用intern本地方法,返回常量池中的对象引用给s1。 new String("abc")是会创建两个对象的,一个是堆对象,一个是常量池中的对象,intern会去判断常量池中是否有,这个时候是有的,所以不会创建,而是改变s1的引用。 不知道这样是否更好理解?

    7
  • 建国
    2019-05-25
    在实际编码中我们应该使用什么方式创建字符传呢? A.String str= "abcdef"; B.String str= new String("abcdef"); C.String str= new String("abcdef"). intern(); D.String str1=str.intern();

    作者回复: 实际编码中,我们要结合实际场景来选择创建字符串的方式,例如,在创建局部变量以及常量时,我们一般使用A的这种方式;如果我们要区别一个字符串创建两个不同的对象来使用时,会选择B;intern一般使用的比较少,例如我们平时会创建很多一样的字符串的对象时,且对象会保存在内存中,我们可以考虑使用intern方法来减少过多重复对象占用内存空间。

    共 3 条评论
    7
  • benben
    2019-06-26
    请教最后一张图第三列的意思是对象成员变量是string的话不会放到常量池是吗?

    作者回复: 是的,运行时动态创建是在堆内存中直接创建的,调用intern方法,会反倒常量池中。

    4
  • 水月
    2019-07-09
    老师请教一个问题,通过抽离出单独类SharedLocation,存储减到了20G,麻烦解析下原理?

    作者回复: 共享一个类,减少在不同的类中重复创建location的信息。

    3
  • Hammy
    2019-06-21
    老师您好,我这里有一个疑问。在听您说完,对象的string属性实质上在运行中是在堆内存中创建而不是引用常量池的时候如雷贯耳一般,觉得自己之前根本没思考过这个问题,完全没想过用intern进行优化。但是我做了一个实验,public class Person { public String name; public void setName(String name) { this.name = name; } public String getName() { return name; } public static void main(String[] args) { Person person1 = new Person(); person1.setName("张三"); Person person2 = new Person(); person2.setName("张三"); System.out.println(person1.name==person2.name); } 这段代码中,我理解如果string是在运行过程中在堆内存生成对象,那么结果应该是false,但是返回的结果是true。这是我的一个疑惑,劳烦老师帮忙看一下我的测试代码哪里不对,还是有理解错误的地方。
    展开

    作者回复: "张三"是常量,而不是一个对象,所以会有问题。我们可以使用外部传值的方式试试。

    共 4 条评论
    3
  • 西门吹水之城
    2019-06-04
    老师您好,看下面的留言,您看我这这么理解对吗? String b=new String(“abc”); for(int i=0;i<10;i++){ String c=new String(i+“”); } 上面的代码中,b和c是不同的,b在编译的时候会将abc放入常量池中,b引用的堆内存,堆内存引用常量池。c在编译时候没有字符串,在运行的时候,会直接存入内存中,不会将字符串放入常量池。这样解释可以吗?
    展开

    作者回复: b是在类加载时,放入到常量池中。其他地方理解没问题。

    3
  • ° BugMaker
    2019-05-31
    刘老师您好!"使用 intern 方法需要注意的一点是,一定要结合实际场景。因为常量池的实现是类似于一个 HashTable 的实现方式,HashTable 存储的数据越大,遍历的时间复杂度就会增加。如果数据过大,会增加整个字符串常量池的负担",那这个Twitter 工程师在 QCon 全球软件开发大会上的演讲的那个 intern 方法是如何做到遍历这么多常量池的数据,同时保证性能的呢?

    作者回复: 你好,如果我们的数据对查询速度没有这么高要求,可以考虑使用。

    3