常量池(后期专门找一篇说一下)
1、class文件中
通过命令:javap -verbose XXX
Constant pool:
#1 = Methodref #23.#50 // java/lang/Object."<init>":()V
#2 = Methodref #22.#51 // com/luban/ziya/string/TestIntern.test5:()V
#3 = String #52 // 1
....
在硬盘上
2、运行时常量池
一般说的常量池就是指这儿
InstanceKlass的一个属性
ConstantPool* _constants;
方法区(元空间)
3、字符串常量池
即String Pool,但是JVM中对应的类是StringTable,底层实现是一个hashtable,看代码
class StringTable : public Hashtable<oop, mtSymbol> {
……
在堆区
Hashtable是如何存储字符串的
例如 name = “aaa”,sex = “man”,zhiye = “teacher”
假设 name进行hashValue后是 11,sex的hashValue = 13,zhiye的hashValue = 11,
存储方式可以参考下图
根据key从hashtable中查数据
1、将key通过hash算法计算成hashValue(name:11)
2、根据11去hashtable中去找,如果index=11关联的元素只有一个,直接返回
3、如果是多个,根据链表进行遍历,比对key
java中的字符串在jvm中是如何存储的
StringTable的Key的生成
1、根据字符串以及字符串的长度计算出hashValue
2、根据hashValue计算出index,这个index就是key
hashValue = hash_string(name, len);
index = hash_to_index(hashValue);
// Pick hashing algorithm
unsigned int StringTable::hash_string(const jchar* s, int len) {
return use_alternate_hashcode() ? AltHashing::murmur3_32(seed(), s, len) :
java_lang_String::hash_code(s, len);
}
// Bucket handling
int hash_to_index(unsigned int full_hash) {
int h = full_hash % _table_size;
assert(h >= 0 && h < _table_size, "Illegal hash value");
return h;
}
StringTable的Value的生成
将Java的String类的实例instanceOopDesc(String类在JVM中的存在形式)封装成HashtableEntry(就是一个结构体)
struct HashtableEntry {
INT_PTR hash;
void* key; //hashValue
void* value; //instanceOopDesc
HashtableEntry* //next;
};
HashtableEntry<oop, mtSymbol>* entry = new_entry(hashValue, string()); // 这里的string()指的就是instanceOopDesc
add_entry(index, entry);
template <class T, MEMFLAGS F> HashtableEntry<T, F>* Hashtable<T, F>::new_entry(unsigned int hashValue, T obj) {
HashtableEntry<T, F>* entry;
entry = (HashtableEntry<T, F>*)BasicHashtable<F>::new_entry(hashValue);
entry->set_literal(obj);
return entry;
}
**String有时候会存储到StringTable,有的时候不会,**后面会有StringBuffer的案例,可以看到对象的值直接就是,而不是指向常量池,这个与与intern还有渊源,
下面是周志明大佬书中的一段内容(这本书强烈建议Java开发者读一下 这本书强烈建议Java开发者读一下 这本书强烈建议Java开发者读一下)
String.hashcode()
String类重写了hashcode方法
public int hashCode() {
int h = this.hash;
if (h == 0 && this.value.length > 0) {
char[] val = this.value;
for(int i = 0; i < this.value.length; ++i) {
h = 31 * h + val[i];
}
this.hash = h;
}
return h;
}
可以看出String的hashcode与String的内容是有关系的,因此下面的代码的hashcode是相等的
public class TestHashcode {
public static void main(String[] args) {
String s1 = "11";
String s2 = new String("11");
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
}
}
不同方式创建字符串在JVM中的存在形式
String 是用一个字符数组来存储值的,基本类型的数组的元信息存放在TypeArrayKlass中,它的对象是存在TypeArrayOopDesc中。
private final char value[];
字符数组在JVM中是怎样存在的
运行以下代码,通过HSDB查看
public class CharArray {
public static void main(String[] args) {
char[] arr = new char[]{'1', '2'};
while (true);
}
}
可以看到元信息是存储在TypeArrayKlass中
一个双引号
以下代码生成了几个oop
String s1 = "1";
生成两个oop 一个String
1、TypeArrayOopDesc char数组
2、InstanceOopDesc String对象
证明
debug以下代码
此时内存中的char[] 和String的数量如下图
当执行完第20行后,结果如下图,可以看到二者均 +1
接下来图解一下,以以下代码为例
public static void test1() {
String s1 = "11";
}
两个双引号
以下代码生成了几个oop
String s1 = "2";
String s2 = "2";
此时你是不是想说三个
继续debug以下代码
执行完24行时
执行完25行时
可以发现增加了两个😆
看图
public static void test1() {
String s1 = "11";
String s2 = "11";
}
当执行String s2 = “11”;的时候发现11的位置已经有value11了,所以就直接指向了同一个String对象,所以还是两个oop,一个String
一个new String
以下代码生成几个oop
String s1 = new String("3");
继续debug
创建过程:
1、去字符串常量池中去查,如果有直接返回对应的String对象,如果没有,就会创建String对象、char数组对象。
2、将这个String对象对应的InstanceOopDesc封装成HashtableEntry,作为StringTable的value进行存储。
3、new String又在堆区创建一个对象,但是是没有数据的,char数组这时候直接指向常量池。
图解
三个oop,两个String
两个new String
以下代码生成几个oop
String s1 = new String("4");
String s2 = new String("4");
执行完38行后
执行完39行后
图解
三个String,四个oop
拼接字符串底层实现
双引号 + 双引号
public static void test() {
String s1 = "1";
String s2 = "1";
String s = s1 + s2;
// 底层实现是new StringBuilder().append("1").append("1").toString();
}
通过上述方法得到,创建了两个String ,4个oop,想不通了。。。,继续分析,首先看一下下图StringBuilder的toString()方法,是不是更懵逼了,明明new String()了。
不要慌,此时注意一下这里的构造函数的参数有三个,可以对比一下和一个参数的构造函数有啥区别。debug走起
执行完10行
可以看到只有一个String,一个char,说白了就是没有在常量池生成记录
执行完12行
看一个特例,看一下会输出什么,注意final
public static void test3() {
final String s1 = "3";
final String s2 = "3";
String s = s1 + s2;
String str = "33";
System.out.println(s == str); //true
}
此时在编译的时候就被优化为String s = 33;
双引号 + new String
public static void test1() {
String s1 = "aa";
String s2 = new String("bb");
String s = s1 + s2;
}
下图是三行代码执行后的结果,创建了4个String ,3个char
看内存图吧,明白一点,画得不对的话请指正。
下面这种写法结果是不一样的
public static void main(String[] args) {
String s1 = "aa" + new String("bb");
}
可以看到创建了3个String ,3个char,此时这种new String的时候只创建了1个String对象。看内存图
new String + new String
String s1 = new String("aa") + new String("bb");
这个应该好理解了,我相信各位大佬都看得懂。
intern做了什么
1、去常量池中找字符串,有,直接返回
2、没有,就会String对应的InstanceOopDesc封装成HashtableEntry,存储
再看一下大佬的讲解
继续看几个案例,
String s1 = new String("aa") + new String("bb");
String s2 = s1.intern();
System.out.println(s1 == s2);//true
内存图
再看一个
String s1 = "aabb";
String s2 = new String("aa") + new String("bb");
String s3 = s1.intern();
内存图
再来一个
String s1 = new String("aa") + new String("bb");
String s3 = s1.intern();
String s2 = "aabb";
差不多了吧,几乎是涵盖了所有的案例,干了两晚够底层,够详细。