本篇博客主要从jvm层面去分析Java字符串在Java内存区域的存储方式
常量池
- class文件常量池: 存放在class文件的静态常量池,相当于内存里面的一个东西序列化到这里面,到内存之后可能会以某种合适的数据结构来存储或索引
- 运行时常量池:
- InstanceKlass的一个属性
- 存在于方法区(元空间)
- 字符串常量池:
- 这个是本文的重点,字符串常量池存在于堆中。
- 字符串常量池的本质是存放于堆中的HashTable
Klass与oop(对String来说)
-
Klass:JVM里面(C++代码)的Klass代表了Java中的Class类
-
oop: 这篇博客的主要内容是字符串,暂时不提oop体系,但是为了之后的叙述方便,这里先说明,在JVM中String对应的实例时instanceOopDesc(别的类的对象也是用这个来表示,也就是这个是Java对象在JVM中的存在形式)
字符串常量池
- HashtableEntry组成
-
key: 每个键值对的key为hashValue,这个hashValue是由一个哈希函数对字符串进行操作得出来的,然后HashTable可以用这个值来找到底层数组的索引,类似HashMap
-
Key的生成方式:1. 通过String的内容和长度生成hash值 2.将hash值转化为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; }
-
value: 键值对的value是String,在jvm中也就是instanceOopDesc
-
key-value entry的生成方式如下
HashtableEntry<oop, mtSymbol>* entry = new_entry(hashValue, string()); 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; }
-
Java生成字符串,底层发生了什么?
String s = "11"形式
public static void test1(){
String s1 = "11";
String s2 = "11";
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
}
上面的代码发生了如下的事情:
- 内存的情况如上图所示当运行到String s1 = “11”;的时候,jvm对于这行代码的等式右边的"11",做的事情首先是利用字符串求出哈希值,然后找到在hashtable的对应位置,判断他是否在常量池中出现过,如果没有出现过,这时候JVM会利用这个值先生成一个char数组,这个char数组存在于堆里面(java里面这些东西都是对象),然后再创建一个String,这个String保持了对刚刚那个char[]的引用,然后这时候做完这个事情之后,就把这个生成的String引用赋值给s1
- 等到了String s2 = “11”;这一步,也是按照刚刚的流程走,只不过这一次,由于刚刚在常量池生成了,所以这次查询HashTable的结果是存在,所以直接就把那个entry里面的value的地址赋值给s2,这样一来,s2,s1引用了同一块内存,那么他们两个==的结果就是true
- 执行完了这两部操作,对于JVM来说,增加了一个String对象,2个oop对象(包括String和char[])
String s = new String(“11”);形式
public static void test2(){
String s1 = new String("11");
String s2 = new String("11");
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
}
- 对于这幅图的理解:首先执行到括号里面的"11"的时候,执行的依然是刚刚那种第一种情况,去HashTable看看是否有存在过,如果不存在的话就创建一个String,这个创建依然是多一个String对象,多两个oop对象
- 然后执行到new String(“11”),new实际上就是去堆去生成一个对象,利用的是String的构造函数,而他的构造函数里面有一个以单个String为参数的,底层大概做的事情就是把传入的参数的char[]引用赋值给新的String,这一步多了1个String,1个oop(就是那个String)
- 然后到String s2 = new String(“11”);的时候,也和上一步差不多,这不过执行到这个"11"的时候HashTable命中了,直接就返回了entry里面的那个String指针,但是由于是new对象,所以这个生成新的String对象的操作还是有的,而且由于构造函数的原因,这个新生成的String的char[]引用还是和常量池那个"11"的一样的
- 那么在这个程序中由于地址不一样,所以==的运算结果为false,但是由于String重写了equals方法,只要字符串内容一样,算出来的哈希值也就一样,那equals是可以比较字符串内容的
- 这个程序生成了3个String,4个oop对象(3String+1char[])
字符串拼接
public static void test3(){
String s1 = "1";
String s2 = "1";
String s = s1+s2;
//s.intern();
String str = "11";
System.out.println(s==str);
}
- 先说一下结果,结果是false
- s1+s2底层是怎么做的?通过分析字节码可以知道,jvm是通过生成一个StringBuilder,然后用StringBuilder的append执行两次,然后在用StringBuilder的toString()方法,而这个toString()方法点进去看可以知道他会new一个String,用的构造函数是三个参数的构造函数,3个参数的构造函数和1个函数的构造函数的区别就在于,他没有在常量池中生成记录,执行这个构造方法之后,oop增加了两个,String对象增加了一个
- 但是如果把s.intern()的注释去掉,这个结果就为true,原因请看如下分析
- s.intern()的作用就是,如果常量池中有就直接返回地址,如果常量池中没有就生成一个entry然后放进去,这个entry的value 就是s
- 那么在执行到"11"的时候由于常量池此时是有值的,所以str引用的是常量池的那个String,而刚刚intern()放进去的时候
编译期确定字符串值的情况(final)
public static void test4(){
final String s1 = "1";
final String s2 = "1";
String s = s1+s2;
//s.intern();
String str = "11";
System.out.println(s == str);
}
- 由于两个final拼接,在编译期的时候就可以确定s的值,这里jvm把他存在常量值里面,那么由于在到"11"的时候常量值有值,所以str拿到的也是那个常量池的那个地址
编译期不确定字符串常量值的情况(final)
public static void test5(){
final String s1 = new String("1");
final String s2 = new String("1");
String s = s1+s2;
//s.intern();
String str = "11";
java
System.out.println(s == str);
}
- 虽然用到了final,但是new String创建了字符串,在编译期无法确定值,然后jvm的处理就不会把他放在常量池中(放的话是特殊优化处理),所以结果为false,一个String在常量池,前面的那个是由刚刚说的StringBuilder拼出来的不放在常量池中