String

前言:对于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种基本创建过程

创建过程
  1. 使用字符串常量池,每当我们使用字面量(String s=”1”;)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就将此字符串对象的地址赋值给引用s(引用s在Java栈中)。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,并将此字符串对象的地址赋值给引用s(引用s在Java栈中)。
  2. 使用字符串常量池,每当我们使用关键字new(即:String s=new String(”1”);)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s。

“+”连接形式创建字符串

  1. String s1=”1”+”2”+”3”;



    使用包含常量的字符串连接创建是也是常量,编译期就能确定了,直接入字符串常量池,当然同样需要判断是否已经存在该字符串。

  2. 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对象。

  3. 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,其和操作系统交流频繁,假如这个类被继承重写的话,难免会是操作系统造成巨大的隐患。最后字符串的不可变性使得同一字符串实例被多个线程共享,所以保障了多线程的安全性。而且类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。

参考博客

深入理解Java中的String

你可能感兴趣的:(String)