String面试2

本文深入探讨了Java中String类的内存结构,包括24字节的组成,以及在不同JVM参数下的表现。同时,详细解释了String的intern()方法的工作原理,以及s1.intern()和s1 = s1.intern()的区别。文章还通过实例分析了各种字符串创建方式的内存差异,如直接赋值、new操作和拼接。最后,阐述了字符串常量池的实现、作用以及与运行时常量池的关系。
摘要由CSDN通过智能技术生成
1、问题
1.字符串在内存里面到底是怎么样存在的?
2.String的 intern 方法到底干了什么?
3.s1.intern(); 和 s1 = s1.intern();一样吗?
4.String s = “hello”;和String s = new String(“hello”);还有String s = “he”+”llo”;以及String s = new String(“he”)+new String(“llo”);的区别是什么?
5.字符串哪些情况会进入字符串常量池?以及什么时候进入字符串常量池?
6.字符串常量池是什么?在内存里面到底是怎么样存在的?和字符串到底是什么关系?
7.字符串常量池和运行时常量池,常量池是什么关系?它们分别是什么?

我们看几个例子,看看大家对常用的String是不是和我们想的一样。

实例1:

public class Example01 {
    public static void main(String[] args) {
        String s1 = "hello world";
        String s2 = new String("hello world");
        System.out.println(s1.equals(s2));
        System.out.println(s1 == s2);

        String s3 = s2.intern();
        System.out.println(s1 == s3);
    }
}
//true
//false
//true

实例2:

public class Example02 {
    public static void main(String[] args) {
        String s1 = new String("1");
        String s2 = "1";
        s1.intern();
        System.out.println(s1 == s2);

        String s3 = new String("2");
        String s4 = "2";
        s3 = s3.intern();
        System.out.println(s3 == s4);

        String s5 = new String("1") + new String("1");
        String s6 = "11";
        s5.intern();
        System.out.println(s5 == s6);

        String s7 = new String("2") + new String("2");
        s7.intern();
        String s8 = "22";
        System.out.println(s7 == s8);
    }
}
//false
//true
//false
//true

实例3:

public class Example03 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String s1 = new String("aa");
        Field field1 = s1.getClass().getDeclaredField("value");
        field1.setAccessible(true);
        ((char[])field1.get(s1))[1] = 'b';
        String s2 = "aa";
        System.out.println("s1: " + s1);
        System.out.println("s2: " + s2);
        System.out.println(s1 == s2);

        String s3 = "bb";
        Field field3 = s3.getClass().getDeclaredField("value");
        field3.setAccessible(true);
        ((char[])field3.get(s3))[1] = 'c';
        String s4 = "bb";
        System.out.println("s3: " + s3);
        System.out.println("s4: " + s4);
        System.out.println(s3 == s4);

        String s5 = new String("c") + new String("c");
        Field field5 = s5.getClass().getDeclaredField("value");
        field5.setAccessible(true);
        ((char[])field5.get(s5))[1] = 'd';
        String s6 = "cc";
        System.out.println("s5: " + s5);
        System.out.println("s6: " + s6);
        System.out.println(s5 == s6);

        String s7 = "d" + "d";
        Field field7 = s7.getClass().getDeclaredField("value");
        field7.setAccessible(true);
        ((char[])field7.get(s7))[1] = 'e';
        String s8 = "dd";
        System.out.println("s7: " + s7);
        System.out.println("s8: " + s8);
        System.out.println(s7 == s8);

        String s9 = new String("e") + new String("e");
        s9 = s9.intern();
        Field field9 = s9.getClass().getDeclaredField("value");
        field9.setAccessible(true);
        ((char[])field9.get(s9))[1] = 'f';
        String s10 = "ee";
        System.out.println("s9: " + s9);
        System.out.println("s10: " + s10);
        System.out.println(s9 == s10);
    }
}
//s1: ab
//s2: ab
//false
//s3: bc
//s4: bc
//true
//s5: cd
//s6: cc
//false
//s7: de
//s8: de
//true
//s9: ef
//s10: ee
//false
2、以下分析

String类结构(JDK8)

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

    private final char value[];

    private int hash;

	public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
}

String在内存里面的结构

在这里插入图片描述

3、问题回答

1.字符串在内存里面到底是怎么样存在的?

以String类型的对象在堆区申请到一块内存区,一共24个字节,其中mark_word占8个字节,_klass指针4个字节,value4个字节,hash4个字节,coder1个字节,对齐3个字节,value指向存放真正字符的byte[]数组对象。当然这个大小跟hotspot是32位还是64位有关,以及64位情况,是否开启指针压缩有关。
-XX:+UseCompressedOops:针对oop指针压缩,-XX:+UseCompressedClassPointers:针对klass指针压缩
以上24个字节是针对64位默认,也就是开启oop指针压缩和klass指针压缩的情况下。

2.String的 intern 方法到底干了什么?

如果常量池中已经有了这个字符串,那么直接返回常量池中它的引用,如果没有,那就将它的引用保存一份到字符串常量池,然后直接返回这个引用。

3.s1.intern(); 和 s1 = s1.intern();一样吗?

不一样。
s1.intern(); 将s1的引用保存一份到字符串常量池。如果常量池中已经有了这个字符串,相对于什么都没做。
s1 = s1.intern(); 将s1的引用保存一份到字符串常量池,如果常量池中已经有了这个字符串,返回常量池中的引用赋值给s1,那么也就是说s1的引用变了,如果常量池中没有这个字符串,那就将它的引用保存一份到字符串常量池。
区别在于:s1=s1.intern(); s1的引用值可能会发生变化,但是s1.intern(); 对s1的引用值不产生任何改变。

4.String s = “hello”;和String s = new String(“hello”);还有String s = “he”+”llo”;以及String s = new String(“he”)+new String(“llo”);的区别是什么?

(1).String s = “hello”;
s指向堆区的一个String对象,并且该对象引用保存在字符串常量池里面。
(2).String s = new String(“hello”);
s指向堆区的一个String对象,并且该对象引用没有保存在字符串常量池里面。执行s=s.intern(); 后,s的引用值改变,指向字符串常量池存在的引用值。
(3).String s = “he”+”llo”;
和String s = “hello”;一样,在编译阶段就会合并为一个字符串。所以跟(1)是一样的。
(4).String s = new String(“he”)+new String(“llo”);
这个要稍微复杂一点,涉及到方法调用,在JDK9以下,采用StringBuilder.append拼接,返回String,在JDK9及以上,采用StringConcatFactory.makeConcatWithConstants拼接,返回String。并且该对象引用没有保存在字符串常量池里面。执行s=s.intern(); 后,s的引用值改变,指向字符串常量池存在的引用值。

5.字符串哪些情况会进入字符串常量池?以及什么时候进入字符串常量池?

只要是””包裹的字符串,都会进入字符串常量池,以及拼接后调用intern方法的字符串。那可能就人会问了,那String s = new String(“hello”);和String s = new String(“he”)+new String(“llo”);这里为什么没有进入字符串常量池,其实,那是因为String s = new String(“hello”);这里产生了两个对象,一个是”hello”包裹的匿名对象,它的引用地址会进入字符串常量池,另外一个是new出来的对象,也就是s最终指向的引用对象,只不过new出来的对象和”hello”包裹的匿名对象指向了相同的byte[]数组对象。而String s = new String(“he”)+new String(“llo”);涉及到5个对象,一个是”he”包裹的匿名对象,它的引用地址会进入字符串常量池,还有一个是”llo”包裹的匿名对象,它的引用地址会进入字符串常量池,另外就是两个new出来的String对象,new出来的两个String对象拼接之后,会再生成1个新对象返回给s,这3个对象都没有进入字符串常量池,如果调用s.intern(),则新生成的拼接String对象会进入字符串常量池。

如果是””包裹的字符串,在类加载的时候,有一个解析阶段,其实在这个时候就进入了字符串常量池,但是对于CONSTANT_String类型在HotSpot VM中的延迟执行的,真正执行是要到首次执行ldc指令的时候,才会真正的进入字符串常量池。而调用String的 intern 方法,方法执行的时候,就会进入字符串常量池。

6.字符串常量池是什么?在内存里面到底是怎么样存在的?和字符串到底是什么关系?

字符串常量池是一个存储java.lang.String实例引用的HashMap,key就是对应对应字符串数组,value就是java.lang.String实例引用。存在方式见String在内存里面的结构图所示。

7.字符串常量池和运行时常量池,常量池是什么关系?它们分别是什么?

常量池/class文件常量池/静态常量池:存在于class字节码文件中的一段二进制数据,存储在磁盘或网络中,未加载到内存。
运行时常量池:就是class文件中的常量池,在运行的过程中,加载到内存后,在方法区的一块内存数据,对应JVM的SymbolTable数据结构存储,存在于元空间。
字符串常量池:字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能。JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化:为字符串开辟一个字符串常量池,类似于缓存;创建字符串常量时,首先坚持字符串常量池是否存在该字符串;存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中。对应JVM的StringTable数据结构存储,StringTable本体存在元空间,实际引用对象存在于堆区。
整型常量池/包装类型常量:此种常量池并非JVM层面,而是Java层面的封装,比如IntegerCache(Integer的内部类),实现了Integer的常量池,其它还有Byte, Short, Integer, Long, Character,Boolean。
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
节省运行时间:比较字符串时,比equals()快。对于两个引用变量,只用判断引用是否相等,也就可以判断实际值是否相等。
ConstantPoolCache主要用于存储某些字节码指令所需的解析(resolve)好的常量项,例如给[get|put]static、[get|put]field、invoke[static|special|virtual|interface|dynamic]等指令对应的常量池项用。
每个InstanceKlass关联着一个ConstantPool,作为该类型的运行时常量池。这个常量池的结构跟Class文件里的常量池是一一对应的。Constant Pool 是一个全局公共数组,每加载一个类,这个类元数据的指针就会加入这个数组,指针指向的实际的数据,例如字面量,符号常量等等,都会放入全局公共 Symbol Table 以及 全局公共 StringTable。
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值