jvm系列-常量池-简介
本文主要总结一下以下三个知识点:
一、类常量池
类常量池是.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_SUPERConstant pool: #1 = Methodref #11.#28 // java/lang/Object."":()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 // "":()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."":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 05 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 06 0 this Lcom/study/jvm/mem/UserService; 33 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, 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:falsestr2 == str3:truestr1 == 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."":(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."":()V 20: new #2 // class java/lang/String 23: dup 24: ldc #7 // String a 26: invokespecial #4 // Method java/lang/String."":(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."":(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的化,还是失效的.