【JVM】Java8中的常量池、字符串池、包装类对象池
1 - 引⾔
2 - 常量池
2.1 你真的懂 Java的“字⾯量”和“常量”吗?
2.2 常量和静态/运⾏时常量池有什么关系?什么是常量池?
2.3 字节码下的常量池以及常量池的加载机制
2.4 是不是所有的数字字⾯量都会被存到常量池中?
3 - 包装类对象池 =JVM 常量池
4 - 字符串池
4.1 字符串池的实现——StringTable
4.2 字符串池存的是实例还是引⽤?
5 - 补充
5.1 永久代为何被 HotSpot VM 废弃?
5.2 为什么 Java 要分常量、简单类型、引⽤类型等
6 - 初学者容易混淆的地⽅
1 - 引⾔
摘录⼀些⽹上流传⽐较⼴泛的认识,但如果你认为只懂这些就够了,这篇⽂章就没有必要继续看下去了
常量池分为静态常量池、运⾏时常量池。
静态常量池在 .class 中,运⾏时常量池在⽅法区中,JDK 1.8 中⽅法区(method area)已经被元空间(metaspace)代替。
字符串池在JDK 1.7 之后被分离到堆区。
String str = new String("Hello world") 创建了 2 个对象,⼀个驻留在字符串池,⼀个分配在 Java 堆,
str 指向堆上的实例。
String.intern() 能在运⾏时向字符串池添加常量。
部分包装类实现了池化技术,-128~127 以内的对象可以重⽤。
本⽂的实例讲解都是针对 HotSpot 虚拟机的,如下图,⼀般 Oracle 官⽹上安装的 JDK 都使⽤该款虚拟机,使⽤ java -version 就能查看相关信息了。
2 - 常量池
2.1 你真的懂 Java的“字⾯量”和“常量”吗?
在计算机科学中,字⾯量(literal)是⽤于表达源代码中⼀个固定值的表⽰法(notation)。⼏乎所有计算机编程语⾔都具有对基本值的字⾯量表⽰,诸如:整数、浮点数以及字符串等 1 。整数是程序中最常⽤的数字,整数在 Java 中就是⼀个整数字⾯量,例如⼗进制的1、2、16等,16进制的0x01、0x0A等。Java 中的字符串字⾯量和其他⼤多数语⾔相同,将⼀系列字符⽤双引号括起来,如 "Hello world"等。
那么常量⼜是什么呢?如果是从 C/C++ 转过来的程序员,⼀般认为常量是被 const 修饰的变量或者某些宏定义,⽽在 Java
中,final 修饰的变量也可以被称为是常量。
但 Java 程序员的圈⼦⾥,常量不单单指 final 变量,任何具有不变性的东西我们将它称为常量也不会带来什么歧义。
偶尔会在某些论坛中看到“字符串是常量,不可修改”。那么,这种说法是从哪⾥来的呢?这就要提到到 Java 中 String 类的设计了,打开 String 的源码,我们看到前⾯的⼏⾏定义如下:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
}
我们看到不仅类定义使⽤ final 修饰,关键的字符数组同样声明为 private final。但这就能保证字符串
的不可修改性吗?并不能,final 修饰类定义只能使类不被继承,字符数组被 final 修饰只能保证 value 不能指向其他内存,但我们仍然可以通过 value[0] = 'V' 的⽅式直接修改 value 的内容。
String 是不可变,关键是因为 SUN 公司的⼯程师,在后⾯所有 String 的⽅法⾥很⼩⼼的没有去动数组⾥的元素,没有暴露内部成员字段。private final char value[] 这⼀句⾥,private的私有访问权限的作⽤都⽐ final ⼤。⽽且设计师还很⼩⼼地把整个 String 设成
final 禁⽌继承,避免被其他⼈继承后破坏。所以 String 是不可变的关键都在底层的实现,⽽不是⼀个 final。考验的是⼯程师构造数据类型,封装数据的功⼒ 2。
关于 String 的不可修改性更详细的内容请参考引⽤ 2 。
2.2 常量和静态/运⾏时常量池有什么关系?什么是常量池?
在Java程序中,有很多的东西是永恒的,不会在运⾏过程中变化。⽐如⼀个类的名字,⼀个类字段的名字/所属类型,⼀个类⽅法的名字/返回类型/参数名与所属类型,⼀个常量,还有在程序中出现的⼤量的字⾯值。⽽这些在JVM解释执⾏程序的时候是⾮常重要的。那么编译器将源程序编译成class⽂件后,会⽤⼀部分字节分类存储这些不变的代码,⽽这些字节我们就称为常量池 3。
Java 中静态/运⾏时常量池并⾮特指保存 final 常量,它还保存诸如字⾯量、类和接⼝全限定名、字段
、⽅法名称、修饰符等永恒不变的东西。
2.3 字节码下的常量池以及常量池的加载机制
JDK 1.8 下常量池存储的常量类型主要是字⾯量和符号引⽤。
下⾯是静态/运⾏时常量池的常量表类型:
常量表类型 标志值(占1 byte) 描述
CONSTANT_Utf8 1 UTF-8编码的Unicode字符串
CONSTANT_Integer 3 int类型的字⾯值
CONSTANT_Float 4 float类型的字⾯值
CONSTANT_Long 5 long类型的字⾯值
CONSTANT_Double 6 double类型的字⾯值
CONSTANT_Class 7 对⼀个类或接⼝的符号引⽤
CONSTANT_String 8 String类型字⾯值的符号引⽤
CONSTANT_Fieldref 9 对⼀个字段的符号引⽤
CONSTANT_Methodref 10 对⼀个类中⽅法的符号引⽤
CONSTANT_InterfaceMethodref 11 对⼀个接⼝中⽅法的符号引⽤
CONSTANT_NameAndType 12 对⼀个字段或⽅法的部分符号引⽤
下⾯讲⼀下符号引⽤:⼀个 Java 程序启动时加载了众多的类,有JDK的,也有我们⾃⼰定义的,那么我们怎么在程序运⾏的时候准确定位到类的位置呢?⽐如 String str = new String("xxx"),我们怎么在虚拟机内存中到 String 这个类的定义(或者说类的字节码)呢?
答案就在常量池的符号引⽤中。在未加载到JVM的时候,在 .class ⽂件的静态常量池中我们可以到这么⼀项 CONSTANT_Class,当然这⼀项仅仅只是符号引⽤,我们只知道有 java.lang.String 这么⼀个类。只有等 JVM 启动,并判断程序⽤到 java.lang.String 的时候才会加载 String 的 .class ⽂件到内存中(准确地说是⽅法区),之后,我们就可以在运⾏时常量池中将原本的符号引⽤替换为直接引⽤了。也就是说实际上我们的定位是依靠运⾏时常量池的,这也就是为什么运⾏时常量池对于动态加载⾮常重要的原因。
详细的内容可以了解⼀下 JVM 的类加载过程(加载、连接和初始化),如下图,将 .class ⽂件中的静态常量池转换为⽅法区的运⾏时常量池发⽣在“Loading”阶段,⽽符号引⽤替换为直接引⽤发⽣在 “Resolution”阶段。
我们特别关注 CONSTANT_Utf8、CONSTANT_String 这两种常量类型。
CONSTANT_Utf8:⽤ UTF-8 编码⽅式来表⽰程序中所有的重要常量字符串。这些字符串包括: ①类或接⼝的全限定名, ②超类的全限定名,③⽗接⼝的全限定名, ④类字段名和所属类型名,⑤类⽅法名和返回类型名、以及参数名和所属类型名,⑥字符串字⾯值。
每⼀个 CONSTANT_Utf8 常量项包括三项信息:length of byte array、length of string、string,以 System.out.println("Hello world") 为例,我们可以到下⾯这两个 utf8 常量项(out、println 相关常量项省略了)。
CONSTANT_String:字符串字⾯量都以 utf8 的形式存储,但是使⽤CONSTANT_Utf8 存储的各种类型字符串这么多,哪些是字符串字⾯量?哪些是全限定名字符串?所以需要⼀些指向该 utf8 项的符号引⽤常量来区分。CONSTANT_Class 的作⽤也是类似的,指向的是类全限定名的 utf8 项。
更加详细的内容参考《Java虚拟机规范 Java SE 8版》4。
2.4 是不是所有的数字字⾯量都会被存到常量池中?
看看下⾯的代码:
void main(){
int i = 1;
}
是不是能在常量池中到CONSTANT_Integer 为 1 的项呢?很遗憾,我们并没有到这么⼀项 ,直到 int i = 32768 我们才在表中到 CONSTANT_Integer 为 32768 的项。
为什么会出现这种情况呢?对于整数字⾯量来说,如果值在 -32768~32767 都会直接嵌⼊指令中,⽽不会保存在常量区。
对于 long、double 都有⼀些类似的情况,⽐如long l = 1L、double d = 1.0,都不到对应的常量项。
但是如果使⽤ final 修饰变量,将其定义成类常量(注意不是在⽅法体内定义的局部常量),结果⼜有所不同,如下:
class main{
final int i = 1;
}
jdk怎么使用此时,我们可以在常量池中到 CONSTANT_Integer 为 1 的项。
3 - 包装类对象池 =JVM 常量池
包装类的对象池(也有称常量池)和JVM的静态/运⾏时常量池没有任何关系。静态/运⾏时常量池有点类似于符号表的概念,与对象池相差甚远。
包装类的对象池是池化技术的应⽤,并⾮是虚拟机层⾯的东西,⽽是 Java 在类封装⾥实现的。打开 Integer 的源代码,到 cache 相关的内容:
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
* <p>
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
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.SavedProperty("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() {
}
}
/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
* <p>
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
IntegerCache 是 Integer 在内部维护的⼀个静态内部类,⽤于对象缓存。通过源码我们知道,Integer 对象池在底层实际上就是⼀个变量名为 cache 的数组,⾥⾯包含了 -128 ~ 127 的 Integer 对象实例。
使⽤对象池的⽅法就是通过 Integer.valueOf() 返回 cache 中的对象,像 Integer i = 10 这种⾃动装箱实际上也是调⽤
Integer.valueOf() 完成的。
如果使⽤的是 new 构造器,则会跳过 valueOf(),所以不会使⽤对象池中的实例。
Integer i1 = 10;
Integer i2 = 10;
Integer i3 = new Integer(10);
Integer i4 = new Integer(10);
Integer i5 = Integer.valueOf(10);
System.out.println(i1 == i2); // true
System.out.println(i2 == i3); // false
System.out.println(i3 == i4); // false
System.out.println(i1 == i5); // true
注意到注释中的⼀句话 “The cache is initialized on first usage”,缓存池的初始化在第⼀次使⽤的时候
已经全部完成,这涉及到设计模式的⼀些应⽤。这和常量池中字⾯量的保存有很⼤区别,Integer 不需要显⽰地出现在代码中才添加到池中,初始化时它已经包含了所有需要缓存的对象。
4 - 字符串池
字符串池也是类似于对象池的这么⼀种概念,但它是 JVM 层⾯的技术。
在 JDK 1.6 以及以前的版本中,字符串池是放在 Perm 区(Permanent Generation,永久代)。Perm 区是⼀个类静态的区域,主要存储⼀些加载类的信息,常量池,⽅法⽚段等内容,容量是固定的,默认在 32 M 到 96 M 间,我们可以通过 -XX:MaxPermSize = N 来配置永久代的⼤⼩,但是在运⾏过程中它仍然还是固定⼤⼩的。也有说 Perm 区实际上就是 HotSpot 下的⽅法区,HotSpot 的开发⼈员更愿意将⽅法区称为 Permanent Generation,这⾥我们不做过多的探讨。
在 JDK 1.7 的版本中,字符串池移到Java Heap。在 JDK 1.8 中永久代的说法被废弃,元空间成为⽅法区的替代品。(本⽂ 5.1 章节补充关于为什么永久代被废弃)
4.1 字符串池的实现——StringTable
由于字符串池是虚拟机层⾯的技术,所以在 String 的类定义中并没有类似 IntegerCache 这样的对象池,String 类中提及缓存/池的概念只有intern() 这个⽅法,我将部分注释做了⼀些翻译和删减:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论