JVM面试题详解系列——Java中几种常量池的区分

在学习JVM相关知识时,尤其时学习JVM内存区域相关内容时,经常遇到各种常量池的概念,每次都是遇到都是模棱两可,让我仔细说一说,可能有说不清楚,其实我在写上一篇文章——JVM面试题详解系列——JVM内存区域详解,就想介绍这个问题。但是这个概念我认为还是很重要而且很难区分,所以我想单独写一篇文章详细介绍一下。
这些都是我看了许多资料加上我的理解总结的,难免会有理解不到位的,如果由错误的地方麻烦大家在评论区@我,提出指正意见。

字符串常量池(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程序中通常有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存,减少OOM错误。

class文件中的常量池(class constant pool)

class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。

在Java源文件被编译成class文件时,其中的所有常量都会被存储在常量池中,而且每个常量在常量池中都有一个唯一编号,可以通过该编号来引用常量池中的常量。

常量池中包含了14种不同类型的常量,每种常量都有一个标志位tag来标识其类型。常量池中的常量可以是数字、字符串、类和接口的符号引用等。常量池的使用可以提高程序的性能和可读性。

常量池中每一项常量都是一个表,这 14 种表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型。

为什么单独设置class文件常量池

Class文件常量池的设计是为了解决Java虚拟机中内存分配和效率问题。在Java程序中,常量通常在编译时就被确定了。如果每次程序运行时都需要重新分配内存来存储这些常量,将会浪费宝贵的内存资源,同时也会降低程序的运行效率。因此,在Java源文件编译时,编译器将常量保存在一个单独的常量池中,并在运行时将常量池加载到内存中,同时将其中的符号引用解析为实际的对象和值。这种常量池的设计不仅可以提高内存利用率,还可以加快类加载、解析和执行代码的速度。

另外,在运行时,如果需要修改常量池中的常量,只需要修改常量池中的相应项,不需要重新进行内存分配和释放,这节约了时间和资源。尽管这段话中提到了方法区,但方法区是用于存储类的元数据信息和常量池等数据的,不是用于存储常量的。因此,常量池的设计既可以提高内存利用率,也可以加快程序的运行速度,在Java程序中有着重要的作用。

如果每次程序运行时都需要重新分配内存来存储这些常量,将会浪费宝贵的内存资源,同时也会降低程序的运行效率。

在Java程序中,使用常量池确实需要将常量池加载到内存中,这样会占用一定的内存空间。但是相对于每次程序运行时都重新分配内存来存储常量,使用常量池的内存占用是很小的。

首先,常量池中的常量是被共享的,即如果多个对象共享同一个常量,它们在内存中只会有一个实例。这样就可以避免为每个对象都分配一份常量的内存空间,从而节省了内存资源。

其次,常量池中存放的常量一般都是基本类型或字符串等简单的对象,它们的大小相对较小。与其每次都重新分配一份内存空间,使用常量池在运行时将常量加载到内存中,常量池的大小一般都是在可控范围之内的。

最后,使用常量池不仅可以节省内存空间,还可以提高程序的性能和效率。在运行时,如果需要用到常量池中的常量,可以直接从常量池中获取它们的值,避免了频繁的内存分配和释放,从而提高了程序的运行效率。

总之,虽然使用常量池也需要将常量池加载到内存中,但相对于每次都重新分配内存来存储常量,它的内存占用是很小的,并且使用常量池还可以提高程序的性能和效率。

在运行时,如果需要修改常量池中的常量,只需要修改常量池中的相应项,不需要重新进行内存分配和释放,这节约了时间和资源。

在Java程序中,如果需要修改常量池中的常量(例如字符串中的字符被修改),则可以通过修改常量池中的相应项实现,而不需要重新进行内存分配和释放。

这是因为常量池中的常量在运行时是只读的,一旦被存储到常量池中,就不能再改变它们的值。然而,如果需要修改常量池中的常量,可以通过取出常量池中的常量,修改常量的值,然后重新将修改后的常量保存回常量池中,从而实现修改常量的目的。

由于常量池中的常量是被共享的,因此如果多个对象共享同一个常量,修改常量池中的常量就会影响到所有引用该常量的对象。这也是Java中常量被称为不可变量的原因之一。

总之,通过在运行时修改常量池中的常量,可以避免重新进行内存分配和释放,这样可以节省时间和资源。

字面量

字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。

在计算机编程领域,字面量(literal)是指程序中硬编码的常量值,以及该常量的类型。比如,数字123、字符串"hello world"、true/false布尔值等就是字面量。

字面量通常是编译时就准备好的,程序执行时直接使用,不需要再进行计算或解释,即它们的值可以直接使用或检查。在代码中使用字面量具有以下优点:

  1. 代码可读性好,易于理解:由于字面量代表常量,直接写在程序中,直接阅读代码就可以理解它们代表的值,不需要查找其他地方的定义。

  2. 方便查错:字面量用于定义固定的常量值,可以降低因常量值错误带来的bug和问题。

  3. 编译器优化:编译器可以在编译时就进行一些优化,比如将字面量操作转换成简单的指令,提高程序的运行效率。

总之,字面量在程序中应用广泛,是编程中十分重要的概念。

符号引用

符号引用是一组用来描述所引用目标的符号,属于编译原理方面的概念,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,主要包括下面几类常量:

  1. 被模块导出或者开放的包(Package)
  2. 类和接口的全限定名(Full Qualified Name)
  3. 字段的名称和描述符(Descriptor)
  4. 方法的名称和描述符
  5. 方法的句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
  6. 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

很多资料只提到了2—4,大家可以重点记一下。

在Java虚拟机中,符号引用是一种编译时的符号名称,它是指用来描述类、接口、字段和方法的名称。

简单来说,符号引用就是一个符号名称,比如类名、字段名、方法名等,它并不指向实际的内存地址,在运行时需要通过解析符号引用得到对应的实际内存地址。 换句话说,符号引用就是一种用于在程序中表示对某个类、属性或方法的引用的标记。

符号引用在Java程序中的使用很广泛,因为Java源代码中引用的类、属性或方法可能在编译时并没有被定义,而只在运行时才被加载到内存中。因此,编译器在编译Java源代码时使用符号引用,而在Java虚拟机运行时才根据符号引用解析出对应的实际内存地址,并执行对应的代码。

在Java虚拟机中,由于符号引用不直接指向实际内存地址,因此需要一些机制来解析符号引用,例如类加载器会加载字节码文件到内存中,并将符号引用转化为直接引用,或者JIT编译器在运行时第一次解析符号引用时,将其转换为直接引用。

直接引用

在JVM 类加载过程中,解析阶段,Java虚拟机将常量池内的符号引用替换为直接引用。直接引用可以帮助程序直接定位到所需的对象。

直接引用一般为下面三类:

  1. 直接指向目标的指针
  2. 相对偏移量
  3. 一个能够直接定位到目标的句柄
句柄

句柄是一个是用来标识对象或者项目的标识符,可以用来描述窗体、文件等,值得注意的是句柄不能是常量

偏移量

计算机汇编语言,是指把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为“有效地址或偏移量”。

直接引用适合虚拟机的布局相关,同一个符号引用在不同的虚拟机上翻译出来的直接引用一般会不一致。如果有了直接引用,那么引用目标必定已经被加载到了内存当中。

运行时常量池(runtime constant pool)

当Java文件被编译成class文件之后,也就是会生成我上面所说的class常量池,那么运行时常量池又是什么时候产生的呢?

JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,JVM就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在上面我也说了,class文件的常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中所引用的是一致的。

运行时常量池相对于class文件的常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。

总结:运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与字符串常量池中的引用值保持一致。

在这里插入图片描述

解析符号引用时,会查询字符串常量池,并将符号引用转换为直接引用,与字符串常量池中的引用值保持一致。

在Java中,符号引用是一种编译时的引用,它并不指向具体的内存地址,而是用来描述所引用的目标的名称、描述符和方法类型等信息。而直接引用则是指向具体内存地址的指针,可以直接访问对象或方法的实际位置。

在Java中,符号引用在代码编译的时候就已经确定了,而直接引用只有在类加载的时候才能确定。因此,在解析符号引用时,需要将其转换为直接引用,才能继续执行代码。在这个转换过程中,需要查询字符串常量池中的引用值,来确认目标的名称和描述符等信息,以便找到对应的实际位置。因此,为了保证符号引用转换为直接引用的正确性,在解析符号引用时需要查询字符串常量池,并将符号引用转换为与字符串常量池中引用值保持一致的直接引用。

可不可以理解直接引用的值是字符串常量池中某个引用的值

是的,可以这样理解。在Java中,字符串常量池是一个保存字符串字面值和符号引用的数据结构。当解析符号引用时,就是将符号引用转换为一个与实际目标相对应的直接引用,而这个直接引用的值就是字符串常量池中与该符号引用对应的引用值。因此,可以理解为直接引用的值是字符串常量池中某个引用的值。

为什么单独设置运行时常量池

运行时常量池是为了加速Java程序的运行和节省内存资源而设置的。在Java虚拟机中,一些常量是在Java程序运行时动态生成的,如字符串连接表达式、调用方法返回值等。如果每次在程序执行这些操作时都需要重新分配内存来存储常量,会显著降低Java程序的性能。

因此,在运行时常量池中,JVM会将class文件常量池中的符号引用解析为实际的对象和值,并存储在运行时常量池中。这样,当程序需要使用这些常量时,只需要在运行时常量池中查找对应的值即可,而不需要重新分配内存,这样可以加快程序的执行速度,提高Java程序的效率。

另外,由于运行时常量池是每个线程私有的,相比于其他共享区域,如方法区,它更不容易发生线程安全问题。在多线程环境中,如果每个线程都有自己的运行时常量池,能够有效地保障线程安全,避免线程之间的干扰。

总之,运行时常量池的设计提高了Java程序的执行效率,同时也能保障程序的线程安全。在Java程序中,常常用到的字符串、数字等常量值都存储在运行时常量池中,它是Java虚拟机中的重要组成部分之一。

三种常量池之间的关系

在Java虚拟机中,有三种不同类型的常量池:字符串常量池、class文件常量池和运行时常量池。这三种常量池之间存在着紧密的关系。

首先,编译Java源文件时,编译器会将其中的所有字符串字面量和其他编译期声明的常量存储在class文件常量池中。因此,class文件常量池相当于是所有编译时常量的根源。

其次,当Java程序在运行时,JVM会将class文件常量池复制到内存中形成运行时常量池,以供程序在运行时动态使用。运行时常量池是每个线程私有的,用于存储常量池中常量的实际值,同时也存储着类、方法等的相关信息。

最后,字符串常量池是一种特殊的、系统级别的常量池,它用于存储所有字符串字面量的实例,以及其他头文件声明的常量实例,它是在程序运行时被创建。

因此,这三个常量池之间的关系如下:

  • class文件常量池:是所有编译时常量的根源,编译器会将其中的所有字符串字面量和其他编译期声明的常量存储在其中。
  • 运行时常量池:是从class文件常量池中复制得到的,在程序运行时会动态使用其中的常量。作为每个线程私有的内存区域,它存储着常量池中常量的实际值,同时也包含类、方法等的相关信息。
  • 字符串常量池:用于存储所有字符串字面量的实例,以及其他头文件声明的常量实例,它是在程序运行时被创建。

总之,这三个常量池之间相互衔接,紧密联系。在Java程序的编译、加载、解析和执行过程中都发 挥着不可替代的作用。

静态变量和常量有什么区别

静态变量和常量在Java中是有区别的,虽然它们都是属于类级别的。

  1. 可变性

静态变量是可以被修改的,而常量是不可被修改的。静态变量可以在程序运行时进行修改,而常量在程序运行时是不可变的。因为静态变量可以被修改,所以它们的值在程序运行期间是可以发生改变的;而常量一般在定义时就确定了它们的值,并且值在程序运行期间不会发生改变。

  1. 初始化

静态变量在定义时需要显式地初始化,否则会使用默认值进行初始化。常量在定义时必须进行初始化,否则会发生编译错误。

  1. 访问方式

在访问静态变量时,需要使用类名来进行访问,例如MyClass.myStaticVariable。而常量可以通过类名或对象来进行访问,例如MyClass.MY_CONSTANT或者myClassInstance.MY_CONSTANT

  1. 内存占用

静态变量在类被加载时就会被分配内存空间,而常量不会被分配额外空间,常量被编译器存储到常量池中,该常量池保存在.class文件中。

总之,静态变量和常量在可变性、初始化、访问方式和内存占用等方面存在差异。静态变量可以在程序运行时修改其值,而常量在程序运行时是不可变的。静态变量需要显式地初始化,而常量在定义时必须初始化。静态变量需要使用类名来进行访问,而常量可以通过类名或对象来进行访问。静态变量在被加载时会分配内存空间,而常量不会分配额外空间。

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

路上阡陌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值