""+new String("") 创建了几个 String 对象?—— JDK1.5优化和 JDK1.7字符串常量池转移到Java Heap

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/bestcxx/article/details/88899648

前言

体能状态先用精神状态,习惯先于决心,聚焦先于喜好.

须知

本文讨论的问题基于Oracle 公司的 HotSpot JDK
Java 中String 字符串非常重要的一点是:String 对象是不可变的
返回String 对象和返回String对象的引用不是一回事,但在本文你可以认为是返回对象的引用

字符串常量池的前世今生

JVM规范和方法区、字符串常量池的关系

在 JVM 规范中,字符串常量池是一个全局配置,位于JVM模型的方法区中.
它有另一个名字叫“非堆”.
但是在具体实现上,并没有严格按照这个标准.

JVM方法区和HotSpot 的永久代、元数据

在上面的段落我们提到了字符串常量池是方法区的一部分.
HotSpot JDK 对方法区的实现在JDK1.7以及之前被叫做永久代,其内存的物理位置和Java Heap(Java 堆)在一起,从JDK1.8开始,永久代被取消,取而代之的被称为元数据,其位置为操作系统的物理内存,比如你的计算机是4G内存,则方法区的实现大小理论上就是4G,而不是之前JVM的永久代的大小了.

字符串常量池和永久代、Java Heap

在具体实现上,我们常用的 Oracle 的 HotSpot JDK 在 JDK1.6以及之前 字符串常量池都被放在永久代(方法区)中,从JDK1.7开始,字符串常量池被放到了Java Heap中.

创建对象的基本原则

""可能在字符串常量池中创建对象

比如

String a="1";

如果字符串常量池没有内容为“1”的对象
对于JDK1.7开始,同时需要满足字符串常量池不包含,堆中内容为“1”的字符串对象的引用
则在字符串常量池新建一个对象,并返回这个新建对象的地址

再比如:

String a="1";
String b="1";

如果字符串常量池已经有一个相同内容的对象,
对于JDK1.7开始,或者满足字符串常量池包含,堆中内容为"1"的字符串对象的引用
则 String b=“1”,会直接将字符串常量池中"1"的地址返回给b,不会重新创建对象

new String() 一定创建至少一个对象

new 一定会在 Java Heap 中创建一个对象
但是 new String一般的形式是 new String(“1”);
这里 “1” 遵循上面提到的规则,可能在字符串常量池创建一个对象

new(对象)+""一定至少创建2个对象

结合上面的两个规则,考虑下面的情况

String a=new String("123");
String b="1";
String c=a+b;

可以转化为

String c=new String("123")+"1";

共会创建四个对象
字符串常量池创建两个对象: “123”和“1”
Java Heap 创建两个对象:“123”和“1234”

intern() 方法可能会创建对象
intern() 的作用

查看 java.lang.String 中 intern() 方法注释,比较重要的内容:
当 intern 方法被调用,如果常量池中已经包含一个与之相等的字符串,则返回常量池中的这个字符串,否则,将该字符串保存到常量池,并返回这个字符串对象的引用

字符串对象调用该方法的时候,比如

String a=new String(123);
a.intern();

JVM 会检查目前字符串常量池中是否有"123"这个字符串,如果是jdk1.7 以及以上版本,则还需要考虑字符串常量池中是否存在Java Heap 中内容为“123”字符串对象的引用

intern()和jdk1.6及之前版本

对于 jdk1.6以及之前的版本,字符串常量池存在于永久代(方法区)中,调用 intern() 方法的结果有两种可能,如果字符串常量池不存在字符串,则在字符串常量池新建一个字符串对象并返回这个新建对象的引用地址:

  • 情况一
//“”会在字符串常量池创建对象,内容为“123“,intern()方法会返回该对象的地址
String a="123".intern();
  • 情况二
/*
* line1:""会在字符串常量池创建对象,内容为“123”,new 会在Java Heap 创建一个对象内容为第二行“123”
* line2:字符串常量池创建“4”,Java Heap 创建对象 “1234”
* line3:intern()方法判断出字符串常量池已经存在字符串“1234”,因为尚不存在,
* 所以在字符串常量池中 新建对象 “1234”,并返回该新建的对象
*/
String a=new String("123");
String b=a+"4";
b.intern();
intern()和jdk1.7及之后版本

jdk1.7以及之后的版本,虽然从JVM规范看,字符串常量池依旧存在于方法区中,但是在HotSpot的具体实现上,字符串常量池已经被放到 Java Heap 中了,这个时候intern()方法的结果依旧有两种可能,如果字符串常量池不存在字符串,且这个字符串保存在Java Heap 中,则直接将这个 字符串的引用 保存在字符串常量池:

  • 情况一,保持不变
//“”会在字符串常量池创建对象,内容为“123“,intern()方法会返回该对象
String a="123".intern();
  • 情况二,
/*
* line1:""会在字符串常量池创建对象,内容为“123”,new 会在Java Heap 创建一个对象内容为第二行“123”
* line2:字符串常量池创建“4”,Java Heap 创建对象 “1234”
* line3:intern()方法判断出字符串常量池已经存在字符串“1234”,因为尚不存在,
* 所以在字符串常量池中 保存Java Heap中内容为“1234”的对象的引用,并返回该引用
*/
String a=new String("123");
String b=a+"4";
b.intern();
jdk1.7 intern() 引起保存对象的引用算是新建对象吗?

我觉得不算,因为保存对象引用毕竟没有从新创建对象的动作,所以自然是不算创建对象了

复杂的情况

jdk1.5 对 “”+""的优化

从 jdk1.5 开始,JVM 对""+""+“”··· 做了优化
多个字符串相加等同于一个字符串
对于使用循环的""+""等于创建一个 StringBuilder 对象,这个在字节码中可以体现
规则如下

但是不等同于

StringBuilder a=new StringBuilder("1");
a.append("2");
a.append("3");
a.toString();
+转 StringBuilder 字节码体现
  • Java 循环实现 “0”+“1”+“2”+“3”+“4”+···“”
public class BTest {
        public static void main(String[] args) {
                String b="0"+"1"+"2"+"3"+"4"+"5";

                String a="0";
                for(int i=1;i<=10;i++){
                        a=a+i+"";
                }
        }
}
  • 字节码

String b=“0”+“1”+“2”+“3”+“4”+“5”; 优化为 “012345”
for循环优化为 StringBuilder
在这里插入图片描述

对StringBuilder 的详细讨论

StringBuilder 和 StringBuffer 的区别在于 StringBuffer 是线程安全的,StringBuilder是非线程安全的,这里我们仅讨论 StringBuilder 的使用到底在多大程度上减少了对象的产生?
结论可能与你想的不太一样

StringBuilder 的底层实现是char 类型数组

看源码我们可以看到,StringBuilder 底层是一个char[]

StringBuilder 的 append("")遵循 ""的规则

由于 append("")遵循 “”的规则,所以实际上,其并没有想象中那样减少对象的产生,请看下面这个实验,

System.out.println(System.getProperty("java.version"));//JDK版本  jdk1.7及以上
System.out.println(System.getProperty("sun.arch.data.model")); //判断是32位还是64位 
StringBuilder a=new StringBuilder("1");//字符串常量池添加“1”
a.append("234");//字符串常量池添加“234”
String b="23";//字符串常量池添加“23”
String c=b+"4";//Java Heap "234"
System.out.println(c.intern()==c);//c.intern() 获取字符串常量池内容,c在Java Heap故false

StringBuilder.toString() 是 String(char value[], int offset, int count)

StringBuilder.toString() 的底层是调用 String 构造方法 String(char value[], int offset, int count) ,该方法并不会向字符串常量池存放对象,不同于 new String(""),但是会在 Java Heap 中创建一个对象

new StringBuilder(“123”).toString()等于new String(“123”)

从创建对象来说,new StringBuilder(“123”).toString()和new String(“123”) 等效
“123"遵循”"的原则,StringBuilder.toString()和new String 都会在Java Heap 创建一个对象

理解最经典问题

问题一
//JDK版本  jdk1.7及以上
System.out.println(System.getProperty("java.version"));
//判断是32位还是64位 
System.out.println(System.getProperty("sun.arch.data.model")); 
//Java Heap 新建对象 “1”,字符串常量池新建对象“1”
String a = new String("1");
//“1”已经存在字符串常量池,b 获得字符串常量池“1”的引用
String b=a.intern();
//“1”已经存在字符串常量池,c 获得字符串常量池“1”的引用
String c = "1";
//a-Java Heap,b字符串常量池:false
System.out.println(a == b);
//b-字符串常量池,c-字符串常量池:true
System.out.println(b == c);

1.8.0_181
64
false
true

问题二
//JDK版本  jdk1.7及以上
System.out.println(System.getProperty("java.version"));
//判断是32位还是64位 
System.out.println(System.getProperty("sun.arch.data.model")); 

//Java Heap,两个"5",一个“55”,字符串常量池 一个“5”,e表示“55”
String e = new String("5") + new String("5");
//字符串常量池不存在“55”,将“55”的引用保存到字符串常量池
String f=e.intern();
//“55”存在于字符串常量池,虽然是Java Heap 中“55”的引用
String g = "55";
//e-Java Heap 的对象,f-Java Heap对象引用:true
System.out.println(e == f);
//e-Java Heap 的对象,f-Java Heap对象引用:true
System.out.println(e == g);

1.8.0_181
64
true
true

问题三

下面创建了几个对象?

//jdk 1.7及以上
String a=new StringBuilder("你").append("好").append("呀").toString();
String b=a.toString();
System.out.println(b.intern()==b);//b.intern()将字符串地址保存到常量池,TRUE

Java Heap 一个:“你好呀”
字符串常量池三个:“你”、“好”、“呀”

字符串 “Java” 被默认加载到字符串常量池?

在《深入了解 Java 虚拟机》一书中,第2版,第57页,作者提供了一个很邪门的例子

//jdk 1.7 及以上版本
String str1=new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern()==str1);//true

String str2=new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern()==str2);//false

在 jdk1.7 环境,按理说代码结构都是一样的,但是运行结果却是 一个true,一个false
我们用上面的原理分析一下前两行
第一行:字符串常量池保存两个对象 “计算机"和"软件”,Java Heap 保存一个对象"计算机软件"
第二行:intern(),由于字符串常量池没有"计算机软件"这个内容,所以将Java Heap 中对象地址保存到字符串常量池, str1.intern()指向str1在Java Heap 的指向,故 str1.intern()==str1
那么对于第二行,我们只能作出猜测,“java”这个字符串可能是 JVM 保留字,已经被提前保存到字符串常量池了

参考连接

[1]、https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html
[2]、https://blog.csdn.net/si444555666777/article/details/81483737
[3]、https://www.jianshu.com/p/039d6df30fea
[4]、https://q.cnblogs.com/q/111117
[5]、https://jiangzhengjun.iteye.com/blog/577039
[6]、https://blog.csdn.net/kingszelda/article/details/54846069
[7]、https://blog.csdn.net/weixin_40304387/article/details/81071816#_135

猜你喜欢

转载自blog.csdn.net/bestcxx/article/details/88899648