[JVM系列]四、深入理解Java虚拟机阅读笔记汇总-含超细思维导图

693 阅读30分钟

JVM系列文章:

[JVM系列]三、一文搞懂JVM垃圾回收

[JVM系列]二、一文彻底搞懂 JVM运行时数据区 和 JVM内存结构

[JVM系列]一、源码->类文件->JVM过程详解(类文件解读/类加载机制/类加载器)

思维导图

先上看书时总结的思维导图

根据深入理解JVM书籍,总结出来的三个部分(内存管理、执行子系统、编译优化)的思维导图(内容太多看不清可以单独打开一个页面来看)

内存管理:

执行子系统:

编译优化:

内存管理

  • JVM内存相关
    • JVM运行时数据区
    • 对象探秘:对象的创建,布局,访问
    • JVM内存异常情况
  • JVM垃圾回收相关
    • 判断对象已经死了?
    • 垃圾回收算法
    • HotSpot算法实现细节
    • 各种垃圾收集器
    • 具体的内存的分配和回收策略
  • JVM内存调优
    • 性能监控,故障分析
    • 调优案例分析

一、JVM内存相关

1.运行时数据区

1.1 程序计数器

  • 它可以看作是当前线程所执行的字节码的行号指示器,它是程序控制流的指示器,分支、循环、跳转、异常处 理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 程序计数器的内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域

1.2 Java虚拟机栈

  • 虚拟机栈描述的是Java方法执行的线程内存模型
  • 每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧用于存储局部变量表操作数栈动态连接方法出口等信
  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
  • 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常

1.3 本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务

1.4 Java堆

  • 此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存(可能存在栈上分配,逃逸分析,编译优化章节里面有说)
  • Java堆是垃圾收集器管理的内存区域
  • 如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。
  • 《Java虚拟机规范》的规定,Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
  • 如果在Java堆中没有内存完成实例分配,并且堆也无法再 扩展时,Java虚拟机将会抛出OutOfMemoryError异常

1.5 方法区

  • 方法区是Java虚拟机规范里面的抽象定义,具体虚拟机实现方法可以不同,如具体实现有永久代和元空间。
  • 它用于存储已被虚拟机加载的类型信息常量静态变量即时编译器编译后的代码缓存等数据
  • 这区域的内存回收目标主要是针对常量池的回收对类型的卸载,但是对类型的卸载条件十分苛刻
  • 如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常

方法区、永久代、元空间、class文件常量池、运行时常量池、字符串常量池的相关概念;👉连接

1.6 运行时常量池

JDK1.8中,字符串常量池被放入到堆空间中,但是,逻辑上还是属于方法区的!运行时常量池还是在元空间里面的,也就是在直接内存里面

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生 成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是**==String类的 intern()方法==**

intern()方法设计的初衷,就是重用String对象,以节省内存消耗。

s1.intern()方法,相当于把当前的s1字符串的值注册到常量池中,但是原本的在堆里面的字符串对象任然存在,除非使用s1 = s1.intern()赋值方法,把s1引用指向常量池里面的字符串,否则s1还是指向的堆中的字符串。

2.对象探秘

2.1 对象的创建

**注意和类的加载进行区别!(加载连接[验证,准备,解析]初始化[clinit])**类加载是在方法区的,加载的是类变量,在对象的创建之前完成。

image-20210207102155782

2.2 对象的布局

image-20210207102543633

对齐填充的作用:HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍

  1. 假设对象没有8字节对齐,而是随机大小分布在内存中,由于这种不规律,会造成设计者的代码逻辑变得异常复杂,因为设计者根本不知道你这个对象到底有多大,从而没有办法完整地取出一整个对象,还有可能在这种不确定中,取到其它对象的数据,造成系统混乱。
  2. 提升性能,假设对象是不等长的,那么为了获取一个完整的对象,就必须一个字节一个字节地去读,直到读到结束符,但是如果8字节对齐后,获取对象就可以以8个字节为单位进行读取,快速获取到一个对象,也不失为一种以空间换时间的设计方案。

2.3 对象的访问

  • 使用句柄
  • 直接指针
image-20210207103202813 image-20210207103215053

3.OutOfMemoryError异常实战

在《Java虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(下文称OOM)异常的可能

3.1 堆OOM

  • 内存泄漏:对象是不需要的
  • 内存溢出:对象是需要的

如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息 以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内 存泄漏的代码的具体位置。

如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查 是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运 行期的内存消耗

3.2 虚拟机栈和本地方法栈

  • StackOverFlowError
  • OutOfMemoryError

3.3 方法区的异常

  • 永久代和元空间

自JDK 7起,原本存放在永久代的字符串常量池被移至Java堆之中

3.4 本机直接内存溢出

Unsafe类 / NIO

二、垃圾回收相关

1.判断对线是否死亡

1.1 引用计数

  • 简单,高效
  • 无法解决循环引用问题

1.2 可达性分析

这个算法的基本思路就是通过 一系列称为**“GC Roots”根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时**,则证明此对象是不可能再被使用的。

固定可作为GCRoots对象的包括一下几种:

  • 虚拟机栈栈帧中的局部变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
  • 方法区类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 方法区常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器
  • 所有被同步锁(synchronized关键字)持有的对象
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

1.3 强软弱虚

写了Demo

引用类型被垃圾回收时间用途生存时间
强引用从来不会对象的一般状态JVM停止运行时终止
软引用当内存不足时对象缓存内存不足时终止
弱引用正常垃圾回收时对象缓存垃圾回收后终止
虚引用正常垃圾回收时跟踪对象的垃圾回收垃圾回收后终止
  • 虚引用特点之一:无法通过虚引用来获取对一个对象的真实引用
  • 虚引用特点之二:虚引用必须与ReferenceQueue一起使用,当GC准备回收一个对象,如果发现它还有虚引用,就会在回收之前,把这个虚引用加入到与之关联的ReferenceQueue中

2.垃圾收集算法

2.1 背景

  • 对象大多朝生夕死

  • 越熬过GC的次数,越不容易给GC

  • 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。因为会一起变老。

    依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称 为**“记忆集”,Remembered Set),这个结构把老年代划分成若干小块**,标识出老年代的哪一块内存会 存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数 据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

2.2 标记-清除

  • 效率不稳定:如果Java堆中包含大量对 象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过 程的执行效率都随对象数量增长而降低。
  • 内存空间碎片化

2.3 标记-整理

  • 移动对象,老年代比较久

2.4 标记-复制

  • 适用于新生代
  • 简单高效
  • 浪费一半空间

HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1

3.HotSpot算法细节实现

3.1 根节点枚举

迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的

大致的意思是,其实不用一个一个的在方法区和局部变量表里面的找GCRoot,有一个OopMap,能方便快捷的找到GCRoot。在OopMap的协助下,HotSpot能快速准确的完成GC Root的枚举。

3.2 安全点

如果采用上面的OopMap,很多指令都需要维护这个oopMap,这样不够高效,所以引入安全点,只有在安全点的时候在同步oopMap,所以只有在安全点的时候才能发生GC

如何在垃圾收集发生时让所有线程(这里其实不包括执行JNI调用的线程)都跑到最近的安全点,然后停顿下来。

  • 抢先式中断: 系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚 拟机实现采用抢先式中断来暂停线程响应GC事件。
  • 主动式中断: 不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最 近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的

3.3 安全区域

使用安全点的设计似乎已经完美解决如何停顿用户线程,让虚拟机进入垃圾回收状态的问题了,但实际情况却并不一定。安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集 过程的安全点。但是,程序“不执行”的时候呢?所谓的程序不执行就是没有分配处理器时间,典型的场景便是用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。对于这种情况,就必须引入安全区域(Safe Region)来解决。

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。

当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全 区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的 阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以 离开安全区域的信号为止。

3.4 记忆集和卡表

记忆集和卡表,主要是解决垃圾回收中的跨代引用问题。

记忆集是一种记录从非收集区域指向收集区的指针集合的抽象数据结构。

卡表是记忆集的一种实现(类似于HashMap实现Map),每个记录精确到一块内存区域,

卡表分为:卡表和卡页,卡页就是固定大小的内存空间,每一个卡表指向一个一个卡页,如果卡也内有跨代引用对象,那么对于的卡表就是1,否则就是0.

在垃圾回收的时候,只要筛选出卡表为1的元素,就能轻易的得出哪写卡页内存块中存在跨代引用,并把他们加入GC Roots中一起扫描,避免的对整个区域进行扫描。

3.5 写屏障

我们已经解决了如何使用记忆集来缩减GC Roots扫描范围的问题,但还没有解决卡表元素如何维护的问题,例如它们何时变脏、谁来把它们变脏等。在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的.

变脏时间点原则上应该发生在引用类型字段赋值的那一刻。但问题是如何变 脏,即如何在对象赋值的那一刻去更新维护卡表呢?假如是解释执行的字节码,那相对好处理,虚拟 机负责每条字节码指令的执行,有充分的介入空间;但在编译执行的场景中呢?经过即时编译后的代 码已经是纯粹的机器指令流了,这就必须找到一个在机器码层面的手段,把维护卡表的动作放到每一 个赋值操作之中

写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值 后的则叫作写后屏障(Post-Write Barrier)。

3.6 并发的可达性分析

相关概念:

  • 三色分析

    • 黑色:GC扫描其所有的引用,发现此节点是可达的
    • 灰色:GC扫描了其部分引用,可达性暂定
    • 白色:GC还没扫描到的节点。
  • 并发时可达性分析可能出现的问题

    1. 把原本消亡的对象错误标记为存活,可以容忍
    2. 把原本存活的对象标记为消亡,后果严重。
  • 当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色:

    1. 赋值器插入了一条或多条从黑色对象到白色对象的新引用
    2. 赋值器删除了全部从灰色对象到白色对象的直接或间接引用
  • 因此,为了解决上面的问题,只需破坏上面两个条件中的一个就行,有两种解决方案

    1. 增量更新:增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新 插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫 描一次

    2. 原始快照:当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索

    CMS是基于增量更新来做并发标记的,G1、Shenandoah则是用原始快照来实现

4.垃圾收集器

image-20210209094933007

4.1 Serial

  • 新生代,串行
  • 单线程工作的收集器,在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束
  • 但是简单高效,没有线程交互开销。
  • image-20210209095321145

4.2 ParNew

  • 新生代,并行
  • ParNew收集器实质上是Serial收集器的多线程并行版本
  • 目前只有它能与CMS 收集器配合工作。也可以理解为从此以后,ParNew合并入CMS,成为它专门处理新生代的组成部 分。ParNew可以说是HotSpot虚拟机中第一款退出历史舞台的垃圾收集器
  • image-20210209100335306

4.3 Parallel Scavenge

  • 新生代,并行,吞吐量优先

  • Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,Scavenge收集器的目标则是达到一个可控制的吞吐量

    image-20210209100813796

4.4 Serial Old

  • 单线程,标记整理
  • CMS失败后的兜底方案
  • image-20210209101130373

4.5 Parallel Old

  • Parallel Old是Parallel Scavenge收集器的老年代版本
  • 并发,标记整理
  • Parallel Scavenge + Parallel Old = 吞吐量组合
  • image-20210209101401439

4.6 CMS

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。使用标记清除算法

  1. 初始标记-STW-(因为很快,所以用单线程)
  2. 并发标记
  3. 重新标记-STW-(并发可达性分析中的增量更新方案)
  4. 并发清除
image-20210209101712288

缺点!:

  1. CMS收集器对处理器资源非常敏感
  2. 由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“并发失败”进而导致另一次由Serial Old兜底的完全“Stop The World”的Full GC的产生
  3. CMS是一款基于**“标记-清除”**算法实现的收集器,大量空间碎片产生

4.7 G1

面向服务端应用的GC收集器,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式,它将Region作 为单次回收的最小单元,每个Region可以扮演不同的分区(新生代,老年代,Eden,Survivor),即每次收集到的内存空间都是Region大小的整数倍。每次根据用户设定的停顿时间,优先处理回收价值收益最大的那些Region。

Region中还有一类特殊的Humongous区域,专门用来存储大对象,而对于那些超过了整个Region容量的超级大对象, 将会被存放在N个连续的Humongous Regio n之中,G1的大多数行为都把Humongous Region作为老年代 的一部分来进行看待

  1. 初始标记
  2. 并发标记
  3. 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录(也就是并发可达性分析里面的*“原始快照”*解决方案,在写屏障里面的写前屏障实现)
  4. 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的。

和CMS的对比:

  1. G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region 之间)上看又是基于“标记-复制”算法实现。而CMS是标记清除,有点拉跨
  2. 无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载 (Overload)都要比CMS要高。 内存占用:G1的卡表维护更加麻烦,因为有很多个Region,要采用Map映射的方式实现卡表 运行时额外执行负载:譬如它们都使用到写屏障,CMS用写后屏障来更新维护卡表;而G1除了使用写后屏障来进行 同样的(由于G1的卡表结构复杂,其实是更烦琐的)卡表维护操作外,为了实现原始快照搜索 (SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况

执行子系统

一、类文件结构

1. 魔数与Class文件版本

2. 常量池

  • 字面量
  • 符号引用

3. 访问标识

这个Class是类还是接口;是否定义为public类型;是否定义为abstract 类型;如果是类的话,是否被声明为final

4. 类索引、父类索引与接口索引集合

类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过 CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的 全限定名字符串。图6-6演示了代码清单6-1中代码的类索引查找过程

image-20210209115058796

5. 字段表集合

6. 方法表集合

7. 属性表集合

二、类加载机制

1. 类初始化时机

image-20210209120429538

2. 类加载过程

image-20210209120608959

三、类加载器

双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请 求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系

  • 启动类加载器:<JAVA_HOME>\lib
  • 扩展类加载器:<JAVA_HOME>\lib\ext
  • 应用程序类加载器:加载用户类路径 (ClassPath)上所有的类库

四、字节码执行引擎

1. 运行时栈帧结构

在编译Java程序源码的时候,栈帧中需要多大的局部变量表,需要多深的操作数栈就已经被分析计算出来,并且写入到方法表的Code属性之中.

1.1 局部变量表

  • 用于存放方法参数和方法内部定义的局部变量
  • 当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递
  • 如果执行的是实例方法,局部变量表的0号索引放的是“this”隐藏参数
  • 如果一个局部变量定义了但没有赋初始值,那它是完全不能使用的

1.2 操作数栈

  • 在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作

1.3 动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用

1.4 方法返回地址

  • 正常调用完成:遇到return字节码指令,将返回值返回给上层的方法调用者
  • 异常调用完成:执行过程中遇到异常,且没有进行处理

方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:

  1. 恢复上层方法的局部变量表和操作数栈,
  2. 把返回值(如果有的话)压入调用者栈帧的操作数栈中,
  3. 调整PC计数器的值 以指向方法调用指令后面的一条指令等

1.5 附加信息

如锁记录

2. 方法调用

方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还未涉及方法内部的具体运行过程

某些调用需要在类加载期间,甚至到运行期间才能确定目标方法 的直接引用

2.1 解析

所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。

这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的(编译器可知,运行期不变)(invokestatic, invokespecial, invokevirtual中final修饰的方法)(静态方法、私有方法、实例构造器、父类方法)

JVM中方法调用的字节码指令:

  • invokestatic。用于调用静态方法。
  • invokespecial。用于调用实例构造器()方法、私有方法和父类中的方法。
  • invokevirtual。用于调用所有的虚方法。
  • invokeinterface。用于调用接口方法,会在运行时再确定一个实现该接口的对象。
  • invokedynamic。先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法

2.2 分派

2.2.1 静态分派

所有依赖静态类型(外观类型)来决定方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载

静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行 的,这点也是为何一些资料选择把它归入“解析”而不是“分派”的原因

2.2.2 动态分派

动态分派,它与Java语言多态性的另外一个重要体现——重写(Override)有着很密切的关联。

重写的本质,就是invokevirtual这条指令:invokevirtual指令运行时解析过程大致分为下面几步:

  1. 找到操作数栈顶第一个元素指向的对象的实际类型,记作C
  2. 如果在C类型中找到与常量中描述符和简单名称都相符的方法,则进行权限校验,如果通过,就查找结束正常执行,没通过返回java.lang.IllegalAccessError异常
  3. 如果在常量池中没找到符合的方法,则按照继承关系从下往上对C的各个父类进行第二步的搜索和验证
  4. 如果始终没找到合适的方法,报java.lang.AbstractMethodError异常。

为了提高效率,JVM里面采用的是虚拟方法表的方法:来存储当前类的方法实际上引用的是谁的方法。

image-20210212101945606

3. 动态类型语言支持

3.1 动态类型语言概念

动态类型语言的关键特征是:它的类型检查的主体过程是在运行期而不是编译器。

换句话说,Java在编译期的时候,就已经将某个对象在类型检查通过后,将方法的符号引用完完全全确定下来了,而对于动态类型语言来说,这些都发生在运行期。

除此之外,变量无类型,而变量值才有类型,这个特点也是动态语言的一个核心特征

比如:C,C++,Java,都是必须要指定变量的静态类型的,是静态类型语言。python,JavaScript等,变量可以不用明确声明类型的,是动态类型语言。

3.2 java.lang.invoke

3.3 invokedynamic

4. 基于栈的字节码解释执行引擎

  • 基于寄存器:快
  • 基于栈:与寄存器硬件无关

编译优化

前端指的是,编译器的前端处理过程,也就是把Java源代码转换成字节码的这个过程

一、前端编译优化

1. 泛型

泛型擦除,就四个字

2. 自动装箱拆箱与循环遍历

注意:

  1. 鉴于包装类 的“==”运算在不遇到算术运算的情况下不会自动拆箱
  2. 以及它们equals()方法不处理数据转型的关系

3. 条件编译

根据布尔常量值的真假,编译器将会把分支中不成立的代码块消除掉

二、后端编译优化

跳过

并发

这里就稍微带过一下,主要的在并发那一个模块开搞

  1. long 和 double 的特殊规则

  2. 协程:内核线程比自己的线程是1 : M的

  3. 当一个对象已经计算过一 致性哈希码后,它就再也无法进入偏向锁状态了。

    而当一个对象当前正处于偏向锁状态,又收到需要 计算其一致性哈希码请求[1]时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。在重量级锁 的实现中,对象头指向了重量级锁的位置,代表重量级锁的ObjectMonitor类里有字段可以记录非加锁 状态(标志位为“01”)下的Mark Word,其中自然可以存储原来的哈希码。

    注意,这里说的计算请求应来自于对Object::hashCode()或者System::identityHashCode(Object)方法的 调用,如果重写了对象的hashCode()方法,计算哈希码时并不会产生这里所说的请求。

  4. 轻量级锁

    1. 虚拟机首先将在当前线程的栈 帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方为 这份拷贝加了一个Displaced前缀,即Displaced Mark Word)
    2. 然后,虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。
      • 如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的 最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态
      • 如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟 机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对 象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。 如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志 的状态值变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线 程也必须进入阻塞状态
  5. 偏向锁:如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互 斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了

    1. 那么当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式。同时使用CAS操作把获取到这个锁的线程 的ID记录在对象的Mark Word之中。(如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关 的同步块时,虚拟机都可以不再进行任何同步操作)
    2. 一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是 否处于被锁定的状态决定是否撤销偏向(偏向模式设置为“0”)
image-20210212154150212