一、说清常量池
Java中的常量池大概有三种:
1、class文件中的(静态常量池)
存储在硬盘上
2、运行时的常量池(HSDB可以看到):一般说的常量池指这个
InstanceKlass 中的一个属性,这是一个动态的常量池存储在内存中的方法区中
ConstantPool* _constants;
3、字符串常量池
String Pool
String Table
HashTable
存储在堆区
字符串常量池底层是StringTable,StringTable底层是HashTable。
问题1:用hashTable是如何存储字符串的?
现有字符串:name=“test”, sex=“man”,job=“teacher”.存储的时候首先会将name进行hash(使用hash算法),假如hash只会的值为:
name的hashValue = 11
sex的hashValue = 13
job的hashValue = 11
此时,根据key从hashTable中查数据的过程如下:
1、将key通过hash算法计算成hashValue (name=11)
2、然后会根据这个hashValue去查询,如果index=hashValue关联的元素只有一个,则直接返回
3、如果是链表,根据链表进行遍历,比对key, 然后返回
二、Java中的字符串在JVM中是如何存储的
这里就涉及到了StringTable。
key如何生成
我们可以看到,这里key的生成是:
1、通过String的内容+长度生成hash值
2、将hash值转为key
2.1、即StringTable生成Key的规则如下:
hashValue = hash_String(name, len);
index = hash_to_index(hashValue);
1、根据字符串以及字符串的长度计算出hashValue
2、根据hashValue计算出index,这个index就是key
// 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;
}
2.2、StringTable Value的生成
HashtableEntry<oop, mtSymbol>* entry = new_entry(hashValue, string()); // hashValue是上面算出来的,String是值
add_entry(index, entry);
将Java的String类的实例instanceOOpDesc封装成HashTableEntry
HashtableEntry的结构如下:
struct HashtableEntry {
INT_PRT hash;
void* key; // 计算得到的hashVlaue
voud* value; // 对应一个instanceOopDesc
HashtableEntry* next;
}
注意,String有时候会存储到StrngTable中,有时候不会!和inter有关系!
OOP体系
Java中的对象在JVM中的存在形式
Klass是java中的类在JVM中的存在形式
我们自己写的String str = ''11", 这个str对象对应一个InstanceOopDesc,这个instanceOopDesc是java中String类的实例。
存储的时候需要将这个实例instanceOopDesc封装成HashTableEnry。
Java中字符串使用StringTable存储,StringTable是HashTable改进后的。
三、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());
// 比较的是什么
System.out.println(s1 == s2); // 比较的是内存地址
System.out.println(s1.equals(s2)); // 比较的是字符串的内容
}
}
运行结果:
1568
1568
false
true
Process finished with exit code 0
这里的hashCode 为什么相等呢?我们来看这个方法的实现:
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
这里的value是一样的,所以说计算出来的hashCode是一样的。注意,这里是因为String这个类自己重写了HashCode和equals方法,对于普通的Java对象,如果没有重写hashCode方法,则使用hashCode获取的是该对象的内存地址。
四、不同方式创建字符串在JVM中的存在形式
String是存储在一个字符数组中的:
/** The value is used for character storage. */
private final char value[];
从OOP体系中来看,基本类型的数组对应的是TypeArryKlass,它的对象是TypeArrayOopDesc。
字符数组在JVM中是这样存在的。
4.1、 String s1 = "1"生成了几个oop,分别是什么?
2个,分别是:
1、TypeArrayOopDesc,char数组
2、InstanceOop, String对象
如何证明:
当我们执行完test1方法后:
接下来,我们来看一下s1在内存中的存储:
一个String中包含一个char数组,一个char数组对应一个typeArrayOopDesc;
String s1 = "2"执行的时候,首先会根据String的value(这里是11)去字符串常量池中查询看是否存在,如果不存在会创建一个String包括char数组(会在底层创建一个typeArrayOopDesc, value是11),然后再创建一个HashTableEntry来保存该字符串“11”.
**4.2、**下面的代码会创建哪几个Oop?
String s1 = "2";
String s2 = "2";
答案:2个!
1、String s1 = "2"执行的时候,首先会根据String的value(这里是11)去字符串常量池中查询看是否存在,如果不存在会创建一个String包括char数组(会在底层创建一个typeArrayOopDesc, value是11),然后再创建一个HashTableEntry来保存该字符串“11”.
2、当执行String s2 = "2"时,此时会判断常量池,发现字符串已经存在,就会直接将栈中的s2指向上一次创建的String(包含chat数组)对象。
这里我们可以这样思考,如果JVM不这样设计,当我们在自己的项目中new了多个内容相同的字符串,就会产生多个String对象,但是他们的内容确实一样的,这样会造成内存的额外占用。所以说JVM维护了一个常量池,来保证value相等的对象只会在堆中存在一份,其他的栈中新的对象(注意,这里其实是赋值引用,因为这里并没有使用new 关键字!)如果value相等,只需要引用到这个堆中的对象即可。
这里只是引用对象而已!如果字符串常量池中已存在该字符串,引用即可,如果不存在创建保存新的字符串。
4.3、 下面的代码会创建哪些Oop?
String s1 = new String("1");
3个;
这里创建了两个String对象,
1、首先会去常量池中判断是否应存在该字符串,如果不存在,先创建一个String(chat[])对象,然后创建一个HahsTableEntry来存储该字符串;
2、此时会同时产生一个typeArrayOopDesc(“11”);
3、因为这里使用了new String(),所以会产生一个String(chat[])对象,然后该对象再指向JVM中的typeArrayOopDesc(“11”)
4.4、 下面的代码创建哪几个Oop?
String s1 = new String("4");
String s2 = new String("4");
4 个
String s1 = "101";
String s2 = "102";
2个String
4个Oop(两个String+两个char[])
String s1 = new String("1");
2个String
3个Oop
String s1 = new String("4");
String s2 = new String("4");
3个String
4个Oop
4.5、字符串拼接底层实现
String s1 = "1";
String s2 = "1";
String s = s1 +s2;
底层做法是:
new StringBuilder().append(“1”).append(“1”)
**4.6、**下面的代码创建了几个String,几个Oop?
String s1 = "1";
String s2 = "1";
String s = s1 + s2;
2个String
4个Oop:2个String+2个char
注意,此时s=“11”对应的值并没有被写入到常量池中。因为s =s1 + s2并不是一个固定的值,所以JVM不认为它是一个常量,固不会主动的将其放入到字符串常量池中,我们可以使用intern方法将其手动入池!
4.7、
String s3 = new String(new char[]{'1', '1'}, 0 ,2);
一个String,一个Char,因为它没有在常量池生成记录,所以少创建一个String。
4.7、intern方法
public static void test7() {
String s1 = "1";
String s2 = "1";
String s = s1 +s2;
String str = "11";
System.out.println(s == str);
}
false
Process finished with exit code 0
public static void test7() {
String s1 = "1";
String s2 = "1";
String s = s1 +s2;
s.intern();
String str = "11";
System.out.println(s == str);
}
true
Process finished with exit code 0
s.intern() 就是将s="11"写入了常量池,这样比较的时候str和s指向的就是同一个String对象的地址。
public static void test8() {
final String s1 = "1";
final String s2 = "1";
String s = s1 +s2;
String str = "11";
}
true
Process finished with exit code 0
String s = s1 +s2; 在编译的时候已经被优化成了 String s = “11”; "11"已经在常量池了,后面不会再创建新的;
栈中到底放什么
1、对于基本数据类型,栈中存储的就是值本身
int a = 10;
2、如果是引用数据类型,栈中存储的是引用(堆中对象的内存地址)
Object obj = new Object();
oop的内存地址放入栈中
false
因为这里的final是指String对象的引用的final的,并不是value,所以s = s1 + s2并不是常量,自然不会进入到常量池中!
补充:
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
如果看结果:
一个String, 一个char数组对象,2个Oop
如果看过程:
1个String,两个char数组对象,
看一看到最后有一个
this.value = Arrays.copyOfRange(value, offset, offset+count);
public static char[] copyOfRange(char[] original, int from, int to) {
int newLength = to - from;
if (newLength < 0)
throw new IllegalArgumentException(from + " > " + to);
char[] copy = new char[newLength];
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}
执行结果是A。
这里要考虑是值传递还是引用传递。
Java中,对象是引用传递,基本数据类型是值传递。但是String有些不用,String都是引用传递。