> 文档中心 > [JavaSE] 认识String类(JVM源码展示、StringTable、字符串常量池、哈希表内存图解)奉劝那些想深入理解String的各位家人,呕心沥血,袒露心声,掏心掏肺的内存图一定要看看

[JavaSE] 认识String类(JVM源码展示、StringTable、字符串常量池、哈希表内存图解)奉劝那些想深入理解String的各位家人,呕心沥血,袒露心声,掏心掏肺的内存图一定要看看

目录

往期回顾,专栏一览

🔴 认识 String 类

🔵 引入思考

🔵 创建字符串

💠 经典错误

🔵 字符串比较相等

💠 扩展

💠 什么是池?

💠 什么是哈希表?

🔵 内存布局分析

💠 JVM源码分析

💠 例1

💠 例2

💠 例3

💠 例4

💠 例5

💠 例6

💠 例7

🔵 测试代码一览

往期回顾,专栏一览

🍉 JavaSE 🍋 AWT 🍑 数据结构
🍅 C1进阶之路 🍒 每日一练 🌽 代码报错
🍈 活动

🍹欢迎各路大佬来到 Nick 主页指点

☀️本期文章将学习 [JavaSE] 认识String类(JVM源码展示、StringTable、字符串常量池、哈希表内存图解),我是博主Nick。✨

✨我的博客主页:Nick_Bears 🌹꧔ꦿ

🌹꧔ꦿ博文内容如对您有所帮助,还请给个点赞 + 关注 + 收藏✨

   

🔴 认识 String 类

🔵 引入思考

🔶 字符串:在C语言里面 是没有字符串这种数据类型的!在 C++/Java 中有这种数据类型,叫 String

  

  • 什么叫字符串? 使用双引号,“abcdef” “a”。

  • 什么叫字符? ‘a’ 孙‘, ‘abc’ 这是错误的。

  

🔶 注意问题:在 Java 中没有所谓的字符串以 \0 一说。

  

🔶 查看API文档简单认识 String

       

🔶 查看源码认识 String

      

🔶 我们看看发现了什么?

  • value[] 是什么鬼?查阅资料后发现原来是char[]。

  • 数组也没开辟空间...空间去哪儿了?(后面讨论)。

  • hash 是啥?不懂!反正我知道默认是0。

  

🔵 创建字符串

🔷 一共有三种方式

public class Demo {    public static void main(String[] args) { //方式1 String str = "Nick很宠粉的"; //方式二(调用构造方法进行构造对象) String str2 = new String("Nick很宠粉的"); //方式三 char[] chars = {'N','i','c','k','很','宠','粉','的'}; String str3 = new String(chars); System.out.println(str); System.out.println(str2); System.out.println(str3);    }}//运行结果Nick很宠粉的Nick很宠粉的Nick很宠粉的

      

🔻 注意事项:

  • "Nick" 这样的字符串字面值常量, 类型也是 String

  • String 也是引用类型。String str = "Nick很宠粉的"; 这样的代码内存布局如下

       

 String str1 = "Nick"; String str2 = str1;

      

思考:既然 str1 str2 都指向 ”Nick“,是否可以做到 str1 修改”Nick“->"Nick很宠粉的"

答:做不到!!

public static void main(String[] args) { String str1 = "Nick"; String str2 = str1; System.out.println(str1); System.out.println(str2); str1 = "Nick很宠粉的"; //这里是修改指向,不是字面值 System.out.println("================"); System.out.println(str1); System.out.println(str2);    }

     

💠 经典错误

    public static void main(String[] args) { String str = "abc"; char[] arr = {'n','i','c','k'}; func(str,arr); System.out.println(str); System.out.println(Arrays.toString(arr));    }    public static void func(String s,char[] arr){ s ="Nick"; arr[0] = 'N';    }//结果abc[N, i, c, k]

    

答:不是说传引用就能改变实参的值,你要看这个引用干啥了,虽然我们传的都是引用,但是 str 传过去只是修改了引用的指向;

草图(大概是对的,后面究细节)

     

🔵 字符串比较相等

🔺 须知:等号比较的是地址

  

💠 扩展

1、概念

  • Class 文件常量池:int a = 10;

  • 运行时常量池: 当程序把编译好的字节码文件,加载到 JVM 中后,会生成一个运行时常量池[方法区],实际上是 Class 文件常量池。

  • 字符串常量池:主要存放字符串常量-> 字符串常量池,本质上是一个哈希表。StringTable,双引号 引起来的字符串常量才会放进去。JDK1.8开始,放在了堆里面。

       

💠 什么是池?

  1. 数据库连接池

  2. 线程池

  3. .......

意义:提高效率。

  

🍋 例子1:现实生活中有一种女神, 称为 "绿茶", 在和高富帅谈着对象的同时, 还可能和别的屌丝搞暧昧. 这时候这个屌丝被 称为 "备胎"。那么为啥要有备胎? 因为一旦和高富帅分手了, 就可以立刻找备胎接盘, 这样 效率比较高。如果这个女神, 同时在和很多个屌丝搞暧昧, 那么这些备胎就称为 备胎池。

     

🍋 例子2:你和女朋友嗑瓜子,要是没有池,你就要用嘴把瓜子嗑开才能喂女朋友吃,但是你提早把一包瓜子全部嗑好了放在了盘子里,你的池就建好了,你女朋友直接拿盘子里的瓜子即可,提高了嗑瓜子效率

    

💠 什么是哈希表?

  

🔶 描述和组织数据的一种方式

  

例子:假如我有12 45 2 7 15 92

问题:如何去查找?

  • 顺序查找 O(n)

  • 排序+二分查找

  

💠 哈希表怎么做的?

  

🔶 存储数据的时候,会根据一个映射关系进行存储,如何映射?需要设计一个函数(哈希函数)

   

假如自己设计的函数是:key % len

      

     

💬 那么,当哈希表无限大的时候,还不是会达到O(n)么?

  

🔸 一般性我们认为哈希表是一个常量,不会无限大的,速度存储很快,甚至快到O(1),接下来跟着Nick来分析一下内存布局吧!

  

🔵 内存布局分析

💠 JVM源码分析

  

🔶 那么StringTable 在哪里?我们就要去看底层 JVM 的源码了

    

🔶 首先我们看到我们在放一个东西时候就会new 一个 StringTable(),它就是一个字符串的哈希表。 

     

🔶 首先把你要放的东西hash一下,得到hashValue得到一个位置,然后在look-up下面去找,如果是空则new_entry,不为空则返回。

     

🔶 这是look-up函数,数组下面是链表,开始遍历,如果哈希相同了就返回value值,没有就是null;  

     

💠 例1

    public static void main(String[] args) { String str1 = "Nick"; String str2 = new String("Nick"); System.out.println(str1==str2);    }//结果false

     

      

🔷 分析:我们的“Nick”在底层是如何存储的?

  • 引入思考 的图知道,“Nick”对象有 两个引用 ,一个 value ,一个 hash , value 是一个数组,因此会以数组的方式存储,数组的地址等于 val 的地址,val 与 hash 合为一个字符串对象

  • 接着我就会把val与字符串常量池进行判断,利用哈希函数,类似于上面说的 key % len

  • 若哈希表中没有,我就在当中开辟一块空间,这块空间的第一个节点包括哈希值,String对象地址值,和后继next。

  • 原引用 str1 指向“Nick”对象地址,现在哈希表节点也指向这个“Nick”对象地址,因此成功将“Nick”,加入了哈希表中。

  • 现在 str2 指向new 出来的一块空间,val=“Nick”,那么我就利用哈希格式先去找有没有这个数组(内部比较数组对应位数),结果肯定是找到了,那么我就将val的引用指向原先 str1 把“Nick”转换为数组的地址上。

  • 但是结果肯定是false,因为 str1 它创建了一个对象,可是 str2 创建了两个,如图所示 0x8899 和 0x1122 ,尽管最终指向val相同,但是==比较的是地址。我们可以思考,如果如下面代码所示,返回一定是true。

     

💠 例2

  public static void main(String[] args) {      String str1 = "Nick";      String str2 = "Nick";      System.out.println(str1==str2);  }//结果true

      

     

💠 例3

    public static void main(String[] args) { String str1 = "Nick"; String str2 = "Ni"+"ck"; // 此时它俩都是常量,在编译的时候就已经确定好了,是"Nick" System.out.println(str1==str2);    }//结果true

      

🔷 如何去证明?我们反编译字节码文件,发现第二个 str 是 Nick

    

💠 例4

    public static void main(String[] args) { String str1 = "Nick"; String str3 = "Ni"; String str4 = str3+"ck"; //此时str3是一个变量,编译的时候不知道是啥 System.out.println(str1 == str4);    }//结果false

    

🔷 我们翻看字节码文件发现编译器不知道 str4 是啥,重新new了一个,也就是说str3和“ck”拼接后的结果Nick不会出现在常量池里面,而是会new一个对象在堆里面

     

💠 例5

    public static void main(String[] args) { String str1 = "11"; String str2 = new String("1")+new String("1"); System.out.println(str1==str2);    }//结果false

   

🔷 我先编译一下,看字节码文件,在分析

    

🔷 我们发现:

  • new了一个StringBuilder对象

  • 通过两个new 出来的String 对象拼接产生的新的StringBuilder

  • 最后调用了toString方法

  

🔷 toString方法有没有重新new对象呢?我们翻找StringBuilder的源码看看

🔷 答案显而易见,是有的!

      

万事俱备,直接上图分析

  1. 前面入池11的情况见上(这里省略)

  2. 我将两个新new出来的“1”都指向数组中的“1”

  3. 因为+(拼接),所以我又新new出来了一个拼接出来的空间(StringBuilder)

  4. 但是我们的拼接出来的“11”不会入池,StringBuilder中的val指向11

  5. 但是我们是要返回String,因此调用toString方法,查看源码发现又new了一个对象

  6. 这个对象的val也指向11,因此我们的str2指向这个对象的地址,也就是图中的的0x999

  7. 不过这个val仍然没有入池

    

💠 例6

    public static void main(String[] args) { String str2 = new String("1")+new String("1"); String str1 = "11"; System.out.println(str1==str2);    }//不同说结果肯定是一样的false

    

稍作修改:

public static void main(String[] args) {    String str2 = new String("1")+new String("1");    str2.intern(); // 手动入池    String str1 = "11";    System.out.println(str1==str2);}//结果true

     

  • 神奇的事情发生了,0x999(toString后的那个对象),手动入池以后,哈希表里就有了11

  • 当我str1=11时,在表中检索到了,因此str1直接指向入池的对象0x999,这是str1和str2就指向同一个地址了,返回true

  

💠 例7

    public static void main(String[] args) { String str1 = "11"; String str2 = new String("1")+new String("1"); str2.intern(); // 手动入池->当字符串常量池中没有才会入池 System.out.println(str1==str2);    }//结果false

        

  • 如果我如上这样换一下呢?我们可以分析先入池了str1的11

  • str2想要也把11入池,不可以,因为池中已经有了,这种情况手动入池失败了

   

   

🔵 测试代码一览

import java.util.Arrays;public class Demo {    public static void main(String[] args) { String str1 = "11"; String str2 = new String("1")+new String("1"); str2.intern(); // 手动入池->当字符串常量池中没有才会入池 System.out.println(str1==str2);    }    public static void main9(String[] args) { String str2 = new String("1")+new String("1"); str2.intern(); // 手动入池 String str1 = "11"; System.out.println(str1==str2);    }    public static void main8(String[] args) { String str1 = "11"; String str2 = new String("1")+new String("1"); System.out.println(str1==str2);    }    public static void main7(String[] args) { String str1 = "Nick"; String str3 = "Ni"; String str4 = str3+"ck"; //此时str3是一个变量,编译的时候不知道是啥 System.out.println(str1 == str4);    }    public static void main6(String[] args) { String str1 = "Nick"; String str2 = "Ni"+"ck"; // 此时它俩都是常量,在编译的时候就已经确定好了,是"hello" str2.intern(); System.out.println(str1==str2);    }    public static void main5(String[] args) { String str1 = "Nick"; String str2 = "Nick"; System.out.println(str1==str2);    }    public static void main4(String[] args) { String str1 = "Nick"; String str2 = new String("Nick"); System.out.println(str1==str2);    }    public static void main3(String[] args) { String str = "abc"; char[] arr = {'n','i','c','k'}; func(str,arr); System.out.println(str); System.out.println(Arrays.toString(arr));    }    public static void func(String s,char[] arr){ s ="Nick"; arr[0] = 'N';    }    public static void main2(String[] args) { String str1 = "Nick"; String str2 = str1; System.out.println(str1); System.out.println(str2); str1 = "Nick很宠粉的"; //这里是修改指向,不是字面值 System.out.println("================"); System.out.println(str1); System.out.println(str2);    }    public static void main1(String[] args) { //方式1 String str = "Nick很宠粉的"; //方式二(调用构造方法进行构造对象) String str2 = new String("Nick很宠粉的"); //方式三 char[] chars = {'N','i','c','k','很','宠','粉','的'}; String str3 = new String(chars); System.out.println(str); System.out.println(str2); System.out.println(str3);    }}

   

☀️ 以上是Nick花了一个上午的时间剖析的内部原理,图片纯手绘,创作不易,希望大家留下宝贵的三连。