资讯专栏INFORMATION COLUMN

在Java虚拟机中,字符串常量到底存放在哪

lewinlee / 1360人阅读

摘要:的三种常量池此外,有三种常量池,即字符串常量池又叫全局字符串池文件常量池运行时常量池。开始虚拟机把字符串常量池位置从永久代挪到堆,又彻底取消,把诸如之类的元数据都挪到堆之外管理。

前言

前阵子和朋友讨论一个问题:

字符串常量归常量池管理,那比如 String str = "abc"; 
"abc"这个对象是放在内存中的哪个位置,是字符串常量池中还是堆?

”这句代码的abc当然在常量池中,只有new String("abc")这个对象才在堆中创建“,他们大概是这么回答。

“abc”这个东西,是放在常量池中,这个答案是错误的。

字符串“abc"的本体、实例,应该是存在于Java堆中。

可能还真的有部分同学对这个知识点不熟悉,今天和大家聊聊字符串这个问题 ~

初学Java时,学到字符串这一部分,有一段代码

String str1 = "hello";
String str2 = new String("hello"); 
书上的解释是:执行第一行的时候,已经把"hello"字符串放到了常量池中,执行第二行代码时,会将常量池中已经存在的"hello"复制一份到堆内存中,创建一个的新的String对象。虽然值一样,但他们是不同的对象。

当时看完这个解释,我产生了很多疑惑。因为在此之前已经知道字符串的底层是char数组实现的。我很疑惑:

他copy一份过去,是copy了char数组呢?

还是copy整个String对象?

"hello" 这个对象实例真的存放在常量池中吗?

当时在网上搜了一些文章和答案,各有说辞,大部分回答都是 "str" 这个对象在常量池中,但也有认为字符串常量实例(或叫对象)是在堆中创建,只是将其引用放到字符串常量池中,交给常量池管理。

JAVA内存区域 — 运行时数据区

理清这个问题前,需要梳理一下前置知识。

从一个经典的示意图讲起,以hotspot虚拟机为例,此内存模型需建立在JDK1.7之前的版本来讨论,JDK1.7之后有所改变,但是原理还是一样的。

Java虚拟机管理的内存是运行时数据区那一部分,简单概括一下其中各个区域的区别:

虚拟机栈:线程私有,生命周期与线程相同,即每条线程都一个独立的栈(VM Stack)。每个方法执行时都会创建一个栈帧,也就是说,当有一条线程执行了多个方法时,就会有一个栈,栈中有多个栈帧。

本地方法栈:线程私有

程序计数器:线程私有

堆Heap:线程共享,是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。 (原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated) 但有特殊情况,随着JIT编译器的发展,逃逸分析和标量替换技术的逐渐成熟,对象也可以在栈上分配。另外,虽说堆是线程共享,但其中也可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)

方法区:线程共享,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

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";
}

回到一开始说到的这句代码,可以来总结一下它的执行过程了。

首先,字面量 "hello" 在编译期,就会被记录在 class文件的 class常量池中。

而当 class文件被加载到内存中后,JVM就会将 class常量池中的大部分内容存放到运行时常量池中,但是字符串 "hello" 的本体(对象)和其他所有对象一样,是会在堆中创建再将引用放到字符串常量池,也就是图一的 Interned Strings的位置。(RednaxelaFX 的文章里,测试结果是在新生代的Eden区。但因为一直有一个引用驻留在字符串常量池,所以不会被GC清理掉)

而到了String str = "hello" 这步,JVM会去字符串常量池中找,如果找到了,JVM会在栈中的局部变量表里创建str变量,然后把字符串常量池中的(hello 对象的)引用复制给 str 变量。

在《深入理解Java虚拟机》这本书中也有字符串相关的解释,举其中几个例子:

例子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");

str1、str2、str3 三个变量所指向的都是不同的对象。(str1 != str2 != str3)

那么,这三个对象里的char数组是否是同一个数组?相信大家都有答案了。

此文所讨论的Java内存模型是建立在JDK1.7之前。JDK 7开始 Hotspot 虚拟机把字符串常量池(Interned String位置)从永久代(PermGen)挪到Heap堆,JDK 8又彻底取消 PermGen,把诸如klass之类的元数据都挪到GC堆之外管理。但不管怎样,基本原理还是不变的,字面量 ”hello“ 等依旧不是放在 Interned String 中。

推荐文章:

请别再拿 “String s = new String("xyz"); 创建了多少个String实例” 来面试了吧

借HSDB来探索HotSpot VM的运行时数据

作者:RednaxelaFX,曾为《深入理解Java虚拟机》提推荐语

java用这样的方式生成字符串:String str = "Hello",到底有没有在堆中创建对象? - 胖君的回答 - 知乎

隆鹏

广州芦苇科技Java开发团队

芦苇科技-广州专业互联网软件服务公司

抓住每一处细节 ,创造每一个美好

关注我们的公众号,了解更多

想和我们一起奋斗吗?lagou搜索“ 芦苇科技 ”或者投放简历到 server@talkmoney.cn 加入我们吧

关注我们,你的评论和点赞对我们最大的支持

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73095.html

相关文章

  • JVM详解1.Java内存模型

    摘要:编译参见深入理解虚拟机节走进之一自己编译源码内存模型运行时数据区域根据虚拟机规范的规定,的内存包括以下几个运运行时数据区域程序计数器程序计数器是一块较小的内存空间,他可以看作是当前线程所执行的字节码的行号指示器。 点击进入我的博客 1.1 基础知识 1.1.1 一些基本概念 JDK(Java Development Kit):Java语言、Java虚拟机、Java API类库JRE(...

    TANKING 评论0 收藏0
  • 深度理解JVM-----运行时数据区域

    摘要:在之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。 以下内容部分转载于: CS-Notes showImg(http://ww1.sinaimg.cn/large/005NT19Ply1g385uooqv9j30kd0slmyw.jpg); 程序计数器(Program...

    tuantuan 评论0 收藏0
  • Java 内存区域详解

    摘要:三对象的内存布局对象在堆中的布局分为三个区域对象头,实例数据,对齐填充。总结了解内存区域是对的深入学习,以前只知道有堆和栈的区分,现在我们了解到了具体的堆栈的作用。 引言 学习Java也有一段时间了,总感觉有些东西学的不是很精通。例如Java内存区域到底是怎么样的?程序是怎么跑的?对象是怎么存放的?这些都影响了我对自己的程序运行的熟悉程度。 一. 运行时数据区域 showImg(/im...

    darry 评论0 收藏0
  • Java内存区域划分和内存分配

    摘要:运行时数据区域虚拟机在执行的过程中会把管理的内存划分为若干个不同的数据区域。方法区的内存收集还是会出现,不过这个区域的内存收集主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时将抛出异常。 运行时数据区域Java虚拟机在执行Java的过程中会把管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,而有的区...

    BDEEFE 评论0 收藏0
  • jvm基础篇一之内存区域

    摘要:堆区堆是虚拟机所管理的内存中最大的一块,它是被所有线程共享的一块内存区域,该区域在虚拟机启动的时候创建。 运行时数据区域    想要了解jvm,那对其内存分配管理的学习是必不可少的;java虚拟机在执行java程序的时候会把它所管理的内存划分成若干数据区域。这些区域有着不同的功能、用途、创建/销毁时间。java虚拟机所分配管理的内存区域如图1所示 程序计数器    程序计数器是一块比较...

    Zachary 评论0 收藏0

发表评论

0条评论

lewinlee

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<