JVM篇:字符串常量池

发布时间:2024年01月05日

String类型字符串常量池问题

public class demo2 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
    }
}

对以上代码进行编译得到字节码文件后使用javap -c [字节码文件]反汇编得到以下信息

Constant pool: //常量池
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = String #25 // a
#3 = String #26 // b
#4 = String #27 // ab
#5 = Class #28 // demo2
#6 = Class #29 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Ldemo2;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 s1
#19 = Utf8 Ljava/lang/String;
#20 = Utf8 s2
#21 = Utf8 s3
#22 = Utf8 SourceFile
#23 = Utf8 demo2.java
#24 = NameAndType #7:#8 // "<init>":()V
#25 = Utf8 a
#26 = Utf8 b
#27 = Utf8 ab
#28 = Utf8 demo2
#29 = Utf8 java/lang/Object
{ //方法区
public demo2(); //构造方法
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ldemo2;
public static void main(java.lang.String[]); //主方法
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String a//ldc的意思是去常量池中加载一个对象,加载的标号为#2
2: astore_1 //存入局部变量,编号为1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
LineNumberTable:
line 3: 0
line 4: 3
line 5: 6
line 6: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 s1 Ljava/lang/String;
6 4 2 s2 Ljava/lang/String;
9 1 3 s3 Ljava/lang/String;
}
SourceFile: "demo2.java"

以上便是反汇编的数据,需要注意的是,常量池(也叫class文件常量池,主要作用是对照表)中的信息都会被加载到运行时常量池(JVM在加载类时会将常量池的所有信息加载到运行时常量池)中,在未执行到ldc时,运行时常量池中的符号并不是java的字符串对象,仅仅是一个符号,执行完ldc后才会将符号转化为对应的字符串对象。当读取完一个字符串对象后,会将该字符串存储在字符串常量池(hashtable结构,不能扩容),方便下次直接获取。

再看一下字符串拼接的反汇编结果

public class demo2 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
    }
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: return

可以看到,字符串拼接实际上是用了StringBuilder类中的append方法然后使用toString方法返回。去查看StringBuilder源码

实际上是创建了一个String对象。那么通过创建对象实例化出来的字符串对象与直接赋值的字符串对象地址相同吗?

public class demo2 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
        System.out.println(s3 == s4);
    }
}

运行结果如下?

false

由此可知,s4引用的是新的字符串对象(存储在堆中)而s3引用的是StringTable中的ab对象

由此可知,通过new创建的字符串对象即使值一样但内存还是不相同的。这是因为创建出来的对象均放在堆内存中

public class demo2 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
        String s5 = "a" + "b";
        System.out.println(s3 == s5);
    }
}

那么如果是字符a与字符b拼接反汇编结果是什么呢

Code:
stack=3, locals=6, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: ldc #4 // String ab
31: astore 5
33: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
36: aload_3
37: aload 5
39: if_acmpne 46
42: iconst_1
43: goto 47
46: iconst_0
47: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
50: return

可以看出来直接从字符串常量池中获取了ab值,而不是通过StringBuilder拼接,原因是:对于字符串对象拼接,这是两个可变对象,在运行时可能会动态更改值,因此需要动态拼接,但是对于硬编码写死的字符串拼接,在编译期间已经确定了s5的值,因此直接区字符串常量池里面查找ab的值即可。

字符串延迟加载体现

public class demo3 {
    public static void main(String[] args) {
        System.out.println("1");
        System.out.println("2");
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9");
        System.out.println("0");

        System.out.println("1");
        System.out.println("2");
        System.out.println("3");
        System.out.println("4");
    }
}

在第一行打断点,去查看String类型的数量来判断字符串是否是延时加载

由上图可以看出,初始的字符串数量为2312个

而在走到第二个断点时,字符串数量以及变成了2322个,加了十个字符串数量。走到第三个断点处字符串数量仍为2322个,因此可以知道,如果字符串常量池中存在该字符串的话会直接返回该字符串的地址,并不会再次存储在字符串常量池。

intern()

public class demo3 {
    //StringTable [a,b,bc]
    public static void main(String[] args) {
        String x = "abc";
        String s = new String("a") + new String("b");// 这个步骤相当于new String("ab")
        //堆 new String("a")  new String("b") new String("ab")
        String s1 = new String("a")+"bc";
        //intern()方法作用是,主动将堆内存中的字符串对象放入字符串常量池中,
        //如果不存在则放入,如果存在不会放入,然后返回StringTable中的对象给intren变量
        //也就是说返回的变量一定是StringTable中的对象
        String intern = s.intern();
        System.out.println(s == "ab");
        String intern1 = s1.intern();
        System.out.println(s1 == x);
        System.out.println(intern1 == x);
    }
}

运行结果如下?

true

false

true

字符串常量池中并没有存在字符串ab,于是执行intern()方法将堆内存中的字符串对象放入字符串常量池中(即将s放入),但是字符串abc已经存在了,因此s1的intern()方法返回的对象是字符串常量池中的,s1本身并没有放入字符串常量池。

public class demo3 {
    //StringTable [a,b]
    public static void main(String[] args) {
        String s = new String("a") + new String("b");
        String intern = s.intern();
        System.out.println(s == "ab");
    }
}

运行结果如下?

false

因为在1.6以下的版本intern()并不是将堆内存中的字符串对象放入字符串常量池中,而是复制一份相同的对象放入,因此即使是调用intern方法也不会将自身放入字符串常量池。

StringTable的位置

在JDK1.6之前,StringTable的位置是在方法区中的永久代当中,但在1.6之后,StringTable被存储在堆内存当中。这样做的好处就是更容易触发GC,减轻字符串对内存的占用情况。

StringTable垃圾回收

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class demo4 {
    public static void main(String[] args) {
        int i = 0;
        try {
            for(int j = 0; j < 10000; j++) { // j = 100, j = 10000
                String.valueOf(j).intern();
                i++;
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println(i);
        }
    }
}

设置运行环境后运行结果为

[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->756K(9728K), 0.0006322 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

10000

Heap

PSYoungGen total 2560K, used 923K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)

eden space 2048K, 21% used [0x00000000ffd00000,0x00000000ffd6cec0,0x00000000fff00000)

from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)

to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)

ParOldGen total 7168K, used 268K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)

object space 7168K, 3% used [0x00000000ff600000,0x00000000ff643040,0x00000000ffd00000)

Metaspace used 3492K, capacity 4498K, committed 4864K, reserved 1056768K

class space used 387K, capacity 390K, committed 512K, reserved 1048576K

SymbolTable statistics:

Number of buckets : 20011 = 160088 bytes, avg 8.000

Number of entries : 14107 = 338568 bytes, avg 24.000

Number of literals : 14107 = 601088 bytes, avg 42.609

Total footprint : = 1099744 bytes

Average bucket size : 0.705

Variance of bucket size : 0.707

Std. dev. of bucket size: 0.841

Maximum bucket size : 6

StringTable statistics:

Number of buckets : 60013 = 480104 bytes, avg 8.000

Number of entries : 10563 = 253512 bytes, avg 24.000

Number of literals : 10563 = 580520 bytes, avg 54.958

Total footprint : = 1314136 bytes

Average bucket size : 0.176

Variance of bucket size : 0.194

Std. dev. of bucket size: 0.441

Maximum bucket size : 3

第一行显示了执行力GC垃圾回收。StringTable statistics中显示了桶数量为60013为默认数量。字符串常量池中存储了10563(应该是11783的)个对象。说明进行了回收操作。

当堆内存中内存不够时,会进行GC垃圾回收操作,字符串常量池中会释放一些没有被引用的字符串对象。

StringTable调优

1、能够调优的原理从StringTable的结构看起。StringTable的实现原理是HashTable(Hash表加链表形式实现)。那么Hash表只要足够大那么地址冲突的发生概率就会变小,减少查询链表的时间。

更改Hash表大小时间上就是更改桶大小:-XX:StringTableSize=桶个数(最少设置为 1009 以上)

2、对于重复创建的字符串,使用intern()入池,而不是创建出String对象存放在堆内存中,这样可以让相同的字符串对象引用一个相同的地址。

总结

常量池中的字符串仅是符号,第一次用到时才会变为对象

利用串池的机制,来避免重复创建字符串对象

字符串变量拼接的原理是StringBuilder(1.8之后)

字符串常量拼接的原理是编译期优化

可以使用intern方法,主动将串池中还没有的字符串对象放入串池

文章来源:https://blog.csdn.net/zmbwcx/article/details/135417794
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。