对于String类型,Java官网的文档是这样子描述的:
String类代表着字符串。java程序中的所有字符串字面值(如"abc")都作为此类的实例实现。
字符串是常量,他们的值在创建之后不能更改。因为 String 对象是不可变的,所以可以共享。
那么,jvm是怎么共享这些字符串的呢?
为了节省内存,提高资源的复用,jvm引入了常量池这个概念,它属于方法区的一部分的,作用之一就是存放编译期间生产的各种字面量和符号引用。而从前面的博文《深入了解JVM—内存区域》我们可以知道,方法区的垃圾回收行为是比较少出现的,该区中的对象基本不会被回收,可以理解成是永久存在的。
因此,缓存在字符串缓冲区中的字符串对象基本是不被回收的,而jvm也正是通过复用这些对象从而达到共享作用。
从上一段话中的概念可以知道,一般情况下,只有编译期间可以确定下来的的字符串才能存放到缓冲区中。为什么要强调是一般情况下呢?因为String类为我们提供了一个intern()方法,它可以帮我们将不存在于缓存池中的java字符串添加到缓存池中,并返回缓存池中该字符串对象的引用。
具体关于intern()方法,后面我们再给出代码做简单说明吧。现在我们将重点放在,什么情况下能够在编译期间直接确定字符串变量值并且将它添加到缓冲区中呢?
如果程序的字符串连接表达式中没有使用变量或者调用方法,那么该字符串变量的值就能够在编译期间确定下来,并且将该字符串缓存在缓冲区中,同时让该变量指向该字符串;否则将无法利用缓冲区,因为使用了变量和调用了方法之后的字符串变量的值只能在运行期间才能确定连接式的值,也就无法在编译期间确定字符串变量的值,从而无法将字符串变量增加到缓冲区并加以利用。
下面我们来看看如何通过代码及其编译过程来验证上述结论吧。
代码一(没有使用变量或者调用方法):
- package com.xiaoxuetu.string;
- public class Test {
- public static void main(String args[]) {
- String param1 = "abc";
- String param2 = "abc" + "def";
- String param3 = "abcdef";
- }
- }
我们先来解释下,ldc的含义是:将常量值从常量池中取出来并且压入栈中。从上图中,我们可以看到第0行、第3行和第6行中,程序分别从常量池中取出 "abc" 和 "abcdef" 并且压入栈中,而且,第3行和第6行中的字符串引用是同一个。这说明了,在编译期间,该字符串变量的值已经确定了下来,并且将该字符串值缓存在缓冲区中,同时让该变量指向该字符串值,后面如果有使用相同的字符串值,则继续指向同一个字符串值。
如果有安装jad的话,我们还可以通过jad -o -a Test.class命令来生成java代码和对应java编译后的ByteCode字节码一起的jad文件,如图所示:
代码二(使用了变量或者调用了方法):
- package com.xiaoxuetu.string;
- public class Application {
- public static void main(String[] args) {
- String param = "abc";
- String param1 = "3abc";
- String param2 = param.length() + "abc";
- }
- }
从上图中,我们看到了param的值引用是从常量池中取出的字符串"abc", param1的引用也是直接从常量池中取出的"3abc";但是param2的值并没有根据运算结果引用常量池中的“3abc”,而是返回调用当前StringBuilder对象的toString()后的生成的字符串引用。这点我们可以直接通过 param2 == param1 来判断,很明显输出结果就是false.
除此之外,我们还可以从该图中的第27行到第37行看出,javas在处理param2 = param.length() + "abc"的时候,是通过StringBuilder实例对象的append()方法来实现的。返回的是StringBuilder对象的引用,所以此时param2的值并没有引用常量池中缓存的已有的对象。对此,官网文档是这么解释的:
Java 语言提供对字符串串联符号("+")以及将其他对象转换为字符串的特殊支持。字符串串联是通过 StringBuilder(或 StringBuffer)类及其 append 方法实现的。字符串转换是通过 toString 方法实现的,该方法由 Object 类定义,并可被 Java 中的所有类继承。有关字符串串联和转换的更多信息,请参阅 Gosling、Joy 和 Steele 合著的 The Java Language Specification。
或许有人看了以后会有以下疑问:
1> 如果将前面代码二中的第6 、7行交换,变成如下:
- String param2 = param.length() + "abc";
- String param1 = "3abc";
答案是不会的,因为param2变量的字符串值必须在运行时才能确定下来,而不是概念中编译期间,真正将"3abc"缓存的反而会是param1这行代码。
2>如果我们通过String newStr = new String("abc");来创建字符串变量,那么abc会不会被缓存呢?而且会不会直接指向缓冲区中的变量呢?
好吧,我们继续看看代码然后通过查看编译消息进行分析:
代码三(使用了变量或者调用了方法):
- package com.xiaoxuetu.string;
- public class Application2 {
- public static void main(String[] args) {
- String param = "abc";
- String newStr = new String("cde");
- String param2 = "cde";
- }
- }
我们看到在创建newStr的String类型对象的时候,先从栈中取出字符串"cde",然后调用String的构造方法通过关键字new 进行创建对象的创建,将新的引用赋给newStr。因此newStr并没有指向缓冲区中的字符串“cde”,所以通过这种方法创建的字符串变量开销往往比较大。
接下来我们讲解一下intern()方法吧。关于这个方法,官网是这么描述的:
当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。
下面我们只给出一个intern()方法使用的例子,具体大家就自行研究咯。
- package com.xiaoxuetu.string;
- public class InternTest {
- public static void main(String[] args) {
- String param = "abc";
- String newStr = new String("abc");
- String param2 = new String("abc");
- newStr.intern();
- param2 = param2.intern(); //param2指向intern返回的常量池中的引用
- System.out.println(param == newStr); //false
- System.out.println(param == param2); //true
- }
- }
最后就总结一下判断字符串是否被缓存到缓冲区的两大要素 :
1>编译期间 : 也就说字符串连接式中没有使用变量或者调用方法。
2>是否使用了intern()方法 : 使用了该方法的字符串变量的值如果不存在缓冲区中将会被缓存。