java开发面试简历,5道String高频面试题,Java日常开发的12个坑,你踩过几个

接下来,我们来细说一下字符串常量池的结构,其实在Hotspot JVM中,字符串常量池StringTable的本质是一张HashTable,那么当我们说将一个字符串放入字符串常量池的时候,实际上放进去的是什么呢?

以字面量的方式创建String对象为例,字符串常量池以及堆栈的结构如下图所示(忽略了jvm中的各种OopDesc实例):

实际上字符串常量池HashTable采用的是数组加**链表的结构,链表中的节点是一个个的HashTableEntry,而HashTableEntry中的value则存储了堆上String对象的引用**。

那么,下一个问题来了,这个字符串对象的引用是什么时候被放到字符串常量池中的?具体可为两种情况:

  • 使用字面量声明String对象时,也就是被双引号包围的字符串,在堆上创建对象,并驻留到字符串常量池中(注意这个用词)

  • 调用intern()方法,当字符串常量池没有相等的字符串时,会保存该字符串的引用

注意!我们在上面用到了一个词驻留,这里对它进行一下规范。当我们说驻留一个字符串到字符串常量池时,指的是创建HashTableEntry,再使它的value指向堆上的String实例,并把HashTableEntry放入字符串常量池,而不是直接把String对象放入字符串常量池中。简单来说,可以理解为将String对象的引用保存在字符串常量池中。

我们把intern()方法放在后面细说,先主要看第一种情况,这里直接整理引用R大的结论:

在类加载阶段,JVM会在堆中创建对应这些class文件常量池中的字符串对象实例,并在字符串常量池中驻留其引用。

这一过程具体是在resolve阶段(个人理解就是resolution解析阶段)执行,但是并不是立即就创建对象并驻留了引用,因为在JVM规范里指明了resolve阶段可以是lazy的。CONSTANT_String会在第一次引用该项的ldc指令被第一次执行到的时候才会resolve。

就HotSpot VM的实现来说,加载类时字符串字面量会进入到运行时常量池,不会进入全局的字符串常量池,即在StringTable中并没有相应的引用,在堆中也没有对应的对象产生。

这里大家可以暂时先记住这个结论,在后面还会用到。

在弄清楚上面几个概念后,我们再回过头来,先看看用字面量声明String的方式,代码如下:

1

2

3

public static void main(String[] args) {

    String s = "Hydra";

}

反编译生成的字节码文件:

1

2

3

4

5

6

7

8

public static void main(java.lang.String[]);

  descriptor: ([Ljava/lang/String;)V

  flags: ACC_PUBLIC, ACC_STATIC

  Code:

    stack=1, locals=2, args_size=1

       0: ldc           #2                  // String Hydra

       2: astore_1

       3: return

解释一下上面的字节码指令:

  • 0: ldc,查找后面索引为#2对应的项,#2表示常量在常量池中的位置。在这个过程中,会触发前面提到的lazy resolve,在resolve过程如果发现StringTable已经有了内容匹配的String引用,则直接返回这个引用,反之如果StringTable里没有内容匹配的String对象的引用,则会在堆里创建一个对应内容的String对象,然后在StringTable驻留这个对象引用,并返回这个引用,之后再压入操作数栈中

  • 2: astore_1,弹出栈顶元素,并将栈顶引用类型值保存到局部变量1中,也就是保存到变量s

  • 3: return,执行void函数返回

可以看到,在这种模式下,只有堆中创建了一个"Hydra"对象,在字符串常量池中驻留了它的引用。并且,如果再给字符串s2s3也用字面量的形式赋值为"Hydra",它们用的都是堆中的唯一这一个对象。

好了,再看一下以构造方法的形式创建字符串的方式:

1

2

3

public static void main(String[] args) {

    String s = new String("Hydra");

}

同样反编译这段代码的字节码文件:

1

2

3

4

5

6

7

8

9

10

11

public static void main(java.lang.String[]);

  descriptor: ([Ljava/lang/String;)V

  flags: ACC_PUBLIC, ACC_STATIC

  Code:

    stack=3, locals=2, args_size=1

       0: new           #2                  // class java/lang/String

       3: dup

       4: ldc           #3                  // String Hydra

       6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V

       9: astore_1

      10: return

看一下和之前不同的字节码指令部分:

  • 0: new,在堆上创建一个String对象,并将它的引用压入操作数栈,注意这时的对象还只是一个空壳,并没有调用类的构造方法进行初始化

  • 3: dup,复制栈顶元素,也就是复制了上面的对象引用,并将复制后的对象引用压入栈顶。这里之所以要进行复制,是因为之后要执行的构造方***从操作数栈弹出需要的参数和这个对象引用本身(这个引用起到的作用就是构造方法中的this指针),如果不进行复制,在弹出后会无法得到初始化后的对象引用

  • 4: ldc,在堆上创建字符串对象,驻留到字符串常量池,并将字符串的引用压入操作数栈

  • 6: invokespecial,执行String的构造方法,这一步执行完成后得到一个完整对象

到这里,我们可以看到一共创建了两个String对象,并且两个都是在堆上创建的,且字面量方式创建的String对象的引用被驻留到了字符串常量池中。而栈里的s只是一个变量,并不是实际意义上的对象,我们不把它包括在内。

其实想要验证这个结论也很简单,可以使用idea中强大的debug功能来直观的对比一下对象数量的变化,先看字面量创建String方式:

这个对象数量的计数器是在debug时,点击下方右侧MemoryLoad classes弹出的。对比语句执行前后可以看到,只创建了一个String对象,以及一个char数组对象,也就是String对象中的value

再看看构造方法创建String的方式:

可以看到,创建了两个String对象,一个char数组对象,也说明了两个String中的value指向了同一个char数组对象,符合我们上面从字节码指令角度解释的结果。

最后再看一下下面的这种情况,当字符串常量池已经驻留过某个字符串引用,再使用构造方法创建String时,创建了几个对象?

1

2

3

4

public static void main(String[] args) {

    String s = "Hydra";

    String s2 = new String("Hydra");

}

答案是只创建一个对象,对于这种重复字面量的字符串,看一下反编译后的字节码指令:

1

2

3

4

5

6

7

8

9

10

Code:

  stack=3, locals=3, args_size=1

     0: ldc           #2                  // String Hydra

     2: astore_1

     3: new           #3                  // class java/lang/String

     6: dup

     7: ldc           #2                  // String Hydra

     9: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V

    12: astore_2

    13: return

可以看到两次执行ldc指令时后面索引相同,而ldc判断是否需要创建新的String实例的依据是根据在第一次执行这条指令时,StringTable是否已经保存了一个对应内容的String实例的引用。所以在第一次执行ldc时会创建String实例,而在第二次ldc就会直接返回而不需要再创建实例了。

第4题,烧脑的 intern


上面我们在研究字符串对象的引用如何驻留到字符串常量池中时,还留下了调用intern方法的方式,下面我们来具体分析。

从字面上理解intern这个单词,作为动词时它有禁闭关押的意思,通过前面的介绍,与其说是将字符串关押到字符串常量池StringTable中,可能将它理解为缓存它的引用会更加贴切。

String的intern()是一个本地方法,可以强制将String驻留进入字符串常量池,可以分为两种情况:

  • 如果字符串常量池中已经驻留了一个等于此String对象内容的字符串引用,则返回此字符串在常量池中的引用

  • 否则,在常量池中创建一个引用指向这个String对象,然后返回常量池中的这个引用

好了,我们下面看一下这段代码,它的运行结果应该是什么?

1

2

3

4

5

6

7

public static void main(String[] args) {

    String s1 = new String("Hydra");

    String s2 = s1.intern();

    System.out.println(s1 == s2);

    System.out.println(s1 == "Hydra");

    System.out.println(s2 == "Hydra");

}

输出打印:

1

2

3

false

false

true

用一张图来描述它们的关系,就很容易明白了:

其实有了第三题的基础,了解这个结构已经很简单了:

  • 在创建s1的时候,其实堆里已经创建了两个字符串对象StringObject1StringObject2,并且在字符串常量池中驻留了StringObject2

  • 当执行s1.intern()方法时,字符串常量池中已经存在内容等于"Hydra"的字符串StringObject2,直接返回这个引用并赋值给s2

  • s1s2指向的是两个不同的String对象,因此返回 fasle

  • s2指向的就是驻留在字符串常量池的StringObject2,因此s2=="Hydra"为 true,而s1指向的不是常量池中的对象引用所以返回false

上面是常量池中已存在内容相等的字符串驻留的情况,下面再看看常量池中不存在的情况,看下面的例子:

1

2

3

4

5

6

public static void main(String[] args) {

    String s1 = new String("Hy") + new String("dra");

    s1.intern();

    String s2 = "Hydra";

    System.out.println(s1 == s2);

}

执行结果:

1

true

简单分析一下这个过程,第一步会在堆上创建"Hy""dra"的字符串对象,并驻留到字符串常量池中。

接下来,完成字符串的拼接操作,前面我们说过,实际上jvm会把拼接优化成StringBuilderappend方法,并最终调用toString方法返回一个String对象。在完成字符串的拼接后,字符串常量池中并没有驻留一个内容等于"Hydra"的字符串。

所以,执行s1.intern()时,会在字符串常量池创建一个引用,指向前面StringBuilder创建的那个字符串,也就是变量s1所指向的字符串对象。在《深入理解Java虚拟机》这本书中,作者对这进行了解释,因为从jdk7开始,字符串常量池就已经移到了堆中,那么这里就只需要在字符串常量池中记录一下首次出现的实例引用即可。

最后,当执行String s2 = "Hydra"时,发现字符串常量池中已经驻留这个字符串,直接返回对象的引用,因此s1s2指向的是相同的对象。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

最后

由于细节内容实在太多了,为了不影响文章的观赏性,只截出了一部分知识点大致的介绍一下,每个小节点里面都有更细化的内容!

小编准备了一份Java进阶学习路线图(Xmind)以及来年金三银四必备的一份《Java面试必备指南》

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!**](https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0)

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值