Java虚拟机知识点梳理

JVM知识点梳理

本文链接:https://blog.csdn.net/feather_wch/article/details/132326246

JMM

定义

1、什么是内存模型?

  1. 在特定操作协议下,对特定内存和高速缓存读写的过程的抽象

2、JMM的作用是什么?

  1. 解决缓存一致性和指令重排序导致的安全问题
  2. 屏蔽具体的平台,保证CPU对内存访问效果一致

3、JMM的主要目的

  1. 定义程序中变量的访问规则:存储和读取
  2. 只针对线程共享变量:静态字段、实例字段、构成对象数组的元素
  3. 线程私有变量:局部变量、方法参数不在考虑范围内

特点

1、JMM内存结构:分为主内存、工作内存,工作内存对应于高速缓存区

  1. 所有变量存储在主内存
  2. 工作内存中放副本
  3. 程序运行时主要访问工作内存

目的

1、JMM的目的是什么?

  1. 避免数据竞争的干扰

变量

2、JMM如何保证变量的并发问题

  1. 线程私有
  2. 线程共享===>原子操作

缓存一致性

1、MESI:M-修改,E-独享互斥,S-共享,I-无效

2、缓存一致性协议是什么?

  1. 多个CPU从主内存读取数据到各自的高速缓存中
  2. 某个CPU修改缓存内数据,会立马同步到主内存
  3. 其他CPU会通过总线嗅探机制,感知到,并且让自己缓存内数据失效

3、缓存加锁是什么?CPU的缓存写回内存会导致其他CPU的缓存失效(基于MESI)

CPU高速缓存区

缘由:高速缓存区(Cache)作为CPU和内存中间:数据复制入Cache中,运算结束放回内存。缓解CPU和内存速度有几个数量级的差距

缓存一致性

多个CPU有缓存一致性问题,如果出现缓存数据不一致以谁的为准?

各个CPU采取一定协议:MSI、MESI等

私有工作内存

1、每个线程都有私有工作内存,操作副本其他线程无法感知到,可见性问题 => volatile

线程一:
while(!initFlag){} // 循环
线程二:
initFlag = true

协议MESI

总线嗅探机制 ==> MQ

重排序

三种

1、重排序分为三类

  1. 即时编译器的指令重排序
  2. CPU的乱序执行
  3. 内存系统的重排序

2、指令重排序为什么会导致问题?

  1. 数据竞争

3、JMM如何避免指令重排序?

  1. 内存屏障

as-if-serial

1、重排序需要遵守as-if-serial原则

2、as-if-serial是什么?

  1. 在单线程的情况下,有顺序执行的假象
  2. 假如数据互相依赖,不会重排序

HB

1、JMM和HB是什么?

  1. JMM的核心关键点在于构建一个跨线程的HB关系

2、无需同步手段就能成立的HB原则,有且仅有八种

  1. 程序次序规则:线程内,按照控制流顺序,书写在前面的操作 HB 书写在后面的操作
  2. 管程锁定规则:对一个锁的unlock操作,HB,对同一个锁的lock操作(时间顺序上)
  3. volatile:volatile的写操作,HB,对同一字段读操作
  4. 线程启动:start,HB,线程的第一个操作
  5. 线程终止:线程中最后一个操作,HB,线程的终止检查
  6. 线程中断:对线程interrupt,HB,线程收到的中断事件
  7. 对象终结:对象构造器的最后一个操作,HB,finalize()
  8. 传递性:A HB B, B HB C, A HB C

3、Happen-Before不代表时间上先发生
4、时间上先发生也不代表Happen-Before

三大特征

1、JMM的构建是围绕三大原则的

  1. 有序性
  2. 可见性
  3. 原子性

2、JMM如何保证原子性的?

  1. 提供了原子性操作:read load use assign store write
  2. 提供了更大范围的原子性:lock和unlock指令
  3. 提供了更高层面字节码指令:monitorenter、monitorExit(对应于synchronized)

3、JMM如何保证有序性?

  1. volatile、synchronized保证线程间操作的有序性
  2. 禁止指令重排序
  3. 持有同一锁的两个代码块串行进入

4、JMM如何保证可见性?

  1. 可见性:一个线程修改了变量,其他线程立即可见
  2. JMM规定变量修改后会同步回主内存,变量读取前从主内存刷新
  3. volatile是变量修改后立即同步回主内存,变量读取前立即刷新
  4. Java中可见性关键字:volatile、synchronized、final

内存屏障

1、JVM内存屏障有哪些类型?

  1. 读读,loadload
  2. 读写,loadstore
  3. 写写,storestore
  4. 写读,storeload

2、JVM如何实现这些屏障的

  1. 代码中读读、读写、写写,方法都是acquire(),X86底层是空指令
  2. 写读,方法是fence(),fence底层是lock指令。底层是刷新写缓存指令

3、lock指令的作用

  1. 本身不是内存屏障,但可以实现内存屏障的效果

4、volatile instance的赋值,instance = xxx,内存屏障是如何做的?

StoreStore
putStatic
StoreLoad,加内存屏障

volatile

1、volatile适合什么场景?有什么限制?
1、volatile两个作用

  1. 禁止指令重排序
  2. 保证可见性

2、volatile如何做到禁止指令重排序的?

  1. 内存屏障
  2. 底层是汇编指令,x86上写读是lock相关指令,读读,读写,写写是no-op(空指令)

3、volatile如何保证可见性?

  1. 汇编指令,写读,情况下 lock前缀的x86指令,保证两个效果
  2. 1-会锁定缓存数据数据对应在主内存中的内存地址,将当前CPU的缓存行数据 立即写回到主内存
  3. 2-写操作,会导致其他CPU缓存了该内存地址的缓存数据失效(MESI)

1、volatile是什么?

  1. JMM的最轻量的同步机制
  2. volatile变量具有可见性
    1. volatile变量对其他线程可见(写操作立即反映到其他线程)
    2. 只能保证拿到的变量值是最新的,不能保证哪个结果被同步回主内存
  3. 具有有序性: 禁止指令重排序
    1. 双重检查加锁:避免对象半初始化问题

2、volatile线程不安全

  1. 不保证原子性:运算操作不是原子性的

3、volatile什么情况下是线程安全的?

  1. 运算结果不依赖变量当前值 ++ –
  2. 变量不和其他状态变量共同参与不变约束:a >= b

4、非volatile指令重排序问题

线程A:1和2交换顺序
1. 初始化
1. initFlag = true
线程B:
1. 判断 initFlag = true
1. 使用相关内容 // 会出错,根本没初始化完成

内存间交互操作

1、八大数据原子操作

  1. 主内存 传输到 工作内存: read
  2.   存储到 工作内存: load
    
  3. 工作内存 传输到 执行引擎: use
  4. 执行引擎 赋值到 工作内存:assign
  5. 工作内存 传输到 主内存:store
  6.     写入到 主内存:write
    
  7. lock:主内存变量加锁 => 表示变量进入线程独占状态
  8. unlock:主内存变量解锁

2、什么是原子操作?不可以再细分

并发

线程

1、线程是什么?

  1. 最轻量级,最基本的调度单元
  2. 可以共享进程资源(内存地址、文件IO)
  3. 又可以独立调度

2、Java线程实现分为三种

  1. 内核线程 1:1
  2. 用户线程 1:N
  3. 混合模式 N:M

内核线程

1、内核线程是什么?1:1是什么意思?

  1. KLT:kernel-level thread
  2. LWP:light-Weight thread 轻量级进程/线程
  3. LWP和KLT是1:1的关系

2、内核线程结构是什么?

  1. P:LWP、LWP、LWP,进程有多个LWP
  2. LWP和KLT:一一对应
  3. KLT通过Thread Scheduler:调度器,将线程任务映射到CPU上
  4. Thread Scheduler:CPU、CPU、CPU

3、内核线程实现的问题是什么?

  1. 基于KLT实现,线程操作需要系统调用,涉及到用户态和内核态切换,代价高
  2. 会消耗内核资源:KLT内核线程数量有限

用户线程 1:N 弃用

1、用户线程结构

  1. CPU:P、P、P CPU资源分配到进程,内核无感知
  2. P:UT、UT、UT 进程有多个用户线程

2、问题

  1. 内核无感知,导致无法帮助处理阻塞
  2. 无法在多CPU情况下帮助映射线程到其他CPU
  3. Java弃用

混合模式 N:M

1、混合模式结构

  1. Thread Schduler 调度多个CPU:和内核线程模式一样
  2. 多个LWP和KLT一一对应:和内核线程模式一样
  3. LWP和进程内多个UT,交叉对应or一一对应

2、LWP和UT共存,LWP是UT和KLT的桥梁

3、优点

  1. UT操作廉价,可以大规模并发
  2. 内核线程用调度器利用多CPU资源调度问题
  3. 内核线程可以处理阻塞等问题

线程调度

1、抢占式:系统分配
2、协同式:线程工作完成后,通知系统切换

线程状态

1、Java定义了六种线程状态

  1. new
  2. running start notify、notifyall
  3. block synchronized,等待获得一个排他锁
  4. waiting wait、join
  5. timed-waiting wait、join、sleep
  6. terminated run结束

2、释放锁的情况

  1. sleep不会释放锁
  2. wait释放锁
  3. join不会释放锁
  4. yield释放线程锁,不释放对象锁

协程

1、协程概念

  1. 协同式调度的用户线程
  2. 协程会完整的进行栈的保护和恢复

2、线程和协程比较

  1. 线程资源有限,调度成本高,数以百万级的请求往几十~200的线程池塞,切换损耗很大
  2. 轻量,协程,几百byte~几KB,并存数量数十万。

3、协程缺点

  1. 需要在应用层实现调用栈、调度器
  2. kotlin 协程 synchronized会阻塞整个线程

4、oracle fiber纤程,是在JVM共存的新并发编程模型

5、Oracle fiber介绍

  1. 相比于传统线程池,响应速度有50~100倍提高
  2. 共用基类
  3. fiber并发分为:
    1. continuation:维护执行现场,保护,切换上下文
    2. 调度器:编排代码执行顺序

线程安全

互斥同步

synchronized

Lock

非阻塞同步

CAS

Atomic类
ABA
自旋时间过长

无同步手段

ThreadLocal

锁优化

偏向锁
轻量级锁-LockRecord
自旋-自适应
锁消除
锁粗化

synchronized

1、synchronized底层是什么?

  1. 底层是monitorenter和monitorexit指令 ==> 体现了JVMM处理原子性时,提供了更高层面的字节码指令

2、synchronized和底层monitorenter需要一个对象参数

  1. 当前对象
  2. 指定对象
  3. 类对象

管程

3、synchronized内部加锁真正的是Monitor对象

  1. 操作系统中的对象

4、Monitor对象的原理

  1. ObjectMonitor:
  2. EntryList:阻塞队列,存储获得了Monitor对象锁的线程
  3. WaitSet:等待队列,存储调用了wait()而阻塞的线程,会释放锁
  4. 1-线程加锁不成功会加到等待队列中(等待队列非EntryList,是另一个数据结构)

5、为什么会有EntryList?同时获得到锁的线程不是只有一个吗?

  1. 在某些情况下,多个线程可以同时获得同一个锁
  2. 例如:在可重入锁的情况下,同一个线程可以多次获取同一个锁。EntryList以便释放时可以顺序正确。
  3. 例如:读写锁时,多个线程持有读锁

原子类

1、原子类的性能,最少高一倍多

CAS

1、原子类借助while+CAS实现 = (自旋)
2、CAS自称无锁是指真的没有锁吗?在CPU层面也是有锁的
3、CompareAndSet源码

->Unsafe.java
->Unsafe.cpp
->1. LOCK_IF_MP: 多核CPU返回lock指令 // 加锁,保证多CPU并行安全
->2. cmpxchg: // 原子比较和交换指令 Compare and Exchange

4、lock是什么?

  1. 缓存行锁
  2. 若超过64byte(跨缓存行)会加总线锁

5、lock cmpxchg的解析,为什么原子指令还需要lock?

  1. lock前缀时,在执行cmpxchg,会对总线上的其他处理器进行锁定,以防止其他线程对同一共享变量进行并发的修改
  2. cmpxchg的操作在多核情况下会有问题,需要lock

6、CAS具有的问题

  1. ABA: 加版本号 ===> AtomicStampRefrence
  2. 原子性:

锁升级

1、锁升级的流程,以及锁各状态之间如何切换?

无状态-001(默认4秒)
|-线程id写入markword(启用偏向锁)
偏向锁-101
|-多个线程轻量竞争,CAS轻量竞争
轻量锁-00
|-自旋不成功,自适应自旋也不行,重度竞争
重量级锁-10 // markword指向monitor
无状态-001
|-未启用偏向锁
轻量锁-00
偏向锁-101
|-调用wait,直接进入重量级锁。重量级锁才有的状态。  ====> wait
重量级锁-10

2、JVM默认4秒后自动开启偏向锁

  1. 4S后new的所有对象都是101,而不是无锁的001
  2. 未开启时,加锁,直接到轻量级锁

3、偏向锁或者无锁进入轻量级锁的检查流程

  1. 检查对象头,无锁(非重量级锁),在栈帧中创建LockRecord,CAS将对象头的MarkWord更新为LockRecord指针
  2. 成功:进入轻量级锁状态
  3. 失败:检查1-MW指向自己的栈帧,代表已经拥有锁,执行(可重入)
  4. 检查2-MW指向其他线程的栈帧中LockRecord,代表有竞争,用重度锁

4、锁升级流程

  1. 默认无锁,4s后进入偏向锁状态
  2. 偏向锁释放时,不作任何操作,方便再次进入时比较threadid
  3. 拿锁时,发现有其他线程拿过锁(有竞争),进入轻量级锁
  4. 有竞争(CAS失败n次-代表起码2个线程在竞争),进入重量级锁。指向Monitor对象。

5、CAS自旋10次,或者可能自适应自旋2~3次,进入重锁

6、分代年龄等信息暂存在其他地方,会恢复。

LongAdder

1、LongAdder用于高并发下替换Atomic类, 高并发计数器

2、LongAdder原理

  1. 采用分段CAS
  2. 只有一个线程:CAS实现,有base数值
  3. 多个线程:cell数组
  4. 1-各个线程处理自己的cell1、cell2、cell3
  5. 2-最后会求和,得到最终计数(无锁,每个线程负责自己的计数,不会有冲突,求和也不会冲突)
  6. 3-数组会根据实际情况增减-有扩容、缩容机制

QUESTION

1、双重检查加锁的对象半始化问题

  1. 对象创建过程中:类加载检查、分配空间、初始化零值、设置对象头、调用init、引用指向该对象(putStatic)
  2. 不使用volatile的instance,会导致在init初始化和引用指向该对象重排序的情况下,拿到还未初始化的实例。
  3. 不会违背as-if-serial原则和HB原则
  4. volatile禁止指令重排序和保证可见性

执行引擎

字节码

1、下面字节码指令导致的效果是怎么样的?

地址 指令
0 ICONST_1
1 ISTORE 0
2 ICONST_2
3 ISTORE 1
4 ILOAD 0
5 ILOAD 1
6 ADD
7 ISTORE 2
8 RETURN

阐述在PC、操作数栈、局部变量表中是如何变换的

编译优化

泛型

解释器

即时编译器

热点代码

探测
优化(OSR)

PSO、ASO、LTO

ART

AOT

分支一

分支二:动态JIT

方法内联

目的

冗余xxx

无用xxx

复写传播

逃逸分析

栈上分配
标量替换
同步消除

隐式异常优化

CHA

非虚
虚->守护内联
->内敛缓存
->单态
->多态
-进程层面
-JVM性能监控

自动内存管理

方法区

1、方法区

  1. 线程共享
  2. 存放JVM已经加载的:类型信息、静态变量、常量、JIT编译后的代码缓存
  3. 回收:常量池回收、对类型卸载

2、运行时常量池

  1. 类加载后会将class文件中常量池的内容放入
  2. 三类数据:字面量、符号引用、直接引用

3、符号引用有哪些?

  1. 类和接口的名称
  2. 字段名称和描述符
  3. 方法名称和描述符
  4. 方法句柄和方法类型
  5. 动态调用点和动态常量

3、具有动态性:String的intern方法

  1. 返回常量池中对应的引用,不存在就新建放入常量池,再返回

2、堆

  1. 最大区域
  2. 存放:对象实例、数组、字符串常量池
  3. OOM:堆无法扩展时 ======================> 多进程

2、为什么要年龄划分?

  1. 为了更好的分配和回收
  2. 新生代、老年代、Eden空间、永久代知识GC技术的具体实现 =====> CMS

GC

新生代(Eden、Surviovr)

老年代

虚拟机栈

1、虚拟机栈是什么?

  1. 线程私有,和线程生命周期同步
  2. 具有OOM和StackOverflow异常
  3. 描述的是Java方法执行的线程内存模型,每个方法执行,代表栈帧入栈和出栈的过程

2、栈帧StackFrame里面是什么?

  1. 局部变量表
  2. 操作数栈
  3. 动态链接 => 1.静态解析(符号-直接) 2.动态链接 3.静态分派
  4. 方法出口

3、局部变量表存放的是什么?

  1. 基本数据类型-存储单位是slot
  2. 对象引用
  3. returnAddress 废弃,异常表处理替代跳转指令

4、压缩指针是什么?

  1. 64位指针压缩为32位,占据4byte,节省4byte

本地方法栈

  1. 面向Natiev方法
  2. 会OOM和StackOverflow
  3. HotSpot将两个栈合二为一

PC寄存器

1、PC

  1. 当前字节码指令的行号指示器 => 所有流程控制(操作)都依赖PC
  2. 字节码解释器通过改变PC值,来获取下一个指令
  3. 线程私有
  4. 较小内存空间,是JVM中唯一没有OOM的区域
  5. 线程执行 Java方法时:PC值为JMV字节码指令的地址。
  6. 线程执行 Native方法时:为空

直接内存

1、直接内存是什么?

  1. 不是JVM规范中的一部分,也不是运行时数据区的一部分
  2. JDK 1.4引入NIO,引入了基于Channel和缓冲区的IO方式
  3. 可以通过unsafe相关API直接分配堆外内存,通过Java堆的DirectByteBuffer对这块内存进行操作
  4. 性能:一定场景,可以避免Native堆和Java堆来回复制数据
  5. 不受JVM大小限制,收到物理内存大小限制

对象

1、对象内存布局

  1. 对象头:Markword、类型指针、数组元素长度
  2. 实例数据:各种类型字段的数据
  3. 对齐填充:占位符,对象起始地址需要是8byte的整数倍(大量实验和理论结果)

2、Markword

  1. 8byte
  2. 包含:hashcode、分代年龄、偏向线程ID、偏向锁、锁状态

3、类型指针:指向类型元数据(方法区)

new指令

1、什么场景下会有new指令

  1. new关键字创建对象
  2. 对象克隆
  3. 对象序列化

创建

1、对象创建流程

  1. 类加载检查
  2. 分配内存空间
  3. 初始化零值:内存空间都设置为0,等效于成员变量都设置为零值
  4. 设置对象头:Markwork、类型引用、对齐 ===> 对象头
  5. init实例初始化 ===> invokespecial
  6. 引用赋值

2、对象创建流程中,new指令对应于1,2,3,4

3、volatile,在对象创建流程中,123456步骤上下会插入monitorEnter和monitorExit?

五种方式
流程
分配内存

TLAB、
父类private隐藏字段也占据空间

内存布局(对象头、实例数据、对齐)

1、对象的内部结构

  1. 对象头
  2. 实例数据(Data1、Data2)
  3. Padding
对象头

1、对象头组成部分

  1. MarkWord
  2. 类型元数据指针
  3. 数组对象长度

2、对象头的MarkWord包含哪些数据?

  1. 偏向锁、偏向线程ID、锁状态、 ====> 锁升级
  2. hashcode
  3. 分代年龄 ===> GC

3、Markword在不同锁状态下的内容

无锁 hashcode 对象分代年龄 0 01
偏向锁 thread id 对象分代年龄 1 01
轻量级锁 指向栈中LockRecord的指针 00
重量级锁 指向重量级锁的指针 10
GC 11

4、为什么起始地址需要是8字节的整数倍?

  1. 实验出的寻址最优解(硬件级别大量实验)

5、如何打印对象的内部组成?

ClassLayout.parseInstance(user).toPrintable()

6、字节序

  1. 大端字节序:高位字节在低地址,方便人类阅读,网络传输 ==================>
  2. 小端字节序:低位字节在低地址,计算机效率高 ====> MMKV

===> HashMap浪费空间

访问(句柄访问、直接访问)

你可能感兴趣的:(Java,JVM,java,开发语言,jvm)