JVM有几种常量池
主要分为: Class文件常量池、运行时常量池,全局字符串常量池,以及基本类型包装类对象常量池。
Class文件常量池
class文件是一组以字节为单位的二进制数据流,在java代码的编译期间,我们编写的java文件就被编译为.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池。 class文件中存在常量池(非运行时常量池),其在编译阶段就已经确定,jvm规范对class文件结构有着严格的规范,必须符合此规范的class文件才能被jvm任何和装载。为了方便说明,我们写个简单的类
class JavaBean{ private int value = 1; public String s = "abc"; public final static int f = 0x101; public void setValue(int v){ final int temp = 3; this.value = temp v; } public int getValue(){ return value; } }
通过javac命令编译之后,用javap -v 命令查看编译后的文件:
class JavaBasicKnowledge.JavaBean minor version: 0 major version: 52 flags: ACC_SUPER Constant pool: #1 = Methodref #6.#29 // java/lang/Object."<init>":()V #2 = Fieldref #5.#30 // JavaBasicKnowledge/JavaBean.value:I #3 = String #31 // abc #4 = Fieldref #5.#32 // JavaBasicKnowledge/JavaBean.s:Ljava/lang/String; #5 = Class #33 // JavaBasicKnowledge/JavaBean #6 = Class #34 // java/lang/Object #7 = Utf8 value #8 = Utf8 I #9 = Utf8 s #10 = Utf8 Ljava/lang/String; #11 = Utf8 f #12 = Utf8 ConstantValue #13 = Integer 257 #14 = Utf8 <init> #15 = Utf8 ()V #16 = Utf8 Code #17 = Utf8 LineNumberTable #18 = Utf8 LocalVariableTable #19 = Utf8 this #20 = Utf8 LJavaBasicKnowledge/JavaBean; #21 = Utf8 setValue #22 = Utf8 (I)V #23 = Utf8 v #24 = Utf8 temp #25 = Utf8 getValue #26 = Utf8 ()I #27 = Utf8 SourceFile #28 = Utf8 StringConstantPool.java #29 = NameAndType #14:#15 // "<init>":()V #30 = NameAndType #7:#8 // value:I #31 = Utf8 abc #32 = NameAndType #9:#10 // s:Ljava/lang/String; #33 = Utf8 JavaBasicKnowledge/JavaBean #34 = Utf8 java/lang/Object
可以看到这个命令之后我们得到了该class文件的版本号、常量池、已经编译后的字节码(这里未列出)。既然是常量池,那么其中存放的肯定是常量,那么什么是“常量”呢? class文件常量池主要存放两大常量:字面量和符号引用。
1) 字面量: 字面量接近java语言层面的常量概念,主要包括:
- 文本字符串,也就是我们经常申明的: public String s = "abc";中的"abc"
#9 = Utf8 s #3 = String #31 // abc #31 = Utf8 abc
- 用final修饰的成员变量,包括静态变量、实例变量和局部变量
#11 = Utf8 f #12 = Utf8 ConstantValue #13 = Integer 257
这里需要说明的一点,上面说的存在于常量池的字面量,指的是数据的值,也就是abc和0x101(257),通过上面对常量池的观察可知这两个字面量是确实存在于常量池的。
image.png
这张图是我们理解的jvm运行时数据区的结构,但是还有不完整的地方,
image.png
这张图中,可以看到,方法区实际上是在一块叫“非堆”的区域包含——可以简单粗略的理解为非堆中包含了永生代,而永生代中又包含了方法区和字符串常量池
image.png
其中的Interned String就是全局共享的“字符串常量池(String Pool)”,和运行时常量池不是一个概念。但我们在代码中申明String s1 = "Hello";这句代码后,在类加载的过程中,类的class文件的信息会被解析到内存的方法区里。
class文件里常量池里大部分数据会被加载到“运行时常量池”,包括String的字面量;但同时“Hello”字符串的一个引用会被存到同样在“非堆”区域的“字符串常量池”中,而"Hello"本体还是和所有对象一样,创建在Java堆中。
当主线程开始创建s1时,虚拟机会先去字符串池中找是否有equals(“Hello”)的String,如果相等就把在字符串池中“Hello”的引用复制给s1;如果找不到相等的字符串,就会在堆中新建一个对象,同时把引用驻留在字符串池,再把引用赋给str。
当用字面量赋值的方法创建字符串时,无论创建多少次,只要字符串的值相同,它们所指向的都是堆中的同一个对象。
字符串常量池的本质
字符串常量池是JVM所维护的一个字符串实例的引用表,在HotSpot VM中,它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用,底层C 实现就是一个Hashtable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。
强调一下:运行时常量池在方法区(Non-heap),而JDK1.7后,字符串常量池被移到了heap区,因此两者根本就不是一个概念
String"字面量" 是何时进入字符串常量池的?
先说结论: 在执行ldc指令时,该指令表示int、float或String型常量从常量池推送至栈顶
JVM规范里Class文件的常量池项的类型,有两种东西:
- CONSTANT_Utf8_info
- CONSTANT_String_info
在HotSpot VM中,运行时常量池里,CONSTANT_Utf8_info可以表示Class文件的方法、字段等等,其结构如下:
image.png
首先是1个字节的tag,表示这是一个CONSTANT_Utf8_info结构的常量,然后是两个字节的length,表示要储存字节的长度,之后是一个字节的byte数组,表示真正的储存的length个长度的字符串。这里需要注意的是,一个字节只是代表这里有一个byte类型的数组,而这个数组的长度当然可以远远大于一个字节。当然,由于CONSTANT_Utf8_info结构只能用u2即两个字节来表示长度,因此长度的最大值为2byte,也就是65535
CONSTANT_String_info是String常量的类型,但它并不直接持有String常量的内容,而是只持有一个index,这个index所指定的另一个常量池项必须是一个CONSTANT_Utf8类型的常量,这里才真正持有字符串的内容
image.png
CONSTANT_Utf8会在类加载的过程中就全部创建出来,而CONSTANT_String则是lazy resolve的,在第一次引用该项的ldc指令被第一次执行到的时候才会resolve。在尚未resolve的时候,HotSpot VM把它的类型叫做JVM_CONSTANT_UnresolvedString,内容跟Class文件里一样只是一个index;等到resolve过后这个项的常量类型就会变成最终的JVM_CONSTANT_String
也就是说,就HotSpot VM的实现来说,加载类的时候,那些字符串字面量会进入到当前类的运行时常量池,不会进入全局的字符串常量池(即在StringTable中并没有相应的引用,在堆中也没有对应的对象产生),在执行ldc指令时,触发lazy resolution这个动作:
ldc字节码在这里的执行语义是:到当前类的运行时常量池(runtime constant pool,HotSpot VM里是ConstantPool ConstantPoolCache)去查找该index对应的项,如果该项尚未resolve则resolve之,并返回resolve后的内容。
在遇到String类型常量时,resolve的过程如果发现StringTable已经有了内容匹配的java.lang.String的引用,则直接返回这个引用,反之,如果StringTable里尚未有内容匹配的String实例的引用,则会在Java堆里创建一个对应内容的String对象,然后在StringTable记录下这个引用,并返回这个引用出去
可见,ldc指令是否需要创建新的String实例,全看在第一次执行这一条ldc指令时,StringTable是否已经记录了一个对应内容的String的引用。
String.intern()用法
String.intern()官方给的定义:
When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.
实际上,就是去拿String的内容去Stringtable里查表,如果存在,则返回引用,不存在,就把该对象的"引用"存在Stringtable表里。
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。
首先我们调用StringBuilder创建了一个"计算机软件"String对象,因为调用了new关键字,因此是在运行时创建,之前JVM中是没有这个字符串的。
在 JDK6 下,intern()会把首次遇到的字符串实例复制到永久代中,返回的也是这个永久代中字符串实例的引用;而在JDK1.7开始,intern()方法不再复制字符串实例,String 的 intern 方法首先将尝试在常量池中查找该对象的引用,如果找到则直接返回该对象在常量池中的引用地址
因此在1.7中,“计算机软件”这个字符串实例只存在一份,存在于java堆中!通过3中的分析,我们知道当String str1 = new StringBuilder("计算机").append("软件").toString();这句代码执行完之后,已经在堆中创建了一个字符串对象,并且在全局字符串常量池中保留了这个字符串的引用,那么str1.intern()直接返回这个引用,这当然满足str1.intern() == str1——都是他自己嘛;对于引用str2,因为JVM中已经有“java”这个字符串了,因此new StringBuilder("ja").append("va").toString()会重新创建一个新的“java”字符串对象,而intern()会返回首次遇到的常量的实例引用,因此他返回的是系统中的那个"java"字符串对象引用(首次),因此会返回false
在 JDK6 下 str1、str2 指向的是新创建的对象,该对象将在 Java Heap 中创建,所以 str1、str2 指向的是 Java Heap 中的内存地址;调用 intern 方法后将尝试在常量池中查找该对象,没找到后将其放入常量池并返回,所以此时 str1/str2.intern() 指向的是常量池中的地址,JDK6常量池在永久代,与堆隔离,所以 s1.intern()和s1 的地址当然不同了。
public class Test2 { public static void main(String[] args) { /** * 首先设置 持久代最大和最小内存占用(限定为10M) * VM args: -XX:PermSize=10M -XX:MaxPremSize=10M */ List<String> list = new ArrayList<String>(); // 无限循环 使用 list 对其引用保证 不被GC intern 方法保证其加入到常量池中 int i = 0; while (true) { // 此处永久执行,最多就是将整个 int 范围转化成字符串并放入常量池 list.add(String.valueOf(i ).intern()); } } }
以上代码在 JDK6 下会出现 Perm 内存溢出,JDK7 or high 则没问题。
JDK6 常量池存在持久代,设置了持久代大小后,不断while循环必将撑满 Perm 导致内存溢出;JDK7 常量池被移动到 Native Heap(Java Heap,HotSpot VM中不区分native堆和Java堆),所以即使设置了持久代大小,也不会对常量池产生影响;不断while循环在当前的代码中,所有int的字符串相加还不至于撑满 Heap 区,所以不会出现异常。
JAVA 基本类型的封装类及对应常量池
java中基本类型的包装类的大部分都实现了常量池技术,这些类是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 } }
在JDK5.0之前是不允许直接将基本数据类型的数据直接赋值给其对应地包装类的,如:Integer i = 5; 但是在JDK5.0中支持这种写法,因为编译器会自动将上面的代码转换成如下代码:Integer i=Integer.valueOf(5);这就是Java的装箱.JDK5.0也提供了自动拆箱:Integer i =5; int j = i;
以上就是我的分享,感谢各位大佬们耐心看完文章,觉得有所收获的朋友们可以点个关注转发收藏一下哦,感谢支持!
,