关于==

该符号对于基本类型和引用类型的作用效果是不同的:

  • 对于基本类型:比较的是值是否相同

    byte、short、int、long、float、double、boolean、char

  • 对于引用类型:比较的是引用是否相同

  • “str” != new String(“str”);

关于equals

equals方法是Object类里面的,每个类都默认继承Object类,所以每个java类都有equals方法。

equals的本质就是==,因为是类里面比较两个引用,所以是引用比较。但是String,Integer等类重写了equals方法,把他变成了值比较。


字符串常量池

在java8中,为了优化字符串的存储和性能,引入了一种称为字符串常量池的机制,该机制在jvm层面提供。

实现前提

  • 在Java中,String 类型是不可变的(immutable),这意味着一旦一个 String 对象被创建,它的内容就不能被改变。

  • 在Java运行时,全局字符串常量池会跟踪所有唯一的字符串字面量,确保相同的字符串字面量只存储一次。当进行垃圾回收时,如果一个字符串对象仍然有至少一个引用指向它(即它仍然被程序中的某个部分使用),那么这个对象就不会被垃圾回收机制回收。

实现原理

  1. 在JVM层面为字符串提供字符串常量池,可以理解为是一个缓存区;

  2. 创建字符串常量时,JVM会检查字符串常量池中是否存在这个字符串;

  3. 若字符串常量池中存在该字符串,则直接返回引用实例;若不存在,先实例化该字符串,并且,将该字符串放入字符串常量池中,以便于下次使用时,直接取用,达到缓存快速使用的效果。

    1
    2
    3
    4
    String str1 = "abc";
    String str2 = "abc";
    System.out.println("str1 == str2: " + (str1 == str2));
    //结果:str1 == str2: true

字符串常量池位置的变化

方法区:和java堆一样,是各个线程共享的区域。当虚拟机需要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据

主要包含两个模块:class文件,运行时常量池

  • 在jdk1.7之前,运行时常量池和字符串常量池都在方法区中,而方法区的实现被称为永久代,在jvm内存中。这个区域的GC主要是针对运行时常量池的回收和对类型的卸载。

  • 在jdk1.7中,字符串常量池被从永久代移到了堆区,运行时常量池保留在永久代中。

  • jdk1.8中,永久代被移除,方法区的实现变成了本地内存中的元空间。此时字符串常量池仍在堆中,运行时常量池在元空间。

为什么要把方法区从jvm内存(永久代)移到本地内存(元空间),原因如下:

  1. 直接内存属于本地系统的IO操作,具有更高的一个IO操作性能,而JVM的堆内存这种,如果有IO操作,也是先复制到直接内存,然后再去进行本地IO操作。经过了一系列的中间流程,性能就会差一些。非直接内存操作:本地IO操作——>直接内存操作(本地内存)——>非直接内存操作(jvm)——>直接内存操作——>本地IO操作,而直接内存操作:本地IO操作——>直接内存操作(本地内存)——>本地IO操作
  2. 永久代有一个无法调整更改的JVM固定大小上限,回收不完全时,会出现OutOfMemoryError问题;而直接内存(元空间)是受到本地机器内存的限制,不会有这种问题。

新建字符串

创建一个字面量字符串,会在字符串常量池创建一个对象,一个引用(str)指向该对象。如果有相同字面值的创建,那么会有多个引用指向该对象。字面量字符串在编译时被识别,生成字节码,并在类加载时放入字符串常量池中。

1
String str1 = "abc";

使用new创建一个字符串:

  • 如果在代码中就在new String(“123”)中写了”123”这个字面值,那么该字面值在编译期间会被识别,生成字节码。

  • 在类加载时,JVM会检查字符串常量池中是否已经存在一个值为 "123" 的字符串对象。如果不存在,JVM会在字符串常量池中创建一个新的字符串对象,并将其放入池中。

  • 无论字符串常量池中是否存在该对象,在运行期间,new String("123") 都会在堆内存中创建一个新的 String 对象实例。

  • 返回堆中String对象的引用。

1
String str2 = new String("123");

即:若常量池里没有”123”字符串,则创建了2个对象和一个堆引用;若有该字符串,则创建了一个对象及对应的引用。

涉及到字符串拼接问题:

String str =”ab” + “cd”;对象个数?

分析:若字符串常量池该字符串对象

  1. 字符串常量池:(1个对象)”abcd”(发生编译时常量折叠) ;
  2. 堆:无
  3. 栈:(1个引用)str->”abcd”
    总共:1个对象+1个引用

String str = new String(“abc”);对象个数?

分析:若字符串常量池该字符串对象

  1. 字符串常量池:(1个对象)”abc”;
  2. 堆:(1个对象)new String(“abc”)
  3. 栈:(1个引用)str->堆
    总共:2个对象+1个引用

String str = new String(“a” + “b”);对象个数?

分析:若字符串常量池该字符串对象

  1. 字符串常量池:(1个对象)”ab”(发生编译时常量折叠);
  2. 堆:(1个对象)new String(“ab”)
  3. 栈:(1个引用)str->堆
    总共:2个对象+1个引用

String str = new String(“ab”) + “ab”;对象个数?

分析:若字符串常量池该字符串对象

  1. 字符串常量池:(1个对象)”ab”;("abab" 是通过拼接生成的,而非直接使用字符串字面量。)

  2. 堆:

    • new String("ab") 创建了1个新的 String 对象。

    • 拼接时,创建了1个 StringBuilder 对象。

    • 拼接后的新 String 对象 "abab" 会存放在堆中。

  3. 栈:(1个引用)str->新 String 对象 "abab"
    总共:4个对象+1个引用

String str = new String(“ab”) + new String(“ab”);对象个数?

分析:若字符串常量池该字符串对象

  1. 字符串常量池:(1个对象)”ab”;

  2. 堆:

    • 两个 new String("ab") 分别创建了两个新的 String 对象。

    • 拼接操作创建了一个 StringBuilder 对象。

    • 拼接结果 "abab" 会创建一个新的 String 对象。

  3. 栈:(1个引用)str->新 String 对象 "abab"
    总共:5个对象+1个引用

String str = new String(“ab”) + new String(“cd”);对象个数?

分析:若字符串常量池该字符串对象

  1. 字符串常量池:(2个对象)”ab”,”cd”;
  2. 堆:(4个对象)new String(“ab”),new String(“cd”), stringbuilder, 拼接后的string对象
  3. 栈:(1个引用)str
    总共:6个对象+1个引用

String str3 = str1 + str2;对象个数?

1
2
3
String str1 = "ab";
String str2 = "cd";
String str3 = str1 + str2;

分析:若字符串常量池该字符串对象

  1. 字符串常量池:(2个对象)”ab”,”cd”,”abcd”;
  2. 堆:无
  3. 栈:(3个引用)str1,str2,str3
    总共:2个对象+3个引用

如何指向字符串池中的特定的对象?

通过String的intern()方法。作用是当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用。

1
2
3
4
5
6
7
8
9
10
String s0= “kvill”; 
String s1=new String(”kvill”);
String s2=new String(“kvill”);
System.out.println( s0==s1 );
System.out.println( “**********” );
s1.intern();
s2=s2.intern(); //把常量池中“kvill”的引用赋给s2
System.out.println( s0==s1);
System.out.println( s0==s1.intern() );
System.out.println( s0==s2 );