ToB企服应用市场:ToB评测及商务社交产业平台

标题: 面试官:String.intern() 有什么用?和常量池有什么关系?问倒一大片! [打印本页]

作者: 饭宝    时间: 2023-8-30 13:18
标题: 面试官:String.intern() 有什么用?和常量池有什么关系?问倒一大片!
作者:GuoMell
来源:blog.csdn.net/gcoder_/article/details/106644312
0. Background

在 JAVA 语言中有8中基本类型和一种比较特殊的类型String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。常量池就类似一个JAVA系统级别提供的缓存。
8种基本类型的常量池都是系统协调的,String类型的常量池比较特殊。
它的主要使用方法有两种:
推荐一个开源免费的 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语言层面的常量概念
符号引用:符号引用主要设涉及编译原理方面的概念
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的长度可以通过参数指定。
字符串常量池设计思想:
2. String.intern()与字符串常量池
  1. /**
  2. * Returns a canonical representation for the string object.
  3. * <p>
  4. * A pool of strings, initially empty, is maintained privately by the
  5. * class String.
  6. * <p>
  7. * When the intern method is invoked, if the pool already contains a
  8. * string equal to this String object as determined by
  9. * the {@link #equals(Object)} method, then the string from the pool is
  10. * returned. Otherwise, this String object is added to the
  11. * pool and a reference to this String object is returned.
  12. * <p>
  13. * It follows that for any two strings s and t,
  14. * s.intern() == t.intern() is true
  15. * if and only if s.equals(t) is true.
  16. * <p>
  17. * All literal strings and string-valued constant expressions are
  18. * interned. String literals are defined in section 3.10.5 of the
  19. * <cite>The Java™ Language Specification</cite>.
  20. *
  21. * @return  a string that has the same contents as this string, but is
  22. *          guaranteed to be from a pool of unique strings.
  23. */
  24. public native String intern();
复制代码
字符串常量池的位置也是随着jdk版本的不同而位置不同。在jdk6中,常量池的位置在永久代(方法区)中,此时常量池中存储的是对象。在jdk7中,常量池的位置在堆中,此时,常量池存储的就是引用了。
在jdk8中,永久代(方法区)被元空间取代了。这里就引出了一个很常见很经典的问题,看下面这段代码。
  1. @Test
  2. public void test(){
  3.     String s = new String("2");
  4.     s.intern();
  5.     String s2 = "2";
  6.     System.out.println(s == s2);
  7.     String s3 = new String("3") + new String("3");
  8.     s3.intern();
  9.     String s4 = "33";
  10.     System.out.println(s3 == s4);
  11. }
  12. //jdk6
  13. //false
  14. //false
  15. //jdk7
  16. //false
  17. //true
复制代码
这段代码在jdk6中输出是false false,但是在jdk7中输出的是false true。我们通过图来一行行解释。
JDK1.6



JDK1.7



3. String.intern()的应用

在大量字符串读取赋值的情况下,使用String.intern()会大大的节省内存空间。
  1. static final int MAX = 1000 * 10000;
  2. static final String[] arr = new String[MAX];
  3. public static void main(String[] args) throws Exception {
  4.     Integer[] DB_DATA = new Integer[10];
  5.     Random random = new Random(10 * 10000);
  6.     for (int i = 0; i < DB_DATA.length; i++) {
  7.         DB_DATA[i] = random.nextInt();
  8.     }
  9. long t = System.currentTimeMillis();
  10.     for (int i = 0; i < MAX; i++) {
  11.         //arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));
  12.          arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
  13.     }
  14. System.out.println((System.currentTimeMillis() - t) + "ms");
  15.     System.gc();
  16. }
复制代码
运行的参数是:-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开发手册(嵩山版)》最新发布,速速下载!
觉得不错,别忘了随手点赞+转发哦!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4