《深入理解java虚拟机》笔记

2023-11-15

深入理解java虚拟机


走进java

java不仅仅是一门编程语言,还是一个由一系列计算机软件和规范形成的技术体系。她有以下优点:

  • 结构严谨,面向对象;
  • 摆脱硬件平台的限制,实现了一次编写,到处运行;
  • 提供了一个相对安全的内存管理和访问机制;
  • 有一套完善的应用程序接口,以及先进的开源生态;

java内存区域与内存溢出异常

运行时数据区域
  1. 程序计数器

线程私有,可以看做当前线程所执行的字节码的行号指示器。

  1. java虚拟机栈

线程私有,对应java方法执行的内存模型,即方法栈帧,存储局部变量表、操作数栈、动态链接、方法出口等信息。

  1. 本地方法栈

线程私有,为虚拟机用到的native方法的内存模型。

  1. java堆

线程共享,唯一的目的就是存放对象实例。虽然java虚拟机规范要求所有的对象都在java堆上分配,但是随着栈上分配、标量替换等优化技术的出现,这一要求也显得不那么严格了。堆内部还可以按照不同的原则细分为更细的区域,但都是为了垃圾回收等目的,并没有改变堆内存储的内容。堆内存的分配要求逻辑上连续,但不要求物理上连续。

  1. 方法区

线程共享,存储已被虚拟机加载的类信息、常量、静态变量、及时编译后的代码等数据。区别于堆中的永久代,将方法区直接以永久代的方式实现并不是一个好主意。

  1. 运行时常量池

方法区的一部分,字节码文件在编译期生成的各种字面量和符号引用,在类加载之后进入方法区的运行时常量池中存放。

  1. 直接内存

在java1.4中引入了NIO类,可以使用native函数直接分配对外内存,然后通过java堆中的DirectByteBuffer对象来对这块内存的引用进行操作,用来提高性能。很显然,直接内存不受java堆的内存限制,但受OS物理内存的限制,也会跑出OutOfMemeoryError异常。

HotSpot虚拟机对象揭秘
对象的创建

当遇到一条new指令时:

  1. 检查在常量池中是否存在类的符号应用,并检查对应的类是否已加载,没有则去加载;
  2. 类加载后,将为对象在堆中分配内存,通常通过指针碰撞法 (带Compact过程)or 空闲列表法(CMS);
  3. 为了防止对象分配存在并发冲突问题,可以采用CAS配上失败重试保证其原子性,也可以使用TLAB(本地线程分配缓冲);
  4. 将出了对象头的内存空间都初始化为0,是为了让java对象的属性不初始化,可以访问各类型的零值;
  5. 设置对象元信息;
  6. 运行init方法,完成对象的创建工作;
对象的内存布局

对象的内存布局可以分为3块区域:对象头、实例数据和对齐填充。

对象头包括对象元数据,这部分根据位数决定元数据所占内存大小,但会复用压缩;还包括类型指针,指向这个对象属于哪个类型。

实例数据存储的是真正有效信息,包括程序代码中定义的各种类型的字段内容,也包括 集成的字段。

对齐填充仅仅起着占位符的作用,是因为HotSpotVM的自动内存管理系统要求对象的起始地址必须是8的整数倍。

对象的访问定位
  1. 使用句柄:在堆中划分一块内存区域作为句柄池,reference中存储的是对象的句柄地址,句柄中包含了对象的实例数据和类型数据;
  2. 直接指针:直接存储的对象地址;

二者各有优劣,使用句柄借助了句柄的缓冲,在对象移动时不必修改reference的内容,而直接内存则少了一次指针寻址,提升了效率。


垃圾收集器与内存分配策略

对象存活判断
引用计数法

给对象添加引用计数器,每增加一个引用,计数器加1,引用失效,计数器减1。实现简单,但存在循环引用的问题。

可达性分析

通过GC Roots对象作为起始点向下搜索,当一个对象到GC Roots不存在引用链时,即可认为该对象不可达,可以回收了。GC Roots对象主要包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中引用的常量。

再谈引用
  • 强引用

代码直接指明的引用,只要引用还在,永远不GC。

  • 软引用

有用但非必须的对象,在OOM之前,会尝试回收这些对象,如果内存还是不够,才会抛出OOM。

  • 弱引用

非必须对象,在下次GC时会直接回收。

  • 虚引用

最弱的引用,不会为对象的生命周期造成影响,也无法取得对象实例,唯一的目的是在GC时获取通知。

回收方法区

方法区(HotSpot中的永久代)的回收主要是回收常量和无用的类。当常量不存在引用时,就可能会被回收。而类是否无用的判断就比较苛刻:不存在该类的对象、加载该类的ClassLoader已回收、对应的Class对象没有在任何地方被引用(无法通过反射方法访问该类的对象)。而且还可以通过虚拟机参数控制是否进行回收。

垃圾收集算法
标记-清除算法

最基础的收集方法,先标记出需要回收的对象,在标记完成后统一回收。主要存在效率问题和空间碎片。

复制算法

将内存分为两块,标记结束后,将存活的对象统一拷贝到另一块内存,然后对原区域整块进行回收,解决了空间碎片问题,但将可用内存减小了一半。

现实中则会将比例进行调整,例如根据8:1:1的比例将内存分配为Eden、SurviorA、SurvivorB三块,每次使用Eden + 1块Survior,回收时将存货对象全部放入另一块Survivor中。但存在分配担保问题。

标记-整理方法

主要是老年代使用,不会有很多对象的复制操作。在标记之后不进行清除,而是向一端进行移动。

HotSpot的算法实现
枚举根节点

准确式GC要求进行时必须停顿所有java执行线程,以避免回收过程中发生引用变动。使用OopMap数据结构在类加载完成之后就预存储哪些位置存在全局性引用,方便在GC时直接定位到。

安全点

HotSpot没有为所有指令设置OopMap,只是在“特定位置”记录这些信息,这就是Safepoint。安全点的选定基本上是以程序“是否具有让程序上时间运行”为标准进行的,例如方法调用、循环跳转、异常跳转等。

安全区域

当程序处于sleep或者blocked的状态,就无法相应jvm的中断请求,自然无法stop the world,这时候就需要安全区域(safe region)。指的是在一段代码片段中,引用关系不会发生变化,可以安全GC。进入安全区域代码时,需要进行标识,离开安全区域时,也要检查GC是否完成。

垃圾收集器
Serial

最基本、最悠久的收集器。单线程收集器,简单而高效,仍是client模式下默认的新生代收集器。

ParNew

其实就是Serial收集器的多线程版本。只有它和Serial能个CMS收集器配合工作。

Parallen Scavenge

使用复制算法的新生代并行多线程收集器,关注点主要在于提升吞吐率,即CPU运行于用户代码的时间与CPU总运行时间的比值。适合在后台运算而不需要太多交互的任务。

Serial Old

Serial的老年代版本,使用“标记-整理”算法,主要是两个目的:一个是jdk1.5之前与Parallel Scavenge搭配使用;另一个是作为CMS失败的备用方案。

Parallel Old

Parallen Scavenge的老年代版本,主要是为了与Parallen Scavenge搭配使用。

CMS

一种以获取最短回收停顿时间为目标的垃圾收集器。整个过程分为四个步骤:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

三个明显的缺点:

  • 对CPU资源敏感
  • 无法处理浮动垃圾
  • 碎片化现象严重
G1

最新的收集器,同时处理新生代和老年代,具有以下特点:

  • 并行与并发
  • 分代收集
  • 空间整合
  • 可预测的停顿
内存分配与回收策略

对象的内存分配,主要是分配在新生代Eden区上,当然如果启用了TLAB,则会优先在TLAB上分配。少数情况下也会直接在老年代中。分配的规则并不是百分之百的,而是根据垃圾收集器组合还有虚拟机中相关参数的设置来决定的。

对象优先在Eden分配

对象分配优先在Eden区中,如果发现空间不足,则会触发一次Minor GC。这里介绍一下Minor GC和Major GC。

  • Minor GC:新生代GC,因为java对象朝生夕灭的特性,所以新生代GC非常频繁,一般回收速度也较快;
  • Major GC:老年代GC,也称Full GC。一次Full GC通常伴随着一次Minor GC。通常耗时也较长。
大对象直接在老年代分配

例如byte[]数组就是典型的大对象。虚拟机设置了参数可以控制超过多大的对象直接进入老年代。

长期存活的对象将进入老年代

对象在Survivor中每熬过一次Minor GC,年龄就增加一岁,当年龄增长到一定程度(默认是15岁)就会进入老年代。这个值也可以通过参数设置。

动态对象年龄判定

虚拟机并不是永远根据设定的参数来控制是否进入老年代。如果Survivor空间中相同年龄的对象大于总对象大小的一般,那么大于改对象年龄的对象都将进入老年代。

空间分配担保

在Minor GC之前,会检查老年代的剩余连续空间是否大于新生代所有对象空间之和,如果成立,则Minor GC是安全的,反之则不安全。如果不安全,虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,如果允许,则会检查老年代最大连续空间是否大于历次Minor GC晋升至老年代的对象大小的平均值,如果大于,则会尝试GC,如果小于或者不允许担保失败,则会直接出发一次Full GC。虽然担保失败会耗费较多时间,但是大部分情况下担保还是成功的,为了避免频繁的Full GC,还是应该打开担保失败的开关。


虚拟机性能监控和故障处理工具

JDK的命令行工具
  • jps:查看进程状况
  • jstat:虚拟机统计信息
  • jinfo:查看java进程的配置参数
  • jmap:生成堆转储快照
  • jhat:分析堆转储快照
  • jstack:生成虚拟机当前时刻的线程快照
JDK可视化工具
  • jconsole
  • virtualVM

类文件结构

无关性的基石

java规范规定的字节码存储格式,是构成平台无关性的基石。java虚拟机不和java在内的任何变成语言绑定,只与“Class文件”这种特定的二进制文件格式所关联。

Class类文件的结构

字节码文件使用一种类似于C语言结构体的伪结构来存储数据。结构中只有两种数据:无符号数和表。

  • 无符号数是基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值;
  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的以“_info”结尾。字节码文件本质上就是一张表。

从前往后,字节码中的数据依次为:

  • 魔数(CAFEBABE)
  • 版本号:Minor Version(2个字节)+ Major Version(2个字节)
  • 常量池
  • 访问标志
  • 类索引、父类索引与接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合
字节码指令简介

java虚拟机的指令由一个字节长度的、代表某种特定操作含义的数字以及跟随其后的零至多个操作所需参数构成。操作码长度为一个字节,所以总数不能超过256个。

字节码与数据类型

java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。但由于指令总数限制,大部分指令都没有支持整数类型byte、char和short。编译器会在编译期或者运行期将byte和short类型的数据带符号扩展成相应的int类型数据,将boolean和char类型数据零位扩展成相应的int类型数据。大多数对于boolean、short、byte都会转为对应的int类型数据。

加载和存储指令

用于将数据在栈帧中的 局部变量表和操作数栈中来回传输。

运算指令

用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作数栈顶。

类型转换指令

用于两种不同的数值类型进行相互转换,一般用于实现代码中显式类型转换操作。

对象创建与访问指令

创建和访问对象的指令。

操作数栈管理指令

直接操作操作数栈的指令。

控制转移指令

可以让java虚拟机有条件或无条件的从指定位置继续执行。

方法调用与返回指令
异常处理指令
同步指令

虚拟机类加载机制

类加载的时机

类从被加载,到卸载出内存为止,整个生命周期包括:

  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化
  • 使用
  • 卸载

java虚拟机规范规定了有且只有5中情况必须立即对类进行初始化(而加载、验证、准备自然要在此之前):

  • 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时;
  • 使用java.lang.reflect包的方法对类进行反射调用的时候;
  • 初始化一个类,发现其父类还没有初始化,必须先进行父类的初始化;
  • 当虚拟机启动时,main方法所在的主类必须先初始化;
  • 使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析的结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个句柄所对应的类没有初始化,则需要先触发其初始化。
加载

属于类加载的一个阶段,主要分为三步:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流;
  2. 将这个字节流中定义的静态存储结构转化为方法区的运行时结构;
  3. 在内存中生成一个此类的对象,作为访问此类在方法区的访问入口;
验证

验证和加载是交叉进行的,但是有着严格的前后顺序。验证阶段主要是验证class文件内二进制数据的合法性,主要包括:

  1. 文件格式验证:是否是CAFEBABE魔术头,java版本是否在有效范围内等,是否存在无效的UTF-8字符;
  2. 元数据验证:验证类是否有父类,子类是否按要求实现父类的方法;
  3. 字节码验证:通过分析数据流和控制流,来确定程序语义的合法性;
  4. 符号引用验证:通过类的全限定名是否能找到对应的类;
准备

为类变量分配内存(方法区)并赋初始值。这里仅包括类变量(static)。这时候没有执行任何类的代码,包括变量声明和赋值,所以变量的值都是初始的默认值,但有一种情况例外,那就是constant变量,这个变量会在准备阶段就初始化为指定的值,而不是初始值。

解析

将常量池中的符号引用替换为直接引用的过程。其中符号引用包括:

  1. 类或接口
  2. 字段
  3. 类方法
  4. 接口方法
  5. 方法类型
  6. 方法句柄
  7. 调用点限定符
初始化

执行类的初始化代码的过程。

类加载器
类与类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间。

双亲委派模型

加载器类型主要分为:

  • 启动类加载器
  • 扩展类加载器
  • 应用程序加载器

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,首先不会自己尝试加载这个类,而是把这个请求委托给父类加载器去完成。

破坏双亲委派模型

三种双亲委派模型被破坏的情况:

  • 发生在双亲委派模型出现之前
  • java.lang.Thread的setContextClassLoader()方法设置加载器;
  • 代码热替换,通过将模块的加载器一起换掉的方式实现代码的热替代;

虚拟机字节码执行引擎

运行时栈帧结构

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机栈的栈元素。

栈帧里面主要有局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。

局部变量表

存放方法参数和内部定义的局部变量。最小单位为slot,通过索引定位的方式来使用。不同于类变量,局部变量表中的变量如果没有初始化,是不能使用的。

操作数栈

存储指令待操作数的栈。两个栈帧的操作数栈可以通过部分区域重叠实现栈帧之间的数据共享。

动态链接

指向运行时常量池中该栈帧所属方法的引用。

方法返回地址

方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可 能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

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

《深入理解java虚拟机》笔记 的相关文章

随机推荐

  • Linux中用root用户打开vscode

    先打开终端切换到root用户 su root 接着输入 sudo code user data dir vscode root
  • 对数器

    对数器的作用 对数器可以说是验证算法是否正确的一种方式 尤其是在笔试的时候 用贪心算法写出的程序 暂时无法用数学公式严格推导证明 只能通过大量的数据集验证算法的正确性 而大量的数据集当中要包括各种情况 各个方面都要考虑到 对我们自己来说 有
  • WDK_学习笔记_区块链+ViT和Swin transformer

    文章目录 摘要 一 項目 Hyperledger fabric技术的深入学习 1 1 安装 2 2 0 只记录问题 其余按文档操作即可 二 深度学习 Version Transformer ViT 和Swin Transformer 2 1
  • 【Unity从零开始制作空洞骑士】①制作人物的移动跳跃转向以及初始的动画制作

    事情的起因 首先我之前在b站的时候突然发现有个大佬说复刻了空洞骑士 点进去一看发现很多场景都福源道非常详细 当时我除了觉得大佬很强的同时也想自己试一下 而且当时对玩家血条设计等都很模糊 就想着问up主 结果因为制作的时间过了很久了 大佬也有
  • Mock入门

    关键参数 name 唯一标识 return value 当被调用时 返回的值 可为函数 side effct 当存在时 return value不生效 返回side effect 导入库 from unittest import mock
  • 用户画像-标签体系

    1 前言 最近在学习用户画像 翻看了 彭友会 的七十多份资料 简单过了一遍赵宏田老师的书 最近又看了许多微信公众号里的文章 整体感受就是 资料太杂 内容太乱 重复的太多 相互间也会有些冲突 但大致可以归为两类 赵宏田老师的一套 另外其它的一
  • PDF文件复制文字

    最近在看电子书时 发现有的一些 PDF 文件看起来像是扫描的 但能直接复制文字 有的则不能 查找相关资料后明白了 不能复制的pdf文件 01 pdf文件加密了 02 扫描和图形格式做的PDF文件 PDF文件如果加密了 对于一些不允许做修改
  • Android关于AutoService、Javapoet讲解

    AutoService会自动在META INF文件夹下生成Processor配置信息文件 该文件里就是实现该服务接口的具体实现类 而当外部程序装配这个模块的时候 就能通过该jar包META INF services 里的配置文件找到具体的实
  • ChatGPT不能代替人类写作的四个原因

    近期留学圈最火的C位当属ChatGPT 作为一款OpenAI开发的语言模型 ChatGPT在文本生成上的优秀表现大大助力了母语非当地语言的留学生们 写邮件 翻译并理解文本乃至写代码 ChatGPT似乎所向披靡 不少同学也产生了这个想法 用它
  • pageHelper分页失效解决方案

    前言 pageHelper是一款优秀的Mybatis分页插件 在项目中可以非常便利的使用 使开发效率得到很大的提升 但不支持一对多结果映射的分页查询 所以在平时的使用时 对于一对多分页会出现分页错误 这篇文章主要对pageHelper分页错
  • activiti学习之服务任务

    写在前面 对于工作流 我们使用最多的是用户任务节点 用户任务节点就是给用户来生成任务的 需要人来手动的处理 而与之对应的还有服务任务节点 这种类型的节点需要人手动的参与而是程序来执行 即执行某个类的某个方法 这个类一般是org activi
  • Java 实现 MD5 加密算法

    1 MD5 加密算法 1 1 MD5 算法介绍 MD5 消息摘要算法 英文 MD5 Message Digest Algorithm 一种被广泛使用的密码散列函数 可以产生出一个128位 16字节 的散列值 hash value 用于确保信
  • 子图匹配算法——VF2算法讲解

    讲的很通透了 https zhuanlan zhihu com p 259393192
  • CSS五款超好用的布局网站

    CSS Grid Generator https cssgrid generator netlify app CSS Layout https csslayout io Flexbox Generator https loading io
  • vtkdicom0.8_vtk9.2_dcmtk3.6.7_qt6.2编译OK

    目录 0 结果展示 1 cmake要点 2 编译报错解决 3 参考链接 0 结果展示
  • 吃透Chisel语言.23.Chisel时序电路(三)——Chisel移位寄存器(Shift Register)详解

    Chisel时序电路 三 Chisel移位寄存器 Shift Register 详解 上一篇文章介绍了Chisel计数器以及一些高级用法 内容很多 学下来肯定收获也会很多 除了计数器以外 还有一种寄存器的应用十分广泛 那就是移位寄存器 这一
  • Linux Test Project(一)

    http www vimlinux com lipeng 2014 09 12 ltp Testing Linux one syscall at a time LTP是从SGI开始的 后由IBM 思科 富士通 SUSE Redhat等组织开
  • Java多线程下 ThreadLocal 的应用实例

    ThreadLocal很容易让人望文生义 想当然地认为是一个 本地线程 其实 ThreadLocal并不是一个 Thread 而是 Thread 的局部变量 也许把它命名为 ThreadLocalVariable更容易让人理解一些 当使用
  • jQuery Ajax 初始化方法

    ajaxSetup headers Authorization auth token cache false 禁用缓存 dataType json contentType application json contentType appli
  • 《深入理解java虚拟机》笔记

    深入理解java虚拟机 走进java java不仅仅是一门编程语言 还是一个由一系列计算机软件和规范形成的技术体系 她有以下优点 结构严谨 面向对象 摆脱硬件平台的限制 实现了一次编写 到处运行 提供了一个相对安全的内存管理和访问机制 有一