小言_互联网的博客

学习String相关知识点的一些总结

403人阅读  评论(0)

由一个例子引出今天的猪脚。


        String ss = "ab";
        String s1 = "a";
        String s2 = "b";
        String s3 = s1 + s2;
        String s33 = "ab";
        String s4 = new String("a" + "b");

        System.out.println(ss == s3);//false
        System.out.println(s3 == s33);//false
        System.out.println(ss == s33);//true
        System.out.println("---------------------------------------");
        System.out.println(s3 == s1+s2);//false
        System.out.println(s3 == s4);//false
        System.out.println(ss == s4);//false
        System.out.println("---------------------------------------");
        String s5 = "a" + "b";
        System.out.println(s4 == s5);//false
        String s51 = "a" + "b";
        System.out.println(s51 == s5);//true
        System.out.println("---------------------------------------");
        final String s6 = "a";
        final String s7 = "b";
        System.out.println(ss == s5);//true
        System.out.println(ss == s6+s7); //true

执行结果 :
false
false
true

false
false
false

false
true

true
true

下面从结果来看分析一下
首先,我们都知道 == 、equals这两种比较是否相等的方式,== 的比较两个基本类型 那么判断 true/false 依据是值是否相等,对于非基本类型比较的是内存的地址是否相同。
ss == s3 false 说明变量String s3 =“a”+“b” 指向的地址不等于 String ss = “ab”;
String ss = "ab" ss 直接指向的是常量池中的“ab”的地址。 而 s3 并不是直接指向常量池中的“ab”地址,故而 “a”+“b” 这一过程 存在类似 new 对象的过程。指向的是其在堆中的地址。

接下来 ss == s33 true 这个说明都是指向的同一地址,都执向了常量池中的 “ab”。

 String s4 = new String("a" + "b");
 String ss = "ab";

s4 == ss false
这个可以结合下面这个常见的问题来解释:
众所周知,String 有两种声明方式

String a = "abc";
String b = new String("abc");

这两种声明方式是有区别的,第一种方式是栈内存a 指向 常量池中的“abc”。
第二种方式 是在堆中开辟内存创建了一个String 对象,然后这个对象指向的是线程池中的“abc”。

换一种说法就是下面这个问题的答案。
String a = new String(“abc”) 创建多少个对象?

  1. 在常量池中查找是否有“abc”对象
    有则返回对应的引用实例
    没有则创建对应的实例对象
  2. 在堆中 new 一个 String(“abc”) 对象
  3. 将对象地址赋值给str4,创建一个引用

所以,` 常量池中没有“abc”字面量则创建两个对象,否则创建一个对象,以及创建一个引用变量,往往会提出这样的变式题:
String str1 = new String(“A”+“B”) ; 会创建多少个对象?
String str2 = new String(“ABC”) + “ABC” ; 会创建多少个对象?

str1:
字符串常量池:“AB” : 1个
堆:new String(“AB”) :1个
引用: str1 :1个
总共 : 5个

str2 :
字符串常量池:“ABC” : 1个
堆:new String(“ABC”) :1个
引用: str2 :1个
总共 : 3个

下面用图形表示一下String a = new String(“abc”)在jvm内存中的分布,

JVM的内存区域

JVM的内存区域分为 程序计数器、 虚拟机栈、 本地方法区、 堆、 方法区 5部分。
这5部分又可以根据是否线程私有,归为两类。

线程私有:程序计数器、虚拟机栈、本地方法区。
线程共享:堆、方法区。

程序计数器(线程私有):一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器 ,这类内存也称为“线程私有”的内存。
正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。

虚拟机栈(线程私有):是描述java方法执行的内存模型 ,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息 。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。

本地方法区(线程私有):本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务 , 如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。

堆(Heap- 线程共享)-运行时数据区:是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域 。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代( Eden 区 、 From Survivor 区 和 To Survivor 区 )和老年代。

方法区/ 永久代(线程共享):即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 . HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存,而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)。
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中 。 Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。
常量池的设计保证了String 类型的唯一性,避免了重复开辟空间,节省了内存。这里可以扩展一个问题,String 为什么被设计成不可变的?

另java8开始 没有永久代一说,改成元空间。
Java 8: 从永久代(PermGen)到元空间(Metaspace)

String类型为什么不可变

下面来聊一下的String 为什么被设计成不可变的?

String是Java中最常用的类,是不可变的(Immutable), 那么String是如何实现Immutable呢,String为什么要设计成不可变呢?
先翻看String 底层实现:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
    
    //other codes
}

String 的底层实现是依靠 char value[] 数组,既然依靠的是基础类型变量,那么他一定是可变的, String 之所以不可变,是因为 Java 的开发者通过技术实现,隔绝了使用者对 String 的底层数据的操作。

String 不可变的技术实现
String 类由关键字 final 修饰,说明该类不可继承
char value[] 属性也被 final 所修饰,说明 value 的引用在创建之后,就不能被改变
以上两点并不能完全实现 String 不可变 ,原因在于:

final int[] value={1,2,3}
      int[] another={4,5,6};
value=another;    // 编译器报错,final不可变

value 被 final 修饰,只能保证引用不被改变,但是 value 所指向的堆中的数组,才是真实的数据,只要能够操作堆中的数组,依旧能改变数据。

final int[] value={1,2,3};
value[2]=100;  //这时候数组里已经是{1,2,100}

所有的成员属性均被 private 关键字所修饰
为了实现 String 不可变,关键在于Java的开发者在设计和开发 String 的过程中,没有暴露任何的内部成员,与此同时 API 的设计是均没有操作 value 的值 , 而是采用 new String() 的方式返回新的字符串,保证了 String 的不可变。
JDK String API 源码:

public static String valueOf(char c) {
        char data[] = {c};
        return new String(data, true);  //采用 new String() 的方式返回新的字符串
    }
    
    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);  //采用 new String() 的方式返回新的字符串
    }

整个String设成final禁止继承,避免被其他人继承后破坏。所以String是不可变的关键都在底层的实现,而不是一个final。

为什么会将 String 设计为不可变?

安全
保证线程安全,在并发场景下,多个线程同时读写资源时,会引竞态条件,由于 String 是不可变的,不会引发线程的问题而保证了线程的安全。
HashCode,当 String 被创建出来的时候,hashcode也会随之被缓存,hashcode的计算与value有关,若 String 可变,那么 hashcode 也会随之变化,针对于 Map、Set 等容器,他们的键值需要保证唯一性和一致性,因此,String 的不可变性使其比其他对象更适合当容器的键值。
性能
当字符串是不可变时,字符串常量池才有意义。字符串常量池的出现,可以减少创建相同字面量的字符串,让不同的引用指向池中同一个字符串,为运行时节约很多的堆内存。若字符串可变,字符串常量池失去意义,基于常量池的String.intern()方法也失效,每次创建新的 String 将在堆内开辟出新的空间,占据更多的内存。

字符串常量池

在《深入理解java虚拟机》这本书上是这样写的:对于HotSpot虚拟机,根据官方发布的路线图信息,现在也有放弃永久代并逐步的改为采用Native Memory来实现方法区的规划了,在目前已经发布的JDK1.7的HotSpot中,已经把原来存放在方法区中的字符串常量池移出。根据查阅的资料显示在JDK1.7以后的版本中字符串常量池移到堆内存区域;同时在jdk1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域。

在 JAVA 语言中有8中基本类型和一种比较特殊的类型String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。常量池就类似一个JAVA系统级别提供的缓存。

java中两种创建字符串对象的方式的分析。

它的主要使用方法有两种:

  • 直接使用双引号声明出来的String对象会直接存储在常量池中。
  • 如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中
String s1 = "abc";
String s2 = "abc";
System.out.println(s1==s2);  true

采用字面值的方式创建一个字符串时,JVM首先会去字符串池中查找是否存在"abc"这个对象,如果不存在,则在字符串常量池中创建"abc"这个对象,然后将池中"abc"这个对象的引用地址返回给"abc"对象的引用s1,这样s1会指向字符串常量池中"abc"这个字符串对象;如果存在,则不创建任何对象,直接将池中"abc"这个对象的地址返回,赋给引用s2。因为s1、s2都是指向同一个字符串池中的"abc"对象,所以结果为true。

String s3 = new String("xyz");
String s4 = new String("xyz");
System.out.println(s3==s4); false

采用new关键字新建一个字符串对象时,JVM首先在字符串池中查找有没有"xyz"这个字符串对象,如果有,则不在池中再去创建"xyz"这个对象了,直接在堆中创建一个"xyz"字符串对象,然后将堆中的这个"xyz"对象的地址返回赋给引用s3,这样,s3就指向了堆中创建的这个"xyz"字符串对象;如果没有,则首先在字符串池中创建一个"xyz"字符串对象,然后再在堆中创建一个"xyz"字符串对象,然后将堆中这个"xyz"字符串对象的地址返回赋给s3引用,这样,s3指向了堆中创建的这个"xyz"字符串对象。s4则指向了堆中创建的另一个"xyz"字符串对象。s3 、s4是两个指向不同对象的引用,结果当然是false。

Intern的实现原理

/**
     * Returns a canonical representation for the string object.
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     */
    public native String intern();

String#intern方法中看到,这个方法是一个 native 的方法,但注释写的非常明了。“ 当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象的引用。

public void test(){
        //发现原来是在JVM启动的时候调用了一些方法,在常量池中已经生成了"java"字符串常量,
         String s2 = new String("ja") + new String("va");
         String s3 =  s2.intern();
         String s4 = "java";
         System.out.println(s2 == s3);fasle
         System.out.println(s3 == s4);true
    }

JDK1.8版本中,String常量池已经从方法区中的运行时常量池分离到堆中了,那么在堆中的String常量池里存的是String对象还是引用呢?

相关资料:

面试别再问我String了
String类型为什么不可变
String字符串“真正存储位置”
String的内存模型,为什么String被设计成不可变的
深入解析String#intern
JDK1.8版本java字符串常量池里存的是String对象还是引用?

如果你也热衷技术欢迎加群一起进步:230274309 。 一起分享,一起进步!少划水,多晒干货!!欢迎大家!!!(进群潜水者勿加)


转载:https://blog.csdn.net/u011733020/article/details/103924943
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场