jvm-运行时数据区(运行时常量池、字符串常量池)

文章目录

      • 运行时常量池和字符串常量池
        • 存储内容
        • 存储位置
        • 常量池区别
        • 字符串常量池如何存储数据
        • 字符串常量池简介
        • 字符串常量池案例分析
          • 案例
          • 分析一
          • 分析二
          • 分析三
          • 分析四
          • 分析五
          • 分析六
          • 分析七
        • String的Intern方法详解
          • intern的作用
          • JDK6中的理解
          • JDK7+的理解
          • intern案例分析
          • intern方法的好处

运行时常量池和字符串常量池

存储内容

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

  • 字面量:
    • 双引号引起来的字符串值,“kkb”
    • 定义为final类型的常量的值。
  • 符号引用:
    • 类或接口的全限定名(包括他的父类和所实现的接口)
    • 变量或方法的名称
    • 变量或方法的描述信息
      • 方法的描述:参数个数、参数类型、方法返回类型等等
      • 变量的描述信息:变量的返回值
    • this

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。

存储位置

在JDK1.6及以前,运行时常量池是方法区的一部分。
在JDK1.7及以后,运行时常量池在Java 堆(Heap)中。
运行时和class常量池一样,运行时常量池也是每个类都有一个。但是字符串常量池全只有一个

常量池区别

class常量池(静态常量池)、运行时常量池、字符串常量池区别:

  • class常量池中存储的是符号引用,而运行时常量池存储的是被解析之后的直接引用。
  • class常量池存在于class文件中,运行时常量池和字符串常量池是存在于JVM内存中。
  • 运行时常量池具有动态性,java运行期间也可能将新的常量放入池中(String#intern()),
  • 字符串常量池逻辑上属于运行时常量池的一部分,但是它和运行时常量池的区别在于,字符串常量池是全局唯一的,而运行时常量池是每个类一个。
字符串常量池如何存储数据

实际上,为了提高匹配速度,即更快的查找某个字符串是否存在于常量池,Java在设计字符串常量池的时候,还搞了一张stringtable, stringtable 有点类似于我们的hashtable,里面保存了字符串的引用。

在jdk6中 StringTable 的长度是固定的,就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长。此时当调用 String.intern() 时会需要到链表上一个一个找,从而导致性能大幅度下降;
在jdk7+, StringTable 的长度可以通过一个参数指定:

  • -XX:StringTableSize=99991
  • stringtable是类似于hashtable的数据结构,hashtable数据结构如下:
    jvm-运行时数据区(运行时常量池、字符串常量池)_第1张图片
    字符串常量池查找字符串的方式:
  • 根据字符串的 hashcode 找到对应entry。如果没冲突,它可能只是一个entry,如果有冲突,它可能是一个entry链表,然后Java再遍历entry链表,匹配引用对应的字符串。
  • 如果找得到字符串,返回引用。如果找不到字符串,会把字符串放到常量池,并把引用保存到stringtable里。
字符串常量池简介

上面我们已经稍微了解过字符串常量池了,它是java为了节省空间而设计的一个内存区域,java中所有的类共享一个字符串常量池。

  • 比如A类中需要一个“hello”的字符串常量,B类也需要同样的字符串常量,他们都是从字符串常量池中获取的字符串,并且获得得到的字符串常量的地址是一样的。
字符串常量池案例分析
  • 1:单独使用””引号创建的字符串都是常量,编译期就已经确定存储到String Pool中。
  • 2:使用new String(“”)创建的对象会存储到heap中,是运行期新创建的。
  • 3:使用只包含常量的字符串连接符如”aa”+”bb”创建的也是常量,编译期就能确定已经存储到String Pool中。
  • 4:使用包含变量的字符串连接如”aa”+s创建的对象是运行期才创建的,存储到heap中。
  • 5:运行期调用String的intern()方法可以向String Pool中动态添加对象。
案例
public class Test { 
	public void test() { 
		String str1 = "abc"; 
		String str2 = new String("abc"); 
		System.out.println(str1 == str2); 
		
		String str3 = new String("abc"); 
		System.out.println(str3 == str2); 
		
		String str4 = "a" + "b"; 
		System.out.println(str4 == "ab"); 
		
		final String s = "a"; 
		String str5 = s + "b"; 
		System.out.println(str5 == "ab"); 
		
		String s1 = "a"; String s2 = "b"; 
		String str6 = s1 + s2; 
		System.out.println(str6 == "ab"); 
		
		String str7 = "abc".substring(02); 
		System.out.println(str7 == "ab"); 
		
		String str8 = "abc".toUpperCase(); 
		System.out.println(str8 == "ABC"); 
		
		String s3 = "ab"; 
		String s4 = "ab" + getString(); 
		System.out.println(s3 == s4); 
		
		String s5 = "a"; 
		String s6 = "abc"; 
		String s7 = s5 + "bc"; 
		System.out.println(s6 == s7.intern()); 
	}
	
	private String getString(){ 
		return "c"; 
	} 
}
分析一
String str1 = "abc"; 
System.out.println(str1 == "abc");
  • 1:栈中开辟一块空间存放引用str1
  • 2:String池中开辟一块空间,存放String常量"abc"
  • 3:引用str1指向池中String常量"abc"
  • 4:str1所指代的地址即常量"abc"所在地址,输出为true
分析二
String str2 = new String("abc"); 
System.out.println(str2 == "abc");
  • 1: 栈中开辟一块空间存放引用str2
  • 2:堆中开辟一块空间存放一个新建的String对象"abc"
  • 3:引用str2指向堆中的新建的String对象"abc"
  • 4:str2所指代的对象地址为堆中地址,而常量"abc"地址在池中,输出为false
分析三
String str2 = new String("abc"); 
String str3 = new String("abc");
System.out.println(str3 == str2);
  • 1:栈中开辟一块空间存放引用str3
  • 2:堆中开辟一块新空间存放另外一个(不同于str2所指)新建的String对象
  • 3:引用str3指向另外新建的那个String对象
  • 4:str3和str2指向堆中不同的String对象,地址也不相同,输出为false
分析四
String str4 = "a" + "b"; 
System.out.println(str4 == "ab");
  • 1: 栈中开辟一块空间存放引用str4
  • 2:根据编译器合并已知量的优化功能,池中开辟一块空间,存放合并后的String常量"ab"
  • 3:引用str4指向池中常量"ab"
  • 4:str4所指即池中常量"ab",输出为true
分析五
String s1 = "a";
String s2 = "b"; 
String str6 = s1 + s2; 
System.out.println(str6 == "ab");
  • 1:栈中开辟一块中间存放引用s1,s1指向池中String常量"a"
  • 2:栈中开辟一块中间存放引用s2,s2指向池中String常量"b"
  • 3: 栈中开辟一块中间存放引用str6
  • 4:s1 + s2通过StringBuilder的最后一步toString()方法还原一个新的String对象"ab",因此 堆中开辟一块空间存放此对象
  • 5:引用str6指向堆中(s1 + s2)所还原的新String对象
  • 6: str6指向的对象在堆中,而常量"ab"在池中,输出为false
分析六
String str7 = "abc".substring(02); 
System.out.println(str7 == "ab");
  • 栈中开辟一块空间存放引用str7
  • substring()方法还原一个新的String对象"ab"(不同于str6所指),堆中开辟一块空间存放此 对象
  • 引用str7指向堆中的新String对象
分析七
String str8 = "abc".toUpperCase();
System.out.println(str8 == "ABC");
  • 1:栈中开辟一块空间存放引用str8
  • 2:toUpperCase()方法还原一个新的String对象"ABC",池中并未开辟新的空间存放String常 量"ABC"
  • 3:引用str8指向堆中的新String对象
String的Intern方法详解
intern的作用

intern的作用是把new出来的字符串的引用添加到stringtable中,java会先计算string的hashcode,查找stringtable中是否已经有string对应的引用了,如果有返回引用(地址),然后没有把字符串的地址放到stringtable中,并返回字符串的引用(地址)。

String a = new String("haha");
System.out.println(a.intern() == a);//false

因为有双引号括起来的字符串,所以会把ldc命令,即"haha"会被我们添加到字符串常量池,它的引用是string的char数组的地址,会被我们添加到stringtable中。所以a.intern的时候,返回的其实是string中的char数组的地址,和a的string实例化地址肯定是不一样的。

String e = new String("jo") + new String("hn"); 
System.out.println(e.intern() == e);//true

new String(“jo”) + new String(“hn”)实际上会转为stringbuffer的append 然后tosring()出来,实际上是new 一个新的string出来。在这个过程中,并没有双引号括起john,也就是说并不会执行ldc然后把john的引用添加到stringtable中,所以intern的时候实际就是把新的string地址(即e的地址)添加到stringtable中并且返回回来。

String f = new String("ja") + new String("va");
System.out.println(f.intern() == f);//false

或许很多朋友感觉很奇怪,这跟上面的例子2基本一模一样,但是却是false呢?这是因为java在启动的时候,会把一部分的字符串添加到字符串常量池中,而这个“java”就是其中之一。所以intern回来的引用是早就添加到字符串常量池中的”java“的引用,所以肯定跟f的原地址不一样。

JDK6中的理解

Jdk6中字符串常量池位于PermGen(永久代)中,PermGen是一块主要用于存放已加载的类信息和 字符串池的大小固定的区域。
执行intern()方法时,若常量池中不存在等值的字符串,JVM就会在常量池中创建一个等值的字符串,然后返回该字符串的引用。除此以外,JVM 会自动在常量池中保存一份之前已使用过的字符串集合。
Jdk6中使用intern()方法的主要问题就在于字符串常量池被保存在PermGen中:

  • 首先,PermGen是一块大小固定的区域,一般不同的平台PermGen的默认大小也不相同, 大致在32M到96M之间。所以不能对不受控制的运行时字符串(如用户输入信息等)使用intern()方法,否则很有可能会引发PermGen内存溢出
  • 其次String对象保存在Java堆区,Java堆区与PermGen是物理隔离的,因此如果对多个不 等值的字符串对象执行intern操作,则会导致内存中存在许多重复的字符串,会造成性能 损失
JDK7+的理解

Jdk7将常量池从PermGen区移到了Java堆区。堆区的大小一般不受限,所以将常量池从PremGen区移到堆区使得常量池的使用不再受限于固定大小。可以使用 -XX:StringTableSize 虚拟机参数设置字符串池的map大小。

  • 字符串池内部实现为一个HashMap,所以当能够确定程序中需要intern的字符串数目时,可以将该map 的size设置为所需数目*2(减少hash冲突),这样就可以使得String.intern()每次都只需要常量时 间和相当小的内存就能够将一个String存入字符串池中。

执行intern操作时,如果常量池已经存在该字符串,则直接返回字符串引用,否则复制该字符串对象的引用到常量池中并返回。
除此之外,位于堆区的常量池中的对象可以被垃圾回收。当常量池中的字符串不再存在指向它的引用时,JVM就会回收该字符串。

intern案例分析
public static void main(String[] args) { 
	String s = new String("1"); 
	s.intern(); 
	String s2 = "1"; 
	System.out.println(s == s2); 
	
	String s3 = new String("1") + new String("1"); 
	s3.intern();
	String s4 = "11"; 
	System.out.println(s3 == s4); 
}

打印结

  • jdk6 下 false false
  • jdk7 下 false true
    具体为什么稍后再解释,然后将 s3.intern(); 语句下调一行,放到 String s4 = “11”; 后面。将s.intern(); 放到 String s2 = “1”; 后面。是什么结果呢?
public static void main(String[] args) { 
	String s = new String("1"); 
	String s2 = "1"; 
	s.intern(); 
	System.out.println(s == s2); 
	
	String s3 = new String("1") + new String("1"); 
	String s4 = "11"; 
	s3.intern(); 
	System.out.println(s3 == s4); 
}

打印结

  • jdk6 下 false false
  • jdk7 下 false false
    jvm-运行时数据区(运行时常量池、字符串常量池)_第2张图片

如上图所示。在 jdk6中上述的所有打印都是 false 的,因为 jdk6中的常量池是放在 Perm 区中的,Perm区和正常的 JAVA Heap 区域是完全分开的。上面说过如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域。所以拿一个 JAVAHeap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用 String.intern方法也是没有任何关系的。

在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的Perm区的,Perm区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用intern 是会直接产生 java.lang.OutOfMemoryError:PermGen space 错误的。

在 jdk7 的版本中,字符串常量池已经从Perm区移到正常的Java Heap区域了。为什么要移动,Perm区域太小是一个主要原因,而且jdk8已经直接取消了Perm区域,而新建立了一个元区域。应该是jdk开发者认为Perm区域已经不适合现在 JAVA 的发展了。正式因为字符串常量池移动到JAVA Heap区域后,再来解释为什么会有上述的打印结果。
jvm-运行时数据区(运行时常量池、字符串常量池)_第3张图片

  • 在第一段代码中,先看 s3和s4字符串。 String s3 = new String(“1”) + new String(“1”); ,这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap中的 s3引用指向的对象。中间还有2个匿名的 new String(“1”) 我们不去讨论它们。此时s3引用对象内容是”11″,但此时常量池中是没有 “11”对象的。
  • 接下来 s3.intern(); 这一句代码,是将 s3中的"11"字符串放入String 常量池中,因为此时常量池中不存在"11"字符串,因此常规做法是跟 jdk6 图中表示的那样,在常量池中生成一个"11"的对象,关键点是 jdk7 中常量池不在Perm区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向s3引用的对象。 也就是说引用地址是相同的。
  • 最后 String s4 = “11”; 这句代码中”11″是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向s3引用对象的一个引用。所以s4引用就指向和s3一样了。因此最后的比较 s3 == s4 是 true。
  • 再看s和 s2 对象。 String s = new String(“1”); 第一句代码,生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。 s.intern(); 这一句是 s 对象去常量池中寻找后发现 “1” 已经在常量池里了。
  • 接下来 String s2 = “1”; 这句代码是生成一个 s2的引用指向常量池中的“1”对象。 结果就是 s 和 s2 的引用地址明显不同。图中画的很清晰。
    jvm-运行时数据区(运行时常量池、字符串常量池)_第4张图片
  • 来看第二段代码,从上边第二幅图中观察。第一段代码和第二段代码的改变就是 s3.intern();的顺序是放在 String s4 = “11”; 后了。这样,首先执行 String s4 = “11”; 声明 s4 的时候常量池中是不存在“11”对象的,执行完毕后,“11“对象是 s4 声明产生的新对象。然后再执行 s3.intern(); 时,常量池中“11”对象已经存在了,因此 s3 和 s4 的引用是不同的。
  • 第二段代码中的 s 和 s2 代码中, s.intern(); ,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码 String s = new String(“1”); 的时候已经生成“1”对象了。下边的s2声明都是直接从常量池中取地址引用的。 s 和 s2 的引用地址是不会相等的。

小结
从上述的例子代码可以看出 jdk7 版本对 intern 操作和常量池都做了一定的修改。主要包括2点

  • 将String常量池从Perm区移动到了Java Heap区
  • String#intern 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象
intern方法的好处

如果在字符串拼接中,有一个参数是非字面量,而是一个变量的话,整个拼接操作会被编译成StringBuilder.append,这种情况编译器是无法知道其确定值的。只有在运行期才能确定。

String s3 = new String("1") + new String("1");

那么,有了这个特性了,intern就有用武之地了。那就是很多时候,我们在程序中得到的字符串是只有在运行期才能确定的,在编译期是无法确定的,那么也就没办法在编译期被加入到常量池中。

这时候,对于那种可能经常使用的字符串,使用intern进行定义,每次JVM运行到这段代码的时候,就
会直接把常量池中该字面值的引用返回,这样就可以减少大量字符串对象的创建了。

static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX]; 
public static void main(String[] args) throws Exception { 
	Integer[] DB_DATA = new Integer[10]; 
	Random random = new Random(10 * 10000); 
	for (int i = 0; i < DB_DATA.length; i++) { 
		DB_DATA[i] = random.nextInt(); 
	}
	long t = System.currentTimeMillis(); 
	for (int i = 0; i < MAX; i++) {
		arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern(); 
	}
	System.out.println((System.currentTimeMillis() - t) + "ms");
	System.gc(); 
}

以上程序会有很多重复的相同的字符串产生,但是这些字符串的值都是只有在运行期才能确定的。所以,只能我们通过intern显示的将其加入常量池,这样可以减少很多字符串的重复创建。

Jdk6 中常量池位于PremGen区,大小受限,不建议使用String.intern()方法,不过Jdk7 将常量池移到了Java堆区,大小可控,可以重新考虑使用String.intern()方法,但是由对比测试可知,使用该方法的耗时不容忽视,所以需要慎重考虑该方法的使用。

String.intern() 方法主要适用于程序中需要保存有限个会被反复使用的值的场景,这样可以减少内存消耗,同时在进行比较操作时减少时耗,提高程序性能。

你可能感兴趣的:(jvm,java,jvm,编程语言)