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. Ceph的参数mon_osd_down_out_subtree_limit细解

    前言 之前跟一个朋友沟通一个其他的问题的时候,发现了有一个参数 mon osd down out subtree limit 一直没有接触到,看了一下这个参数还是很有作用的,本篇将讲述这个参数的作用和 ...

  2. python爬虫 selenium 抓取 今日头条(ajax异步加载)

    from selenium import webdriver from lxml import etree from pyquery import PyQuery as pq import time ...

  3. 《Machine Learning in Action》—— 懂的都懂,不懂的也能懂。非线性支持向量机

    说在前面:前几天,公众号不是给大家推送了第二篇关于决策树的文章嘛.阅读过的读者应该会发现,在最后排版已经有点乱套了.真的很抱歉,也不知道咋回事,到了后期Markdown格式文件的内容就解析出现问题了, ...

  4. 阿里面试官:小伙子,你给我说一下Spring Bean初始化的几种常规方式吧

    前言 通过构造方法实例化通过静态工厂实例化通过实例工厂实例化通过FactoryBean实例化 RumenzA实体类 package com.rumenz; public class RumenzA { ...

  5. 通过Camtasia来添加各种各样的光标效果

    在十几二十年前的时候,我们想要学习新的知识需要到学校和培训班才行,但是现在只要有一台电脑.一部手机或者平板,我们在家里也能找到我们喜欢的课程来学习了,微课也因此而生. 同样的,有了想要学习知识的学生, ...

  6. guitar pro系列教程(一):Guitar Pro主界面之记谱功能的详细解析【上】

    相信弹吉他的朋友们对guitar pro这款软件并不陌生,也有很多朋友用它来看谱制谱.而GP有很多实用功能,能够使我们看谱更清晰,制谱更便捷,所以让我们一起来看看吧 Guitar Pro对初学作曲,特 ...

  7. MGR(MySQL Group Replication)部署测试

    1. 环境说明 192.168.11.131 mgr1 主节点 192.168.11.132 mgr2 从节点 192.168.11.133 mgr3 从节点 2. 在mgr1.mgr2.mgr3上安 ...

  8. Go-Web编程_表单_0x02_验证表单的输入

    开发Web的一个原则就是,不能信任用户输入的任何信息,所以验证和过滤用户的输入信息就变得非常重要,我们经常会在微博.新闻中听到某某网站被入侵了,存在什么漏洞,这些大多是因为网站对于用户输入的信息没有做 ...

  9. 如何循序渐进、有效地学习JavaScript?

    转载链接:https://www.zhihu.com/question/19713563/answer/23068003 分享一篇 超毛 的一篇文章<如何学习javascript>(原文链 ...

  10. 技巧:如何区分dll程序集的编译目标平台(同样适用于查看程序集的其它依赖)

    我们在进行net core迁移过程中,有时候需要区分一个dll是针对netstandard平台还是net framework. 本文提供一个技巧来快速区分:通过工具dnSpy打开目标dll,按照如下截 ...