• JVM stringTable的理解学习


    学习目标:

    • StringTable的内存结构和调整参数
    • 如何将字符串放到字符串常量池
    • 声明一个字符串的底层字节码
    • 字符串拼接的底层字节码
    • new String和new StringBuilder的区别
    • intern()方法

    StringTable的内存结构和调整

    StringTable为一个HashTable结构,结构如下:
    key的计算

    • 使用了字符串和字符串长度计算获得一个唯一的hash值,这个hash值和变量名没有关系奥
    • 利用这个hash值获得它对应的唯一的序号,把这个序号作为Key
    • 这个key就可以在hash表里用hash函数计算获得一个位置了。

    value
    记录的是这个对象的真实内存地址
    在这里插入图片描述

    hashtable的基本链接的结构

    struct HashtableEntry {
            INT_PTR hash;
            void* key;    //hashValue
            void* value;   //instanceOopDesc
            HashtableEntry* //next;
        };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    StringTable的大小是可以调整的,通过参数-XX:StringTableSize进行调整

    • java6中默认大小为1009
    • java7中的默认大小为60013
    • java8中可选的最小大小为1009

    StringTable在jvm的逻辑内存地址也在发生变化

    • jdk6中它在方法区里
    • jdk7/8中它在堆中

    如何将字符串放到字符串常量池

    放到字符串常量池并不是指向字符串常量池,每个字符串对象都指向它真实的字符串地址,字符串常量池就是维护对象名(key)和它指向的真实字符串的地址(value)

    1. “abc” 双引号String 对象会自动放入常量池,常量池中记录了这个变量的key和真实物理地址
    2. 调用String的intern 方法也会将对象放入到常量池中,这里请看inter()方法介绍

    直接声明一个字符串的字节码和底层进行的操作

    public class StringTest {
        public static void main(String[] args) {
            String str1 = "helloworld";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    对应的字节码文件

    0 ldc #2 <helloworld>
    2 astore_1
    3 return
    
    • 1
    • 2
    • 3

    从字节码文件里可以看到使用了ldc字节码指令获取到了字符串helloworld,这说明这个helloworld已经在内存中了,其实这里是因为字符串属于常量信息,无论是类字段还是局部变量,字符串都属于一种常量信息,所以常量信息会被编译到常量池中,在类加载的链接阶段加载到内存并分配好了内存空间以及赋值
    在链接阶段就会创建好一个字符串对象,并且放到字符串常量池中

    在这里插入图片描述
    在这里插入图片描述

    具体加载过程使用此图进行说明
    String的底层实现是一个char数组或者后来的byte数组,
    因此声明一个String变量

    1. 首先会生成这个变量的hash值,去字符串数组里看看是否已经存在,如果存在,就直接指向已存在的对象
    2. 如果不存在会创建String对象和一个数组对象,数组对象存值,String对象指向这个数组对象
    3. 后将这个字符串放到字符串常量池中

    字符串拼接的底层字节码

    对于变量内的字符串相加,编译会进行优化

    public class StringTest {
        public static void main(String[] args) {
            String a = "hello" + "word";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    对应的字节码文件如下

    0 ldc #2 <helloword>
    2 astore_1
    3 return
    
    
    • 1
    • 2
    • 3
    • 4

    从字节码文件里可以看出来jvm底层进行了优化,把两个字符串合并了

    对于两个字符串变量相加

    public class StringTest {
        public static void main(String[] args) {
            String a = "hello" ;
            String b = "word";
            String c = a+b;
            System.out.println(c.intern()=="helloword");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    对应的字节码文件如下:

     0 ldc #2 <hello>
     2 astore_1
     3 ldc #3 <word>
     5 astore_2
     6 new #4 <java/lang/StringBuilder>
     9 dup
    10 invokespecial #5 <java/lang/StringBuilder.<init> : ()V>
    13 aload_1
    14 invokevirtual #6 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
    17 aload_2
    18 invokevirtual #6 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
    21 invokevirtual #7 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
    24 astore_3
    25 return
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    从字节码文件可以看出执行了如下过程 (new StringBuilder()).append(a).append(b).toString();

    1. 创建了StringBuilder对象,通过append方法进行了字符拼接,
    2. 然后通过toString返回了一个新的字符串对象。

    所以字符变量拼接底层实际上是使用了StringBuilder对象的append方法,因此如果进行循环多次字符变量拼接过程中会反复创建StringBuilder对象,因此推荐显示使用StringBuilder完成字符拼接操作

    此外toString()操作底层实现就是返回了一个new String(“字符串”)对象


    new String和new StringBuilder的区别

    new String()

    public class StringTest {
        public static void main(String[] args) {
            String abc = new String("abc");
            String abcdef = new String("abc")+ new String("def");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    对应的字节码文件

     0 new #2 <java/lang/String>
     3 dup
     4 ldc #3 <abc>
     6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
     9 astore_1
    10 new #5 <java/lang/StringBuilder>
    13 dup
    14 invokespecial #6 <java/lang/StringBuilder.<init> : ()V>
    17 new #2 <java/lang/String>
    20 dup
    21 ldc #3 <abc>
    23 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
    26 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
    29 new #2 <java/lang/String>
    32 dup
    33 ldc #8 <def>
    35 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
    38 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
    41 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
    44 astore_2
    45 return
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    可以看出无论是声明单个字符,还是拼接字符,new String()用于初始化的字符串都是被加载到了内存中的字符串常量池中的
    因此new String过程创建了两个对象一个是new String对象,一个是初始化的字符串对象

    具体实现过程说明如下:
    1.首先会对赋值的字符串进行对象创建,创建过程就是上面的字符串创建流程。
    2.创建完成之后进行new String的操作,创建一个对象
    3.对象的char对象指向之前创建出来的字符地址
    在这里插入图片描述
    在这里插入图片描述

    new StringBuilder

    public class StringTest {
        public static void main(String[] args) {
            String abcdef = new String("abc")+ new String("def");
            String abcd = new StringBuilder("abc").toString();
            String abdd = new StringBuilder("abc").append("def").toString();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    对应的字节码文件

     0 new #2 <java/lang/StringBuilder>
     3 dup
     4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
     7 new #4 <java/lang/String>
    10 dup
    11 ldc #5 <abc>
    13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
    16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
    19 new #4 <java/lang/String>
    22 dup
    23 ldc #8 <def>
    25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
    28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
    31 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
    34 astore_1
    35 new #2 <java/lang/StringBuilder>
    38 dup
    39 ldc #5 <abc>
    41 invokespecial #10 <java/lang/StringBuilder.<init> : (Ljava/lang/String;)V>
    44 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
    47 astore_2
    48 new #2 <java/lang/StringBuilder>
    51 dup
    52 ldc #5 <abc>
    54 invokespecial #10 <java/lang/StringBuilder.<init> : (Ljava/lang/String;)V>
    57 ldc #8 <def>
    59 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
    62 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
    65 astore_3
    66 return
    
    
    • 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

    从字节码文件可以看出StringBuilder的初始参数字符串和append()方法里的字符串参数是被加载到字符串常量池的,但是合并后的字符串是没有放到字符串常量池的,上面已经说过StringBuilder是用来进行字符串拼接的,通过toString方法返回拼接后的字符对象。因此通过字节可以看出通过toString返回拼接后的对象,该对象的字符串其实并没有在字符串常量池中创建对应的字符串

    总结:

    1. new String()创建字符串的时候,它的初始化常量会查询一遍字符串常量池,没有的话会放进去,所以new String的字符永远是已经被加载到字符串常量池了,所以返回的是堆中的一个对象,这个对象没有放到常池中,但是它指向的字符串是被记录在字符串常量池中的
    2. new StringBuilder().append()创建字符串的时候,也是同理,它的各个参数对应的字符串也是永远都被已经加载到字符串常量池了,但是合并后的字符串却没有加载到字符串常量池,因为是动态创建的,所以没通过字符串常量池进行查询,因此这个最终通过toString返回的对象对应的字符串记录是没有在字符串常量池中进行记录的

    字符拼接的具体过程如下:
    注意:有地方说 把下面的 s改成 String s = “aa”+new String("bb") 创建对象会变少,而我经过调试发现并没有变少还是4个
    在这里插入图片描述


    Intern()方法

    在程序中创建一个字符串的时候,如果同过类来创建,往往会声明一个变量获得对应的对象,在比较的时候如果直接拿这个变量比较的话,其实比较的是这个变量对象在堆中的内存地址,这个地址对于不同对象肯定是不同的。为了比较对象真实的字符串信息,因此需要intern()方法获得对象指向的真正的字符串地址。

    • intern方法在字符串常量池中根据这个对象的名字来寻找是否存在该对象,找到了返回地址
      • 如果没找到:在jdk6中,会创建一个字符串,然后返回这个字符串地址,但是他创建的这个字符串是把堆空间中的字符串对象复制到了永久代空间里的,因此对象的地址发生了变化。字符串常量池指向的地址和
      • 如果没找到:在jdk7/8中,会先查找字符串常量池,然后创建一个字符串,这个字符串指向的是堆中存在的字符串,但是返回的地址是真实对象地址
        总的来说jdk67的差异就是应为永久代和元空间的位置不同导致的运行逻辑不同
    public class StringTest {
        public static void main(String[] args) {
    
            String s1 = "ab";
            String a = new String("ab");
            System.out.println(a==s1);  //false因为a是一个新的对象地址  而s1是ab地址
            System.out.println(a.intern()==s1); //true 因为a.intern()去字符串常量表里找他的字符对应的地址,这个地址和s1是同一个
    
            String s = new StringBuilder("a").append("b").toString(); //创建一个新的对象
            System.out.println(s1==s); //false  新对象地址和ab不同
            System.out.println(s.intern()==s1);
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
  • 相关阅读:
    shell脚本之数组元素排序
    【换根DP】CF1882 D
    面试官都震惊,你这网络基础可以啊!
    Flutter小米商城
    创造可持续价值的下一代
    求两个单链表的第一个交点
    python使用sqlalchemy模块创建MySQL数据库连接、更新(update)数据库表中满足条件的数据
    数据结构:复杂度分析
    maya显示隐藏 动画长度
    R语言ggplot2可视化:使用ggpubr包的ggbarplot函数可视化水平柱状图(条形图)、使用orientation参数设置柱状图转置为条形图
  • 原文地址:https://blog.csdn.net/qq_37771209/article/details/126327882