java常量池问题图解(Java常量池及其应用)
说起常量池就不得不提方法区,都清楚常量池位于方法区,那么除了常量池,方法区还存储那类信息呢,或者说它俩有什么区别呢?很多人会把常量池和方法区混为一谈,实际上,可以理解为方法区包含多个常量池。这里说个题外话,JDK8的HotSpot虚拟机将方法区数据放到一个叫元空间区域存储,详细区别及原因见下文的参考资料。
常量池从特性上可以分为两类,一类是class文件中的常量池,一类是运行区的常量池。下面逐一来介绍这两个类型的常量池。
class文件中的常量池
Java 文件编译为 class 文件后,都将产生当前类独有的常量池,它包括了关于类,方法,接口等中的常量,也包括字符串常量,我们称之为静态常量池,它是生成.class文件里面存放的信息。静态常量池根据信息类型有分为两大类:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
1.类和接口的全限定名
2.字段名称和描述符
Constant pool信息即为该类的,#4指向#25表示对该类描述,#3 是程序中定义的 String 类型的字面值 "Fuck you man",它包含指向一个utf8编码字符串 "Fuck you man" 的索引 #24。也有描述变了名称的如#17描述了变量的名称为“say”。至于如何读取调用的这就涉及类加载等相关信息了,感兴趣的可以深挖一下。
方法中运行时常量池
静态常量池数据将在类加载后进入方法区的运行时常量池中存放。从内容上说它是包含静态常量池的数据的。和静态常量池相比,运行时常量池可以动态添加常量,具备动态添加功能。开发人员比较熟悉的String类的intern()就可以动态添加字符串常量。如:
Stringstr=new String(“a”) new String(“b”);
str.intern();
说了那么多,那么常量池有什么好处呢?常量池避免频繁的创建和销毁对象而影响系统性能,在创建重复对象节省了时间,由于具有共享性,也节省了内存空间。
Java的基本类型的包装类绝大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean。如下代码运行结果证明这些包装类都实现了常量池技术。
两种浮点数类型的包装类Float,Double并没有实现常量池技术。如下代码输出结果均为false。
Double dl1=1.2;
Double dl2=1.2;
System.out.println("dl1==dl2:" (dl1==dl2));//dl1==dl2:false
Float ft1=1.2f;
Float ft2=1.2f;
System.out.println("ft1==ft2" (ft1==ft2));//ft1==ft2:false
由于上述五种基本类型的包装类实现的常量池技术都类似,这里挑选Integer实现细节来分析。
Integer i1 = 88;
Integer i2 = newInteger(88);
System.out.println("i1==i2:" (i1==i2));//i1==i2:false
这个原因很好理解,i1的88来自于常量池,而i2是新创建对象。而“==”比较的又是引用所指向的地址,所以结果为false。那么i1是如何获取常量池里面数据呢,还是javap命令,通过javap -c 可以查看到class文件生成的JVM指令码。格式问题这里我直接截图。
bipush将一个88常量值推送至栈顶,invokestatic调Integer.valueOf(int i)静态方法。也就是可以理解成Java在编译的时候会直接将Integer i1 = 88封装成Integer i1=Integer.valueOf(88)。而valueOf源码如下:
public staticInteger valueOf(inti) {
if(i >= IntegerCache.low&& i <=IntegerCache.high)
returnIntegerCache.cache[i (-IntegerCache.low)];
returnnewInteger(i);
}
所以内部常量池实现其实是通过一个内部缓存来实现的,但是要注意默认创建了
数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对
象。若想提供上限,可以通过配置文件配置java.lang.Integer.IntegerCache.high的 大小
调整。对于实现源码如下:
// high value may be configured by property
inth = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if(integerCacheHighPropValue != null) {
try{
inti = 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.
}
}
来一波关于Integer经常出现比较。
Integer fuck1 = 69;
Integer fuck2 = 69;
Integer fuck3 = 0;
Integer fuck4 = newInteger(69);
Integer fuck5 = newInteger(69);
Integer fuck6 = newInteger(0);
System.out.println("fuck1=fuck2: " (fuck1 == fuck2));//fuck1=fuck2:true
System.out.println("fuck1=fuck2 fuck3: " (fuck1 == fuck2 fuck3));//fuck1=fuck2 fuck3:true
System.out.println("fuck1=fuck4: " (fuck1 ==fuck4));//fuck1=fuck4: false
System.out.println("fuck4=fuck5: " (fuck4 ==fuck5));//fuck4=fuck5: false
System.out.println("fuck4=fuck5 fuck6: " (fuck4 == fuck5 fuck6));//fuck4=fuck5 fuck6:true
System.out.println("69=fuck5 fuck6: " (69 == fuck5 fuck6));//69=fuck5 fuck6: true
这里直接贴出运行的结果,其他结果都比较好理解,这里分析fuck4=fuck5 fuck6 的原因。对于该语句, 这个操作符不适用于Integer对象, 可以作用于int,所以fuck5和fuck6会进行自动拆箱操作,变成对应的int基本类型,进行相加。于是就会变成了fuck4==69,这个时候问题又来了,fuck4这个时候是个Integer对象,而右边是一个int类型的数据,是无法进行判等操作的,没办法fuck4也只能脱掉裤子,进行拆箱操作。所以最终比较的是 69==69,结果显然为true。关于自动拆箱和装箱操作见参靠资料。
String也实现了常量池技术,相比较以上五种类型的实现方式,String类若在编译期成class文件时候,会直接把明确的字符串加入到常量池中,上述关于静态常量池介绍局的例子中,可以看到 say和answer对应的字符串都是属于常量池的,当然也可以通过java.lang.String.intern()动态添加常量字符串。先看一个关于String几个常见的例子:
String str1 = "man";
String str2 = newString("man");
System.out.println("str1==str2:" (str1==str2));//str1==str2:false
str1直接在常量池中拿取对象,而str2相当于直接在堆内存空间创建新的对象。
使用了new 就会伴随新对象的产生。继续看使用“ ”操作的例子:
String str1 = "蛤";
String str2 = "蟆";
String str3 = "蛤" "蟆";
String str4 = str1 str2;
String str5 = "蛤蟆";
System.out.println("str3 == str4:" (str3 == str4));//str3 == str4:false
System.out.println("str3 == str5:" (str3 == str5));//str3 == str5:true
str3和str5相等原因是,str3的值有两个引号的文本使用 操作而成,所以str3会在编译器就会确认str3的值,所以其值也是来自常量池。str4是由两个变量拼接而成,所以在编译期并不能确认。来看一下字节码:
ldc将int、float或String型常量值从常量池中推送至栈顶,astore index或者astore_index将栈顶数值存入当前栈帧的局部变量数组中指定下标(index)处的变量中,栈顶数值出栈。依据以上指令含义,astore_1, astore_2, astore_3分别为变量str1,str2,str3赋值,且值都来自与常量池。str1 str2编译器在这里做了优化,不是使用直接的new,从JDK5开始,字符串变量的 操作,会默认使用StringBuilder,所以有时候使用显示的StringBuilder进行操作字符串 操作,和直接使用 操作是没有区别的。所以对于以下例子,根据上文分析后者性能还没有直接 的性能高:
String str3 = "蛤" "蟆";String str6=new StringBuilder().append("蛤").append("蟆").toString();
关于String类的intern()方法就以一篇博客中错误为例来讲解。看以下图片中关于intern()方法的讲解:
这个例子的前提是认为是认为“kvill”这个字符串并不在常量池,是否“kvill”真的不在常量池中么?直接查看String s1=newString("kvill");代码的字节码:
圈出来部分说明,“kvill”在编译期就已经确认了其为在常量池中,且指令码中存在4: ldc #3 // String kvill的指令,所以“kvill”在使用new String(“kvill”)时,“kvill”本身已经存在于常量池中。所以前提是错误的,要使“kvill”不在常量池中,可以使用如下方式:
String str1 = newString("蛤") newString("蟆");
String str2 = str1.intern();
System.out.println("str1==str2:" (str1==str2));
但是结果并不为false,说明了上述佐证是错误的,那句对方法intern()描述是正确的。
如有错漏之处,欢迎关注公众号"程序员杂谈",留言指正~~~~~
福利一张,请收下
,
免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com