场景
String.intern()
String.intern() 方法用于在字符串常量池中查找是否存在与指定字符串相等的字符串。
如果找到了,就返回该字符串的引用;否则,就在字符串常量池中创建一个新的字符串对象,并返回对它的引用。
这个方法对于避免创建重复的字符串对象非常有用,特别是在处理大量字符串数据时,可以显著减少内存使用。
需要明确Java(这里是JDK1.8)创建字符串的不同:
1.使用双引号声明的字符串对象会保存在字符串常量池中
2.使用new关键字创建的字符串对象会先从字符串常量池中找,如果没找到就创建一个,
然后再在堆中创建字符串对象;如果找到了,就直接在堆中创建字符串对象
看下面的两段代码,猜测下运行结果
String str1 = new String("公众号:霸道的程序猿");
String str2 = str1.intern();
System.out.println(str1 == str2);
这段运行结果为false
第一行,字符串常量池中会先创建一个公众号:霸道的程序猿的对象,
然后堆中会再创建一个公众号:霸道的程序猿的对象,str1引用的是堆中的对象。
第二行,str1执行intern()方法,该方法会从字符串常量池中查找公众号:霸道的程序猿,
常量池中存在公众号:霸道的程序猿字符串是因为第一行代码已经在字符串常量池创建公众号:霸道的程序猿字符串,
所以str2引用的是字符串常量池中的对象。str1 和 str2 的引用地址是不同的,str1一个来自堆,str2一个来自字符串常量池,
所以输出的结果为 false。
这里为什么还会再堆中再创建一个公众号:霸道的程序猿的对象,是因为:
Java 7之后,由于字符串常量池被移动到了堆中,执行String.intern()方法时,
如果堆中已经存在了该对象,字符串常量池中就不会创建新的对象,而是直接保存堆中对象的引用。
这一优化节省了一部分内存空间。
那么再看下面的代码:
String str3 = new String("公众号")+new
String("霸道的程序猿");
String str4 =
str3.intern();
System.out.println(str3 ==
str4);
输出结果为:true
第一行,字符串常量池中会先创建两个对象公众号和霸道的程序猿,然后堆中会再创建两个匿名对象公众号和霸道的程序猿,
最后还有一个公众号霸道的程序猿,str3引用的是堆中公众号霸道的程序猿对象。
第二行,str3 执行intern()方法,该方法会从字符串常量池中查找公众号霸道的程序猿对象是否存在,此时字符串常量池不存在,但是堆中存在
字符串常量池中保存的是堆中公众号霸道的程序猿对象的引用,也就是说,str3和str4的引用地址是相同,所以输出的结果为 true。
具体步骤如下:
创建 "公众号" 字符串对象,存储在字符串常量池中。
创建 "霸道的程序猿" 字符串对象,存储在字符串常量池中。
执行 new String("公众号"),在堆上创建一个字符串对象,内容为 "公众号"。
执行 new String("霸道的程序猿"),在堆上创建一个字符串对象,内容为 "霸道的程序猿"。
执行 new String("公众号") + new String("霸道的程序猿"),会创建一个 StringBuilder 对象,并将 "公众号" 和 "霸道的程序猿" 追加到其中,
然后调用 StringBuilder对象的ToString() 方法,将其转换为一个新的字符串对象,内容为 "公众号霸道的程序猿"。
这个新的字符串对象存储在堆上。
特别说明:
编译器遇到 + 号这个操作符的时候,会将 new String("公众号") + new String("霸道的程序猿") 编译代码如下:
new StringBuilder().append("公众号").append("霸道的程序猿").toString();
实际步骤如下:
创建一个 StringBuilder 对象。
StringBuilder 对象上调用 append("公众号"),将 "公众号" 追加到 StringBuilder 中。
在 StringBuilder 对象上调用 append("霸道的程序猿"),将 "霸道的程序猿" 追加到 StringBuilder 中。
在 StringBuilder 对象上调用 toString() 方法,将 StringBuilder 转换为一个新的字符串对象,内容为 "公众号霸道的程序猿"
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
实现
下面做一下在极端条件下使用String.intern()与不使用的区别
参考以上测试工具的使用,编写如下测试代码
import java.util.Random;
import
java.util.concurrent.TimeUnit;
public class StringInternTest {
static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];
public static void main(String[] args) throws InterruptedException {
Integer[] DB_DATA = new Integer[10];
Random random = new Random(10 * 10000);
for (int i = 0; i < DB_DATA.length; i++) {
DB_DATA[i] = random.nextInt();
}
long t =
System.currentTimeMillis();
for (int i = 0; i
< MAX; i++) {
//arr[i] = new
String(String.valueOf(DB_DATA[i % DB_DATA.length]));
arr[i] = new String(String.valueOf(DB_DATA[i %
DB_DATA.length])).intern();
}
System.out.println((System.currentTimeMillis() -
t) + "ms");
System.gc();
TimeUnit.SECONDS.sleep(25);
}
}
性能测试结果对比
不使用intern()方法
使用intern()方法
这里对比内存占用消耗工具的使用参考如下
JVM常用工具中jmap实现手动进行堆转储(heap dump文件)并使用MAT(Memory Analyzer Tool)进行堆分析-内存消耗分析:
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/140549397
这个例子来源于美团技术团队提供,具体原理与流程可参考如下:
https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html
另一个例子是推特优化发布消息使用String.intern()节省内存的操作:
文章可参考如下:
https://www.codecentric.de/wissens-hub/blog/save-memory-by-using-string-intern-in-java
这里简单记录下
每次发布消息状态的时候,都会产生一个地址信息,以当时 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.setCount
sharedLocation.setRegion(messageInfo.getCountryCode().intern());
Location location = new Location();
location.set(sharedLocation);
location.set(messageInfo.getLongitude());
location.set(messageInfo.getLatitude());
使用 intern 方法需要注意的一点是,一定要结合实际场景。
因为常量池的实现是类似于一个 HashTable 的实现方式,HashTable 存储的数据越大,遍历的时间复杂度就会增加。
如果数据过大,会增加整个字符串常量池的负担。
对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用intern()方法能够节省内存空间。
大的网站平台,需要内存中存储大量的字符串。比如社交网站,很多人都存储:北京市、海淀区等信息。
这时候如果字符串都调用intern()方法,就会很明显降低内存的大小。
标签:Java,String,对象,intern,new,字符串,霸道 From: https://www.cnblogs.com/badaoliumangqizhi/p/18315507