【JVM】JVM基础知识:常量池、类加载、JVM内存模型、对象的创建过程

2023-11-16

前言

讲一下大概的内容(类加载-JVM内存模型-对象的创建-垃圾回收-JVM调优(入门))

最近抽了时间学了一直都很想学的 JVM,之前也学过一点,也发布过一些零散的文章,但这篇文章会更加全面,学完这篇文章就足以应对有关 JVM 的面试(如果遇到没有的题,请写在评论区)。

本文主要从以下六个方面来讲述:

  1. 类加载之前
  2. 类加载。
  3. JVM 内存模型。
  4. 对象的创建。
  5. 垃圾回收。
  6. JVM 调优(入门)。

本文主要按照对象的生命周期:字节码文件的编译到被加载进 JVM,再到对象的创建,最后到各种垃圾收集器对对象的回收流程,最后的最后再补上 JVM 调优。

类加载之前

在类加载之前,我们必然需要写好一段代码,并将这段代码编译好,得到一个 class 文件,这个文件就是我们所说的字节码文件,一说到字节码就会提到静态常量池,还有运行时常量池是什么?

这一部分我们就来普及这些概念:

  1. 字节码
  2. 常量池( 静态常量池 & 运行时常量池、字面量 & 符号引用)

字节码

首先,我们需要一个 java 文件,像这样。
在这里插入图片描述

然后,编译它,使用javac User.java或者 IDEA 编辑器编译都可以。

然后得到一个 User.class,这就是字节码文件。
在这里插入图片描述

字节码的含义

当我们使用文本编辑工具打开字节码文件,会看到很多很多根本看不懂的十六进制数(除了最前面的cafe babe,这个数被称为魔数,用来标识这个文件是 java 的字节码文件)。

在这里插入图片描述

如果想要一个字一个字地去解读这份字节码文件,可以翻看我之前的文章,但这个不是特别重要,毕竟我们不需要敲字节码来开发,这些都是给虚拟机看的。

我们可以使用javap -v User.class命令来把字节码编译成更加可读的指令码。

javap -v User.class > User.txt 这个命令可以把指令码文件输出到 User.txt 中,于是我们可以得到这样一个文件:

在这里插入图片描述

好像变得更容易读懂了,又好像没有那么容易读懂。

像这里的 Constant pool 就是我们常说的静态常量池,后续会进行解读,而 Code 里面的就是指令码,想要读懂指令码就需要参照 Java指令码表。

常量池

我们可以在字节码中看到静态常量池,它和运行时常量池有什么关系?还有字符串常量池、基础类型常量池又是什么?

静态常量池

静态常量池说的就是字节码文件中的 Constant pool,这里存放的都是字面量符号引用

在这里插入图片描述

字面量

字面量就是那种我们写在代码中的字符串或者基本数据类型的值,比如下图的"11",1

在这里插入图片描述

但如果把这个代码编译成字节码,我们只能看到字符串的字面量出现在 Constant pool 中,而不会出现在 Code 中。

符号引用

在这里插入图片描述

符号引用的主要表现形式是字符串,主要包括以下三种类型:

  • 类和接口的全限定名(比如上图的#4、#5)
  • 字段的名称和描述符(比如#2、#3)
  • 方法的名称和描述符(比如#1)

因为字节码文件是静态的,还没有被加载进内存,所以无法使用准确的地址来表示要引用那个类/接口、字段、方法,于是就使用了一段(能够准确地表达想要引用什么的)符号来表示要引用那个类、接口、字段、方法。

在经过类加载器加载进入 JVM 时,类加载器就会把这个符号引用转换为一个可以被引用的地址,这个过程就是把符号引用转换成直接引用

当然,直接引用也有三种分类:

  • 目标的地址
  • 相对的偏移量
  • 一个可以间接定位的句柄

按我的理解:

  • 类的静态成员属性或者静态方法的引用转换成目标的地址的符号引用
  • 对象的成员属性的引用转换成相对的偏移量的符号引用
  • 对象或者接口(不能明确调用那个类的对象)的方法的引用转换成句柄的符号引用

总之,直接引用就是内存中实打实的可以被调用的地址。

运行时常量池

当静态常量池被加载进 JVM 后,就变成了运行时常量池,而运行时常量池存放着类信息,属性信息,方法信息以及字符串常量池,当然还有根据不同的对象类型被分为 Integer 常量池、Long 常量池等基础数据类型的包装类常量池。

这一小节,统统给大家讲明白。

字符串常量池

字符串常量池是一个比较复杂的常量池。

为什么会有字符串常量池?

字符串不是基础数据类型,它是一个对象,但它的使用频率却和基础数据类型差不多,高频率地创建对象会极大地影响程序的性能。

JVM 为了提高程序的性能和减少内存的开销,于是给字符串的创建增加了一层缓存,以此来减少相同字符串的创建。

字符串常量池的位置

首先,我们要了解一下字符串常量池的存放位置。

先放一张 JVM 内存模型的图片,看不懂没关系,可以看到后面的JVM内存模型再回来看。

在这里插入图片描述

字符串主要涉及的内存区域是堆和方法区。

在 JDK1.6 时,字符串常量池是在运行时常量池里的,而运行时常量池是放在方法区里的永久代中,除非手动把字符串加入到方法区(这个操作后面会说),否则只有在静态常量池中的字符串才会被加载到字符串常量池。

所以 JDK1.6 的字符串分布大概是这样的。

在这里插入图片描述

从 JDK1.7 开始,JVM 就把字符串常量池从永久代中转移到了堆,运行时常量池还在永久代中,永久代还在方法区中。

所以 JDK1.7 之后的字符串分布大概是这样的。

在这里插入图片描述

到了 JDK1.8,就没有永久代了,字符串常量池在堆中,运行时常量池就是方法区内了。

所以我们要探讨字符串的设计原理需要分为 JDK1.7 之前和之后两种情况来讨论。

三种字符串操作

然后,讲一下字符串的一些操作,这样我们才能知道什么时候字符串会在堆里创建,什么时候在字符串常量池中创建。

  1. 直接赋值一个字符串
String s="abc";

通过这种方式创建的字符串都是在字符串常量池里面的,因为 JVM 在加载这个字面量“abc”的时候,会先去字符串常量池中通过 equals(key) 的方式找一下有没有相同的字符串,如果有就返回,没有就在字符串常量池中创建一个。

  1. new String()
String s=new String("aaa");

通过这种方式创建的字符串都是在堆里的,这也很好记,通过 new 创建的对象都是在堆里的,不会再去字符串常量池中判断。

但是,这个操作会同时创建两个字符串,一个在字符串常量池中,是在加载字节码时加载字面量"aaa"创建的,另一个是直接 new String() 操作时创建在堆里的。

  1. String.intern()方法
String s1 = new String("bbb");
String s2 = s1.intern();
System.out.println(s1==s2); //false

String.intern()方法是一个 native 方法,想看它的代码只能去下载 JVM 的源码了。

(这个方法逻辑不同版本不一样,详情往下看)

看起来这个逻辑和直接赋值一个字符串差不多,但这个方法的调用对象在或者不在字符串常量池里,就会有不同的情况。

回到上面的代码String s2 = s1.intern();,这里的 s1 是创建在堆上的,不在字符串常量池里的,在字符串常量池里的字符串是在字节码加载到 JVM 时,JVM 根据字面量"bbb"创建的。

这里以 JDK1.8 为标准画了一个图, s2 指向的是字符串常量池内的字符串"bbb"。

在这里插入图片描述

所以 s1==s2 是 false。

那不同版本的 intern 方法差别有多大?

JDK1.7 之前,在调用 intern 方法时,JVM 会通过 equals(key) 的方式在字符串常量池中去找一下这个字符串,如果有相同的就返回,没有就新建一个

JDK1.7 之后,在调用 intern 方法时,JVM 会通过 equals(key) 的方式在字符串常量池中去找一下这个字符串,如果有相同的就返回,没有就把调用这个方法的对象加入字符串常量池

字符串常量池从方法区转移到了堆和 intern 方法的调整会带来多大的影响呢?让我们看一下下面这段代码。

String s1=new String("aa")+new String("bb");
String s2=s1.intern();
System.out.println(s1==s2);

/*
在 JDK1.7 之前,输出的结果是 false,总共创建了 6 个字符串。
在 JDK1.7 之后,输出的结果是 true,总共创建了 5 个字符串。
*/

你知道为什么会这样吗?

说不出来也没关系,我来给你详细地讲述一下(说出来这一段就不用看了)。

字符串常量池实际上是一个类似 Map 的数据结构,想要从字符串常量池中查找字符串是需要通过 equals(key) 的方式去查找的,所以实际上的字符串常量池不是包含着字符串,而是把字符串的引用存储在一个 map 里,如下图。

在这里插入图片描述

接下来,我就按照 JDK1.7 之前和之后两种情况分别画图演示一下上面三行代码的运行过程。

JDK1.7 之前
在这里插入图片描述

  1. 第一行代码会在加载字节码的时候根据字面量在字符串常量池(在方法区里)创建两个字符串"aa","bb",紧接着有因为 new String() 在堆里创建了两个字符串"aa","bb",后来因为两个字符串相加又在堆里创建了一个字符串"aabb"
  2. "aabb"调用 intern 方法,因为这个字符串不在堆里,字符串常量池里是找不到这个字符串的,所以又会在字符串常量池中再创建一个字符串"aabb"

所以,s1 指向的是堆里的"aabb",而 s2 指向的是方法区里被字符串常量池引用的"aabb",这不是同一个字符串,s1==s2 是 false。

JDK1.7 之后

在这里插入图片描述

  1. 和 JDK1.7 之前一样,只不过字符串都是在堆里的。
  2. "aabb"调用 intern 方法,但在字符串常量池中找不到,于是就把这个字符串加入到字符串常量池中了,再把这个字符串赋值给 s2。

所以,s1 和 s2 指向的都是同一个字符串,s1==s2 是 true。

关于字符串的练习题

最后用几个笔试可能会经常遇到的练习题来帮助大家更了解字符串的三种操作。

习题1:

String s3 = "xiaobai";
String s4 = "xiaobai";
String s5 = "xiao"+"bai";
System.out.println(s3 == s4); // true
System.out.println(s4 == s5); // true

这道题很简单,s3 = s4 是因为他们的字面量是一样的,所以必然是同一个字符串,而 s5 则是在编译成字节码的时候会做出优化,s5 的字面量也是"xiaobai"

习题2:

String s6 = "xiaobai";
String s7 = new String("xiaobai");
String s8 = "xiao"+new String("bai");
System.out.println(s6 == s7); // false
System.out.println(s6 == s8); // false
System.out.println(s7 == s8); // false

String s12 = "xiaobai";
String s13 = "bai";
String s14 = "xiao"+s13;
System.out.println(s12 == s14); //false

s6 是通过字面量生成的,所以位于字符串常量池的;s7 是通过 new String 生成的,所以是一个新的字符串;而 s8 实际上也是 new String 生成的,因为 Java 的编译器是没办法优化"xiao"+new String("bai")(字面量+对象/对象引用)的情况,只能优化"xiao"+"bai"(字面量+字面量)这种情况。

在这里插入图片描述

可以看到字面量+对象/对象引用的情况,是会调用 StringBuilder 来拼接字符串的,最后调用 StringBuilder.toString() 返回一个新的字符串。

字面量+字面量的情况是包含字符串字面量+基础类型字面的情况的,什么意思呢?看一下下面的代码就懂了。

String s9 = "xiaobai1";
String s10 = "xiaobai"+1; 
String s11 = "xiaobai"+'1'; 
System.out.println(s9 == s10); // true
System.out.println(s9 == s11); // true

在编译之后,s9、s10、s11 的字面量都会变成"xiaobai1"

总结

最后放一个字符串常量池在 JDK1.7前后 的区别的表格。

JDK1.7之前 JDK1.7之后
字符串常量池的位置 方法区
直接赋值 在字符串常量池中创建一个字符串 在字符串常量池中创建一个字符串
new String() 在堆上创建一个字符串 在堆上创建一个字符串
intern() 先在字符串常量池中找有没有相同的字符串,有就返回,没有就重新创建一个 先在字符串常量池找有没有相同的字符串,找到就返回,找不到就把调用该方法的字符串加入字符串常量池并返回
基础数据类型常量池

基础数据类型常量池主要是针对八种基础数据类型的包装类做的缓存,而且这些缓存都是直接使用 Java 代码实现的,和字符串常量池的实现是不一样的。

基础数据类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Char
boolean Boolean

(我除了 int 的包装类,其他就把第一个字母大写了,如果有写错了,请以官方的为标准。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【JVM】JVM基础知识:常量池、类加载、JVM内存模型、对象的创建过程 的相关文章

随机推荐

  • vue中实现el-table点选和鼠标框选功能

    实现思路 项目有两个需求 既能在el table实现点选又能实现鼠标框选 一 点选实现思路 使用el table的cellClick方法 1 直接给点击的cell添加类名 cell classList add blue cell 然后把获取
  • golang 读取文件最后一行_测试用例是开发人员最后一块遮羞布

    最近一周写一个比较复杂的业务模块 越写到后面真心越心虚 操作越来越复杂了 代码也逐渐凌乱了起来 比如一个接口 传入的是一个比较复杂的大json 我需要解析这个大json 然后根据json中字段进行增删改查 调用第三方服务等操作 告诉前端接口
  • 微信支付配置流程

    微信支付配置流程 1 微信支付配置 微信公众号平台 gt 如果没有正式域名就要在安全中心配置白名单IP 2 微信公众号 微信公众号设置 gt 在功能设置里面的网页授权域名 3 商户号 产品中心 gt 开发配置 gt 配置jsApi支付页面的
  • vue3中a-table表格默认选中禁止选择

    效果 代码 使用row selection中的getCheckboxProps属性 其中preserveSelectedRowKeys属性设置为true 是为了表格切换页的时候 保留其他页面选中的数据 点击按钮 出现弹窗 const add
  • 深度学习:图像增强

    https blog csdn net zhangjunhit article details 79554140
  • Unity 使Text文本内容配合音频逐个显示并动态设置富文本

    在做一个项目时 需要Text文本内容逐个显示 并且配合音频的播放速度 当音频结束时 文本也显示完毕 而且给每一段文本设置不同的颜色和字号 代码如下 using System Collections using System Collecti
  • jenkins exec command 命令不执行

    本文记述的exec command 命令不执行情况是因为用户权限导致的 根据结果反馈就好像该设置不存在一样但是也没有报错 情况1 前端项目linux截图如下 图中1标志为jenkins所打的包 但是2标志是root账号的dist文件 因权限
  • plsql 登录后,提示数据库字符集(AL32UTF8)和客户端字符集(ZHS16GBK)不一致

    plsql 登录后提示 Database character set AL32UTF8 and Client character set ZHS16GBK are different Character set conversion may
  • Qt信号槽连接方式源码解读

    前言 Qt的五 四 种连接方式 在上一篇已经讲明 本篇主要分析在源码上是如何实现这几种连接方式的 本次源码为Qt 5 15 2 搞懂务必认真阅读最后添加注释后的代码 connect时会做什么 已知connect是可以实现一个信号连接多个槽的
  • ORT执行推理如何指定device编号

    法1 在执行推理前运行命令指定卡号 如 export CUDA VISIBLE DEVICES 1 python test py ox resnet50 16 fp32 说明 编号从0开始 通过上面的方式指定后 推理任务会在第2张卡中运行
  • window 服务器不稳定,服务器Windows系统突发情况的解决办法

    服务器Windows系统现在还是大部分的站长的选择 有很多用户是使用Windows作为网站服务器的系统 今天小编对于服务器Windows系统在维护过程中出现的几种突发情况 来讲讲解决办法 1 终端协议错误 如果用记事本或其他编辑器在远程终端
  • lambda 函数完美使用指南

    来源 萝卜大杂烩 今天我们来学习 Python 中的 lambda 函数 并探讨使用它的优点和局限性 什么是 Python 中的 Lambda 函数 lambda 函数是一个匿名函数 即 没有名称定义 它可以接受任意数量的参数 但与普通函数
  • 浅拷贝和深拷贝的区别

    浅拷贝 Shallow Copy 和深拷贝 Deep Copy 是两种复制对象的方式 它们之间的主要区别在于复制过程中是否会复制对象内部的引用类型数据 浅拷贝 Shallow Copy 浅拷贝仅复制了对象本身以及对象内部的基本数据类型 如i
  • 医学图像2D/3D可视化 ITK-SNAP软件使用

    软件下载链接 ITK SNAP Home 1 导入医学图像 nii gz文件 File gt Open Main Image 点击 Browse 切记不能有中文路径 gt Next 2 载入相应的分割图数据 nii gz文件 Segment
  • Android学习笔记——归纳整理

    目录 一 Android系统架构 二 Actvity相关 2 1基础相关 2 2 Intent相关 2 2 1 Intent的组成 2 2 2 显式Intent 2 2 3 隐式Intent 2 2 4 Intent属性 2 2 5 Int
  • Spring 单元测试中如何进行 mock

    我们在使用 Spring 开发项目时 都会用到依赖注入 如果程序依赖了外部系统或者不可控组件 比如依赖数据库 网络通信 文件系统等 我们在编写单元测试时 并不需要实际对外部系统进行操作 这时就要将被测试代码与外部系统进行解耦 而这种解耦方法
  • objects365数据集下载

    链接 https pan baidu com s 1QiWm8hCJus3LstZkz6Mzdw 提取码 wmrx
  • MySQL的索引和事务

    一 索引 一 索引概念 索引是一种特殊的文件 包含数据库中所有记录的引用 可以对表中的一列或多列创建索引 并指定索引的类型 存储引擎 每种索引在不同的存储引擎中的实现都有可能不同 索引类似数组的下标 通过下标拿到数组中的数据是很快的 同样通
  • 字符串右移n位

    题目 实现字符串右移几位 即 abcd 移两位变 cdab 思路 申请一个与待移位同样大小的数组 用来保存移位后的字符串 通过公式计算出简化的移位数 得到某字符移位后的新位置后 就将其字符值存放到新数组的对应位置 循环第3步 直至检测到字符
  • 【JVM】JVM基础知识:常量池、类加载、JVM内存模型、对象的创建过程

    前言 讲一下大概的内容 类加载 JVM内存模型 对象的创建 垃圾回收 JVM调优 入门 最近抽了时间学了一直都很想学的 JVM 之前也学过一点 也发布过一些零散的文章 但这篇文章会更加全面 学完这篇文章就足以应对有关 JVM 的面试 如果遇