在很多在面试的过程中,只要我们在日常的工作中,做足充分的准备,在面试之前,多深入的学习一下一些基础的知识,我想对于你想要的那个薪资,你就会更近一步。
当然了,结果是肯定很明显的了,这里就不多说什么了,今天的主角是常量池,上面三大常量池,我会按照面试官的需求,直接反编译,然后给大家看一下具体的实现原理。
我们都知道,在大学最一开始学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);
它的过程如下:
通过new创建了一个String对象,此时String Table并没有记录 s.intern(),查看String
Table发现,并没有这样的一个字符串,那么新增记录并且返回对应的地址,即s1指向snew出来的string对象
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同样做出了努力,示例代码如下:
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); }
总结如下:
今日份分享已结束,请大家多多包涵和指点!