本文默认基于64位的JDK 8(其虚拟机实现是HotSpot),除非特别说明。
Java堆内有:Java对象、字符串常量池、包装器类常量池等。
一、Java对象
参见《Java对象》。
二、字符串常量池
2.1、概念
字符串常量池,英文名称为“String Literal Pool”或者“String Constant Pool”,几点基本说明:
- 处于Java堆中,是为了复用String实例对象(在Java语言中,String实例对象不可修改,因此,既能节省内存又不至于引入并发修改等问题)而设计的一种机制所关联的内存区域
- 如上所述,“字符串常量池”是一种机制,该机制后续可知本质通过几个Java对象实现,契合“Java对象在Java堆内”
- 在设计之初,“字符串常量池”逻辑上属于“运行时常量池[1]”,而且他们都放置在方法区,比如在JDK 6中;但是现在前者和后者分别放置于Java堆和方法区,虽然“逻辑上属于”仍然成立,但已经名存实亡,意义不大
“字符串常量池”的实现原理如下:
- 维护一个String实例对象池P,再维护一个映射对象stringTable,可类比为
Map<Integer,List<String>>
,其“键”为“String实例对象的hashCode
值”,其“键值”为“P内具有对应hashCode
值的String实例对象的引用值集合”,图示见图1 - 与“字符串常量池”交互的唯一途径是通过String类的
intern()
本地方法(实现原理示意见以下源代码1)[2]:- 只有String实例对象才能调用String类的
intern()
本地方法,因此,使用“字符串常量池”机制,虽然最终可以复用String实例对象,但是中间过程中生成“临时的String实例对象”在所难免,这些“临时的String实例对象”最后由于“引用不可达”而被垃圾回收 - 对每一个字符串字面量(如“hello”,“world”等以直接形式给出的字符串),JVM会创建一个String实例对象,然后自动隐式调用其上的
intern()
方法
- 只有String实例对象才能调用String类的
图1
源代码1:
1 | public class String { |
2.2、实验代码
实验代码如下:
1 | package com.dslztx; |
以上代码执行结果如下:
1 | true |
三、包装器类常量池
3.1、概念
包装器类(包括“Boolean,Byte,Character,Short,Integer,Long”,不包括“Float,Double”)常量池,几点基本说明:
- 处于Java堆中,是为了复用包装器类实例对象(在Java语言中,包装器类实例对象不可修改,因此,既能节省内存又不至于引入并发修改等问题)而设计的一种机制所关联的内存区域
- 如上所述,包装器类常量池是一种机制,该机制后续可知本质通过几个Java对象实现,契合“Java对象在Java堆内”
“包装器类常量池”的实现原理如下:
- 维护一个包装器类实例对象池P,再维护一个映射对象,其“键”为“基本类型值”,其“键值”为“基本类型值对应的包装器类实例对象引用值”
- 与“包装器类常量池”交互的唯一途径是通过包装器类的
valueOf(boolean/byte/char/short/int/long)
方法(以Integer为例进行说明,实现原理示意见以下源代码2):- 基本类型(boolean,byte,char,short,int,long)的字面量值在转换为对应的包装器类实例时,会自动隐式调用相应包装器类的
valueOf(boolean/byte/char/short/int/long)
方法
- 基本类型(boolean,byte,char,short,int,long)的字面量值在转换为对应的包装器类实例时,会自动隐式调用相应包装器类的
源代码2:
1 | import java.util.Properties; |
3.2、实验代码
1 | package com.dslztx; |
执行结果如下:
1 | true |
四、其他
4.1、JDK 6中的字符串常量池机制和实现
JDK 6中的“字符串常量池机制和实现”跟JDK 8中的“字符串常量池机制和实现”相比有很大不同:
- 在JDK 8的“字符串常量池机制和实现”中,“字符串常量池”内存区域处于Java堆中;而在JDK 6的“字符串常量池机制和实现”中,“字符串常量池”内存区域处于方法区中,图示见图2
- 针对
intern()
方法的实现逻辑,两者的区别:- 在JDK 8中,如果“字符串常量池”中不存在一个String实例对象与当前String实例对象A的内容相同,那么将A的引用值Aa加入到“字符串常量池”中的stringTable(相当于A被加入到“字符串常量池”),返回Aa;否则返回“字符串常量池”已存在String实例对象的引用值
- 在JDK 6中,如果“字符串常量池”中不存在一个String实例对象与当前String实例对象A的内容相同,那么在“字符串常量池”中复制新建(因为分处于“Java堆”和“方法区”)一个String实例对象B,B的内容与A相同,将B的引用值Bb加入到“字符串常量值”中的stringTable,返回Bb;否则返回“字符串常量池”已存在String实例对象的引用值
图2
4.1.1、证明实验1
有如下一段代码,分别使用JDK 6和JDK 8执行:
1 | public class RuntimeConstantPoolExp { |
1、使用JDK 6执行
执行结果如下所示:
1 | false |
在第一个判断中,由于“str1”指向Java堆中的String实例对象,而“str1.intern()”指向“字符串常量池”中的String实例对象,故而判断结果为“false”。
在第二个判断中,由于“str2”指向Java堆中的String实例对象,而“str2.intern()”指向“字符串常量池”中的String实例对象,故而判断结果也为“false”。
2、使用JDK 8执行
执行结果如下所示:
1 | true |
在第一个判断中,由于“str1”和“str1.intern()”都指向Java堆中的同一个String实例对象,故而判断结果为“true”。
在第二个判断中,由于“str2”指向Java堆中的String实例A,而“str2.intern()”指向Java堆中的另外一个String实例对象B,B指向的String实例对象在“字符串常量池”中。A和B不同,因为“java”字符串字面量已在之前加载“sun.misc.Version”类时被加载——private static final String launcher_name = "java";
,B指向彼时生成的String实例对象,故而判断结果为“false”。
4.1.2、证明实验2
另外有如下一段代码,分别使用JDK 6和JDK 8执行:
1 | import java.util.ArrayList; |
1、使用JDK 6执行
执行命令为:
1 | javac RuntimeConstantPoolOOM.java |
执行结果如下所示:
1 | java.lang.OutOfMemoryError: PermGen space |
实验中方法区的内存使用量被设为固定的10M,而“字符串常量池”内存区域属于“方法区”,因此“字符串常量池”内存区域能够使用的内存上限也为10M。因此,当通过String.valueOf(i++).intern()
方法不断消耗“字符串常量池”的内存时,最终导致出现“OutOfMemoryError”异常。另外在异常信息中,还提示“PermGen space”,这是因为在JDK 6的HotSpot虚拟机中,使用“永久代”来实现方法区。
2、使用JDK 8执行
执行命令为:
1 | javac RuntimeConstantPoolOOM.java |
执行结果如下所示:
1 | java.lang.OutOfMemoryError: GC overhead limit exceeded |
实验中Java堆的内存使用量被设为固定的50M,而“字符串常量池”内存区域属于“Java堆”,因此“字符串常量池”内存区域能够使用的内存上限也为50M。因此,当通过String.valueOf(i++).intern()
方法不断消耗“字符串常量池”的内存时,最终导致出现“OutOfMemoryError”异常。
需要注意的是,这里抛出的是“java.lang.OutOfMemoryError: GC overhead limit exceeded”异常,而不是预期的“java.lang.OutOfMemoryError: Java heap space”异常,它们的本质都是“Java堆内存不足”,只不过是两种表现,抛出以上哪种异常并不能被准确预测。[3]
4.1.3、证明实验3
现有如下一段代码,分别使用JDK 6和JDK 8执行:
1 | public class Main { |
1、使用JDK 6执行
执行命令为:
1 | javac Main.java |
执行结果如下所示:
1 | false |
解析说明见下面代码中注释:
1 | /** |
2、使用JDK 8执行
执行命令为:
1 | javac Main.java |
执行结果如下所示:
1 | false |
解析说明见下面代码中注释:
1 | /** |
4.1.4、证明实验4
现有如下一段代码(它是“4.1.3、证明实验3”中代码的变种),分别使用JDK 6和JDK 8执行:
1 | public class Main { |
1、使用JDK 6执行
执行命令为:
1 | javac Main.java |
执行结果如下所示:
1 | false |
解析说明见下面代码中注释:
1 | /** |
2、使用JDK 8执行
执行命令为:
1 | javac Main.java |
执行结果如下所示:
1 | false |
解析说明见下面代码中注释:
1 | /** |
4.2、其他
- 本文的很多描述有很多瑕疵,比如“处理字符串字面量的时机是在加载类文件的过程中,而不是在执行相应代码语句的过程中(如有
String a=“hello world”
代码语句,处理字符串字面量“hello world”的时机是在加载代码语句所在类文件的过程中,而不是在执行该代码语句的过程中)”等。但是,经过分析和实验可以发现,这些瑕疵,对于我们介绍本文的核心内容并没有很大的负面影响 - 要想透彻完整了解很多概念,最好的方式还是看JDK的源代码
参考文献
[1]《方法区》
[2]java.lang.String
类中public native String intern()
方法的JavaDoc
[3]https://blog.csdn.net/renfufei/article/details/77585294
[4]http://openjdk.java.net/jeps/122
[5]http://java-performance.info/string-intern-in-java-6-7-8/
[6]http://tech.meituan.com/in_depth_understanding_string_intern.html
[7]http://blog.csdn.net/u010297957/article/details/50995869
[8]http://droidyue.com/blog/2014/12/21/string-literal-pool-in-java/index.html
[9]https://jimlife.wordpress.com/2007/08/10/java-constant-pool-string/
[10]http://www.javaranch.com/journal/200409/ScjpTipLine-StringsLiterally.html
[11]http://theopentutorials.com/tutorials/java/strings/string-literal-pool/
[12]http://www.thejavageek.com/2013/06/19/the-string-constant-pool/
[13]http://tangxman.github.io/2015/07/27/the-difference-of-java-string-pool/
[14]https://www.hollischuang.com/archives/6569