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

前言

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

须知

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

字符串常量池的前世今生
JVM 规范之方法区

JVM规范规定 方法区包含 类常量、运行时常量池、而运行时常量池中比较特殊的是字符串常量池。

HotSpot JVM 中字符串常量池 是 运行时常量池中特殊的存在

每一个类都有一个运行时常量池,而字符串类型为了实现存储效率最大化,字符串常量池是全局通用的

HotSpot JVM 常量池存储位置的变化
Jdk版本类常量运行时常量池字符串常量池垃圾回收
1.6及之前非堆(No-heap)-永久代非堆(No-heap)-永久代非堆(No-heap)-永久代Full Gc 回收永久代
1.7非堆(No-heap)-永久代heap(堆)heap(堆)Full Gc 回收永久代
1.8元数据(宿主机物理内存)heap(堆)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 保留字,已经被提前保存到字符串常量池了

基本类型的缓存问题

对于基本类型即使使用 装箱类型,当值在 -128到127之间时依旧可能是同一个对象,因为这个时候JVM存在一个缓存问题
看下面到实验,这意味着,使用 == 判断数值类型到值总是存在一些风险的,
看下面的实验

/**
	 * 基本装箱类型 -128至127之间的对象指向同一个内存地址
	 * @param args
	 */
	public static void main(String[] args) {
		int i=new Integer(100);
		int j=new Integer(100);
		System.out.println(i==j);//true
		Integer m=new Integer(200);
		Integer n=new Integer(200);
		System.out.println(m==n);//false
		System.out.println(m.intValue()==n.intValue());//true
	}
装箱类型Integer,Boolean,Short,Byte,Short,Long,Float,Double的Cache问题

装箱类型Integer,Boolean,Short,Byte,Short,Long,Float,Double的Cache问题
装箱类型通过类型的valueOf装箱,具体装箱方法可以通过查看类型的valueOf源码。较低版本JDK没有Cache。
Interger:Cache中含有-128-127,可以通过修改JVM参数-Djava.lang.IntegerCache.high=200来间接设置IntegerCache.high值。也可以通过设置参数-XX:AutoBoxCacheMax来达到目的。
Boolean:true与false都是存在Cache内,和自己new Boolean不是在一个空间
Byte的256个值全部在Cache种,和自己new Byte不是在一个空间
Short,Long两种类型的cache范围为-128-127,无法调整最大尺寸。
Float,Double没有Cache

参考连接

[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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值