我把Java反编译了给你解析三大常量池,面试官主动为你加薪

2021/4/27 14:25:43

本文主要是介绍我把Java反编译了给你解析三大常量池,面试官主动为你加薪,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

    • class常量池
    • 运行时常量池
    • 字符串常量池
    • 总结
    • Integer缓存
    • 总结
    • 常量池的内存分布问题

今日分享开始啦,请大家多多指教~

在很多在面试的过程中,只要我们在日常的工作中,做足充分的准备,在面试之前,多深入的学习一下一些基础的知识,我想对于你想要的那个薪资,你就会更近一步。

当然了,结果是肯定很明显的了,这里就不多说什么了,今天的主角是常量池,上面三大常量池,我会按照面试官的需求,直接反编译,然后给大家看一下具体的实现原理。

class常量池

我们都知道,在大学最一开始学Java的时候,都是文本操作,Javac,java命令执行,而代码经过Javac之后会生成一个xxx.class文件,这是java引以为傲的可移植性的基石。

class文件中,入口是一个u2类型的数据,也就是占据2个字节,用来给常量池的容量计数,其中第0个用于表达“不引用任何一个常量”。在这两个字节之后就是编译器为我们生成的常量了,这些常量包含了两大类:字面量和符号引用,通过一个例子看一下:

public class ThreePoolDemo {
    int a=1;
}

javap反编译结果如下:

Classfile 
Constant pool:
   #1 = Class              #2             // com/hustdj/jdkStudy/threePool/ThreePoolDemo
   #2 = Utf8               com/hustdj/jdkStudy/threePool/ThreePoolDemo
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Methodref          #3.#11         // java/lang/Object."<init>":()V
  #11 = NameAndType        #7:#8          // "<init>":()V
  #12 = Fieldref           #1.#13         // com/hustdj/jdkStudy/threePool/ThreePoolDemo.a:I
  #13 = NameAndType        #5:#6          // a:I
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/hustdj/jdkStudy/threePool/ThreePoolDemo;
  #18 = Utf8               SourceFile
  #19 = Utf8               ThreePoolDemo.java
{
  int a;
    descriptor: I
    flags: (0x0000)

  public com.hustdj.jdkStudy.threePool.ThreePoolDemo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #10                 // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #12                 // Field a:I
         9: return
      LineNumberTable:
        line 3: 0
        line 4: 4
        line 3: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/hustdj/jdkStudy/threePool/ThreePoolDemo;
}
SourceFile: "ThreePoolDemo.java"

通过反编译我们一睹Constant Pool阵容,密密麻麻一大段,我们不妨就关注关注我们定义的成员变量a。

//在<init>方法的第六行
6: putfield      #12                 
//可以看到进行了putfield,给成员变量赋值,虽然后面的注释提醒了我们是变量a
//但是不妨跟着去看看常量池中的#12
#12 = Fieldref           #1.#13         
//这是一个Fieldref它又指向了#,#13,继续追踪
//#1代表是哪一个类,它又指向了一个UTF8的常量,这个常量就保存了完整的类名
#1 = Class              #2             
#2 = Utf8               com/hustdj/jdkStudy/threePool/ThreePoolDemo
//#13告诉了你这个变量的name和type
#13 = NameAndType        #5:#6
//name是a,type是int
#5 = Utf8               a
#6 = Utf8               I

可以看到,在方法给成员变量a赋值是怎么赋值的,通过Constant Pool来确定我们要给com/hustdj/jdkStudy/threePool/ThreePoolDemo对象的name为a类型为int的这么一个变量赋值,相当于一个通讯录,我要找一个人,你就告诉我这个人住在那里,姓甚名谁。

但是此刻它们都是符号引用,也就是说还仅仅是一串UTF8的字符串,通过Constant Pool确定了一串字符串,对应要找的哪个字段、方法、对象,而这些符号引用需要等到类加载的解析阶段变成直接引用,也就是直接指向对应的内存指针、偏移量等。

运行时常量池

在《Java虚拟机规范8》中是这样描述的 运行时常量池(Runtime constant
pool)是class文件中每一个类或者接口的常量池表(constant
pool)的运行时表示形式,它包含了若干常量,从编译期可知的数值字面量到必须在运行期解析之后才能获得的方法、字段引用。也就是说class常量池=运行时常量池,只不过是不同的表现形式而已,一个是静态的,一个是动态的,其中静态的符号引用也都在运行时被解析成了动态的直接引用。

其实这就很好理解了,字面意思,也就是说运行时常量池是和类绑定的,我们设定的类和接口都会有自己的常量池,而常量池的内存又是在方法区(概念上的)进行分配的,但是,这个地方就会引起一个问题:每个虚拟机,因为种类以及版本不同,都会有不同的实现方式。

以常用的Hotspot虚拟机为例:

1、在1.6运行时常量池以及字符串常量池存放在方法区,此时Hotspot对于方法区的实现为永久代

2、在1.7字符串常量池被从方法区拿到了堆,运行时常量池还留在方法区中

3、在1.8中hotspot移除了永久代用元空间取代它,字符串常量池还在堆中,而运行时常量池依然在方法区也就是元空间(堆外内存)

字符串常量池

为了减少频繁创建相同字符串的开销,JVM弄了一个String Pool,它是全局共享的,整个JVM独一份,与之对应的有一个StringTable,,简单来说它就是一个Hash Map,key–字符串字面量,value–指向真正的字符串对象的指针。

任何通过字面量创建字符串的方式都需要先通过HashMap检查,如果有这个字面量,则直接返回value,如果没有则创建一个。示例如下:

public class StringPoolDemo {
    public static void main(String[] args) {
        String a="123";
        String b="123";
        System.out.println(a==b);
    }
}
//输出为true

它的过程如下:
在这里插入图片描述
如果这样呢?

public class StringPoolDemo {
    public static void main(String[] args) {
        String a = new String("123");
        String b="123";
        System.out.println(a==b);
    }
}
//输出false

它的过程如下:
在这里插入图片描述
如果这样呢?

public class StringPoolDemo {
    public static void main(String[] args) {
        String a = new String("123");
        String b=a.intern();
        System.out.println(a==b);
    }
}

过程如下:
在这里插入图片描述

String s = new String(new char[]{'1', '2', '3'});
String s1=s.intern();
String s2 = "123";
System.out.println(s1==s);
System.out.println(s1==s2);
System.out.println(s==s2);

它的过程如下:
在这里插入图片描述

  1. 通过new创建了一个String对象,此时String Table并没有记录 s.intern(),查看String

  2. Table发现,并没有这样的一个字符串,那么新增记录并且返回对应的地址,即s1指向snew出来的string对象

  3. s=“123”,同样想去string table里面查看,发现已经有这样的字符串了,直接返回地址即可

所以s=s1=s2,三者指向了相同的对象

总结

  • 直接根据字面量创建字符串对象,首先检查string
    table有没有这个字符串字面量,有的话直接返回对应的对象地址,没有则创建一个string对象,并且string
    table记录字符串字面量->对象地址的映射

  • new必定会在heap中创建一个对象

  • intern执行的思路与通过字面量创建的思路一致,先检查string
    table有没有这样的字符串,有的话直接返回对象地址,没有则入池,创建映射

再加入一些编译期优化呢?以下代码摘自Java语言规范8。

package com.hustdj.jdkStudy.threePool;

public class StringPoolDemo {
    public static void main(String[] args) {
        String hello="Hello",lo="lo";
        System.out.println(hello=="Hello");
        System.out.println(Other.hello==hello);
        System.out.println(com.hustdj.jdkStudy.other.Other.hello==hello);
        System.out.println(hello=="Hel"+"lo");
        System.out.println(hello=="Hel"+lo);
        System.out.println(hello==("Hel"+lo).intern());
    }
}

class Other{
    public static String hello="Hello";
}


package com.hustdj.jdkStudy.other;

public class Other {
    public static String hello="Hello";
}

输出结果如下:

true
true
true
true
false
true

解释如下:

//字符串池是JVM层面的,与类、包无关
System.out.println(hello=="Hello");
System.out.println(Other.hello==hello);
System.out.println(com.hustdj.jdkStudy.other.Other.hello==hello);
//编译期优化自动转换成:hello=="Hello"
System.out.println(hello=="Hel"+"lo");
//通过StringBuilder.toString等于:new String("Hello");
System.out.println(hello=="Hel"+lo);
//intern操作时,string pool已经有"Hello"对象了,直接返回相同的引用,可以理解为入池失败
System.out.println(hello==("Hel"+lo).intern());

可见JVM为了减少相同String对象的重复创建还是做了不少努力呀!

Integer缓存

同样是减少重复对象的创建,Integer同样做出了努力,示例代码如下:

public class UnboxingTest {
    public static void main(String[] args) {
        Integer a=1;
        Integer b=1;
        System.out.println(a==b);
    }
}
//输出结果为true

Integer和String难道说采用了同样的策略,Integer池?当然不是,遇事不妨先看看字节码。

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: iconst_1
         1: invokestatic  #16                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: astore_1
         5: iconst_1
         6: invokestatic  #16                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         9: astore_2
        10: getstatic     #22                 // Field java/lang/System.out:Ljava/io/PrintStream;
        13: aload_1
        14: aload_2
        15: if_acmpne     22
        18: iconst_1
        19: goto          23
        22: iconst_0
        23: invokevirtual #28                 // Method java/io/PrintStream.println:(Z)V
        26: return

可以看到Integer a= 1实际的指令应该是Integer a =Integer.valueOf(1)

那么我们来看看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() {}
}

不难发现,在Integer类初始完成之后就已经存在了-128<=value<=127的所有Integer对象,valueOf传入的参数如果在这之间的话直接返回相应的对象即可,并且上限是可以修改的。

此外,Short、Character、Long、Byte、Boolean都是有缓存处理的,而Float、Double没有,它们的valueOf如下:

public static Short valueOf(short s) {
    final int offset = 128;
    int sAsInt = s;
    if (sAsInt >= -128 && sAsInt <= 127) { // must cache
        return ShortCache.cache[sAsInt + offset];
    }
    return new Short(s);
}

public static Character valueOf(char c) {
    if (c <= 127) { // must cache
        return CharacterCache.cache[(int)c];
    }
    return new Character(c);
}

public static Long valueOf(long l) {
    final int offset = 128;
    if (l >= -128 && l <= 127) { // will cache
        return LongCache.cache[(int)l + offset];
    }
    return new Long(l);
}

public static Byte valueOf(byte b) {
    final int offset = 128;
    return ByteCache.cache[(int)b + offset];
}

public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}

public static Float valueOf(float f) {
    return new Float(f);
}

public static Double valueOf(double d) {
    return new Double(d);
}

总结

  • String Pool是JVM层面实现的,Integer这些是Java层面通过静态代码块在类加载的初始化阶段完成的
  • Integer的默认缓存范围为[-128,127],其它详见代码,Float、Double并不提供缓存
  • Integer的缓存上限可扩大,最大为Integer.MAX_VALUE - (-low) -1

常量池的内存分布问题

总结如下:

  • 永久代/方法区也属于GC Heap的一部分
  • SymbolTable / StringTable,这俩table一直在native memory里面
  • JDK6的以永久代(PermGen)作为方法区的实现,除了JIT编译的代码存在native
    memory中以外,其他的方法区的数据都存在永久代中(此时的String Pool中的字符串示例都是在永久代中的)
  • JDK7还是以永久代作为方法区的实现把Symbol的存储从PermGen移动到了native
    memory把静态变量从instanceKlass末尾(位于PermGen内)移动到了java.lang.Class对象的末尾(位于普通Java
    heap内)StringTable引用的java.lang.String实例则从PermGen移动到了普通Java heap
  • JDK8中永久代彻底被移除,用元空间作为方法区的实现

今日份分享已结束,请大家多多包涵和指点!



这篇关于我把Java反编译了给你解析三大常量池,面试官主动为你加薪的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程