JVM8-一道字符串题题带你认识常量池
请先做题并把答案记在心里
package sandwich.test2;
/**
* @author sandwich
* @date 2021/4/20
*/
public class StringTest {
public static void main(String[] args) {
String a = "abc";
String b = new String("abc");
String c = b.intern();
System.out.println(a == b);
System.out.println(b == c);
System.out.println(a == c);
}
}
答案在文章最后面
常量池
常量池的数据存储在方法区,分以下三种
1.Class常量池(静态常量池)
在 class 文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池 (Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用。
如下是一个class反汇编的内容,请看Constant pool部分内容
PS D:\git\test\target\classes\sandwich> javap -v .\Person.class
Classfile /D:/git/test/target/classes/sandwich/Person.class
Last modified 2021年4月17日; size 957 bytes
MD5 checksum df5c1bcb5d0f1d5be2998dd1d8d6b44e
Compiled from "Person.java"
public class sandwich.Person
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #2 // sandwich/Person
super_class: #14 // java/lang/Object
interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #14.#35 // java/lang/Object."<init>":()V
#2 = Class #36 // sandwich/Person
#3 = Methodref #2.#35 // sandwich/Person."<init>":()V
#4 = Methodref #2.#37 // sandwich/Person.work:()I
#5 = Fieldref #38.#39 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Class #40 // java/lang/StringBuilder
#7 = Methodref #6.#35 // java/lang/StringBuilder."<init>":()V
#8 = String #41 // result=
#9 = Methodref #6.#42 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#10 = Methodref #6.#43 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#11 = Methodref #6.#44 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#12 = Methodref #45.#46 // java/io/PrintStream.println:(Ljava/lang/String;)V
#13 = Methodref #14.#47 // java/lang/Object.hashCode:()I
#14 = Class #48 // java/lang/Object
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 this
#21 = Utf8 Lsandwich/Person;
#22 = Utf8 work
#23 = Utf8 ()I
#24 = Utf8 i
#25 = Utf8 I
#26 = Utf8 j
#27 = Utf8 main
#28 = Utf8 ([Ljava/lang/String;)V
#29 = Utf8 args
#30 = Utf8 [Ljava/lang/String;
#31 = Utf8 person
#32 = Utf8 z
#33 = Utf8 SourceFile
#34 = Utf8 Person.java
#35 = NameAndType #15:#16 // "<init>":()V
#36 = Utf8 sandwich/Person
#37 = NameAndType #22:#23 // work:()I
#38 = Class #49 // java/lang/System
#39 = NameAndType #50:#51 // out:Ljava/io/PrintStream;
#40 = Utf8 java/lang/StringBuilder
#41 = Utf8 result=
#42 = NameAndType #52:#53 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#43 = NameAndType #52:#54 // append:(I)Ljava/lang/StringBuilder;
#44 = NameAndType #55:#56 // toString:()Ljava/lang/String;
#45 = Class #57 // java/io/PrintStream
#46 = NameAndType #58:#59 // println:(Ljava/lang/String;)V
#47 = NameAndType #60:#23 // hashCode:()I
#48 = Utf8 java/lang/Object
#49 = Utf8 java/lang/System
#50 = Utf8 out
#51 = Utf8 Ljava/io/PrintStream;
#52 = Utf8 append
#53 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#54 = Utf8 (I)Ljava/lang/StringBuilder;
#55 = Utf8 toString
#56 = Utf8 ()Ljava/lang/String;
#57 = Utf8 java/io/PrintStream
#58 = Utf8 println
#59 = Utf8 (Ljava/lang/String;)V
#60 = Utf8 hashCode
{
public sandwich.Person();
descriptor: ()V
flags: (0x0001) 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 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lsandwich/Person;
public int work();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: iconst_3
1: istore_1
2: iconst_5
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: ireturn
LineNumberTable:
line 10: 0
line 11: 2
line 12: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lsandwich/Person;
2 9 1 i I
4 7 2 j I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: new #2 // class sandwich/Person
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method work:()I
12: istore_2
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: new #6 // class java/lang/StringBuilder
19: dup
20: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
23: ldc #8 // String result=
25: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: iload_2
29: invokevirtual #10 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
32: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
35: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
38: aload_1
39: invokevirtual #13 // Method java/lang/Object.hashCode:()I
42: pop
43: return
LineNumberTable:
line 16: 0
line 17: 8
line 18: 13
line 20: 38
line 21: 43
LocalVariableTable:
Start Length Slot Name Signature
0 44 0 args [Ljava/lang/String;
8 36 1 person Lsandwich/Person;
13 31 2 z I
}
SourceFile: "Person.java"
字面量:给基本类型变量赋值的方式就叫做字面量或者字面值。 比如:String a=“b” ,这里“b”就是字符串字面量,同样类推还有整数字面值、浮点类型字面量、字符字面量。
符号引用 :符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,JAVA 在编译的时候一个每个 java 类都会被编译成一个 class 文件,但在编译的时候虚拟机并不知道所引用类的地址(实际地址),就用符号引用来代替,而在类的解析阶段(后续 JVM 类加载会具体讲到)就是为了把 这个符号引用转化成为真正的地址的阶段。 一个 java 类(假设为 People 类)被编译成一个 class 文件时,如果 People 类引用了 Tool 类,但是在编译时 People 类并不知道引用类的实际内存地址,因 此只能使用符号引用(org.simple.Tool)来代替。而在类装载器装载 People 类时,此时可以通过虚拟机获取 Tool 类的实际内存地址,因此便可以既将符号 org.simple.Tool 替换为 Tool 类的实际内存地址。
2.运行时常量池
运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池(Constant_Pool)的运行时表示形式,它包括了若干种不同的常量: 从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。(这个是虚拟机规范中的描述,很生涩) 运行时常量池是在类加载完成之后,将 Class 常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用。
运行时常量池在 JDK1.7 版本之后,就移到堆内存中了,这里指的是物理空间,而逻辑上还是属于方法区(方法区是逻辑分区)。 在 JDK1.8 中,使用元空间代替永久代来实现方法区,但是方法区并没有改变,所谓"Your father will always be your father"。变动的只是方法 区中内容的物理存放位置,但是运行时常量池和字符串常量池被移动到了堆中。但是不论它们物理上如何存放,逻辑上还是属于方法区的
3.字符串常量池
字符串常量池这个概念是最有争议的。
我们从它的作用和 JVM 设计它用于解决什么问题的点来分析它。 以 JDK1.8 为例,字符串常量池是存放在堆中,并且与 java.lang.String 类有很大关系。设计这块内存区域的原因在于:String 对象作为 Java 语言中重
要的数据类型,是内存中占据空间最大的一个对象。高效地使用字符串,可以提升系统的整体性能。 所以要彻底弄懂,我们的重心其实在于深入理解 String。
3.1 String 类分析(JDK1.8)
String 对象是对 char 数组进行了封装实现的对象,主要有 2 个成员变量:char 数组,hash 值。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
3.2 String 对象的不可变性
了解了 String 对象的实现后,你有没有发现在实现代码中 String 类被 final 关键字修饰了,而且变量 char 数组也被 final 修饰了。 我们知道类被 final 修饰代表该类不可继承,而 char[]被 final+private 修饰,代表了 String 对象不可被更改。Java 实现的这个特性叫作 String 对象的不 可变性,即 String 对象一旦创建成功,就不能再对它进行改变。
Java 这样做的好处在哪里呢?
1.保证 String 对象的安全性。 假设 String 对象是可变的,那么 String 对象将可能被恶意修改。
2.保证 hash 属性值不会频繁变更。确保了唯一性,使得类似 HashMap 容器才能实现相应的 key-value 缓存功能.
3.可以实现字符串常量池。在 Java 中,通常有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,如 String str=“abc”;另一种是字 符串变量通过 new 形式的创建,如 String str = new String(“abc”)。
3.3 String 的创建方式及内存分配的方式
1、String str=“abc”; 当代码中使用这种方式创建字符串对象时,JVM 首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中 被创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。(str 只是一个引用)
String str = "abc"
代码编译加载时,会在常量池中创建常量"abc", 运行时,返回常量池中的字符串引用
堆 字符串常量池 “abc”
2、String str = new String(“abc”) 。 首先在编译类文件时,"abc"常量字符串将会放入到常量结构中,在类加载时,“abc"将会在常量池中创建;其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的"abc” 字符串,在堆内存中创建一个 String 对象;最后,str 将引用 String 对象。
String str = new String("abc")
1.代码编译加载时,会在常量池中创建常量“abc”
堆 字符串常量池 “abc” 2.在调用new是,会在堆中创建String对象,并引用常量池中的字符串对象char[]数组,并返回String对象引用。
堆 字符串常量池 String对象 “abc”
3、 使用 new,对象会创建在堆中,同时赋值的话,会在常量池中创建一个字符串对象,复制到堆中。 具体的复制过程是先将常量池中的字符串压入栈中,在使用 String 的构造方法是,会拿到栈中的字符串作为构方法的参数。 这个构造函数是一个 char 数组的赋值过程,而不是 new 出来的,所以是引用了常量池中的字符串对象。存在引用关系。
public class Location {
private String city;
private String region;
}
public class model{
Location location = new Location;
location.setCity("广州");
location.setRegion("珠江新城");
}
在运行时,创建的String在堆中,直接创建,不会在常量池中创建
堆 字符串常量池 广州 珠江新城
4.String str="ab"+"cd"+"ef"
前面我讲过 String 对象是不可变的,如果我们使用 String 对象相加,拼接我们想要的字符串,是不是就会产生多个
对象呢?例如以下代码: 分析代码可知:首先会生成 ab 对象,再生成 abcd 对象,最后生成 abcdef 对象,从理论上来说,这段代码是低效的。 编译器自动优化了这行代码,编译后的代码,你会发现编译器自动优化了这行代码,如下 String str= "abcdef";
5.intern
String 的 intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用。
/**
* @author sandwich
* @date 2021/4/20
*/
public class InternTest {
public static void main(String[] args) {
String a = new String("Sandwich").intern();
String b = new String("Sandwich").intern();
System.out.println(a == b);
}
}
//结果是true
1、new Sting() 会在堆内存中创建一个 a 的 String 对象,"Sandwich"将会在常量池中创建
2、在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。
3、调用 new Sting() 会在堆内存中创建一个 b 的 String 对象。
4、在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。 所以 a 和 b 引用的是同一个对象。
现在可以公布文章开头的答案了
这道题主要是考考常量池
答案是:
false
false
true
题解:a 是常量池的字符串(在方法区)的引用,b在调用new时会在堆中创建String对象,并引用常量池中的字符串对象char[]数组。并返回String对象引用,c直接返回b在常量池创建的常量引用