• 理解字符串常量池(JVM)


    大纲

    在这里插入图片描述

    思考

    1. 如何查看字符串常量池(StringTable)?

      使用 jclasslib 插件打开字节码,选择 常量池 -> 显示所选 -> CONSTANT_String_info,左侧过滤后的内容即为字符串常量池

    2. 字符串常量池、方法区、永久代和元空间的关系?

      方法区是逻辑概念,永久代和元空间是方法区的实现。字符串常量池总是在堆上:

      • JDK7之前,方法区的实现是永久代,永久代是堆的一部分,而字符串常量池是永久代的一部分。
      • JDK7时,方法区的实现是永久代,永久代是堆的一部分,字符串常量池也是堆的一部分,字符串常量池和永久代并列。
      • JDK8及之后,方法区的实现是元空间,元空间是操作系统直接内存,字符串常量池还是堆的一部分。
    3. Java中什么是常量?static final 修饰的量就是常量吗?

      • 从字节码层面上看,字段有 ConstantValue 修饰的就是常量(包含对常量池的索引),如下图所示
        在这里插入图片描述
      • 从 Java 源代码层面上看
        • 被 static final 修饰的 int、double 等基本数据类型(不包含 Integer 等包装类)
        • 被 static final 修饰的 String
        • 字面量,例如 1 或者 "abcd"

      注意事项:static final String s = "abc" 中的 s 是常量,但 static final String s = new String("abc") 中的 s 并不是常量,同样 static final Integer num = 1 中的 num 也不是常量。

    4. 字符串拼接问题

      • 只有常量的情况,会存在编译优化,只会将最后生成的字符串添加到字符串常量池,例如 String s = "ab" + "cd" 字符串常量池中只会出现 "abcd"
      • 出现变量的情况,会创建 StringBuilder 对象,通过 append() 来追加字符内容,最终调用 toString() 来创建字符串对象。

    测试 intern() 的作用

    发现一个 bug,下面的代码如何批量测试时,internTest03() 方法的结果是 false、true。单独测试该方法时,返回结果是 true、true。

    public class InternTest {
        @Test
        public void internTest01() {
            String s = new StringBuilder()
                    .append('a')
                    .append('b')
                    .append('c')
                    .append('d')
                    .toString();
    
            String intern = s.intern();
    
            System.out.println(intern == s);//true
        }
    
    
        @Test
        public void internTest02() {
            String s0 = "abcd";
    
            String s = new StringBuilder()
                    .append('a')
                    .append('b')
                    .append('c')
                    .append('d')
                    .toString();
    
            String intern = s.intern();
    
            System.out.println(intern != s);//true
            System.out.println(intern == s0);//true
        }
    
    
        @Test
        public void internTest03() {
    
            String s = new StringBuilder()
                    .append('a')
                    .append('b')
                    .append('c')
                    .append('d')
                    .toString();
    
            String intern = s.intern();
            String s0 = "abcd";
    
            System.out.println(intern == s);//true
            System.out.println(intern == s0);//true
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    测试创建多少个对象的代码

    
    public class HowManyObjectTest {
    
    
        @Test
        public void howManyObjectTest01() {
            String s = "abcd";
        }
    
        @Test
        public void howManyObjectTest02() {
            String s = "ab" + "cd";
        }
    
    
        @Test
        public void howManyObjectTest03() {
            String s = new String("abcd");
        }
    
        @Test
        public void howManyObjectTest04() {
            String s = new String("ab" + "cd");
    
            // 验证字符串常量池中是否存在某个字符串的方法:
            // 还可以通过jclasslib直接查看字符串常量池中是否出现"ab"
            // String ab = new String("a") + new String("b");
            // String intern = ab.intern();
            // System.out.println(ab == intern);  //true,证明"ab"是通过ab.intern()放入字符串常量池的,之前并不存在。
        }
    
        @Test
        public void howManyObjectTest05() {
            String s = new String("abcd") + "abcd";
        }
    
    
        static String s1 = "aaa";
        static String s2 = "bbb";
    
        @Test
        public void howManyObjectTest06() {
            String s = s1 + s2;
        }
    
        static final String s3 = "aaaa";
        static final String s4 = "bbbb";
    
        @Test
        public void howManyObjectTest07() {
            String s = s3 + s4;
        }
    
    
        static final String s5 = new String("aaaaa");
        static final String s6 = new String("bbbbb");
    
        @Test
        public void howManyObjectTest08() {
            String s = s5 + s6;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    问题一:new String("abcd") 会创建多少个对象?

     0 new #3 <java/lang/String>
     3 dup
     4 ldc #2 <abcd>
     6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
     9 astore_1
    10 return
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上面是 new String("abcd") 的字节码,下面对上面的内容进行解释:

    1. new #3:表示创建一个 java.lang.String 类型的对象(分配对象空间),并将地址压入操作数栈。
      (其中 #3 表示字符串常量池中的索引,此时指向 java.lang.String)

      在这里插入图片描述

    2. dup:对操作数栈的栈顶元素进行复制,并压入操作数栈

      在这里插入图片描述

    3. ldc #2:从常量池中加载字符串 “abcd” 对象,并压入操作数栈

      在这里插入图片描述

    4. invokespecial #4:调用 String 类初始化方法 init<>,由于调用含参构造,因此消耗操作数栈中的两个参数
      (从更通用的角度来看,这个 init<> 的逻辑包含 (a)显示赋值、(b)代码块赋值、(c)构造器方法调用)

      在这里插入图片描述

    5. astore_1:将初始化完成的 String 对象的引用赋值给局部变量 s

      在这里插入图片描述

    问题二:new String("abcd") + "abcd" 会创建多少个对象?

     0 new #5 <java/lang/StringBuilder>
     3 dup
     4 invokespecial #6 <java/lang/StringBuilder.<init> : ()V>
     7 new #3 <java/lang/String>
    10 dup
    11 ldc #2 <abcd>
    13 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
    16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
    19 ldc #2 <abcd>
    21 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
    24 invokevirtual #8 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
    27 astore_1
    28 return
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    1. new $5、dup、invokespecial #6:创建一个 StringBuilder 对象,并初始化

      在这里插入图片描述

    2. new #3、dup、ldc #2、invokespecial #4:new String("abcd") 的代码逻辑

      在这里插入图片描述

    3. invokevirtual #7:调用 append() 方法,返回值类型还是 StringBuilder,消耗操作数栈中的两个元素,并返回一个元素

      注意:如果此时发生GC,那么 0x100 对应的 String 对象会被标记为垃圾。因为从 GCRoot 出发不可达。疑问:字符串常量池是否会进行垃圾回收?其中的对象在什么情况下会进行垃圾回收?

      在这里插入图片描述

    4. ldc #2:直接压入字符串常量池中地址
      在这里插入图片描述

    5. invokevirtual #7:再次调用 append() 方法

      在这里插入图片描述

    6. invokespecial #8:调用 toString() 方法,创建 String 对象

      在这里插入图片描述

    7. astore_1:赋值给局部变量 s
      在这里插入图片描述

    其它问题

    1. 验证 String s = "ab" + "cd" 执行完成后,字符串常量池中并不存在 "ab" 和 "cd"
    2. 使用 StringBuilder 对象调用 append() 逐个追加字符,最终得到的字符串不会放入到字符串常量池中
    3. 使用 char[] 来创建 String 对象,得到的字符串不会放入到字符串常量池中,StringBuilder 本质上维护了一个动态扩容的 char[]
    4. static final Integer num = 1 和 static final String s = new String("abcd") 中的 num 和 s 均不是常量(可以通过 clinit<> 的代码逻辑进行查看)
  • 相关阅读:
    PHP语言之正则表达式
    ssm大型商场移动导游系统的设计与实现毕业设计源码100853
    异构数据库
    【软件工程_UML—StartUML作图工具】startUML怎么画interface接口
    grav安装教程
    我是如何从零到成为 Apache 顶级项目的 Committer
    Typora图床上传配置:PicGo+Gitee 不完全指南
    【Java】RestClient的使用
    全网最详细教程(上):教你如何从0-1制作出一张可视化大屏
    前端AJAX讲解
  • 原文地址:https://blog.csdn.net/qq_43570534/article/details/137980047