您的位置 首页 java

Java常量池及其应用

说起常量池就不得不提方法区,都清楚常量池位于方法区,那么除了常量池,方法区还存储那类信息呢,或者说它俩有什么区别呢?很多人会把常量池和方法区混为一谈,实际上,可以理解为方法区包含多个常量池。这里说个题外话,JDK8的HotSpot虚拟机将方法区数据放到一个叫元空间区域存储,详细区别及原因见下文的参考资料。

常量池从特性上可以分为两类,一类是class文件中的常量池,一类是运行区的常量池。下面逐一来介绍这两个类型的常量池。

class 文件中的常量池

Java 文件编译为 class 文件后,都将产生当前类独有的常量池,它包括了关于类,方法,接口等中的常量,也包括 字符串常量 ,我们称之为静态常量池,它是生成.class文件里面存放的信息。静态常量池根据信息类型有分为两大类: 字面量 (Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本 字符串 ,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

1.类和接口的全限定名

2.字段名称和描述符

3.方法名称和描述符

javap是JDK自带的反汇编器,可以查看java编译器为我们生成的字节码。javap -verbose 命令可以满足查看静态常量池的信息。编写一段代码,来看看其相关静态常量池信息。

/**

* SB Created by xxxx on 2016/11/15 17:14.

*/

public class SB {

public static void main(String[] args) {

String say= “Hi, man ;

String answer= “Fuck you man” ;

}

}

编译完成后执行javap –verbose SB 会生成一个很长的文件,其中包含以下一段信息(由于格式问题,这里直接截图了):

Java常量池及其应用

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。如下代码运行结果证明这些包装类都实现了常量池技术。

Java常量池及其应用

两种浮点数类型的包装类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 = new Integer(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 static Integer valueOf( int i) {

if (i >= IntegerCache. low && i <=IntegerCache. high )

return IntegerCache. cache [i + (-IntegerCache. low )];

returnnew Integer(i);

}

所以内部常量池实现其实是通过一个内部缓存来实现的,但是要注意默认创建了

数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对

象。若想提供上限,可以通过配置文件配置java.lang.Integer.IntegerCache.high的 大小

调整。对于实现源码如下:

// 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.

}

}

来一波关于Integer经常出现比较。

Integer fuck1 = 69;

Integer fuck2 = 69;

Integer fuck3 = 0;

Integer fuck4 = new Integer(69);

Integer fuck5 = new Integer(69);

Integer fuck6 = new Integer(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 = new String( “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是由两个变量拼接而成,所以在编译期并不能确认。来看一下字节码:

Java常量池及其应用

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()方法的讲解:

Java常量池及其应用

这个例子的前提是认为是认为“kvill”这个字符串并不在常量池,是否“kvill”真的不在常量池中么?直接查看String s1=newString(“kvill”);代码的字节码:

Java常量池及其应用

圈出来部分说明,“kvill”在编译期就已经确认了其为在常量池中,且指令码中存在4: ldc #3 // String kvill的指令,所以“kvill”在使用new String(“kvill”)时,“kvill”本身已经存在于常量池中。所以前提是错误的,要使“kvill”不在常量池中,可以使用如下方式:

String str1 = new String( )+ new String( );

String str2 = str1.intern();

System. out .println( “str1==str2:” +(str1==str2));

但是结果并不为false,说明了上述佐证是错误的,那句对方法intern()描述是正确的。

如有错漏之处,欢迎关注公众号” 程序员杂谈 “,留言指正~~~~~

Java常量池及其应用

福利一张,请收下

文章来源:智云一二三科技

文章标题:Java常量池及其应用

文章地址:https://www.zhihuclub.com/195028.shtml

关于作者: 智云科技

热门文章

网站地图