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

class常量池

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

  1. public class ThreePoolDemo {
  2. int a=1;
  3. }

javap反编译结果如下:

  1. Classfile
  2. Constant pool:
  3. #1 = Class #2 // com/hustdj/jdkStudy/threePool/ThreePoolDemo
  4. #2 = Utf8 com/hustdj/jdkStudy/threePool/ThreePoolDemo
  5. #3 = Class #4 // java/lang/Object
  6. #4 = Utf8 java/lang/Object
  7. #5 = Utf8 a
  8. #6 = Utf8 I
  9. #7 = Utf8 <init>
  10. #8 = Utf8 ()V
  11. #9 = Utf8 Code
  12. #10 = Methodref #3.#11 // java/lang/Object."<init>":()V
  13. #11 = NameAndType #7:#8 // "<init>":()V
  14. #12 = Fieldref #1.#13 // com/hustdj/jdkStudy/threePool/ThreePoolDemo.a:I
  15. #13 = NameAndType #5:#6 // a:I
  16. #14 = Utf8 LineNumberTable
  17. #15 = Utf8 LocalVariableTable
  18. #16 = Utf8 this
  19. #17 = Utf8 Lcom/hustdj/jdkStudy/threePool/ThreePoolDemo;
  20. #18 = Utf8 SourceFile
  21. #19 = Utf8 ThreePoolDemo.java
  22. {
  23. int a;
  24. descriptor: I
  25. flags: (0x0000)
  26. public com.hustdj.jdkStudy.threePool.ThreePoolDemo();
  27. descriptor: ()V
  28. flags: (0x0001) ACC_PUBLIC
  29. Code:
  30. stack=2, locals=1, args_size=1
  31. 0: aload_0
  32. 1: invokespecial #10 // Method java/lang/Object."<init>":()V
  33. 4: aload_0
  34. 5: iconst_1
  35. 6: putfield #12 // Field a:I
  36. 9: return
  37. LineNumberTable:
  38. line 3: 0
  39. line 4: 4
  40. line 3: 9
  41. LocalVariableTable:
  42. Start Length Slot Name Signature
  43. 0 10 0 this Lcom/hustdj/jdkStudy/threePool/ThreePoolDemo;
  44. }
  45. SourceFile: "ThreePoolDemo.java"

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

  1. //在<init>方法的第六行
  2. 6: putfield #12
  3. //可以看到进行了putfield,给成员变量赋值,虽然后面的注释提醒了我们是变量a
  4. //但是不妨跟着去看看常量池中的#12
  5. #12 = Fieldref #1.#13
  6. //这是一个Fieldref它又指向了#,#13,继续追踪
  7. //#1代表是哪一个类,它又指向了一个UTF8的常量,这个常量就保存了完整的类名
  8. #1 = Class #2
  9. #2 = Utf8 com/hustdj/jdkStudy/threePool/ThreePoolDemo
  10. //#13告诉了你这个变量的name和type
  11. #13 = NameAndType #5:#6
  12. //name是a,type是int
  13. #5 = Utf8 a
  14. #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,如果没有则创建一个。示例如下:

  1. public class StringPoolDemo {
  2. public static void main(String[] args) {
  3. String a="123";
  4. String b="123";
  5. System.out.println(a==b);
  6. }
  7. }
  8. //输出为true

它的过程如下:

如果这样呢?

  1. public class StringPoolDemo {
  2. public static void main(String[] args) {
  3. String a = new String("123");
  4. String b="123";
  5. System.out.println(a==b);
  6. }
  7. }
  8. //输出false

它的过程如下:

如果这样呢?

  1. public class StringPoolDemo {
  2. public static void main(String[] args) {
  3. String a = new String("123");
  4. String b=a.intern();
  5. System.out.println(a==b);
  6. }
  7. }

过程如下:

  1. String s = new String(new char[]{'1', '2', '3'});
  2. String s1=s.intern();
  3. String s2 = "123";
  4. System.out.println(s1==s);
  5. System.out.println(s1==s2);
  6. 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

  1. package com.hustdj.jdkStudy.threePool;
  2. public class StringPoolDemo {
  3. public static void main(String[] args) {
  4. String hello="Hello",lo="lo";
  5. System.out.println(hello=="Hello");
  6. System.out.println(Other.hello==hello);
  7. System.out.println(com.hustdj.jdkStudy.other.Other.hello==hello);
  8. System.out.println(hello=="Hel"+"lo");
  9. System.out.println(hello=="Hel"+lo);
  10. System.out.println(hello==("Hel"+lo).intern());
  11. }
  12. }
  13. class Other{
  14. public static String hello="Hello";
  15. }
  16. package com.hustdj.jdkStudy.other;
  17. public class Other {
  18. public static String hello="Hello";
  19. }

输出结果如下:

  1. true
  2. true
  3. true
  4. true
  5. false
  6. true

解释如下:

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

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

Integer缓存

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

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

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

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

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

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

  1. public static Integer valueOf(int i) {
  2. if (i >= IntegerCache.low && i <= IntegerCache.high)
  3. return IntegerCache.cache[i + (-IntegerCache.low)];
  4. return new Integer(i);
  5. }
  6. private static class IntegerCache {
  7. static final int low = -128;
  8. static final int high;
  9. static final Integer cache[];
  10. static {
  11. // high value may be configured by property
  12. int h = 127;
  13. String integerCacheHighPropValue =
  14. sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
  15. if (integerCacheHighPropValue != null) {
  16. try {
  17. int i = parseInt(integerCacheHighPropValue);
  18. i = Math.max(i, 127);
  19. // Maximum array size is Integer.MAX_VALUE
  20. h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
  21. } catch( NumberFormatException nfe) {
  22. // If the property cannot be parsed into an int, ignore it.
  23. }
  24. }
  25. high = h;
  26. cache = new Integer[(high - low) + 1];
  27. int j = low;
  28. for(int k = 0; k < cache.length; k++)
  29. cache[k] = new Integer(j++);
  30. // range [-128, 127] must be interned (JLS7 5.1.7)
  31. assert IntegerCache.high >= 127;
  32. }
  33. private IntegerCache() {}
  34. }

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

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

  1. public static Short valueOf(short s) {
  2. final int offset = 128;
  3. int sAsInt = s;
  4. if (sAsInt >= -128 && sAsInt <= 127) { // must cache
  5. return ShortCache.cache[sAsInt + offset];
  6. }
  7. return new Short(s);
  8. }
  9. public static Character valueOf(char c) {
  10. if (c <= 127) { // must cache
  11. return CharacterCache.cache[(int)c];
  12. }
  13. return new Character(c);
  14. }
  15. public static Long valueOf(long l) {
  16. final int offset = 128;
  17. if (l >= -128 && l <= 127) { // will cache
  18. return LongCache.cache[(int)l + offset];
  19. }
  20. return new Long(l);
  21. }
  22. public static Byte valueOf(byte b) {
  23. final int offset = 128;
  24. return ByteCache.cache[(int)b + offset];
  25. }
  26. public static Boolean valueOf(boolean b) {
  27. return (b ? TRUE : FALSE);
  28. }
  29. public static Float valueOf(float f) {
  30. return new Float(f);
  31. }
  32. public static Double valueOf(double d) {
  33. return new Double(d);
  34. }

总结

  • 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. Python _PyQt5对话框

    Python 调用PyQt5 制作对话框,退出时候有二次确认(注:默认是直接退出) 1 # -*- ytf-8 -*- 2 """ 3 用PyQt建一个对话框,退出时提示 ...

  2. [web安全原理分析]-XEE漏洞入门

    前言 1 前言 XXE漏洞 XXE漏洞全称(XML External Entity Injection)即xml外部实体注入漏洞,XXE漏洞发生在应用程序解析XML输入时,没有禁止外部实体的加载,导致 ...

  3. 思维导图软件iMindMap制作技巧有哪些

    iMindMap11是iMindMap全新的版本.它可以提供给我们更好的灵活性以便我们将我们的思维进行可视化,并进一步的呈现和开发出属于自己的想法以及思维方式.在iMindMap中我们可以利用思维导图 ...

  4. Java中对象在内存中的大小、分配等问题

    Java创建一个对象的过程 是否对象指向的类已经加载到内存了 如果没有加载,就要经过load.linking(verification.preparation.resolution).initiali ...

  5. 5. Idea集成Git

    5.1 引入本地安装的Git 5.2 本地库的初始化操作 5.3 本地库的基本操作 add与commit 控制台查看commit记录 查看Log 5.4 远程库的基本操作 远程库第一次pull到本地库 ...

  6. GitHub 上 1.3k Star 的 strman-java 项目有值得学习的地方吗?源码视角

    大家好,我是沉默王二. 很多初学编程的同学,经常给我吐槽,说:"二哥,你在敲代码的时候会不会有这样一种感觉,写着写着看不下去了,觉得自己写出来的代码就好像屎一样?" 这里我必须得说 ...

  7. linux设置共享文件夹 - samba

    安装samba sudo apt-get install samba 配置 /etc/samba/smb.conf 的global模块添加security = user 最下加入 [share] pa ...

  8. 记一次腾讯TBS浏览服务集成实践

    这次的分享源于最近的实际开发工作. 项目需求是 在原生Android应用中嵌入WebView,放置用于支撑音视频直播业务的Web页: 另外还需提供Word.Excel.PowerPoint.PDF等常 ...

  9. apply 、call 以及 bind 的使用和区别

    一.被apply和call调用的函数中没有传递参数 (一)不传参数 结果: (二)传递 null 结果: 总结: 1.当使用 apply和 call去调用函数并且没有传递参数时,前提这个函数中也没有传 ...

  10. 关于transition动画效果中,滚动条会闪一下就消失的问题

    具体问题说明: 我在通过transition来改变width的长度,在transition变化过程中,底下的滚动条会闪烁一下. 问题原理:因为是里面容器没办法完全被装下,并且容器的宽度被限制住了. 解 ...