北京网站名称注册证书,市场营销案例,天津 网站制作,wordpress机器爬虫爬资讯String 的基本特性
String#xff1a;字符串#xff0c;使用一对 “” 引起来表示
// 两种定义方式
String s1 atguigu; // 字面量的定义方式
String s2 new String(hello);String 声明为 final 的#xff0c;不可被继承String 实现了 Serializ…String 的基本特性
String字符串使用一对 “” 引起来表示
// 两种定义方式
String s1 atguigu; // 字面量的定义方式
String s2 new String(hello);String 声明为 final 的不可被继承String 实现了 Serializable 接口表示字符串支持序列化的。String 实现了 Comparable 接口表示 String 可以比较大小String 在 JDK 8 及以前内部定义了 final char[] value 用于存储字符串数据。JDK 9 时改为了 byte[]String 代表不可变的字符序列。简称不可变性 当对字符串重新赋值时需要重写指定内存区域赋值不能使用原有的 value 进行复制。当对现有的字符串进行连接操作时也需要重新指定内存区域赋值不能使用原有的 value 进行赋值当调用 String 的 replace() 方法修改指定字符或字符串时也需要重新指定内存区域赋值不能使用原有的 value 进行赋值 通过字面量的方式区别于 new给一个字符串复制此时的字符串值声明在字符串常量池中。字符串常量池中是不会存储相同内容的字符串的。 String 的 String Pool 是一个固定大小的 Hashtable默认值大小长度是 1009.如果放进 String Pool 的 String 非常多就会造成 Hash 冲突严重从而导致链表会很长而链表长了后直接会造成的影响就是当调用 String.intern 时性能会大幅下降。使用 -XX:StringTableSize 可设置 StringTable 的长度在 JDK 6 中 StringTable 是固定的就是 1009 的长度所以如果常量池中的字符串过多就会导致效率下降很快。StringTableSize 设置没有要求。在 JDK 7 中StringTable 的长度默认值是 60013JDK 8 开始设置 StringTable 的长度的话1009 是可设置的最小值
[外链图片转存中…(img-9uBEhwJD-1720061415931)] [外链图片转存中…(img-P7npCvJT-1720061415932)] String 在 JDK 9 中存储结构的变更 官网说明JEP 254: Compact Strings :::warning
Motivation
The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused.
动机
String类的当前实现将字符存储在字符数组中每个字符使用两个字节十六位。从许多不同的应用程序收集的数据表明字符串是堆使用的主要组成部分此外大多数String对象仅包含Latin-1 字符。此类字符只需要一个字节的存储空间因此此类String对象的内部字符数组中一半的空间将未使用。
Description
We propose to change the internal representation of the String class from a UTF-16 char array to a byte array plus an encoding-flag field. The new String class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used. 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. This is purely an implementation change, with no changes to existing public interfaces. There are no plans to add any new public APIs or other interfaces.
描述
我们建议将String类的内部表示形式从UTF-16字符数组更改为字节数组加上编码标志字段。新的字符串类将根据字符串的内容存储编码为ISO-8859-1/Latin-1每个字符一个字节或UTF-16每个字符两个字节的字符。编码标志将指示使用哪种编码。即可以根据字符串的内容存储编码来使用不同的存储方式 AbstractStringBuilder、StringBuilder和StringBuffer等字符串相关类将更新为使用相同的表示形式HotSpot VM的内在字符串操作也是如此。 这纯粹是一种实现更改对现有的公共接口没有更改。没有计划添加任何新的公共API或其他接口。 ::: 结论 String 再也不用 char[] 来存储了改成了 byte[] 加上编码标记节约了一些空间。而StringBuilder和StringBuffer等字符串相关类也更新为使用相同的表示形式。HotSpot VM的内在字符串操作也是如此。
package chapter13;import org.junit.Test;public class StringTest1 {Testpublic void test1() {String s1 abc; // 字面量定义的方式abc存储在字符串常量池中String s2 abc;System.out.println(s1 s2); // trueSystem.out.println(s1);System.out.println(s2);}
}
测试结果 [外链图片转存中…(img-QpfZ90OA-1720061415932)]
package chapter13;import org.junit.Test;public class StringTest1 {Testpublic void test1() {String s1 abc; // 字面量定义的方式abc存储在字符串常量池中String s2 abc;s1 hello;System.out.println(s1 s2); // 判断地址falseSystem.out.println(s1);System.out.println(s2);}Testpublic void test2() {String s1 abc;String s2 abc;s2 def;System.out.println(s2); // abcSystem.out.println(s1); // abcdef}Testpublic void test3() {String s1 abc;String s2 s1.replace(a, m);System.out.println(s1); // abc体现了 String 的不可变性System.out.println(s2); // mbc}
}
例题
package chapter13;public class StringExer {String str new String(good);char[] ch {t,e,s,t};public void change(String str, char[] ch){str test ok;ch[0] b;}public static void main(String[] args) {StringExer ex new StringExer();ex.change(ex.str, ex.ch);System.out.println(ex.str); // goodSystem.out.println(ex.ch); // best}
}
结果输出 [外链图片转存中…(img-zkC9T4MG-1720061415933)] 当主线程执行到方法 ex.chang(ex.str, ex.ch) 时change 方法入栈其局部变量表中包含两个变量分别是 str 和 ch主线程将成员变量 str 的地址值复制了一份传递给了 change 方法的局部变量 str此时如果输出打印 str其值将会和成员变量 str 一样均为 good。但是 change 方法又重新将 “test ok” 赋值给了 str由于“test ok”之前在字符串常量池中并不存在所以虚拟机会将“test ok”添加到字符串常量池中并将地址赋值给 str此时 str 的值就变成了 “test ok”。随着 change 方法执行结束虚拟机栈会将其出栈此时的局部变量 str 也就失去了作用所以在打印成员变量 str 的值时没有发生改变。要想改变成员变量 str 的值则 str “test ok”; 应该改为 this.str “test ok”;。
String 的内存分配
在 Java 语言中有 8 中基本数据类型和一种比较特殊的类型 String。这些类型为了使它们在运行过程中速度更快、更节省内存都提供了一种常量池的概念。常量池就类似于一个 Java 系统级别提供的缓存。8 中基本数据类型的常量池都是系统协调的String 类型的常量池比较特殊它的主要使用方法有两种。 直接使用双引号声明出来的 String 对象会直接存储在常量池中。 比如String info “atguigu.com”; 如果不是用双引号声明的 String 对象可以使用 String 提供的 intern() 方法使用 new String(“hello”) 方式创建的对象hello会被存放在堆中常量池之外的地方。 Java 6 及以前字符串常量池存放在永久代Java 7 中 Oracle 的工程师对字符串池的逻辑做了很大的改变即将字符串常量池的位置调整到 Java 堆中。 所有的字符串都保存在堆Heap中和其他普通对象一样这样可以让你在进行调优应用时进需要调整堆大小就可以了。字符串常量池概念原本使用得比较多但是这个改动使得我们有足够的理由重新考虑在 Java 7 中使用 String.iintern()。 Java 8 元空间字符串常量在堆。
StringTable 为什么要调整 官网Java SE 7 Features and Enhancements :::warning Area: HotSpot Synopsis: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences. RFE: 6962931 :::
PermSize永久代比较小如果大量创建字符串永久代很容易报 OOM永久代垃圾回收频率低
String 的基本操作
案例一
package chapter13;public class StringTest4 {public static void main(String[] args) {System.out.println();System.out.println(1);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); //System.out.println(1);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); //}
}
[外链图片转存中…(img-r7is8Wqd-1720061415933)] 以 debug 模式启动程序 第一个断点处 [外链图片转存中…(img-W6SeIfEo-1720061415934)] 第二个断点处 [外链图片转存中…(img-Iy4R4zel-1720061415934)] 继续单步执行 [外链图片转存中…(img-kxdJeTuy-1720061415934)] 直接跳到下一个断点 [外链图片转存中…(img-Q97JAr8m-1720061415935)] 继续单步执行 [外链图片转存中…(img-jM3dWsPA-1720061415935)] 继续单步执行 [外链图片转存中…(img-3VBlVvZq-1720061415935)] 直接跳至最后一个断点 [外链图片转存中…(img-joBE9YkB-1720061415935)] 代码执行结束后字符串个数仍为 1160未再发生变化 [外链图片转存中…(img-h4tofune-1720061415936)]
总结Java 语言规范里要求完全相同的字符串字面量应该包含同样的 Unicode 字符序列包含同一份码点序列的常量并且必须是指向同一个 String 类实例。
案例二
[外链图片转存中…(img-z8uLgJqC-1720061415936)] 内存分析 [外链图片转存中…(img-hIbjFcT9-1720061415937)] [外链图片转存中…(img-rI8hY8To-1720061415937)] 第 7 行代码创建了一个字符串。该字符串被存放在堆空间中字符串常量池中并且在 foo() 栈空间中创建了一个指向该字符串的一个引用。
字符串拼接操作
1、常量与常量的拼接结果在常量池原理是编译器优化 2、常量池中不会存在相同内容的常量 3、只要其中有一个是变量结果就在堆中。变量拼接的原理是 StringBuilder 4、如果拼接的结果是调用 intern() 方法则主动将常量池中还没有的字符串对象放入池中并返回此对象地址。
package chapter13;import org.junit.Test;public class StringTest5 {Testpublic void test1() {String s1 a b c;String s2 abc; // abc 一定是放在字符串常量池中的将此地址赋值给 s2/*** 最终 .java 编译成 .class再执行 .class* 编译期就已经确定下来* 验证方式可以直接在idea中查看编译后.class文件其显示的内容如下* String s1 abc;* String s2 abc;* 也可以通过 jclasslib 进行验证 */System.out.println(s1 s2); // trueSystem.out.println(s1.equals(s2)); // true}Testpublic void test2() {String s1 javaEE;String s2 hadoop;String s3 javaEEhadoop;String s4 javaEE hadoop;// 如果拼接符号的前后出现了 new则需要在堆空间中 new String()具体的内容为拼接后的结果String s5 s1 hadoop;String s6 javaEE s2;String s7 s1 s2;System.out.println(s3 s4); // trueSystem.out.println(s3 s5); // falseSystem.out.println(s3 s6); // falseSystem.out.println(s3 s7); // falseSystem.out.println(s5 s6); // falseSystem.out.println(s5 s7); // falseSystem.out.println(s6 s7); // false// intern()判断字符串常量池中是否存在 javaEEhadoop 值如果存在则返回常量池中 javaEEhadoop 的地址值// 如果字符串常量池中不存在 javaEEhadoop则向字符串常量池中添加 javaEEhadoop并返回 javaEEhadoop 的地址值String s8 s6.intern();System.out.println(s3 s8); // true}Testpublic void test3() {String s1 a;String s2 b;String s3 ab;String s4 s1 s2;System.out.println(s3 s4); // false}Testpublic void test4() {final String s1 a;final String s2 b;String s3 ab;String s4 s1 s2;System.out.println(s3 s4); // true}
}
test1() 方法分析 直接在idea中查看编译后的 SpringTest5.class文件 [外链图片转存中…(img-E2vqETLQ-1720061415937)] 使用 jclsslib可以看到两组命令完全一致。 [外链图片转存中…(img-FGk2v9nw-1720061415937)] test3() 分析 [外链图片转存中…(img-TWh5g7kR-1720061415938)] s1 s2 的执行细节如下 :::warning StringBuilder s new StringBuilder(); 这里变量 s 是为了方便理解而临时定义的 s.append(“a”); s.append(“b”); s.toString() -- 约等于 new String(“ab”); ::: 补充在 JDK5.0 以后使用的 StringBuilder在 JDK5.0 之前使用的 StringBuffer test4() 分析 字符串拼接操作不一定使用的是 StringBuilder 如果拼接符号左右两边都是字符串常量或常量引用则仍然使用编译期优化即非 StringBuilder 的方式。 在定义类、方法、基本数据类型、引用数据类型时能使用 final 建议使用。 变量 s1、s2 被 final 修饰后则变成了常量 final 在编译的时候就会分配了准备阶段会显示初始化。
拼接操作与 append 操作的效率对比
package chapter13;import org.junit.Test;public class StringTest5 {Testpublic void test6() {long start System.currentTimeMillis();
// method1(100000); //1804method2(100000); // 3long end System.currentTimeMillis();System.out.println(花费的时间为 (end - start));}private void method2(int highLevel) {StringBuilder src new StringBuilder();for (int i 0; i highLevel; i) {src.append(a);// 每次循环都会创建一个 StringBuilder、String}}private void method1(int highLevel) {String src ;for (int i 0; i highLevel; i) {src src a;}}
}
测试 method1 将 test6() 方法中的 method2() 注释掉 [外链图片转存中…(img-uYU75FcF-1720061415938)] 测试 method12将 test6() 方法中的 method1() 注释掉 [外链图片转存中…(img-dhPHwtsv-1720061415938)] 总结 通过 StringBuilder 的 append() 的方式添加字符串的效率要远高于使用 String 的字符串拼接方式。 原因
StringBuilder 的 append() 的方式自始至终只创建了一个 StringBuilder 的对象。使用 String 的字符串拼接方式创建了多个 StringBuilder 和 String 对象使用 String 的字符串拼接方式内存中由于创建了较多的 StringBuilder 和 String 的对象内存占用更大如果进行 GC需要花费额外的时间。
使用 StringBuilder 的 append() 的方式可继续改进 在实际开发中如果基本确定需要添加所有的字符串长度不高于某个限定值 highLevel 的情况下建议使用构造器 StringBuilder s new StringBuilder(highLevel); // new char[highLevel]。 StringBuilder 底层是使用得 char 型数组存储字符串的如果使用如果使用空参构造器append() 方法会确认 append 进来的字符串是否超出了当前字符数组的容量如果超出了就需要进行扩容。扩容次数过多了效率就会下降。所以在一开始就设置好字符数组的大小不用每次进行扩容。即使设置的容量不够append 也会自动进行扩容的。
intern() 的使用
概述
如果不是用双引号声明的 String 对象可以使用 String 提供的 intern 方法intern 方法会从字符串常量池中查询当前字符串是否存在若不存在就会将当前字符串放入常量池中。
比如String myInfo new String(“I love atguigu”).intern();
也就是说如果在任意字符上调用 String.intern 方法那么其返回结果所指向的那个类实例必须和直接以常量形式出现的字符串实例完全相同。因此下列表达式的值必定是 true :::warning (“a” “b” “c”).intern() “abc” ::: 通俗点讲Interned String 就是确保字符串在内存里只有一份拷贝这样可以节约内存空间加快字符串操作任务的执行速度。注意这个值会被存放在字符串内部池String Intern Pool。 如何保证变量 s 指向的是字符串常量池中的数据呢 有两种方式
方式一字面量定义的方式 String s “hello”; 方式二调用 intern 方法 String s new String(“hello”).intern();String s new StringBuilder(“hello”).toString().intern();
JDK6 VS JDK7/8 中的intern
示例一
问题
new String(“ab”) 会创建几个对象
package test.chapater13;public class StringNewTest {public static void main(String[] args) {String str new String(ab);}
}
两个。一个对象是 new 关键字在堆空间中创建的。另一个对象是字符串常量池中的对象字节码指令 ldc。 [外链图片转存中…(img-1YCwyQ4C-1720061415938)]
new String(“a”) new String(“b”) 会创建几个对象
package chapter13;public class StringNewTest {public static void main(String[] args) {String str new String(a) new String(b);}
}
[外链图片转存中…(img-fGlwdgiK-1720061415938)] 根据字节码文件分析 对象 1 new StringBuilder() 对象 2 new String(“a”) 对象 3 字符串常量池中的 “a” 对象 4 new Stirng(“b”) 对象 5 字符串常量池中的 “b” 对象 6 StringBuilder 的 toString() 方法– new String(“ab”) StringBuilder 的 toString() 方法的调用并没有在字符串常量池中生成 “ab” [外链图片转存中…(img-G6X5i813-1720061415938)]
示例二
package test.chapater13;public class StringIntern1 {public static void main(String[] args) {String s new String(1);s.intern(); // 调用此方法之前字符串常量池中已经存在”1“String s2 1;System.out.println(s s2); // jdk6: false jdk8: falseString s3 new String(1) new String(1);// s3变量记录的地址为new String(11)// 执行完上一行代码以后字符串常量池中并不存在11s3.intern(); // 在字符串常量池中生成11。// jdk6 : 创建了一个新的对象11也就有了新的地址// jdk7 : 此时常量池中并没有创建11而是创建了一个指向那个堆空间中new String(11)的地址String s4 11; // s4 变量记录的地址使用的是上一行代码执行时在常量池中生成的11的地址System.out.println(s3 s4); // jdk6: false jdk8: true}
}
JDK6 环境下测试结果 [外链图片转存中…(img-L6gOWZkY-1720061415939)] 变量 s 指向的是堆空间中的 String 对象由于字符串常量池中并没有对象 “1”new 在执行时同时向字符串常量池中添加了对象1变量 s2 指向的是字符串常量池中的对象1。所以 s s2 的结果为 false。s.intern() 方法加不加无所谓 new String() 操作会将创建的字符串添加到字符串常量池中。 变量 s3 最终指向了堆中对象11根据示例一可以得知new String(“1”) new String(“1”); 代码最终并没有把对象11 添加进字符串常量池中。s3.intern() 则会将 “11” 添加进字符串常量池。s4 指向的是字符串常量池中的对象 “11”。所以 s3 s4 的结果为false。 [外链图片转存中…(img-RltUbUdW-1720061415939)] JDK8 环境下测试结果 [外链图片转存中…(img-uyLuzPrh-1720061415939)] jdk8 中 s3.intern() 会将堆中 “11” 对象的地址拷贝一份到字符串常量池中。s4 “11” 检测到字符串常量池中已经存在 “11”会把字符串常量池中的 “11” 的地址赋给变量 s4即 s4 最终会指向堆中的 “11”。 [外链图片转存中…(img-kI9hv89K-1720061415939)]
示例三
package chapter13;public class StringIntern1 {public static void main(String[] args) {String s3 new String(1) new String(1);// 执行完上一行代码以后字符串常量池中不存在11String s4 11; // 在字符串常量池中生成对象11String s5 s3.intern();System.out.println(s3 s4);System.out.println(s5 s4);}
}
执行结果 [外链图片转存中…(img-K9POizS2-1720061415939)]
总结 String 的 intern() 的使用
JDK 1.6 中将这个字符串对象尝试放入字符串常量池。 如果字符串常量池中有则并不会放入返回已有的字符串常量池中的对象的地址如果没有会把此对象复制一份放入字符串常量池并返回字符串常量池中的对象地址。 JDK 1.7 起将这个字符串对象尝试放入字符串常量池 如果字符串常量池中有则并不会放入返回已有的字符串常量池中的对象的地址如果没有则会把对象的引用地址复制一份放入字符串常量池并返回字符串常量池中的引用地址
[外链图片转存中…(img-lRIJJP59-1720061415939)] s2 “ab” 最终比较的是地址值而ab的地址值则是指向了字符串常量池中的ab。
Intern 的空间效率测试
package chapter13;public class StringIntern2 {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) {
// 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();}}
}
未使用 intern 方法 [外链图片转存中…(img-Lc0O47Qp-1720061415939)] 使用 intern 方法 [外链图片转存中…(img-VhAPUHrW-1720061415940)] 可以看到在未使用 intern 方法的情况内存的占用率要远大于使用 intern 方法的。这是因为数组 data 中的值是固定的for 循环体是不断向字符数组中赋重复值只不过数组索引位置不同而已。也就是说字符串常量池中早就已经生成了1~10的对象。
arr[i] new String(String.valueOf(data[i % data.length])).intern();这段代码的执行逻辑是这样的首先在堆中生成 new String(String.valueOf(data[i % data.length]) 的对象生成后判断字符串常量池中是否存在如果存在则将字符串常量池中的对象地址返回给 arr[i]。此时 new String(String.valueOf(data[i % data.length]) 这个对象便没有了引用于是垃圾回收器便可以将其回收。这就是为什么使用 intern 方法后内存空间占用较小的原因。 String str new String(“abc”)str直接用new String“abc”创建abc这字符串在一出现就自动创建成对象存放到常量池中所以常量池里面存放的是abc字符串的引用并不是str创建的对象的引用。
结论对于程序中大量存在的字符串尤其其中存在很多复杂字符串时使用 intern() 可以节省内存空间。
.StringTable 的垃圾回收
参数设置 :::warning -Xms15m -Xmx15m -XX:PrintStringTableStatistics -XX:PrintGCDetails :::
G1 中的 String 去重操作
背景对许多 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这样以后的时候就可以共享这个数组了。 命令行选项 UseStringDeduplicationbool开启 String 去重默认是不开启的需要手动开启。PrintStringDeduplicationStatisticsbool打印详细的去重统计信息StringDeduplicationAgeThresholduintx达到这个年龄的 String 对象被认为是去重的候选对象