Java是一种
动态连接的语言,常量池的做用很是重要,常量池中除了包含代码中所定义的各类基本类型(如int、long等等)和对象型(如String及
数组)的常量值外,还包含一些以
文本形式出现的符号引用,好比:
类和接口的全限定名;
字段的名称和描述符;
方法的名称和描述符。
在C语言中,若是一个程序要调用其它库中的函数,在连接时,该函数在库中的位置(即相对于
库文件开头的
偏移量)会被写在程序中,在运行时,直接去这个地址调用函数;
而在Java语言中不是这样,一切都是动态的。编译时,若是发现对其它类方法的调用或者对其它类字段的引用的语句,记录进class文件中的只能是一个
文本形式的符号引用,在链接过程当中,
虚拟机根据这个文本信息去查找对应的方法或字段。
因此,与Java语言中的所谓“常量”不一样,class文件中的“常量”内容很非富,这些常量集中在class中的一个区域存放,一个紧接着一个,这里就称为“常量池”。
在Java程序中,有不少的东西是永恒的,不会在运行过程当中变化。好比一个类的名字,一个类字段的名字/所属类型,一个类方法的名字/返回类型/参数名与所属类型,一个常量,还有在程序中出现的大量的字面值。
好比下面小段源码中粗体代码显示的部分:
public class
ClassTest {
private
String itemS ="咱们 ";
private final
int itemI =100 ;
public void
setItemS (String para ){...}
}
而这些在JVM
解释执行程序的时候是很是重要的。那么
编译器将源程序编译成class文件后,会用一部分字节分类
存储这些粗体代码。而这些字节咱们就称为常量池。事实上,只有JVM
加载class后,在方法区中为它们开辟了空间才更像一个“池”。
正如上面所示,一个程序中有不少永恒的相似粗体代码显示的部分。每个都是常量池中的一个常量表(常量项)。而这些常量表之间又有不一样,class文件共有11种常量表,以下所示:
常量表类型
|
标志值(占1 byte)
|
描述
|
CONSTANT_Utf8
|
1
|
|
CONSTANT_Integer
|
3
|
int类型的字面值
|
CONSTANT_Float
|
4
|
float类型的字面值
|
CONSTANT_Long
|
5
|
long类型的字面值
|
CONSTANT_Double
|
6
|
double类型的字面值
|
CONSTANT_Class
|
7
|
对一个类或接口的符号引用
|
CONSTANT_String
|
8
|
String类型字面值的引用
|
CONSTANT_Fieldref
|
9
|
对一个字段的符号引用
|
CONSTANT_Methodref
|
10
|
对一个类中方法的符号引用
|
CONSTANT_InterfaceMethodref
|
11
|
对一个接口中方法的符号引用
|
CONSTANT_NameAndType
|
12
|
对一个字段或方法的部分符号引用
|
(1) CONSTANT_Utf8 用UTF-8编码方式来表示程序中全部的重要常量字符串。这些字符串包括: ①类或接口的全限定名, ②超类的全限定名,③父接口的全限定名, ④类字段名和所属类型名,⑤类方法名和返回类型名、以及参数名和所属类型名。⑥字符串字面值
表格式: tag(标志1:占1byte) length(字符串所占字节的长度,占2byte) bytes(字符串字节序列)
(2) CONSTANT_Integer、 CONSTANT_Float、 CONSTANT_Long、 CONSTANT_Double 全部基本数据类型的字面值。好比在程序中出现的1用CONSTANT_Integer表示。3.1415926F用 CONSTANT_Float表示。
表格式: tag bytes(基本数据类型所需使用的字节序列)
(3) CONSTANT_Class 使用符号引用来表示类或接口。咱们知道全部类名都以 CONSTANT_Utf8表的形式存储。可是咱们并不知道 CONSTANT_Utf8表中哪些字符串是类名,那些是方法名。所以咱们必须用一个指向类名字符串的符号引用常量来代表。
表格式: tag name_index(给出表示类或接口名的CONSTANT_Utf8表的索引)
(4) CONSTANT_String 同 CONSTANT_Class,指向包含字符串字面值的 CONSTANT_Utf8表。
表格式: tag string_index(给出表示字符串字面值的CONSTANT_Utf8表的索引)
(5) CONSTANT_Fieldref 、 CONSTANT_Methodref、 CONSTANT_InterfaceMethodref 指向包含该字段或方法所属类名的 CONSTANT_Utf8表,以及指向包含该字段或方法的名字和描述符的 CONSTANT_NameAndType 表
表格式: tag class _index(给出包含所属类名的CONSTANT_Utf8表的索引) name_and_type_index(包含字段名或方法名以及描述符的 CONSTANT_NameAndType表 的索引)
(6) CONSTANT_NameAndType 指向包含字段名或方法名以及描述符的 CONSTANT_Utf8表。
表格式: tag name_index(给出表示字段名或方法名的CONSTANT_Utf8表的索引) type_index(给出表示描述符的CONSTANT_Utf8表的索引)
在Java源代码中的每个字面值字符串,都会在编译成class文件阶段,造成标志号为8(CONSTANT_String_info)的常量表 。 当JVM加载 class文件的时候,会为对应的常量池创建一个内存数据结构,并存放在方法区中。同时JVM会自动为CONSTANT_String_info常量表中的字符串常量的字面值 在堆中建立新的String对象(intern字符串对象 ,又叫拘留字符串对象)。而后把CONSTANT_String_info常量表的入口地址转变成这个堆中String对象的直接地址(常量池解析)。
拘留字符串对象
源代码中全部相同字面值的字符串常量只可能创建惟一 一个拘留字符串对象。 实际上JVM是经过一个记录了拘留字符串引用的内部数据结构来维持这一特性的。在Java程序中,能够调用String的intern()方法来使得一个常规字符串对象成为拘留字符串对象。
(1)String s=new String("Hello world"); 编译成class文件后的指令(在
myeclipse中查看):
事实上,在运行这段指令以前,JVM就已经为"Hello world"在堆中建立了一个拘留字符串( 值得注意的是:若是源程序中还有一个"Hello world"字符串常量,那么他们都对应了同一个堆中的拘留字符串)。而后用这个拘留字符串的值来初始化堆中用new指令建立出来的新的String对象,局部变量s实际上存储的是new出来的堆对象地址。
(2)String s="Hello world";
这跟(1)中建立指令有很大的不一样,此时局部变量s存储的是早已建立好的拘留字符串的堆地址。
java常量池技术 java中的常量池技术,是为了方便快捷地建立某些对象而出现的,当须要一个对象时,就能够从池中取一个出来(若是池中没有则建立一个),则在须要重复建立相等变量时节省了不少时间。常量池其实也就是一个内存空间,常量池存在于方法区中。
String类也是java中用得多的类,一样为了建立String对象的方便,也实现了常量池的技术。
测试代码以下
:
public class Test{
public static void main(String[] args){
//s1,s2分别位于堆中不一样空间
String s1=new String("hello");
String s2=new String("hello");
System.out.println(s1==s2);//输出false
//s3,s4位于池中同一空间
String s3="hello" String s4="hello";
System.out.println(s3==s4);//输出true
}
}
用new String()建立的字符串不是常量,不能在编译期就肯定,因此new String()建立的字符串不放入常量池中,他们有本身的地址空间。
String 对象(内存)的不变性机制会使修改String字符串时,产生大量的对象,由于每次改变字符串,都会生成一个新的String。 java 为了更有效的使用内存,常量池在编译期碰见String 字符串时,它会检查该池内是否已经存在相同的String 字符串,若是找到,就把新变量的引用指向现有的字符串对象,不建立任何新的String 常量对象,没找到再建立新的。因此对一个字符串对象的任何修改,都会产生一个新的字符串对象,原来的依然存在,等待垃圾回收。
代码:
String a = “test”;
String b = “test”;
String b = b+"java";
a,b同时指向常量池中的常量值"text",b=b+"java"以后,b原先指向一个常量,内容为"test”,经过对b进行+"java" 操做后,b以前所指向的那个值没有改变,但此时b不指向原来那个变量值了,而指向了另外一个String变量,内容为”text java“。原来那个变量还存在于内存之中,只是b这个变量再也不指向它了。
八种基本类型的包装类和对象池 java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可以使用常量池,也即对象不负责建立和管理小于127的这些类的对象。 一些对应的测试代码:
public class Test{ public static void main(String[] args){
//5种整形的包装类Byte,Short,Integer,Long,Character的对象,
//在值小于127时可使用常量池
Integer i1=127;
Integer i2=127;
System.out.println(i1==i2); //输出true
//值大于127时,不会从常量池中取对象
Integer i3=128;
Integer i4=128;
System.out.println(i3==i4); //输出false
//Boolean类也实现了常量池技术
Boolean bool1=true;
Boolean bool2=true;
System.out.println(bool1==bool2); //输出true
//浮点类型的包装类没有实现常量池技术
Double d1=1.0;
Double d2=1.0;
System.out.println(d1==d2); //输出false
}
}
对Integer对象的代码补充
public static Integer valueOf(int i) {
final int offset = 128;
if (i >= -128 && i <= 127) {
return IntegerCache.cache[i + offset];
}
return new Integer(i);
}
当你直接给一个Integer对象一个int值的时候,其实它调用了valueOf方法,而后你赋的这个值很特别,是128,那么没有进行cache方法,至关于new了两个新对象。因此问题中定义a、b的两句代码就相似于:
Integer a = new Integer(128);
Integer b = new Integer(128);
这个时候再问你,输出结果是什么?你就知道是false了。若是把这个数换成127,再执行:
Integer a = 127;
Integer b = 127;
System.out.println(a == b);
结果就是:true
进行对象比较时最好仍是使用equals,便于按照本身的目的进行控制。这里引出equals()和==,equals比较的是字符串字面值即比较内容,==比较引用。
看一下IntegerCache这个类里面的内容
:
private static class IntegerCache {
private IntegerCache() {
}
static final Integer cache[] = new Integer[-(-128) + 127 + 1];
static {
for (int i = 0; i < cache.length; i++)
cache[i] = new Integer(i - 128);
}
}
因为cache[]在IntegerCache类中是静态数组,也就是只须要
初始化一次,即static{......}部分,因此,若是Integer对象初始化时是-128~127的范围,就不须要再从新定义申请空间,都是同一个对象---在IntegerCache.cache中,这样能够在必定程度上提升效率。
针对String方面的补充
在同包同类下,引用自同一String对象.
在同包不一样类下,引用自同一String对象.
在不一样包不一样类下,依然引用自同一String对象.
在编译成.class时可以识别为同一字符串的,自动优化成常量,因此也引用自同一String对象.
在运行时建立的字符串具备独立的内存地址,因此不引用自同一String对象.
String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,
若是有则返回一个引用,没有则添加本身的字符串进入常量池,注意:只是字符串部分。
因此这时会存在2份拷贝,常量池的部分被String类私有并管理,本身的那份按对象生命周期继续使用。
返回字符串对象的规范化表示形式
一个初始值为空的字符串池,它由类 String 私有地维护。
当调用 intern 方法时,若是池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法肯定),则返回池中的字符串。不然,将此 String 对象添加到池中,而且返回此 String 对象的引用。
它遵循对于任何两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。
全部字面值字符串和字符串赋值常量表达式都是内部的。
------------------------------------代码演示补充-------------------------------------
String s0= "java";
String s1=new String("java");
String s2=new String("java");
s1.intern();
s2=s2.intern(); //把常量池中"java"的引用赋给s2
System.out.println( s0==s1);//false “ intern返回的引用没有引用变量接收~ s1.intern();等于废代码.”
System.out.println( s0==s1.intern() );//true
System.out.println( s0==s2 );//true
------------------------------------代码演示补充-------------------------------------
String s1=new String("java");
String s2=s1.intern();//s1 检查常量池,发现没有就拷贝本身的字符串进去
//s2 引用该字符串常量池的地址
System.out.println(s2 == s1);//false
System.out.println( s2==s1.intern());//true
System.out.println( s1==s1.intern());// false