String创建内存的分配以及方法区常量池之间的关系

最近在查重温vm内存的结构的过程中,发现了有些地方不曾知道的东西,就是常量池随着jdk版本由1.6到1.7再到1.8内存结构也随之发生了一些改变,这里我单独领出来string作为字符常量池的代表,将这个改变讲下

先说下基本的知识点:
String在正常的内存分配:
在JDK6及之前版本中,String Pool里放的都是字符串常量;在JDK7.0中,由于String.intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。

/**
 * 运行结果为true false
 */
String s1 = "AB";
String s2 = "AB";
String s3 = new String("AB");
String s4 = new String("AB");

由于常量池中不存在两个相同的对象,所以s1和s2都是指向JVM字符串常量池中的"AB"对象。
在这里插入图片描述
当执行String s1 = "AB"时,JVM首先会去字符串常量池中检查是否存在"AB"对象,如果不存在,则在字符串常量池中创建"AB"对象,并将"AB"对象的地址返回给s1;如果存在,则不创建任何对象,直接将字符串常量池中"AB"对象的地址返回给s1。所以s2直接拿到常量池中"AB"对象的地址。

采用new关键字新建一个字符串对象时,String s3 = new String(“AB”);,JVM首先在字符串池中查找有没有"AB"这个字符串对象,如果有,则不在池中再去创建"AB"这个对象了,直接在堆中创建一个"xyz"字符串对象,然后将堆中的这个"AB"对象的地址返回赋给引用s3,这样,s3就指向了堆中创建的这个"AB"字符串对象;如果没有,则首先在字符串池中创建一个"AB"字符串对象,然后再在堆中创建一个"AB"字符串对象,然后将堆中这个"AB"字符串对象的地址返回赋给s3引用,这样,s3指向了堆中创建的这个"AB"字符串对象。s4则指向了堆中创建的另一个"xyz"字符串对象。s3 、s4是两个指向不同对象的引用,结果当然是false。

注意:常量在编译期已经确定存储在常量池中了

字符串常量拼接:

        String str3 = "Hello"+" word";
		String str4 = "Hello word";
		System.out.println(str3 == str4); //true

上面是字符串常量拼接的例子:在编译时,JVM编译器对字符串做了优化,str3就被优化成“Hello word”,str3和str4指向字符串常量池同一个字符串常量,所以==比较为true

字符串常量+字符串变量、字符串变量之间的拼接

        String str5 = "Hello";
		String str6 = " word";
		String str7 = "Hello word";
		String str8 = str5+" word";
		System.out.println(str7 == str8);  //false

String通过+号来拼接字符串的时候,如果有字符串变量参与,实际上底层会转成通过StringBuilder的append( )方法来实现,大致过程如下

        StringBuilder sb = new StringBuilder( );
		sb.append(str5);
		sb.append(" word");
		str8 = sb.toString();

StringBuilder 的 toString( )方法底层new了一个String对象,所以str8在堆内存中重新开辟了新空间,而str7指向常量池,所以str7 == str8为false。
变量字符串拼接和常量字符串拼接结果是不一样的。因为变量字符串拼接是先开辟空间,然后再拼接。

知识点1:常量池的分类

class文件常量池
在Class文件中除了有类的版本【高版本可以加载低版本】、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table)【此时没有加载进内存,也就是在文件中】,用于存放编译期生成的各种字面量和符号引用。

下面对字面量和符号引用进行说明
字面量
字面量类似与我们平常说的常量,主要包括:
1、文本字符串:就是我们在代码中能够看到的字符串,例如String a = “aa”。其中”aa”就是字面量。
2、被final修饰的变量。
3、符号引用
主要包括以下常量:
类和接口和全限定名:例如对于String这个类,它的全限定名就是java/lang/String。
4、字段的名称和描述符:所谓字段就是类或者接口中声明的变量,包括类级别变量(static)和实例级的变量。
方法的名称和描述符。所谓描述符就相当于方法的参数类型+返回值类型。
2.2 运行时常量池
我们知道类加载器会加载对应的Class文件,而上面的class文件中的常量池,会在类加载后进入方法区中的运行时常量池【此时存在在内存中】。并且需要的注意的是,运行时常量池是全局共享的,多个类共用一个运行时常量池。并且class文件中常量池多个相同的字符串在运行时常量池只会存在一份。
注意运行时常量池存在于方法区中。

2.3 字符串常量池
  看名字我们就可以知道字符串常量池会用来存放字符串,也就是说常量池中的文本字符串会在类加载时进入字符串常量池。
那字符串常量池和运行时常量池是什么关系呢?上面我们说常量池中的字面量会在类加载后进入运行时常量池,其中字面量中有包括文本字符串,显然从这段文字我们可以知道字符串常量池存在于运行时常量池中。也就存在于方法区中。
不过在周志明那本深入java虚拟机中有说到,到了JDK1.7时,字符串常量池就被移出了方法区,转移到了堆里了。
那么我们可以推断,到了JDK1.7以及之后的版本中,运行时常量池并没有包含字符串常量池,运行时常量池存在于方法区中,而字符串常量池存在于堆中。

知识点2:intern()函数

intern():书上是这样描述它的作用的,如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符添加到常量池中,并返回此String对象的引用。

事实上,intern函数的作用在1.6以前 和 1.7以后是有不同的处理;

先看1.6:
  在这里插入图片描述
先解释下,返回指向该常量的引用其实就是指的是堆内存地址中保存的一个地址,既常量池的对象地址
看个详细点的例子:

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

首先,见到"haha",常量池中没有这个常量,所以会在常量池中放下这个常量对象,"haha"被添加到字符串常量池,然后在stringTable中添加该常量的引用(a.intern()引用就是常量池对象地址)。而a这个引用指向的是堆中这个String对象的地址,所以肯定是不同的。(而且一个在堆,一个在方法区中)。

在1.6中,所以需要谨慎使用intern方法,避免常量池中字符串过多,导致性能变慢,甚至发生PermGen内存溢出。

在1.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.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。用地址上来说,因为“计算机软件”这个字符串常量,是没有出现过在常量池中的,(append方法不会再常量池生成对象,只有new String会)所以调用intern()方法的时候,会在常量池中生成一个"计算机软件"的常量,并且返回常量池该对象的引用,这个和原地址堆内存引用是不一致的
而在jdk1.7中的结果是true和false。第一个输出中,因为“计算机软件”这个字符串常量,是没有出现过在常量池中的,所以调用intern()方法的时候,会在常量池中生成一个"计算机软件"的引用,注意是引用哦!
而str1所指向的也是这个堆对象的引用,所以第一个是true。

而第二个,首先查资料发现,由于JVM的 特殊性在JVM启动的时候调用了一些方法,在常量池中已经生成了“java”字符串常量。

所以,str2指向的是堆中的String对象,内容是"java",而这个str2调用intern的时候,常量池中会发现已经有了这个常量对象,所以无论是jdk1.6还是jdk1.7都会返回这个已经存在了的"java"常量对象池的引用,那肯定呵str2引用指向的堆地址是不同的,所以false。

再看一个例子:

String str2 = new String("str")+new String("kkk");
str2.intern();
String str1 = "strkkk";
System.out.println(str2==str1);//true

这个返回true的原因也一样,str2的时候,只有一个堆的String对象,然后调用intern,常量池中没有“strkkk”这个常量对象,于是常量池中生成了一个对这个堆中string对象的引用。

然后给str1赋值的时候,因为是带引号的,所以去常量池中找,发现有这个常量对象,就返回这个常量对象的引用,也就是str2引用所指向的堆中的String对象的地址。

所以str2和str1指向的是同一个东西,所以为true。

总结:直接使用双引号声明出来的String对象会直接存储在字符串常量池中,如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法是一个native方法,intern方法会从字符串常量池中查询当前字符串是否存在,如果存在,就直接返回当前字符串;如果不存在就会将当前字符串放入常量池中,之后再返回。

JDK1.7的改动:

将String常量池 从 Perm 区移动到了 Java Heap区
String.intern() 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中,String对象是不可变的,这意味着一旦创建,它的值就不能被修改。当我们进行字符串拼接时,会创建一个新的String对象来存储拼接后的结果。在拼接过程中,如果所有的操作数都是常量,那么编译器会在编译期间对其进行优化,将结果存储在常量池中。例如,对于字符串"hello"和"world"的拼接,编译器会将其优化为"helloworld",并直接在常量池中引用该字符串。这样,当我们使用相同的字符串字面量进行拼接时,会直接引用常量池中的字符串对象,因此它们的引用会相等。\[1\] 然而,如果拼接过程中存在变量,那么结果会在堆中创建一个新的String对象。例如,对于字符串str1和"world"的拼接,由于str1是一个变量,编译器无法确定拼接结果,因此会在堆中创建一个新的String对象。因此,str2和str3的引用不相等。\[1\] 另外,当我们使用new关键字创建String对象时,会在堆中创建一个新的String对象,即使其值与常量池中的字符串相同。因此,对于字符串str1和str2的比较,虽然它们的值相等,但它们的引用不相等。\[2\] 需要注意的是,使用final修饰的String字段进行拼接时,编译器会将其视为常量,并在编译期间对其进行优化。因此,对于final字段的拼接,结果会存储在常量池中,并且相同的拼接操作会引用相同的字符串对象。\[3\] 总结起来,字符串的动态内存分配取决于拼接操作中是否存在变量以及是否使用了final修饰符。如果所有操作数都是常量,结果会存储在常量池中;如果存在变量,结果会在堆中创建一个新的String对象。 #### 引用[.reference_title] - *1* [String对象内存分配分析](https://blog.csdn.net/weixin_65349299/article/details/124414009)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [String变量内存分配](https://blog.csdn.net/hc1428090104/article/details/99618025)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值