本文主要总结一下以下三个知识点:
- 类常量池
- 运行时常量池
- 字符串常量池
一、类常量池
类常量池是.class字节码文件中内容,保存了Java类中大多数信息,如方法信息、变量信息等.
它是.class字节码文件中的概念.
如下,定义一个java类:
package com.study.jvm.mem;
public class UserService {
private final static Long ID=10L;
private static String name = "user";
public String userInfo(){
String userInfo = "{name:x,age:1}";
return "userInfo";
}
}
查看字节码:
javap -v UserService.class
Classfile /Users/dev/workspace-study/study/jvm/target/classes/com/study/jvm/mem/UserService.class
Last modified 2022-6-1; size 664 bytes
MD5 checksum 7e46f52a2426c92ecd39d80a1393fb99
Compiled from "UserService.java"
public class com.study.jvm.mem.UserService
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #11.#28 // java/lang/Object."<init>":()V
#2 = String #29 // {name:x,age:1}
#3 = String #23 // userInfo
#4 = Long 10l
#6 = Methodref #30.#31 // java/lang/Long.valueOf:(J)Ljava/lang/Long;
#7 = Fieldref #10.#32 // com/study/jvm/mem/UserService.ID:Ljava/lang/Long;
#8 = String #33 // user
#9 = Fieldref #10.#34 // com/study/jvm/mem/UserService.name:Ljava/lang/String;
#10 = Class #35 // com/study/jvm/mem/UserService
#11 = Class #36 // java/lang/Object
#12 = Utf8 ID
#13 = Utf8 Ljava/lang/Long;
#14 = Utf8 name
#15 = Utf8 Ljava/lang/String;
#16 = Utf8 <init>
#17 = Utf8 ()V
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 LocalVariableTable
#21 = Utf8 this
#22 = Utf8 Lcom/study/jvm/mem/UserService;
#23 = Utf8 userInfo
#24 = Utf8 ()Ljava/lang/String;
#25 = Utf8 <clinit>
#26 = Utf8 SourceFile
#27 = Utf8 UserService.java
#28 = NameAndType #16:#17 // "<init>":()V
#29 = Utf8 {name:x,age:1}
#30 = Class #37 // java/lang/Long
#31 = NameAndType #38:#39 // valueOf:(J)Ljava/lang/Long;
#32 = NameAndType #12:#13 // ID:Ljava/lang/Long;
#33 = Utf8 user
#34 = NameAndType #14:#15 // name:Ljava/lang/String;
#35 = Utf8 com/study/jvm/mem/UserService
#36 = Utf8 java/lang/Object
#37 = Utf8 java/lang/Long
#38 = Utf8 valueOf
#39 = Utf8 (J)Ljava/lang/Long;
{
public com.study.jvm.mem.UserService();
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/study/jvm/mem/UserService;
public java.lang.String userInfo();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String {name:x,age:1}
2: astore_1
3: ldc #3 // String userInfo
5: areturn
LineNumberTable:
line 11: 0
line 12: 3
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/study/jvm/mem/UserService;
3 3 1 userInfo Ljava/lang/String;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: ldc2_w #4 // long 10l
3: invokestatic #6 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
6: putstatic #7 // Field ID:Ljava/lang/Long;
9: ldc #8 // String user
11: putstatic #9 // Field name:Ljava/lang/String;
14: return
LineNumberTable:
line 5: 0
line 7: 9
}
SourceFile: "UserService.java"
其中Constant pool,就是.class文件的常量池.
我们可以从中看到我们定义的常量信息和方法信息.
private final static Long ID=10L;
字节码表示如下:
#7 = Fieldref #10.#32 // com/study/jvm/mem/UserService.ID:Ljava/lang/Long;
#12 = Utf8 ID
#32 = NameAndType #12:#13 // ID:Ljava/lang/Long;
二、运行时常量池
在虚拟机的类加载阶段,jvm会把该.class的字节流所代表的静态存储结构转化为方法区的运行时数据结构.
运行时常量池有以下特点:
- 每一个.class文件都会分配一个运行时常量池来存储当前类.class文件中的常量池信息,这些信息主要是编译期生成的各种字面量和符号引用.
- 运行时常量池相对于class文件常量池,是动态的.
三、字符串常量池
字符串常量池是专门针对String类型设计的常量池.是当前应用所有线程共享的,每个jvm只有一个.
3.1.为什么要单独对字符串设计一个常量池
首先看下String的定义:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
从源码中可以看出:
- String被final修饰,表示无法被继承
- 属性value被final修改,表示赋值后无法被修改
所以String具有不可变性.
由于在Java中String变量会被大量使用,如果每一次声明一个String,都为其分配内存空间,存储对应的char[] ,将会导致极大的空间浪费.
所以在jvm中提出了字符串常量池的概念,当初始化一个String变量时,如果该字符串已经在字符串常量池已经存在,就直接返回该字符串的引用.否则,往字符串常量池添加该字符串,并返回其引用.
其引用关系如下:
3.2.从几个例子来理解字符串常量池
3.2.1.String.intern()
package com.study.jvm.mem;
public class StringService {
public static void main(String[] args) {
String str1 = new String("abc");
String str2 = "abc";
String str3 = str1.intern();
System.out.println("str1 == str2:"+(str1==str2));
System.out.println("str2 == str3:"+(str3==str2));
System.out.println("str1 == str3:"+(str1==str2));
}
}
str1 == str2:false
str2 == str3:true
str1 == str3:false
str1:指向地址为堆中为string对象分配的内存地址
str2: 指向字符串常量池 abc 的地址
intern操作的含义:
- 将当前字符串添加到字符串常量池,并返回该字符串在字符串常量池的内存地址
- 如果字符串常量池已经存在该字符串,则直接返回该字符串地址
String str1 = new String("abc");
str1在内存中的string对象,在初始化完成后,对象的实例数据部分会存储"abc"这个内容.
但是经过intern操作,会将str1堆内对象的数据引用指向字符串常量池的"abc",如3.1内的图.
3.2.2.String+String
package com.study.jvm.mem;
public class StringService {
public static void main(String[] args) {
String str1 = new String("abc");
String str2 = "abc";
String str3 = new String("a")+new String("bc");
System.out.println("str1 == str2:"+(str1==str2));
System.out.println("str2 == str3:"+(str3==str2));
System.out.println("str1 == str3:"+(str1==str2));
System.out.println("str1.intern == str3.intern:"+(str1.intern()==str2.intern()));
System.out.println("str2.intern == str3.intern:"+(str2.intern()==str2.intern()));
}
}
看下这段代码对应的字节码:
Code:
stack=4, locals=4, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String abc
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: ldc #3 // String abc
12: astore_2
13: new #5 // class java/lang/StringBuilder
16: dup
17: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
20: new #2 // class java/lang/String
23: dup
24: ldc #7 // String a
26: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
29: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
32: new #2 // class java/lang/String
35: dup
36: ldc #9 // String bc
38: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
41: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
44: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
47: astore_3
我们可以看出
String str3 = new String("a")+new String("bc");
这行代码一共做了以下操作:
- new StringBuilder
- new String(“a”)
- 在字符串常量池 添加 a
- new String(“bc”)
- 在字符串添加 bc
- StringBuilder.toString() 操作又new String(“abc”),但是未往字符串常量池添加.
所以str1,str2,str3对应的内存分布如下:
堆上的字符串对象a和字符串对象bc,会在最近的一次垃圾回收时被回收,因为根本没有不可达.
3.2.3.总结
- new String 返回的时堆上的地址,但是不会把string自动添加到字符串常量池
- String a = “abc”,会自动把abc添加到字符串常量池,并返回字符串在字符串常量池的内存地址
- String.intern会把当前堆上的字符串添加到字符串常量池,并把堆上该字符串引用指向到字符串常量池字符串地址,
- 在程序中定义字符串推荐 String a= “abc”,或String a = new String(“abc”).intern,提高字符串利用率.
3.3.其他类型的常量池
在java中,除了存在字符串常量池,其他封装类也有对应的常量池,只不过字符串常量池是jvm级别的,而其他封装类常量池是在各自的类里面实现.
这些常量池范围如下:
- Byte、Short、Integer、Long:[-128,127]
- Character:[0,127]
- Boolean: [True,False]
以Integer为例:
public final class Integer extends Number implements Comparable<Integer> {
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
}
常量池生效是在调用valueOf方法时.直接new的化,还是失效的.