彻底弄懂JVM常量池

一、绪论

注:本文提到的JVM都是HotSpot

Class文件常量池

Java源代码经过编译后生成.class文件。.class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。

字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。
在这里插入图片描述

图片引用自:理解归纳方法区和常量池

字符串常量池

为什么产生字符串常量池与字符串驻留

字符串驻留(String interning)是字符串常量池产生的根本原因。大意如下:

所谓字符串驻留,是指在系统中,对每个字面量唯一的字符串,都只保留唯一的一份副本,称作“驻留量”(intern),并且它们都是不可变的。这些彼此不同的字符串被存储在字符串常量池中。

各编程语言有各自的方法来取得字符串常量池中的驻留量,或者将一个字符串驻留,比如Java中的String.intern()。在Java中,所有编译期能确定的字符串也都会自动驻留。

不仅字符串可以驻留。例如在Java中,[-128,127]区间内的Integer被缓存在内部类IntegerCache中,这个类就相当于整形常量池。在该区间内两个数值相同的整形值,在自动装箱后实际上指向堆内的同一个Integer对象(也就是驻留量),可以参考Integer.valueOf()方法的源码。

字符串驻留是设计模式中的享元模式(flyweight pattern)的典型实现,这里就不展开描述了。

换句话说,存在字符串常量池主要是为了减少内存使用并提高内存中现有实例的重用。当为字符串对象分配不同的值时,新值将作为单独的实例注册在字符串常量池中。

String.intern()

在JDK中,String.intern()方法是一个native方法,这个方法是干什么的呢?

在 JAVA 语言中有8中基本类型和一种比较特殊的类型String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。常量池就类似一个JAVA系统级别提供的缓存。

8种基本类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种:

  • 直接使用双引号声明出来的String对象会直接存储在常量池中。
  • 如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中

字符串常量池位置

在JDK7中,驻留字符串不再在永久代上分配,而是在Java堆的主要部分(新生代和老年代)分配。

由此可得,JDK6的字符串常量池位于永久代(它是HotSpot的方法区实现)。到了JDK7,字符串常量池就直接放在堆里。下面用《深入理解Java虚拟机(第二版)》的经典例子来证明。它产生一个无限递增的数字字符串序列,并依次放进字符串常量池。

public class OOMExample {
    public static void main(String[] args) {
        // 使用List保持引用,避免常量池被GC
        List<String> list = new ArrayList<String>();
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

JVM参数统一为:

-Xms8m -Xmx8m -XX:PermSize=8m -XX:MaxPermSize=8m -XX:+UseParallelGC -XX:+PrintGCDetails

然后分别在JDK6、7、8的环境下运行,观察输出结果。

JDK6:

[GC [PSYoungGen: 2012K->304K(2368K)] 2012K->420K(7872K), 0.0014317 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC [PSYoungGen: 2352K->320K(2368K)] 2468K->705K(7872K), 0.0013064 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
[GC [PSYoungGen: 1331K->288K(2368K)] 1717K->697K(7872K), 0.0007446 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 288K->0K(2368K)] [PSOldGen: 409K->617K(5504K)] 697K->617K(7872K) [PSPermGen: 8191K->8191K(8192K)], 0.0130018 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
[GC [PSYoungGen: 0K->0K(2368K)] 617K->617K(7872K), 0.0001804 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 0K->0K(2368K)] [PSOldGen: 617K->471K(5504K)] 617K->471K(7872K) [PSPermGen: 8191K->8180K(8192K)], 0.0134341 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
......
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    at java.lang.String.intern(Native Method)
    at me.lmagics.OOMExample.main(OOMExample.java:16)

JDK7:

[GC [PSYoungGen: 2048K->507K(2560K)] 2048K->1651K(8192K), 0.0026340 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC [PSYoungGen: 2555K->501K(2560K)] 3699K->3389K(8192K), 0.0028820 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC [PSYoungGen: 2549K->496K(2560K)] 5437K->5192K(8192K), 0.0038110 secs] [Times: user=0.01 sys=0.01, real=0.01 secs]
[Full GC [PSYoungGen: 496K->0K(2560K)] [ParOldGen: 4696K->5101K(5632K)] 5192K->5101K(8192K) [PSPermGen: 2603K->2602K(8192K)], 0.0622090 secs] [Times: user=0.27 sys=0.00, real=0.06 secs]
[Full GC [PSYoungGen: 2048K->1535K(2560K)] [ParOldGen: 5101K->5180K(5632K)] 7149K->6716K(8192K) [PSPermGen: 2602K->2602K(8192K)], 0.0550730 secs] [Times: user=0.28 sys=0.01, real=0.05 secs]
[Full GC [PSYoungGen: 2048K->2047K(2560K)] [ParOldGen: 5180K->5180K(5632K)] 7228K->7228K(8192K) [PSPermGen: 2602K->2602K(8192K)], 0.0287170 secs] [Times: user=0.14 sys=0.00, real=0.03 secs]
......
[Full GC [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 5543K->5543K(5632K)] 7591K->7591K(8192K) [PSPermGen: 2602K->2602K(8192K)], 0.0285530 secs] [Times: user=0.16 sys=0.00, real=0.03 secs]
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
[Full GC [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: 5546K->220K(5632K)] 7594K->220K(8192K) [PSPermGen: 2627K->2627K(8192K)], 0.0052340 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
    at java.lang.Integer.toString(Integer.java:331)
    at java.lang.String.valueOf(String.java:2954)
    at me.lmagics.OOMExample.main(OOMExample.java:16)

JDK8:

Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=8m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=8m; support was removed in 8.0
[GC (Allocation Failure) [PSYoungGen: 1536K->482K(2048K)] 1536K->1210K(7680K), 0.0017302 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2018K->505K(2048K)] 2746K->2581K(7680K), 0.0021425 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2041K->501K(2048K)] 4117K->3969K(7680K), 0.0021064 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 2037K->496K(2048K)] 5505K->5276K(7680K), 0.0025973 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 496K->0K(2048K)] [ParOldGen: 4780K->5090K(5632K)] 5276K->5090K(7680K), [Metaspace: 2652K->2652K(1056768K)], 0.0587041 secs] [Times: user=0.30 sys=0.01, real=0.05 secs]
[Full GC (Ergonomics) [PSYoungGen: 1412K->880K(2048K)] [ParOldGen: 5090K->5570K(5632K)] 6503K->6451K(7680K), [Metaspace: 2652K->2652K(1056768K)], 0.0334546 secs] [Times: user=0.17 sys=0.00, real=0.03 secs]
[Full GC (Ergonomics) [PSYoungGen: 1536K->1535K(2048K)] [ParOldGen: 5570K->5154K(5632K)] 7106K->6690K(7680K), [Metaspace: 2652K->2652K(1056768K)], 0.0320396 secs] [Times: user=0.15 sys=0.00, real=0.04 secs]
......
[Full GC (Ergonomics) [PSYoungGen: 1535K->1535K(2048K)] [ParOldGen: 5542K->5542K(5632K)] 7078K->7078K(7680K), [Metaspace: 2652K->2652K(1056768K)], 0.0273170 secs] [Times: user=0.17 sys=0.00, real=0.03 secs]
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
[Full GC (Ergonomics) [PSYoungGen: 1536K->0K(2048K)] [ParOldGen: 5545K->267K(5632K)] 7081K->267K(7680K), [Metaspace: 2677K->2677K(1056768K)], 0.0039194 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
    at java.lang.Integer.toString(Integer.java:401)
    at java.lang.String.valueOf(String.java:3099)
    at me.lmagics.OOMExample.main(OOMExample.java:16)

从以上输出结果可以看出:

  • JDK6报永久代OOM,证明字符串常量池确实在永久代;
  • JDK7和8均报超出GC临界限制。在HotSpot中,一旦JVM检查到用98%以上的时间来GC,而回收了少于2%的堆空间,就会报这个错误。如果使用参数-XX:-UseGCOverheadLimit来关闭检查,那么一段时间后就会抛出常见的“java.lang.OutOfMemoryError: Java heap space”。这证明字符串常量池确实移动到了堆中;
  • JDK8还会报设置永久代的参数无效。这是因为JDK8已经完全移除了永久代,改用元空间(Metaspace)来实现方法区了。在GC日志中也可以看到Metaspace GC的情况。

问:为什么字符串常量池要从永久代移动到堆,并且后来永久代还被元空间替代了?
答:永久代作为HotSpot方法区的实现很不好用,并且其他JVM实现都没有永久代。字符串常量池在堆中可以获得更大的内存空间。

根据Java虚拟机规范的规定:

方法区存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容等等。
虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不做GC与压缩。

在HotSpot中,方法区是存在GC的,就是堆空间的分代GC直接扩展过来。由于方法区内的数据相对于新生代和老年代来讲更“静态”一些,为了保持命名一致性,才把这里叫做“永久代”。

永久代的不好用主要体现在它难以调控。它的内存大小是由-XX:PermSize和-XX:MaxPermSize两个参数定死的,如果设定得太小,当常量池过大或者动态加载类的元数据过多时,就会直接OOM。如果设定得太大,会挤占原本可用于堆的空间,也会增大GC的压力。

另外,在JDK7时代就开始推动HotSpot与JRockit两套虚拟机的融合,而JRockit是不存在永久代的,因此HotSpot最后也取消了它。新加入的元空间则位于本地内存(native memory)中,消除了原来的大小限制,变得更加灵活。关于元空间的更多细节就不展开,请参见这里:Metaspace in Java 8
在这里插入图片描述

字符串常量池内存储的是什么

这个问题因为不容易验证,经常引起争吵。

来看下面一段代码:

public class StringPoolExample {
    public static void main(String[] args) {
        String s1 = new String("a");   // #1
        s1.intern();                   // #2
        String s2 = "a";               // #3
        System.out.println(s1 == s2);

        String s3 = s2 + s2;           // #4
        s3.intern();                   // #5
        String s4 = "aa";              // #6
        System.out.println(s3 == s4);
    }
}

这段代码在JDK6执行,输出false false;但在JDK7/8执行,输出false true。根据结果的不同,可以推测出字符串常量池内的存储也发生了变化。借助ProcessOn画图详细分析一下:

JDK6:
在这里插入图片描述
在#1语句中,创建了多少个对象?这是面试中极常见的问题,答案是2个,堆中及字符串常量池中各一个。由于"a"是字面量,因此它会自动驻留。#2语句调用intern()时,字符串常量池中就已经存在它了。#3语句会直接找到常量池中的"a",故s1与s2的引用地址是不同的。

#4语句中,s3引用的字符串的值不能在编译期确定,因此生成了一个新的String对象。使用#5语句调用intern()时,常量池里还不存在"aa",将它加入进去。#6语句也会直接找到常量池中的"aa",故s3与s4的引用地址也是不同的。

JDK7/8:

#1~#3语句的执行结果与上面相同,不再赘述。
而#4~#6执行完后为什么会返回true?既然==运算符比较的是引用类型的地址,那么只能说明s3和s4的引用地址是一样的。因此,上面的图应该做一个改动:
在这里插入图片描述
#5语句在执行时,堆中存在String对象"aa",但常量池中没有。这时不再像JDK6一样将对象加入常量池,而是将对"aa"的引用加入。该引用与s3引用的对象都是堆中的同一个String对象。这样,#6语句在常量池中找到"aa"时,实际上是找到了与s3相同的引用,所以s3 == s4是成立的。

结论:
在JDK6中,字符串常量池里保存的都是String对象。
在JDK7/8中,对于字符串字面量(当然也包括常量表达式),常量池里会直接保存String对象。如果是编译期不能确定的字符串,调用intern()方法会使得常量池中保存对堆内String对象的引用,而不会在常量池内再生成一个对象。之所以做这种改动,可能是考虑到字符串常量池已经移动到了堆中,因此没有必要在池内和池外各保留一个对象,这样节省空间。

引用:深入解析String#intern
JVM字符串常量池及相关知识的再探究

运行时常量池

当java文件被编译成class文件之后,也就是会生成我上面所说的class常量池,那么运行时常量池又是什么时候产生的呢?

jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在上面我也说了,class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。
在这里插入图片描述
如图所示,虚拟机栈每一个栈帧,都有一个指向运行时常量池的引用,该引用指向针对该帧正在执行的方法的类的常量池。

引用自:https://stackoverflow.com/questions/10209952/what-is-the-purpose-of-the-java-constant-pool

class文件常量池和运行时常量池

在这里插入图片描述
class文件常量池存储的是当class文件被java虚拟机加载进来后存放在方法区的一些字面量和符号引用,字面量包括字符串,基本类型的常量。

引用自:字符串常量池、class常量池和运行时常量池

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值