字符串常量池(string pool)
字符串常量池是JVM为了提升性能和减少内存消耗针对字符串(String类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
当需要使用字符串时,先去字符串池中查看该字符串是否已经存在,如果存在,则可以直接使用,如果不存在,初始化,并将该字符串放入字符串常量池中。
字符串常量池的位置也是随着JDK版本的不同而位置不同。在JDK1.6及之前,字符串常量池的位置在永久代中,此时字符串常量池中存储的是字符串对象;在JDK1.7时,字符串常量池的位置从永久代移动到了Java堆中,此时,字符串常量池存储的就是字符串对象的引用,具体的实例对象是在堆中开辟的一块空间存放的;在JDK1.8及之后,永久代被元空间取代了。
HotSpot 虚拟机中字符串常量池的实现是 src/hotspot/share/classfile/stringTable.cpp ,StringTable 本质上就是一个HashSet ,容量为 StringTableSize(可以通过 -XX:StringTableSize 参数来设置)。
StringTable 中保存的是字符串对象的引用,字符串对象的引用指向堆中的字符串对象。
注意:在JDK1.7时,静态变量和字符串常量池一起从永久代中移动到了Java堆中。
JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为永久代的GC回收效率太低,只有在整堆收集(Full GC)的时候才会被执行GC,而Java程序中通常有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
class文件中的常量池(class constant pool)
class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。
在Java源文件被编译成class文件时,其中的所有常量都会被存储在常量池中,而且每个常量在常量池中都有一个唯一编号,可以通过该编号来引用常量池中的常量。
常量池中每一项常量都是一个表,这 14 种表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型。
为什么单独设置class文件常量池
Class文件常量池是为了解决Java虚拟机中内存分配和效率问题而设置的。具体来说,Java虚拟机中的内存是通过JVM在内存中开辟一块特殊区域来管理的,而这个特殊区域叫做方法区(Method Area),其中包含了Java程序中所有的类、接口、字段、方法等信息。而Java程序中的常量通常是在编译时确定的,如果每次程序运行时都需要重新分配内存来存储这些常量,会严重浪费内存资源和降低程序运行效率。
因此,在编译Java源文件时,编译器会将其中的所有常量存储在一个单独的常量池中,然后在运行时将这个常量池加载到内存中,同时将其中的符号引用解析为实际的对象和值。 这种常量池的设计,不仅可以提高内存利用率,还可以加快类加载、解析和执行代码的速度。同时,在运行时,如果需要修改常量池中的常量,只需要修改常量池中的相应项就可以了,无需重新进行内存分配和释放,也就节约了时间和资源。
总之,Java中的常量池设计,既可以提高内存利用率,又可以加快程序的运行速度,在Java程序中有着重要的作用。
字面量
字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。
在计算机编程领域,字面量(literal)是指程序中硬编码的常量值,以及该常量的类型。比如,数字123、字符串"hello world"、true/false布尔值等就是字面量。
字面量通常是编译时就准备好的,程序执行时直接使用,不需要再进行计算或解释,即它们的值可以直接使用或检查。在代码中使用字面量具有以下优点:
-
代码可读性好,易于理解:由于字面量代表常量,直接写在程序中,直接阅读代码就可以理解它们代表的值,不需要查找其他地方的定义。
-
方便查错:字面量用于定义固定的常量值,可以降低因常量值错误带来的bug和问题。
-
编译器优化:编译器可以在编译时就进行一些优化,比如将字面量操作转换成简单的指令,提高程序的运行效率。
总之,字面量在程序中应用广泛,是编程中十分重要的概念。
符号引用
符号引用是一组用来描述所引用目标的符号,属于编译原理方面的概念,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,主要包括下面几类常量:
- 被模块导出或者开放的包(Package)
- 类和接口的全限定名(Full Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
- 方法的句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
- 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
很多资料只提到了2—4,大家可以重点记一下。
在Java虚拟机中,符号引用是一种编译时的符号名称,它是指用来描述类、接口、字段和方法的名称。
简单来说,符号引用就是一个符号名称,比如类名、字段名、方法名等,它并不指向实际的内存地址,在运行时需要通过解析符号引用得到对应的实际内存地址。 换句话说,符号引用就是一种用于在程序中表示对某个类、属性或方法的引用的标记。
符号引用在Java程序中的使用很广泛,因为Java源代码中引用的类、属性或方法可能在编译时并没有被定义,而只在运行时才被加载到内存中。因此,编译器在编译Java源代码时使用符号引用,而在Java虚拟机运行时才根据符号引用解析出对应的实际内存地址,并执行对应的代码。
在Java虚拟机中,由于符号引用不直接指向实际内存地址,因此需要一些机制来解析符号引用,例如类加载器会加载字节码文件到内存中,并将符号引用转化为直接引用,或者JIT编译器在运行时第一次解析符号引用时,将其转换为直接引用。
直接引用
在JVM 类加载过程中,解析阶段,Java虚拟机将常量池内的符号引用替换为直接引用。直接引用可以帮助程序直接定位到所需的对象。
直接引用一般为下面三类:
- 直接指向目标的指针
- 相对偏移量
- 一个能够直接定位到目标的句柄
句柄
句柄是一个是用来标识对象或者项目的标识符,可以用来描述窗体、文件等,值得注意的是句柄不能是常量
偏移量
计算机汇编语言,是指把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为“有效地址或偏移量”。
直接引用适合虚拟机的布局相关,同一个符号引用在不同的虚拟机上翻译出来的直接引用一般会不一致。如果有了直接引用,那么引用目标必定已经被加载到了内存当中。
运行时常量池(runtime constant pool)
当Java文件被编译成class文件之后,也就是会生成我上面所说的class常量池,那么运行时常量池又是什么时候产生的呢?
JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,JVM就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在上面我也说了,class文件的常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中所引用的是一致的。
运行时常量池相对于class文件的常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
总结:运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与字符串常量池中的引用值保持一致。
为什么单独设置运行时常量池
运行时常量池是为了加速Java程序的运行和节省内存资源而设置的。在Java虚拟机中,一些常量是在Java程序运行时动态生成的,如字符串连接表达式、调用方法返回值等。如果每次在程序执行这些操作时都需要重新分配内存来存储常量,会显著降低Java程序的性能。
因此,在运行时常量池中,JVM会将class文件常量池中的符号引用解析为实际的对象和值,并存储在运行时常量池中。这样,当程序需要使用这些常量时,只需要在运行时常量池中查找对应的值即可,而不需要重新分配内存,这样可以加快程序的执行速度,提高Java程序的效率。
另外,由于运行时常量池是每个线程私有的,相比于其他共享区域,如方法区,它更不容易发生线程安全问题。在多线程环境中,如果每个线程都有自己的运行时常量池,能够有效地保障线程安全,避免线程之间的干扰。
总之,运行时常量池的设计提高了Java程序的执行效率,同时也能保障程序的线程安全。在Java程序中,常常用到的字符串、数字等常量值都存储在运行时常量池中,它是Java虚拟机中的重要组成部分之一。
三种常量池之间的关系
在Java虚拟机中,有三种不同类型的常量池:字符串常量池、class文件常量池和运行时常量池。这三种常量池之间存在着紧密的关系。
首先,编译Java源文件时,编译器会将其中的所有字符串字面量和其他编译期声明的常量存储在class文件常量池中。因此,class文件常量池相当于是所有编译时常量的根源。
其次,当Java程序在运行时,JVM会将class文件常量池复制到内存中形成运行时常量池,以供程序在运行时动态使用。运行时常量池是每个线程私有的,用于存储常量池中常量的实际值,同时也存储着类、方法等的相关信息。
最后,字符串常量池是一种特殊的、系统级别的常量池,它用于存储所有字符串字面量的实例,以及其他头文件声明的常量实例,它是在程序运行时被创建。
因此,这三个常量池之间的关系如下:
- class文件常量池:是所有编译时常量的根源,编译器会将其中的所有字符串字面量和其他编译期声明的常量存储在其中。
- 运行时常量池:是从class文件常量池中复制得到的,在程序运行时会动态使用其中的常量。作为每个线程私有的内存区域,它存储着常量池中常量的实际值,同时也包含类、方法等的相关信息。
- 字符串常量池:用于存储所有字符串字面量的实例,以及其他头文件声明的常量实例,它是在程序运行时被创建。
总之,这三个常量池之间相互衔接,紧密联系。在Java程序的编译、加载、解析和执行过程中都发挥着不可替代的