> 文档中心 > jvm系列-常量池-简介

jvm系列-常量池-简介

本文主要总结一下以下三个知识点:

  1. 常量
  2. 运行时常量池
  3. 字符串常量池

一、类常量池

类常量池是.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的字节流所代表的静态存储结构转化为方法区的运行时数据结构.

运行时常量池有以下特点:

  1. 每一个.class文件都会分配一个运行时常量池来存储当前类.class文件中的常量池信息,这些信息主要是编译期生成的各种字面量和符号引用.
  2. 运行时常量池相对于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[];    }

从源码中可以看出:

  1. String被final修饰,表示无法被继承
  2. 属性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操作的含义:

  1. 将当前字符串添加到字符串常量池,并返回该字符串在字符串常量池的内存地址
    1. 如果字符串常量池已经存在该字符串,则直接返回该字符串地址
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");

这行代码一共做了以下操作:

  1. new StringBuilder
  2. new String(“a”)
  3. 在字符串常量池 添加 a
  4. new String(“bc”)
  5. 在字符串添加 bc
  6. StringBuilder.toString() 操作又new String(“abc”),但是未往字符串常量池添加.

所以str1,str2,str3对应的内存分布如下:

在这里插入图片描述

堆上的字符串对象a和字符串对象bc,会在最近的一次垃圾回收时被回收,因为根本没有不可达.

3.2.3.总结

  1. new String 返回的时堆上的地址,但是不会把string自动添加到字符串常量池
  2. String a = “abc”,会自动把abc添加到字符串常量池,并返回字符串在字符串常量池的内存地址
  3. String.intern会把当前堆上的字符串添加到字符串常量池,并把堆上该字符串引用指向到字符串常量池字符串地址,
  4. 在程序中定义字符串推荐 String a= “abc”,或String a = new String(“abc”).intern,提高字符串利用率.

3.3.其他类型的常量池

在java中,除了存在字符串常量池,其他封装类也有对应的常量池,只不过字符串常量池是jvm级别的,而其他封装类常量池是在各自的类里面实现.

这些常量池范围如下:

  1. Byte、Short、Integer、Long:[-128,127]
  2. Character:[0,127]
  3. 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的化,还是失效的.