JVM常量池


经典参考文章:
字符串常量池深入解析

什么是常量

如果是从 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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值