目录
一、String讲解
1.String(String字符串常量)
2.String 拼接方式与性能的影响
二、StringBuffer 和 StringBuilder 讲解
1.StringBuffer 和 StringBuilder 使用场景:(StringBuffer、StringBuilder字符串变量)
2.StringBuffer的使用
3.StringBuffer与StringBuilder的线程安全性问题
4.synchronized的含义:(线程同步机制)
5.String和StringBuffer的效率问题
6.StringBuffer与String的可变性问题。
三、String、StringBuffer、StringBuilder各指标参数
四、String、StringBuffer、StringBuilder的继承结构
五、String、StringBuffer、StringBuilder在JVM中的原理讲解
1. Java class文件结构 和常量池
2.JVM运行class文件
3.通过操作码|助忆符|指令 (二进制指令)区别字符串对象的创建方式
六、常见争论
【常见争论1】关于字符串相等关系的争论
【常见争论2】 字符串“+”操作的是否相等
七、String、StringBuffer和StringBuilder知识总结
1.值的可变性
2.线程安全
3.三者性能
4.三者数据量与类型选择与使用推荐
5.数据存储
八、拓展相关知识
1.操作码|助忆符|指令相关讲解
2.String.intern()方法讲解
String 类型和 StringBuffer 类型的主要性能区别其实在于 String 是不可变的对象(final修饰), 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象,这样不仅效率低下,而且大量浪费有限的内存空间,所以经常改变内容的字符串最好不要用 String 。因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会相当慢。
我们可以看到,初始String值为“hello”,然后在这个字符串后面加上新的字符串“world”,这个过程是需要重新在栈堆内存中开辟内存空间的,最终得到了“hello world”字符串也相应的需要开辟内存空间,这样短短的两个字符串,却需要开辟三次内存空间,不得不说这是对内存空间的极大浪费。为了应对经常性的字符串相关的操作,就需要使用Java提供的其他两个操作字符串的类——StringBuffer类和StringBuild类来对此种变化字符串进行处理。
在某些特别情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢,而特别是以下的字符串对象生成中, String 效率是远要比 StringBuffer 快的:
(1)String对象初始化之前赋值前拼接字符串方式
String S1 = "This is only a" + " simple" + " test";
StringBuffer Sb = new StringBuffer("This is only a").append(" simple").append(" test");
String对象初始值之前进行赋值,拼接字符串方式比较快。
(2)String对象间的字符串拼接方式
String S2 = “This is only a”; String S3 = “ simple”;
String S4 = “ test”; String S1 = S2 +S3 + S4;//调用StringBuilder的toString()方法在堆中创建的String对象
多个String对象间的字符串拼接方式比较慢,因为需要调用stringbuilder的方法在堆中创建新的 string对象。
这时候 JVM 会规规矩矩的按照原来的方式去做。
实例解析:
你会很惊讶的发现,生成 String S1 对象的速度简直太快了,而这个时候 StringBuffer 居然速度上根本一点都不占优势。其实这是 JVM 的一个把戏,在 JVM 眼里,这个 String S1 = “This is only a” + “ simple” + “test”; 其实就是:String S1 = “This is only a simple test”; 所以当然不需要太多的时间了。但大家这里要注意的是,如果你的字符串是来自另外的 String 对象的话,速度就没那么快了,譬如(2)string对象间的字符串拼接方式:这时候 JVM 会规规矩矩的按照原来的方式去做。
StringBuffer 和 StringBuilder 特点及使用场景:
1>.应用于对字符串进行修改的时候,特别是字符串对象经常改变的情况下。
2>.和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。
3>.由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。
Java.lang.StringBuffer线程安全的可变字符序列。一个类似于 String 的字符串缓冲区,但不能修改(可以末尾添加和指定地点添加字符却不能修改)。虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。可将字符串缓冲区安全地用于多个线程。可以在必要时对这些方法进行同步,因此任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。
StringBuffer和StringBuilder 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。append 方法始终将这些字符添加到缓冲区的末端;而 insert 方法则在指定的点添加字符。
append():始终将这些字符添加到缓冲区的末端
insert():在指定的点添加字符
实例举例:
如果 z 引用一个当前内容是“start”的字符串缓冲区对象,z.append("le") 字符串缓冲区包含“startle”。同样的“start”, z.insert(4, "le") 将更改字符串缓冲区,使之包含“starlet”。
StringBuffer线性同步,StringBuilder 非线性同步
StringBuffer和StringBuilder可以算是双胞胎了,这两者的方法没有很大区别。但在线程安全性方面,StringBuffer允许多线程进行字符操作。这是因为在源代码中StringBuffer的很多方法都被关键字synchronized (同步锁)修饰了。
java.lang.StringBuilder一个可变的字符序列是5.0新增的。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的方法基本相同。
每一个类对象都对应一把锁,当某个线程A调用类对象中的synchronized方法M时,必须获得对象的锁才能够执行M方法,否则线程A阻塞。一旦线程A开始执行M方法,将独占对象的锁。使得其它需要调用对象的M方法的线程阻塞。只有线程A执行完毕,释放锁后。那些阻塞线程才有机会重新调用M方法。这就是解决线程同步问题的锁机制。
synchronized是java里的一个关键字,可以用来给对象和方法或者代码块加锁,当它锁定一个方法或者代码块的时候,同一时刻最多只有一条线程执行这段代码。
多线程编程中StringBuffer比StringBuilder要安全 。如果有多个线程需要对同一个字符串缓冲区进行操作的时候,StringBuffer应该是不二选择。
注意: 对于String是否安全的说法,String是不可变的。线程对于堆中指定的一个String对象只能读取,所以很安全。
首先说明一点:StringBuffer和StringBuilder可谓双胞胎,StringBuilder是1.5新引入的,其前身就是StringBuffer。StringBuilder的效率比StringBuffer稍高,如果不考虑线程安全,StringBuilder应该是首选。另外,JVM运行程序主要的时间耗费是在创建对象和回收对象上。
我们用下面的代码运行1W次字符串的连接操作,测试String,StringBuffer所运行的时间。
//测试代码
public class RunTime{
public static void main(String[] args){
● 测试代码位置1
long beginTime=System.currentTimeMillis();
for(int i=0;i<10000;i++){
● 测试代码位置2
}
long endTime=System.currentTimeMillis();
System.out.println(endTime-beginTime);
}
}
(1) String常量与String(对象间的拼接)变量的"+"操作比较
▲测试①代码:
(测试代码位置1)
String str="";
(测试代码位置2)
str="Heart"+"Raid";
[耗时: 0ms]
▲测试②代码
(测试代码位置1)
String s1="Heart";
String s2="Raid";
String str="";
(测试代码位置2)
str=s1+s2;
[耗时: 15—16ms]
结论:String常量的“+连接” 速度稍优于 String变量(对象间的拼接)的“+连接”。
实例解析:
测试①的"Heart"+"Raid"在编译阶段就已经连接起来,形成了一个字符串常量"HeartRaid",并指向堆中的拘留字符串对象。运行时只需要将"HeartRaid"指向的拘留字符串对象地址取出1W次,存放在局部变量str中。这确实不需要什么时间。
测试②中str=s1+s2,局部变量s1和s2存放的是两个不同的拘留字符串对象的地址。然后会通过下面三个步骤完成“+连接”:
StringBuilder temp=new StringBuilder(s1),
temp.append(s2);
str=temp.toString();
我们发现,虽然在中间的时候也用到了append()方法,但是在开始和结束的时候分别创建了StringBuilder和String对象。可想而知:调用1W次,是不是就创建了1W次这两种对象,不划算。但是,String变量的"+连接"操作比String常量的"+连接"操作使用的更加广泛。
(2)String对象的"累+"连接操作与StringBuffer对象的append()累和连接操作比较。
▲测试①代码:
(代码位置1)
String s1="Heart";
String s="";
(代码位置2)
s=s+s1;
[耗时: 4200—4500ms]
▲测试②代码
(代码位置1)
String s1="Heart";
StringBuffer sb=new StringBuffer();
(代码位置2)
sb.append(s1);
[耗时: 0ms(当循环100000次的时候,耗时大概16—31ms)]
结论:大量字符串累加时,StringBuffer的append()效率远好于String对象的"累+"连接
实例解析:测试① 中的s=s+s1,JVM会利用首先创建一个StringBuilder,并利用append方法完成s和s1所指向的字符串对象值的合并操作,接着调用StringBuilder的 toString()方法在堆中创建一个新的String对象,其值为刚才字符串的合并结果。而局部变量s指向了新创建的String对象。
因为String对象中的value[]是不能改变的,每一次合并后字符串值都需要创建一个新的String对象来存放。循环1W次自然需要创建1W个String对象和1W个StringBuilder对象,效率低就可想而知了。
测试②中sb.append(s1);只需要将自己的value[]数组不停的扩大来存放s1即可。循环过程中无需在堆中创建任何新的对象。效率高就不足为奇了。
我们先看看这两个类的部分源代码:
//String (final不可变值)
public final class String
{
private final char value[];
public String(String original) {
// 把原字符串original切分成字符数组并赋给value[];
}
}
//StringBuffer (非final可变值)
public final class StringBuffer extends AbstractStringBuilder
{
char value[]; //继承了父类AbstractStringBuilder中的value[]
public StringBuffer(String str) {
super(str.length() + 16); //继承父类的构造器,并创建一个大小为str.length()+16的value[]数组
append(str); //将str切分成字符序列并加入到value[]中
}
}
很显然,String和StringBuffer中的value[]都用于存储字符序列。但是,
(1) String中的是常量(final)数组,只能被赋值一次。
比如:new String("abc")使得value[]={'a','b','c'}(查看jdk String 就是这么实现的),之后这个String对象中的value[]再也不能改变了。这也正是大家常说的,String是不可变的原因 。
注意:这个对初学者来说有个误区,有人说String str1=new String("abc"); str1=new String("cba");不是改变了字符串str1吗?那么你有必要先搞懂对象引用和对象本身的区别。这里我简单的说明一下,对象本身指的是存放在堆空间中的该对象的实例数据(非静态非常量字段)。而对象引用指的是堆中对象本身所存放的地址,一般方法区和Java栈中存储的都是对象引用,而非对象本身的数据。
(2) StringBuffer中的value[]就是一个很普通的数组,而且可以通过append()方法将新字符串加入value[]末尾。这样也就改变了value[]的内容和大小了。
比如:new StringBuffer("abc")使得value[]={'a','b','c','',''...}(注意构造的长度是str.length()+16)。如果再将这个对象append("abc"),那么这个对象中的value[]={'a','b','c','a','b','c',''....}。这也就是为什么大家说 StringBuffer是可变字符串 的涵义了。从这一点也可以看出,StringBuffer中的value[]完全可以作为字符串的缓冲区功能。其累加性能是很不错的。
总结:String和StringBuffer可不可变。本质上是指对象中的value[]字符数组可不可变,而不是对象引用可不可变。
类型 |
详情 |
参数变化 |
线程安全 |
多线程支持 |
String |
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间 |
不可变 |
线程安全 |
|
StringBuffer |
StringBuffer是可变类和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量 |
可变 |
线程安全 |
多线程操作字符串 |
StringBuilder |
可变类,速度更快 |
可变 |
线程不安全 |
单线程操作字符串 |
CharSequence接口:
表示是一个字符序列,提供了一系列抽象方法,不能用在set和map这些类集中,String、StringBuffer、StringBuilder都实现了这个接口,因此他们都属于字符序列,并且要覆写CharSequence中的几个操作方法 。
Appendable接口:
表示实现此接口的子类可以对字符或字符串进行追加操作【提供有append(char c),append(CharSequence s)方法】,StringBuffer和StringBuilder都实现了这个接口,它们可以进行字符序列的追加操作 ,而String没有实现此接口,因此它不能进行拼接操作,也就不能改变内容。此接口并没有规定多线程访问是否安全,要在子类中规定。通过观察源码可以发现,StringBuilder和StringBuffer操作方法几乎一样,而唯一不同的是,StringBuffer中大部分方法都使用synchronized关键字声明,完成了线程了同步操作,而StringBuilder没有,因此,StringBuffer的安全性更好。但相反的,线程同步带来的问题就是性能问题,每条线程都要排队等待,而StringBuilder由于线程异步,具有更好的性能。
我们都知道,Java程序要运行,首先需要编译器将源代码文件编译成字节码文件(也就是.class文件)。然后在由JVM解释执行。
class文件是8位字节的二进制流 。这些二进制流的涵义由一些紧凑的有意义的项组成。比如class字节流中最开始的4个字节组成的项叫做魔数 (magic),其意义在于分辨class文件(值为0xCAFEBABE)与非class文件。class字节流大致结构如下图左侧。
其中,在class文件中有一个非常重要的项——(constant pool)常量池 。这个常量池专门放置源代码中的符号信息(并且不同的符号信息放置在不同标志的常量表中)。如上图右侧是HelloWorld代码中的常量表(HelloWorld代码如下),其中有四个不同类型的常量表(四个不同的常量池入口)。
public class HelloWorld{
void hello(){
System.out.println("Hello world");
}
}
通过上图可见,代码中的"Hello world"字符串字面值被编译之后,可以清楚的看到存放在了class常量池中的字符串常量表中(上图右侧红框区域)。
源代码编译成class文件之后,JVM就要运行这个class文件。它首先会用类装载器加载进class文件。然后需要创建许多内存数据结构来存放class文件中的字节数据。比如class文件对应的类信息数据、常量池结构、方法中的二进制指令序列、类方法与字段的描述信息等等。当然,在运行的时候,还需要为方法创建栈帧等。这么多的内存结构当然需要管理,JVM会把这些东西都组织到几个“运行时数据区 ”中。
上面我们提到了,在Java源代码中的每一个字面值字符串,都会在编译成class文件阶段,形成标志号 为8(CONSTANT_String_info)的常量表 。 当JVM加载 class文件的时候,会为对应的常量池建立一个内存数据结构,并存放在方法区中。同时JVM会自动为CONSTANT_String_info常量表中 的字符串常量字面值 在堆中创建新的String对象(intern字符串 对象,又叫拘留字符串对象)。然后把CONSTANT_String_info常量表的入口地址转变成这个堆中String对象的直接地址(常量池解析)。
这里很关键的就是这个拘留字符串对象 。源代码中所有相同字面值的字符串常量只可能建立唯一一个拘留字符串对象。 实际上JVM是通过一个记录了拘留字符串引用的内部数据结构来维持这一特性的。在Java程序中,可以调用String的intern()方法来使得一个常规字符串对象成为拘留字符串对象。
根据二进制指令来区别两种字符串对象的创建方式:
(1) String s=new String("Hello world");编译成class文件后的指令(在myeclipse中查看):
Class字节码指令集代码:
0 new java.lang.String [15] //在堆中分配一个String类对象的空间,并将该对象的地址堆入操作数栈。
3 dup //复制操作数栈顶数据,并压入操作数栈。该指令使得操作数栈中有两个String对象的引用值。
4 ldc [17] //将常量池中的字符串常量"Hello world"指向的堆中拘留String对象的地址压入操作数栈
6 invokespecial java.lang.String(java.lang.String) [19] //调用String的初始化方法,弹出操作数栈栈顶的两个对象地址,用拘留String对象的值初始化new指令创建的String对象,然后将这个对象的引用压入操作数栈
9 astore_1 [s] // 弹出操作数栈顶数据存放在局部变量区的第一个位置上。此时存放的是new指令创建出的,已经被初始化的String对象的地址 (此时的栈顶值弹出存入局部变量中去)。
注意:
【这里有个dup指令。其作用就是复制之前分配的Java.lang.String空间的引用并压入栈顶。那么这里为什么需要这样么做呢?因为invokespecial指令通过[15]这个常量池入口寻找到了java.lang.String()构造方法,构造方法虽然找到了。但是必须还得知道是谁的构造方法,所以要将之前分配的空间的应用压入栈顶让invokespecial命令应用才知道原来这个构造方法是刚才创建的那个引用的,调用完成之后将栈顶的值弹出。之后调用astore_1将此时的栈顶值弹出存入局部变量中去。】
事实上,在运行这段指令之前,JVM就已经为"Hello world"在堆中创建了一个拘留字符串( 值得注意的是:如果源程序中还有一个"Hello world"字符串常量,那么他们都对应了同一个堆中的拘留字符串)。然后用这个拘留字符串的值来初始化堆中用new指令创建出来的新的String对象,局部变量s实际上存储的是new出来的堆对象地址。 大家注意了,此时在JVM管理的堆中,有两个相同字符串值的String对象:一个是拘留字符串对象,一个是new新建的字符串对象。
(2)将String s="Hello world";编译成class文件后的指令:
Class字节码指令集代码
0 ldc [15]//将常量池中的字符串常量"Hello world"指向的堆中拘留String对象的地址压入操作数栈
2 astore_1 [str] // 弹出操作数栈顶数据存放在局部变量区的第一个位置上。此时存放的是拘留字符串对象在堆中的地址
和上面的创建指令有很大的不同,局部变量s存储的是早已创建好的拘留字符串的堆地址(没有new 的对象了)。 大家好好想想,如果还有一条穿件语句String s1="Hello word";此时堆中有几个值为"Hello world"的字符串呢?答案是1个。
String类型脱光了其实也很普通。真正让她神秘的原因就在于CONSTANT_String_info常量表 和拘留字符串对象 的存在。
//代码1
String sa=new String("Hello world");
String sb=new String("Hello world");
System.out.println(sa==sb); // false
//代码2
String sc="Hello world";
String sd="Hello world";
System.out.println(sc==sd); // true
实例解析:
代码1中局部变量sa,sb中存储的是JVM在堆中new出来的两个String对象的内存地址。虽然这两个String对象的值(char[]存放的字符序列)都是"Hello world"。 因此"=="比较的是两个不同的堆地址。代码2中局部变量sc,sd中存储的也是地址,但却都是常量池中"Hello world"指向的堆的唯一的那个拘留字符串对象的地址 。自然相等了。
结论:
- 在JVM在堆中new出来的两个String对象的内存地址,值相同,但是堆地址不同。
- 在String直接赋值时,都是常量池中指向的堆的唯一的拘留字符串对象的地址。值相同,堆地址也相等。(String直接赋值赋值时会在常量池中查看是否存在该字符串,若存在直接保存该值的堆地址,然而new则不同,都会重新创建并生成堆地址)。
//代码1
String sa = "ab"; //拘留字符串对象
String sb = "cd"; //拘留字符串对象
String sab=sa+sb; //调用StringBuilder的toString()方法在堆中创建的String对象
String s="abcd"; //拘留字符串对象
System.out.println(sab==s); // false
//代码2
String sc="ab"+"cd";
String sd="abcd";
System.out.println(sc==sd); //true
实例解析:
代码1中局部变量sa,sb存储的是堆中两个拘留字符串对象的地址。而当执行sa+sb时,JVM首先会在堆中创建一个StringBuilder类,同时用sa指向的拘留字符串对象完成初始化,然后调用append方法完成对sb所指向的拘留字符串的合并操作,接着调用StringBuilder的toString()方法在堆中创建一个String对象,最后将刚生成的String对象的堆地址存放在局部变量sab中。而局部变量s存储的是常量池中"abcd"所对应的拘留字符串对象的地址。 sab与s地址当然不一样了。这里要注意了,代码1的堆中实际上有五个字符串对象:三个拘留字符串对象、一个String对象和一个StringBuilder对象。
代码2中"ab"+"cd"会直接在编译期就合并成常量"abcd", 因此相同字面值常量"abcd"所对应的是同一个拘留字符串对象,自然地址也就相同。
结论:
- 字符串在进行两个对象合并后,创建StringBuilder并调用StringBuilder的toString()方法在堆中创建新的String对象,从而在堆中生成新的堆地址。而字符串直接赋值则是在常量池中,所对应的拘留字符串对象的地址。
- 两个字符串直接在编译期就拼接合并与字符串直接赋值,他们编译前都是相同的字面值常量,所对应的也会是同一个拘留字符串对象,自然地址也就相同。
性能比较:String
使用解析:
在编译阶段就能够确定的字符串常量,没有必要创建String或StringBuffer对象。直接使用字符串常量的"+"连接操作效率最高。
StringBuffer对象的append效率要高于String对象的"+"连接操作。
不停的创建对象是程序低效的一个重要原因。那么相同的字符串值能否在堆中只创建一个String对象呢。显然拘留字符串能够做到这一点,除了程序中的字符串常量会被JVM自动创建拘留字符串之外,调用String的intern()方法也能做到这一点。当调用intern()时,如果常量池中已经有了当前String的值,那么返回这个常量指向拘留对象的地址。如果没有,则将String值加入常量池中,并创建一个新的拘留字符串对象。
注意:初始化上的区别,String可以空赋值,后者不行,报错。
String存储在字符串常量池中。StringBuffer和StringBuilder存储在堆内存空间。
(1).指令
指令都是二进制的,一条指令长度不一,从一些微控制器中的4位(bit)到一些超长指令字系统中的几百位。指令的种类和多少与具体的机型有关。其实如果在class文件中说0a 是一个指令也可以(如下图表示的)并有与之相对应的助记符。同时图二opcode也可以说是一个指令。
图一:
(二)指令分类:操作数(二级制),操作码(二进制值)。操作码决定要完成的操作其实就是指令序列号,java虚拟机操作码的长度限制为一个字节,用来告诉CPU执行哪条指令。操作数指参加运算的数据及其所在的单元地址。
注意:
(1)因为虚拟机的操作码长度只有一个字节,所以包含了数据类型的操作码就为指令集的设计带来了很压力。如果每一种与数据类型相关的指令都支持Java虚拟机所有运行数据类型的话,那么指令的数量恐怕就超出一个字节所能表示的数量范围了,即并非每种数据类型和每一种操作都有对应的指令。
(2)大部分指令都没有支持byte,char,boolean,short,都是使用相应的对int 类型作为运算类型运行的。
(2).助记符
为了反映执行指令的功能所做的英文简写
图二:
使用new创建的字符串对象,如果想将这个对象的引用加入到字符串常量池,可以使用intern()方法。调用intern()后,首先检查字符串常量池中是否有该对象的引用,如果存在,则将这个引用返回给变量,否则将引用加入并返回给变量。
(1).String.intern()使用原理
String.intern()是一个Native方法,底层调用C++的 StringTable::intern方法实现。当通过语句str.intern()调用intern()方法后,JVM 就会在当前类的常量池中查找是否存在与str等值的String,若存在则直接返回常量池中相应Strnig的引用;若不存在,则会在常量池中创建一个等值的String,然后返回这个String在常量池中的引用。因此,只要是等值的String对象,使用intern()方法返回的都是常量池中同一个String引用,所以,这些等值的String对象通过intern()后使用==是可以匹配的。
(2).实例解析:
代码一:
String s = "ssc"//这时添加到常量池中
String s2 = new String("ssc");
String s3 = s2.intern();//这时常量池相同字符已存在,intern()直接先从常量池中拿引用地址
System.out.println(s == s3); // true
代码二:
String s1 = new String("s") + new String("sc");
s1.intern();//常量池字符不存在,则会在常量池中创建一个等值的String,然后返回这个String在常量池中的引用
String s3 = "ssc";//直接获取现有常量池的引用地址
System.out.println(s1 == s3); // true
代码三:
String s = "ssc";
String s1 = "ss";
String s2 = "c";
String s3 = s1+s2;
System.out.println(s == s3.intern()); // true
//s3.intern()这时会将s之前存在常量池的引用地址直接使用
intern();这个方法会导致,jvm会检测字符串常量池中是否有对象“ssc”的引用,如果没有,则添加此对象的引用到常量池中去;如果有,会把指向堆中对象“ssc”的引用赋值给s3 。也就是说,s3也会指向堆。
(3).String.intern() jdk6\7版本的区别
JDK6 |
JDK7及之后 |
|
intern()方法 |
会把首次遇到的字符串实例复制到常量池中,并返回此引用 |
会把首次遇到的字符串实例的引用添加到常量池中,并返回此引用 |
String.intern() in JDK6
Jdk6中常量池位于PermGen(永久代)中,PermGen是一块主要用于存放已加载的类信息和字符串池的大小固定的区域。执行intern()方法时,若常量池中不存在等值的字符串,JVM就会在常量池中创建一个等值的字符串,然后返回该字符串的引用。除此以外,JVM 会自动在常量池中保存一份之前已使用过的字符串集合。Jdk6中使用intern()方法的主要问题就在于常量池被保存在PermGen中:首先,PermGen是一块大小固定的区域,一般不同的平台PermGen的默认大小也不相同,大致在32M到96M之间。所以不能对不受控制的运行时字符串(如用户输入信息等)使用intern()方法,否则很有可能会引发PermGen内存溢出;其次String对象保存在Java堆区,Java堆区与PermGen是物理隔离的,因此如果对多个不等值的字符串对象执行intern操作,则会导致内存中存在许多重复的字符串,会造成性能损失。
String.intern() in JDK7
Jdk7将常量池从PermGen区移到了Java堆区,执行intern操作时,如果常量池已经存在该字符串,则直接返回字符串引用,否则复制该字符串对象的引用到常量池中并返回。堆区的大小一般不受限,所以将常量池从PremGen区移到堆区使得常量池的使用不再受限于固定大小。除此之外,位于堆区的常量池中的对象可以被垃圾回收。当常量池中的字符串不再存在指向它的引用时,JVM就会回收该字符串。可以使用 -XX:StringTableSize 虚拟机参数设置字符串池的map大小。字符串池内部实现为一个HashMap,所以当能够确定程序中需要intern的字符串数目时,可以将该map的size设置为所需数目*2(减少hash冲突),这样就可以使得String.intern()每次都只需要常量时间和相当小的内存就能够将一个String存入字符串池中。
(4).intern()适用场景
Jdk6中常量池位于PermGen区,大小受限,所以不建议适用intern()方法,当需要字符串池时,需要自己使用HashMap实现。Jdk7、8中,常量池由PermGen区移到了堆区,还可以通过-XX:StringTableSize参数设置StringTable的大小,常量池的使用不再受限,由此可以重新考虑使用intern()方法。intern()方法优点:执行速度非常快,直接使用==进行比较要比使用equals()方法快很多;内存占用少。虽然intern()方法的优点看上去很诱人,但若不是在恰当的场合中使用该方法的话,便非但不能获得如此好处,反而还可能会有性能损失。
下面程序对比了使用intern()方法和未使用intern()方法存储100万个String时的性能,从输出结果可以看出,若是单纯使用intern()方法进行数据存储的话,程序运行时间要远高于未使用intern()方法时:
public class InternTest {
public static void main(String[] args) {
print("noIntern: " + noIntern());
print("intern: " + intern());
}
private static long noIntern(){
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
int j = i % 100;
String str = String.valueOf(j);
}
return System.currentTimeMillis() - start;
}
private static long intern(){
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
int j = i % 100;
String str = String.valueOf(j).intern();
}
return System.currentTimeMillis() - start;
}
}
程序运行结果:
noIntern: 48 // 未使用intern方法时,存储100万个String所需时间
intern: 99 // 使用intern方法时,存储100万个String所需时间
(5).String.intern()总结
String.intern()方法是一种手动将字符串加入常量池中的方法,原理如下:如果在常量池中存在与调用intern()方法的字符串等值的字符串,就直接返回常量池中相应字符串的引用,否则在常量池中复制一份该字符串,并将其引用返回(Jdk7中会直接在常量池中保存当前字符串的引用);Jdk6 中常量池位于PremGen区,大小受限,不建议使用String.intern()方法,不过Jdk7 将常量池移到了Java堆区,大小可控,可以重新考虑使用String.intern()方法,但是由对比测试可知,使用该方法的耗时不容忽视,所以需要慎重考虑该方法的使用;String.intern()方法主要适用于程序中需要保存有限个会被反复使用的值的场景,这样可以减少内存消耗,同时在进行比较操作时减少时耗,提高程序性能。