关于java:面试官Stringintern-有什么用和常量池有什么关系问倒一大片

41次阅读

共计 6090 个字符,预计需要花费 16 分钟才能阅读完成。

作者:GuoMell \
起源:blog.csdn.net/gcoder_/article/details/106644312

0. Background

在 JAVA 语言中有 8 中根本类型和一种比拟非凡的类型 String。这些类型为了使他们在运行过程中速度更快,更节俭内存,都提供了一种常量池的概念。常量池就相似一个 JAVA 零碎级别提供的缓存。

8 种根本类型的常量池都是零碎协调的,String 类型的常量池比拟非凡。

它的次要应用办法有两种:

  • 间接应用双引号申明进去的 String 对象会间接存储在常量池中。
  • 如果不是用双引号申明的 String 对象,能够应用 String 提供的 intern 办法。intern 办法会从字符串常量池中查问以后字符串是否存在,若不存在就会将以后字符串放入常量池中

举荐一个开源收费的 Spring Boot 实战我的项目:

https://github.com/javastacks/spring-boot-best-practice

1. 常量池

1.1 常量池是什么?

JVM 常量池次要分为 Class 文件常量池、运行时常量池,全局字符串常量池,以及根本类型包装类对象常量池

1.1.0 办法区

办法区的作用是存储 Java 类的构造信息,当创建对象后,对象的类型信息存储在办法区中,实例数据寄存在堆中。类型信息是定义在 Java 代码中的常量、动态变量、以及类中申明的各种办法,办法字段等;实例数据则是在 Java 中创立的对象实例以及他们的值。

该区域进行内存回收的次要目标是对常量池的回收和对内存数据的卸载;个别说这个区域的内存回收率比起 Java 堆低得多。

1.1.1 Class 文件常量池

class 文件是一组以字节为单位的二进制数据流,在 Java 代码的编译期间,咱们编写的 Java 文件就被编译为.class 文件格式的二进制数据寄存在磁盘中,其中就包含 class 文件常量池。

class 文件常量池次要寄存两大常量: 字面量和符号援用

字面量:字面量靠近 java 语言层面的常量概念

  • 文本字符串,也就是咱们常常申明的:public String s = “abc”; 中的 ”abc”
  • 用 final 润饰的成员变量,包含动态变量、实例变量和局部变量:public final static int f = 0x101;,final int temp = 3;
  • 而对于根本类型数据 (甚至是办法中的局部变量),如 int value = 1 常量池中只保留了他的的字段描述符 int 和字段的名称 value,他们的字面量不会存在于常量池。

符号援用:符号援用次要设波及编译原理方面的概念

  • 类和接口的全限定名,也就是 java/lang/String; 这样,将类名中原来的 ”.“替换为”/” 失去的,次要用于在运行时解析失去类的间接援用
  • 字段的名称和描述符,字段也就是类或者接口中申明的变量,包含类级别变量和实例级的变量
  • 办法中的名称和描述符,也即参数类型 + 返回值

1.1.2 运行时常量池

当 Java 文件被编译成 class 文件之后,会生成下面的 class 文件常量池,JVM 在执行某个类的时候,必须通过加载、链接(验证、筹备、解析)、初始化的步鄹,运行时常量池则是在 JVM 将类加载到内存后,就会将 class 常量池中的内容寄存到运行时常量池中,也就是 class 常量池被加载到内存之后的版本,是办法区的一部分。

在解析阶段,会把符号援用替换为间接援用,解析的过程会去查问字符串常量池,也就 StringTable,以保障运行时常量池所援用的字符串与字符串常量池中是统一的。

运行时常量池绝对于 class 常量池一大特色就是具备动态性,Java 标准并不要求常量只能在运行时才产生,也就是说运行时常量池的内容并不全副来自 class 常量池,在运行时能够通过代码生成常量并将其放入运行时常量池中,这种个性被用的最多的就是 String.intern()。

1.1.3 字符串常量池

在 JDK6.0 及之前版本,字符串常量池寄存在办法区中,在 JDK7.0 版本当前,字符串常量池被移到了堆中了。至于为什么移到堆内,大略是因为办法区的内存空间太小了。在 HotSpot VM 里实现的 string pool 性能的是一个 StringTable 类,它是一个 Hash 表,默认值大小长度是 1009;这个 StringTable 在每个 HotSpot VM 的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了 StringTable 上。

在 JDK6.0 中,StringTable 的长度是固定的,长度就是 1009,因而如果放入 String Pool 中的 String 十分多,就会造成 hash 抵触,导致链表过长,当调用 String#intern() 时会须要到链表上一个一个找,从而导致性能大幅度降落;在 JDK7.0 中,StringTable 的长度能够通过参数指定。

字符串常量池设计思维:

  • 字符串的调配,和其余的对象调配一样,消耗昂扬的工夫与空间代价,作为最根底的数据类型,大量频繁的创立字符串,极大水平地影响程序的性能
  • JVM 为了进步性能和缩小内存开销,在实例化字符串常量的时候进行了一些优化
    • 为字符串开拓一个字符串常量池,相似于缓存区
    • 创立字符串常量时,首先查看字符串常量池是否存在该字符串
    • 存在该字符串,返回援用实例,不存在,实例化该字符串并放入池中
  • 实现的根底
    • 实现该优化的根底是因为字符串是不可变的,能够不必放心数据抵触进行共享
    • 运行时实例创立的全局字符串常量池中有一个表,总是为池中每个惟一的字符串对象保护一个援用, 这就意味着它们始终援用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收

2. String.intern() 与字符串常量池

/**
 * Returns a canonical representation for the string object.
 * <p>
 * A pool of strings, initially empty, is maintained privately by the
 * class <code>String</code>.
 * <p>
 * When the intern method is invoked, if the pool already contains a
 * string equal to this <code>String</code> object as determined by
 * the {@link #equals(Object)} method, then the string from the pool is
 * returned. Otherwise, this <code>String</code> object is added to the
 * pool and a reference to this <code>String</code> object is returned.
 * <p>
 * It follows that for any two strings <code>s</code> and <code>t</code>,
 * <code>s.intern()&nbsp;==&nbsp;t.intern()</code> is <code>true</code>
 * if and only if <code>s.equals(t)</code> is <code>true</code>.
 * <p>
 * All literal strings and string-valued constant expressions are
 * interned. String literals are defined in section 3.10.5 of the
 * <cite>The Java&trade; Language Specification</cite>.
 *
 * @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();

字符串常量池的地位也是随着 jdk 版本的不同而地位不同。在 jdk6 中,常量池的地位在永恒代(办法区)中,此时常量池中存储的是对象。在 jdk7 中,常量池的地位在堆中,此时,常量池存储的就是援用了。

在 jdk8 中,永恒代(办法区)被元空间取代了。这里就引出了一个很常见很经典的问题,看上面这段代码。

@Test
public void test(){String s = new String("2");
    s.intern();
    String s2 = "2";
    System.out.println(s == s2);

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

//jdk6
//false
//false

//jdk7
//false
//true

这段代码在 jdk6 中输入是 false false,然而在 jdk7 中输入的是 false true。咱们通过图来一行行解释。

JDK1.6

  • String s = new String(“2”); 创立了两个对象,一个在堆中的 StringObject 对象,一个是在常量池中的“2”对象。
  • s.intern(); 在常量池中寻找与 s 变量内容雷同的对象,发现曾经存在内容雷同对象“2”,返回对象 2 的地址。
  • String s2 = “2”; 应用字面量创立,在常量池寻找是否有雷同内容的对象,发现有,返回对象 ”2″ 的地址。
  • System.out.println(s == s2); 从下面能够剖析出,s 变量和 s2 变量地址指向的是不同的对象,所以返回 false

  • String s3 = new String(“3”) + new String(“3”); 创立了两个对象,一个在堆中的 StringObject 对象,一个是在常量池中的“3”对象。两头还有 2 个匿名的 new String(“3”) 咱们不去探讨它们。
  • s3.intern(); 在常量池中寻找与 s3 变量内容雷同的对象,没有发现“33”对象,在常量池中创立“33”对象,返回“33”对象的地址。
  • String s4 = “33”; 应用字面量创立,在常量池寻找是否有雷同内容的对象,发现有,返回对象 ”33″ 的地址。
  • System.out.println(s3 == s4); 从下面能够剖析出,s3 变量和 s4 变量地址指向的是不同的对象,所以返回 false

JDK1.7

  • String s = new String(“2”); 创立了两个对象,一个在堆中的 StringObject 对象,一个是在堆中的“2”对象,并在常量池中保留“2”对象的援用地址。
  • s.intern(); 在常量池中寻找与 s 变量内容雷同的对象,发现曾经存在内容雷同对象“2”,返回对象“2”的援用地址。
  • String s2 = “2”; 应用字面量创立,在常量池寻找是否有雷同内容的对象,发现有,返回对象“2”的援用地址。
  • System.out.println(s == s2); 从下面能够剖析出,s 变量和 s2 变量地址指向的是不同的对象,所以返回 false

  • String s3 = new String(“3”) + new String(“3”); 创立了两个对象,一个在堆中的 StringObject 对象,一个是在堆中的“3”对象,并在常量池中保留“3”对象的援用地址。两头还有 2 个匿名的 new String(“3”) 咱们不去探讨它们。
  • s3.intern(); 在常量池中寻找与 s3 变量内容雷同的对象,没有发现“33”对象,将 s3 对应的 StringObject 对象的地址保留到常量池中,返回 StringObject 对象的地址。
  • String s4 = “33”; 应用字面量创立,在常量池寻找是否有雷同内容的对象,发现有,返回其地址,也就是 StringObject 对象的援用地址。
  • System.out.println(s3 == s4); 从下面能够剖析出,s3 变量和 s4 变量地址指向的是雷同的对象,所以返回 true。

3. String.intern() 的利用

在大量字符串读取赋值的状况下,应用 String.intern() 会大大的节俭内存空间。

static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];

public static void main(String[] args) throws Exception {Integer[] DB_DATA = new Integer[10];
    Random random = new Random(10 * 10000);
    for (int i = 0; i < DB_DATA.length; i++) {DB_DATA[i] = random.nextInt();}
 long t = System.currentTimeMillis();
    for (int i = 0; i < MAX; i++) {//arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));
         arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();}

 System.out.println((System.currentTimeMillis() - t) + "ms");
    System.gc();}

运行的参数是:-Xmx2g -Xms2g -Xmn1500M 上述代码是一个演示代码,其中有两条语句不一样,一条是应用 intern,一条是未应用 intern。发现不应用 intern 的代码生成了 1000w 个字符串,占用了大概 640m 空间。

应用了 intern 的代码生成了 1345 个字符串,占用总空间 133k 左右。其实通过观察程序中只是用到了 10 个字符串,所以精确计算后应该是正好相差 100w 倍。尽管例子有些极其,但的确能精确反馈出 intern 应用后产生的微小空间节俭。

利用 String 的不变性,String.intern() 办法实质就是维持了一个 String 的常量池,而且池里的 String 应该都是惟一的。这样,咱们便能够利用这种唯一性,来做一些文章了。咱们能够利用池里 String 的对象来做锁,实现对资源的管制。比方一个城市的某种资源同一时间只能一个线程拜访,那就能够把城市名的 String 对象作为锁,放到常量池中去,同一时间只能一个线程取得。

不当的应用:fastjson 中对所有的 json 的 key 应用了 intern 办法,缓存到了字符串常量池中,这样每次读取的时候就会十分快,大大减少工夫和空间,而且 json 的 key 通常都是不变的。然而这个中央没有思考到大量的 json key 如果是变动的,那就会给字符串常量池带来很大的累赘。

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿 (2022 最新版)

2. 劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4. 别再写满屏的爆爆爆炸类了,试试装璜器模式,这才是优雅的形式!!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0