String字符串相加的原理
因为string是非常常用的类, jvm对其进行了优化, jdk7之前jvm维护了很多的字符串常量在方法去的常量池中, jdk后常量池迁移到了堆中
方法区是一个运行时jvm管理的内存区域,是一个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态常量等。
使用引号来创建字符串
- 单独(注意是单独)使用引号来创建字符串的方式,字符串都是常量,在编译期已经确定存储在常量池中了。
- 用引号创建一个字符串的时候,首先会去常量池中寻找有没有相等的这个常量对象,没有的话就在常量池中创建这个常量对象;有的话就直接返回这个常量对象的引用。
所以看这个例子:
string str1 = "hello"; string str2 = "hello"; system.out.println(str1 == str2);//true
new的方式创建字符串
string a = new string("abc");
new这个关键字,毫无疑问会在堆中分配内存,创建一个string类的对象。因此,a这个在栈中的引用指向的是堆中的这个string对象的。
然后,因为"abc"是个常量,所以会去常量池中找,有没有这个常量存在,没的话分配一个空间,放这个"abc"常量,并将这个常量对象的空间地址给到堆中string对象里面;如果常量池中已经有了这个常量,就直接用那个常量池中的常量对象的引用呗,就只需要创建一个堆中的string对象。
new构造方法中传入字符串常量, 会在堆中创建一个string对象, 但是这个对象不会再去新建字符数组(value) 来存储内容了, 会直接使用字符串常量对象中字符数组(value)应用,
具体方法参考
/** * initializes a newly created {@code string} object so that it represents * the same sequence of characters as the argument; in other words, the * newly created string is a copy of the argument string. unless an * explicit copy of {@code original} is needed, use of this constructor is * unnecessary since strings are immutable. * * @param original * a {@code string} */ public string(string original) { this.value = original.value; //只是把传入对象的value和引用传给新的对象, 两个对象其实是共用同一个数组 this.hash = original.hash; }value 虽然是private修饰的, 但是构造方法中通过original.value;还是可以直接获取另外一个对象的值. 因为这两个对象是相同的类的对象
所以有下面的结果
public static void main(string[] args) { string s1 = new string("hello"); string s2 = "hello"; string s3 = new string("hello"); system.out.println(s1 == s2);// false system.out.println(s1.equals(s2));// true system.out.println(s1 == s3);//false }
关于“+”运算符
常量直接相加:
string s1 = "hello" + "word"; string s2 = "helloword"; system.out,println(s1 == s2);//true
这里的true 是因为编译期直接就把 s1 优化成了 string s1 = "helloword"; 所以后面相等
非常量直接相加
public static void main(string[] args) { string s1 = "a"; string s2 = "b"; string s3 = new string("b"); string s4 = s1 + s3; string s5="ab"; string s6 = s1 + s2; string s66= s1 + s2; string s7 = "a" + s2; string s8 = s1 + "b"; string s9 = "a" + "b"; system.out.println(s2 == s3); //false system.out.println(s4 == s5); //false s4 是使用了stringbulider来相加了 system.out.println(s4 == s6); //false s4和s6 两个都是使用了stringbulider来相加了 system.out.println(s6 == s66); //false 两个都是使用了stringbulider来相加了 system.out.println(s5 == s7); //false s7是使用了stringbulider来相加了 system.out.println(s5 == s8); //false s8是使用了stringbulider来相加了 system.out.println(s7 == s8); //false 两个都是使用了stringbulider来相加了 system.out.println(s9 == s8); //false 两个都是使用了stringbulider来相加了 }
总结下就是:
两个或者两个以上的字符串常量直接相加,在预编译的时候“+”会被优化,相当于把两个或者两个以上字符串常量自动合成一个字符串常量.
编译期就会优化, 编译的字节码直接就把加号去掉了, 直接定义一个常量
其他方式的字符串相加都会使用到 stringbuilder的.
string的intern()方法.
这是一个native的方法,书上是这样描述它的作用的:如果字符串常量池中已经包含一个等于此string对象的字符串,则返回代表池中这个字符串的string对象;否则,将此string对象包含的字符添加到常量池中,并返回此string对象的引用。
并提到,在jdk1.6及其之前的版本,由于常量池分配在永久代内,我们可以通过-xx:permsize和-xx:maxpermsize限制方法区的大小从而间接限制常量池的容量。
不仅如此,在intern方法返回的引用上,jdk1.6和jdk1.7也有个地方不一样,来看看书本上给的例子:
public static void main(string[] args) { string str1 = new stringbuilder("计算机").append("软件").tostring(); system.out.println(str1.intern() == str1); string str2 = new stringbuilder("ja").append("va").tostring(); system.out.println(str2.intern() == str2); }
这段代码在jdk1.6中,会得到两个false,在jdk1.7中运行,会得到一个true和一个false。
书上说,产生差异的原因是:在jdk1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用,而由stringbuilder创建的字符串实例在java堆上,所以必然不是同一个引用,将返回false。
而jdk1.7的intern()不会再复制实例,只是在常量池中记录首次出现的实例的引用,因此intern()返回的引用和stringbuilder创建的那个字符串的实例是同一个。对str2比较返回false是因为"java"这个字符串在执行stringbuilder.tostring()之前就已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。
jdk6和7 的差异是因为 7中常量池移动到堆中了, 并且对于常量池的处理也有差异, 6会把堆中的字符串复制一份副本到常量池中,
7 只是把堆中的字符串对象的引用放入常量池中, 所以第一个str1.intern()返回的也只是一个指向堆中对象的引用, 所以第一个出现false.
7的第二个false 是因为常量池中已经有了"java"对象了, 所以str2.intern()返回的是指向常量池中对象的引用, str2是指向堆中对象的引用, 所以false
stringtable的小说明
只是为了提高速度, 把常量池中的字符串常量维护了一个hashtable. 方便查找常量
这里先再提一下字符串常量池,实际上,为了提高匹配速度,也就是为了更快地查找某个字符串是否在常量池中,java在设计常量池的时候,还搞了张stringtable,这个有点像我们的hashtable,根据字符串的hashcode定位到对应的桶,然后遍历数组查找该字符串对应的引用。如果找得到字符串,则返回引用,找不到则会把字符串常量放到常量池中,并把引用保存到stringtable了里面。
在jdk7、8中,可以通过-xx:stringtablesize参数stringtable大小
jdk1.6及其之前的intern()方法
在jdk6中,常量池在永久代分配内存,永久代和java堆的内存是物理隔离的,执行intern方法时,如果常量池不存在该字符串,虚拟机会在常量池中复制该字符串,并返回引用;如果已经存在该字符串了,则直接返回这个常量池中的这个常量对象的引用。所以需要谨慎使用intern方法,避免常量池中字符串过多,导致性能变慢,甚至发生permgen内存溢出。
看一个图片来理解下:(图片来自https://blog.csdn.net/soonfly/article/details/70147205)
当然,这个常量池和堆是物理隔离的。
总之就是,要抓住“复制”这个字眼,常量池中存的是内容为"abc"的常量对象。
看个详细点的例子:
public static void main(string[] args) { string a = new string("haha"); system.out.println(a.intern() == a);//false }
首先,见到"haha",产量池中没有这个常量,所以会在常量池中放下这个常量对象,底层是通过ldc命令,"haha"被添加到字符串常量池,然后在stringtable中添加该常量的引用(引用好像是这个string对象中的char数组的地址),而a这个引用指向的是堆中这个string对象的地址,所以肯定是不同的。(而且一个在堆,一个在方法区中)。
jdk1.7的intern()方法
jdk 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。
当然这个时候,常量池被从方法区中移出来到了堆中。
看个图:
(图片来自https://blog.csdn.net/soonfly/article/details/70147205)
所以再看回我们书上的那个例子
public static void main(string[] args) { string str1 = new stringbuilder("计算机").append("软件").tostring(); system.out.println(str1.intern() == str1); string str2 = new stringbuilder("ja").append("va").tostring(); system.out.println(str2.intern() == str2);
再看一个例子:
string str2 = new string("str")+new string("01"); str2.intern(); string str1 = "str01"; system.out.println(str2==str1);//true
这个返回true的原因也一样,str2的时候,只有一个堆的string对象,然后调用intern,常量池中没有“str01”这个常量对象,于是常量池中生成了一个对这个堆中string对象的引用。
然后给str1赋值的时候,因为是带引号的,所以去常量池中找,发现有这个常量对象,就返回这个常量对象的引用,也就是str2引用所指向的堆中的string对象的地址。
所以str2和str1指向的是同一个东西,所以为true。
jdk7中虽然是把引用复制到常量池中, 但是不影响常量池的功能, 常量池就是减少常量的创建, 增加性能. 常量池中是引用还是能起到减少常量的作用, 因为引用最终还是会指向真实的对象.
参考博客
https://www.cnblogs.com/wangshen31/p/10404353.html