从JVM看String及intern方法
文章目录
前言
学习JVM过程中,遇到String的intern()方法,然后在网上找了很多发现都不怎么系统,很多说法也都不一致。所以笔者决定研究一下,以此记录下来。
先看一下intern()的意义
简单说就是放入字符串常量池
实现方式的演变
- JDK1.7之前:调用这个方法,先去字符串常量池中看是否已经存在,如果已经存在,那么直接返回这个常量在字符串常量池中的地址值,如果不存在,则在字符串常量池中创建一个,并返回其地址值。
- JDK1.7及之后:调用这个方法,先去字符串常量池中看是否已经存在,如果已经存在,那么直接返回这个常量在字符串常量池中的地址值,如果不存在,则在字符串常量池中保存一份堆中的引用,并返回其地址值。
这也是JDK7对常量池保存内容的优化:通过字面量(常量)的方式和new String()的方式创建字符串,常量池保存的是对象;通过intern()方法,常量池保存的引用(一份堆中的引用的复制)。
以下均在JDK7以后版本分析~
创建String的几种方式
-
String str= "abc"
:通过字面量的方式。先看常量池中是否已经存在相同字符串 (equals()
),若存在,返回常量池中的引用。若不存在,在常量池中创建字符串对象并返回其引用。 -
String str = new String("abc")
:通过显式创建字符串对象的方式。先在堆中创建一个对象,再看常量池中是否已经存在相同字符串,若存在,直接返回堆中的引用。若不存在,在常量池中创建字符串对象并返回堆中的引用。 -
String str = "abc" + "de"
:通过常量拼接的方式。会编译优化,等效于String str = "abcde"
。 -
String str = new String("abc") + new String("de")
:涉及对象的拼接。底层是通过创建StringBuilder对象用append方法,最后通过toString转成String(只有new String()
才会在常量池中创建对象)。共创建5个对象:堆中(“abc”,“de”,“adbde”),字符串常量池中(“abc”,“de”)。
这也说明了,在循环体中无论用哪种形式拼接字符串,都会创建很多对象,浪费内存,影响性能~
通过4个例子完整分析intern()影响的内存结构
System.identityHashCode(obj)表示对象引用地址的 hashcode。
常量池逻辑属于方法区,物理上存在于堆中,为方便描述下面不予区分了。
- eg1
@Test
public void function1() {
String str1 = new String("abc");
String str2 = "abc";
String intern = str1.intern();
System.out.println(str1==str2);//false
System.out.println("str1: "+System.identityHashCode(str1));//1018081122
System.out.println("str2: "+System.identityHashCode(str2));//242131142
System.out.println("intern: "+System.identityHashCode(intern));//242131142
}
String str1 = new String("abc")
在堆和常量池各创建一个对象,str1指向堆。String str2 = "abc"
在常量池中找到了"abc"的引用,所以直接返回常量池的引用。String intern = str1.intern()
在常量池中找到了"abc"的引用,所以直接返回常量池的引用。
- eg2
@Test
public void function2() {
String str1 = new String("abc");
String intern = str1.intern();
String str2 = "abc";
System.out.println(str1==str2);//false
System.out.println("str1: "+System.identityHashCode(str1));//1018081122
System.out.println("str2: "+System.identityHashCode(str2));//242131142
System.out.println("intern: "+System.identityHashCode(intern));//242131142
}
这里只是改变了第2,3行顺序,但结构同eg2完全一样。
- eg3
@Test
public void function3() {
// String hel = new String("hel");
// String lo = new String("lo");
// String str1 = hel+lo;
String str1 = new String("hel") + new String("lo");
String intern = str1.intern();
String str2 = "hello";
System.out.println(str1 == str2);//true
System.out.println("str1: " + System.identityHashCode(str1));//1018081122
System.out.println("str2: " + System.identityHashCode(str2));//1018081122
System.out.println("intern: " + System.identityHashCode(intern));//1018081122
// System.out.println(System.identityHashCode(hel));//242131142
// System.out.println(System.identityHashCode(hel.intern()));//1782113663
// System.out.println(System.identityHashCode(lo));//1433867275
// System.out.println(System.identityHashCode(lo.intern()));//476800120
}
释放注释,则可打印详细的对象的内存地址hashcode。String str1 = new String("hel") + new String("lo");
共创建5个对象:堆中(“hel”,“lo”,“hello”),字符串常量池中(“hel”,“lo”)。str1指向堆。String intern = str1.intern();
在常量池中没找到"hello"的引用,所以在常量池中保存一份hello对象的引用,并返回该引用地址。String str2 = "hello";
在常量池中找到了"abc"的引用,所以直接返回常量池的引用。
str1指向堆的引用,intern 和str2指向的是str1的堆中引用的复制品,所以都是相等的。
- eg4
改变了第2,3行顺序
@Test
public void function4() {
// String hel = new String("hel");
// String lo = new String("lo");
// String str1 = hel+lo;
String str1 = new String("hel") + new String("lo");
String str2 = "hello";
String intern = str1.intern();
System.out.println(str1 == str2);//true
System.out.println("str1: " + System.identityHashCode(str1));//1018081122
System.out.println("str2: " + System.identityHashCode(str2));//242131142
System.out.println("intern: " + System.identityHashCode(intern));//242131142
// System.out.println(System.identityHashCode(hel));//242131142
// System.out.println(System.identityHashCode(hel.intern()));//1782113663
// System.out.println(System.identityHashCode(lo));//1433867275
// System.out.println(System.identityHashCode(lo.intern()));//476800120
}
String str1 = new String("hel") + new String("lo");
先创建5个对象:堆中(“hel”,“lo”,“hello”),字符串常量池中(“hel”,“lo”)。str1指向堆。String str2 = "hello";
在常量池中没找到了"abc"的引用,所以创建对象并返回常量池的引用。String intern = str1.intern();
在常量池中找到了"hello"的引用,所以直接返回该引用地址。
str1指向堆的引用,intern 和str2指向的是str1的常量池中的引用,所以str1和str2不相等。
String随着JDK做的改变
JDK1.6:常量池存在于PermGen区(永久代)。缺点:永久代大小不好指定。与Java堆物理隔离,intern()可能产生对个重复的字符串,浪费性能。
JDK1.7:常量池存在于堆中。优点:堆大小不再受于固定大小。位于堆区的常量池,可以被垃圾回收。字符串常量池内部维护一个HashMap,通过需要intern()数量的2倍设置为size(减少hash冲突)。性能更好。
JDK1.8 :存储数据结构char[] 。
JDK1.9 :存储数据结构byte[]。动机:大多数String存的是英文,iso-8859等,只有一个字节就够了(char是两个字节),优化为byte[],如果是中文再用两个字节。
两个问题
大家看到最后应该对字符串常量池及intern方法有了一定的理解。但是这里还有两个问题:
1.在eg3的例子中如果字符串是"java"、"12"、"1122"
等一些特殊字符串例子就不成立了。猜测:jvm初始化的时候常量池中就已经存在这些字符串了。
2.通过单测和主线程运行同样代码,结果不一样。查阅后,什么魔法值?
public static void main(String[] args) {
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);//true
}
@Test
public void fun4() {
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);//false
}
到这实在不懂了,渴望分享交流,有兴趣的可以研究一下。
写到最后的感受
从我遇到intern方法想弄懂那刻开始,这期间我查阅了好多,发现言论都不一样,如:字符串常量池中保存的引用还是对象、String str = new String("abc")
是创建几个对象、String str = new String("abc") + new String("de")
常量池中到底有没有str等。
就这些技术而言,不是很深入的东西一查一大堆,而更加深入一点的问题,就不容易找到答案了。技术的道路是孤独的,这是很平常的事,做技术的人应该有一颗“想要弄懂”的心,然后才能发现和创造。最后,祝大家在技术的路上越走越远,对自己人生追求的事物越走越近!
本文不足的地方,欢迎指正和交流~
本文地址:https://blog.csdn.net/weixin_42476498/article/details/107349701