String的基本特性
- String: 字符串,使用一对 " "引起来表示,
String s1 = "doubily"; // 字面量的含义
String s2 = new String("doubily");
- String 的声明为 final的,不可被继承
- String 实现了 Serializable 接口: 表示字符串是支持序列化的,实现了 Comparable接口: 表示 String 可以比较大小
- String 1.9 再也不用 char[] 来存储了,改为了 byte[] 加上编码标记,节约了一些空间
- String 代表不可变的字符序列,当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的 value 进行赋值,当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的 value 赋值。 当调用 String 的 replace() 方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的 value 进行赋值
public class StringTest1 {
public static void main(String[] args) {
test1();
test2();
test3();
}
public static void test1() {
String s1 = "abc"; // 字面量定义的方式,"abc" 存储在字符串常量池
String s2 = "abc";
//s1 = "hello";
System.out.println(s1 == s2);
System.out.println(s1);
System.out.println(s2);
}
public static void test2() {
String s1 = "abc";
String s2 = "abc";
s2 += "def";
System.out.println(s1);
System.out.println(s2);
}
public static void test3() {
String s1 = "abc";
String s2 = s1.replace('a','m');
System.out.println(s1);
System.out.println(s2);
}
}
- 通过字面量的方式(区别于 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是可设置的最小值
String的内存分配
- 在 Java 语言中有8中基本数据类型和一些比较特殊的类型 String。这些类型为了使它们运行过程中速度更快、更节省内存,都提供了一种常量池的概念
- 8种基本数据类型的常量池都是系统协调的,String 常量池比较特殊
1. 直接使用双引号声明出来的 String 对象会直接存储在常量池种
String info = "doubily";
2. 如果不是使用双引号声明的 String 对象,可以使用 String 提供的 Intern() 方法
- java 6 以及之前,字符串常量池存放在永久代
- java 7及以后, 将字符串常量池调整到 java 堆内
/**
* 设置永久代大小
* jdk6: --XX:PermSize=6m --XX:MaxPermSize=6m -Xms6m -Xmx6m
* jdk8: MetaspaceSize=6m --XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m
*/
public class StringTest3 {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
short i = 0;
while (true){
set.add(String.valueOf(i++).intern());
}
}
}
StringTable 为什么要调整:
- permSize 默认比较小
- 永久代垃圾回收频率低
字符串拼接操作
- 常量与常量的拼接结果在常量池,原理是编译期优化
- 常量池种不会存在相同内容的常量
- 只要其中有一个是变量,结果就在堆中,变量拼接的原理是StringBuilder
- 如果拼接的结果调用 intern() 方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
public class StringTest5 {
public static void main(String[] args) {
test1();
}
public static void test1(){
String s1 = "a" + "b" + "c";
String s2 = "abc"; // 一定是放在字符串常量池中,将此地址
System.out.println(s1 == s2); // true 编译期优化
System.out.println(s1.equals(s2));
}
public static void test2(){
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";
// 如果拼接符号的前后出现了变量,则相当于在堆空间中 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);
System.out.println(s3 == s6);
System.out.println(s3 == s7);
System.out.println(s5 == s6);
System.out.println(s5 == s7);
System.out.println(s6 == s7);
// intern 判断字符串常量池中是否存在值,如果存在,则返回常量池中的地址,如果不存在,则在常量池中加载一份,并返回对象地址
String s8 = s6.intern();
System.out.println(s3 == s8);
}
public static void test3(){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
/**
* StringBuilder s = new StringBuilder();
* s.append("a");
* s.append("b");
* s.toString(); 。类似与 new String()
* 在jdk5.0 之后使用 StringBuilder, 在jdk5.0 之后使用 StringBuffer
*/
String s4 = s1 + s2;
System.out.println(s3 == s4);
}
public static void test4(){
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
// 如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非 StringBuilder 方法
// 针对 final修饰类、方法、基本数据类型、引用数据类型的量的结构,能使用 final 建议使用上
String s4 = s1 + s2;
System.out.println(s3 == s4);
}
public void method1(int highLevel){
String src = "";
for (int i = 0; i < highLevel; i++){
src = src + "a"; // 每次循环都会创建一个StringBuilder, string
}
}
/**
* 通过 StringBuilder 的 append() 的方式添加字符串的效率要远高于使用 String 的字符串拼接方式
* StringBuilder 的 append() 的方法自始至终只创建一个 StringBuilder 对象
* String 字符串拼接方式:创建多个 StringBuilder 和 String 对象
* 使用 String 的字符串拼接方式: 内存中由于创建了较多的 StringBuilder 和 String ,占用大量内存
* @param highLevel
*/
public void method2(int highLevel){
StringBuilder sb = new StringBuilder();
for (int i = 0; i < highLevel; i++){
sb.append("a"); // 每次循环都会创建一个StringBuilder, string
}
}
}
intern() 的使用
- 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法: intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。比如: String myInfo = new String("doubily").intern();
- 在任意字符串上调用 String.intern 方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串完全相同。因此 ("a" + "b" +"c").intern() == "abc" 为 true;
- Interned String 就是确保字符串在内部里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)
- jdk1.6 中,如果串池中有,则并不会放入。返回已有的串池中的对象地址,如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址
- jdk1.7起,如果串池中有,则并不会放入。返回已有的串池中的对象地址,如果没有,则会把对象引用地址复制一份,放入串中,并返回串池中的引用地址
/**
* String s = "doubily"; // 字面量定义的方式
* String s = new String("doubily").intern();
*/
public class StringIntern {
public static void main(String[] args) {
String s = new String("1"); // 堆空间的对象
s.intern(); // 调用此方法之前,字符串常量池已经存在 1
String s2 = "1";
System.out.println(s == s2); // jdk6 false jdk7/8: false s 堆空间的引用地址 s2 常量池地址
String s3 = new String("1") + new String("1"); // s3 变量记录的地址为: new String("11") 字符串常量池中不存在 11
s3.intern(); // 在字符串常量池中生成 "11", jdk6: 创建了一个新的对象 "11",也就有了新的地址 jdk7: 常量池记录了创建 "11" 的地址
String s4 = "11"; // 使用的是上一行代码执行时,在常量池中生成的 "11" 的地址
System.out.println(s3 == s4);// jdk6 false jdk7/8: true
String s5 = new String("1") + new String("1"); // String("11")
String s6 = "11"; // 常量池中生成对象 "11"
s5.intern();
String s7 = s5.intern();
System.out.println(s5 == s6);// jdk6 false jdk7/8: false
System.out.println(s7 == s6); // true
}
}
new String("ab") 会创建几个对象:
public class StringNewTest {
public static void main(String[] args) {
/**
* 2 个对象,看字节码
* 0 new #2 <java/lang/String> 一对象是通过 new 在堆空间创建的
* 3 dup
* 4 ldc #3 <ab> 字符串常量池中的对象 ldc
* 6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
* 9 astore_1
* 10 return
*/
// String str = new String("ab");
/**
* 0 new #2 <java/lang/StringBuilder> new StringBuilder()
* 3 dup
* 4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
* 7 new #4 <java/lang/String> new String("a")
* 10 dup
* 11 ldc #5 <a> 常量池中的 "a"
* 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> new String("b")
* 22 dup
* 23 ldc #8 <b> 常量池中的 "b"
* 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;> // new String("ab") 字符串常量池中不存在 ab
* 34 astore_1
* 35 return
*/
String str = new String("a") + new String("b");
}
}
熟悉1:
Intern() 空间测试:
/**
* 对于字符串尤其存在很多重复字符串时,使用 intern() 可以节省很多空间
*/
public class StringExer2 {
static final int MAX_COUNT = 1000 * 10000;
static final String[] arr = new String[MAX_COUNT];
public static void main(String[] args) throws InterruptedException {
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++){
//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));
Thread.sleep(10000000);
}
}
StringTable 的垃圾回收
/**
* 垃圾回收 -Xms15m -Xmx15m -XX:+PrintGCDetails -XX:+PrintStringTableStatistics
*/
public class StringGCTest {
public static void main(String[] args) {
for (int j = 0; j < 1000000; j++) {
String.valueOf(j).intern();
}
}
}
G1 中的 String 去重操作
背景:
- 堆存活数据集合里面 String 对象占了 25%
- 堆存活数据集合里面重复的 String 队友有 13.5%
- String 对象的平均长度是 45
实现:
- 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的 String 对象
- 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的 String 对象
- 使用一个 hashtable 来记录所有被 String 对象使用的不重复的 char 数组。当去重的时候,会查这个 hashtable,来看堆上是否已经存在一个一模一样的 char 数组
- 如果存在, String 对象会被调整引用那个数组,释放堆原来的数组的引用,最终被垃圾收集器回收掉
- 如果查找失败,char 数组会插入到 hashtable,这样以后的时候就可以共享这个数组了
命令行:
- UseStringDeduplication(bool): 开启 String 去重,默认是不开启的,需要手动开启
- PrintStringDeduplicationStatistics(bool): 打印详细的去重统计信息
- StringDeduplicationAgeThreshold(uintx): 达到这个年龄的 String 对象被认为是去重的候选对象