Java-String.intern的深入研究
When---什么时候需要了解String的intern方法:
面试的时候(蜜汁尴尬)!虽然不想承认,不过面试的时候经常碰到这种高逼格的问题来考察我们是否真正理解了String的不可变性、String常量池的设计以及String.intern方法所做的事情。但其实,我们在实际的编程中也可能碰到可以利用String.intern方法来提高程序效率或者减少内存占用的情况,这个我们等下会细说。
What---String.intern方法究竟做了什么:
Returns a canonical representation for the string object. A pool of strings, initially empty, is maintained privately by the class String. When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned. It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true. All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java? Language Specification.
上面是jdk源码中对intern方法的详细解释。简单来说就是intern用来返回常量池中的某字符串,如果常量池中已经存在该字符串,则直接返回常量池中该对象的引用。否则,在常量池中加入该对象,然后 返回引用。下面的一个例子详细的解释了intern的作用过程:
Now lets understand how Java handles these strings. When you create two string literals:
String name1 = "Ram";
String name2 = "Ram";
In this case, JVM searches String constant pool for value "Ram", and if it does not find it there then it allocates a new memory space and store value "Ram" and return its reference to name1. Similarly, for name2 it checks String constant pool for value "Ram" but this time it find "Ram" there so it does nothing simply return the reference to name2 variable. The way how java handles only one copy of distinct string is called String interning.
How---String.intern方法在jdk1.7之前和之后的区别:
简单的说其实就一个:在jdk1.7之前,字符串常量存储在方法区的PermGen Space。在jdk1.7之后,字符串常量重新被移到了堆中。
Back---重回String设计的初衷:
Java中的String被设计成不可变的,出于以下几点考虑:
1. 字符串常量池的需要。字符串常量池的诞生是为了提升效率和减少内存分配。可以说我们编程有百分之八十的时间在处理字符串,而处理的字符串中有很大概率会出现重复的情况。正因为String的不可变性,常量池很容易被管理和优化。
2. 安全性考虑。正因为使用字符串的场景如此之多,所以设计成不可变可以有效的防止字符串被有意或者无意的篡改。从java源码中String的设计中我们不难发现,该类被final修饰,同时所有的属性都被final修饰,在源码中也未暴露任何成员变量的修改方法。(当然如果我们想,通过反射或者Unsafe直接操作内存的手段也可以实现对所谓不可变String的修改)。
3. 作为HashMap、HashTable等hash型数据key的必要。因为不可变的设计,jvm底层很容易在缓存String对象的时候缓存其hashcode,这样在执行效率上会大大提升。
Deeper---直接来看例子:
首先来试试下面程序的运行结果是否与预想的一致:
String s1 = new String("aaa");
String s2 = "aaa";
System.out.println(s1 == s2); // false
s1 = new String("bbb").intern();
s2 = "bbb";
System.out.println(s1 == s2); // true
s1 = "ccc";
s2 = "ccc";
System.out.println(s1 == s2); // true
s1 = new String("ddd").intern();
s2 = new String("ddd").intern();
System.out.println(s1 == s2); // true
s1 = "ab" + "cd";
s2 = "abcd";
System.out.println(s1 == s2); // true
String temp = "hh";
s1 = "a" + temp;
// 如果调用s1.intern 则最终返回true
s2 = "ahh";
System.out.println(s1 == s2); // false
temp = "hh".intern();
s1 = "a" + temp;
s2 = "ahh";
System.out.println(s1 == s2); // false
temp = "hh".intern();
s1 = ("a" + temp).intern();
s2 = "ahh";
System.out.println(s1 == s2); // true
s1 = new String("1"); // 同时会生成堆中的对象 以及常量池中1的对象,但是此时s1是指向堆中的对象的
s1.intern(); // 常量池中的已经存在
s2 = "1";
System.out.println(s1 == s2); // false
String s3 = new String("1") + new String("1"); // 此时生成了四个对象 常量池中的"1" + 2个堆中的"1" + s3指向的堆中的对象(注此时常量池不会生成"11")
s3.intern(); // jdk1.7之后,常量池不仅仅可以存储对象,还可以存储对象的引用,会直接将s3的地址存储在常量池
String s4 = "11"; // jdk1.7之后,常量池中的地址其实就是s3的地址
System.out.println(s3 == s4); // jdk1.7之前false, jdk1.7之后true
s3 = new String("2") + new String("2");
s4 = "22"; // 常量池中不存在22,所以会新开辟一个存储22对象的常量池地址
s3.intern(); // 常量池22的地址和s3的地址不同
System.out.println(s3 == s4); // false
// 对于什么时候会在常量池存储字符串对象,我想我们可以基本得出结论: 1. 显示调用String的intern方法的时候; 2. 直接声明字符串字面常量的时候,例如: String a = "aaa";
// 3. 字符串直接常量相加的时候,例如: String c = "aa" + "bb"; 其中的aa/bb只要有任何一个不是字符串字面常量形式,都不会在常量池生成"aabb". 且此时jvm做了优化,不// 会同时生成"aa"和"bb"在字符串常量池中
如果有出入的话,再来看看具体的字节码分析:
/**
* 字节码为:
* 0: ldc #16; //String 11 --- 从常量池加载字符串常量11
2: astore_1 --- 将11的引用存到本地变量1,其实就是将s指向常量池中11的位置
*/
String s = "11"; /**
* 0: new #16; //class java/lang/String --- 新开辟了一个地址,存储new出来的对象
3: dup --- 将new出来的对象复制了一份到栈顶(也就是s1最终指向的是堆中的另一个存储字符串11的地址)
4: ldc #18; //String 11
6: invokespecial #20; //Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
*/
String s1 = new String("11"); /**
* 0: new #16; //class java/lang/StringBuilder --- 可以看到jdk对字符串拼接做了优化,先是建了一个StringBuilder对象
3: dup
4: new #18; //class java/lang/String --- 创建String对象
7: dup
8: ldc #20; //String 1 --- 从常量池加载了1(此时常量池和堆中都会存字符串对象)
10: invokespecial #22; //Method java/lang/String."<init>":(Ljava/lang/String;)V --- 初始化String("1")对象
13: invokestatic #25; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #29; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V --- 初始化StringBuilder对象
19: new #18; //class java/lang/String
22: dup
23: ldc #20; //String 1
25: invokespecial #22; //Method java/lang/String."<init>":(Ljava/lang/String;)V
28: invokevirtual #30; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #34; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1 ---从上可以看到实际上常量池目前只存了1
36: invokevirtual #38; //Method java/lang/String.intern:()Ljava/lang/String; --- 调用String.intern中,jdk1.7以后,常量池也是堆中的一部分且常量池可以存引用,这里直接存的是s2的引用
39: pop --- 这里直接返回的是栈顶的元素
*/
String s2 = new String("1") + new String("1");
s2.intern(); /**
* 0: ldc #16; //String abc --- 可以看到此时常量池直接存储的是:abc, 而不会a、b、c各存一份
2: astore_1
*/
String s3 = "a" + "b" + "c"; /**
0: new #16; //class java/lang/StringBuilder
3: dup
4: ldc #18; //String why --- 常量池的why
6: invokespecial #20; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
9: ldc #23; //String true --- 常量池的true
11: invokevirtual #25; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: invokevirtual #29; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
17: astore_1
*/
String s1 = new StringBuilder("why").append("true").toString();
System.out.println(s1 == s1.intern()); // jdk1.7之前为false,之后为true
下面我们延伸一下来讲讲字符串拼接的优化问题:
1 String a = "1";
2 for (int i=0; i<10; i++) {
3 a += i;
4 }
0: ldc #16; //String 1
2: astore_1
3: iconst_0
4: istore_2 --- 循环开始
5: goto 30
8: new #18; //class java/lang/StringBuilder --- 每个循环都建了一个StringBuilder对象,对性能有损耗
11: dup
12: aload_1
13: invokestatic #20; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #26; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
19: iload_2
20: invokevirtual #29; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
23: invokevirtual #33; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
26: astore_1
27: iinc 2, 1 ---- 计数加1
30: iload_2
31: bipush 10
33: if_icmplt 8 String a = "1";
for (int i=0; i<10; i++) {
a += "1";
}
的字节码为:
0: ldc #16; //String 1
2: astore_1
3: iconst_0
4: istore_2
5: goto 31
8: new #18; //class java/lang/StringBuilder ---还是会每次建立一个StringBuilder对象
11: dup
12: aload_1
13: invokestatic #20; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #26; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
19: ldc #16; //String 1 ---和上一个循环的区别也仅仅在于这里是从常量池加载1,
21: invokevirtual #29; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #33; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore_1
28: iinc 2, 1
31: iload_2
32: bipush 10
34: if_icmplt 8
可知,真正的性能瓶颈在于每次循环都建了一个StringBuilder对象
所以我们优化一下 :
StringBuilder sb = new StringBuilder("1");
for (int i=0; i<10; i++) {
sb.append("1");
}
对应的字节码为:
0: new #16; //class java/lang/StringBuilder -- 在循环直接初始化了StringBuilder对象
3: dup
4: ldc #18; //String 1
6: invokespecial #20; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
9: astore_1
10: iconst_0
11: istore_2
12: goto 25
15: aload_1
16: ldc #18; //String 1
18: invokevirtual #23; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: pop
22: iinc 2, 1
25: iload_2
26: bipush 10
28: if_icmplt 15
Where---String.intern的使用:
我们直接看一个例子来结束String.intern之旅吧:
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])); // --- 每次都要new一个对象
// arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern(); --- 其实虽然这么多字符串,但是类型最多为10个,大部分重复的字符串,大大减少内存
} System.out.println((System.currentTimeMillis() - t) + "ms");
System.gc();
参考链接:
http://www.360doc.com/content/14/0721/16/1073512_396062351.shtml
https://www.cnblogs.com/SaraMoring/p/5713732.html
Java-String.intern的深入研究的更多相关文章
- 深入理解Java String#intern() 内存模型
原文出处: codelog.me 大家知道,Java中string.intern()方法调用会先去字符串常量池中查找相应的字符串,如果字符串不存在,就会在字符串常量池中创建该字符串然后再返回. 字符串 ...
- Java String.intern()_学习笔记
参考:https://www.jianshu.com/p/0d1c003d2ff5 String.intern() String.intern()是native方法,底层调用c++中的StringTa ...
- 聊聊Java String.intern 背后你不知道的知识
Java的 String类有个有意思的public方法: public String intern() 返回标准表示的字符串对象.String类维护私有字符串池. 调用此方法时,如果字符串池已经包含等 ...
- java String.intern();
0.引言 什么都先不说,先看下面这个引入的例子: String str1 = new String("SEU")+ new String("Calvin"); ...
- 深入理解java String 及intern
一.字符串问题 字符串在我们平时的编码工作中其实用的非常多,并且用起来也比较简单,所以很少有人对其做特别深入的研究.倒是面试或者笔试的时候,往往会涉及比较深入和难度大一点的问题.我在招聘的时候也偶尔会 ...
- 通过反编译深入理解Java String及intern(转)
通过反编译深入理解Java String及intern 原文传送门:http://www.cnblogs.com/paddix/p/5326863.html 一.字符串问题 字符串在我们平时的编码工作 ...
- 通过反编译深入理解Java String及intern
一.字符串问题 字符串在我们平时的编码工作中其实用的非常多,并且用起来也比较简单,所以很少有人对其做特别深入的研究.倒是面试或者笔试的时候,往往会涉及比较深入和难度大一点的问题.我在招聘的时候也偶尔会 ...
- 通过反编译看Java String及intern内幕--费元星站长
通过反编译看Java String及intern内幕 一.字符串问题 字符串在我们平时的编码工作中其实用的非常多,并且用起来也比较简单,所以很少有人对其做特别深入的研究.倒是面试或者笔试的时候,往 ...
- 【Java必修课】String.intern()原来还能这么用(原理与应用)
1 简介 String.intern()是JDK一早就提供的native方法,不由Java实现,而是底层JVM实现,这让我们对它的窥探提高了难度.特别是在Oracle收购了Sun公司后,源代码不开源了 ...
- (转)通过反编译深入理解Java String及intern
原文链接:https://www.cnblogs.com/paddix/p/5326863.html 一.字符串问题 字符串在我们平时的编码工作中用的非常多,并且用起来非常简单,所以很少有人对其做特别 ...
随机推荐
- Oracle12c_安装1——准备工作
1.建议用户和组 su root #切换到root groupadd oinstall #创建用户组oinstall groupadd dba #创建用户组dba useradd -g oinstal ...
- K:线性表的实现—链表
单链表的概念: 采用链式存储方式存储的线性表称之为链表,链表中每个节点包含存放数据元素的值的数据域和存放指向逻辑上相邻节点的指针域.若一个节点中只包含一个指针域,则称此链表为单链表. 单链表的特点: ...
- 面向对象 初级版 (Preview) 未完
概述: 面向过程:根据业务逻辑从上到下写垒代码 函数式:将某功能代码封装到函数里,日后使用无需重复编写,直接调用韩顺即可. 面向对象: 对函数进行分类和封装,让开发'更快更强' 面向对象和面向过程的通 ...
- 关于微信小程序,一些想法
不负众望,小程序终于在昨天1月9日正式上线,从凌晨微信公开课发布了微信<一月九日,一年之约>到现在,整整一天的时间,朋友圈.自媒体平台都在不断的发着关于小程序的各种文章,"APP ...
- 微信小程序开发教程目录
本系列教程是自己在工作中使用到而记录的,没有顺序之分 如有错误之处,请给与指正,也不希望误导了别人 微信小程序开发教程目录 微信小程序之注册和入门 微信小程序之HTTPS请求 微信小程序开发之选项卡 ...
- Jenkins:基于linux构建ivy项目
Jenkins:基于linux构建ivy项目 (二) 基于以上<Jenkins:VMware虚拟机Linux系统的详细安装和使用教程(一)>的配置再进行对ivy项目构建: 启动tomcat ...
- Python可视化库Matplotlib的使用
一.导入数据 import pandas as pd unrate = pd.read_csv('unrate.csv') unrate['DATE'] = pd.to_datetime(unrate ...
- Redis学习笔记(二)Redis支持的5种数据类型的总结之String和Hash
引言 在Redis学习笔记(一)中我们已经会安装并且简单使用Redis了,接下来我们一起来学习下Redis支持的5大数据类型. 简介 Redis是REmote DIctionary Server(远程 ...
- Elasticsearch Head插件实践
简介 Elasticsearch是一个基于Lucene的搜索服务器.它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口.Elasticsearch是用Java开发的,并作为Ap ...
- 【高精度乘法】NOIP2003麦森数
题目描述 形如2^{P}-12P−1的素数称为麦森数,这时PP一定也是个素数.但反过来不一定,即如果PP是个素数,2^{P}-12P−1不一定也是素数.到1998年底,人们已找到了37个麦森数.最大的 ...