此前我写过另外一篇关于字符串常量池的面试题
本篇博客的内容能证明我上一篇博客中的推论
面试题原题:
public class TestDemo {
@Test
public void test01() {
String str1 = new StringBuilder("ja").append("va").toString();
String str2 = str1.intern();
System.out.println(str1 == str2);// 结果是false
String str3 = new StringBuilder("hello").append("world").toString();
String str4 = str3.intern();
System.out.println(str3 == str4);// 结果是true
}
}
关于String.intern()
的正确理解:以str3.intern();
为例,如果字符串常量中没有"helloworld"
这个字符串,则将这个字符串对象存入字符串常量池中,然后返回这个字符串对象。如果有则直接返回字符串常量池中的对应的那个对象,也就是被str4
引用。
关于上面这句话需要注意!
在对象str3
的创建到输出true
这个过程中,如果按照我的解释:
第一行代码:
String str3 = new StringBuilder(“hello”).append(“world”).toString();
在堆中的Eden区根据已有字符串常量"hello"
、"world"
创建一个新的字符串对象"helloworld"
,此时这个对象只是在堆中的Eden区,而字符串常量池则在堆的Old区。
第二行代码:
String str4 = str3.intern();
str3调用intern方法,因为字符串常量池中没有"helloworld"
对象,则将这个对象存入字符串常量池中,然后将这个字符串对象返回。
细节:
很多人在这块没有搞懂,为什么我要这样解释,下面我娓娓道来。
这得涉及到 jvm 的内存管理的知识点。
当我们创建一个对象的时候,意味着需要申请一块空间用来存放这个新创建的对象。可这里有一个问题,jvm 如何管理内存的?
jvm 返回的用于分配新建对象的地址区域的首地址,这块区域(指分配给对象的内存)肯定是不能被其它类使用的,不然内存岂不是乱来了?
换句话说jvm 是有做内存管理的,下面借用《深入理解java虚拟机》原文作为说明
意思就是说虚拟机的内存管理我们可以用一张表来记录哪些内存是被使用的,哪些是空闲的,如果要进行gc,也很简单,就是对被使用的内存进行垃圾回收,然后涉及到垃圾回收算法,标记清除,标记整理,标记复制
,其中后面两种可以将内存整理到一块,可以避免产生大量内存碎片,造成内存浪费。
那对于第一行到第二行代码就可以这样完整的解释
第一行代码:jvm根据这张内存管理表,找到一块空闲的空间,然后将"helloworld"
对象存到这块空间中,然后将这个地址在表中记录下来(表示这块区域就是这个对象)。
而String str3 = new StringBuilder(“hello”).append(“world”).toString();的操作,str3的赋值底层的操作相当于将指针指向了表中的这条记录,而值才是这个"helloworld"
对象的真实地址,意味着str3指向的是表中的记录,这个值在虚拟机栈中是不会改变的(如果没有其它的赋值操作)。而当我们移动对象的时候,只需要更新一下表,那么虚拟机栈中这个str3变量依然能找到移动后的对象。
最近看了一下windows的内存管理,猜测JVM的内存管理是借鉴了windows等操作系统句柄的概念,相当于对象移动后,依然可以通过句柄去访问对象,对于用户来说,只需要通过句柄进行访问,而操作系统只需要将句柄所引用的指针指向对象新的地址,这样也就可以在移动对象后,用户无察觉的情况下对内存整理回收等操作
画两张图说明这个过程
下面是创建对象的过程
下面是调用intern()
方法做的事情,将对象移动,更新内存管理表中对象的内存位置。
这样就做到了对象的移动后还能通过同一个str可以访问到移动后的那个对象。
关于对象移动其实在gc的时候也会导致对象移动,原理和这个差不多。只不过在这个案例中我们是通过navtive
声明的intern()
方法完成对象的移动,而不是gc导致对象移动。
回过头来再看看这道面试题
这道题目就很简单了
str4做的事情就是增加了一条引用记录相当于下面的截图,显然两个对象的地址是同一个(都是0xffffffff
),因此会返回true
上面的已经讲完,作为补充,把上面那道面试题的另外一种情况关于"java"
返回false的也用图来解释一遍,就是
“java”
字符串在Version类中有定义过例如我的jdk中关于Version的部分代码
除了"java"
,还有"1.8.0_201"
、"Java(TM) SE Runtime Environment"
、"1.8.0_201-b09"
这些字符串常量也是在字符串常量池中已有(可以自己根据jdk版本找到对应的这些信息)
public class Version {
private static final String launcher_name = "java";
private static final String java_version = "1.8.0_201";
private static final String java_runtime_name = "Java(TM) SE Runtime Environment";
private static final String java_profile_name = "";
private static final String java_runtime_version = "1.8.0_201-b09";
......//省略其它定义
}
而我们执行str1的第一行代码String str1 = new StringBuilder(“ja”).append(“va”).toString();就是根据已有字符串"ja"、"va"
再Eden区中创建一个"java"字符串对象,注意字符串常量池中已经有了这个字符对象,这是两个不同的对象,这两个对象存在内存中的位置也不同。
而str2.intern()
方法就相当于下面这样将str2指向了引用字符串常量池中的那个地址0xffffffff
,而str1指向的是0xf1f2f3f4
因此str1==str2
返回的是false
,因为两个地址不同,对于非基本类型==
比较的是地址,也就是比较0xf1f2f3f4
、0xffffffff