> 文章列表 > JVM中StringTable详解

JVM中StringTable详解

JVM中StringTable详解

文章目录

  • 1. String
    • 1.1 String的基本特性
    • 1.2 String 的存储结构变化
    • 1.3 基本特性
  • 2. String的内存分配
  • 3. String基本操作
  • 4. 字符串变量拼接
  • 5. intern()的使用
  • 6. StringTable的垃圾回收机制
  • 7. stringtable的性能调优

1. String

1.1 String的基本特性

  • String:字符串,使用一对""引起来表示
  • String声明为final的,不可被继承
  • String实现了Serializable接口:表示字符串是支持序列化的。
  • String实现了Comparable接口:表示String可以比较大小
  • StringJDK8及以前内部定义了final char[] value用于存储字符串数据。JDK9时改为byte[]

1.2 String 的存储结构变化

String在JDK9中存储结构变更:官方网站说明:JEP 254: Compact Strings (java.net)
对官方中的内容说明进行翻译:
动机

目前String类的实现将字符存储在一个char数组中,每个字符使用两个字节(16位)。从许多不同的应用中收集到的数据表明,字符串是堆使用的主要组成部分,此外,大多数字符串对象只包含Latin-1字符。这些字符只需要一个字节的存储空间,因此这些字符串对象的内部字符数组中有一半的空间没有被使用。
说明
我们建议将String类的内部表示方法从UTF-16字符数组改为字节数组加编码标志域。新的String类将根据字符串的内容,以ISO-8859-1/Latin-1(每个字符一个字节)或UTF-16(每个字符两个字节)的方式存储字符编码。编码标志将表明使用的是哪种编码。

与字符串相关的类,如AbstractStringBuilderStringBuilderStringBuffer将被更新以使用相同的表示方法,HotSpot VM的内在字符串操作也是如此

这纯粹是一个实现上的变化,对现有的公共接口没有变化。目前没有计划增加任何新的公共API或其他接口。

迄今为止所做的原型设计工作证实了内存占用的预期减少,GC活动的大幅减少,以及在某些角落情况下的轻微性能倒退。

结论:String再也不用char[] 来存储了,改成了byte [] 加上编码标记,节约了一些空间

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {@Stableprivate final byte[] value;
}

1.3 基本特性

String:代表不可变的字符序列。简称:不可变性

● 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
● 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
● 当调用stringreplace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。

通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中

字符串常量池是不会存储相同内容的字符串的

StringString Pool是一个固定大小的Hashtable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降。

使用-XX:StringTablesize可设置StringTable的长度

● 在JDK6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTablesize设置没有要求
● 在JDK7中,StringTable的长度默认值是60013StringTablesize设置没有要求
● 在JDK8中,设置StringTable长度的话,1009是可以设置的最小值

2. String的内存分配

在Java语言中有8种基本的数据类型和比较特殊的数据类型String。这些类型为了使他们运行的更加的快速,更节省内存空间,都提供了一种叫做常量池的概念。
但是,8种基本数据类型都是系统协调的,String数据类型的常量池比较特殊,她的使用方法有两种.

  • 如果是直接使用双引号声明出来的String对象的会直接存储在常量池中。
  • 如果不是双引号声明的String对象可以通过String提供的intern()方法。

下面是StringTable的在JVM内存结构划分中的位置:

JDK1.6及之前 有永久代,字符串常量池、静态变量存放在永久代上
JDK1.7 有永久代,但已经逐步”去永久代“,字符串常量池、静态变量以及保存在堆中了。
JDK1.8及之后 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆中

具体的文章内容看:JVM方法区在JDK6、JDK7、JDK8变化

StringTable为什么要调整地方?


在JDK 7中,内部字符串不再分配在Java堆的永久代中,而是分配在Java堆的主要部分(称为年轻代和老年代),与应用程序创建的其他对象一起。这种变化将导致更多的数据驻留在主Java堆中,而更少的数据在永久代中,因此可能需要调整堆的大小。大多数应用程序将看到由于这一变化而导致的堆使用的相对较小的差异,但加载许多类或大量使用String.intern()方法的大型应用程序将看到更明显的差异

3. String基本操作

@Test
public void test1() {System.out.print1n("1"); //2321System.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("10"); //2330System.out.println("1"); //2321System.out.println("2"); //2322System.out.println("3");System.out.println("4");System.out.println("5");System.out.print1n("6");System.out.print1n("7");System.out.println("8");System.out.println("9");System.out.println("10");//2330
}

Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String类实例。

小结:字面量创建字符串对象是懒惰的,即只有执行到相应代码才会创建相应对象(和一般的类不同)并放入串池中。如果串池中已经有了,就直接使用串池中的对象(让引用变量指向已有的对象)。串池中的对象只会存在一份,也就是只会有一个“a”对象

我们对代码来进行举例说明:

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

假设有上面的代码:通过: javap -v Demo1_22 进行反编译。上面代码中,通过字符串字面量的方式创建了几个 String。对于变量s1, s2, s3,都知道被放在了栈中,后面的字符串存储在哪里?

Code:stack=1, locals=4, args_size=10: ldc           #2                  // String a2: astore_13: ldc           #3                  // String b5: astore_26: ldc           #4                  // String ab8: astore_39: return

通常这里的#2 就是a,当类加载的时候,常量池中的信息会加载到运行时常量池中,此时的a,b,ab都还是符号,没有变成Java对象。当运行此方法,执行到对应的代码时,才会将符号a变成“a”字符串对象,并将对象放入StringTable中。 需要注意的是,普通的Java对象在类加载的时候就会生成并放入堆中,而这种方式生成的String不同,只有当执行到新建String的代码时才会生成字符串对象。

StringTable是一个哈希表,长度固定,“a”就是哈希表的key

一开始的时候,会根据“a”到串池中找其对象,一开始是没有的,所以就会创建一个并放入串池中。串池为 [“a”]
执行到指令ldc #3时,会和上面一样,生成一个“b”对象并放入串池中,串池变为[“a”, “b”]
后面会生成“ab”对象并放入串池中。串池变为[“a”, “b”, “ab”]

4. 字符串变量拼接

● 常量与常量的拼接结果在常量池,原理是编译期优化
● 常量池中不会存在相同内容的变量
● 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
● 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址

在熟悉了上面关于s = ''这种方式的具体原理之后: 看看关于str = s1 + s2 的时候

public class Demo1_22 {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[]);Code:0: ldc           #2                  // String a2: astore_13: ldc           #3                  // String b5: astore_26: ldc           #4                  // String ab8: astore_39: new           #5                  // class java/lang/StringBuilder12: dup13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V16: aload_117: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;20: aload_221: 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        429: return

有关于s4 = s1 + s2

前面的指令我们已经很熟悉,观察行号为9的指令,这里是个new。这就说明s4的创建方式和s1、s2、s3不同,它是在堆里新建了一个对象,前面根据字面量创建的则是在串池中生成了字符串对象。

观察行号9的指令后面的注释,可以知道这里是new了一个StringBuilder对象。

接着看1721,可以发现“s1 + s2”的方式是通过StringBuilder对象调用append方法实现的。

最后看24,最后是调用了toString方法生成了新的字符串对象。

@Overridepublic String toString() {// Create a copy, don't share the arrayreturn new String(value, 0, count);}

这里将StringBuilder 中的值在new 了一个string的对象。

总结:以上分析就想要说明:即当两个字符串变量拼接时,jvm会创建一个StringBuilder对象,利用其append方法实现变量的拼接。最后再通过其toString方法生成一个新的String对象。   最后我们看输出结果,发现s3不等于s4,这说明s3指向串池中的“ab”对象,s4指向堆中的“ab”对象。这是两个不同的对象。
下面看几段代码就可以很好区分了,具体的还是要自己编译看看背后的逻辑:

  public static void test1() {// 都是常量,前端编译期会进行代码优化// 通过idea直接看对应的反编译的class文件,会显示 String s1 = "abc"; 说明做了代码优化String s1 = "a" + "b" + "c";  String s2 = "abc"; // true,有上述可知,s1和s2实际上指向字符串常量池中的同一个值System.out.println(s1 == s2); }

[举例子2]

public static void test5() {String s1 = "javaEE";String s2 = "hadoop";String s3 = "javaEEhadoop";String s4 = "javaEE" + "hadoop";    String s5 = s1 + "hadoop";String s6 = "javaEE" + s2;String s7 = s1 + s2;System.out.println(s3 == s4); // true 编译期优化System.out.println(s3 == s5); // false s1是变量,不能编译期优化System.out.println(s3 == s6); // false s2是变量,不能编译期优化System.out.println(s3 == s7); // false s1、s2都是变量System.out.println(s5 == s6); // false s5、s6 不同的对象实例System.out.println(s5 == s7); // false s5、s7 不同的对象实例System.out.println(s6 == s7); // false s6、s7 不同的对象实例String s8 = s6.intern();System.out.println(s3 == s8); // true intern之后,s8和s3一样,指向字符串常量池中的"javaEEhadoop"
}

[举例子3]

public void test6(){String s0 = "beijing";String s1 = "bei";String s2 = "jing";String s3 = s1 + s2;System.out.println(s0 == s3); // false s3指向对象实例,s0指向字符串常量池中的"beijing"String s7 = "shanxi";final String s4 = "shan";final String s5 = "xi";String s6 = s4 + s5;System.out.println(s6 == s7); // true s4和s5是final修饰的,编译期就能确定s6的值了
}

● 不使用final修饰,即为变量。如s3行的s1和s2,会通过new StringBuilder进行拼接
● 使用final修饰,即为常量。会在编译器进行代码优化。在实际开发中,能够使用final的,尽量使用

5. intern()的使用

String中的intern的使用
对于下面这段代码进行分析:

 String s = new String("a") + new String("b");String s2 = s.intern(); System.out.println(s1 == "ab"); // trueSystem.out.println(s == "ab")// false

首先来进行反编译

 Code:stack=4, locals=3, args_size=10: new           #2                  // class java/lang/StringBuilder3: dup4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V7: new           #4                  // class java/lang/String10: dup11: ldc           #5                  // String a13: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;19: new           #4                  // class java/lang/String22: dup

对反编译的结果来进行解读:

开始一看到+号,首先new一个StringBuilder的对象
看到new String("a") 之后,先将a放入到StringTable中,之后在堆中创一个String的对象。两个在通过append来进行拼接,通过StringBuildertoString()方法获得一个新的String对象。
此时StringTable中有元素a, b, c ,堆中新增了元素 new String('a'), new String('b'), new String('ab'), new StringBuilder()

对于intern方法的作用就是在尝试把堆中对象放入串池中。如果串池中已有,会返回串池中的对象。并且s调用intern方法后依旧指向堆中的对象。如果串池中没有,会在串池中创建一个“ab”对象并返回,并且会让s指向串池中的“ab”对象。
但是,注意的是:上面是JDK1.7之后的做法,``JDK1.6,当一个String调用intern`方法时,如果串池中没有,会将堆中的字符串对象复制一份放到串池中,最后返回StringTable中刚加入的对象。并不会将s指向串池中的对象(如果没有话)。

面试题

public static void main(String[] args) {String s1 = "a";String s2 = "b";String s3 = "a" + "b";String s4 = s1 + s2;String s5 = "ab";String s6 = s4.intern();// 问System.out.println(s3 == s4);System.out.println(s3 == s5);System.out.println(s3 == s6);String x2 = new String("c") + new String("d"); // new String("cd");x2.intern(); // 1String x1 = "cd"; // 2// 如果是jdk1.6的话,会有什么样的不同System.out.println(x1 == x2);}
}

对于intern分析步骤:先看一下当前字符串在StringTable中是否存在,看jdk版本决定是否要拷贝一份到StringTable中。


使用总结:
JDK1.6中,将这个字符串对象尝试放入串池。

● 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
● 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址

JDK1.7起,将这个字符串对象尝试放入串池。

● 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
● 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址


6. StringTable的垃圾回收机制

掩饰垃圾回收:
-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
首先来看一段什么也没有做的代码:

public static void main(String[] args) {int i = 0;try{}catch (Throwable e) {e.printStackTrace();}finally {System.out.println(i);}}
0
HeapPSYoungGen      total 2560K, used 1484K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)eden space 2048K, 72% used [0x00000007bfd00000,0x00000007bfe73248,0x00000007bff00000)from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)to   space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)ParOldGen       total 7168K, used 0K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)object space 7168K, 0% used [0x00000007bf600000,0x00000007bf600000,0x00000007bfd00000)Metaspace       used 3157K, capacity 4496K, committed 4864K, reserved 1056768Kclass space    used 347K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     12378 =    297072 bytes, avg  24.000
Number of literals      :     12378 =    476656 bytes, avg  38.508
Total footprint         :           =    933816 bytes
Average bucket size     :     0.619
Variance of bucket size :     0.621
Std. dev. of bucket size:     0.788
Maximum bucket size     :         6
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :       900 =     21600 bytes, avg  24.000
Number of literals      :       900 =     60736 bytes, avg  67.484
Total footprint         :           =    562440 bytes
Average bucket size     :     0.015
Variance of bucket size :     0.015
Std. dev. of bucket size:     0.122
Maximum bucket size     :         2

对于

StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :       900 =     21600 bytes, avg  24.000
Number of literals      :       900 =     60736 bytes, avg  67.484
Total footprint         :           =    562440 bytes

Number of buckets 代表的是stringtable中数组的长度,而number of entries代表的是键值对(也就是串池中string的个数)的个数。

修改原来的代码,产生大量的没有引用的字符对象放到stringtable中。

public static void main(String[] args) {int i = 0;try{for(int j = 0 ; j < 100000; j ++){String.valueOf(j).intern();i++;}}catch (Throwable e) {e.printStackTrace();}finally {System.out.println(i);}}

直接产生了GC信息

[GC (Allocation Failure) [PSYoungGen: 2048K->512K(2560K)] 2048K->536K(9728K), 0.0023053 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2560K->496K(2560K)] 2584K->520K(9728K), 0.0012180 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2544K->512K(2560K)] 2568K->552K(9728K), 0.0011401 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
100000

7. stringtable的性能调优

  • 调整桶的个数
    由于StringTable的底层是HashTable,所以是有桶的概念在的,也就是数组的长度 。可以通过参数来设置: -XX:StringTableSize=1009
  • 考虑将字符串对象是否入池
    如果在系统中需要有大量的字符串可以使用intern的方法将字符串入池,而不是直接使用默认放入串池的方法。

最难不过坚持