JVM:String底层
常量池包括class文件常量池、运行时常量池和字符串常量池。
*常量池查看方法参考“JVM:类加载&类加载器”/实验
运行时常量池:一般意义上所指的常量池。是InstanceKlass的一个属性。存储于方法区(元空间)。
/openjdk/hotspot/src/share/vm/oops/instanceKlass.hpp
Class****文件常量池: 可通过 javap –verbose 对象全限定名查看constant pool。存储在硬盘。
字符串常量池**(String Pool)**:底层是StringTable。存储在堆区。继承链:HashTable – StringTable – String Pool
/openjdk/hotspot/src/share/vm/classfile/symbolTable.hpp
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中的存储
StringTable中key的生成方式
-> 根据字符串(name)和字符串长度计算出hashValue
-> 根据hashValue计算出index(作为key)
/openjdk/hotspot/src/share/vm/classfile/symbolTable.cpp
StringTable中value的生成方式
-> 调用new_entry()将Java的String类实例instanceOopDesc封装成HashtableEntry
* instanceOopDesc: OOP(ordinary object pointer)体系是Java对象在jvm中的存在形式 //相对于Klass是Java类在jvm中的存在形式
/openjdk/hotspot/src/share/vm/classfile/symbolTable.cpp
new_entry()包含有关HashtableEntry的一些链表操作。
/openjdk/hotspot/src/share/vm/utilities/hashtable.cpp
HashtableEntry是一个单向链表结点结构。value对应要封装的字符串对象InstanceOopDesc,key对应hashValue。
/openjdk/jdk/src/windows/native/sun/windows/Hashtable.h
创建String的底层实现
实验1
结果:两次输出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形式存在
-> 代码中声明字符数组
-> HSDB attach到对应进程,查看main线程堆栈,找到[C的内存地址
-> Inspector查看证明元信息存储在TypeArrayKlass,对象为OOP(TypeArrayOopDesc)。
实验3 String s1 = “1”;语句生成了几个OOP?2个。
\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个。
证明:
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个。
证明:
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个。
证明:
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个。
证明:
创建了几个对象?
-> 如果问创建了几个String对象?-> 2个。
-> 如果问创建了几个OOP对象? -> 4个。
String拼接
实验8
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的两种构造方法)
所以总共创建了2个String,4个OOP(2个char数组,2个String)。
对应字节码:
|
|
说明拼接语句String s=s1+s2底层是用new StringBuilder().append(“1”).apend(“1”).toString()实现的。
String的两种构造方法
StringBuilder的toString()调用了String(char[] value, int offset, int count)的构造方法。
StringBuilder.class
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
copyOfRange()底层又重新生成了char[];
Arrays.class
String.intern()
**intern()**去常量池中找字符串,如果有直接返回,如果没有就把String对应的instanceOopDesc封装成HashTableEntry存储(写入常量池)。
实验9
结果:有执行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
结果:有没有执行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
结果:没有执行intern输出false。
原因:new得到的对象不是常量(类似uuid的动态生成,见”JVM: 类加载&类加载器”)。– 虽然用了final修饰,只能表示引用是final,引用指向的值并不是final。
(没有执行intern版本)字节码:
-—
练习1
结果:实验显示该语句新增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
结果:即使前面有语句1,语句2仍然新增4个String和3个char数组。
原因:语句1在编译时已经被计算替换为s1=“1111”(查看字节码可证);语句2过程分析同练习1。
e.g.