Java的字符串常量池存储在哪里,堆还是栈?

110

我知道JVM使用常量池和字符串常量池来处理字符串字面量。但我不知道JVM使用哪种类型的内存来存储字符串常量字面量。是栈还是堆?由于字符串常量字面量与任何实例都没有关联,因此我会认为它将被存储在栈中。但是,如果字面量没有被任何实例引用,它必须通过GC运行进行回收(如果我错了,请纠正我),那么如果它存储在栈中,该如何处理?


12
如何将池存储在堆栈上?你知道堆栈的概念吗? - The Scrum Meister
1
嗨 Scrum Meister,我试图意味着它不可能。对于错误的惯例感到抱歉。关于GC,刚刚我了解到。谢谢。 - Rengasami Ramanujam
6个回答

78
答案从技术上讲都不是。根据Java虚拟机规范,存储字符串字面量的区域在运行时常量池中。运行时常量池内存区域是按类或接口分配的,因此它与任何对象实例都没有关系。运行时常量池是方法区的子集,方法区“存储每个类的结构,例如运行时常量池、字段和方法数据以及方法和构造函数的代码,包括在类和实例初始化中使用的特殊方法和接口类型初始化”。虚拟机规范说,尽管方法区在逻辑上是堆的一部分,但它并不指定在方法区中分配的内存受到垃圾回收或与分配给堆的正常数据结构相关的其他行为的约束。

10
实际上,当类被加载到虚拟机中时,字符串常量将被复制到堆中的虚拟机全局字符串池(如 Stephen C 所说,在永久代中),因为不同类中相等的字符串字面值必须是相同的 String 对象(按照 JLS 规定)。 - Paŭlo Ebermann
1
谢谢大家的回答。通过这次讨论,我学到了很多东西。很高兴认识你们 :) - Rengasami Ramanujam
5
这句话的意思是,对于Sun的虚拟机来说是正确的,但并不一定适用于JVM的所有实现。JVM规范提到,尽管运行时常量池和方法区在逻辑上属于堆,但它们的行为不一定相同。这只是一个轻微的语义差异 :) - Duane Moore
https://dev59.com/14fca4cB1Zd3GeqPfS1I - Kanagavelu Sugumar

59

正如这个答案所解释的那样,在JVM中字符串池的确切位置未指定,并且可能因一种JVM实现而异。

有趣的是,直到Java 7,热点JVM上的池在堆的永久代空间中,但是自从Java 7以来,它已经被移动到堆的主要部分中

区域: HotSpot
摘要: 在JDK 7中,interned字符串不再在Java堆的永久代中分配,而是在Java堆的主要部分中分配(称为年轻代和老年代),与应用程序创建的其他对象一起。这种变化将导致更多的数据驻留在主Java堆中,较少的数据驻留在永久代中,因此可能需要调整堆大小。由于这个变化,大多数应用程序的堆使用量只会有相对较小的差别,但是加载许多类或大量使用String.intern()方法的较大应用程序将看到更显著的差别。 RFE: 6962931

在Java 8 Hotspot中,永久代已经完全被移除。


31

字符串常量并不存储在栈上。从来没在栈上存过。事实上,没有任何对象被存储在栈上。

字符串常量(或者更准确地说,代表它们的String对象)历史上被存储在一个名为"permgen"堆的堆中。("Permgen"是Permanent Generation的缩写。)

在正常情况下,字符串常量和permgen堆中的其他大部分内容都是“永久”可达的,并且不会被垃圾收集。(例如,字符串常量始终可以从使用它们的代码对象中访问。)但是,您可以配置JVM以尝试查找并收集不再需要的动态加载类,这可能会导致字符串常量被垃圾收集。

澄清 #1 - 我并不是说Permgen不会被垃圾收集。它会被收集,通常发生在JVM决定运行Full GC时。我的观点是只要使用字符串常量的代码是可达的,字符串常量就将是可达的,而只要代码的类加载器是可达的,对于默认的类加载器而言,这意味着“永远”。

澄清 #2 - 实际上,Java 7及更高版本使用常规堆来保存字符串池。因此,代表字符串常量和intern'd字符串的String对象实际上在常规堆中。(有关详细信息,请参见@assylias的答案。)


但我仍在努力找出字符串字面值和使用new创建的字符串之间的区别。

没有什么“区别”。它真的非常简单:

  • 代表/对应于字符串字面值的String对象存储在字符串池中。
  • String::intern调用创建的String对象存储在字符串池中。
  • 所有其他String对象都不存储在字符串池中。

还有一个单独的问题,那就是字符串池存放的位置。在Java 7之前,字符串池存放在永久代堆中。从Java 7开始,它存放在主堆中。


23

字符串池

字符串池(有时也称为字符串规范化)是一种将具有相同值但不同标识的多个字符串对象替换为单个共享字符串对象的过程。您可以通过保留自己的Map<String, String>(根据您的需求可能包含软引用或弱引用)并使用映射值作为规范化值来实现此目标。或者,您可以使用JDK提供的String.intern()方法。

在Java 6时代,由于池化失控可能导致OutOfMemoryException,许多标准禁止使用String.intern()。Oracle Java 7对字符串池的实现进行了相当大的更改。您可以在https://bugs.java.com/bugdatabase/view_bug?bug_id=6962931https://bugs.java.com/bugdatabase/view_bug?bug_id=6962930中了解详细信息。

Java 6中的String.intern()

在那些美好的日子里,所有的内部化字符串都存储在PermGen中,这是堆的固定大小部分,主要用于存储已加载的类和字符串池。除了显式内部化的字符串,PermGen字符串池还包含程序中先前使用过的所有字面字符串(这里的关键词是使用——如果一个类或方法从未被加载/调用,其中定义的任何常量都不会被加载)。
Java 6中这种字符串池的最大问题是它的位置——PermGen。PermGen具有固定的大小,在运行时无法扩展。您可以使用-XX:MaxPermSize=96m选项来设置它。据我所知,默认的PermGen大小在32M和96M之间,取决于平台。您可以增加其大小,但其大小仍然是固定的。这种限制要求非常谨慎地使用String.intern——最好不要使用此方法来内部化任何不受控制的用户输入。这就是为什么在Java 6时代,字符串池大多是在手动管理的映射中实现的。
Java 7中的String.intern()
Oracle工程师在Java 7中对字符串池逻辑进行了非常重要的更改 - 字符串池被重新定位到堆中。这意味着您不再受到独立固定大小内存区域的限制。所有字符串现在都位于堆中,就像大多数其他普通对象一样,这使您只需管理堆大小即可调整应用程序。从技术上讲,这一点就足以重新考虑在Java 7程序中使用String.intern()的原因。但还有其他原因。
字符串池的值会被垃圾回收。
是的,如果在您的程序根中没有对JVM字符串池中的所有字符串的引用,它们都有资格进行垃圾回收。这适用于所有讨论过的Java版本。这意味着,如果您的字符串在作用域之外,并且没有其他对它的引用,它将从JVM字符串池中进行垃圾回收。
作为有资格进行垃圾回收并驻留在堆中的JVM字符串池,似乎是所有字符串的正确位置,不是吗?从理论上讲,这是正确的-未使用的字符串将从池中进行垃圾回收,使用的字符串将允许您在从输入中获取相同字符串时节省内存。看起来是一种完美的内存节省策略?几乎如此。在做出任何决策之前,您必须了解字符串池的实现方式。

11

正如其他答案所解释的那样,Java中的内存分为两部分:

1. 栈:每个线程都会创建一个栈,它存储帧,帧又存储本地变量。如果一个变量是引用类型,则该变量引用堆中实际对象的内存位置。

2. 堆:所有类型的对象都将在堆中创建。

堆内存再次分为3个部分:

1. 年轻代:存储寿命短暂的对象,年轻代本身又可以分为两个类别:伊甸园空间幸存者空间

2. 老年代:存储经过多次垃圾回收周期后仍被引用的对象。

3. 永久代:存储程序的元数据,例如运行时常量池。

字符串常量池属于堆内存的永久代区域。

我们可以使用javap -verbose class_name命令查看代码字节码的运行时常量池,它将显示方法引用(#Methodref)、类对象(#Class)和字符串字面量(#String)等信息。

runtime-constant-pool

您可以在我的文章《JVM如何处理方法重载和覆盖》中了解更多信息。


请公开任何关联,不要通过发布内容的方式来推广您的网站。请参阅如何撰写好答案? - user3956566

10

在这里已经包含了很好的答案,但从我的角度来看还有一些缺失 - 一个插图。

正如您已经知道的那样,JVM将分配给Java程序的内存分为两部分。 堆栈(stack)用于执行目的,堆(heap)用于存储目的。 在堆内存中,JVM为字符串字面量分配了一些内存。 堆内存的这一部分称为字符串常量池

例如,如果您初始化以下对象:

String s1 = "abc"; 
String s2 = "123";
String obj1 = new String("abc");
String obj2 = new String("def");
String obj3 = new String("456);

字符串字面值 s1s2 将进入字符串常量池,对象 obj1obj2obj3 将进入堆内存。它们都将从栈中引用。

此外,请注意,“abc”将出现在堆内和字符串常量池中。为什么 String s1 = "abc"String obj1 = new String("abc") 会以这种方式创建?这是因为 String obj1 = new String("abc") 明确地创建了一个新的并具有不同引用的字符串对象的实例,而如果字符串常量池中有可用的实例,则 String s1 = "abc" 可能会重用该实例。更详细的解释请参见:https://dev59.com/bXA75IYBdhLWcg3wbojM#3298542

enter image description here


在给定的图表中,“def”和“456”文字将存在于哪里?它们将如何被引用? - Satyendra
感谢您的评论@Satyendra,我已经更新了插图和答案。 - Johnny
@Stas为什么又创建了一个String对象“abc”..它应该使用引用obj1指向字面值,对吧? - user6091735
这是因为String obj1 = new String("abc") 显式创建了一个新的,引用上不同的String对象实例,而String s1 = "abc" 可能会重用字符串常量池中可用的实例。欲获得更详细的解释,请访问:https://dev59.com/bXA75IYBdhLWcg3wbojM#3298542 - Johnny

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接