1. String的特性
- 定义方式
- String str = "hello";
- String str = "hello";
- String声明为final,不可被继承
- String实现了Serializable接口和Comparable接口
- String的底层存储结构在jdk1.8之后从char[]变为了byte[]
- String不可变,String一旦定义,不可修改,修改=重新定义
- String常量池String pool中不会出现相同的内容,String pool本质上是一个hashtable,jdk6中长度未1009,jdk7以后为60013(解决hash冲突),该长度可以通过-XX:StringTableSize指定, jdk8之后此值最小1009
2. 内存分配
使用字面量的方式创建的String对象直接存放在常量池中
String hello = "hello";存放在常量池中
jdk6: 常量池位于永久代中
jdk7: 常量池挪到了堆
jdk8: 堆
为什么挪到了堆?
- 永久代空间较小
- 垃圾回收不易,fullGC频率极低
3. 从内存角度看字符串拼接
String hello = "hel"+"lo";
- 常量与常量的结果放在常量池,原理是编译期优化
这是常量+常量
- 如果其中一个是变量,则结果就在堆中(此堆区别于常量池所在堆),原理是StringBuilder拼接
- 如果变量+常量的拼接结果 调用了intern()方法,如果此结果在常量池中不存在,就会将该结果放入常量池中
常见问题:
Strings1="hello"; Strings2="hello"; System.out.println(s1==s2);// true 内存地址均为常量池中的helloStrings3=s1+"world"; Strings4=s2+"world"; System.out.println(s3==s4); // false 每个对象的内存地址都是不一样的Strings5= (s1+"world").intern(); System.out.println(s4==s5); // true 将结果放入到了常量池,所以是true
3.1 字符串变量拼接
从字节码看字符串拼接的本质
/*** @Author: Zy* @Date: 2021/12/7 18:50* 测试字符串拼接底层内存结构*/publicclassStringConcatTest { publicstaticvoidmain(String[] args) { Strings1="hello"; Strings2="world"; Strings3=s1+s2; } }
可以看到两个变量拼接本质上就是StringBuilder的append;
3.2 字符串变量拼接测试
既然说拼接底层使用的是StringBuilder,为什么还说他比较慢
代码测试如下:
packagecom.zy.study12; /*** @Author: Zy* @Date: 2021/12/7 21:39* 测试字符串拼接和使用StringBuilder.append方法的时间*/publicclassStringBuilderTest { /*** 使用拼接的方式拼接100000个字符串* @author Zy* @date 2021/12/7* @param* @return void*/publicstaticvoidstringConcat(){ longstart=System.currentTimeMillis(); Stringresult=""; for (inti=0; i<100000; i++) { result+="test"; } longend=System.currentTimeMillis(); System.out.println(end-start); } /*** 创建一个StringBuilder对象,进行append* @author Zy* @date 2021/12/7* @param* @return void*/publicstaticvoidstringBuilderAppend(){ longstart=System.currentTimeMillis(); StringBuilderbuilder=newStringBuilder(); for (inti=0; i<100000; i++) { builder.append("test"); } longend=System.currentTimeMillis(); System.out.println(end-start); } publicstaticvoidmain(String[] args) { // 字符串拼接StringBuilderTest.stringConcat(); // append拼接StringBuilderTest.stringBuilderAppend(); } }
结果差距巨大:
这个结果应当是显而易见的,但是我们要思考的是为什么?
原因:
- 字符串拼接每次拼接都会创建一个StringBuilder对象,而且在返回的时候会再次新建一个String对象(StringBuilder.toString()方法),每次都创建了两个对象,耗时严重
- 每次循环创建两个对象占用内存巨大
- 内存占用过大,GC的次数也会增加,GC是最耗时的操作
即使是StringBuilder单一对象append,也可以进行优化:
如果确定了StringBuilder的次数,那么就可以在创建StringBuilder的时候指定他的容量,从而减少StringBuilder的扩容操作,扩容操作也是非常耗时的.
/*** 指定StringBuilder容量*/publicstaticvoidstringBuilderAppend(){ longstart=System.currentTimeMillis(); StringBuilderbuilder=newStringBuilder(100000); for (inti=0; i<100000; i++) { builder.append("test"); } longend=System.currentTimeMillis(); System.out.println(end-start); }
4. String的intern方法
此方法在上文用过,如果调用此方法的字符串不在常量池中,就将此字符串放到常量池中,如果此字符串在常量池中,那么就返回常量池中的字符串
该方法为native方法,即本地方法
源码注释:
Returnsacanonicalrepresentationforthestringobject. Apoolofstrings, initiallyempty, ismaintainedprivatelybytheclassString. Whentheinternmethodisinvoked, ifthepoolalreadycontainsastringequaltothisStringobjectasdeterminedbytheequals(Object) method, thenthestringfromthepoolisreturned. Otherwise, thisStringobjectisaddedtothepoolandareferencetothisStringobjectisreturned. Itfollowsthatforanytwostringssandt, s.intern() ==t.intern() istrueifandonlyifs.equals(t) istrue. Allliteralstringsandstring-valuedconstantexpressionsareinterned. Stringliteralsaredefinedinsection3.10.5oftheTheJava™LanguageSpecification. Returns: astringthathasthesamecontentsasthisstring, butisguaranteedtobefromapoolofuniquestrings. publicnativeStringintern();
翻译:
返回字符串对象的规范表示。 字符串池最初是空的,由 String 类私下维护。 当调用 intern 方法时,如果池中已经包含一个等于该 String 对象的字符串(由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中并返回对此 String 对象的引用。 因此,对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为真时,s.intern() == t.intern() 为真。 所有文字字符串和字符串值常量表达式都被实习。字符串文字在 Java™ 语言规范的第 3.10.5 节中定义。 返回: 与此字符串具有相同内容的字符串,但保证来自唯一字符串池。
例子:
("a"+"b"+"c").intern() == "abc";
intern() 方法在不同的jdk版本有着不一样的表现
jdk6: 调用intern()方法,如果该字符串不在常量池中存在,就直接在永久代的字符串常量池中创建该字符串对象
jdk7/8及以后: 调用intern()方法,如果该字符串不在常量池中,那么不会再元空间常量池中创建一个新的对象,而是直接使用堆空间中的String对象的地址作为字符串常量池的地址.
Stringtest=newString("hello") +newString("world"); Stringintern=test.intern(); Stringtest1="helloworld"; System.out.println(intern==test); // jdk6中为false jdk7/8中为true
4.1 intern()的用处
如上文所说,intern()方法可以获取常量池中的对象,如果对象不存在会新建一个,那么当系统中有大量重复字符串存储的时候就可以使用intern()方法
好处:
避免在堆中创建大量对象,占用堆空间; 因为一个字符串在常量池中维护过一次以后,下次再使用intern()方法就会获取常量池中对象的引用,而不是再在堆中创建一个新对象
但是要酌情使用,非频繁的字符串对象不要放入常量池,因为常量池位于方法区,不易gc
5. 题目理解String底层
5.1 new String("ab")创建了几个对象
答案: 两个
- new 创建的String 对象
- "ab" 字面量创建的字符串常量池对象
证明: 通过字节码指令看
5.2 字符串对象拼接创建了多少对象
// 创建了多少个对象Stringtest=newString("hello") +newString("world");
从字节码上看应该是创建了五个对象
但是如果再往深层看, StringBuilder的toString()方法也是创建了一个新的String对象的.
6. StringTable的垃圾回收
String也是有垃圾回收的
证明:
packagecom.zy.study12; /*** @Author: Zy* @Date: 2021/12/13 21:37* 测试String的垃圾回收* -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails*/publicclassStringGc { publicstaticvoidmain(String[] args) { for (inti=0; i<100000; i++) { String.valueOf(i).intern(); } } }
-Xms15m-Xmx15m-XX:+PrintStringTableStatistics -XX:+PrintGCDetails 堆内存15m 打印StringTable的统计数据 打印gc详细信息
输出:
gc信息,发生了一次YGC,回收了一次eden区
StringTable统计信息,可以看到到6w多后,就不再增大,等待垃圾回收后再次创建.
6.1 扩展: G1中对String的去重操作
jdk8中默认的垃圾回收期G1在回收的时候会对String进行去重操作
- 去重去的是堆中的对象,而不是常量池
- jdk1.8中String底层的存储是char[],去重也是针对此char[]
- 去重的操作: 分析每一个可能存在重复的char[],放入一个不重复的hashtable中,之后有另外一个队列会查询此hashtable,如果存在一个一样的char[],那么就会对两个char[]进行去重,释放其中一个char[]的引用,如果此hashtable中不存在该char[],那么就将此char[]放入hasttable中.
jvm参数开启去重(默认不开启):
UseStringDeduplication 开启去重 true/false PrintStringDeduplicationStatistics 打印去重统计信息 true/false StringDeduplicationAgeThreshold 设置去重候选对象的年龄,如果达到此值,被认为可能是重复对象