1. StringTable
不同JDK版本StringTable 位置
1.1 String 的基本特性
- String 字符串,使用一对 “” 引起来表示。
- String 类声明为 final 的,不可被继承
- String: 实现了Serializable 接口:表示字符串是支持序列化的, 实现了Comparable 接口:表示String可以比较大小。
- String在JDK 8及以前内部定义了final char[] value用于存储字符串数据。JDK 9时改为了byte[]。(http://openjdk.java.net/jeps/254)
结论:String 再也不用 char[] 来存储了,改成了 byte[] 加上编码标记,节约了一些空间。
public final class String implments java.io.Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value;
// ...
}
StringBuffer、StringBuilder 现状:
String-related classes such as AbstractStringBuilder, StringBuilder and StringBuffer will be updated to use the same representation, as will the HotSpot VM’s intrinsic (固有的/内置的) string operations.
-
String:代表不可变的字符序列(不可变性)
- 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
- 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
- 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不饿能使用原有的value进行赋值。
-
通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
-
字符串常量池中是不会存储相同内容的字符串的。
- String 的String Pool是一个固定大小的Hashtable,默认 值大小长度是1009。如果放进 String Pool 的String非常多,就会造成 Hash 冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用 String.intern() 时性能会大幅下降。
- 使用
-XX:StringTableSize
可设置StringTable的长度。 - 在 JDK6 中 StringTable 是固定的,就是 1009 的长度,所以如果常量池的字符串过多就会导致效率下降很快。StringTableSize设置没有要求。
- 在 JDK7 中,StringTable的长度默认值是 60013
- 从 JDK8 开始,设置StringTable长度时,1009是可设置的最小值。
1.2 String 的内存分配
- 在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
- 常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。
- 直接使用双引号声明出来的String对象会直接存储在常量池中。
- 比如:String info = “ABC”;
- 如果不是用双引号声明的String对象,可以使用String提供的intern() 方法。
- 直接使用双引号声明出来的String对象会直接存储在常量池中。
- Java 6及以前,字符串常量池存放在永久代。
- Java 7中 Oracle 的工程师对字符串池的逻辑做了很大的改变,即将 字符串常量池的位置调整到了Java堆内。
- 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
- 字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在 Java 7 中使用 String.intern()。
- Java 8 方法区的实现由永久代改为元空间后,字符串常量存储在堆。
StringTable为什么要调整?
- PermSize 默认比较小
- 永久代垃圾回收频率低
官网:
https://www.oracle.com/technetwork/java/javase/jdk7-relnotes-418459.html#jdk7changes
1.3 String 的基本操作
Java 语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String类实例。
1.4 字符串拼接操作
- 常量与常量的拼接结果在常量池,原理是编译期优化。
- 常量池中不会存在相同内容的常量。
- 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder。
- 如果拼接的结果调用 intern() 方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
测试代码 - 编译期优化:
@Test
public void test1() {
String s1 = "a" + "b" + "c"; //编译期优化:等同"abc"
String s2 = "abc";
/**
* 最终.java 编译成.class, 再执行.class
* String s1 = "abc"
* String s2 = "abc";
*/
System.out.println(s1 == s2); //true
System.out.println(s1.equals(s2)); //true
}
测试代码 - 拼接前后出现变量:
@Test
public void test2() {
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop"; //编译期优化:等同"javaEEhadoop"
// 如果拼接符号前后出现变量,则相当于在堆空间中new String(),具体内容为拼接后的结果。
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4); //true
System.out.println(s3 == s5); //false
System.out.println(s3 == s6); //false
System.out.println(s3 == s7); //false
System.out.println(s5 == s6); //false
System.out.println(s5 == s7); //false
System.out.println(s6 == s7); //false
// intern():判断字符串常量池中是否存在 javaEEhadoop 的值:
// 如果存在:则返回常量池中这个字符串的地址。
// 如果不存在:则在常量池中加载一份 javaEEhadoop 并返回此对象的地址。
String s8 = s6.intern();
System.out.println(s3 == s8); //true
}
测试代码 - 拼接前后出现变量:
@Test
public void test3() {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
/**
* 如下的 s1 + s2 的执行细节(连接符号两边出现 变量,s是临时变量):
* 1、StringBuilder s = new StringBuilder();
* 2、s.append(s1);
* 3、s.append(s2);
* 4、s.toString(); ---> (堆)约等于 new String("ab")
*
* 补充:在JDK 5.0 之后使用的是StringBuilder,在JDK 5.0 之前使用的是StringBuffer
*/
String s4 = s1 + s2;
System.out.println(s3 == s4); //false
}
测试代码 - 拼接前后为常量:
@Test
public void test4() {
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
//连接符号两边仍为 常量
System.out.println(s3 == s4); //true
}
字符串拼接操作不一定使用的是StringBuilder:
- 如果拼接符号左右两边都是字符串常量("")或常量引用(final),则仍然使用编译期优化
针对于final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上final的时候建议使用上。
测试代码 - 拼接 & append 效率比对:
public void method1() {
String str = "";
for (int i = 0; i < 100000; i++) {
str = str + "a"; //每次循环都会创建一个StringBuidler、String
}
}
public void method2() {
// 只需要创建一个StringBuilder
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 100000; i++) {
builder.append("a");
}
}
@Test
public void test() {
long startTime = System.currentTimeMillis();
//method1();
//method2();
long endTime = System.currentTimeMillis();
System.out.println("消耗时间:" + (endTime - startTime));
}
调用 method1:
调用 method2:
通过StringBuilder的append()方法添加字符串的效率 远高于 使用String的字符串拼接方式。
对比:
- 创建对象问题:
- StringBuilder的append()方法:自始至终之仅创建一个对象
- String的字符串拼接方式:创建过多个 StringBuilder 和 String 对象
- String的字符串拼接方式:内存中由于创建过多个 StringBuilder 和 String 对象,内存占用过大,GC需花费额外时间
优化:
- 在实际开发中,如果基本确定前前后后添加的字符串长度不高于某个限定值highLevel,通过构造器为StringBuilder指定capacity容量highLevel(避免数组频繁扩容占用内存, StringBuilder s = new StringBuillder(highLevel);)
1.5 intern() 的使用
1.5.1 概述
intern():判断字符串常量池中是否存在 javaEEhadoop 的值:
- 存在:则返回常量池中这个字符串的地址。
- 不存在:则在常量池中加载一份 javaEEhadoop 并返回此对象的地址。
s.intern() == t.intern() 等价于 s.equals(t)
如果表示用双引号声明的String对象,可以使用String提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
// 比如:
String myInfo = new String("I love you").intern();
即,如果在任意字符串上调用String.intern方法,那么其返回结果所指向的类实例,必须和直接以常量形式出现的字符串实例完全相同。
因此,下列表达式的结果必定是true:
("a" + "b" + "c").intern() == "abc"
通俗点讲,Interned String 就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被放在字符串内部池(String Intern Pool)。
/**
* 如何保证变量s指向的是字符串常量池中的数据呢?
* 两种方式:
* 1、字面量声明方式:String s = "ABC";
* 2、intern()方法:String s = new String("ABC").intern();
* String s = new StringBuidler("ABC").toString().intern();
*/
题目:new String(“ab”)会创建几个对象?
- String对象有两个:
- 一个对象是通过new 关键字在堆空间创建
- 一个对象是常量池中的"ab"(字节码指令ldc)
扩展:new String(“a”) + new String(“b”)会创建几个对象?
- 对象1:new StringBuilder()
- 对象2:new String(“a”)
- 对象3:常量池中的"a"
- 对象4:new String(“b”)
- 对象5:常量池中的"b"
- 深入剖析:StringBuilder 的 toString():
- 对象6:new String(“ab”)
- 调用 toString(),在字符串常量池中,没有生成"ab"
1.5.2 举例
JDK 6 – vs – JDK 7/8
public class StringIntern1 {
public static void main(String[] args) {
/// 1、第一种情况
String s = new String("1");
s.intern(); // 调用此方法之前,字符串常量池中已经存在了"1"
String s2 = "1";
System.out.println(s == s2);
/**
* JDK 6 + 7/8 :
* false:
* s : new关键字在堆空间创建的地址
* s1: 字符串常量池中对象的地址
*/
/// 2、第二种情况
String s3 = new String("1") + new String("1"); //s3变量记录的地址为:new String("11")
// 执行完上一行代码后,字符串常量池不存在"11"
s3.intern(); // 在字符串常量池中生成"11":JDK6:创建了一个新的对象"11",也就有新的地址。JDK7:此时常量池中并没有创建"11",而是指向(记录)堆空间之前new的"11"的地址
String s4 = "11"; // s4变量记录的地址:上一行代码执行时,在常量池中生成的"11"的地址
System.out.println(s3 == s4);
}
}
JDK 6:false + false
JDK 7/8:false + true
JDK 8
public class StringIntern2 {
public static void main(String[] args) {
/// 3、第三种情况
String s3 = new String("1") + new String("1");
// 执行完上一行代码后,字符串常量池不存在"11"
String s4 = "11"; // 在字符串常量池中生成对象"11"
String s5 = s3.intern(); //常量池中已经生成对象"11",这一步仅可使s5持有常量池中地址
System.out.println(s3 == s4); //false
System.out.println(s5 == s4); //true
}
}
总结String的intern()的使用:
- JDK 1.6 中,将这个字符串对象尝试放入串池。
- 若串池中有,则并不会放入。返回已有的串池中的对象的地址。
- 若串池中没有,则会把 此对象复制一份,放入串池,并返回串池中的 对象地址。
- JDK 1.7 起,将这个字符串对象尝试放入串池。
- 如果串池中有,则并不会放入。返回已有的串池中的对象的地址。
- 若串池中没有,则会把 对象的引用地址复制一份,放入串池,并返回串池中的 引用地址。
1.5.3 intern() : 空间效率测试
/**
* 使用intern测试效率:空间角度
*/
public class StringInternTest {
static final int MAX_COUNT = 1000 * 1000;
static final String[] arr = new String[MAX_COUNT];
public static void main(String[] args) {
Integer[] data = new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
long start = System.currentTimeMillis();
for (int i = 0; i < MAX_COUNT; i++) {
/* 测试焦点:是否调用 intern() 方法 */
// arr[i] = new String(String.valueOf(data[i % data.length]));
// arr[i] = new String(String.valueOf(data[i % data.length])).intern();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.gc();
}
}
未使用intern():
使用intern():
对于程序中大量存在的字符串,尤其其中存在很多重复字符串时,使用 intern() 可以节省内存空间。
大型网站平台,需要内存中存储大量字符串(社交网站)。这是候如果字符串都调用 intern() 方法,就会明显降低内存大小。
1.6 StringTable的垃圾回收
/**
* String 的垃圾回收:
* -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
*
* 参数 -XX:+PrintStringTableStatistics : 打印字符串常量池统计信息
*/
public class StringGCTest {
public static void main(String[] args) {
// 依次调大参数,0 -> 100 -> 100000,观察是否发生GC
for (int i = 0; i < 100; i++) {
String.valueOf(i).intern();
}
}
}
0 (取消for循环):
100:
100000:
发生GC:
参数不足100000:
1.7 G1中的String去重操作
官网:
http://openjdk.java.net/jeps/192
去重:针对char型数组
String str1 = new String("hello");
String str2 = new String("hello");
-
背景:对许多Java应用(有大有小)做的测试得出以下结果:
- 堆存活数据集合里面String对象占了25%
- 堆存活数据集合里面重复的String对象有13.5%
- String对象平均长度是45
-
许多大规模的Java应用的瓶颈在于内存,测试表明,在这些类型的应用里面,Java堆中存活的数据集合差不多25%是String对象。更进一步,这里面差不多一半String对象是重复的,即:
-
string1.equals(string2) = true
-
堆上存在重复的String对象必然是一种内存浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的String对象进行去重,这样就能避免浪费内存。
-
-
实现:
- 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象。
- 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象。
- 使用一个hashtable来记录所有的被String对象使用的不重复的char数组。去重时,查询hashtable,看堆上是否已经存在一个一模一样的char数组。
- 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。
- 如果查找失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组了。
-
命令行选项
UseStringDeduplication (bool)
:开启String去重,默认不开启,需手动开启。PrintStringDeduplicationStatistics (bool)
:打印详细的去重统计信息StringDeduplicationAgeThreshold (uintx)
:达到这个年龄的String对象被认为是去重的候选对象
对每一个访问的对象都会检查是否是候选的要去重的String对象。
-
如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象。
-
使用一个hashtable来记录所有的被String对象使用的不重复的char数组。去重时,查询hashtable,看堆上是否已经存在一个一模一样的char数组。
-
如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。
-
如果查找失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组了。
-
命令行选项
UseStringDeduplication (bool)
:开启String去重,默认不开启,需手动开启。PrintStringDeduplicationStatistics (bool)
:打印详细的去重统计信息StringDeduplicationAgeThreshold (uintx)
:达到这个年龄的String对象被认为是去重的候选对象