前阵子和朋友讨论一个问题:java
字符串常量归常量池管理,那好比 String str = "abc"; "abc"这个对象是放在内存中的哪一个位置,是字符串常量池中仍是堆?
”这句代码的abc固然在常量池中,只有new String("abc")这个对象才在堆中建立“,他们大概是这么回答。面试
“abc”这个东西,是放在常量池中,这个答案是错误的。数组
字符串“abc"的本体、实例,应该是存在于Java堆中。app
可能还真的有部分同窗对这个知识点不熟悉,今天和你们聊聊字符串这个问题 ~测试
初学Java时,学到字符串这一部分,有一段代码ui
String str1 = "hello"; String str2 = new String("hello");
书上的解释是:执行第一行的时候,已经把"hello"字符串放到了常量池中,执行第二行代码时,会将常量池中已经存在的"hello"复制一份到堆内存中,建立一个的新的String对象。虽然值同样,但他们是不一样的对象。
当时看完这个解释,我产生了不少疑惑。由于在此以前已经知道字符串的底层是char数组实现的。我很疑惑:spa
当时在网上搜了一些文章和答案,各有说辞,大部分回答都是 "str" 这个对象在常量池中,但也有认为字符串常量实例(或叫对象)是在堆中建立,只是将其引用放到字符串常量池中,交给常量池管理。线程
理清这个问题前,须要梳理一下前置知识。code
从一个经典的示意图讲起,以hotspot虚拟机为例,此内存模型需创建在JDK1.7以前的版原本讨论,JDK1.7以后有所改变,可是原理仍是同样的。server
Java虚拟机管理的内存是运行时数据区那一部分,简单归纳一下其中各个区域的区别:
此外,Java有三种常量池,即字符串常量池(又叫全局字符串池)、class文件常量池、运行时常量池。
(图一)
1. 字符串常量池(也叫全局字符串池、string pool、string literal pool)
字符串常量池在每一个VM中只有一份,他在内存中的位置如图,红色箭头所指向的区域 Interned Strings
2. 运行时常量池(runtime constant pool)
当程序运行到某个类时,class文件中的信息就会被解析到内存的方法区里的运行时常量池中。看图可清晰感知到每个类被加载进来都会产生一个运行时常量池,由此可知,每一个类都有一个运行时常量池。它在内存中的位置如图,蓝色箭头所指向的区域,方法区中的Class Date中的运行时常量池(Run-Time Constant Pool)
(图二)
3. class文件常量池(class constant pool)
class常量池是在编译后每一个class文件都有的,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是 常量池(constant pool table),用于存放编译器生成的各类字面量(Literal)和符号引用(Symbolic References)。字面量就是咱们所说的常量概念,如文本字符串、被声明为final的常量值等。他在class文件中的位置如上图所示,Constant Pool 中。
public static void main(String[] args) { String str = "hello"; }
回到一开始说到的这句代码,能够来总结一下它的执行过程了。
例子1
(原文)运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各类字面量和符号引用, 这部份内容将在类加载后进入方法区的运行时常量池中存放。
最后一句描述不太准确,编译期生成的各类字面量并非所有进入方法区的运行时常量池中。字符串字面量就不进入运行时常量池,而是在堆中建立了对象,再将引用驻留到字符串常量池中。
例子2
代码清单2-7 String.intern()返回引用的测试 public class RuntimeConstantPoolOOM{ public static void main(String[]args){ String str1=new StringBuilder("计算机").append("软件").toString(); System.out.println(str1.intern()==str1); String str2=new StringBuilder("ja").append("va").toString(); System.out.println(str2.intern()==str2); } }
(原文)这段代码在JDK 1.6中运行,会获得两个false,而在JDK 1.7中运行,会获得一个true和一个false。产生差别的缘由是:在JDK 1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用,而由StringBuilder建立的字符串实例在Java堆上,因此必然不是同一个引用,将返回false。而JDK 1.7(以及部分其余虚拟机,例如JRockit)的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,所以intern()返回的引用和由StringBuilder建立的那个字符串实例是同一个。对str2比较返回false是由于 “java” 这个字符串在执行StringBuilder.toString()以前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,所以返回true。
原文解释也不太准确,我以为在 JDK 1.6中,intern()并不会把首次遇到的字符串实例复制到永久代中,而是会将实例再复制一份到堆(heap)中,而后将其引用放入字符串常量池中进行管理,因此此代码返回false。而JDK1.7中的intern()不会再复制实例,直接将首次遇到的此字符串实例的引用,放入字符串常量池,因而返回true。关于此观点,还没看到大神文章实锤,欢迎讨论。
最后再延伸一点,你们都知道,字符串的value是final修饰的char数组,那么如下这段代码:
// private final char value[]; String str1 = "hello world"; String str2 = new String("hello world"); String str3 = new String("hello world");
str一、str二、str3 三个变量所指向的都是不一样的对象。(str1 != str2 != str3)
那么,这三个对象里的char数组是不是同一个数组?相信你们都有答案了。
此文所讨论的Java内存模型是创建在JDK1.7以前。JDK 7开始 Hotspot 虚拟机把字符串常量池(Interned String位置)从永久代(PermGen)挪到Heap堆,JDK 8又完全取消 PermGen,把诸如klass之类的元数据都挪到GC堆以外管理。但无论怎样,基本原理仍是不变的,字面量 ”hello“ 等依旧不是放在 Interned String 中。
推荐文章:
做者:RednaxelaFX,曾为《深刻理解Java虚拟机》提推荐语
隆鹏
广州芦苇科技Java开发团队
芦苇科技-广州专业互联网软件服务公司
抓住每一处细节 ,创造每个美好
关注咱们的公众号,了解更多
想和咱们一块儿奋斗吗?lagou搜索“ 芦苇科技 ”或者投放简历到 server@talkmoney.cn 加入咱们吧
关注咱们,你的评论和点赞对咱们最大的支持