一、String的基本特性
- String:字符串,使用一对 “” 引起来表示
- String被声明为final的,不可被继承
- String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示String可以比较大小
- String在jdk8及以前内部定义了
final char value[]
用于存储字符串数据。JDK9时改为byte[]
二、为什么 JDK9 改变了 String 的结构
官方文档:http://openjdk.java.net/jeps/254
为什么改为 byte[] 存储?
- String类的当前实现将字符存储在char数组中,每个字符使用两个字节(16位)。
- 从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分,而且大多数字符串对象只包含拉丁字符(Latin-1)。这些字符只需要一个字节的存储空间,因此这些字符串对象的内部char数组中有一半的空间将不会使用,产生了大量浪费。
- 之前 String 类使用 UTF-16 的 char[] 数组存储,现在改为 byte[] 数组 外加一个编码标识存储。该编码表示如果你的字符是ISO-8859-1或者Latin-1,那么只需要一个字节存。如果你是其它字符集,比如UTF-8,你仍然用两个字节存
- 结论:String再也不用char[] 来存储了,改成了byte [] 加上编码标记,节约了一些空间
- 同时基于String的数据结构,例如StringBuffer和StringBuilder也同样做了修改
2.1 String 的基本特性
- String:代表不可变的字符序列。简称:不可变性。
- 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
- 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
- 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
- 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
代码
字节码指令
- 取字符串 “abc” 时,使用的是同一个符号引用:#2
- 取字符串 “hello” 时,使用的是另一个符号引用:#3
当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
当调用string的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
一道笔试题
str 的内容并没有变:“test ok” 位于字符串常量池中的另一个区域(地址),进行赋值操作并没有修改原来 str 指向的引用的内容
2.2 String 的底层结构
字符串常量池是不会存储相同内容的字符串的
- String的String Pool(字符串常量池)是一个固定大小的Hashtable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern()方法时性能会大幅下降。
- 使用-XX:StringTablesize可设置StringTable的长度
- 在JDK6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快,StringTablesize设置没有要求
- 在JDK7中,StringTable的长度默认值是60013,StringTablesize设置没有要求
- 在JDK8中,StringTable的长度默认值是60013,StringTable可以设置的最小值为1009
测试不同 StringTable 长度下,程序的性能
代码
- -XX:StringTableSize=1009 :程序耗时 143ms
- -XX:StringTableSize=100009 :程序耗时 47ms
三、String 的内存分配
- 在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
- 常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。
- 直接使用双引号声明出来的String对象会直接存储在常量池中。比如:
String info="atguigu.com";
- 如果不是用双引号声明的String对象,可以使用String提供的intern()方法。这个后面重点谈
- Java 6及以前,字符串常量池存放在永久代
- Java 7中 Oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内
- 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
- 字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用String.intern()。
- Java8元空间,字符串常量在堆
3.1 StringTable 为什么要调整?
官方文档:https://www.oracle.com/java/technologies/javase/jdk7-relnotes.html#jdk7changes
- 为什么要调整位置?
- 永久代的默认空间大小比较小
- 永久代垃圾回收频率低,大量的字符串无法及时回收,容易进行Full GC产生STW或者容易产生OOM:PermGen Space
- 堆中空间足够大,字符串可被及时回收
- 在JDK 7中,interned字符串不再在Java堆的永久代中分配,而是在Java堆的主要部分(称为年轻代和年老代)中分配,与应用程序创建的其他对象一起分配。
- 此更改将导致驻留在主Java堆中的数据更多,驻留在永久生成中的数据更少,因此可能需要调整堆大小。
代码示例
输出结果:我真没骗你,字符串真的在堆中(JDK8)
四、String 的基本操作
Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String类实例。
4.1 举例1
public class StringTest4 {
public static void main(String[] args) {
System.out.println();//2293
System.out.println("1");//2294
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");//2303
//如下的字符串"1" 到 "10"不会再次加载
System.out.println("1");//2304
System.out.println("2");//2304
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");//2304
}
}
分析字符串常量池的变化
1、程序启动时已经加载了 2293 个字符串常量
2、加载了一个换行符(println),所以多了一个
3、加载了字符串常量 “1”~“9”
4、加载字符串常量 “10”
5、之后的字符串”1” 到 “10”不会再次加载
4.2 举例2
分析运行时内存(foo() 方法是实例方法,其实图中少了一个 this 局部变量)
五、字符串拼接操作
5.1 先说结论
- 常量与常量的拼接结果在常量池,原理是编译期优化
- 常量池中不会存在相同内容的变量
- 拼接前后,只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
- 如果拼接的结果调用intern()方法,根据该字符串是否在常量池中存在,分为:
- 如果存在,则返回字符串在常量池中的地址
- 如果字符串常量池中不存在该字符串,则在常量池中创建一份,并返回此对象的地址
1、常量与常量的拼接结果在常量池,原理是编译期优化
代码
从字节码指令看出:编译器做了优化,将 “a” + “b” + “c” 优化成了 “abc”
IDEA 反编译 class 文件后,来看这个问题
2、拼接前后,只要其中有一个是变量,结果就在堆中
调用 intern() 方法,则主动将字符串对象存入字符串常量池中,并将其地址返回
从字节码角度来看:拼接前后有变量,都会使用到 StringBuilder 类
5.2 字符串拼接的底层细节
举例1
字节码指令
举例2
从字节码角度来看:为变量 s4 赋值时,直接使用 #16 符号引用,即字符串常量 “ab”
拼接操作与 append 操作的效率对比
- 体会执行效率:通过StringBuilder的append()的方式添加字符串的效率要远高于使用String的字符串拼接方式!
- 原因:
- StringBuilder的append()的方式:
- 自始至终中只创建过一个StringBuilder的对象
- 使用String的字符串拼接方式:
- 创建过多个StringBuilder和String(调的toString方法)的对象,内存占用更大;
- 如果进行GC,需要花费额外的时间(在拼接的过程中产生的一些中间字符串可能永远也用不到,会产生大量垃圾字符串)。
- 改进的空间:
- 在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值highLevel的情况下,建议使用构造器实例化:
StringBuilder s = new StringBuilder(highLevel); //new char[highLevel]
- 这样可以避免频繁扩容
六、intern() 的使用
6.1 intern() 方法的说明
- intern是一个native方法,调用的是底层C的方法
- 字符串常量池池最初是空的,由String类私有地维护。在调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串内容相等的字符串,则返回池中的字符串地址。否则,该字符串对象将被添加到池中,并返回对该字符串对象的地址。(这是源码里的大概翻译)
- 如果不是用双引号声明的String对象,可以使用String提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。比如:
String myInfo = new string("I love atguigu").intern();
- 也就是说,如果在任意字符串上调用String.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true
("a"+"b"+"c").intern()=="abc"
- 通俗点讲,Interned String就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)
6.2 new String() 的说明
6.2.1 new String(“ab”)会创建几个对象?
字节码指令
0 new #2 <java/lang/String>
:在堆中创建了一个 String 对象
4 ldc #3 <ab>
:在字符串常量池中放入 “ab”(如果之前字符串常量池中没有 “ab” 的话)
6.2.2 new String(“a”) + new String(“b”) 会创建几个对象?
代码
字节码指令
答案是4个或5个或6个
字节码指令分析:
0 new #2 <java/lang/StringBuilder>
:拼接字符串会创建一个 StringBuilder 对象
7 new #4 <java/lang/String>
:创建 String 对象,对应于 new String(“a”)
11 ldc #5 <a>
:在字符串常量池中放入 “a”(如果之前字符串常量池中没有 “a” 的话)
19 new #4 <java/lang/String>
:创建 String 对象,对应于 new String(“b”)
23 ldc #8 <b>
:在字符串常量池中放入 “b”(如果之前字符串常量池中没有 “b” 的话)
31 invokevirtual #9 <java/lang/StringBuilder.toString>
:调用 StringBuilder 的 toString() 方法,会生成一个 String 对象
6.3 有点难的面试题
有点难的面试题
解释的已经比较清楚了,下面看一下内存图
内存分析
JDK6 :正常眼光判断即可
- new String() 即在堆中
- str.intern() 则把字符串放入常量池中
JDK7及后续版本,注意大坑
6.3.1 面试题的拓展
6.4 intern() 方法的练习
练习 1
JDK6
JDK7/8
练习2
练习3
验证
输出结果:
6.5 intern() 的效率测试(空间角度)
1、直接 new String :由于每个 String 对象都是 new 出来的,所以程序需要维护大量存放在堆空间中的 String 实例,程序内存占用也会变高
2、使用 intern() 方法:由于数组中字符串的引用都指向字符串常量池中的字符串,所以程序需要维护的 String 对象更少,内存占用也更低
结论:
- 对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用intern()方法能够节省很大的内存空间。
- 大的网站平台,需要内存中存储大量的字符串。比如社交网站,很多人都存储:北京市、海淀区等信息。这时候如果字符串都调用intern() 方法,就会很明显降低内存的大小。
七、StringTable 的垃圾回收
输出结果:
- 在 PSYoungGen 区发生了垃圾回收
- Number of entries 和 Number of literals 明显没有 100000
- 以上两点均说明 StringTable 区发生了垃圾回收
八、G1 中的 String 去重操作
官方文档:http://openjdk.java.net/jeps/192
暂时了解一下,后面会详解垃圾回收器
String去重操作的背景
注意不是字符串常量池的去重操作,字符串常量池本身就没有重复的
- 背景:对许多Java应用(有大的也有小的)做的测试得出以下结果:
- 堆存活数据集合里面String对象占了25%
- 堆存活数据集合里面重复的String对象有13.5%
- String对象的平均长度是45
- 许多大规模的Java应用的瓶颈在于内存,测试表明,在这些类型的应用里面,Java堆中存活的数据集合差不多25%是String对象。更进一步,这里面差不多一半String对象是重复的,重复的意思是说:
str1.equals(str2)= true
。堆上存在重复的String对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的String对象进行去重,这样就能避免浪费内存。
String 去重的的实现
- 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象。
- 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象。
- 使用一个Hashtable来记录所有的被String对象使用的不重复的char数组。当去重的时候,会查这个Hashtable,来看堆上是否已经存在一个一模一样的char数组。
- 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。
- 如果查找失败,char数组会被插入到Hashtable,这样以后的时候就可以共享这个数组了。
命令行选项
- UseStringDeduplication(bool) :开启String去重,默认是不开启的,需要手动开启。
- PrintStringDeduplicationStatistics(bool) :打印详细的去重统计信息
- stringDeduplicationAgeThreshold(uintx) :达到这个年龄的String对象被认为是去重的候选对象
- JVM字符串常量池StringTable
String的基本特性 String:字符串,使用一对""引起来表示. String sl = "hello"://字面量的定义方式: String s2 = ...
- 从HotSpot VM源码看字符串常量池(StringTable)和intern()方法
引言 字符串常量池(StringTable)是JVM中一个重要的结构,它有助于避免重复创建相同内容的String对象.那么StringTable是怎么实现的?"把字符串加入到字符串常量池中& ...
- Java中的常量池(字符串常量池、class常量池和运行时常量池)
转载. https://blog.csdn.net/zm13007310400/article/details/77534349 简介: 这几天在看Java虚拟机方面的知识时,看到了有几种不同常量池的 ...
- JVM 常量池、运行时常量池、字符串常量池
常量池: 即class文件常量池,是class文件的一部分,用于保存编译时确定的数据. 保存的内容如下图: D:\java\test\out\production\test>javap -ver ...
- Java String类相关知识梳理(含字符串常量池(String Pool)知识)
目录 1. String类是什么 1.1 定义 1.2 类结构 1.3 所在的包 2. String类的底层数据结构 3. 关于 intern() 方法(重点) 3.1 作用 3.2 字符串常量池(S ...
- Java String:字符串常量池(转)
作为最基础的引用数据类型,Java 设计者为 String 提供了字符串常量池以提高其性能,那么字符串常量池的具体原理是什么? 字符串常量池的设计思想是什么? 字符串常量池在哪里? 如何操作字符串常量 ...
- Class常量池、运行时常量池、字符串常量池的一些思考
Class常量池.运行时常量池.字符串常量池 class常量池 java代码经过编译之后都成了xxx.class文件,这是java引以为傲的可移植性的基石.class文件中,在CAFEBABE.主次版 ...
- Java中,那些关于String和字符串常量池你不得不知道的东西
老套的笔试题 在一些老套的笔试题中,会要你判断s1==s2为false还是true,s1.equals(s2)为false还是true. String s1 = new String("xy ...
- Java字符串常量池
JVM为了减少字符串对象的重复创建,维护了一个特殊的内存,这段内存被称为字符串常量池. Java中字符串对象的创建有两种形式:一种是字面量形式,String str = "a":一 ...
随机推荐
- 『无为则无心』Python基础 — 41、Python中文件的读写操作(一)
目录 1.文件操作步骤 2.文件的读写操作 (1)文件的打开 (2)打开文件模式 (3)获取一个文件对象 (4)关于文件路径 1.文件操作步骤 当我们要读取或者写入文件时,我们需要打开文件,在操作完毕 ...
- MySQL 5.7主从搭建(同一台机器)
主从复制原理:复制是 MySQL 的一项功能,允许服务器将更改从一个实例复制到另一个实例. 1)主服务器将所有数据和结构更改记录到二进制日志中. 2)从属服务器从主服务器请求该二进制日志并在本地应用其 ...
- C# 读取txt文件生成Word文档
本文将以C#程序代码为例介绍如何来读取txt文件中的内容,生成Word文档.在编辑代码前,可参考如下代码环境进行配置: Visual Studio 2017 .Net Framework 4.6.1 ...
- Vue之性能调优
打包优化 1. 屏蔽 sourceMap sourceMap作用:项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错. 在config目录的index.js ...
- Java的代理机制
Java的代理机制 使用代理 Proxzy 可以在运行时创建一组给定接口的新类,这种功能只有在编译时无法确定需要实现哪种接口时才需要使用. 1. 使用代理的时机 假如有一个表示接口的 Class 对象 ...
- Java编程中标识符注意点以及注释
标识符注意点 所有的标识符都应该以字母(A-Z或者a-z),美元符($),或者下划线(_)开始 首字符之后可以是字母(A-Z或者a-z),美元符($),下划线(_)或数 字的任何字符组合 不能使用关键 ...
- MySQL 1093 - You can't specify target table 'sc' for update in FROM clause
错误代码如下: #(8) 把"邓维杰"同学的成绩全部删除. SELECT * FROM sc WHERE EXISTS(SELECT * FROM student WHERE st ...
- iOS 即使通讯第三方SDK 资料
第三方即时通讯SDK,下面是一些主流的第三方的即时通讯SDK,尽管不能查看里面的源代码,但通过查看头文件,能为实现自己的即使通讯SDK提供很好的思路.(备用) 容云 容联.云通讯 IMSDK - 轻松 ...
- php函数(parse_str()
parse_str()函数 把查询字符串解析到变量中 parse_str(string, array); string 规定要解析的字符串 array 存储变量的数组名称 例子: <?php p ...
- ios xib约束适配要点
基本上以下几点就能解决除横屏以外的适配问题 1.页边距约束 (Leading and Trailing space) 页边间距约束分前部间距约束(Leading space constaint)和尾部 ...