Class常量池、运行时常量池、字符串常量池

class常量池

java代码经过编译之后都成了xxx.class文件,这是java引以为傲的可移植性的基石。class文件中,在CAFEBABE、主次版本号之后就是常量池入口了,入口是一个u2类型的数据,也就是占据2个字节,用来给常量池的容量计数,假设这个u2的数字为0x0016,那么对应十进制为22,那么常量池中右21个常量,1-21,其中第0个用于表达“不引用任何一个常量”。在这两个字节之后就是编译器为我们生成的常量了,这些常量包含了两大类:字面量符号引用,通过一个例子看一下:

public class ThreePoolDemo {
int a=1;
}

javap反编译结果如下:

Classfile
Constant pool:
#1 = Class #2 // com/hustdj/jdkStudy/threePool/ThreePoolDemo
#2 = Utf8 com/hustdj/jdkStudy/threePool/ThreePoolDemo
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Methodref #3.#11 // java/lang/Object."<init>":()V
#11 = NameAndType #7:#8 // "<init>":()V
#12 = Fieldref #1.#13 // com/hustdj/jdkStudy/threePool/ThreePoolDemo.a:I
#13 = NameAndType #5:#6 // a:I
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/hustdj/jdkStudy/threePool/ThreePoolDemo;
#18 = Utf8 SourceFile
#19 = Utf8 ThreePoolDemo.java
{
int a;
descriptor: I
flags: (0x0000) public com.hustdj.jdkStudy.threePool.ThreePoolDemo();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #10 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #12 // Field a:I
9: return
LineNumberTable:
line 3: 0
line 4: 4
line 3: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/hustdj/jdkStudy/threePool/ThreePoolDemo;
}
SourceFile: "ThreePoolDemo.java"

通过反编译我们一睹Constant Pool真容,密密麻麻一大段,我们不妨就关注关注我们定义的成员变量a

//在<init>方法的第六行
6: putfield #12
//可以看到进行了putfield,给成员变量赋值,虽然后面的注释提醒了我们是变量a
//但是不妨跟着去看看常量池中的#12
#12 = Fieldref #1.#13
//这是一个Fieldref它又指向了#,#13,继续追踪
//#1代表是哪一个类,它又指向了一个UTF8的常量,这个常量就保存了完整的类名
#1 = Class #2
#2 = Utf8 com/hustdj/jdkStudy/threePool/ThreePoolDemo
//#13告诉了你这个变量的name和type
#13 = NameAndType #5:#6
//name是a,type是int
#5 = Utf8 a
#6 = Utf8 I

可以看到,在方法给成员变量a赋值是怎么赋值的,通过Constant Pool来确定我们要给com/hustdj/jdkStudy/threePool/ThreePoolDemo对象的name为a类型为int的这么一个变量赋值,相当于一个通讯录,我要找一个人,你就告诉我这个人住在那里,姓甚名谁。但是此刻它们都是符号引用,也就是说还仅仅是一串UTF8的字符串,通过Constant Pool确定了一串字符串,对应要找的哪个字段、方法、对象,而这些符号引用需要等到类加载的解析阶段变成直接引用,也就是直接指向对应的内存指针、偏移量等

运行时常量池

在《Java虚拟机规范8》中是这样描述的,运行时常量池(Runtime constant pool)是class文件中每一个类或者接口的常量池表(constant pool)的运行时表示形式,它包含了若干常量,从编译期可知的数值字面量到必须在运行期解析之后才能获得的方法、字段引用。也就是说class常量池=运行时常量池,只不过是不同的表现形式而已,一个是静态的,一个是动态的,其中静态的符号引用也都在运行时被解析成了动态的直接引用。

那么运行时常量池是和类绑定的,每个类、接口有自己的运行时常量池,每一个运行时常量池的内存是在方法区进行分配的,这只是概念上的方法区,每个虚拟机有自己的实现,同一个虚拟机不同的版本也有不同的实现,以常用的Hotspot虚拟机为例

  • 在1.6运行时常量池以及字符串常量池存放在方法区,此时Hotspot对于方法区的实现为永久代(关于是否属于堆内存https://www.zhihu.com/question/49044988)永久代属于GC heap的一部分
  • 在1.7字符串常量池被从方法区拿到了堆,运行时常量池还留在方法区中
  • 在1.8中hotspot移除了永久代用元空间取代它,字符串常量池还在堆中,而运行时常量池依然在方法区也就是元空间(堆外内存)

字符串常量池

为了减少频繁创建相同字符串的开销,JVM弄了一个String Pool,它是全局共享的,整个JVM独一份,与之对应的有一个StringTable,,简单来说它就是一个Hash Map,key--字符串字面量,value--指向真正的字符串对象的指针。任何通过字面量创建字符串的方式都需要先通过HashMap检查,如果有这个字面量,则直接返回value,如果没有则创建一个。示例如下:

public class StringPoolDemo {
public static void main(String[] args) {
String a="123";
String b="123";
System.out.println(a==b);
}
}
//输出为true

它的过程如下:

如果这样呢?

public class StringPoolDemo {
public static void main(String[] args) {
String a = new String("123");
String b="123";
System.out.println(a==b);
}
}
//输出false

它的过程如下:

如果这样呢?

public class StringPoolDemo {
public static void main(String[] args) {
String a = new String("123");
String b=a.intern();
System.out.println(a==b);
}
}

过程如下:

String s = new String(new char[]{'1', '2', '3'});
String s1=s.intern();
String s2 = "123";
System.out.println(s1==s);
System.out.println(s1==s2);
System.out.println(s==s2);

它的过程如下:

  1. 通过new创建了一个String对象,此时String Table并没有记录
  2. s.intern(),查看String Table发现,并没有这样的一个字符串,那么新增记录并且返回对应的地址,即s1指向snew出来的string对象
  3. s="123",同样想去string table里面查看,发现已经有这样的字符串了,直接返回地址即可

所以s=s1=s2,三者指向了相同的对象

总结一下:

  • 直接根据字面量创建字符串对象,首先检查string table有没有这个字符串字面量,有的话直接返回对应的对象地址,没有则创建一个string对象,并且string table记录字符串字面量->对象地址的映射
  • new必定会在heap中创建一个对象
  • intern执行的思路与通过字面量创建的思路一致,先检查string table有没有这样的字符串,有的话直接返回对象地址,没有则入池,创建映射

再加入一些编译期优化呢?以下代码摘自Java语言规范8

package com.hustdj.jdkStudy.threePool;

public class StringPoolDemo {
public static void main(String[] args) {
String hello="Hello",lo="lo";
System.out.println(hello=="Hello");
System.out.println(Other.hello==hello);
System.out.println(com.hustdj.jdkStudy.other.Other.hello==hello);
System.out.println(hello=="Hel"+"lo");
System.out.println(hello=="Hel"+lo);
System.out.println(hello==("Hel"+lo).intern());
}
} class Other{
public static String hello="Hello";
} package com.hustdj.jdkStudy.other; public class Other {
public static String hello="Hello";
}

输出结果如下:

true
true
true
true
false
true

解释如下:

//字符串池是JVM层面的,与类、包无关
System.out.println(hello=="Hello");
System.out.println(Other.hello==hello);
System.out.println(com.hustdj.jdkStudy.other.Other.hello==hello);
//编译期优化自动转换成:hello=="Hello"
System.out.println(hello=="Hel"+"lo");
//通过StringBuilder.toString等于:new String("Hello");
System.out.println(hello=="Hel"+lo);
//intern操作时,string pool已经有"Hello"对象了,直接返回相同的引用,可以理解为入池失败
System.out.println(hello==("Hel"+lo).intern());

可见JVM为了减少相同String对象的重复创建还是做了不少努力呀

Integer缓存

同样是减少重复对象的创建,Integer同样做出了努力,示例代码如下:

public class UnboxingTest {
public static void main(String[] args) {
Integer a=1;
Integer b=1;
System.out.println(a==b);
}
}
//输出结果为true

Integer和String难道说采用了同样的策略,Integer池?当然不是,遇事不决先看看字节码

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: iconst_1
1: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1
5: iconst_1
6: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: astore_2
10: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream;
13: aload_1
14: aload_2
15: if_acmpne 22
18: iconst_1
19: goto 23
22: iconst_0
23: invokevirtual #28 // Method java/io/PrintStream.println:(Z)V
26: return

可以看到Integer a= 1实际的指令应该是Integer a =Integer.valueOf(1)

那么我们来看看Integer的源码:

public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
} private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[]; static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h; cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
} private IntegerCache() {}
}

不难发现,在Integer类初始完成之后就已经存在了-128<=value<=127的所有Integer对象,valueOf传入的参数如果在这之间的话直接返回相应的对象即可,并且上限是可以修改的。

此外,Short、Character、Long、Byte、Boolean都是有缓存处理的,而Float、Double没有,它们的valueOf如下

public static Short valueOf(short s) {
final int offset = 128;
int sAsInt = s;
if (sAsInt >= -128 && sAsInt <= 127) { // must cache
return ShortCache.cache[sAsInt + offset];
}
return new Short(s);
} public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
} public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
} public static Byte valueOf(byte b) {
final int offset = 128;
return ByteCache.cache[(int)b + offset];
} public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
} public static Float valueOf(float f) {
return new Float(f);
} public static Double valueOf(double d) {
return new Double(d);
}

总结

  • String Pool是JVM层面实现的,Integer这些是Java层面通过静态代码块在类加载的初始化阶段完成的
  • Integer的默认缓存范围为[-128,127],其它详见代码,Float、Double并不提供缓存
  • Integer的缓存上限可扩大,最大为Integer.MAX_VALUE - (-low) -1

常量池的内存分布问题

前面关于常量池的内存分布已经做了介绍,这里再补充一些。详见关于问题方法区的Class信息,又称为永久代,是否属于Java堆?的知乎讨论https://www.zhihu.com/question/49044988

总结如下:

  • 永久代/方法区也属于GC Heap的一部分

  • SymbolTable / StringTable,这俩table一直在native memory里面

  • JDK6的以永久代(PermGen)作为方法区的实现,除了JIT编译的代码存在native memory中以外,其他的方法区的数据都存在永久代中(此时的String Pool中的字符串示例都是在永久代中的)

  • JDK7还是以永久代作为方法区的实现

    • 把Symbol的存储从PermGen移动到了native memory
    • 把静态变量从instanceKlass末尾(位于PermGen内)移动到了java.lang.Class对象的末尾(位于普通Java heap内)
    • StringTable引用的java.lang.String实例则从PermGen移动到了普通Java heap
  • JDK8中永久代彻底被移除,用元空间作为方法区的实现

为什么需要移来移去呢?

在PermGen中元数据可能会随着每一次Full GC发生而进行移动。HotSpot虚拟机的每种类型的垃圾回收器都需要特殊处理PermGen中的元数据,分离出来以后可以简化Full GC以及对以后的并发隔离类元数据等方面进行优化。

参考文献

https://www.zhihu.com/question/49044988

https://segmentfault.com/a/1190000012577387

Class常量池、运行时常量池、字符串常量池的一些思考的更多相关文章

  1. java中的编译时常量与运行时常量

    常量是程序运行期间恒定不变的量,许多程序设计语言都有某种方式,向编译器告知一块数据是恒定不变的,例如C++中的const和Java中的final. 根据编译器的不同行为,常量又分为编译时常量和运行时常 ...

  2. EF6 Create Different DataContext on runtime(运行时改变连接字符串)

    引言   在使用EF时,有时我们需要在程序运行过程中动态更改EF的连接字符串,但不幸的时EF是否对 ConfigurationManager.RefreshSection("xxx" ...

  3. 彻底搞清楚class常量池、运行时常量池、字符串常量池

    彻底搞清楚class常量池.运行时常量池.字符串常量池 常量池-静态常量池 也叫 class文件常量池,主要存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference ...

  4. 对JVM运行时常量池的一些理解

    1.JVM运行时常量池在内存的方法区中(在jdk8中,移除了方法区) 2.JVM运行时常量池中的内容主要是从各个类型的class文件的常量池中获取,对于字符串常量,可以调用intern方法人为添加,而 ...

  5. JVM详解之:运行时常量池

    目录 简介 class文件中的常量池 运行时常量池 静态常量详解 String常量 数字常量 符号引用详解 String Pool字符串常量池 总结 简介 JVM在运行的时候会对class文件进行加载 ...

  6. 类的加载,链接和初始化——1运行时常量池(来自于java虚拟机规范英文版本+本人的翻译和理解)

    加载(loading):通过一个特定的名字,找到类或接口的二进制表示,并通过这个二进制表示创建一个类或接口的过程. 链接:是获取类或接口并把它结合到JVM的运行时状态中,以让类或接口可以被执行 初始化 ...

  7. Java中String字符串常量池总结

    最近到广州某建站互联网公司面试,当时面试官问假设有两个字符串String a="abc",String b = "abc";问输出a==b是true还是fals ...

  8. String:字符串常量池

    String:字符串常量池 作为最基础的引用数据类型,Java 设计者为 String 提供了字符串常量池以提高其性能,那么字符串常量池的具体原理是什么,我们带着以下三个问题,去理解字符串常量池: 字 ...

  9. Java字符串常量池是什么?为什么要有这种常量池?

    简单介绍 Java中的字符串常量池(String Pool)是存储在Java堆内存中的字符串池.我们知道String是java中比较特殊的类,我们可以使用new运算符创建String对象,也可以用双引 ...

随机推荐

  1. CentOS 7 静态IP配置

    CentOS 7 网络配置还有一个有趣的现象,我们都习惯使用 ifconfig 命令查看自己的网络信息和IP地址,但是在 CentOS 7 是无法执行的,如下图所示: 原因是 CentOS 7 使用 ...

  2. a^b(取模运算)

    a^b(sdtbu oj 1222) Description 对于任意两个正整数a,b(0 <= a, b < 10000)计算ab各位数字的和的各位数字的和的各位数字的和的各位数字的和. ...

  3. 02、Spring-HelloWorld

    0. 环境准备 1) jar包 jar包我会帮大家准备好的,所以不用担心找不到Jar包  链接:https://pan.baidu.com/s/1JJcYaspK07JL53vU-q-BUQ 提取码: ...

  4. beef抓包简析

    搭建完了beef就想简答的抓下包分析下 这是第一个包,追踪它 返回demo页面,并发现其中的脚本 window.location.protocol表示协议http, window.location.h ...

  5. Boom 3D支持的音乐都有什么格式

    Boom 3D作为一款专业的3D环绕音效软件,支持多种音频.视频播放格式,除了常用的MP3.WMA.WAV音频格式外,Boom 3D还支持FLAC.AAC这些比较高级的音频格式.同时,Boom 3D还 ...

  6. 知识点:C语言进阶提高篇,自定义数据类型:枚举

    一.枚举的概念 枚举是C语言中的一种基本数据类型,并不是构造类型,它可以用于声明一组常数.当一个变量有几个固定的可能取值时,可以将这个变量定义为枚举类型.比如,你可以用一个枚举类型的变量来表示季节,因 ...

  7. dubbo协议之编码请求对象体

    上节我们看了如何编码请求头,这节一起看下过程中,对请求对象的编码,涉及对接口,方法,方法参数类型,方法参数进行编码,DubboCodec中重写了这个方法: request.getData向下转型成Rp ...

  8. 【mq读书笔记】消息确认(失败消息,定时队列重新消费)

    接上文的集群模式,监听器返回RECONSUME_LATER,需要将将这些消息发送给Broker延迟消息.如果发送ack消息失败,将延迟5s后提交线程池进行消费. 入口:ConsumeMessageCo ...

  9. Beta冲刺随笔——Day_Three

    这个作业属于哪个课程 软件工程 (福州大学至诚学院 - 计算机工程系) 这个作业要求在哪里 Beta 冲刺 这个作业的目标 团队进行Beta冲刺 作业正文 正文 其他参考文献 无 今日事今日毕 林涛: ...

  10. Java面试专题-集合篇(2)