String
String的基本特性
String:字符串,使用一对 “” 引起来表示
-
String s1 = “ylcandzy”;//字面量的定义方式
-
String s2 = new String("hello");
String声明为final的,不可被继承
String实现了Serializable接口;表示字符串是支持序列化的,实现Comparable接口:表示String可以比较大小
String在jdk8及以前内部定义了final char[] value用于存储字符串数据。jdk9时改为byte[]
- String存储结构变更
结论:String再也不用 char[] 来存储,改为 byte[] 加上编码标记,节约了一些空间
public final class String implements 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)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
package com.ylc.java05; import org.junit.Test; /** * * * String基本使用:验证String的不可变性 * 并不会在原有的地址上进行修改,只会创建一个新的地址 * * @Author ylc * @Date 2022/3/25 16:02 * @Version 1.0 */ public class StringTest01 { @Test public void test01(){ String s1 = "abc";//字面量定义的方式,"abc"存储在字符串常量池中 String s2 = "abc"; s2 = "hello"; System.out.println(s1 == s2); //判断地址 System.out.println(s1); } @Test public void test02(){ String s1 = "abc"; String s2 = "abc"; s2 += "ylc"; System.out.println(s1 == s2); System.out.println(s2); } @Test public void test03(){ String s1 = "abc"; String s2 = s1.replace("a","y"); System.out.println(s1 == s2); } }
字符串常量池中是不会存储相同内容的字符串的。
- String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降。
- 使用-XX:StringTableSize可以设置StringTable的长度
- 在jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTableSize设置没有要求。
- 在jdk7中StringTable的长度默认值60013,1009是可以设置的最小值。
- jdk8开始,1009是可设置的最小值。
String内存分配
- 常量池就是类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它是主要使用方法有两种
- 直接使用双引号声明出来的String对象会直接存储在常量池中。(比如:String info = “ylcandzy”);
- 如果不是用双引号声明的String对象,可以使用String提供的intern()方法。
- Java 6及以前,字符串常量池存放在永久代。
- Java 7中字符串常量池的位置调整到Java堆中。
- Java 8元空间,字符串常量在堆。
- StringTable为什么要调整?
- permSize默认比较小
- 永久代垃圾回收频率低
package com.ylc.java05; import java.util.HashSet; import java.util.Set; /** * * jdk6: * -XX:MetaspaceSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m * * jdk8: * -XX:MetaspaceSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m * @Author ylc * @Date 2022/3/25 17:47 * @Version 1.0 */ public class StringTest02 { public static void main(String[] args) { //使用Set保持着常量池引用,避免full gc 回收常量池行为 Set<String> set = new HashSet<String>(); //在short可以取值的范围内足以让6MB的PermSize或heap产生OOM short i = 0; while (true){ set.add(String.valueOf(i++).intern()); } } }
String的基本操作
package com.ylc.java05; /** * @Author ylc * @Date 2022/3/25 18:23 * @Version 1.0 */ public class StringTest03 { public static void main(String[] args) { int i = 1; Object obj = new Object(); StringTest03 mem = new StringTest03(); mem.foo(obj); } private void foo(Object param){ String str = param.toString(); System.out.println(str); } }
字符串拼接操作
- 常量与常量的拼接结果在常量池,原理是编译期优化
- 常量池中不会存在相同内容的常量。
- 只要其中有一个变量,结果就是在堆中。变量拼接的原理是StringBuilder
- 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
@Test public void test02(){ String s1 = "ylc"; String s2 = "zy"; String s3 = "ylcandzy"; String s4 = "ylcand" + "zy";//编译期间优化 //如果拼接符号的前后出现了变量,则相当于堆空间new String(),具体的内容为拼接的结果:ylcandzy String s5 = s1 + "zy"; String s6 = "ylc" +s2; String s7 = s1 + s2; System.out.println(s3 == s4); 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():判断字符串常量池中是否存在ylcandzy值,如果存在,则返回常量池中ylcandzy的地址; //如果字符串常量池不存在ylcandzy,则在常量池中加载一份ylcandzy,并返回对象的地址 String s8 = s6.intern(); System.out.println(s3 == s8); } }
A:字符串变量拼接操作的底层原理
- 如果拼接符号的前后出现了变量,则相当于堆空间new String(),具体的内容为拼接的结果:ylcandzy
- //intern():判断字符串常量池中是否存在ylcandzy值,如果存在,则返回常量池中ylcandzy的地址;
- //如果字符串常量池不存在ylcandzy,则在常量池中加载一份ylcandzy
- 1,字符串拼接操作不一定使用的是StringBuilder
- 如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译器优化,即非StringBuilder的方式
- 针对于final修饰类,方法,基本数据类型,引用数据类型的量结构时,能使用上final的时候建议使用
package com.ylc.java05; import org.junit.Test; /** * @Author ylc * @Date 2022/3/25 18:43 * @Version 1.0 */ public class StringTest04 { @Test public void test01(){ 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); System.out.println(s1.equals(s2)); } @Test public void test02(){ String s1 = "ylc"; String s2 = "zy"; String s3 = "ylcandzy"; String s4 = "ylcand" + "zy";//编译期间优化 //如果拼接符号的前后出现了变量,则相当于堆空间new String(),具体的内容为拼接的结果:ylcandzy String s5 = s1 + "zy"; String s6 = "ylc" +s2; String s7 = s1 + s2; System.out.println(s3 == s4); 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():判断字符串常量池中是否存在ylcandzy值,如果存在,则返回常量池中ylcandzy的地址; //如果字符串常量池不存在ylcandzy,则在常量池中加载一份ylcandzy,并返回对象的地址 String s8 = s6.intern(); System.out.println(s3 == s8); } @Test public void test03(){ String s1 = "a"; String s2 = "b"; String s3 = "ab"; /** * * 如下s1 + s2 的执行细节:(变量s是我临时定义的) * StringBuilder s = new StringBuilder; * s.append("a") * s.append("b") * s.toString() --> 约等于 new String("ab") * * 补充:在jdk5.0之后使用的是StringBuilder,jdk5.0之前是使用的StringBuilder */ String s4 = s1 + s2; System.out.println(s3 == s4);//false 创建了一个新对象 } /* 1,字符串拼接操作不一定使用的是StringBuilder 如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译器优化,即非StringBuilder的方式 2,针对于final修饰类,方法,基本数据类型,引用数据类型的量结构时,能使用上final的时候建议使用 */ @Test public void test04(){ final String s1 = "a"; final String s2 = "b"; String s3 = "ab"; String s4 = s1 + s2; System.out.println(s3 == s4); } }
B:拼接操作与append操作进行对比
体会执行效率:通过StringBuilder的append()的方式添加字符串的效率要远高于使用String的字符串拼接方式!
详情:a StringBuilder的append()的方式:自始至终中只创建过一个StringBuilder的对象
使用String的字符串拼接方式:创建过多个StringBuilder和String的对象
b 使用功能String的字符串拼接方式:内存中由于创建了较多的StringBuilder和String的对象,内存占用更大;
如果要使用GC,需要花费额外的时间。
改进时间:在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值highLevel的情况下,
建议使用构造器。
StringBuilder s = new StringBuilder(highLevel);//new char[highLevel]
@Test public void test06(){ long start = System.currentTimeMillis(); // method01(100000);//4699 method02(100000);//花费时间为: 4 long end = System.currentTimeMillis(); System.out.println("花费时间为: " + (end - start)); } public void method01(int highLevel){ String str = ""; for (int i = 0; i < highLevel;i++){ str = str + "a";//每次循环都会创建一个StringBuilder,String } // System.out.println(str); } public void method02(int highLevel){ //只需要创建一个StringBuilder StringBuilder src = new StringBuilder(); for (int i = 0; i < highLevel; i++) { src.append("a"); } // System.out.println(src); } }
intern()使用
- intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
- 比如: String myInfo = new String(“ylc”).intern();
- 也就是说,如果在任意字符串上调用String.intern方法,那么其返回结果所指向那个类实例,必须和直接以常量形式出现的字符串实例完全相同。
- (“a”+“b”+“c”).intern() == "abc" //true
- 通俗点讲,Interned String就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。(存放在 Pool)
- A:new String()到底能创建多少个对象(面试题)?
package com.ylc.java06; /** * @Author ylc * @Date 2022/3/26 14:21 * @Version 1.0 * * 题目: * new String("ab")会创建几个对象?看字节码,就两个 * 一个对象是: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" */ public class StringNewTest { public static void main(String[] args) { // String str01 = new String("ab"); String str02 = new String("a") + new String("b"); } }
关于intern()的面试题!!!
package com.ylc.java06; /** * @Author ylc * @Date 2022/3/26 11:31 * @Version 1.0 * * 如何保证变量s指向的是字符串常量池中的数据呢? * 方式一: String s = “ylc”;字面量定义方式 * * 方式二: String s = new String("ylc").intern(); * String s = new StringBuilder("ylc").toString().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:false String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("22") //执行完上一行代码以后,字符串常量池中,是否存在"11"呢? 不存在 s3.intern();//字符串常量池中生成"11". // 如何理解:jdk6:创建了一个新对象"11",也就是新的地址 //jdk7:此时常量中并没有创建"11",而是创建一个指向堆空间中new String String s4 = "11";//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池生成的"11"的地址 System.out.println(s3 == s4);//jdk6:false jdk7:true } }
inter()是使用:jdk6 vs jdk7/8
jdk1.6中,将这个字符串对象尝试放入串池。
如果串池中有,则并不会放入。返回已有的串池中的对象的地址
如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址。
jdk1.7起,将这个字符串对象尝试放入串池。
如果串池中有。则并不会放入。返回已有的串池中的对象的地址
如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址。
intern()练习1:
package com.ylc.java06; import com.sun.xml.internal.ws.api.model.wsdl.WSDLOutput; /** * @Author ylc * @Date 2022/3/26 18:13 * @Version 1.0 */ public class StringIntern01 { public static void main(String[] args) { // String s = new String("a") + new String("b"); //new String("ab") //在上一行代码执行完以后,字符串常量池中并没有“ab” String s2 = s.intern();//jdk6:在串池中创建一个字符串“ab” //jdk8:串池中没有创建字符串“ab”,而是创建了一个引用地址 System.out.println(s2 == "ab");//jdk8:true jdk6:true System.out.println(s == "ab");//jdk8:true jdk6:false } }
inter()练习2:
package com.ylc.java06; /** * @Author ylc * @Date 2022/3/26 18:47 * @Version 1.0 */ public class StringIntern02 { public static void main(String[] args) { String s1 = new String("ab");//执行完以后,会在字符串常量池中会生成 // String s2 = new String("a") + new String("b"); //执行完以后,不会在字符串常量池中生成 s1.intern(); String s2 = "ab"; System.out.println(s1 == s2); //false } }
intern()空间效率测试:
使用intern() 测试执行效率:空间使用上
结论:对于程序中大量存在的字符串,尤其其中存在很多重复字符串时,使用intern()可以减少内存空间。
package com.ylc.java06; import com.ylc.day01.Test; /** * @Author ylc * @Date 2022/3/26 22:27 * @Version 1.0 */ public class StringIntern03 { 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,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])).intern(); } long end = System.currentTimeMillis(); System.out.println("花费的时间为:" + (end - start)); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } System.gc(); } }
StringTable的垃圾回收
-Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
package com.ylc; /** * @Author ylc * @Date 2022/3/27 9:27 * @Version 1.0 */ public class StringGCTest { public static void main(String[] args) { for (int i = 0; i < 10000; i++) { String.valueOf(i).intern(); } } }
G1的String去重操作
背景:对许多应用(有大也有小)做的测试有以下结果:
- 堆存活数据集合里面String对象占了25%
- 堆存活数据集合里面重复String对象有13.5%
- String对象的平均长度是45
Java堆中存活的数据集合差不多25%是String对象,堆上存在重复的String对象必然是一种内存的浪费。
命令行选项:
- UseStringDeduplication(bool):开启String去重,默认是不开启,需要手动开启。
- PrintStringDeduplicationStatistics(bool):打印详细去重统计信息。
- StringDeplicationAgeThreshold(uintx):达到这个年龄的String对象被认为是去重的后选对象。
总结:String重点在于intern()方法,需多次理解底层,其次内存分配。整体上并不难
String源码
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** The offset is the first index of the storage that is used. */ private final int offset; /** The count is the number of characters in the String. */ private final int count; /** Cache the hash code for the string */ private int hash; // Default to 0 /** use serialVersionUID from JDK 1.0.2 for interoperability */ private static final long serialVersionUID = -6849794470754667710L; ........ }
posted on 2022-03-25 22:59 只想做加法(ylc) 阅读(156) 评论(0) 编辑 收藏 举报
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?