String:字符串,使用一对 “” 引发来表示html
String s1 = "mogublog" ; // 字面量的定义方式 String s2 = new String("moxi"); // new 对象的方式java
String声明为final的,不可被继承 String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示String能够比较大小面试
string在jdk8及之前内部定义了final char[] value用于存储字符串数据。JDK9时改成byte[]数组
String类的jdk8以前的实现将字符存储在char数组中,每一个字符使用两个字节(16位)。缓存
可是从许多不一样的应用程序收集的数据代表,字符串是堆使用的主要组成部分,并且大多数字符串对象只包含拉丁字符。这些字符只须要一个字节的存储空间,所以这些字符串对象的内部char数组中有一半的空间将不会使用。 例如 存ab 这个字符,若是使用char 数组,就要分配两个字符的空间,即 四个字节, 可是ab 做为英文,自己只需占用两个字节便可数据结构
以前 String 类使用 UTF-16 的 char[] 数组存储,如今改成 byte[] 数组 外加一个编码标志位存储,该编码标志将指定 String 类中 byte[] 数组的编码方式app
结论:String不再用char[] 来存储了,改为了byte [] 加上编码标记,节约了一些空间 ,jvm
同时基于String的数据结构,例如StringBuffer和StringBuilder也一样作了修改ide
在Java语言中有8种基本数据类型和一种比较特殊的很是经常使用的类型String。这些类型为了使它们在运行过程当中速度更快、更节省内存,都提供了一种常量池的概念。函数
常量池就相似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。
代码演示:
class Memory { public static void main(String[] args) {//line 1 int i = 1;//line 2 Object obj = new Object();//line 3 Memory mem = new Memory();//line 4 mem.foo(obj);//line 5 }//line 9 private void foo(Object param) {//line 6 String str = param.toString();//line 7 System.out.println(str); }//line 8 }复制代码
示意图:
如上图所示,, 堆中的Object 对象在调用toString 方法后,将在String pool 中生成一个字符串对象,并返回给 顶层栈帧foo方法中的局部变量 str
String 内存分配的演进过程
JDK6 :
JDK7:
为何要调整String 常量池的位置呢
字符串常量池是不会存储相同内容的字符串的
字符串常量池中同一个字符串只存在一份
Java语言规范里要求彻底相同的字符串字面量,应该包含一样的Unicode字符序列(包含同一份码点序列的常量),而且必须是指向同一个String类实例。
代码示例:
public class StringTest4 { public static void main(String[] args) { System.out.println();//2330 System.out.println("1");//2331 个字符串 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("10");//2340 个字符串 //以下的字符串"1" 到 "10"不会再次加载 System.out.println("1");//2341 System.out.println("2");//2341 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");//2341 } }复制代码
在第一波开始,堆中的String 个数:
第一波结束时, 字符串个数,加了十个:
以后就再没增长过了:
测试不一样的 StringTable长度下,程序的性能
首先先建立一个拥有10W行不一样字符的文件,程序自行编写,这里就不演示了
/** * -XX:StringTableSize=1009 */ public class StringTest2 { public static void main(String[] args) { BufferedReader br = null; try { br = new BufferedReader(new FileReader("words.txt")); long start = System.currentTimeMillis(); String data; while ((data = br.readLine()) != null) { //若是字符串常量池中没有对应data的字符串的话,则在常量池中生成 data.intern(); } long end = System.currentTimeMillis(); System.out.println("花费的时间为:" + (end - start));//1009:143ms 100009:47ms } catch (IOException e) { e.printStackTrace(); } finally { if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } } }复制代码
代码一:
public void test1() { String s1 = "a" + "b" + "c";//编译期优化:等同于"abc" String s2 = "abc"; //"abc"必定是放在字符串常量池中,将此地址赋给s2 /* * 最终.java编译成.class,再执行.class * String s1 = "abc"; * String s2 = "abc" */ System.out.println(s1 == s2); //true System.out.println(s1.equals(s2)); //true }复制代码
结果打印的都是 true,无论是 地址值仍是 内容 都同样,看下面解析出的字节码指令,在指令0的位置从常量池中直接加载"abc"
0 ldc #2 <abc> 2 astore_1 3 ldc #2 <abc> 5 astore_2 6 getstatic #3 <java/lang/System.out> 9 aload_1 10 aload_2 11 if_acmpne 18 (+7) 14 iconst_1 15 goto 19 (+4) 18 iconst_0 19 invokevirtual #4 <java/io/PrintStream.println> 22 getstatic #3 <java/lang/System.out> 25 aload_1 26 aload_2 27 invokevirtual #5 <java/lang/String.equals> 30 invokevirtual #4 <java/io/PrintStream.println> 33 return复制代码
代码二:
public void test2(){ String s1 = "javaEE"; String s2 = "hadoop"; String s3 = "javaEEhadoop"; String s4 = "javaEE" + "hadoop";//编译期优化 //若是拼接符号的先后出现了变量,则至关于在堆空间中new String(),具体的内容为拼接的结果:javaEEhadoop String s5 = s1 + "hadoop"; String s6 = "javaEE" + s2; String s7 = s1 + s2; System.out.println(s3 == s4);//true //后面都是false 的缘由是,只要拼接时 有变量参与,都是至关于new 一个 不从常量池中共享 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的地址; //若是字符串常量池中不存在javaEEhadoop,则在常量池中加载一份javaEEhadoop,并返回此对象的地址。 String s8 = s6.intern(); //s6 虽然是堆中new出来的,可是调用intern方法返回出的是 常量池中的,因此和 s3 地址同样 System.out.println(s3 == s8);//true }复制代码
前面说到, 一旦拼接操做有变量引用的参与,而不是所有都是字面量的形式, 就会 在堆中 新new一个对象,这是为何呢?,下面用一个简单的代码解释
代码:
public void test3(){ String s1 = "a"; String s2 = "b"; String s3 = "ab"; String s4 = s1 + s2;//"ab" System.out.println(s3 == s4);//false }复制代码
字节码指令:
0 ldc #14 <a> //将字符a 从字符串常量池中获取 2 astore_1 //放入 局部变量索引为 1 的位置 (0为this) 3 ldc #15 <b> //将字符b从字符串常量池中获取 5 astore_2 //放入 局部变量索引为 2 的位置 6 ldc #16 <ab> //将字符ab 从字符串常量池中获取 8 astore_3 //放入 局部变量索引为 3 的位置 9 new #9 <java/lang/StringBuilder> // new 一个 StringBuilder对象,开辟空间 12 dup 13 invokespecial #10 <java/lang/StringBuilder.<init>> //初始化该StringBuilder对象 16 aload_1 // 加载 局部变量表中索引为1的值 ,也就是 a 17 invokevirtual #11 <java/lang/StringBuilder.append> //调用 StringBuilder对象 的 append 方法 20 aload_2 //加载 b 21 invokevirtual #11 <java/lang/StringBuilder.append> //一样append 24 invokevirtual #12 <java/lang/StringBuilder.toString> //最后调用StringBuilder对象的toString方法 27 astore 4 //放入 局部变量表 索引为 4的位置 29 getstatic #3 <java/lang/System.out> 32 aload_3 33 aload 4 35 if_acmpne 42 (+7) 38 iconst_1 39 goto 43 (+4) 42 iconst_0 43 invokevirtual #4 <java/io/PrintStream.println> 46 return复制代码
从上面的 字节码 逐行解释中看, 带有 变量的字符串拼接,其底层是使用 StringBuilder 对象
至关于以下代码:
StringBuilder s = new StringBuilder(); s.append("a") s.append("b") s.toString() // 约等于 new String("ab"),方法内就是 new String ,可是有些不一样 后面说复制代码
补充:在jdk5.0以后使用的是StringBuilder,在jdk5.0以前使用的是StringBuffer
是否是全部字符串变量的拼接操做都是使用的是StringBuilder
那确定是否是的,看下面这种状况:
public void test4(){ final String s1 = "a"; final String s2 = "b"; String s3 = "ab"; String s4 = s1 + s2; System.out.println(s3 == s4);//true }复制代码
字节码: 看 指令9的位置,为 s1+s2 的操做,并无使用 拼接的方式,而是在编译器就已经处理好了
0 ldc #14 <a> 2 astore_1 3 ldc #15 <b> 5 astore_2 6 ldc #16 <ab> 8 astore_3 9 ldc #16 <ab> 11 astore 4 13 getstatic #3 <java/lang/System.out> 16 aload_3 17 aload 4 19 if_acmpne 26 (+7) 22 iconst_1 23 goto 27 (+4) 26 iconst_0 27 invokevirtual #4 <java/io/PrintStream.println> 30 return复制代码
结论: 和直接使用字面量相同, 若是拼接字符变量都为 final ,都是能够在编译器就能够肯定结果的, 因此也被编译器优化了(在写代码时,能够写final的均可以写上,优化代码)
代码
public void test6(){ long start = System.currentTimeMillis(); //method1(100000);//4014 method2(100000);//7 long end = System.currentTimeMillis(); System.out.println("花费的时间为:" + (end - start)); } public void method1(int highLevel){ String src = ""; for(int i = 0;i < highLevel;i++){ src = src + "a";//每次循环都会建立一个StringBuilder、String } } public void method2(int highLevel){ //只须要建立一个StringBuilder StringBuilder src = new StringBuilder(); for (int i = 0; i < highLevel; i++) { src.append("a"); } }复制代码
咱们发现, 使用method1方法,拼接字符串10w次, 使用时间为 4014ms,
而使用method2方法,append 方式,仅仅只需7ms, 差距如此之大
为何?
对于上面的StringBuilder方式,还有更一步的改进方式
查看 StringBuilder 类底层的实现时,发现初始定义一个 char 型数组(JDK8),用于append操做, 在数组长度不够时,会建立一个更长的,并进行拷贝, 这也有点浪费时间,因此在实际开发中,若是基本肯定要前先后后添加的字符串长度不高于某个限定值highLevel的状况下,建议使用构造器实例化:
StringBuilder s = new StringBuilder(highLevel); //new char[highLevel]复制代码
前面已经有使用过,而且大概解释过,如今具体说明一下
public native String intern();复制代码
intern是一个native方法,调用的是底层C的方法
字符串池最初是空的。在调用intern方法时,若是池中已经包含了由equals(object)方法肯定的与该字符串对象相等的字符串(也就是值相等),则返回池中的字符串。不然,该字符串对象值放一个到池中,并返回对该字符串对象的引用。
也就是说,若是在任意字符串上调用String.intern方法,那么其返回结果所指向的那个类实例,必须和直接以字面量形式出现的字符串实例彻底相同。所以,下列表达式的值一定是true
("a"+"b"+"c").intern()=="abc"
通俗点讲,String就是确保字符串在常量池里有一份拷贝,这样能够节约内存空间,加快字符串操做任务的执行速度。
如何保证 变量s 指向的是字符串常量池中的数据呢?
方式一 : String s = "Hello"; //直接使用字面量
方式二: 使用 intern方法
这个面试题应该不少人都是知道的,答案是一个或者两个, 下面用字节码指令证实这个事
public class StringNewTest { public static void main(String[] args) { String str = new String("ab"); } }复制代码
字节码指令:
0 new #2 <java/lang/String> //堆中建立 String对象 3 dup 4 ldc #3 <ab> //从 常量池中获取 ab 字符串对象,若是没有 则会建立 6 invokespecial #4 <java/lang/String.<init>> //使用 ab 字面量去初始化 堆中的 String 对象 9 astore_1 10 return复制代码
因此有上面的结论 ,一个或两个, 若是常量池中没有这个字面量的状况是是会建立两个的
看看 String类的带参构造函数
private final char[] value; private int hash; //... public String(String var1) { this.value = var1.value; this.hash = var1.hash; }复制代码
能够看到,将常量池String对象中的value 属性,也就是维护的char[] 和字符的hash值赋给了 new String 对象的 value属性和 hash属性,因此 new 出的String 对象的内容为参数中的值, 这也说明了,虽然 常量池中的String对象 和new 出的 String 对象自己地址值不一样,可是他们所维护的char[]倒是同一个
那么new String(“a”) + new String(“b”) 会建立几个对象?
public class StringNewTest { public static void main(String[] args) { String str = new String("a") + new String("b"); } }复制代码
字节码指令:
0 new #2 <java/lang/StringBuilder> // 1. StringBuilder 对象 3 dup 4 invokespecial #3 <java/lang/StringBuilder.<init>> 7 new #4 <java/lang/String> // 2. new String("a") 对象 10 dup 11 ldc #5 <a> //3. 常量池中 a 字符串对象 13 invokespecial #6 <java/lang/String.<init>> 16 invokevirtual #7 <java/lang/StringBuilder.append> 19 new #4 <java/lang/String> // 4. new String 对象 22 dup 23 ldc #8 <b> //5. 常量池中 b 字符串对象 25 invokespecial #6 <java/lang/String.<init>> 28 invokevirtual #7 <java/lang/StringBuilder.append> 31 invokevirtual #9 <java/lang/StringBuilder.toString> //toString 返回的 34 astore_1 35 return 复制代码
全部上面的代码 最多建立 6个对象
深刻剖析: StringBuilder的toString():
代码:
public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }复制代码
这是StringBuilder 对象的toString 方法, 将 StringBuilder 类中append 处理的char[] 链接为一个字符串,
和普通的 new String("ab")不同, 并无使用字面量的形式建立,因此没有在 常量池中建立"ab" 字符串
因此这里就建立了一个
在前面说到, intern() 方法是将判断调用者字符串对象的值 是否在常量池中存在,若是存在 则返回常量池中的那个对象引用,若是不存在则 造一个, 可是这个造一个 ,在随着JDK7 将字符串常量池移到堆空间时,发生了变化
代码示例:
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 // 执行完下一行代码之后,字符串常量池中,是否存在"11"呢?答案:不存在!! String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("11") s3.intern(); String s4 = "11"; System.out.println(s3 == s4);// jdk6:false jdk7/8:true } }复制代码
上面的代码中
第一个输出语句 不管如何都是打印false,那是由于在建立 new String("1")时已经在堆中建立了 字符串 "1",因此intern() 方法没有再建立,而S2则引用了常量池中的对象,因此和 new String()的 s 一直都是false
第二个输出语句 在jdk6 中打印了 false, 是由于 代码 new String("1") + new String("1")至关于 建立了一个new String("11") 赋予了 s3 ,可是根据上一节说到的,并无在常量池中生成 "11",因此 当调用intern() 方法时, 建立了一个新的字符串 "11" 放到了常量池中,注意 此时的环境为JDK6, 字符串常量池在 方法区中,和new的对象不在一块儿,因此是建立一个新的方式
可是在JDK7 及之后的版本,倒是打印 true,是由于 字符串常量挪到了 堆中,和 new的对象在一块儿,因此杜绝空间浪费, 调用intern() 方法建立对象时,没有拷贝新的,而是存了 new String() 对象的引用到字符串常量池中,因此打印为 true
内存示意图:
JDK6:
JDK7
扩展
public class StringIntern1 { public static void main(String[] args) { String s3 = new String("1") + new String("1");//new String("11") //在字符串常量池中生成对象"11" String s4 = "11"; String s5 = s3.intern(); System.out.println(s3 == s4);//false System.out.println(s5 == s4);//true } }复制代码
将上面的代码中 声明字面量提到调用intern() 方法先后,打印结果就固定了,那是由于在声明"11" 时已经建立一个新的字符串到常量池中. 因此一定是false;
当调用 intern 方法时:
JDK1.6中,将这个字符串对象尝试放入串池。
JDK1.7起,将这个字符串对象尝试放入串池。
看下面的一段代码:
public class StringIntern2 { static final int MAX_COUNT = 1000 * 10000; 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++) { // 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(); } } }复制代码
上面的代码中,将 1-10 每次使用 String.valueOf 的方式转换为 字符串 存入数组中,循环1000W次, 下面看 使用intern方法和不使用的区别
不使用: valueOf 方法中每次都在堆中new 了一个新的字符串对象, 因此共建立了 1000w个对象
使用intern: 虽然每次也都建立了 String 对象, 可是最后返回的 倒是intern 方法返回的 字符串常量池中的,会被重用, 而由于数组中并无指向在堆中建立的String 对象,将在垃圾回收时 被销毁,减小内存,查看内存数据也是如此
结论:
代码演示: 建立10w个字符串
public class StringGCTest { public static void main(String[] args) { for (int j = 0; j < 100000; j++) { String.valueOf(j).intern(); } } }复制代码
jvm 参数: -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails 打印 字符串常量池中的信息和垃圾回收的信息
打印信息以下, 很明显在新生代PSYoungGen发生了垃圾回收的行为, 而且看出 字符串常量池中也不足10w个对象
打印内容:
Heap PSYoungGen total 4608K, used 3883K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000) eden space 4096K, 82% used [0x00000000ffb00000,0x00000000ffe50fb0,0x00000000fff00000) from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000) to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000) ParOldGen total 11264K, used 228K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000) object space 11264K, 2% used [0x00000000ff000000,0x00000000ff039010,0x00000000ffb00000) Metaspace used 3472K, capacity 4496K, committed 4864K, reserved 1056768K class space used 381K, capacity 388K, committed 512K, reserved 1048576K SymbolTable statistics: Number of buckets : 20011 = 160088 bytes, avg 8.000 Number of entries : 14158 = 339792 bytes, avg 24.000 Number of literals : 14158 = 603200 bytes, avg 42.605 Total footprint : = 1103080 bytes Average bucket size : 0.708 Variance of bucket size : 0.711 Std. dev. of bucket size: 0.843 Maximum bucket size : 6 StringTable statistics: Number of buckets : 60013 = 480104 bytes, avg 8.000 Number of entries : 62943 = 1510632 bytes, avg 24.000 Number of literals : 62943 = 3584040 bytes, avg 56.941 Total footprint : = 5574776 bytes Average bucket size : 1.049 Variance of bucket size : 0.824 Std. dev. of bucket size: 0.908 Maximum bucket size : 5复制代码
补充: G1中 String 去重操做
这个去重操做针对的不是 String自己,由于 String 常量池中 自己就不是重复的,而是针对 new String() 中维护的char[]
String 去重操做的背景
对许多Java应用(有大的也有小的)作的测试得出如下结果:
许多大规模的Java应用的瓶颈在于内存,测试代表,在这些类型的应用里面,Java堆中存活的数据集合差很少25%是String对象。更进一步,这里面差很少一半String对象是重复的,重复的意思是说:
str1.equals(str2)= true。堆上存在重复的String对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的String对象进行去重,这样就能避免浪费内存。
String 去重的的具体实现
命令行选项: