JVM:String底层

常量池包括class文件常量池、运行时常量池和字符串常量池。

*常量池查看方法参考“JVM:类加载&类加载器”/实验

运行时常量池:一般意义上所指的常量池。是InstanceKlass的一个属性。存储于方法区(元空间)。

/openjdk/hotspot/src/share/vm/oops/instanceKlass.hpp

class InstanceKlass : public Klass {
    …
    ConstantPool* _constants;
    ….
}

Class文件常量池: 可通过 javap –verbose 对象全限定名查看constant pool。存储在硬盘。

字符串常量池(String Pool):底层是StringTable。存储在堆区。继承链:HashTable StringTable String Pool

/openjdk/hotspot/src/share/vm/classfile/symbolTable.hpp

class StringTable : public RehashableHashtable<oop, mtSymbol> {
    …
}

 

hashtable如何存储字符串

hashtable的底层是数组+链表。

- 用hash算法对字符串对象计算得到hashValue,按照hashValue将key和value放入hashtable中,如果有冲突的放进该hashValue的链表中。

e.g.name=”ziya”; sex=”man”; zhiye=”teacher”

 name/sex /zhiye的hash value为11/13/11

根据key从hashtable查找数据:

-> 用hash算法对key计算得到hashValue (e.g. key:name -> hashValue 11)

-> 根据hashValue区hashtable中找,如果index=hashValue的元素只有1个,直接返回;

-> 如果元素有多个,根据链表进行遍历比对key

 

 

java字符串在jvm中的存储

StringTablekey的生成方式

-> 根据字符串(name)和字符串长度计算出hashValue

-> 根据hashValue计算出index(作为key)

/openjdk/hotspot/src/share/vm/classfile/symbolTable.cpp

oop StringTable::basic_add(int index_arg, Handle string, jchar* name,
                           int len, unsigned int hashValue_arg, TRAPS) {
    …
    hashValue = hash_string(name, len);
    index = hash_to_index(hashValue);
    …
}

StringTablevalue的生成方式

-> 调用new_entry()将Java的String类实例instanceOopDesc封装成HashtableEntry

instanceOopDescOOP(ordinary object pointer)体系是Java对象在jvm中的存在形式 //相对于Klass是Java类在jvm中的存在形式

/openjdk/hotspot/src/share/vm/classfile/symbolTable.cpp

//string()就是instanceOopDesc
oop StringTable::basic_add(int index_arg, Handle string, jchar* name,
                           int len, unsigned int hashValue_arg, TRAPS) {
    …
    HashtableEntry<oop, mtSymbol>* entry = new_entry(hashValue, string());
    add_entry(index, entry);
    …
}

new_entry()包含有关HashtableEntry的一些链表操作。

/openjdk/hotspot/src/share/vm/utilities/hashtable.cpp

template <MEMFLAGS F> BasicHashtableEntry<F>* BasicHashtable<F>::new_entry(unsigned int hashValue) {
    …
}

HashtableEntry是一个单向链表结点结构。value对应要封装的字符串对象InstanceOopDesc,key对应hashValue。

/openjdk/jdk/src/windows/native/sun/windows/Hashtable.h

struct HashtableEntry {
    INT_PTR hash;
    void* key;
    void* value;
    HashtableEntry* next;
};

 

 

创建String的底层实现

实验1 

public class Test {
    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));
    }
}

结果:两次输出hashCode值相同,s1==s2为false, s1.equals(s2)为true。

原因:1) 因为hashCode就是根据字符串值计算得到的,字符串值一样hashCode就会一样。

         2) s1==s2比较的是地址。

* String重写了Object中的hashCode方法。

 

String的值是存储在字符数组char[] value中的。

基本数据类型数组的对象生成的实例为TypeArrayOopDesc(对应Klass体系中基本数据类型数组的元信息存放在TypeArrayKlass)。

实验2:证明字符数组在jvm中以TypeArrayOopDesc形式存在

public class Test {
    public static void main(String[] args) {
        char[] arr=new char[]{'1', '2'};
        while (true);
    }
}

-> 代码中声明字符数组

-> HSDB attach到对应进程,查看main线程堆栈,找到[C的内存地址

-> Inspector查看证明元信息存储在TypeArrayKlass,对象为OOP(TypeArrayOopDesc)。

 

 

实验3 String s1 = “1”;语句生成了几个OOP?2个。

public class Test {
    public static void main(String[] args) {
        test3();
    }
    public static void test3() {
        String s1="11";
    }
}

1) TypeArrayOopDesc – char数组

2) InstanceOop – String对象

证明:在语句处设置断点, idea debug模式运行程序(Debug模式单步验证)

-> 勾选memory,memory layout点击load classes

-> 执行完String s1=“11”时,String和char[]的count都增1;

原因:

//底层

因为是字面量,该String值会放在字符串常量池

-> 在字符串常量池中找有没有该value(“11”),如果有则直接返回对应的String对象; 

-> 如果没有找到,创建该value的typeArrayOopDesc,再创建String, String中包含char数组,char数组指向该typeArrayOopDesc;

-> 在字符串常量池表创建HashTableEntry指向String(将String对象对应的InstanceOopDesc封装成HashTableEntry作为StringTable的value存储)。

*面试题:创建了几个对象

-> 先问清楚问的是String对象还是OOP对象

-> 如果问创建了几个String对象-> 1个。

-> 如果问创建了几个OOP对象 -> 2个: 1个char数组,1个String对象对应的OOP。

//HashTableEntry是C++对象,不算入OOP对象。

 

实验4 String s1 = “11”; String s2=”11”;语句生成了几个OOP? 2个。

证明:

public class Test {
    public static void main(String[] args) {
        test4();
    }
    public static void test4() {
        String s1="11";
        String s2="11";
    }
}

Debug模式单步验证,

-> 执行完String s1=“11”时,String和char[]都增1;

-> 执行完String s2=“11”时,count没有增加;

原因:

 

-> s1的创建参考实验1;

-> 创建s2时,在字符串常量池中有找到该值,不需要再创建,S2直接和S1指向同一个String对象。

*面试题:创建了几个对象 

-> 先问清楚问的是String对象还是OOP对象

-> 如果问创建了几个String对象-> 1个。

-> 如果问创建了几个OOP对象 -> 2个: 1个char数组,1个String对象(对应的OOP)。

 

实验5 String s1 = new String(“11”)语句生成了几个OOP? 3个。

证明:

public class Test {
    public static void main(String[] args) {
        test5();
    }
    public static void test5() {
        String s1=new String("11");
    }
}

Debug模式单步验证,

-> 执行完String s1=new String(“11”)时,String增2,char[]增1;

原因:

-> 在字符串常量池中找,发现没有该value(“11”)

-> 创建HashTableEntry指向String,String指向typeArrayOopDesc;

-> 因为new,又在堆区再创建一个String对象,其char数组直接指向已创建的typeArrayOopDesc。

 

实验6 String s1 = new String(“11”); String s2 = new String(“11”);语句生成了几个OOP? 4个。

证明:

public class Test {
    public static void main(String[] args) {
        test6();
    }
    public static void test6() {
        String s1=new String("11");
        String s2=new String("11");
    }
}

Debug模式单步验证,

-> 执行完String s1=new String(“11”)时,String增2,char[]增1;

-> 执行完String s2=new String(“11”)时,String增1。

原因:

-> s1的创建参考实验5;

-> 创建s2时,因为new,再创建一个String对象指向同一个typeArrayOopDesc。

 

实验7 String s1 = “11”; String s2=”22”;语句生成了几个OOP? 4个。

证明:

public class Test {
  public static void main(String[] args) {
      test7();
  }
  public static void test7() {
      String s1="11";
      String s2="22";
  }
}

创建了几个对象?

-> 如果问创建了几个String对象?-> 2个。

-> 如果问创建了几个OOP对象? -> 4个。

 

 

String拼接

实验8

public class Test {
  public static void main(String[] args) {
      test8();
  }
  public static void test8() {
      String s1="1";
      String s2="1";
      String s = s1+s2;
  }
}

Debug模式单步验证,

-> 执行完String s1=”1”时,char[]和String的count都增1;

-> 执行完String s2=”1”时,count没有增加;

-> 执行完String s=s1+s2时,char[]和String的count都增1。

  //因为语句3底层调用StringBuilder.toString()==调用String构造方法String(value, offset, count),不会在常量池生成记录,只创建了1个String对象。(参考:String的两种构造方法)

 

所以总共创建了2String4OOP(2个char数组,2个String)。

对应字节码:

 0 ldc #2 <1>
 2 astore_0
 3 ldc #2 <1>
 5 astore_1
 6 new #3 <java/lang/StringBuilder>
 9 dup
10 invokespecial #4 <java/lang/StringBuilder.<init>>
13 aload_0
14 invokevirtual #5 <java/lang/StringBuilder.append>
17 aload_1
18 invokevirtual #5 <java/lang/StringBuilder.append>
21 invokevirtual #6 <java/lang/StringBuilder.toString>
24 astore_2
25 return

说明拼接语句String s=s1+s2底层是用new StringBuilder().append(“1”).apend(“1”).toString()实现的。

 

String的两种构造方法

StringBuilder的toString()调用了String(char[] value, int offset, int count)的构造方法。

StringBuilder.class

Public String toString() {
      return new String(this.value, 0, this.count);
}

String(value)会创建2个String,3个OOP (实验运行String s2=new String(“22”),结果char[]增1,String增2)

String(value, offset, count)创建1个String,2个OOP(可实验运行String s1=new String(new char[]{‘1’,’1’},0,count); 结果char[]和String都增1证明)。该构造方法不会在常量池生成记录

*从结果来看,String(value, offset, count)创建了2个OOP,但从过程来讲则创建了3个OOP(1个String和2个char数组)。因为:

    String(value, offset, count) 用到了copyOfRange();

    String.class

Public String(char[], int offset, int count) {
    …
    this.value = Arrays.copyOfRange(value, offset, offset + count);
    …
}

    copyOfRange()底层又重新生成了char[];

    Arrays.class

Public static char[] copyOfRange(char[] original, int from, int to) {
    …
    char[] copy = new char[newLength];
    …
}

 

 

String.intern()

intern()去常量池中找字符串,如果有直接返回,如果没有就把String对应的instanceOopDesc封装成HashTableEntry存储(写入常量池)。

实验9

public class Test {
    public static void main(String[] args) {
        test9();
    }
    public static void test9() {
        String s1="1";
        String s2="1";
        String s = s1+s2;
        s.intern();
        String str="11";
        System.out.println(s==str);
    }
}

结果:有执行intern输出true,没有执行intern输出false

原因:

如果没有调用intern,String s=s1+s2执行时不会在常量池中生成记录,所以String str=”11”执行时依然会生成新String;

如果有调用intern,intern()将s=”11”写入常量池,后面str就会在常量池中找到该值,直接指向s所创建的String对象。

Debug模式单步验证,

如果没有调用intern,执行完String str=”11”时,String和char[]的count都增1;

如果有调用intern,执行完String str=”11”时,count没有增加。

 

实验10

public class Test {
    public static void main(String[] args) {
        test10();
    }
    public static void test10() {
        final String s1="3";
        final String s2="3";
        String s = s1+s2;
        s.intern();
        String str="33";
        System.out.println(s==str);
    }
}

结果:有没有执行intern都输出true。

原因:因为s1和s2都是常量,编译优化时已经将String s=s1+s2变成String s=”33”,33”会被存储到常量池。

(没有执行intern版本)字节码:

//常量池17位为CONSTANT_String_info “33”。

Debug模式单步验证,(如果没有调用intern方法)

-> 执行完final String s1=”3”时String和char[]的count都增1;

-> 执行完final String s2=”3”时count没有增加;

-> 执行完String s=s1+s2时String和char[]的count都增1;

-> 执行完String str=”33”时count没有增加 (因为已经能够在常量池找到)。

 

实验11

public class Test {
    public static void main(String[] args) {
        test11();
    }
    public static void test11() {
        final String s1=new String("5");
        final String s2=new String("5");
        String s = s1+s2;
        //s.intern();
        String str="55";
        System.out.println(s==str);
    }
}

结果:没有执行intern输出false。

原因:new得到的对象不是常量(类似uuid的动态生成,见”JVM: 类加载&类加载器”)。-- 虽然用了final修饰,只能表示引用是final,引用指向的值并不是final。

(没有执行intern版本)字节码:

 

 

 

----

练习1

String s1 = “11”+new String(“22”);

结果:实验显示该语句新增4个String和3个char数组。

原因:可以拆开来分析:

1) “11” -> 1个String,1个char数组;

2) new String(“22”) -> 2个String,1个char数组;

3) 拼接->底层调用StringBuilder.toString()->底层调用String(value, offset, count)-> 1个String,1个char数组。

所以总共生成4个String,3个char数组。

 

练习2

String s1 = “11”+”11”;
String s2 = “11”+new String(“22”);

结果:即使前面有语句1,语句2仍然新增4个String和3个char数组。

原因:语句1在编译时已经被计算替换为s1=“1111”(查看字节码可证);语句2过程分析同练习1。

e.g.

 

posted @ 2020-09-19 23:46  丹尼尔奥利瓦  阅读(411)  评论(1编辑  收藏  举报