1. Class文件常量池

class文件是一组二进制字节流,class文件常量池在编译阶段就已经确定。class文件常量池主要存放两大常量:字面量符号引用

  1. class JavaBean{
  2. private int value = 1;
  3. public String s = "abc";
  4. public final static int f = 0x101;
  5. public void setValue(int v){
  6. final int temp = 3;
  7. this.value = temp + v;
  8. }
  9. public int getValue(){
  10. return value;
  11. }
  12. }

通过 javac 命令编译之后,用 javap -v 命令查看编译后的文件:

  1. class JavaBasicKnowledge.JavaBean
  2. minor version: 0
  3. major version: 52
  4. flags: ACC_SUPER
  5. Constant pool:
  6. #1 = Methodref #6.#29 // java/lang/Object."<init>":()V
  7. #2 = Fieldref #5.#30 // JavaBasicKnowledge/JavaBean.value:I
  8. #3 = String #31 // abc
  9. #4 = Fieldref #5.#32 // JavaBasicKnowledge/JavaBean.s:Ljava/lang/String;
  10. #5 = Class #33 // JavaBasicKnowledge/JavaBean
  11. #6 = Class #34 // java/lang/Object
  12. #7 = Utf8 value
  13. #8 = Utf8 I
  14. #9 = Utf8 s
  15. #10 = Utf8 Ljava/lang/String;
  16. #11 = Utf8 f
  17. #12 = Utf8 ConstantValue
  18. #13 = Integer 257
  19. #14 = Utf8 <init>
  20. #15 = Utf8 ()V
  21. #16 = Utf8 Code
  22. #17 = Utf8 LineNumberTable
  23. #18 = Utf8 LocalVariableTable
  24. #19 = Utf8 this
  25. #20 = Utf8 LJavaBasicKnowledge/JavaBean;
  26. #21 = Utf8 setValue
  27. #22 = Utf8 (I)V
  28. #23 = Utf8 v
  29. #24 = Utf8 temp
  30. #25 = Utf8 getValue
  31. #26 = Utf8 ()I
  32. #27 = Utf8 SourceFile
  33. #28 = Utf8 StringConstantPool.java
  34. #29 = NameAndType #14:#15 // "<init>":()V
  35. #30 = NameAndType #7:#8 // value:I
  36. #31 = Utf8 abc
  37. #32 = NameAndType #9:#10 // s:Ljava/lang/String;
  38. #33 = Utf8 JavaBasicKnowledge/JavaBean
  39. #34 = Utf8 java/lang/Object

1.1. 字面量

字面量接近 java 语言层面的常量概念。

【字符串】

字符串的值。

  1. // public String s = "abc"; 中的 "abc"。
  2. #9 = Utf8 s
  3. #3 = String #31
  4. #31 = Utf8 abc

【final 修饰变量】

包括成员变量、静态变量、实例变量和局部变量。

  1. // public final static int f = 0x101; 中的0x101。
  2. #11 = Utf8 f
  3. #12 = Utf8 ConstantValue
  4. #13 = Integer 257

【注意】

  • 字面量指的是数据的值,class文件常量池中一般指字符串的值和常量的值。
  • 对于基本类型数据和局部变量,他们的字面量不会存在于class文件常量池。
  1. // private int value = 1; 常量池中只保留了字段描述符 I 和字段名称 value
  2. #7 = Utf8 value
  3. #8 = Utf8 I

1.2. 符号引用

符号引用主要设涉及编译原理方面的概念。

【类和接口的全限定名】

将类名中原来的 “.” 替换为 “/“ 得到的,主要用于在运行时解析得到类的直接引用。

  1. #5 = Class #33
  2. #33 = Utf8 JavaBasicKnowledge/JavaBean

【字段名称和描述符】

类或者接口中声明的变量名称和类型,包括类变量和实例变量。

  1. #4 = Fieldref #5.#32
  2. #5 = Class #33
  3. #32 = NameAndType #7:#8
  4. // 基本数据类型的变量,常量池只保留字段名称和字段描述符
  5. // private int value = 1;
  6. #7 = Utf8 value // 变量名
  7. #8 = Utf8 I // 变量类型
  8. // 形参变量和局部变量,常量池只保留字段名称
  9. // public void setValue(int v) {}
  10. #23 = Utf8 v // 形参变量名
  11. // final int temp = 3;
  12. #24 = Utf8 temp // 局部变量名

【方法名称和描述符】

方法的名称和参数类型、返回值类型

  1. // public void setValue(int v) {}
  2. #21 = Utf8 setValue // 方法名
  3. #22 = Utf8 (I)V // 参数类型 I 返回值类型 V
  4. #25 = Utf8 getValue // 方法名
  5. #26 = Utf8 ()I // 参数类型空 返回值类型 I

2. 运行时常量池

运行时常量池是方法区的一部分,不同的类共用一个运行时常量池。

  • Class 加载过程,class 文件常量池会进入运行时常量池。
  • 多个 class 文件常量池中相同的字符串在运行时常量池只会存在一份。
  • class 文件常量池中的符号引用,会在 Class 的解析阶段翻译成直接引用,一起存储在运行时常量池中。
  • 运行时常量池具有动态性,其中的内容并不全部来自 class 常量池,在运行时可以通过代码生成常量并将其放入,这种特性被用的最多的就是 String.intern()

3. 字符串常量池

字符串常量池和运行时常量池不是一个概念。

  • JDK 1.7 以前字符串常量池和运行时常量池都在方法区中。
  • JDK 1.7 及以后运行时常量池在方法区中,字符串常量池在堆中。

3.1. 创建字符串对象的方式

3.1.1. 字面量创建

  1. String s = "abc";
  1. JVM 先去字符串常量池中查找 equals("abc") 的对象。
  2. 如果有则不用创建,直接将字符串常量池中的 "abc" 对象的引用赋值给 s
  3. 如果没有则在堆中创建一个新的 "abc" 对象,并将堆中的 "abc" 对象的引用驻留一份在字符串常量池中,再将字符串常量池中的 "abc" 对象的引用赋值给 s

s => 字符串常量池中(同时也是堆中)的 “abc” 对象。且无论创建多少次,只要字符串的值相同,它们都指向堆中的同一个对象。

3.1.2. 构造方法创建

  1. String s = new String("abc");
  1. JVM 先去字符串常量池中查找 equals("abc") 的对象。
  2. 如果有则在堆中创建一个新的 "abc" 对象。
  3. 如果没有则在堆和字符串常量池中各创建一个新的 "abc" 对象。
  4. 将字符串常量池中的 "abc" 对象的引用赋值给 s

s => 堆中的 “abc” 对象。

  1. String s1 = "Hello";
  2. String s2 = "Hello";
  3. String s3 = "Hel" + "lo";
  4. String s4 = "Hel" + new String("lo");
  5. String s5 = new String("Hello");
  6. String s6 = new String("Hello");
  7. String s7 = "H";
  8. String s8 = "ello";
  9. String s9 = s7 + s8;
  10. // true。 s1和s2都指向字符串常量池中的同一个"Hello"对象
  11. System.out.println(s1 == s2);
  12. // true。 s3会在编译时合并成"Hello"对象,因此s1和s3都指向字符串常量池中的同一个"Hello"对象
  13. System.out.println(s1 == s3);
  14. // false。s4只会创建"Hel"对象,"lo"对象会在堆中创建,因此s1和s4指向的对象不同
  15. System.out.println(s1 == s4);
  16. // false。s1指向字符串常量池中的"Hello"对象,s5指向堆中的"Hello"对象
  17. System.out.println(s1 == s5);
  18. // true。 s1指向字符串常量池中的"Hello"对象,s5.intern()会返回字符串常量池中的"Hello"对象
  19. System.out.println(s1 == s5.intern());
  20. // false。s9会在运行时动态赋值,因此s1和s9指向的对象不同
  21. System.out.println(s1 == s9);
  22. // false。s5和s6指向堆中不同的"Hello"对象
  23. System.out.println(s5 == s6);
  24. // true。 s5.intern()和s6.intern()都会返回字符串常量池中的同一个"Hello"对象
  25. System.out.println(s5.intern() == s6.intern());

3.2. intern 方法

将对应的字面量进行特殊处理。

  • JDK 1.6及以前:检查字面量对象是否在字符串常量池中,如果存在则直接返回字符串常量池的对象,如果不存在,则在字符串常量池创建并返回该字面量对象。
  • JDK 1.7及以后:检查字面量对象是否在字符串常量池中,如果存在则直接返回字符串常量池的对象,如果不存在,则把堆中的字面量对象的引用添加到字符串常量池中,并返回该字面量对象。

4. 包装类对象常量池

java 基本类型的包装类 Byte,Short,Integer,Long,Character,Boolean 实现了常量池技术,而 Float 和 Double 则没有实现。

另外上面这 6 种整型的包装类也只是在对应值在 [-128,127] 区间时才使用缓存数据, 超出此范围仍然会去创建新的对象。

  1. Integer i1 = 127;
  2. Integer i2 = 127;
  3. System.out.println(i1 == i2); // true。 s1和s2都指向常量池中的同一个对象
  4. i2 = 200;
  5. System.out.println(i1 == i2); // false。s2重新创建了一个新的对象,s1和s2指向不同对象
  6. Integer i3 = 128;
  7. Integer i4 = 128;
  8. System.out.println(i3 == i4); // false。128超出范围,s1和s2都指向不同对象
  9. Integer i5 = -128;
  10. Integer i6 = -128;
  11. System.out.println(i5 == i6); // true。 s1和s2都指向常量池中的同一个对象
  12. Boolean bool1 = false;
  13. Boolean bool2 = false;
  14. System.out.println(bool1 == bool2); // true。 s1和s2都指向常量池中的同一个对象
  15. Double d1 = 1.0;
  16. Double d2 = 1.0;
  17. System.out.println(d1 == d2); // false。Double类没有使用常量池。s1和s2指向不同对象