前言:对于java开发人员,想必对String的使用已经很熟悉了,但可能对其内部的一些机制与细节不甚了解,本篇博客将对String的部分机制做总结
部分源码
public int hashCode();
public native String intern();
public boolean equals(Object anObject) ;
String类是不可继承的
public final class String
hashCode
先看源码:
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;
}
由上图可以,String重写了Object的本地方法
String的存储(HotSpot为例)
String在jdk6及之前是存放到Perm Gen区(永久区)的,在jdk7之后存放在Heap(堆)里,也就是说字符串常量池从方法区中移到了Heap中(这里的Heap不是指metaspace的native heap)。
什么是常量?
用final修饰的成员变量表示常量,值一旦给定就无法改变!
final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。
常量池的两种形态
1) 静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
2) 运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池
注:记住jdk7(包括jdk7)之前,方法区的数据在jvm开辟的内存中,但是jdk8将方法区放到metaspace里了,也就是说之前没有移到java heap中的数据移到了native heap中了。但是字符串常量池在jdk7的时候被移到了java heap中,并没有在metasapce里。
Srtring在JVM层解析
字符串的2种基本创建过程
- 使用字符串常量池,每当我们使用字面量(String s=”1”;)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就将此字符串对象的地址赋值给引用s(引用s在Java栈中)。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,并将此字符串对象的地址赋值给引用s(引用s在Java栈中)。
- 使用字符串常量池,每当我们使用关键字new(即:String s=new String(”1”);)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s。
“+”连接形式创建字符串
-
String s1=”1”+”2”+”3”;
使用包含常量的字符串连接创建是也是常量,编译期就能确定了,直接入字符串常量池,当然同样需要判断是否已经存在该字符串。
-
String s2=”1”+”3”+new String(“1”)+”4”;
当使用“+”连接字符串中含有变量时,也是在运行期才能确定的。首先连接操作最开始时如果都是字符串常量,编译后将尽可能多的字符串常量连接在一起,形成新的字符串常量参与后续的连接(可通过反编译工具jd-gui进行查看)。 接下来的字符串连接是从左向右依次进行,对于不同的字符串,首先以最左边的字符串为参数创建StringBuilder对象(可变字符串对象),然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象(注意:中间的多个字符串常量不会自动拼接)。
实际上的实现过程为:String s2=new StringBuilder(“13”).append(new String(“1”)).append(“4”).toString();
当使用+进行多个字符串连接时,实际上是产生了一个StringBuilder对象和一个String对象。 -
String s3=new String(“1”)+new String(“1”);
相当于:String s2=new StringBuilder(“”).append(new String(“1”)).append(new String(“1”)).toString();
String.intern()解析
public class StringTest {
public static void main(String[] args) {
// TODO 自动生成的方法存根
String s3 = new String("1") + new String("1");
System.out.println(s3 == s3.intern());
}
JDK6的执行结果为:false
JDK7和JDK8的执行结果为:true
JDK6的内存模型如下:
JDK7JDK8的内存模型如下:
原因:
JDK7中,字符串常量池已经被转移至Java堆中,开发人员也对intern 方法做了一些修改。因为字符串常量池和new的对象都存于Java堆中,为了优化性能和减少内存开销,当调用 intern 方法时,如果常量池中已经存在该字符串,则返回池中字符串;否则直接存储堆中的引用,也就是字符串常量池中存储的是指向堆里的对象。所以结果为true。
String被设计成不可变和不能被继承的原因
String是不可变和不能被继承的(final修饰),这样设计的原因主要是为了设计考虑、效率和安全性。
字符串常量池的需要
只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。假若字符串对象允许改变,那么将会导致各种逻辑错误,比如改变一个对象会影响到另一个独立对象. 严格来说,这种常量池的思想,是一种优化手段。
String对象缓存HashCode
Java中的String对象的哈希码被频繁地使用,字符串的不可变性保证了hash码的唯一性。正是有了唯一的hashcode,jvm才可以快速的发现字符串是否被创建,提高了效率。
安全性
首先String被许多Java类用来当参数,如果字符串可变,那么会引起各种严重错误和安全漏洞。再者String作为核心类,很多的内部方法的实现都是本地调用的,即调用操作系统本地API,其和操作系统交流频繁,假如这个类被继承重写的话,难免会是操作系统造成巨大的隐患。最后字符串的不可变性使得同一字符串实例被多个线程共享,所以保障了多线程的安全性。而且类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。