String 深入JVM底层解析

一、说清常量池

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都是引用传递。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值