文章目录
- 【JVM系列】深入理解Java虚拟机(JVM)的核心技术 :从静态到运行时的秘密(二、Java常量池揭秘)
- 1. 基本概念
- 2. 常量池分类
- 2.1 运行常量池
- 2.2 静态常量池
- 2.3 字符串常量池
- 3. String 类
- 3.1 创建String对象的两种方式和区别
- 3.2 字符串的特性
- 3.3 StringBuffer 类
- 3.4 String intern方法
- 3.5 String延迟加载
- 3.6 如何证明字符串常量池是存放在堆中
- 3.6.1 JDK1.8字符串常量池存放在堆中
- 3.6.2 JDK1.6字符串常量池存放在方法区
- 3.6.3 为什么jdk7 String常量池放入到堆中
【JVM系列】深入理解Java虚拟机(JVM)的核心技术 :从静态到运行时的秘密(二、Java常量池揭秘)
JVM内存结构
1. 基本概念
常量池(Class文件常量池):.java
经过编译后生成的.class
文件,是class
文件的资源仓库。
常量池中主要存放:字面量(文本字符串,final常量)和符号引用(类和接口的全局定名,字段的名称和描述,方法的名称和描述)
常量池:通过一张表,虚拟机根据该常量表扎到执行的类名、方法名、参数类型、字面量。
2. 常量池分类
- 运行时常量池:类加载器读取
class
文件到内存中,该常量池就是运行时常量池; - 静态常量池:
java
编译为class
还没有被类加载器加载该class
文件; - 字符串常量池:
JDK7
之前是存放在方法区、JDK7
存放在堆中 、JDK 8
方法区改为元空间,字符串常量池还是存放在我们堆中。
2.1 运行常量池
运行时常量池是类加载器将描述类的数据读入到内存中,并且进行相应的解析之后,形成的数据结构。它是类或接口的直接组成部分,当一个类被加载到JVM中时,它的运行时常量池也就随之建立。运行时常量池包含了编译期间生成的各种字面量(literal)和符号引用(symbolic references),并且这些常量池中的元素在类被加载到内存之后,可以被JVM动态解析为直接引用。
特点:
- 动态性:运行时常量池中的内容并非固定不变,随着程序运行,可能会有新的常量加入到池中。例如,字符串字面量的
intern
方法就是一个典型的例子,它可以将新的字符串添加到常量池中。 - 共享性:运行时常量池中的内容可以被类或接口的所有实例以及该类或接口中的任何方法共享。
- 位置:在JDK 7及以后的版本中,字符串常量池被移到了堆中,而其他常量(如类和接口的符号引用)则位于元空间(Metaspace)。
用途:
- 运行时常量池主要用于存储类的各种常量信息,如字段名、方法名、字面量等。
- 字符串常量池(String Intern Pool)是运行时常量池的一部分,它允许开发者通过
String.intern()
方法获取字符串字面量的唯一引用。
示例:
public class Test01 {public static void main(String[] args) {String str1 = "HelloWorld";String str2 = "HelloWorld";System.out.println(str1 == str2); // 输出 true,因为两个字符串引用相同的常量池条目}
}
在这个例子中,str1
和str2
都引用了字符串常量池中的同一个条目,因此它们的引用是相等的。
2.2 静态常量池
静态常量池指的是.class
文件中的常量池部分,它是编译器生成的信息集合,包含了类的所有常量信息。在编译阶段,编译器会生成一个包含所有常量信息的常量池,并将其嵌入到.class
文件中。这个常量池的内容是固定的,不会随程序的运行而改变。
特点:
- 静态性:静态常量池的内容是在编译时确定的,并且在类加载之前就已经存在于
.class
文件中。 - 持久性:静态常量池的内容不会随着程序的运行而发生变化,它是类的一部分,存储在磁盘上的
.class
文件中。 - 位置:静态常量池存在于
.class
文件中,当类被加载到JVM时,这部分内容被解析并转化为运行时常量池。
用途:
- 静态常量池主要用于存储类的各种常量信息,如字段名、方法名、字面量等。
- 在编译阶段,编译器会检查所有的字面量和符号引用是否存在于常量池中,如果不存在,则会产生编译错误。
示例:
public class Test02 {public static final String CONSTANT_STRING = "HelloWorld";public static void main(String[] args) {System.out.println(CONSTANT_STRING);}
}
在这个例子中,CONSTANT_STRING
是一个静态常量,它被存储在.class
文件的常量池中。当编译器遇到这个常量时,它会确保这个字符串字面量被正确地记录在常量池中。
2.3 字符串常量池
JDK1.7
之前,运行时常量池(字符串常量池也在里边)是存放在方法区,此时方法区的实现是永久带;
JDK1.7
字符串常量池被单独从方法区移到堆中,运行时常量池剩下的还在永久带(方法区);
JDK1.8
,永久带更名为元空间(方法区的新的实现),但字符串常量池池还在堆中,运行时常量池在元空间(方法区)。
3. String 类
3.1 创建String对象的两种方式和区别
方式1:直接赋值 String s1 ="zhaoli";
从常量池査看是否有"zhaoli"
数据空间,如果有直接指向;如果没有则重新创建之后指向它;s1最终指向的是常量池的空间地址。
方式2:调用构造器 String s2 = new String(“zhaoli”);
先从堆中创建空间,里面维护了value
属性数组,指向常量池的"zhaoli"
空间,如果常量池没有"zhaoli"
,重新创建,如果有直接通过value
指向,最终指向的是堆中的空间地址。
3.2 字符串的特性
-
String
是一个final
类,代表不可变的字符序列 -
字符串是不可变的,一个字符串对象一旦被分配,其内容是不可变的。
public class StringTest {public static void main(String[] args) {String str = "zhaoli";str = "hello world";}
}
在字符常量池中会存在2个字符串值
Constant pool: #1 = Methodref #5.#21 // java/lang/Object."<init>":()V #2 = String #22 // zhaoli #3 = String #23 // hello world //...此处省略#22 = Utf8 zhaoli#23 = Utf8 hello world#24 = Utf8 com/zhaoli/jvm/test03/StringTest#25 = Utf8 java/lang/Object
{public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=1, locals=2, args_size=10: ldc #2 // String zhaoli2: astore_13: ldc #3 // String hello world5: astore_16: returnLineNumberTable:line 8: 0line 9: 3line 10: 6LocalVariableTable:Start Length Slot Name Signature0 7 0 args [Ljava/lang/String;3 4 1 str Ljava/lang/String;
}
注意看#2
、#3
、#22
、#23
,其实在堆空间是存在了两个字符串值的。
3.3 StringBuffer 类
java.lang.StringBuilder
代表可变的字符序列,可以对字符串内容进行增删,很多方法与String
相同,StringBuilder
是一个容器。StringBuilder
字串追加效率比String
要高。
在Java中,当你使用 +
操作符来连接字符串时,实际上会触发一个字符串连接操作。
String a = "zhaoli";
String b = "22";
String str = a + b;
这段代码首先定义了两个字符串变量 a
和 b
,然后将它们连接起来并赋值给 str
。底层的实现过程如下:
-
创建StringBuilder或StringBuffer对象:
Java虚拟机(JVM)内部会自动创建一个StringBuilder
对象(如果是多线程环境可能会使用StringBuffer
),这是因为字符串连接操作会通过StringBuilder
或StringBuffer
类来高效地处理字符串构建过程。 -
追加操作:
初始情况下,这个StringBuilder
对象是空的。然后,它会调用append
方法依次追加a
和b
的内容。当b
是一个String
类型时,可以直接追加;如果b
是其他类型(如上面例子中的Integer
字符串表示"22"),则需要先转换为字符串形式。 -
创建最终的String对象:
在所有需要追加的部分都被添加到StringBuilder
对象之后,会调用toString()
方法来创建一个新的String
对象。这个新创建的字符串就是a
和b
的连接结果,并且会被赋予给str
变量。
下面是一个更详细的伪代码描述:
StringBuilder sb = new StringBuilder();// 创建StringBuilder对象
sb.append(a);// 追加a的内容
sb.append(b);// 追加b的内容(这里假设b是String类型)
str = sb.toString();// 创建String对象,并赋值给str
请注意,如果你知道字符串连接操作会被频繁执行,并且连接操作涉及多个字符串或非字符串数据类型,那么最好显式地使用 StringBuilder
或 StringBuffer
类,以避免不必要的性能开销。例如:
StringBuilder sb = new StringBuilder();
sb.append(a);
sb.append(b);
String str = sb.toString();
这种方式可以更有效地管理内存资源,并提高程序的执行效率。
汇编代码如下
descriptor: ()V
flags: ACC_PUBLIC
Code:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 6: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcom/zhaoli/jvm/test03/Test01;13: aload_114: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;17: aload_218: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;24: astore_325: returnLineNumberTable:line 8: 0line 9: 3line 10: 6line 11: 25LocalVariableTable:Start Length Slot Name Signature0 26 0 args [Ljava/lang/String;3 23 1 a Ljava/lang/String;6 20 2 b Ljava/lang/String;25 1 3 str Ljava/lang/String;
3.4 String intern方法
在Java中,String
类的 intern
方法是一个用来优化字符串常量池使用的工具方法。它的主要作用是检查字符串是否已经存在于字符串常量池中。如果该字符串已经在池中,则返回池中已存在的字符串引用;否则,将该字符串添加到池中,并返回此字符串的引用。
使用场景:
- 当你需要确保字符串在内存中只有一个副本时,可以使用
intern
方法。 - 常用于确保某些字符串字面量在整个应用程序中保持唯一性,比如用于键的字符串。
实现原理:
当调用 String
对象的 intern
方法时,它会执行以下步骤:
- 检查字符串常量池中是否存在一个与当前字符串对象内容相等的对象。
- 如果存在这样的对象,就返回池中已有字符串的引用。
- 如果不存在,就将当前字符串对象添加到常量池中,并返回当前字符串对象的引用。
示例代码:
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1 == str2); // 输出 false,因为str1和str2指向不同的对象
str1 = str1.intern();
str2 = str2.intern();
System.out.println(str1 == str2); // 输出 true,因为现在str1和str2都指向常量池中的同一个对象
注意事项:
intern
方法的性能取决于JVM的实现以及字符串常量池的大小和配置。在某些情况下,频繁调用intern
方法可能会导致内存消耗增加。- 使用
intern
方法可以提高字符串比较的效率,因为在比较引用相等(==
)时,可以直接比较对象引用而不需要进行逐字符的比较。
弱引用特性:
从Java 7开始,字符串常量池中的字符串是通过弱引用 (WeakReference
) 来维护的。这意味着如果系统内存压力较大,垃圾回收器可能会回收这些字符串对象。因此,在内存紧张的情况下,调用 intern
方法的行为可能不如预期那样稳定。
总之,String
类的 intern
方法是一种优化手段,可以用来减少内存中重复字符串的存储次数,但是使用时需要考虑其潜在的性能影响和内存管理行为。
3.5 String延迟加载
字符串是懒加载的,当真正需要加载的字符串才会加载到内存中,如果已经在常量池中存在则直接复用该常量池内存地址。
将项目Debug
启动后勾选Memory
,每执行一行代码点击Load classes
,观察java.lang.String
对应的Count
值的变化。
3.6 如何证明字符串常量池是存放在堆中
JDK7
开始字符串常量池存放在堆中。
3.6.1 JDK1.8字符串常量池存放在堆中
public class StringDemo01 {// JDK1.8 设置 堆内存大小 -Xmx1m -Xms1m -XX:-UseGCOverheadLimitpublic static void main(String[] args) {ArrayList<String> arrayList = new ArrayList<>();int count = 0;try {for (int i = 0; i < 250000; i++) {arrayList.add((i + "").intern());count++;}} catch (Exception e) {e.printStackTrace();} finally {System.out.println("循环次数:" + count);}}
}
报错:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
3.6.2 JDK1.6字符串常量池存放在方法区
public class StringDemo01 {// jdk1.6 设置 方法区大小参数:-XX:MaxPermSize=1mpublic static void main(String[] args) {ArrayList arrayList = new ArrayList();int count = 0;try {for (int i = 0; i < 2500000; i++) {arrayList.add((i + "").intern());count++;}} catch (Exception e) {e.printStackTrace();} finally {System.out.println("循环次数:" + count);}}
}
报错:Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
3.6.3 为什么jdk7 String常量池放入到堆中
在Java 7之前,字符串常量池是位于永久代(PermGen space)中的。然而,从Java 7开始,字符串常量池被移到了堆(Heap)中。这一改变有几个主要原因:
-
PermGen 空间限制:
- 在Java 6及更早版本中,永久代空间是有限的,并且不容易调整大小。如果大量的类加载或字符串实习化(interning)发生,可能导致 PermGen 空间的溢出。
- 移动字符串常量池到堆中,使得这部分内存的管理更加灵活,可以根据应用程序的需求动态调整大小。
-
HotSpot VM 的改进:
- 从Java 7开始,Oracle对HotSpot VM进行了许多改进,包括引入了新的元空间(Metaspace)机制来替代永久代。虽然元空间主要用于存储类元数据,但它也体现了Oracle对于内存管理的新思路。
- 字符串常量池移至堆中也是这一系列改进的一部分。
-
更好的内存管理:
- 将字符串常量池放置在堆中后,可以更好地利用垃圾收集机制来管理和回收不再使用的字符串对象。
- 堆内存通常比永久代更大,这使得更多的字符串可以被实习化,从而减少内存中的字符串冗余。
-
一致性:
- 由于字符串常量池现在位于堆中,这使得其内存管理与其他对象更加一致。这样简化了开发者的理解负担,并且减少了不同内存区域之间复杂的交互。
-
性能优化:
- 字符串常量池的重新定位也有助于性能上的优化。由于字符串对象本身就在堆中,将常量池也放在同一区域可以减少不同内存区域之间的数据移动,从而提升性能。
综上所述,将字符串常量池移到堆中是为了提高系统的健壮性和灵活性,同时减少内存管理方面的复杂度,并提高性能。这一改变反映了Java平台随着硬件和软件的发展不断演进的过程。