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

@Testpublic 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开发手册(嵩山版)》最新公布,速速下载!

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