文章目录
经典参考文章:
字符串常量池深入解析
什么是常量
如果是从 C/C++ 转过来的程序员,一般认为常量是被 const 修饰的变量或者某些宏定义,而在Java 中final 修饰的变量也可以被称为是常量。
java中,常量不单单指 final修饰的变量,任何具有不可变性的东西都被称为常量。
有时候,我们会看到这种说法:字符串是常量,不可修改。其实并不仅仅因为String底层使用final修饰String类,但final只保证了类不被继承,字符数组使用final修饰只能保证value属性不指向其他内存地址,我们仍然可以通过value[0]=‘H’的方式修改value,因此字符串数组还使用了private关键字修饰。
private final char value[];
字面量是指这个数据的本身就是这样,如:整数、浮点数以及字符串等。可见,字面量也是常量
常量池
- 常量池分为两种:静态常量池和运行时常量池
- 常量池中主要存放两大类常量:字面量和符号引用量
(1)字面量则属于Java语言层面常量的概念,包括字符串常量、基本数据类型、final修饰的成员变量
(2)符号引用量则属于编译原理方面的概念,包括了如下三种类型的常量:类和接口的完全限定名、字段名称和描述符、方法名称和描述符
包装类的对象池和JVM的静态/运行时常量池没有任何关系
- Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值在-128到127时才可使用对象池。
- IntegerCache 是 Integer 在内部维护的一个静态内部类,用于对象缓存。通过源码我们知道,Integer 对象池在底层实际上就是一个变量名为 cache 的数组,里面包含了 -128 ~ 127 的 Integer 对象实例。
- 使用对象池的方法就是通过 Integer.valueOf() 返回 cache 中的对象,像 Integer i = 10 这种自动装箱实际上也是调用 Integer.valueOf() 完成的。如果使用的是 new 构造器,则会跳过 valueOf(),所以不会使用对象池中的实例。
- 包装类缓存池的初始化在类加载时已经完成,是享元设计模式的一种应用,属于java代码层面,而常量池属于JVM层面。
javap命令反编译后的文件
源码为:
class Main{
public static final main(String args[]){
System.out.println("helloword");
}
}
D:\demo\redis-demo\target\classes\top\onething\redis\controller>javap -v Main.class
Classfile /D:/demo/redis-demo/target/classes/top/onething/redis/controller/Main.class
Last modified 2020-10-11; size 573 bytes
MD5 checksum 943294ef1349c2ad42724e21bc44d2f0
Compiled from "Main.java"
public class top.onething.redis.controller.Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // helloword
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // top/onething/redis/controller/Main
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Ltop/onething/redis/controller/Main;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 Main.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 helloword
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 top/onething/redis/controller/Main
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public top.onething.redis.controller.Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ltop/onething/redis/controller/Main;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String helloword
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 10: 0
line 11: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "Main.java"
静态常量池
- 静态常量池在编译时就已经确定
- 静态常量池是class字节码文件中的常量池,class文件中的常量池不仅仅包含字符串字面量、数字常变量,还包含类、方法的信息。
java源码文件通过javac指令编译为class字节码文件后,通过javap指令反编译后即可看到静态常量池,其中javap指令为:javap -v class字节码文件名,这里反编译后的指令即JVM规范规定的指令
运行时常量池
- 运行时常量池则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,并把里面的符号地址替换为真实地址。我们常说的常量池,就是指方法区中的运行时常量池。
- 运行时常量池中的常量,基本来源于各个class文件中的常量池。(即每个class文件都有对应的常量池)
静态常量池、运行时常量池、字符串常量池的位置
- 静态常量池:诞生于编译阶段,存在于Class文件中
- 运行时常量池:诞生于JVM运行时,存在于内存的元空间中。运行时常量池其实就是将编译后的类信息放入运行时的一个区域中,用来动态获取类信息。
注意:每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。 - 字符串常量池:存在于堆中。字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中
字符串常量池
String的基本特性
- String在JDK8及以前底层为字符数组【private final char value[]】;
String在JDK9优化为byte数组+字符编码集【private final byte[] value; 】,一个字符可能有1个byte空间存储,也可能用多个byte空间存储。 - 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
字符串拼接的三种方式详解
(1)“+”拼接;
(2)concat拼接;
(3)StringBuilder(StringBuffer)拼接
public class Demo {
public static void main(String[] args) {
String str = "12" + 34;
String str1 = str + "abc";
System.out.println(str);
System.out.println(str1);
}
}
字节码文件通过javap -v -l Demo.class命令反编译如下:
结论:
(1)“+”拼接常量,如:“ab”+“123”或abc+123,编译时就会直接拼接,并放入静态常量池。
(2)“+”拼接含变量,如:“变量”+变量a,或者 String类型的变量a+String类型的变量b,编译时实际时:
new StringBuilder().append("+号前参数").append("+号后参数").toString()
(3)从下面可以看出,在循环中使用"+"进行拼接,每次循环都会新创建一个StringBuilder对象。为了避免每循环一次就new一个StringBuilder对象,应该显性的使用StringBuilder,这样可以换节省性能。
public class Demo {
public static void main(String[] args) {
String str = "";
for (int i = 0; i < 5; i++) {
str = str + i;
}
System.out.println(str);
}
}
优化后:
public class Demo {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
sb.append(i) ;
}
System.out.println(sb);
}
}
字符串常量池实际是不会扩容的哈希表
- 字符串常量池是一个固定大小的哈希表StringTable,默认值为1009【哈希桶的长度】,也是StringTable长度可设置的最小值,可以通过使用-XX:StringTableSize设置StringTable的长度。
如果放进StringPool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后会直接造成的影响就是当调用String intern时性能会大幅下降 - 字符串常量池中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。
- 字符串常量池在虚拟机中只存在一个,被所有类和线程共享。
- 字符串常量池当中的字符串不允许重复存放
- JVM是通过equals方法来确定在字符串常量池是否存在相同字符串的引用的。
(1)直接使用双引号声明出来的String对象的引用会直接存储在常量池中
(2)使用new、toString等方式创建的对象均处于堆中,不会主动进行常量池的检查
(4)非纯字面常量相加,各部分会被优化成类似StringBuffer().append(某字符串对象).append("某字符串对象).append(某字符串对象).toString();的形式,置于堆中,不会主动进行常量池的检查
情况1:
String s1 = new String("1") + new String("1");
情况2:
String s2 = "a"+s1;
情况3:
String s3 = new StringBuilder("123").append("abc");
(3)使用String提供的intern方法将字符串存储在常量池中:
JDK8中两种创建字符串对象的方式的分析
String s1 = "abc";
String s2 = "abc";
System.out.println(s1==s2); //结果true
(1)执行第一句代码时,JVM首先会去字符串常量池中查找是否存在相同字符串的引用,发现不存在后在堆中创建"abc"这个字符串对象,然后将该对象的引用放入常量池中,然后再将该引用返回给s1;
(2)执行第二句代码时,JVM去字符串常量池中检查是否存在相同字符串的引用,发现已经存在相同字符串“abc”的引用【第1句代码产生的】,则不在创建任何对象,直接将池中"abc"字符串的引用返回,赋给引用s2。
(3)因为s1、s2都被赋值了字符串常量池中相同的引用,所以结果为true。
String s3 = new String("xyz");
String s4 = new String("xyz");
System.out.println(s3==s4); //false
(1)执行第1句
执行new String(“xyz”)时,先把构造方法的参数“xyz”拿到常量池中查看是否存在相同字符串的引用,发现不存在,发现不存在后在堆中创建"abc"这个字符串对象,然后将该对象的引用放入常量池中。再执行new,即通过构造方法直接在堆内存中新建xyz的字符串对象,注意执行new时,不会去检查常量池,而是直接将新建的字符串对象引用赋值给s3。
(2)执行第2句
执行new String(“xyz”)时,先把构造方法的参数“xyz”拿到常量池中查看是否存在相同字符串的引用,发现已经存在,则不再创建。在执行new,即通过构造方法直接在堆内存中新建xyz的字符串对象,注意执行new时,不会去检查常量池,而是直接将新建的字符串对象引用赋值给s4。
(3) s3、s4都分别指向堆内存中的不同地址,而没有使用字符串常量池中的引用,因此为false。
String的intern方法执行流程分析
- 如果字符串常量池中存在当前字符串,则并不会放入,而是返回池中字符串的引用【返回的字符串引用和当前字符串的引用可能相同,也可能不同】
- 如果没有,则把对象的引用地址复制一份,放入字符串常量池,并返回该字符串的引用
情况1:
//s1被赋值堆内存的引用,此时常量池中没有字符串11的引用
String s1 = new String("1") + new String("1");
//s1堆内存的引用被放入常量池,并且该引用被赋值给s2
String s2 = s1.intern();
String s3 = "11"
System.out.println(s1==s2); //true
情况2:
//“abc”堆内存中创建后,直接存放到了常量池,调用intern方法时检查发现已经存在abc,直接返回了常量池中存放的abc引用
System.out.println("abc".intern() == "abc");
情况3:
//str创建过程没有进行字符串常量池检查,直接在堆中创建了字符串对象,而"ab"则进行了
String str = new String("ab");
System.out.println(str=="ab"); //false
//str是新创建的,intern方法执行时发现常量池中已存在ab,就直接返回了字面量ab的引用
System.out.println(str==str.intern());//false
字符串常量池的特殊情况
"java"字符串在JVM启动时它的引用就存入了字符串常量池
原因:
JVM启动的时候就会加载类sun.misc.Version,该类中的成员变量使用了“java”、空字符等字符串,即Java启动的时候“java”、空字符的对象引用就放入了字符串常量池。
new String(引用)的情形
new String(s1)、new String(“hello”)都会分别在堆内存中创建一个新的对象,但是对象内部的字符数组使用的是同一个对象。
String s1 = "hello";
String s2 = new String(s1);
String s3 = new String("hello");
System.out.println(s1 == s2); //false
System.out.println(s1 == s3); //false
System.out.println(s2 == s3); //false