盒子
盒子
文章目录
  1. ChangeLog
  2. 讲得好啊…
  3. 说明
  4. 实现
  5. equals()
  6. 创建对象
  7. 区别
    1. 先简单说一下
    2. 字符串常量池
    3. 理解一下 intern()
  • JVM 内存模型
  • 拼接字符串
    1. javap 使用
    2. 测试
  • String 分析(栈,堆,字符串常量池)

    ChangeLog

    2019-03-23 继续了解了一下字符串常量池…

    讲得好啊…

    Java 源码分析 — String 的设计

    这篇讲的太好了,源码方面的分析直接看这篇吧…

    JDK1.8关于运行时常量池, 字符串常量池的要点

    Java 永久代去哪儿了

    JAVA中String.intern的理解

    几张图轻松理解String.intern()

    深入解析String#intern

    在线查看 jdk8 源码 这里

    说明

    讨论若未作说明,皆基于 jdk 1.8 +

    这一块太抽象了,很多地方要有图才说明得清楚…我只是稍作记录,便于回顾,就不弄那么详细了…

    1
    2
    3
    public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    }

    能看到,String 类由 final 修饰,这代表 String 初始化后就不能改变.这里的改变,指的是创建的对象不可变.

    String,StringBuffer 和 StringBuilder 都是 final 类,String 不可变是由于里面真正存储数据的属性 :

    1
    2
    /** The value is used for character storage. */
    private final char value[];

    value 数组为 final…

    比如:

    1
    2
    String str = "hello";
    str = "world";

    看起来似乎是改变了,实际上只是引用的指向变了,第一次创建的那个对象 “hello” 并没有变化.

    而 String 为 final,效果是 String 不能被继承,避免被其他人继承后破坏.

    其次,String 实现了三个接口.

    实现

    看源码,底层由一个 char 数组实现

    1
    private final char value[];

    String 不可变,String 类中每一个看起来会修改 String 值的方法,实际上都是新建了一个对象.

    以 substring() 举例.

    1
    2
    3
    4
    5
    6
    7
    public String substring(int beginIndex, int endIndex) {
    ...
    int subLen = endIndex - beginIndex;
    ...
    return ((beginIndex == 0) && (endIndex == value.length)) ? this
    : new String(value, beginIndex, subLen);
    }

    实际上返回了一个新对象.

    PS : 本来想用字符串的拼接举例的,比如

    1
    String str = "hell" + "o";

    记得在 C++ 中可以重载 “+” 运算符,可是 JAVA 怎么做呢…

    [java 编程思想]

    java 并不允许程序员重载任何操作符,用于 String 的 “+” 和 “+=” 是 java 中仅有的两个重载过的操作符.

    jdk 1.8 文档

    字符串连接是通过StringBuilder (或StringBuffer )类及其append方法实现的.

    [java 编程思想]

    编译期做了一定程度的优化,自动引入了 StringBuilder,调用它的 append 和 toString 方法实现拼接.

    不能依赖编译器的优化,频繁拼接应选用 StringBuilder.

    equals()

    注意,equals() 与 == 在默认情况下都是比较变量指向的首地址.

    查看源码可以发现 String 重写了 equals(),使其目的变成比较字符串对象的内容是否一致…

    创建对象

    String 创建对象有两种方式.

    1
    2
    1. String str1 = "hello";
    2. String str2 = new String("hello");

    区别

    先简单说一下

    首先第一种,看一段代码.

    1
    2
    3
    4
    String s1 = “abc”; 
    String s2 = “abc”;
    System.out.println(s1 == s2);
    输出 : true

    通过双引号指定的字符串对象,在编译期间确定,通过 equals() 查找常量池有无该字符串,若有,返回.若无,创建再返回.

    PS : 用 equals() 来比较,比较的到底是字符串对象还是字符串对象的引用都无妨了…

    s1,s2 都指向字符串常量池中的同一个地址.

    第二种.

    1
    2
    3
    4
    String s1 = new String(“abc”); 
    String s2 = new String(“abc”);
    System.out.println(s1 == s2);
    输出 : false

    s1,s2 是堆中两个不同对象的引用.

    看看 jdk1.8 的源码 :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /**
    * Initializes a newly created {@code String} object so that it represents
    * the same sequence of characters as the argument; in other words, the
    * newly created string is a copy of the argument string. Unless an
    * explicit copy of {@code original} is needed, use of this constructor is
    * unnecessary since Strings are immutable.
    *
    */
    public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
    }

    这种方式实际上创建了两个对象.

    in other words, the newly created string is a copy of the argument string.

    参数实际传的是一个 String 对象的引用(也就是上面提到的用 双引号 指定字符串对象,此时会有与字符串常量池中数据的判断).这里生成了一个对象,这个对象在编译期间确定.

    第二次又用 new 关键字创建了一个对象.这个对象实际是参数对象的副本.这一块和普通对象的创建一致.

    查看 对象的创建

    字符串常量池

    字符串常量池是干吗用的??

    因为字符串作为一个基础的数据类型,使用非常频繁.

    而我们已经知道,String 对象是不可变的.所以当我们需要多个 String 对象时,它们实际上指向的同一个对象还是不同的对象,都没什么关系.

    基于此,为了减少频繁创建字符串所带来的性能影响,JVM 为字符串开辟了一个类似于缓存区的空间,这个空间就是字符串常量池.在字符串常量池中,每个字符串只保存一份.(通过 equals() 判断 String 的值)

    第一种创建方式 中,之所以打印”true”,就是因为对于同一个字符串对象,字符串常量池中只保存一份.实际比较的其实是字符串常量池中的同一个地址.

    jdk 1.7 以前,字符串常量池在运行时常量池里,而运行时常量池放在方法区.

    不过字符串常量池放在方法区 会导致内存溢出错误.(与 JVM 内存分配联系)

    于是在 jdk 1.7 中,把字符串常量池从方法区移动到堆中.(运行时常量池中的其他东西不变动)

    方法区是 JVM 的概念模型.这一块联系 JVM 的内存模型.

    永久代和元空间是方法区的两种不同具体实现.

    • MetaSpace(元空间) –> 使用本地内存
    • PermGen(永久代) –> 使用 JVM 的内存

    字符串常量池存在永久代中,更容易出现性能问题和内存溢出.

    jdk 1.7 中,把字符串常量池从方法区移动到堆中.

    • 类和方法的信息大小难以确定,给永久代的大小指定带来困难.
    • 永久代会为 GC 带来不必要的复杂度.

    jdk 1.8 中,HotSpot 虚拟机用元空间取代了永久代.

    好处 : java.lang.OutOfMemoryError : PermGen 这个异常不复存在了

    理解一下 intern()

    1
    2
    3
    String str = "hello";
    1. System.out.println(str);
    2. System.out.println(str.intern());

    从输出结果来看,1,2 没区别…

    1
    2
    hello
    hello

    intern() 返回的是字符串对象的规范表示.

    它返回的内容取自具有唯一字符串的值.

    其实也就是说它返回的值来自字符串常量池.

    看网上的介绍,对于字符串常量池中到底保存的是字符串对象,还是字符串对象的引用,众说纷纭…

    看 jdk1.8 对 intern() 的说明.

    When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object)method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

    这个说明对于理解原理貌似没有啥参考价值…

    jdk6 和 jdk6+ 的描述是一样的,但是 jdk6 和 jdk6+ 的实现并不一致.

    尝试跟踪源码.

    1
    public native String intern();

    是个本地方法,继续深入 :

    在线查看

    1
    2
    3
    4
    5
    \openjdk7\jdk\src\share\native\java\lang\String.c
    Java_java_lang_String_intern(JNIEnv *env, jobject this)
    {
    return JVM_InternString(env, this);
    }

    日…到这步跟不下去了.还是搜索一下看看别人有没有关于这玩意儿的资料吧…

    果然找到!!

    深入解析String#intern

    还是直接给结果吧…

    看段代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public static void main(String[] args) {
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
    }

    jdk6 输出 : false false
    jdk7 输出 : false true

    why?

    • jdk6

      上面已经提到过,String s = new String("1"); 这种方式创建字符串实际生成了两个字符串对象.

      首先,构造器中传入了一个字符串对象 “1”,它就被放在字符串常量池中.

      然后在堆中创建一个字符串对象,s 指向这个字符串对象的首地址.

      调用 s.intern() ,字符串常量池已经存在该对象,直接返回.

      s2 指向的就是字符串常量池中 “1” 的首地址.

      而 == 判断的是地址是否相同.显然不同.

      s3 与 s4 不同之处在于,执行 s3.intern() 时,字符串常量池中还没有字符串对象,所以需要在字符串常量池中创建一个对象.其余的与上相同.

    • jdk 7

      s 与 s2 情况与 jdk6 一样.

      但是执行 s3.intern() 时,字符串常量池中还没有字符串对象,此时先堆中有没有该字符串对象,如果有,就把堆中字符串对象的引用保存到字符串常量池中.

      所以 s4 指向的和 s3 一样,都是堆中 String 对象的引用.

      所以判断为 true.

    总结 :

    jdk6 : 当调用 intern 方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用.否则,将该字符串对象添加到字符串常量池中,并且返回该字符串对象的引用.

    jdk6+ : 当调用 intern 方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用.否则,如果该字符串对象已经存在于 Java堆 中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用.

    jdk6 字符串只存在于 字符串常量池.

    jdk6+ 字符串存在于字符串常量池和 Java堆.

    那么回到上面那个问题,字符串常量池中到底保存的是字符串对象,还是字符串对象的引用?

    答案就是,jdk6 保存的是字符串对象,jdk6+ 既保存了字符串对象,又保存了字符串对象的引用.

    JVM 内存模型

    https://hqweay.cn/2018/10/09/jvm/#内存管理机制

    拼接字符串

    [JAVA 编程思想中文版] P284

    尝试了一下拼接字符串,然后调用 jdk 自带的反编译工具查看…

    拼接字符串被 JVM 优化,操作过于频繁时会使用 StringBuilder.

    javap 使用

    首先对 java 文件进行编译.如 Test.java

    javac Test.java

    生成了 Test.class

    再执行 javap -c Test.calss

    或者 javap -c Test

    即可查看反编译的代码.

    测试

    1
    String str4 = "haha" + "hehe" + 112 + "shsh";

    当只有这么一行的时候,反编译效果.

    1
    2
    ldc           #5                  // String hahahehe112shsh
    astore 4

    后面并未注释使用了 StringBuilder.

    但是执行

    1
    2
    3
    for(int i = 0; i < 15; i++){
    str4 += "llo";
    }

    反编译发现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    30: new           #6                  // class java/lang/StringBuilder
    33: dup
    34: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
    37: aload 4
    39: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    42: ldc #9 // String llo
    44: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    47: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    50: astore 4

    可以大概看出使用了 StringBuilder 的append().