解决困扰已久的常量池问题&以及String的内存存放问题?

1 常量池-Class 常量池

class文件格式采用类似于c语言结构体的伪结构来存储数据,伪结构只有两种数据类型:无符号和表。
无符号数属于基本的数据类型:u1,u2,u4,u8代表1,2,4,8个字节的无符号数,可以用来描述数字、索引引用,数量值或者按照UTF-8编码构成字符串值。表是由多个无符号数或者其他表作为数据构成的复合数据类型,习惯性以“_info”结尾,整个class文件本质就是一张表。无论是无符号数还是表,当需要描述同一类型数量不定的多个数据时,经常会使用一个前置的容器计数器加若干连续数据项的形式,表示连续的某一类型的数据的集合。

魔数与class文件版本:1.每个class文件的头4个字节称为魔数(Magic Number),作用是确定这个文件是否为一个能被虚拟机接受的class文件。class文件的魔数值为:0xCAFEBABE。2.紧接着魔数的4个字节存储的是class的版本号:5、6字节是次版本号,7、8字节是主版本号

在版本号之后是常量池入口,常量池是class文件结构中与其他项目关联最多的数据类型,常量池入口需要放置一项u2类型的数据,代表常量池容量计数值。常量池是当Class文件被Java虚拟机加载进来后存放在方法区:各种字面量 和 符号引用。
在这里插入图片描述

字段和方法描述符:是用来描述字段的数据类型、方法的参数列表和返回值。根据描述符规则,基本数据类型和无返回值的void的类型用一个大写字母表示,对象则用字符L加对象全限定名来表示。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

2 运行时常量池

运行时常量池是方法区的一部分。运行时常量池是当Class文件被加载到内存后,Java虚拟机会 将Class文件常量池里的内容转移到运行时常量池里(运行时常量池也是每个类都有一个)。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。

方法区的Class文件信息,Class常量池和运行时常量池的三者关系:
在这里插入图片描述

3 字符串常量池

字符串常量池又称为:字符串池,全局字符串池,英文也叫String Pool。 在工作中,String类是我们使用频率非常高的一种对象类型。JVM为了提升性能和减少内存开销,避免字符串的重复创建,其维护了一块特殊的内存空间,这就是我们今天要讨论的核心:字符串常量池。字符串常量池由String类私有的维护。

在JDK7之前字符串常量池是在永久代里边的,但是在JDK7之后,把字符串常量池分进了堆里边。
在堆中的字符串常量池: 堆里边的字符串常量池存放的是字符串的引用或者字符串(两者都有)。
在这里插入图片描述
在Java中有两种创建字符串对象的方式:1、采用字面值的方式赋值;2、采用new关键字新建一个字符串对象。这两种方式在性能和内存占用方面存在着差别。

public class a {
    public static void main(String[] args) {
        String str1="aaa";
        String str2="aaa";
        System.out.println(str1==str2);   
    }
}
运行结果:
true

采用字面值的方式创建一个字符串时,JVM首先会去字符串池中查找是否存在"aaa"这个对象,如果不存在,则在字符串池中创建"aaa"这个对象,然后将池中"aaa"这个对象的引用地址返回给字符串常量str,这样str会指向池中"aaa"这个字符串对象;如果存在,则不创建任何对象,直接将池中"aaa"这个对象的地址返回,赋给字符串常量。

对于上述的例子:这是因为,创建字符串对象str2时,字符串池中已经存在"aaa"这个对象,直接把对象"aaa"的引用地址返回给str2,这样str2指向了池中"aaa"这个对象,也就是说str1和str2指向了同一个对象,因此语句System.out.println(str1== str2)输出:true

public class a {
    public static void main(String[] args) {
        String str1=new String("aaa");
        String str2=new String("aaa");
        System.out.println(str1==str2);
    }
}
运行结果:
false

采用new关键字新建一个字符串对象时,JVM首先在字符串常量池中查找有没有"aaa"这个字符串对象,如果有,则不在池中再去创建"aaa"这个对象了,直接在堆中创建一个"aaa"字符串对象,然后将堆中的这个"aaa"对象的地址返回赋给引用str1,这样,str1就指向了堆中创建的这个"aaa"字符串对象;如果没有,则首先在字符串常量池池中创建一个"aaa"字符串对象,然后再在堆中创建一个"aaa"字符串对象,然后将堆中这个"aaa"字符串对象的地址返回赋给str1引用,这样,str1指向了堆中创建的这个"aaa"字符串对象。

对于上述的例子:
因为,采用new关键字创建对象时,每次new出来的都是一个新的对象,也即是说引用str1和str2指向的是两个不同的对象,因此语句
System.out.println(str1 == str2)输出:false

字符串池的优点就是避免了相同内容的字符串的创建,节省了内存,省去了创建相同字符串的时间,同时提升了性能;另一方面,字符串池的缺点就是牺牲了JVM在常量池中遍历对象所需要的时间,不过其时间成本相比而言比较低。

4 字符串常量池和运行时常量池之间的关系

在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代。
在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说 字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代。
在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之,,这时候字符串常量池还在堆,运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间(Metaspace)。

5 String.intern在JDK6和JDK7之后的区别(重点)

JDK6和JDK7中该方法的功能是一致的,不同的是常量池位置的改变(JDK7将常量池放在了堆空间中)。intern的方法返回字符串对象的规范表示形式。其中它做的事情是:首先去判断该字符串是否在常量池中存在,如果存在返回常量池中的字符串,如果在字符串常量池中不存在,先在字符串常量池中添加该字符串,然后返回引用地址。

String s1 = new String("1");
s1.intern();
String s2 = "1";
System.out.println(s1 == s2);

运行结果:
JDK6运行结果:false
JDK7运行结果:false

上边例子中s1是new出来对象存放的位置的引用,s2是存放在字符串常量池的字符串的引用,所以两者不同。

String s1 = new String("1");
System.out.println(s1.intern() == s1);

运行结果:
JDK6运行结果:false
JDK7运行结果:false

上边例子中s1是new出来对象存放的位置的引用,s1.intern()返回的是字符串常量池里字符串的引用。

String s1 = new String("1") + new String("1");
s1.intern();
String s2 = "11";
System.out.println(s1 == s2);

运行结果:
JDK6运行结果:false
JDK7运行结果:true

JDK6中,s1.intern()运行时,首先去常量池查找,发现没有该常量,则在常量池中开辟空间存储"11",返回常量池中的值(注意这里也没有使用该返回值),第三行中,s2直接指向常量池里边的字符串,所以s1和s2不相等。有可能会有小伙伴问为啥s1.intern()发现没有该常量呢,那是因为:

String s1 = new String(“1”) + new String(“1”);这行代码实际操作是,创建了一个StringBuilder对象,然后一路append,最后toString,而toString其实是又重新new了一个String对象,然后把对象给s1,此时并没有在字符串常量池中添加常量。

JDK7中,由于字符串常量池在堆空间中,所以在s1.intern()运行时,发现字符串常量池没有常量,则添加堆中“11”对象的引用到字符串常量池,这个引用返回堆空间“11”地址(注意这里也没有使用该返回值),这时s2通过查找字符串常量池中的常量,查到的是s1.intern()存在字符串常量池里的“11”对象的引用,既然都是指向堆上的“11”对象,所以s1和s2相等。

6 字符串常量池里存放的是引用还是字面量

我在上面的代码中讲了在JDK7中字符串常量池在堆上,仔细看看上例啥时候会放引用。那么啥时候会放字面量在字符串常量池呢,那就是在我们new一个String对象的时候,如果字符串常量池里边有字面量那么就不会放,如果字符串常量池没有就会放字面量。看一个例子:

public class a {
    public static void main(String[] args) {
        String str1= new String("123");
        String str2=new String("123");
        System.out.println(str1==str2); // false
        System.out.println(str1.intern()==str2.intern()); // true
    }
}

首先 String str1= new String(“123”);会在堆中创建一个对象,返回这个对象的引用给str1,同时它还会在字符串常量池中检查有没有有没有"123"这个对象,如果没有就再创建一个对象(也就是123这个字面量)在字符串常量池中。注意这里是创建了两个对象。但是当我们字符串常量池里边有123这个对象,那么就不用继续创建了。
上面例子的false那是因为堆中的123对象不是同一个对象,但是第二个str1.intern和s2.intern指的都是字符串常量池里的123对象所以是true。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值