深入理解Java虚拟机——深入理解 String(彻底搞懂 String 对象内存分配以及intern

深入理解Java虚拟机——深入理解 String(彻底搞懂 String 对象内存分配以及intern,第1张

深入理解JVM——深入理解 String(彻底搞懂 String 对象内存分配以及intern方法,不想学明白都难)
  • 1. 前言
    • 1.1 抛出问题
      • 1.1.1 问题1——首例输出结果究竟是?
      • 1.1.2 问题2——字符串常量池中什么时候有了"java"
      • 1.1.3 问题3——StringBuilder 与 String 的问题
      • 1.1.4 问题4——对 String.intern() 理解多少?
  • 2. 带着问题找答案
    • 2.1 彻底搞懂不同方式创建String 对象的内存分配
      • 2.1.1 几种常见的创建String的例子
      • 2.1.2 彻底搞懂每个例子创建对象背后的内存
        • 2.1.2.1 先说说 String 的四种情况
        • 2.1.2.2 再说说 String 与 StringBuilder 的几种情况
          • (1) 查看字节码文件
        • 2.1.2.3 这种方式创建字符串常量池中没有!!
          • (1) 不得不提的 intern() 方法
          • (2) 常见字符串的拼接——不放常量池
    • 2.2 最后说说字符串 "java"
  • 3. 附:JVM简单内存分析图(JDK6、JDK7、JDK8的变化过程)
  • 4. 参考书籍

1. 前言
  • 最近在看周大大的《深入理解Java虚拟机》,有些例子还真挺有意思,反而例子的主点清楚了而例子里的小点懵懵懂懂,前段时间看前面章节的时候看到过一个关于Stringintern() 方法的一个例子,我觉得还挺有意思的,不过千万不要觉得是说知识点有意思,而是例子本身有意思。所以今天整理一下给大家分享一下,对String再有一个深入的理解。
1.1 抛出问题 1.1.1 问题1——首例输出结果究竟是?
  • 问题就是对于下面的例子输出结果是?

    我已经把书本上的原代码敲了下来,来一起看看吧:

    public class RuntimeConstantPoolOOM {
        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   false 
    JDK1.7/1.8--->true  false
    

    对,答案是根据JDK的版本不同而不同,所以如果你对JDK版本变化不是很了解的话,可以先了解一下,我们下面也有介绍
    深入理解Java虚拟机——关于方法区的变化 + JVM简单内存变化图.

  • 如果有疑问,我们先来看一下作者的解释吧:

1.1.2 问题2——字符串常量池中什么时候有了"java"

上面的解释不知道你有没有看明白呢,如果提前了解永久代和元空间的话,这解释已经很明白了,但是不知道你好奇不好奇第二个例子为什么是“java”字符串,根据解释:“java”这个字符串在执行String-Builder.toString()之前就已经出现过了,字符串常量池中已经有它的引用了,为什么已经有了?换成别的试试呢?我们不妨试试:


第一张图是JDK8运行的结果,第二张图是JDK6运行的结果,问题是在Java8里为什么只有拼接“java”字符串的时候是false,按照书上的解释,如果把例子换一下或者就没这么多疑问了,比如,换成“studyjava”

String str2_0 = "studyjava";
String str2 = new StringBuilder("study").append("java").toString();
System.out.println(str2.intern() == str2);


这么理解的话,好像就简单的多了,所以有了我们的问题2:“java”这个字符串什么时候到常量池中的呢?

1.1.3 问题3——StringBuilder 与 String 的问题
  • 说这个问题之前必须先回顾一下,还记得上面说的下面的例子吧,前面两个例子在JDK8运行的结果都是true,关键是后面两个是true还是false呢?
    String str1 = new StringBuilder("计算机").append("软件").toString();
    System.out.println(str1.intern() == str1);
    
     String str2 = new StringBuilder("study").append("Java").toString();
     System.out.println(str2.intern() == str2);
    
     String str3 = new String("网络工程");
     System.out.println(str3.intern() == str3);
    
     String str4 = new String("learnPython");
     System.out.println(str4.intern() == str4);
    
    
  • 来看看运行结果如何吧:

    好了,如果你有疑问,不妨先搁置,先记下问题,继续往下看吧,答案在后面……
1.1.4 问题4——对 String.intern() 理解多少?
  • 要说对 String.intern() 到底理解多少,不妨看下面几个例子,如果输出结果都能对的话,那我就要问还想学 String.intern() 的什么?也可以告诉我一下,互相学习一下,哈哈……
2. 带着问题找答案 2.1 彻底搞懂不同方式创建String 对象的内存分配 2.1.1 几种常见的创建String的例子
  • 首先,在看 intern() 方法之前,对于创建 String 对象的几种方式以及不一样的方式可能创建几个对象,还比如 String s1 = "abc";String s2 = new String("abc");这两种方式创建对象有什么不同等问题,相信你应该了解的很清楚了,我们不妨看几个简单的例子,直接看代码吧:
    String s1 = "abc";
    String s2 = new String("abc");
    
    String s3 = "abcdef";
    String s4 = "abc" + "def";
    String s5 = s1 + "def";
    String s6 = new String("abc") + new String("def");
    
    System.out.println(s1==s2);
    System.out.println(s3==s4);
    System.out.println(s3==s5);
    System.out.println(s3==s6);
    System.out.println(s5==s6);
    
    答案自己看截图,我们这里就不做过多讲解了:
2.1.2 彻底搞懂每个例子创建对象背后的内存 2.1.2.1 先说说 String 的四种情况
  • 这里我们简单再说说:
    String s1 = "abc";这种方式是字面量创建对象,会字符串 "abc" 存储在常量池中的,然后引用变量s1指向该字符串 "abc"在常量池中的地址。再执行String s2 = "abc"; 时,常量池中已经有了字符串"abc",会让s2直接指向常量池中的 "abc”地址(因为字符串常量池是全局共享的,池中不允许存放相同的字符串),所以 s1 == s2 为true

    ② 另外String有不可变性,比如:String str1 = "abc"; str1 = "def"; ,在执行 str1 = "def"; 时,不会去修改常量池中"abc"的值,而是在字符串常量池中重新创建一个新的字符串"def"。

    ③ 对于 String s2 = new String("abc"); 用这种构造器方式的创建对象时, 会在常量池外的堆中创建一个 “abc” ,然后栈中的引用变量 s2 会指向这个地址, 同时也会把字符串 “abc” 缓存到字符串常量池中(前提是字符串 “abc” 在常量池中还没有创建过),即:在内存中开辟了两份地址。

    ④ 对于 String s4 = "abc" + "def";来讲,其实就等同于 String s4 = “abcdef” ;,所以,可以理解为这种方式就是通过字面量方式创建的,创建后的字符串就存放在字符串常量池中。你可以理解为是编译期的优化,我们也可以看一下编译后的class文件,就更加清楚了:

2.1.2.2 再说说 String 与 StringBuilder 的几种情况
  • 我们接着 2.1.2.1 往下看,把下面的和 StringBuilder 有点关系的几种情况看完,接下来的 intern() 和上面的大多问题就自然都明白了。
  • 接着上面的 来说,我们上面对于 String s4 = "abc" + "def";的理解是看了编译后的class文件,但是如果对于下面几个你用了反编译,可能产生怀疑,所以上面的 ④ 我们暂且就那样用吧,毕竟也是很好理解的,关键是下面的怎么解释呢?
    String s1 = "abc";
    String s2 = "def";
    String s3 = s1 + "def";
    String s3_2 = "abc" + s2;
    String s4 = s1 + s2;
    
    String s5 = new StringBuilder("wx").append("yz").toString();
    
    不妨看看反编译的效果,本来没有疑问的,让你瞬间不知如何解释,猜测是跟 StringBuilder 有关系,但是 String s3 = s1 + "def"; 的反编译又说不通,有点意思哈:

    好了,别猜了,这个作为参考吧,你了解了解就行了,那么对于 String s3 = s1 + "def"; String s3_2 = "abc" + s2; String s4 = s1 + s2;的底层原理到底是什么?要不要看class文件呢?要看!但是不是通过反编译看!而是通过字节码指令来看!来吧,一起来看看吧……
(1) 查看字节码文件
  • 用命令 javap -v -p 查看,结果如图:


    这样看还好吧,比起16位的字节码来说,应该是,嗯可看的,机器、汇编、再高级,前辈们真的太牛了,人类真的了不起!好了,感叹归感叹,认清现实,还是老老实实做个码农吧,面向业务功能无脑开发,怕是摸鱼打酱油成了部分办公室的常态,说了点废话,还是画图吧,再画一个配图再简单说说,这个就保证你搞得明明白白的,先看图:

    看完图好像应该都不用解释了哈,咱就说这个吧,关键是这一个在反编译时也没看出来啥,那就继续吧,该 了哈

    对于 String s1 = "abc"; String s3 = s1 + "def"; 来讲,底层创建的过程是:
    第一:先 new StringBuilder() (你可以理解成有对象,也可以理解为匿名创建);
    第二:然后再调用 StringBuilder 的 append() 方法,所以 “+” 号前面可以理解为:
    new StringBuilder().append(s1) ,这里的 s1 其实就是常量池中的 “abc” 字符串;
    第三:到 “+” 号了,它就等同于 StringBuilder 的 append(),所以 + “def” 就是 .append("def") *** 作,所以到这里,连起来就是:
    new StringBuilder().append(s1).append("def")
    第四:看上面图应该注意到最后返回的是 String 类型的,所以到最后,又调用了 StringBuilder 的 toString() 方法,所以连起来就是:

    new StringBuilder().append(s1).append("def").toString()
    

    所以StringBuilder 的 toString() 方法里应该就调用了String 的构造器,自己下去可以看看源码也确实是,但是这个地方你可得注意了,虽然用了String的构造器,你可千万别认为这整个创建对象的过程与String s3 = new String("abcdef");方式创建对象的是一样的,还真不一样,因为这种用 "+" 号拼接的,不会把结果字符串 "abcdef" 放在常量池种一份,只放在堆中!!!
    所以,《2.1.1 几种常见的创建String的例子》的运行结果只要一个true其他都是false,这里我们就不多少了。
    这个搞清楚了,剩下的拼接应该不用说了哈,自己也可以测试测试看看,有时候用眼还真不如用手。

    接下来就来就来介绍一下什么情况下创建的字符串会同时在常量池中也开辟一份空间呢。

2.1.2.3 这种方式创建字符串常量池中没有!! (1) 不得不提的 intern() 方法
  • 等了好久了哈,intern() 终于来了,为什么来的这么迟,因为部分友友String的初上还没搞明白,就来看 intern() 方法,所以看一次忘一次,每次看完都是一知半解的,所以把上面知识也给大家整理了一下。

  • 因为是我们常见类的一个方法,所以如果上面都懂,那个这个介绍可以说跟 equals() 方法一样好理解,真的好不夸张,一起看看吧。

  • 对于 String result = str.intern();的解释(其中str可以是通过各种形式转化之后得到的String对象,假如其值为"XXX"

    ① 执行 str.intern() 时,会在常量池中判断,如果常量池中有"XXX"就把"XXX"在常量池中的地址返回给 result。

    ② 如果在常量池中没有找到字符串"XXX",分JDK6和JDK7及以上两种情况,看表格:

     如果在常量池中没有找到,分JDK6和JDK7及以上
    JDK6会把 字符串"XXX" 复制一份到永久代里,然后返回永久代里的引用地址给 result。所以开篇周大大的例子对于JDK6都是 false
    JDK7及以上因为字符串常量池在堆中,所以JVM的大佬们设计是这样的:调用 str.intern()时,会在常量池中记录一下字符串 “XXX” 在堆中的引用地址(而不是在常量池中复制一份字符串"XXX"),并把这个引用地址返回给 result。

    ③ 还有一点需要注意:执行 intern() 方法不一定有返回值,但是只要执行,就会有上面①②两种情况的判断,只是不返回地址而已。

  • 可能这样说有点空洞,举个例子(两种情况对比一下)你就更明白了:

    内存结构大概可以这么理解着看:

  • 看完这个例子之后,你大概也就明白了点 String s = "X"; String str = s + "XX"; 执行完之后,只要字符串"X" 和 “XX” 在常量池中,但"XXX"并不在常量池中。

    我们接下再简单看看什么情况下创建的字符串在常量池中,什么情况下不在!

(2) 常见字符串的拼接——不放常量池
  • 先看下面3个例子,看看有没有完全悟透:
String str0 = new String("mnop");
String test0 = str0.intern();
System.out.println(str0 == test0);

String str1 = new StringBuilder("abcd").toString();
String test = str1.intern();
System.out.println(str1 == test);

String str2 = new StringBuilder("wx").append("yz").toString();
String test2 = str2.intern();
System.out.println(str2 == test2);

来,看看结果,顺便解释一下:

怎么知道"wx"、"yz"在常量池中个开辟了一份空间呢?来,看字节码指令文件:

  • 总结一句话就是:拼接字符串后的结果字符串不会存储在字符串常量池中,只在堆中开辟了空间
    最后,该 了是吧,对于 String s6 = new String("abc") + new String("def"); 来说,我觉得相关问题都可以拜拜了吧,如果你说还是不是很清楚,那我觉得你肯定没有认真看到这里,所以都到这里了,这个真的就没说的任何必要了。

  • 所以,上面的问题除了一个字符串 “java” 问题,应该现在每个问题都不是问题了吧
    ⑥ 对于 String s6 = new String("abc") + new String("def"); 来说

    如果简单内存结构方面不是很清楚的话,可以看先看下面这篇文章先了解清楚:

    简单分析 JVM 对象的内存(对象的创建)以及String相关知识.

2.2 最后说说字符串 “java”
  • 哎,终于到特殊字符串 “java” 了,也是因为上面周大大的例子,让我好奇心倍增,才有了今天这篇文章,本来主题应该是这个好奇的玩意的,但是不知不觉就写多了,哎不说了,还是看看 “java” 特殊在哪里吧,可能我太菜,但是我之前是真的不知道,虽然你知道了不会提高你的身价,但是满足了你的好奇也是一件趣事吧!
  • 来一起看看它在哪里,怎么就提前有了呢,到底是被哪个类提前加载进去了呢?

    哦,原来是这里,挺逗,好了,剩下的如果你想玩的话,自己玩吧!
3. 附:JVM简单内存分析图(JDK6、JDK7、JDK8的变化过程)
  • 如果你想对下图的了解还有点含糊的话,可以看看这篇文章:
    深入理解Java虚拟机——关于方法区的变化 + JVM简单内存变化图.
4. 参考书籍

《周志明》著 《深入理解Java虚拟机》

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/725787.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-26
下一篇 2022-04-26

发表评论

登录后才能评论

评论列表(0条)

保存