我知道JVM使用常量池和字符串常量池来处理字符串字面量。但我不知道JVM使用哪种类型的内存来存储字符串常量字面量。是栈还是堆?由于字符串常量字面量与任何实例都没有关联,因此我会认为它将被存储在栈中。但是,如果字面量没有被任何实例引用,它必须通过GC运行进行回收(如果我错了,请纠正我),那么如果它存储在栈中,该如何处理?
我知道JVM使用常量池和字符串常量池来处理字符串字面量。但我不知道JVM使用哪种类型的内存来存储字符串常量字面量。是栈还是堆?由于字符串常量字面量与任何实例都没有关联,因此我会认为它将被存储在栈中。但是,如果字面量没有被任何实例引用,它必须通过GC运行进行回收(如果我错了,请纠正我),那么如果它存储在栈中,该如何处理?
正如这个答案所解释的那样,在JVM中字符串池的确切位置未指定,并且可能因一种JVM实现而异。
有趣的是,直到Java 7,热点JVM上的池在堆的永久代空间中,但是自从Java 7以来,它已经被移动到堆的主要部分中:
区域: HotSpot
摘要: 在JDK 7中,interned字符串不再在Java堆的永久代中分配,而是在Java堆的主要部分中分配(称为年轻代和老年代),与应用程序创建的其他对象一起。这种变化将导致更多的数据驻留在主Java堆中,较少的数据驻留在永久代中,因此可能需要调整堆大小。由于这个变化,大多数应用程序的堆使用量只会有相对较小的差别,但是加载许多类或大量使用String.intern()方法的较大应用程序将看到更显著的差别。 RFE: 6962931
在Java 8 Hotspot中,永久代已经完全被移除。
字符串常量并不存储在栈上。从来没在栈上存过。事实上,没有任何对象被存储在栈上。
字符串常量(或者更准确地说,代表它们的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开始,它存放在主堆中。
字符串池
字符串池(有时也称为字符串规范化)是一种将具有相同值但不同标识的多个字符串对象替换为单个共享字符串对象的过程。您可以通过保留自己的Map<String, String>(根据您的需求可能包含软引用或弱引用)并使用映射值作为规范化值来实现此目标。或者,您可以使用JDK提供的String.intern()方法。
在Java 6时代,由于池化失控可能导致OutOfMemoryException,许多标准禁止使用String.intern()。Oracle Java 7对字符串池的实现进行了相当大的更改。您可以在https://bugs.java.com/bugdatabase/view_bug?bug_id=6962931和https://bugs.java.com/bugdatabase/view_bug?bug_id=6962930中了解详细信息。
Java 6中的String.intern()
在那些美好的日子里,所有的内部化字符串都存储在PermGen中,这是堆的固定大小部分,主要用于存储已加载的类和字符串池。除了显式内部化的字符串,PermGen字符串池还包含程序中先前使用过的所有字面字符串(这里的关键词是使用——如果一个类或方法从未被加载/调用,其中定义的任何常量都不会被加载)。正如其他答案所解释的那样,Java中的内存分为两部分:
1. 栈:每个线程都会创建一个栈,它存储帧,帧又存储本地变量。如果一个变量是引用类型,则该变量引用堆中实际对象的内存位置。
2. 堆:所有类型的对象都将在堆中创建。
堆内存再次分为3个部分:
1. 年轻代:存储寿命短暂的对象,年轻代本身又可以分为两个类别:伊甸园空间和幸存者空间。
2. 老年代:存储经过多次垃圾回收周期后仍被引用的对象。
3. 永久代:存储程序的元数据,例如运行时常量池。
字符串常量池属于堆内存的永久代区域。
我们可以使用javap -verbose class_name
命令查看代码字节码的运行时常量池,它将显示方法引用(#Methodref)、类对象(#Class)和字符串字面量(#String)等信息。
您可以在我的文章《JVM如何处理方法重载和覆盖》中了解更多信息。
在这里已经包含了很好的答案,但从我的角度来看还有一些缺失 - 一个插图。
正如您已经知道的那样,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);
字符串字面值 s1
和 s2
将进入字符串常量池,对象 obj1
、obj2
和 obj3
将进入堆内存。它们都将从栈中引用。
此外,请注意,“abc”将出现在堆内和字符串常量池中。为什么 String s1 = "abc"
和 String obj1 = new String("abc")
会以这种方式创建?这是因为 String obj1 = new String("abc")
明确地创建了一个新的并具有不同引用的字符串对象的实例,而如果字符串常量池中有可用的实例,则 String s1 = "abc"
可能会重用该实例。更详细的解释请参见:https://dev59.com/bXA75IYBdhLWcg3wbojM#3298542