简介
本文介绍Java的String的intern方法的原理。
常量池简介
在 JAVA 语言中有8种基本类型和一种比较特殊的类型String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池(在方法区)的概念。常量池就类似一个JAVA系统级别提供的缓存。
8种基本类型的常量池都是系统协调的,String类型的常量池比较特殊。String的常量池的主要使用方法有两种:
- 直接使用双引号声明出来的String对象会直接存储在常量池中。
- 如果不是用双引号声明的String对象,可以使用String提供的intern方法将其放到常量池。
intern方法简介(JDK7之后)
原型
public native String intern();
说明:
- 从字符串常量池中查询当前字符串是否存在(通过equals判断)。
- 如果存在,返回常量池中的字符串引用。
- 如果不存在,把这个String对象引用存到常量池,然后返回这个String对象的引用。
返回值:都是返回String变量对应的常量池中字符串的引用。
以上内容可以从intern的注释中得到:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { // 其他代码 /** * Returns a canonical representation for the string object. * <p> * A pool of strings, initially empty, is maintained privately by the * class {@code String}. * <p> * When the intern method is invoked, if the pool already contains a * string equal to this {@code String} object as determined by * the {@link #equals(Object)} method, then the string from the pool is * returned. Otherwise, this {@code String} object is added to the * pool and a reference to this {@code String} object is returned. * <p> * It follows that for any two strings {@code s} and {@code t}, * {@code s.intern() == t.intern()} is {@code true} * if and only if {@code s.equals(t)} is {@code true}. * <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™ 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(); }
原理(JDK6与JDK7之后)
常量池里的字符串的由来
- JDK6及以前调用String.intern()
- 若常量池中有,则返回常量池中这个字符串的引用
- 若常量池中没有,则拷贝一份对象,放到常量池(永久代)中;返回值是常量池(永久代)中对应字符串实例的引用。
- JDK7及以后调用String.intern()
- 若常量池中有,则返回常量池中这个字符串的引用
- 若常量池中没有,则拷贝一份引用,放到常量池(堆)中;(JDK1.7将String常量池从Perm区移动到了Java Heap区)
实例分析
代码测试
例程1:
package com.example.a; public class Demo { public static void main(String argv[]) { String s1 = new String("1"); s1.intern(); String s2 = "1"; System.out.println(s1 == s2); String s3 = new String("1") + new String("1"); s3.intern(); String s4 = "11"; System.out.println(s3 == s4); } }
结果
jdk6:false false jdk7:false true jdk8:false true
例程2:
package com.example.a; public class Demo { public static void main(String argv[]) { String s1 = new String("1"); s1.intern(); String s2 = "1"; System.out.println(s1 == s2); String s3 = new String("1") + new String("1"); String s4 = "11"; s3.intern(); System.out.println(s3 == s4); } }
上述代码第二部分有个对调。
结果
jdk6:false false jdk7:false false jdk8:false false
例程分析
下边图中:绿色线条代表 String 对象的内容指向。 红色线条代表地址指向。
JDK1.6(例程1与例程2)
如上图所示。首先说一下 jdk6中的情况,在 jdk6中上述的所有打印都是 false 的,因为 jdk6中的常量池是放在 Perm 区中的,Perm区和正常的 JAVA Heap 区域是完全分开的。上面说过如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new出来的 String 对象是放在 JAVA Heap 区域。所以拿一个 JAVA Heap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用String.intern方法也是没有任何关系的。
JDK1.7(例程1的分析)
在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的Perm区的,Perm区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用 intern 会产生java.lang.OutOfMemoryError:PermGen space错误的。
在 jdk7 的版本中,字符串常量池已经从Perm区移到正常的Java Heap区域了。为什么要移动,Perm 区域太小是一个主要原因,当然据消息称jdk8已经直接取消了Perm区域,新建立了一个元区域。应该是jdk开发者认为Perm区域已经不适合现在 JAVA的发展了。字符串常量池移动到JAVA Heap区域,现在解释为什么会有上述的打印结果。
- String s1 = new String(“1”);
- 分析:这行代码生成了2个对象(常量池中的“1” 和 JavaHeap 中的字符串对象)。s.intern(); 这一句是 s1 对象去常量池中寻找后发现 “1” 已经在常量池里了。
- 此时s1指向的是Java Heap中的字符串对象。
- String s2 = “1”;
- 分析:这行代码生成一个 s2的引用指向常量池中的“1”对象。 结果就是 s1 和 s2 的引用地址不同。
- String s3 = new String(“1”) + new String(“1”);
- 分析:这行代码生成了2个对象(字符串常量池中的“1” 和 Java Heap中的 s3 引用指向的对象“11”(中间还有2个匿名的new String(“1”)我们不讨论它)。
- 此时s3 是Java Heap中的字符串对象的引用,对象内容是”11″,此时常量池中是没有 “11”对象的。
- s3.intern();
- 分析:这行代码将 s3中的”11″字符串放入String 常量池中,因为此时常量池中不存在”11″字符串,因此常规做法是跟 jdk6 图中表示的那样,在常量池中生成一个”11″的对象,关键点是 jdk7 中常量池不在Perm区域,而是在堆中了。常量池中不需再存储一份对象了,可以直接存储堆中的引用。这份引用指向s3引用的对象。 也就是说引用地址是相同的。
- 此时,s3是Java Heap中的字符串对象的引用,对象内容是”11″,此时常量池中是有 “11”对象,它保存的就是s3引用地址。
- String s4 = “11”;
- 这行代码”11″是显式声明的,因此会直接去常量池中创建,创建时发现已经有这个对象了。
- 此时:s4 == 常量池的“11”对象引用 == s3引用对象的引用
JDK1.7(例程2的分析)
- String s1 = new String(“1”);s1.intern();String s2 = “1”;
- 分析:s1.intern();,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码String s = new String(“1”);的时候已经生成“1”对象了。下边的s2声明都是直接从常量池中取地址引用的。 s1 和 s2 的引用地址是不会相等的。
- String s3 = new String(“1”) + new String(“1”);
- 分析:这行代码生成了2个对象(字符串常量池中的“1” 和 Java Heap中的 s3 引用指向的对象“11”(中间还有2个匿名的new String(“1”)我们不讨论它)。
- 此时s3 是Java Heap中的字符串对象的引用,对象内容是”11″,此时常量池中是没有 “11”对象的。
- String s4 = “11”;
- 分析:声明 s4 的时候常量池中是不存在“11”对象的,执行完后,s4是常量池里“11“对象的引用。
- s3.intern();
- 分析:此时常量池中“11”对象已经存在了,不会有任何操作,s3仍然是堆中String对象的引用。因此 s3 != s4
应用实例
package org.example.a; import java.util.Random; public class Demo { static final int MAX = 1000 * 10000; static final String[] arr = new String[MAX]; public static void main(String argv[]) { 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(); } }
上述代码是一个演示代码,其中有两条语句不一样,一条是使用 intern,一条是未使用 intern。
运行的参数是:-Xmx2g -Xms2g -Xmn1500M
不用intern
2160ms
使用intern
826ms
通过上述结果,我们发现不使用 intern 的代码生成了1000w 个字符串,占用了大约640m 空间。 使用了 intern 的代码生成了1345个字符串,占用总空间 133k 左右。其实通过观察程序中只是用到了10个字符串,所以准确计算后应该是正好相差100w 倍。虽然例子有些极端,但确实能准确反应出 intern 使用后产生的巨大空间节省。
细心的同学会发现没使用 intern 方法时间上也稍长。
请先
!