面试官:说说 String.intern() 和常量池?不同 JDK 版本有什么区别?

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 面试官:说说 String.intern() 和常量池?不同 JDK 版本有什么区别?

0. Background

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

8种基本类型的常量池都是系统协调的,String类型的常量池比较特殊。


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


直接使用双引号声明出来的String对象会直接存储在常量池中。

如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中


1. 常量池

1.1 常量池是什么?

image.png


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中,永久代(方法区)被元空间取代了。这里就引出了一个很常见很经典的问题,看下面这段代码。


Java 8 系列教程推荐看这里:


https://www.javastack.cn/categories/Java/Java8/


@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

image.png


  1. String s = new String("2");创建了两个对象,一个在堆中的StringObject对象,一个是在常量池中的“2”对象。
  2. s.intern();在常量池中寻找与s变量内容相同的对象,发现已经存在内容相同对象“2”,返回对象2的地址。
  3. String s2 = "2";使用字面量创建,在常量池寻找是否有相同内容的对象,发现有,返回对象"2"的地址。
  4. System.out.println(s == s2);从上面可以分析出,s变量和s2变量地址指向的是不同的对象,所以返回false
  5. String s3 = new String("3") + new String("3");创建了两个对象,一个在堆中的StringObject对象,一个是在常量池中的“3”对象。中间还有2个匿名的new String(“3”)我们不去讨论它们。
  6. s3.intern();在常量池中寻找与s3变量内容相同的对象,没有发现“33”对象,在常量池中创建“33”对象,返回“33”对象的地址。
  7. String s4 = "33";使用字面量创建,在常量池寻找是否有相同内容的对象,发现有,返回对象"33"的地址。
  8. System.out.println(s3 == s4);从上面可以分析出,s3变量和s4变量地址指向的是不同的对象,所以返回false


JDK1.7

image.png


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 如果是变化的,那就会给字符串常量池带来很大的负担。


原文链接:https://blog.csdn.net/gcoder_/article/details/106644312



相关文章
|
22天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
54 14
|
21天前
|
Java 关系型数据库 数据库
京东面试:聊聊Spring事务?Spring事务的10种失效场景?加入型传播和嵌套型传播有什么区别?
45岁老架构师尼恩分享了Spring事务的核心知识点,包括事务的两种管理方式(编程式和声明式)、@Transactional注解的五大属性(transactionManager、propagation、isolation、timeout、readOnly、rollbackFor)、事务的七种传播行为、事务隔离级别及其与数据库隔离级别的关系,以及Spring事务的10种失效场景。尼恩还强调了面试中如何给出高质量答案,推荐阅读《尼恩Java面试宝典PDF》以提升面试表现。更多技术资料可在公众号【技术自由圈】获取。
|
23天前
|
Java 编译器 API
深入解析:JDK与JVM的区别及联系
在Java开发和运行环境中,JDK(Java Development Kit)和JVM(Java Virtual Machine)是两个核心概念,它们在Java程序的开发、编译和运行过程中扮演着不同的角色。本文将深入解析JDK与JVM的区别及其内在联系,为Java开发者提供清晰的技术干货。
24 1
|
1月前
|
安全
String、StringBuffer、StringBuilder的区别
String 由 char[] 数组构成,使用了 final 修饰,对 String 进行改变时每次都会新生成一个 String 对象,然后把指针指向新的引用对象。 StringBuffer可变并且线程安全;有一定缓冲区容量,字符串大小没超过容量,不会重新分配新的容量,适合多线程操作字符串; StringBuiler可变并且线程不安全。速度比StringBuffer更快,适合单线程操作字符串。 操作少量字符数据用 String;单线程操作大量数据用 StringBuilder;多线程操作大量数据用 StringBuffer
|
1月前
|
小程序 Java 程序员
JDK 和 JRE 有什么区别
JDK(Java Development Kit)是Java开发工具包,包含编译器、调试器等开发工具,用于开发Java程序。JRE(Java Runtime Environment)是Java运行环境,包含Java虚拟机和类库,用于运行Java程序。简言之,JDK用于编写Java程序,JRE用于运行这些程序。
44 1
|
1月前
|
存储 缓存 网络协议
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点,GET、POST的区别,Cookie与Session
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点、状态码、报文格式,GET、POST的区别,DNS的解析过程、数字证书、Cookie与Session,对称加密和非对称加密
|
2月前
|
缓存 Java Maven
java: 警告: 源发行版 11 需要目标发行版 11 无效的目标发行版: 11 jdk版本不符,项目jdk版本为其他版本
如何解决Java项目中因JDK版本不匹配导致的编译错误,包括修改`pom.xml`文件、调整项目结构、设置Maven和JDK版本,以及清理缓存和重启IDEA。
61 1
java: 警告: 源发行版 11 需要目标发行版 11 无效的目标发行版: 11 jdk版本不符,项目jdk版本为其他版本
|
2月前
|
Java 关系型数据库 MySQL
【编程基础知识】Eclipse连接MySQL 8.0时的JDK版本和驱动问题全解析
本文详细解析了在使用Eclipse连接MySQL 8.0时常见的JDK版本不兼容、驱动类错误和时区设置问题,并提供了清晰的解决方案。通过正确配置JDK版本、选择合适的驱动类和设置时区,确保Java应用能够顺利连接MySQL 8.0。
255 1
|
2月前
|
Java
Java基础之 JDK8 HashMap 源码分析(中间写出与JDK7的区别)
这篇文章详细分析了Java中HashMap的源码,包括JDK8与JDK7的区别、构造函数、put和get方法的实现,以及位运算法的应用,并讨论了JDK8中的优化,如链表转红黑树的阈值和扩容机制。
35 1
|
2月前
|
编译器
经典面试题:变量的声明和定义有什么区别
在编程领域,变量的“声明”与“定义”是经典面试题之一。声明告诉编译器一个变量的存在,但不分配内存,通常包含变量类型和名称;而定义则为变量分配内存空间,一个变量必须至少被定义一次。简而言之,声明是告知变量形式,定义则是实际创建变量并准备使用。
下一篇
DataWorks