JVM-033-StringTable-String的概述和基本操作
String在JDk9中存储结构的变更
String在jdk8及以前内部定义了final char value[]用于存储字符串数据。JDK9时改为final byte[] value
官方文档:http://openjdk.java.net/jeps/254
原因:
String类的当前实现将字符存储在char数组中,每个字符使用两个字节(16位)。
从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分,而且大多数字符串对象只包含拉丁字符(Latin-1)。这些字符只需要一个字节的存储空间,因此这些字符串对象的内部char数组中有一半的空间将不会使用,产生了大量浪费。
之前 String 类使用 UTF-16编码 的 char[] 数组存储,现在改为 byte[] 数组 外加一个编码标识存储。该编码表示如果你的字符集编码是ISO-8859-1或者Latin-1,那么只需要一个字节存。如果你是其它字符集编码,比如UTF-8,你仍然用两个字节存
结论:String再也不用char[] 来存储了,改成了byte [] 加上编码标记,节约了一些空间
同时基于String的数据结构,例如StringBuffer和StringBuilder也同样做了修改
1
2
3
4// 之前
private final char value[];
// 之后
private final byte[] value
String 的基本特性
定义
String:字符串,使用一对 “” 引起来表示
1
2String s1 = "buubiu" ; // 字面量的定义方式
String s2 = new String("buubiu"); // new 对象的方式
不可继承
- String被声明为final的,不可被继承
支持序列化
- String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示String可以比较大小
不可变性
String:代表不可变的字符序列。简称:不可变性。
字符串常量池不允许存储相同的内容,所以 s1 == s2 为 true
1
2
3
4
5
6
7public void test1() {
String s1 = "abc";//字面量定义的方式
String s2 = "abc";
System.out.println(s1 == s2);//判断地址:true
System.out.println(s1);//abc
System.out.println(s2);//abc
}当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值,所以 s1 == s2 为 false
1
2
3
4
5
6
7
8public void test1() {
String s1 = "abc";//字面量定义的方式
String s2 = "abc";
s1 = "hello";
System.out.println(s1 == s2);//判断地址:false
System.out.println(s1);//hello
System.out.println(s2);//abc
}当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值,所以 s2 为 abcdef
1
2
3
4
5
6
7
8public void test1() {
String s1 = "abc";//字面量定义的方式
String s2 = "abc";
s2 += "def";
System.out.println(s1 == s2);//判断地址:false
System.out.println(s1);//abc
System.out.println(s2);//abcdef
}当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值,所以 s1 不会改变,s2 为 mbc
1
2
3
4
5
6public void test3() {
String s1 = "abc";//字面量定义的方式
String s2 = s1.replace('a', 'm');
System.out.println(s1);//abc
System.out.println(s2);//mbc
}笔试题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class StringExer {
String str = new String("good");
char[] ch = {'t', 'e', 's', 't'};
public void change(String str, char ch[]) {
str = "test ok";
ch[0] = 'b';
}
public static void main(String[] args) {
StringExer ex = new StringExer();
ex.change(ex.str, ex.ch);
System.out.println(ex.str);//good
System.out.println(ex.ch);//best
}
}
总结:通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
String 的底层结构
字符串常量池是不会存储相同内容的字符串的
- String的String Pool(字符串常量池)是一个固定大小的Hashtable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern()方法时性能会大幅下降。
- 使用
-XX:StringTablesize=xx
可设置StringTable的长度 - 在JDK6中StringTable是固定的,就是
1009
的长度,所以如果常量池中的字符串过多就会导致效率下降很快,StringTablesize设置没有要求 - 在JDK7中,StringTable的长度默认值是
60013
,StringTablesize设置没有要求 - JDK8 开始,设置 StringTablbe 的长度的话,1009 是可设置的最小值。
不同 StringTable 长度下,程序的性能
1 | /** |
1 | public class StringTest2 { |
- -XX:StringTableSize=1009 :程序耗时 143ms
- -XX:StringTableSize=100009 :程序耗时 47ms
String 的内存分配
- 在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
- 常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种:
- 直接使用双引号声明出来的String对象会直接存储在常量池中。比如:
String info="buubiu";
- 如果不是用双引号声明的String对象,可以使用String提供的intern()方法。
- 直接使用双引号声明出来的String对象会直接存储在常量池中。比如:
内存分配的演进:
Java 6及以前,字符串常量池存放在永久代
Java 7中 Oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内
所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用String.intern()。
Java8元空间,字符串常量在堆
StringTable 为什么要调整?
官方文档:https://www.oracle.com/java/technologies/javase/jdk7-relnotes.html#jdk7changes
- 为什么要调整位置?
- 永久代的默认空间大小比较小
- 永久代垃圾回收频率低,大量的字符串无法及时回收,容易进行Full GC产生STW或者容易产生OOM:PermGen Space
- 堆中空间足够大,字符串可被及时回收
- 在JDK 7中,interned字符串不再在Java堆的永久代中分配,而是在Java堆的主要部分(称为年轻代和年老代)中分配,与应用程序创建的其他对象一起分配。
- 此更改将导致驻留在主Java堆中的数据更多,驻留在永久生成中的数据更少,因此可能需要调整堆大小。
代码示例
1 | /** |
输出结果:
jdk6:
1 | Exception in thread "main" java.lang.OutOfMemoryError: PermGen space |
jdk8:
1 | Exception in thread "main" java.lang.OutOfMemoryError: Java heap space |
String 的基本操作
举例一
Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String类实例。
1 | public class StringTest4 { |
分析字符串常量池的变化
程序启动时已经加载了 2293 个字符串常量
加载了一个换行符(println),所以多了一个
加载了字符串常量 “1”~“9”
加载字符串常量 “10”
之后的字符串”1” 到 “10”不会再次加载
举例二
1 | //官方示例代码 |
分析运行时内存(foo() 方法是实例方法,其实图中少了一个 this 局部变量)
在第7行创建了一个字符串 str
,它存在堆空间的字符串常量池中,并且在foo()栈空间创建了一个引用来指向字符串常量池中。
JVM-033-StringTable-String的概述和基本操作