字符串常量池String-Pool是干啥的?声明String到底new 不 new?
public class Program01
{
public static void main(String[] args)
{
01. String a1 = "HELLO";
02. String a2 = "HELLO";
03. String a3 = new String("HELLO");
04. String a4 = "HELLO";
}
}
这个java程序你能理解他的底层操作么?比如最后程序执行完毕,a4的值是什么呢?
大部分同学可能理解a4的值是个地址,但它的值(地址),和a1相同还是和a3相同呢?
前置知识点:
<1>内存包括:
Java 内存(简化)
├── 栈(Stack)
│ └── 局部变量(如 a1, a2 等引用)
├── 堆(Heap)
│ ├── 普通对象(new 出来的)
│ └── 字符串常量池(String Pool)
│ └── 字面量字符串对象(如 "HELLO")
└── 方法区(元空间,Java 8+)
└── 类信息、常量池(Class Constant Pool,注意不是 String Pool)
<2> String ss = new String("ABC");
main里如果有这个声明和初始化语句的话,ss在main的栈空间存储,它的值是地址。
“ABC” 存放在堆空间里,语句里的new 算符就是为它申请空间用的。
此时请你先不要考虑"ABC"是放在堆的普通对象区域,还是字符串常量池区域
<3> 字符串常量池(String Pool)规则
- 程序里所有使用双引号直接赋值的字符串(如 "HELLO")会在编译期放入字符串常量池(如果池中还没有的话)。
- 再次用双引号创建相同内容的字符串时,会直接引用常量池中已有的对象。
- 使用 new String("HELLO") 会在堆上新建一个对象,不会与常量池的对象共用地址(但构造时如果常量池没有会先放入常量池)。
public class Program01
{
public static void main(String[] args)
{
01. String a1 = "HELLO";
02. String a2 = "HELLO";
03. String a3 = new String("HELLO");
04. String a4 = "HELLO";
}
}对于这个程序,我们来稍微仔细分析一下程序的4条语句到底执行了什么:
Time_01
内存:初始这样的:
-------------------------------------------
stack (main的):
-------------------------------------------
heap:
普通对象:
字符串常量池String Pool:
-------------------------------------------
方法区等....其他
Time_02
当程序还没有正式运行,即在编译时刻:
编译器发现代码中的字符串字面量 "HELLO",会将其记录在 .class 文件的常量池表中。
当程序运行时,JVM 在类加载阶段会将 "HELLO" 放入运行时的字符串常量池(在堆中)。
-------------------------------------------
stack (main的):
-------------------------------------------
heap:
普通对象 :
字符串常量池String Pool:0X3465:“HELLO” //为"HELLO"分配了地址0X3465
-------------------------------------------
方法区等....其他
Time_03
01. String a1 = "HELLO";
当这条语句执行,先检查堆的字符串常量池,我们发现有“HELLO”
那么直接给a1 赋值地址:0X3465
-------------------------------------------
stack (main的):
a1=0X3465-------------------------------------------
heap:
普通对象 :
字符串常量池String Pool:0X3465:“HELLO”
-------------------------------------------
方法区等....其他
Time_04
02. String a2 = "HELLO";
依旧先检查堆的字符串常量池,我们发现有“HELLO”
那么直接给a2 赋值地址:0X3465
-------------------------------------------
stack (main的):
a1=0X3465
a2=0X3465
-------------------------------------------
heap:
普通对象 :
字符串常量池String Pool:0X3465:“HELLO”
-------------------------------------------
方法区等....其他
Time_05
03. String a3 = new String "HELLO";
先检查常量池,已有 "HELLO"(不再新建字面量对象)
但注意,毕竟new算符出现了
那么就在堆的普通区域 new 一个全新的 String 对象(假设地址 0X5789),内容也是 "HELLO",a3 指向 0X5789。此时堆中会有两个 "HELLO" 对象:
一个在字符串常量池(0x3465) / 一个在堆普通区域(0x5789),a3指向后者-------------------------------------------
stack (main的):
a1=0X3465
a2=0X3465
a3=0X5789
-------------------------------------------
heap:
普通对象 :0X5789:"HELLO" //为"HELLO"分配了地址0X5789
字符串常量池String Pool:0X3465:“HELLO”
-------------------------------------------
方法区等....其他
Time_06
04. String a4 = "HELLO";
先检查常量池,已有 "HELLO"
那么直接给a4 赋值地址:0X3465-------------------------------------------
stack (main的):
a1=0X3465
a2=0X3465
a3=0X5789
a4=0X3465
-------------------------------------------
heap:
普通对象 :0X5789:"HELLO"
字符串常量池String Pool:0X3465:“HELLO”-------------------------------------------
方法区等....其他
总结:
所以从整个程序运行的过程看,编译时刻和声明a3 时发生的事情比较隐藏,对我们是陌生的:
<1>编译期:
Java 编译器(javac)在编译 Program01.java 成 Program01.class 时,看到代码中的字符串字面量 "HELLO",会把它记录在 .class 文件的常量池表(Class文件常量池) 中。
这不是运行时的内存,只是类文件里的一张表。类加载时(运行前):
JVM 加载 Program01.class 时,会把类常量池中的字符串字面量 "HELLO" 放入运行时的字符串常量池(Heap 里),这时 "HELLO" 对象就已经在堆的字符串池里了(地址 0x3465)。运行 main 时:
执行到 String a1 = "HELLO"; 时,"HELLO" 已经在常量池里了(类加载时放入了),所以 a1 直接得到引用 0x3465。<2>声明a3时:
注意前置知识点提到的字符串常量池规则第3条:
使用 new String("HELLO") 会在堆上新建一个对象,不会与常量池的对象共用地址(但构造时如果常量池没有会先放入常量池)。
