一、开篇
同学们面试的时候总会被问到字符串常量池的问题吧?如果你是死记硬背的答案,那么我想看了我这篇文章,你应该以后能胸有成竹了。
跟着Alan,走起!
二、概述
1. 常量池表(constant_pool table)
- Class文件中存储所有常量(包括字符串)的table。
- 它是Class 字节码文件中的一类结构化数据,还不是运行时的内容。
2. 运行时常量池(Runtime Constant Pool)
- JVM 运行时内存中方法区的一部分,这是运行时的内容。
- 这部分数据绝大部分是随着JVM 运行,从常量池表转化而来,每个Class 都对应一个运行时常量池。
- 上面说绝大部分是因为:除了Class 中常量池内容,还可能包括动态生成并加入这里的内容。
3. 字符串常量池(String Pool)
字符串常量池与运行时常量池不是一个概念:
- String Pool 是JVM 实例全局共享的全局只有一个,而Runtime Constant Pool 每个类都有一个。
- String Pool 只记录字符串对象,而Runtime Constant Pool 记录各种对象。
JVM规范要求进入这里的String 实例叫“被驻留的字符串 - interned string”,各个JVM 可以有不同的实现,HotSpot 是设置了一个哈希表 - StringTable 来引用堆中的字符串实例,被引用就是被驻留。
字符串池在JDK 1.7 之后存在于Heap 堆中,旧版存在于方法区中。
4. 享元模式
其实字符串常量池这个问题涉及到一个设计模式,叫“享元模式”,顾名思义 - - - > 共享元素模式。
也就是说:一个系统中如果有多处用到了相同的一个元素,那么我们应该只存储一份此元素,而让所有地方都引用这一个元素。
Java中String部分就是根据享元模式设计的,而那个存储元素的地方就叫做“字符串常量池 - String Pool”。
为了描述准确,以下我将用上述的3个英文来描述:constant_pool table
、Runtime Constant Pool
、String Pool
。
三、详解
感觉好像前面1和2说了点概念,就好像快说完了…… 那直接来个Example 吧!
在*.java
文件中有如下代码:
int a = 1;
String b = "asd";
首先,
1
和"asd"
会在经过javac(或者其他编译器)编译过后变为Class文件中constant_pool table
的内容。当我们的程序运行时,也就是说JVM运行时,每个Class字节码
constant_pool table
中的内容会被加载到JVM 内存中的方法区中各自Class
对象的Runtime Constant Pool
中。一个没有被
String Pool
包含的Runtime Constant Pool
中的字符串(这里是"asd"
)会被加入到String Pool
中(HosSpot 使用hashtable 引用方式),步骤如下:在Java Heap 中根据
"asd"
字面量创建一个字符串对象。将字面量
"asd"
与字符串对象的引用在hashtable 中关联起来,键 - 值 形式是:"asd"
=对象的引用地址
。
另外来说,当一个新的字符串出现在Runtime Constant Pool
中时怎么判断需不需要在Java Heap中创建新对象呢?
策略是这样:
会先去根据equals来比较
Runtime Constant Pool
中的这个字符串是否和String Pool
中某一个是相等的(也就是找是否已经存在),如果有那么就不创建,直接使用其引用;反之,如上3。
如此,就实现了享元模式,提高的内存利用效率。
四、例子
上面的描述其实对于能看到这篇文章的程序员们来说很好懂,下面我们来分析两个具体的问题。
String s = new String(“abc”) 创建了几个对象?
结论:2个或1个。
解析:
首先,出现了字面量
"abc"
,那么去String Pool
中查找是否有相同字符串存在:如果存在就不会新建对象,否则就在Heap 中用字面量"abc"
首先创建1个String 对象。接着,
new String("abc")
,关键字new 一定会在Heap 中创建1个新对象,然后调用接收String 参数的构造器进行初始化。变量s
的引用是这个String 对象。
说明以下程序的输出?
public static void main(String[] args) { String s1 = "abc"; String s2 = new String("abc"); String s3 = "a" + "bc"; System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s1 == s1.intern()); }
结论:
System.out.println(s1 == s2);//flase System.out.println(s1 == s3);//true System.out.println(s1 == s1.intern());//true
解析
首先,与题1所答同理,s1 和s2 肯定不是一个引用。
其次
String s3 = "a" + "bc";
这行代码最终是"abc"
,根据s1,它已经在String pool
中了,所以s3和s1是引用的同一个对象。最后,
s1.intern()
方法:将某个String对象在运行期动态的加入String pool
(如果pool中已经有一个了就不加)并返回String pool
中保证唯一的一个字符串对象的引用。所以,还是会返回和s1同一个对象的引用,所以true
。
完
参考文献:
[ 1 ] 周志明.深入理解Java虚拟机[M].第2版.北京:机械工业出版社,2015.8.
[ 2 ] Tim Lindholm,Frank Yellin,Gilad Bracha,Alex Buckley.The Java® Virtual Machine Specification . Java SE 8 Edition . 英文版[EB/OL].2015-02-13.
[ 3 ] James Gosling,Bill Joy,Guy Steele,Gilad Bracha,Alex Buckley.The Java® Language Specification . Java SE 8 Edition . 英文版[EB/OL].2015-02-13.
追加记录一个我追踪OpenJDK 关于String pool
实现方式的相关记录,还没有追到底
String的native String intern()方法源码
openjdk\jdk\src\share\native\java\lang\String.c#include "jvm.h" #include "java_lang_String.h" JNIEXPORT jobject JNICALL Java_java_lang_String_intern(JNIEnv *env, jobject this) { return JVM_InternString(env, this); }
在jvm头文件中
openjdk\jdk\src\share\javavm\export\jvm.h/* * java.lang.String */ JNIEXPORT jstring JNICALL JVM_InternString(JNIEnv *env, jstring str);
在jvm.cpp文件中
openjdk\hotspot\src\share\vm\prims\jvm.cpp// String support /// JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str)) JVMWrapper("JVM_InternString"); JvmtiVMObjectAllocEventCollector oam; if (str == NULL) return NULL; oop string = JNIHandles::resolve_non_null(str); oop result = StringTable::intern(string, CHECK_NULL); return (jstring) JNIHandles::make_local(env, result); JVM_END