JVM-人人都懂常量池

·  阅读 587

一、静态常量池和运行时常量池

我们通过javap -v MyTest.class解析字节码文件

public class com.example.spring.jvmTest.MyTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#23         // java/lang/Object."<init>":()V
   #2 = Methodref          #5.#24         // com/example/spring/jvmTest/MyTest.math:()V
   #3 = Class              #25            // com/example/spring/jvmTest/A
   #4 = Methodref          #3.#23         // com/example/spring/jvmTest/A."<init>":()V
   #5 = Class              #26            // com/example/spring/jvmTest/MyTest
   #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               Lcom/example/spring/jvmTest/MyTest;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               math
  #19 = Utf8               a
  #20 = Utf8               Lcom/example/spring/jvmTest/A;
  #21 = Utf8               SourceFile
  #22 = Utf8               MyTest.java
  #23 = NameAndType        #7:#8          // "<init>":()V
  #24 = NameAndType        #18:#8         // math:()V
  #25 = Utf8               com/example/spring/jvmTest/A
  #26 = Utf8               com/example/spring/jvmTest/MyTest
  #27 = Utf8               java/lang/Object
{
  public com.example.spring.jvmTest.MyTest();
    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/example/spring/jvmTest/MyTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #2                  // Method math:()V
         3: return
      LineNumberTable:
        line 6: 0
        line 7: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  args   [Ljava/lang/String;

  public static void math();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=0
         0: new           #3                  // class com/example/spring/jvmTest/A
         3: dup
         4: invokespecial #4                  // Method com/example/spring/jvmTest/A."<init>":()V
         7: astore_0
         8: return
      LineNumberTable:
        line 10: 0
        line 11: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            8       1     0     a   Lcom/example/spring/jvmTest/A;
}
SourceFile: "MyTest.java"

复制代码

其中的Constant pool就是我们的静态常量池,即在加载期间class文件内部的常量池

而我们代码在运行的过程中,需要把这些静态常量池中的数据加载到内存中,一旦加载进内存,就叫做运行时常量池

二、字符串常量池

JDK1.6之前字符串常量池位于方法区(永久代),1.6之后从方法区脱离出来,放于堆区

示例1(JDK1.8的环境):

public static void main(String[] args) {
        String s1 = "hello";
        String s2 = new String("hello");
        String s3 = s2.internn();
        System.err.println(s1==s2);
        System.err.println(s2==s3);
        System.err.println(s1==s3);
        
        StringBuilder sb = new StringBuilder();
        sb.append("llo");
        String s4 = sb.toString().intern();
        System.err.println("llo".equals(s4));
    }

输出:
false
false
true
true
复制代码
  • String s1 = "hello"会去字符串常量池中通过equals()找是否有"hello"的对象,如果有,则直接返回该字符串。如果没有,则在常量池中创建一个"hello"的对象

  • String s2 = new String("hello")",它分为两步,

  1. 先去字符串常量池中通过equals()找是否有"hello"的对象,如果没有,则在常量池中创建一个"hello"的对象,如果有,就进行第二步。
  2. 再去堆区创建一个"hello"的字符串对象,返回堆区对象的引用
  • String s3 = s2.internn()会去字符串常量池中通过equals()找是否有s2的对象,如果有,则直接返回该对象。如果没有:
  1. 如果是JDK1.6,则在字符串常量池中创建一个新的对象,并且在字符串常量池中新建一个指针,指向字符串常量池中的新对象,返回新对象的引用
  2. 如果是JDK1.6以上,则在常量池中新建一个指针,指向堆中的对象,返回堆中对象的引用
  • sb.append("llo")会直接在堆区创建一个"llo"的对象,不会在字符串常量池中创建,返回堆区对象的引用。

示例2(JDK1.8的环境):

public static void main(String[] args) {
            String s1 = new String("he")+new String("llo");
            String s3 = s1.intern();
            System.err.println(s1==s3);
    }
 JDK6输出:false
 JDK6以上输出:true
复制代码
  1. new String("he"),先去字符串常量池中通过equals()找是否有"he"的字符串,没有,在字符串常量池中创建一个"he"的字符串,再去堆区创建一个"he"的字符串对象
  2. new String("llo"),先去字符串常量池中通过equals()找是否有"llo"的字符串,没有,在字符串常量池中创建一个"llo"的字符串,再去堆区创建一个"llo"的字符串对象
  3. "he"+"llo",代码在编译后变成了sb.append("he").append("llo"),既然是StringBuider,则只会在堆区生成一个"hello"对象
  4. s1.intern(),先去字符串常量池中通过equals()找是否有s1的字符串,没有,则返回堆中的对象

示例3(JDK1.8的环境):

public static void main(String[] args) {
        String ab = "ab";
        String k = "a"+"b";
        System.err.println(ab==k);
    }
 输出:true
复制代码

"a"+"b"这种常量之间的"+"号会在编译期间优化为"ab" ,它们都指向字符串常量池中的"ab",所以为true

示例4(JDK1.8的环境):

public static void main(String[] args) {
        String ab = "ab";
        String b = "b";
        String c = "a"+b;
        System.err.println(ab==c);
    }
 输出:false
复制代码

"a"+b这种因为引用类型b在编译期间是无法确定具体的值的, 编译之后变成了StringBuider("a").append(b),即会在堆中生成"ab"对象,c指向这个对象,所以为false

示例5(JDK1.8的环境):

public static void main(String[] args) {
        String ab = "ab";
        final String b = "b";
        String c = "a"+b;
        System.err.println(ab==c);
    }
 输出:true
复制代码

和上面的例子就一个final的区别,final修饰的变量在编译期间就能确定下来,即"a"+b就是"a"+"b",所以为true

三、基本类型包装类的对象池

我们来看一段代码 :

public static void main(String[] args) {
        Integer a = 127;
        Integer b = 127;

        Integer a1 = 128;
        Integer b1 = 128;

        Integer a2 = new Integer(127);
        Integer b2 = new Integer(127);
        System.err.println(a==b);
        System.err.println(a1==b1);
        System.err.println(a2==b2);
    }
输出:
true
false
false
复制代码

那么为什么呢

  1. 其实是Integer内部维护了一个本地缓存,缓存范围是-128到127,Integer a = 127在编译期间变成了Integer.valueOf(127),会直接走缓存逻辑,在缓存内的数字,直接从缓存拿即可
  2. new Integer(127) 这种不会走缓存逻辑的,而是直接去堆区创建对象

注意

8大包装类中除了Float和Double没有实现缓存以外(因为Float和Double的范围太广了,没有缓存的必要),其他的包装类都实现了

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改