Java 学习目录
上一章 JVM 深入理解 - 内存区域
说明
JVM 中的字符串常量池是一个非常有争议的概念,各类书籍和网站上众说纷纭。而且没有官方定义,所以我们从他的作用和 JVM 设计他解决什么问题来分析它。
字符串常量池
字符串驻留
字符串驻留(String interning)是字符串常量池产生的根本原因。英文维基上解释,大意如下:
所谓字符串驻留,是指在系统中,对每个字面量唯一的字符串,都只保留唯一的一份副本,称作“驻留量”(intern),并且它们都是不可变的。这些彼此不同的字符串被存储在字符串常量池中。
各编程语言有各自的方法来取得字符串常量池中的驻留量,或者将一个字符串驻留,比如Java中的String.intern()。在Java中,所有编译期能确定的字符串也都会自动驻留。
字符串字面量
前面提到了字符串字面量(String literal)的概念。Java语言规范中说:
字符串字面量是双引号括起来的0或多个字符。它是对String类实例的引用。
一个字符串字面量总是引用String类的同一个实例。这是因为字符串字面量以及字符串常量表达式都通过使用String.intern()方法而驻留了,从而可以共享唯一的实例。
什么是字符串常量池
在 JVM 运行期间字符串所占用的空间绝对是数一数二的,而字符串常量池就是针对字符串在 JVM 所占空间进行的优化手段。
- 首先对象分配需要付出时间和空间上的开销,字符串可以说是和 8 个基本类型一样常用的类型,甚至比 8 个基本类型更加常用,频繁的创建字符串对象,对性能的影响是非常大的,所以用常量池的方式可以很大程度上降低对象创建、分配内存空间的开销,从而优化 JVM 的性能。
- 当然 JVM 在 8 种基本类型上也是有优化的。8 种基本类型的常量池都是系统协调的。
比如 Integer [-128,127] 区间内的 Integer 被缓存在内部类 IntegerCache 中,这个类就相当于整形常量池。在该区间内两个数值相同的整形值,在自动装箱后实际上指向堆内的同一个 Integer 对象(也就是驻留量),可以参考Integer.valueOf()方法的源码,当然这个大小是可以设置的。
-XX:AutoBoxCacheMax= size
后边会有一个章节专门讲解。
特性
常量池中不存在两个相同的对象
存放位置
JDK 1.6 方法区(永久代)。
JDK 1.7 以上(含)实际存储地址是 “堆”(但逻辑上还是归 方法区 的,因为 JVM 规范是这么规定的)。
数据结构
在 HotSpot 中字符串常量池是通过一个 StringTable 类(一个 Hash 表,非 Java 实现)实现的;这个 StringTable 在每个 HotSpot 的实例中只有一份,被所有的线程共享。
存放内容
在 JDK 1.6 及之前版本:字符串常量池中放的都是字符串常量;
在 JDK 1.7 中:由于 String.intern() 发生了改变,因此字符串常量池中存放放于堆内的字符串对象的引用。
放入规则
什么样的数据会放入字符串常量池中?
- 直接使用双引号声明出来的 String 对象会直接存储在常量池中。也就是说,编译器可以识别的字符串字面量都可以放入字符串常量池中。
- 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
深入了解
我们都简单的了解了字符串常量池的特性,以及简单的原理了。下面我们通过代码来了解下。
- String 部分源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
.........
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
}
简单看了一下上边的方法与属性,下面我们来说一下四个点。
- public final class String implements java.io.Serializable, Comparable< String >, CharSequence
- String 类是 使用了 final 就说明这个类是不可继承。也就保证了 String 的安全性。
- Serializable 可序列化。
- Comparable 用于字符串的比较
- private final char value[];
- 上边代码所描述的是一个不可实例化的数组,但声明方式为什么是这样,不应该是声明为“ private final char[] value;”吗?这也是数组的一种声明方式,只是我们比较少见到,也许是因为历史原因 String 保留使用着这种不常用的数组声明方式。
- private int hash;
- 字符串常量池查重使用的。
- 配合其他需要用到 hash 值的地方,例如 HashMap 中的 key - value 使用。
- public native String intern();
如果仔细看一下上面的 Java Doc 他的解释如下。
String类会维护一个私有的、初始为空的字符串池。
当调用 intern() 方法时,如果该池中已经存在与本字符串 this 字面量相同的一个字符串(用 equals() 方法判定),那么就直接返回池中的那个字符串。如果不存在,那么 this 会被加入池中(驻留),并返回对它的引用。
对两个字符串 s 和 t ,当且仅当 s.equals(t) 为真时,s.intern() == t.intern() 才为真。
所有字符串字面量和字符串常量表达式都会被驻留。
代码详解
代码说完了下面我们看图说话。
开局一张图剩下全靠编,大家好下面来到我编你听阶段。
public class StringTest {
public static void main(String[] args) {
String a1 = "吴勉";
String a2 = "吴勉";
String b1 = new String("吴勉");
String b2 = b1.intern();
System.out.println("编号 :1 ; " + a1 == a2);
System.out.println("编号 :2 ; " + a2 == b1);
System.out.println("编号 :3 ; " + a2 == b2);
}
}
结果 :true,false,true ;
为什么会这样呢,我们配合一张图来说明
- 编号 1
这个值是 true 的原因是因为在编译器就发现的 “吴勉” 这个值所以将这个值放入了字符串你常量池。
经过是:
- a1 定义字面量 吴勉,
- 根据字面量 吴勉 的 hash 值查找字符串常量池中的管理表,查找是否有该 hash 值。
- 没有,将吴勉作为字符串驻留,存入字符串常量池中。并返回该引用。
- a2 定义字面量 吴勉
- 根据字面量 吴勉 的 hash 值查找字符串常量池中的管理表,查找是否有该 hash 值。
- 有,返回该引用。
- 编号 2
String b1 = new String(“吴勉”);
b1 在创建时新建的一个 String 对象,然后 String 对象在引用字符串常量池中的 “吴勉”。
所以 a2 = b1 的比较是那 吴勉在字符串常量池中的引用与 new String 这个对象的内存地址进行比较所以不等。 - 编号 3
String b2 = b1.intern();
这个 intern 方法前边也说了。
所以他获取的是字符串常量池中的引用所以。他一定也是同一个引用。
好就说到这里吧! 还有很多超级多的细节,以后可能会讲解,现在没法详细说,你们如果有时间可以尝试使用。
JPS 加 java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB 测试一下相关内容
JHSDB 工具可以看见的内容有很多,这里简单介绍下一下内容。
- 当前启动项目的所有线程
- 加载的类
- 堆内的所有对象
- 栈信息
- 对内存分配
总结:
- 做好 String 字符串性能优化,可以提高系统的整体性能。在这个理论基础上,Java 版本在迭代中通过不断地更改成员变量,节约内存空间,对 String 对象进行优化。
- 千里之堤,溃于蚁穴。日常编程中,我们往往可能就是对一个小小的字符串了解不够深入,使用不够恰当,从而引发线上事故。