剖析字符串与数组的底层实现
字符数组的存储方式
JVM有三种模型:
- 1.Oop模型:Java对象对应的C++对象
- 2.Klass模型:Java类在JVM对应的C++对象
- 3.handle模型
字符串常量池
即String Pool,但是JVM中对应的类是StringTable,底层实现是一个hashtable,如代码所示
JVM有三种常量池:
- 1.静态常量池(通过字节码方式查到地引用都是间接引用)
- 2.运行时常量池
- 3.字符串常量池->StringTable
key生成规则->String内容+长度生成哈希值,然后将hash值取模转为key
value生成规则->将Java地String类的实例InstanceOopDesc封装成HashtableEntry
字符串常量池位置
JDK1.6及之前:有永久代,运行时常量池在永久代,运行时常量池包含字符串常量池,Perm区域只有4m,一旦常量池大量使用intern很容易发生永久代的OOM
JDK1.7:有永久代,但已经逐步"去永久代",字符串常量池从永久代里的运行时常量池分离到堆里
JDK1.8及之后,无永久代,运行时常量池在元空间,字符串常量池依然在堆里
字符串常量池的设计思想:
- 1.字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序地性能
- 2.JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
2.1 为字符串开辟一个字符串常量池,类似于缓存区
2.2 创建字符串常量时,首先查询字符串常量池是否存在该字符串
2.3 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中
字符串常量池设计原理
字符串常量池底层时HotSpot的C++实现的。底层类似一个Hashtable,保存的本质上是字符串对象的引用,来看一道比较常见的案例,图中的代码创建了多少个String对象
// JDK6:false 创建了6个对象
// JDK7及以上:true 创建了5个对象
为什么输出会有这些变化呢?主要还是字符串从永久代中脱离、移入堆区的原因,intern()方法也相应发生了变化
同时也解释了JDK1.6中字符串溢出会抛出OutOfMemoryError:PermGen Space.而在JDK1.7及以上版本会抛出OutOfMemoryError:Java heap space
在JDK1.6中,调用intern()首先会在字符串池中寻找equals相等的字符串,加入字符串存在就返回该字符串在字符串池中的引用,假如字符串不存在,虚拟机会重新在永久代上创建一个实例,将StringTable的一个表项指向这个新创建的实例
在JDK1.7(及以上版本)中,由于字符串池不在永久代了,intern()做了一些修改,更方便地利用堆中的对象。字符串存在时和JDK1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例
我们再来看一个例子
JDK1.7以上:false true
JDK1.6: false false
jdk1.6代码图
在JDK1.6中上述的所有打印都是false,因为jdk6的常量池是放在Perm区中的,Perm区和正常的Java Heap区域是完全分开的。如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而new出来的String对象是放在Java Heap区域。所以拿一个Java Heap区域的对象地址和字符串常量池的对象地址进行比较比较肯定是不相同的,即使调用String.inern方法也是没有关系的
jdk1.7代码图
这里要明确一点的是,在Jdk6以及以前的版本中,字符串的常量池是放在堆的Perm区的,Perm区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用intern是会直接产生java.lang.OutOfMemoryError:PermGen Space错误的。Perm区域太小是一个主要原因,当然在1.8中已经直接取消了Perm区域,而新建立了一个元区域。应该是jdk开发者认为Perm区域已经不适合现在Java的发展了。
正是因为字符串常量池移动到Java Heap区域后,再看下面解释。
- 1.先看s3和s4字符串,String s3 = new String("1") + new String("1");这句代码中现在生成了两个对象,一个是字符串常量池中的"1",另一个是Java Heap中的s3引用指向的对象。中间还有2个匿名的new String("1"),不去讨论他们,此时s3引用对象内容是"11",但此时常量池中是没有"11"对象的
- 2.接下来,s3.intern(); 这一句代码,是将s3中的"11"字符串放入String常量池中,因为此时常量池中不存在"11"字符串,因此常规做法是跟jdk6图中所示的一样,再常量池中生成一个"11"的对象,关键点是jdk7中常量池不在Perm区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向s3引用的对象。也就是说引用地址是相同的
- 3.最后String s4 = "11";这句代码中"11"是显式声明的,因此会直接区常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向s3引用对象的一个引用。所以s4引用就指向和s3一样了,因此最后的比较s3 == s4 是true
- 4.再看s和s2对象,String s = new String("1");第一句代码,生成了2个对象。常量池中的"1"和Java Heap中的字符串对象 s.intern()这一句是s对象区常量池中寻找后发现"1"已经再常量池里了
- 5.接下来String s2 = "1";这句代码是生成一个s2的引用指向常量池中的"1"对象。结果就是s和s2引用地址明显不同
例子二
JDK1.7以上:false false
JDK1.6 false false
- 1.代码一和代码二的改变就是s3.intern的顺序是放在了String s4 = "11"后了,这样,首先执行String s4 = "11";声明s4的时候常量池中是不存在"11"对象的。执行完毕后"11"对象是s4声明产生的对象。然后再执行s3.intern时,发现常量池中"11"对象已经存在了,因此s3和s4的引用时不同的
- 2.s和s2代码中,s.intern()这一句往后放也不会有什么影响了,因为对象池中执行第一句代码String s = new String("1");的时候已经生成"1"对象的了,下边的s2声明都是直接从常量池中取地址引用的,s和s2的引用地址是不会相等的
key的生成方式
1.通过String的内容 + 长度生成hash值
2.将hash值转为key
hash生成方式
通过hash计算索引
value的生成方式
将Java的String类的实例InstanceOopDesc封装成HashtableEntry