目录
Class文件常量池:
package test;
public class HelloWorld {
public String s = "abc";
public final static int i = 32767;
private int value = 1;
public final static int f = 0x101;
public static void main(String[] args)
{
Integer integer = 1122334455;
System.out.println("Hello3");
}
}
当java文件被编译成class文件之后,会生成class常量池,通过javac命令编译某个java类之后,用javap -v 命令查看编译后的文件,可以看到该class文件的版本号、常量池、以及编译后的字节码:(此处只放了常量池)
Constant pool:
#1 = Methodref #11.#36 // java/lang/Object."<init>":()V
#2 = String #37 // abc
#3 = Fieldref #10.#38 // test/HelloWorld.s:Ljava/lang/String;
#4 = Fieldref #10.#39 // test/HelloWorld.value:I
#5 = Integer 1122334455
#6 = Methodref #40.#41 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#7 = Fieldref #42.#43 // java/lang/System.out:Ljava/io/PrintStream;
#8 = String #44 // Hello3
#9 = Methodref #45.#46 // java/io/PrintStream.println:(Ljava/lang/String;)V
#10 = Class #47 // test/HelloWorld
#11 = Class #48 // java/lang/Object
#12 = Utf8 s
#13 = Utf8 Ljava/lang/String;
#14 = Utf8 i
#15 = Utf8 I
#16 = Utf8 ConstantValue
#17 = Integer 32767
#18 = Utf8 value
#19 = Utf8 f
#20 = Integer 257
#21 = Utf8 <init>
#22 = Utf8 ()V
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 LocalVariableTable
#26 = Utf8 this
#27 = Utf8 Ltest/HelloWorld;
#28 = Utf8 main
#29 = Utf8 ([Ljava/lang/String;)V
#30 = Utf8 args
#31 = Utf8 [Ljava/lang/String;
#32 = Utf8 integer
#33 = Utf8 Ljava/lang/Integer;
#34 = Utf8 SourceFile
#35 = Utf8 HelloWorld.java
#36 = NameAndType #21:#22 // "<init>":()V
#37 = Utf8 abc
#38 = NameAndType #12:#13 // s:Ljava/lang/String;
#39 = NameAndType #18:#15 // value:I
#40 = Class #49 // java/lang/Integer
#41 = NameAndType #50:#51 // valueOf:(I)Ljava/lang/Integer;
#42 = Class #52 // java/lang/System
#43 = NameAndType #53:#54 // out:Ljava/io/PrintStream;
#44 = Utf8 Hello3
#45 = Class #55 // java/io/PrintStream
#46 = NameAndType #56:#57 // println:(Ljava/lang/String;)V
#47 = Utf8 test/HelloWorld
#48 = Utf8 java/lang/Object
#49 = Utf8 java/lang/Integer
#50 = Utf8 valueOf
#51 = Utf8 (I)Ljava/lang/Integer;
#52 = Utf8 java/lang/System
#53 = Utf8 out
#54 = Utf8 Ljava/io/PrintStream;
#55 = Utf8 java/io/PrintStream
#56 = Utf8 println
#57 = Utf8 (Ljava/lang/String;)V
一般包括下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
class文件常量池主要存放两大类常量:字面量和符号引用
字面量:也就是我们经常申明的: public String s = "abc";中的"abc"
用final修饰的基本类型变量,包括静态变量、实例变量和局部变量而对于基本类型数据(甚至是方法中的局部变量),也就是上面的private int value = 1;常量池中只保留了他的的字段描述符I和字段的名称value,他们的字面量不会存在于常量池。(但当int值大于等于32768时也会存在于常量池,原因待分析)
如图,CONSTANT_Utf8_info可以表示Class文件的方法、字段等等,其结构如下
首先是1个字节的tag,表示这是一个CONSTANT_Utf8_info结构的常量,然后是两个字节的length,表示要储存字节的长度(长度的最大值为2byte,也就是65535),之后是一个字节的byte数组,表示真正的储存的length个长度的字符串。
符号引用:
符号引用是一组符号来描述所引用的目标,如CONSTANT_String_info是String常量的类型,但它并不直接持有String常量的内容,而是只持有一个index,这个index所指定的另一个常量池项必须是一个CONSTANT_Utf8类型的常量,这里才真正持有字符串的内容
常量类型:
常量池的每一项常量都是一个表,一共有如下表所示的11种各不相同的表结构数据,这每个表开始的第一位都是一个字节的标志位(取值1-12),代表当前这个常量属于哪种常量类型。
运行时常量池:
运行时常量池是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。逻辑上运行时常量池包含了class文件常量池、字符串常量池、基本类型包装类对象常量池,这里描述主要针对的是class文件常量池的运行时表现形式。
类加载过程中必须经过加载、链接(验证、准备、解析)、初始化环节,其中第一步加载的时候需要完成:
- 通过一个类的全限定名来获取此类的二进制字节流;
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;(包含了class文件常量池进入运行时常量池的过程)
- 在内存中生成一个类对象,代表加载的这个类,这个对象是java.lang.Class,它作为方法区这个类的各种数据访问的入口。
以上第2步会将类里的字面量和符号引用放入运行时常量池。运行时常量池中保存着一些class文件中描述的符号引用,同时在类的解析阶段还会将这些符号引用翻译出直接引用(直接指向实例对象的指针,内存地址)。
即当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。解析的过程会去查询全局字符串池,也就是StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。
全局字符串常量池:
字符串常量池自jdk1.7开始存于堆中,在JVM中只有一份,存放的是字符串常量的引用值。
在解析阶段或执行阶段,堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象), 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),被所有的类共享。
String.intern:
jdk1.7:拿String的内容去Stringtable里查表,如果存在,则返回引用,不存在,就把该对象的"引用"存在Stringtable表里。
jdk1.6:拿String的内容去Stringtable里查表,如果存在,则返回引用,不存在,就把堆中该字符串实例复制到永久代,并把永久代里的对象的"引用"存在Stringtable表里。
在 JDK6 下,intern()会把首次遇到的字符串实例复制到永久代中,返回的也是这个永久代中字符串实例的引用;而在JDK1.7开始,字符串常量池移入堆区,intern()方法不再复制字符串实例,String 的 intern 方法首先将尝试在常量池中查找该对象的引用,如果找到则直接返回该对象在常量池中的引用地址,找不到将直接把该对象引用存入字符串常量池并返回。
public class RuntimeConstantPoolOOM{
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}
以上代码,在 JDK6 下执行结果为 false、false,在 JDK7 以上执行结果为 true、false。
- 在 JDK 1.6 中,调用 intern() 首先会在字符串池中寻找 equal() 相等的字符串,假如字符串存在就返回该字符串在字符串池中的引用;假如字符串不存在,虚拟机会重新在永久代上创建一个实例,将 StringTable 的一个表项指向这个新创建的实例。
- 在 JDK 1.7 中,由于字符串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的对象。字符串存在时和 JDK 1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例。
由上面两个图,也不难理解为什么 JDK 1.6 字符串池溢出会抛出 OutOfMemoryError: PermGen space ,而在 JDK 1.7 抛出 OutOfMemoryError: Java heap space 。
何时创建String对象:
- 生成一个字符串首先判断字符串常量池是否有这么一个字符串,没有则会在常量池放入该字符串,再进行接下来的操作。
- 对于new String("ab")这样的一个操作会生成的对象个数可能是1,也可能是2,如果常量池中有“ab”字符串,那么就直接在堆分配一块内存,此时创建了一个对象存放“ab”,否则,首先在方法区的字符串常量池分配内存存放“ab”,然后在堆分配一块内存,存放“ab”,此时创建了两个对象。
- 对于String a="ab"这样的一个操作,生成的对象个数可能是0,也可能是1,原因同上。
String test1 = "hello"+"world"; //编译时直接优化为"helloworld",字符串常量池不会创建"hello"和"world",而直接创建"helloworld"
String hello = "hello"; //字符串常量池创建"hello"
String test2 = hello + "world"; //字符串常量池创建"world",堆中创建一个新的"helloworld"
System.out.println(test2.intern() == test2); //false,证明了上面的结论,由于常量池中已存在"helloworld",因此跟test2是不同的引用
System.out.println(test1 == test2); //false, test2是后来堆中新建的一个对象
String test3 = new String("hello")+new String("world1"); //常量池创建hello1,堆中创建"helloworld1"
System.out.println(test3.intern() == test3); //true, test3为堆里的首个"helloworld1",intern后其引用存入常量池,因此引用为同一个
字符串拼接:
字符串拼接使用+,从jdk 5开始,Java就对String字符串的+操作优化为StringBuilder.append(), 但是使用+每次都会new一个新的StringBuilder对象,因此当循环次数多时,效率会低很多。
基本类型包装类对象常量池:
java中部分基本类型的包装实现了常量池技术,类似于String,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。
上面这5种整型的包装类也只是在对应值小于等于127时才可使用对象常量池,也即对象常量池不负责创建和管理大于127的这些类的对象。
public class StringConstantPool{
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
}
}