【底层原理之旅—攻克你的技术盲点之JVM常量池】|Java 刷题打卡

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 【底层原理之旅—攻克你的技术盲点之JVM常量池】|Java 刷题打卡

题目


攻克你的技术盲点之JVM常量池




知识点



什么是常量


用final修饰的成员变量表示常量,值一旦给定就无法改变



  • final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量






JVM中的方法区

JVM的方法区里存放着类的版本,字段,方法,接口和常量池。常量池里存储着字面量和符号引用





Java中的常量池


实际上分为两种形态:静态常量池和运行时常量池



image.png



静态常量池

静态常量池,即class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间



class常量池

image.png



  • 当java文件被编译成class文件之后,会在class文件中生成我们所说的class常量池。
  • class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的【各种字面量】(文本字符串、被声明为final的常量、基本数据类型的值)和【符号引用】(类和接口的全限定名、字段的名称和描述符、方法的名称和描述符),这部分内容将在类加载后进入方法区的运行时常量池中存放。


image.png


常量池中存放的符号信息,在JVM执行指令时需要依赖使用。常量池中的所有项都具有如下通用格式:

cp_info {
    u1 tag;     //表示cp_info的单字节标记位
    u1 info[];  //两个或更多的字节表示这个常量的信息,信息格式由tag的值确定
}
复制代码

支持的类型信息如下:


image.png


以CONSTANT_Class为例,它用于表示类或者接口,格式如下:

CONSTANT_Class_info {
 u1 tag;
 u2 name_index;
复制代码



  • CONSTANT_Class_info类型是由一个tag和一个name_index组成。


  • tag:这个值为CONSTANT_Class (7),代表着属于一个类的引用(CONSTANT_Fieldref代表字段引用)
  • name_index中的index表示它是一个索引,引用的是CONSTANT_UTF8_info




  • CONSTANT_Utf8_info用于表示字符常量的值,结构如下所示:
CONSTANT_Utf8_info {
 u1 tag;
 u2 length;
 u1 bytes[length];
}
复制代码




  • tag表示为:CONSTANT_Utf8(1),代表着属于一个字符串值得引用
  • length:指明了bytes[]数组的长度bytes[]数组引用了上一个length作为其长度字符常量采用改进过的UTF-8编码表示。




多余说一句:对于静态常量池我们需要知道它存在于编译器,如果说与运行时有关的话,可以说运行时中的常量是JVM加载class文件之后进行分配的


运行时常量池


运行时常量池,运行时常量池是方法区的一部分,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池


  • 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
  • 当类加载到内存中后,JVM就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个



  • 运行时常量池用来动态获取类信息,包括:class文件元信息描述、编译后的代码数据、引用类型数据、类文件常量池的其他数据等
  • class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值,加载阶段:将每个class常量池中的符号引用值转存到运行时常量池中。
  • 之后经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的string pool,以保证运行时常量池所引用的字符串与字符串常量池中所引用的是一致的





字符串常量池(string pool)


字符串常量池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中记住:在jdk1.8后string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。string pool在每个HotSpot VM的实例只有一份,被所有的类共享。



  • 字符串池里的内容是在类加载完成,经过验证、准备阶段之后存放在字符串常量池中。关于字符串常量池的具体实现我们这里先不展开,后面用专门的文章来进行讲解。
  • 字符串常量池的处理机制我们前面文章已经讲到,只会存储一份,被所有的类共享。基本流程是:创建字符串之前检查常量池中是否存在,如果存在则获取其引用,如果不存在则创建并存入,返回新对象引用




不同版本的字符串常量池


字符串常量池随着JDK版本的演化所在的位置也在不断的变化,下面我们会专门用图讲解一下。

image.png在JDK1.7字符串常量池和静态变量被从方法区拿到了堆中,运行时常量池剩下的还在方法区, 也就是hotspot中的永久代。

image.png


在JDK8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆,运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间(Metaspace)

image.png





需要注意的点:


常量池的好处


常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。



例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。



  1. 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间
  2. 节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等







拓展延伸


String s1=new String("abc"); 
复制代码


首先你要理解常量池,  这是一个特殊的共享区域,literate(符号引用), Class这些可以在内存中共享的不经常改变的东西,都可以放在这里。



  • 上面的代码会有两个String被创建,一个是你的Class被ClassLoader加载时,你的"abc"被作为常量读入,在字符串常量池里创建了一个共享的"abc"
  • 然后,当调用到new  String("abc")的时候,会在heap里创建这个new  String("abc");

考虑类加载阶段和实际执行时。




  • 类加载对一个类只会进行一次。"abc"在类加载时就已经创建并驻留了(如果该类被加载之前已经有"abc"字符串被驻留过则不需要重复创建,直接使用驻留的"abc"实例)。驻留的字符串是放在全局共享的字符串常量池中的
  • "abc"字面量对应的String实例已经固定了,不会再被重复创建。所以这段代码将常量池中的对象复制一份放到heap中,并且把heap中的这个对象的引用交给s1持有

public class Test{
    public static String a = "a";
    public static void main(){
        String b = "b";
    }
}
复制代码


使用Java自带的反编译工具反编译一下,编译后输入javap -verbose Test.cass


image.png

可以发现两个静态String变量都放入了常量池中


public class Test2{
    public static String str = "laji" + "MySQL";
    public static void main(){
    }
}
复制代码

在编译前先分析一波,按理说,既然是静态String常量,那么理应出现在常量池(Constant Pool)中


image.png


不会出现单独的两个元素的字符串常量,只会存在连接以后的数据。


public class Test2_2{
    public static void main(String[] args){
        String string1 = "laji";  
        String string2 = "MySQL";  
        String string3 = string1+string2;  
        String string4 = string1+"C";   
    }
}


image.png



  • 对于直接做+运算的两个字符串(字面量)常量,并不会放入String常量池中,而是直接把运算后的结果放入常量池中。
  • 对于先声明的字符串字面量常量,会放入常量池,但是若使用字面量的引用进行运算就不会把运算后的结果放入常量池中了



总结一下就是JVM会对String常量的运算进行优化,未声明的,只放结果;已经声明的,只放声明。


public class Test3{
    public static void main(String[] args){
        String str = "laji";
        String str2 = new String("MySQL");
        String str3 = new String("laji");
        System.out.println(str==str3);// 运行后结果为false
    }
}


image.png


首先是new一个对象时,明明是在堆中实例化一个对象,怎么会出现常量池中?

这里的"MySQL"并不是字符串常量出现在常量池中的,而是以字面量出现的,实例化操作(new的过程)是在运行时才执行的,编译时并没有在堆中生成相应的对象。



最后输出的结果之所以是false,就是因为str指向的”laji”是存放在常量池中的,而str3指向的”laji”是存放在堆中的,==比较的是引用(地址),当然是false


public class Test4{
    public static void main(String[] args){
        String str = "laji";
        String str2 = new String("laji");
        String str3 = null;
        System.out.println(str==str2);// 运行后结果为false
        str3 = str2.intern();
        System.out.println(str==str3);// 运行后结果为true
    }
}
复制代码


显然,str3在初始化的时候是从字符串常量池中获取到的值。



JDK1.7中JVM把String常量区从方法区中移除了;JDK1.8中JVM把String常量池移入了堆中,同时取消了“永久代”,改用元空间代替(Metaspace)

import java.util.ArrayList;
public class TestString {
    public static void main(String[] args) {
            String str = "abc";  
            char[] array = {'a', 'b', 'c'};  
            String str2 = new String(array);  
            //使用intern()将str2字符串内容放入常量池  
            str2 = str2.intern();  
            //这个比较用来说明字符串字面常量和我们
            //使用intern处理后的字符串是在同一个地方  
            System.out.println(str == str2);  
    }
}
复制代码






总结一下


  • 静态变量(静态常量池)处于编译器,存在于class文件内,可通过javap - verbose命令查看字符串合并时查看的是静态常量池里面的内容;



  • 字符串常量池曾经属于运行时常量池的一部分,位于方法区,但随着JVM版本的演变,二者已经分开。在JDK8以后字符串常量池位于堆中,而运行时常量池位于方法区






















相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
打赏
0
0
0
0
376
分享
相关文章
单位网络监控软件:Java 技术驱动的高效网络监管体系构建
在数字化办公时代,构建基于Java技术的单位网络监控软件至关重要。该软件能精准监管单位网络活动,保障信息安全,提升工作效率。通过网络流量监测、访问控制及连接状态监控等模块,实现高效网络监管,确保网络稳定、安全、高效运行。
86 11
Java注解的底层源码剖析与技术认识
Java注解(Annotation)是Java 5引入的一种新特性,它提供了一种在代码中添加元数据(Metadata)的方式。注解本身并不是代码的一部分,它们不会直接影响代码的执行,但可以在编译、类加载和运行时被读取和处理。注解为开发者提供了一种以非侵入性的方式为代码提供额外信息的手段,这些信息可以用于生成文档、编译时检查、运行时处理等。
91 7
智慧产科一体化管理平台源码,基于Java,Vue,ElementUI技术开发,二开快捷
智慧产科一体化管理平台覆盖从备孕到产后42天的全流程管理,构建科室协同、医患沟通及智能设备互联平台。通过移动端扫码建卡、自助报道、智能采集数据等手段优化就诊流程,提升孕妇就诊体验,并实现高危孕产妇五色管理和孕妇学校三位一体化管理,全面提升妇幼健康宣教质量。
46 12
SaaS云计算技术的智慧工地源码,基于Java+Spring Cloud框架开发
智慧工地源码基于微服务+Java+Spring Cloud +UniApp +MySql架构,利用传感器、监控摄像头、AI、大数据等技术,实现施工现场的实时监测、数据分析与智能决策。平台涵盖人员、车辆、视频监控、施工质量、设备、环境和能耗管理七大维度,提供可视化管理、智能化报警、移动智能办公及分布计算存储等功能,全面提升工地的安全性、效率和质量。
CRaC技术助力ACS上的Java应用启动加速
容器计算服务借助ACS的柔性算力特性并搭配CRaC技术极致地提升Java类应用的启动速度。
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
JavaFX是Java的下一代图形用户界面工具包。JavaFX是一组图形和媒体API,我们可以用它们来创建和部署富客户端应用程序。 JavaFX允许开发人员快速构建丰富的跨平台应用程序,允许开发人员在单个编程接口中组合图形,动画和UI控件。本文详细介绍了JavaFx的常见用法,相信读完本教程你一定有所收获!
2223 2
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
建筑施工一体化信息管理平台源码,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
智慧工地云平台是专为建筑施工领域打造的一体化信息管理平台,利用大数据、云计算、物联网等技术,实现施工区域各系统数据汇总与可视化管理。平台涵盖人员、设备、物料、环境等关键因素的实时监控与数据分析,提供远程指挥、决策支持等功能,提升工作效率,促进产业信息化发展。系统由PC端、APP移动端及项目、监管、数据屏三大平台组成,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
109 7
JVM原理与实现——Synchronized关键字
在多线程Java程序中,`Synchronized`关键字用于确保线程安全。本文深入探讨其工作原理,通过分析字节码`monitorenter`和`monitorexit`,解释JVM如何实现同步机制。文章展示了`Synchronized`方法的编译结果,并详细解析了轻量锁和重度锁的实现过程,包括Mark Word的状态变化及CAS操作的应用。最后简要介绍了`ObjectMonitor::enter()`函数在获取重度锁时的作用。
JVM原理与实现——Synchronized关键字
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
622 166
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
742 1

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等