• JVM复习【面试】


    JVM复习【面试】

    前言

    发布于
    2022/11/20 16:45

    写作于
    2022/11/19

    以下内容源自深入理解Java虚拟机
    仅供学习交流使用

    推荐

    《深入理解Java虚拟机

    复习【JVM】

    第一部分 走进Java

    第1章 走进Java /2

    第二部分 自动内存管理机制

    第2章 Java内存区域与内存溢出异常 /38

    2.2 运行时数据区 /38

    2.2.2 Java虚拟机栈 /39

    2.3 HotSpot虚拟机对象探秘

    2.3.1 对象的创建 /44
    2.3.2 对象的内存布局 /47
    		{	对象头	{ Mark Word
    					{	类型指针
    内存布局	{	实例数据
    		{	对齐填充
    		
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.4 实战:OutOfMemoryError异常 /50

    2.4.3 方法区和运行时常量池溢出 /56
    String.intern()
    
    • 1

    第3章 垃圾收集器与内存分配策略 /61

    3.2 对象已死吗 /62

    3.2.1 引用计数法 /62
    3.2.2 可达性分析算法 /64
    {	引用计数法——对象之间循环引用的问题
    {	可达性分析算法
    
    • 1
    • 2
    		{	虚拟机栈()中引用的对象
    		{	方法区中类静态属性引用的对象
    GCROOTS	{
    		{	方法区中常量引用的对象
    		{	本地方法栈中JNI()引用的对象
    
    • 1
    • 2
    • 3
    • 4
    • 5
    3.2.3 再谈引用 /65
    {	强——不会自动回收
    {	软——内存不够时回收
    {	弱——下一次回收
    {	虚——被收集器回收时收到一个系统通知
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    3.2.4 生存还是死亡 /66

    两次标记过程
    finalize()自救一次

    3.2.5 回收方法区 /68
    无用类
    {	该类所有实例都已经被回收
    {	加载该类的ClassLoader已经被回收
    {	该类对应的Kava.lang.Class对象没有在任何地方被引用
    
    • 1
    • 2
    • 3
    • 4

    3.3 垃圾收集算法 /69

    {	标记-清除算法	{	两次扫描,效率不高
    				{	产生大量内存碎片
    {	复制算法	Eden:Survivor=8:1	
    			E+S1-->S2	存活对象放不下是	分配担保机制	进入老年代
    {	标记-整理算法	
    			让所有存活的对象都向一端移动,然后清理掉端边界以外的内存
    {	分代收集算法
    			把对分为新生代和老年代,根据其特点采取适当的算法
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3.4 HotSpot的算法实现 /72

    3.4.1 枚举根节点 /72
    Stop The World
    可达性分析确保一致性快照中进行
    OopMap——虚拟机直接得知哪些地方存放这对象引用
    
    • 1
    • 2
    • 3
    3.4.2 安全点 /73
    只有在“特定的位置记录OopMap”
    SafePoint
    GC只有到达安全带你才能暂停
    指令序列复用
    抢断式中断——不在安全点就回复
    主动式中断——中断标志
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    3.4.3 安全区域

    范围更大

    3.5 垃圾收集器 /75

    3.5.1 Serial 收集器 /76
    单线程收集器
    串行
    STW
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    3.5.2 ParNew 收集器 /77
    多线程收集
    并行
    
    • 1
    • 2

    在这里插入图片描述

    3.5.3 Parallel Scavenge 收集器 /79
    可控制吞吐量
    吞吐量=用户线程/(用户线程+垃圾收集线程)
    并行
    
    • 1
    • 2
    • 3
    3.5.4 Serial Old 收集器 /80
    老年代的Serial 收集器
    
    • 1
    3.5.5 Parallel Old 收集器 /80
    老年代的 Parallel Scavenge 收集器
    
    • 1
    3.5.6 CMS收集器
    最短回收停顿时间
    标记清除
    4个步骤
    {	初始标记——STW
    {	并发标记
    {	重新标记——STW
    {	并发清除
    
    缺点
    {	对CPU资源敏感
    {	无法处理浮动垃圾
    {	标记清除的产生大量碎片
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    3.5.7 G1收集器 /84
    特点
    {	并发与并行
    {	分代收集
    {	空间整理
    {	可预测的停顿
    避免完全堆扫描对象是否存活
    Remembered Set 
    4个步骤
    {	初始标记		停顿线程耗时很短 	串行
    {	并发标记		找出存活对象		并行
    {	最终标记		停顿线程			并行
    {	筛选回收		回收				并发
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    3.5.8 理解GC日志 /89
    3.5.9 垃圾收集器参数总结 /90

    3.6 内存分配与回收策略 /91

    3.6.1 对象优先在Eden分配 /91
    当Eden区没有足够空间进行分配,Minor GC
    
    • 1
    3.6.2 大对象直接进入老年代 /93
    避免在Eden区及两个Survivor区之间发生大量的内存复制
    
    • 1
    3.6.3 长期存活的对象将进入老年代 /95
    -XX:MaxTenuringThreshold=15 默认
    
    • 1
    3.6.4 动态对象年龄判断 /98
    如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,
    年龄大于或等于该年龄的对象就可以直接进入老年代
    
    • 1
    • 2
    3.6.5 空间分配担保 /98
    检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
    确认Minor GC 是否是安全的
    
    HandlePromotionFailure 是否允许担保失败
    	true 检查老年代最大可用的连续空间是否大于
    		 历次晋升到老年代的平均大小(经验值)
    				是:冒险地Minor GC
    				否:
    	false 不允许冒险 先进行Full GC
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    第4章 虚拟机性能监控与故障处理工具 /101

    第5章 调优案例分析与实战 /132

    第三部分 虚拟机执行子系统

    第6章 类文件结构 /162

    6.3.1 魔数与Class文件的版本 /166
    每个Class文件的头4个字节称为魔数(Magic Number),
    它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。
    OxCAFEBABE
    紧接着魔数的4个字节存储的是Class文件的版本号:
    第5和第6个字节是次版本号(Minor Version),
    第7和第8个字节是主版本号(Major Version)
    高版本的JDK能向下兼容以前版本的Class文件,
    但不能运行以后版本的Class文件,
    即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    6.4 字节码指令简介 /196

    第7章 虚拟机类加载机制 /209

    7.2 类加载时机 /210

    在这里插入图片描述

    有且只有5种情况必须立即对类进行初始化  主动引用
    1)new、getstatic、putstatic、invokestatic
    2)反射
    3)父类的先初始化
    4)主类的先初始化
    5)动态语言支持
    
    被动引用不会触发初始化
    1子类引用父类的静态字段,不会导致子类初始化
    2通过数组定义来引用类
    3常量在编译阶段会存入调用类的常量池,本质并没有直接引用到定义常量的类
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    7.3 类加载过程 /214

    7.3.1 加载 /214
    加载完成以下3件事情
    1通过一个类的全限定名来获取定义此类的二进制字节流。
    2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    3)在内存中生成一个代表这个类的java.lang.Class对象,
      作为方法区这个类的各种数据的访问入口。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    7.3.2 验证 /216
    1.文件格式验证
    2.元数据验证
    3.字节码验证
    4.符号引用验证
    
    • 1
    • 2
    • 3
    • 4
    7.3.3 准备 /219
    类变量 0值
    static final constValue
    
    • 1
    • 2
    7.3.4 解析 /220
    1.类或接口的解析
    2.字段解析
    3.类方法解析
    4.接口方法解析
    
    • 1
    • 2
    • 3
    • 4
    7.3.5 初始化 /225
    执行类构造器方法()方法的过程
    由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句产生的
    编译器收集的顺序是由语句在源文件中出现的先后顺序所决定的
    
    • 1
    • 2
    • 3

    7.4 类加载器 /227

    通过一个类的全限定名来获取描述此类的二进制字节流
    
    • 1
    7.4.1 类与类加载器 /228
    对于任意一个类都需要由加载它的类加载器和这个类本身
    一同确立其在Java虚拟机的唯一性,每一个类加载器都拥有一个独立的类名称空间
    
    • 1
    • 2
    7.4.2 双亲委派模型
    要求
    除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
    父子关系通过组合实现。
    
    工作过程
    不会自己先加载,而是委派给父类加载器来完成
    
    代码清单
    先检查是否已经被加载过,若没有加载则调用父类加载器的loadClasg)方法,
    若父加载器为空则默认使用启动类加载器作为父加载器。如父类加载失败,抛出
    ClassNotFoundException 异常后,再调用自己的findClass()方法进行加载。
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    7.4.3 破坏双亲委派模型 /223
    ①protected的findClass()
    ②线程上下文类加载器
    ③程序动态性的追求(代码热替换 模块热部署)
    
    • 1
    • 2
    • 3

    第8章 虚拟机字节码执行引擎 /236

    第9章 类加载及执行子系统的案例与实战 /276

    第四部分 程序编译与代码优化

    第10章 早期(编译期)优化 /302

    10.3 Java语法糖的味道 /311

    10.3.1 泛型与类型擦除 /311
    真实泛型 
    通过类型膨胀实现
    伪泛型
    类型擦除,变为原生类型,在相应的地方插入了强制转型代码
    
    • 1
    • 2
    • 3
    • 4
    10.3.2 自动装箱、拆箱与循环遍历 /315
    自动装箱、拆箱——对应的包装、还原  Integer.valueOf() intValue()
    循环遍历——迭代器
    变长参数——数组类型的参数
    
    • 1
    • 2
    • 3
    10.3.3 条件编译 /317
    if(true){
    	语句1;
    }else{
    	语句2;
    }
    -->
    语句1;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    第11章 晚期(运行期)优化 /329

    第五部分 高效并发

    第12章 Java内存模型与线程 /360

    12.3 Java内存模型 /362

    12.3.1 主内存与工作内存 /363
    Java内存模刑规守了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问间对方工作内存中的恋量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图12-2所示。
    
    
    • 1
    • 2

    在这里插入图片描述

    12.3.2 内存间交互操作 /364
    8个操作
    lock unlock read load use assign store write
    1.lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
    2.unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释
    放后的变量才可以被其他线程锁定。
    3.read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作
    内存中,以便随后的 load动作使用。
    4.load(载入):作用于工作内存的变量,它把 read操作从主内存中得到的变量值放入
    工作内存的变量副本中。
    5.use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,
    每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
    6.assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作
    内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
    7.store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存
    中,以便随后的 write操作使用。
    8.write(写入)作用于主内存的变量,它把store操作从工作内存中得到的变量的值放
    入主内存的变量中。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    8个规则
    1.不允许read和 load、store和 write操作之一单独出现,即不允许一个变量从主内存读
    取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
    2.不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把
    该变化同步回主内存。
    3.不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同
    步回主内存中。
    4.一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始
    化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,
    必须先执行过了assign和 load操作。
    5.一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条
    线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会
    被解锁。
    6.如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使
    用这个变量前,需要重新执行load 或assign操作初始化变量的值。
    7.如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允
    许去unlock 一个被其他线程锁定住的变量。
    8.对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write
    操作)。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    12.3.3 对于volatile型变量的特殊规则 /366
    保证可见性 不保证原子性
    
    禁止指令重排
    lock addl $0x0,(%esp)操作 内存屏障
    空操作:把ESP寄存器的值加0
    
    关键在于lock前缀,查询IA32手册,它的作用是使得本CPU的Cache写入了内存,
    该写入动作也会引起别的CPU或者别的内核无效化 (Invalidate)其 Cache,这种操作相当于
    对Cache中的变量做了一次前面介绍Java内存模式中所说的“store和 write”操作。所以通
    过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。
    
    那为何说它禁止指令重排序呢?
    因此,lock addl $Ox0,( %esp)指令把修改同步到内存时,意味着所有之前的操作都已经执行完
    成,这样便形成了“指令重排序无法越过内存屏障”的效果。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    在本节的最后,我们回头看一下Java内存模型中对 volatile变量定义的特殊规则。假
    定T表示一个线程,V和W分别表示两个volatile型变量,那么在进行 read、load、use、
    assign、store和 write操作时需要满足如下规则;
    
    1.只有当线程T对变量√执行的前一个动作是load 的时候,线程T才能对变量V执行
    use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才
    能对变量V执行load 动作。线程T对变量V的use动作可以认为是和线程T对变量
    V的 load、read动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次
    使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做
    的修改后的值)。
    2.只有当线程T对变量V执行的前一个动作是assign 的时候,线程T才能对变量V执行
    store动作;并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才
    能对变量V执行assign动作。线程T对变量V的 assign动作可以认为是和线程T对变
    量V的store、write动作相关联,必须连续一起出现(这条规则要求在工作内存中,
    每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量
    V所做的修改)。
    3.假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相
    关联的load或store动作,假定动作P是和动作F相应的对变量V的read或 write动
    作;类似的,假定动作B是线程T对变量W实施的use或assign动作,假定动作G
    是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的
    read或 write动作。如果A先于B,那么P先于Q(这条规则要求volatile修饰的变
    量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    12.3.4 对于long和double型变量的特殊规则 /372
    JMM 非原子协定
    虚拟机 原子操作
    
    • 1
    • 2
    12.3.5 原子性、可见性、有序性
    原子性 8个原子操作
    synchronized	隐式的lock、unlock
    {	monitorenter
    {	monitorexit
    
    可见性
    当一个线程修改了共享变量的值,其他线程能够立即得知这个修改
    {	volatile	保证新值立即同步到主内存中,以及每次使用前立即从主内存刷新
    {	synchronized 对于一个变量执行unlock操作之前,必须先把此变量同步会主内存
    {	final	被final修饰的字段在构造器一旦初始化完成,
    			那么在其他线程就能看见final字段的值
    
    有序性
    如果在本线程内观察,所有操作都是有序的;	
    	线程内表现为串行的语义
    如果在另一个线程观察另一个线程,所有操作都是无序的
    	 “指令重排序”现象和“工作内存与主内存同步延迟”现象
    {volatile	禁止指令重排序的语义
    {synchronized	一个变量在同一时刻值允许一条线程对其进行lock操作
    				决定了持有同一个锁的两个同步块只能串行进入
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    12.3.6 先行发生原则 /375
    先行发生是Java内存模型定义的俩项操作之间的偏序关系
    
    下面是Java 内存模型下一些“天然的”先行发生关系,这些先行发生关系无需任何同步
    器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法
    从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。
    
    1.程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前
    面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代
    码顺序,因为要考虑分支、循环等结构。
    2.管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的
    lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
    3.volatile变量规则 (Volatile Variable Rulce):对一个volatile变量的写操作先行发生于
    生。这里的“后面“对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
    4.线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每
    一个动作。
    5.线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程
    的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段
    检测到线程已经终止执行。
    6.线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于
    被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到
    是否有中断发生。
    7.对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行
    发生于它的finalize()方法的开始。
    8.传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,
    那就可以得出操作A先行发生于操作C的结论。
    
    Java语言无须任何同步手段保障就能成立的先行发生规则就只有上面这些了,笔者演
    示一下如何使用这些规则去判定操作间是否具备顺序性,对于读写共享变量的操作来说,就
    是线程是否安全,读者还可以从下面这个例子中感受一下“时间上的先后顺序”与“先行发
    生”之间有什么不同。演示例子如代码清单12-9所示。
    
    结论:时间先后顺序余先行发生原则之间基本没有太大的关系
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    12.4 Java与线程 /378

    12.4.1 线程的实现 /378
    1.使用内核线程实现
    2.使用用户线程实现
    3.使用用户线程加轻量级进程混合实现
    4.Java线程的实现
    	基于操作系统原生线程模型
    
    • 1
    • 2
    • 3
    • 4
    • 5
    12.4.2 Java线程调度 /381
    协同式线程调度
    抢占式线程调度 √
    
    • 1
    • 2
    12.4.3 状态转换
    新建 		New						创建
    运行 		Runnable				就绪、运行
    无限期等待 	Waiting
    限期等待 	Timed Waiting
    阻塞			Blocked 				阻塞
    终止			Terminated				终止
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    第13章 线程安全和锁优化 /378

    13.2 线程安全 /385

    13.2.1 Java语言中的线程安全 /386
    1.不可变
    String、枚举类、Long和Double等数值包装类,BigInteger和BigDecimal等大数据类型
    2.绝对线程安全
    大多数都不是绝对线程安全
    3.相对线程安全
    Vector、Hashtable、Collections的synchronizedCollection()方法包装的集合
    4.线程兼容
    大多数都是线程兼容的 ArrayList、HashMap
    5.线程对立
    应当经历避免 suspend()和resume() 死锁
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    13.2.2 线程安全的实现方法 /390
    1.互斥同步 阻塞同步 悲观
    synchronized	monitorenter monitorexit    
    
    在Java中,最基本的互斥同步手段就是svnchronized关键字,synchronized关键字经过
    编译之后,会在同步块的前后分别形成monitorenter和 monitorexit这两个字节码指令,
    这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java程
    序中的synchronized明确指定了对象参数,那就是这个对象的reference;如果没有明确
    指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例
    或Class对象来作为锁对象。
    
    根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。如果
    这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,
    在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。如果获取对象
    锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
    
    在虚拟机规范对monitorenter和monitorexit的行为描述中,有两点是需要特别注意的。
    首先,synchronized同步块对问一条线程来说是重入的,不会出现自己把自己锁死的问题,
    其次,同步块在已进入的线程执仃元之刖,公阻基后面具他线程的进入。第12章讲过,Java
    到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系
    来帮忙完成,这就需要从用户态转换到核心态,因此状态转换需要耗费很多的处理器时
    间。对于代码简单的同步块(如被synchronized修饰的getter()或setter(方法),
    状态转换消耗的时间有可能比用户代码执行的时间还要长。所以 synchronized是Java语言
    中一个重量级(Heavyweight)的操作,有经验的程序员都会在确实必要的情况下才使用
    这种操作。而虚拟机本身也会进行一些优化,譬如在通知操作系统阻塞线程之前加入一段
    自旋等待过程,避免频繁地切入到核心态之中。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    重入锁ReentrantLock lock()和unlock()配合try/final
    高级功能 等待可中断 可实现公平锁 锁可以绑定多个条件
    
    等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,
    改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。
    
    公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;
    而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。
    synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,
    但可以通过带布尔值的构造函数要求使用公平锁。
    
    锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,
    而在 synchronized中,锁对象的wait()和notify()或notifyAll()方法
    可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,
    而ReentrantLock则无须这样做,只需要多次调用newCondition(方法即可。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    2.非阻塞同步
    测试并设置(Test-and-Set)。
    获取并增加(Fetch-and-Increment)。
    交换(Swap).
    比较并交换(Compare-and-Swap,下文称CAS).
    加载链接/条件存储(Load-Linked/Store-Conditional,下文称LL/SC)。
    
    CAS指令需要有3个操作数,分别是内存位置(在Java中可以简单理解为变量的内存
    地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,当且
    仅当符合旧预期值A时,处理器用新值B更新V的值,否则它就不执行更新,但是无论
    是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作。
    
    CAS的ABA问题
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    3.无同步阻塞
    可重入代码(Reentrant Code):这种代码也叫做纯代码(Pure Code),可以在代码执行
    的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,
    原来的程序不会出现任何错误。
    
    线程本地存储(Thread Local Storaoe), 如果一段代码中所需要的数据必须与其他代码共
    享,那就看看这些共享数据的代码是否能促证在同一个线程中由地行?如何能促证,我们就可以
    把共享数据的可见范围限制在同一个线程之内,这样。无须同步能促证线程之间不出现数据
    争用的问题。
    
    符合这种特点的应用并不少见,大部分使用消息队列的架构植式(如“生产者-消费
    者”模式)都会将产品的消费过程尽量在一个线程中消费完,其中最重要的一个应用实例就
    是经典Web交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方
    式,这种处理方式的广泛应用使得很多Web服务端应用都可以使用线程本地存储来解决线程
    安全问题。
    Java语言中,如果一个变量要被多线程访问,可以使用volatile关键字声明它为“易变
    的”;如果一个变量要被某个线程独享,Java中就没有类似C+中_declspec(thread) 这样
    的关键字,不过还是可以通过java.lang.ThreadLocal类来实现线程本地存储的功能。每一个
    线程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.
    threadLocalHashCode为键,以本地线程变量为值的K-V值对,ThreadLocal对象就是当
    前线程的ThreadLocalMap的访回人口,母一个 ThreadLocal对象都包含了一个独一无二的
    threadLocalHashCode 值,使用选下就以仕线性K-V值对中找回对应的本地线程变量。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    13.3 锁优化 /397

    13.3.1 自旋锁和自适应自旋 /398
    自旋锁——忙循环——白白浪费处理器资源
    自适应自旋——前一次在同一个锁上的自旋时间以及锁的拥有者状态来决定
    
    • 1
    • 2
    前面我们讨论互斥同步的时候,提到了互斥同步对性能最大的影啊是阻塞的实现,挂
    起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的开发性能带来了很大
    的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很
    短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理
    器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一
    下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等
    待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
    
    自旋锁在JDK 1.4.2中就已经引入,只不过默认是关闭的,可以使用-XX:+UseSpinning
    参数来开启,在JDK 1.6中就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处
    理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要古用处理器时间的,
    因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间
    很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性
    能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然
    没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次,用户
    可以使用参数-XX:PreBlockSpin来更改。
    
    在JDK 1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前
    一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等
    待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有
    可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如 100个循环。另外,如果
    对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避
    免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对
    程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    13.3.2 锁消除 /398
    虚拟机即时编译器在运行时,
    对一些代码要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除
    
    • 1
    • 2
    13.3.3 锁粗化 /398
    如果虚拟机探测到
    有这样一串零碎的操作都对同一个对象加锁,
    将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,
    
    • 1
    • 2
    • 3
    13.3.4 轻量级锁 /400
    锁记录	CAS操作
    有两条以上的线程争用同一个锁,膨胀为重量级锁
    
    • 1
    • 2
    轻量级锁是JDK 1.6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作
    系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调
    一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,
    减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
    
    要理解轻量级锁,以及后面会讲到的偏向锁的原理和运作过程,必须从HotSpot 虚拟机
    的对象(对象头部分)的内存布局开始介绍。HotSpot虚拟机的对象头(Object Header)分
    为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分
    代年龄(Generational GC Age)等,这部分数据的长度在32位和64位的虚拟机中分别为32bit
    和64bit,官方称它为“Mark Word",它是实现轻量级锁和偏向锁的关键。另外一部分用于
    存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存
    储数组长度。
    
    对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,
    Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根
    据对象的状态复用自己的存储空间。例如,在32位的HotSpot虚拟机中对象未被锁定的状态
    下,Mark Word 的 32bit空间中的25bit用于存储对象哈希码(HashCode),4bit用于存储对
    象分代年龄,2bit用于存储锁标志位,1bit因\固定为0,在其他状态(轻量级锁定、重量级锁定、
    GC标记、可偏向)下对象的存储内容见表13-1。
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述

    简单地介绍了对象的内存布局后,我们把话题返回到轻量级锁的执行过程上。在代码
    进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将
    在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的
    Mark Word 的拷贝(官方把这份拷贝加了一个 Displaced前级,即 Displaced Mark Word),这
    时候线程堆栈与对象头的状态如图13-3所示。
    
    然后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指
    针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的
    锁标志位(Mark Word的最后2bit)将转变为“00”,即表示此对象处于轻量级锁定状态,这
    时候线程堆栈与对象头的状态如图13-4所示。
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈
    帧,如果只说明当前线程已经拥有了这个对象的镇,那就可以直接进入同步块继续执行,否
    则能明这个慎对象已经被其他线程抢占。如果有两条以上的线程争用同一个锁,那轻量级
    锁就不再有效,要膨胀为重量级锁,锁标志的状态值变为“10,Mark Word平存储的就是指
    向重量级值(互斥量》的指针,后面等待锁的线程也要进人阻塞状态。
    
    上面描述的是轻量级锁的加锁过程,它的解锁过程也是通过CAS 操作来进行的,如果
    对象的Mark Word仍然指向着线程的锁记录,那就用CAS 操作把对象当削的 Mark Word和
    线程中复制的 Displaced Mark Word替换回来,如果替换成功,整个同步过程就完成了。如果
    替换失败,说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。
    
    轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不
    存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥
    量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有
    竞争的情况下,轻量级锁会比传统的重量级锁更慢。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    13.3.5 偏向锁 /402
    消除数据在无竞争情况下的同步原语
    
    • 1
    偏向锁也是JDK 1.6中引人的一项锁优化,它的目的是消除数据在无竞争情况下的同步
    原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去
    消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操
    作都不做了。
    
    偏向锁的“偏”",就是偏心的“偏”、偏祖的“偏”,它的意思是这个锁会偏向于第一个
    获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的
    线程将永远不需要再进行同步。
    
    如果读者读懂了前面轻量级锁中关于对象头 Mark Word与线程之间的操作过
    程,那偏向锁的原理理解起来就会很简单。假设当前虚拟机启用了偏向锁(启用参
    数-XX:+UseBiasedLocking,这是JDK 1.6的默认值),那么,当锁对象第一次被线程获取的
    时候,虚拟机将会把对象头中的标志位设为“01”,即偏向糍式。同时使用CAS操作把获
    取到这个锁的线程的ID记录在对象的Mark Word之由加里CAS操作成功,持有偏向锁
    的线程以后每次进入这个锁相关的同步块时,虚抑机都可以不再进行江何同步操作(例如
    Locking、Unlocking及对Mark Word 的 Update等)。
    
    当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前前是否处
    于被锁定的状态,撤销偏向(Revoke Bins)后恢复到未锁定(标志位为“01”)或轻量级锁
    定(标志位为“00")的状态,后续的同步操作就加上而个领的轻量锅销那样执行。偏向锁、
    轻量级锁的状态转化及对象 Mark Word的关系如图13-5所示。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在这里插入图片描述

    偏向锁可以提高带有同步但无竟争的程序性能。它同样是一个带有效益权衡(Trade
    Off)性质的优化,也就是说,它开不一定总是对程序运行有利,如果程序中大多数的锁总是
    被多个不同的线程访问,那偏回模式就定多尔的。在具体问题具体分析的前提下,有时候使
    用参数-XX:-UseBiasedLocking来禁止偏向锁优化反而可以提升性能。
    
    • 1
    • 2
    • 3
    • 4

    最后

    2022/11/19 17:28

    这篇博客能写好的原因是:站在巨人的肩膀上

    这篇博客要写好的目的是:做别人的肩膀

    开源:为爱发电

    学习:为我而行

  • 相关阅读:
    目标检测算法——YOLOv5/YOLOv7改进之结合CBAM
    Java零基础(第十三章)---》方法覆盖和多态
    什么是谐波?谐波的危害
    【Dotnet 工具箱】JIEJIE.NET - 强大的 .NET 代码混淆工具
    【愚公系列】2022年09月 python人工智能-PyTorch环境配置及安装
    无人化在线静电监控系统的组成
    快解析的ERP远程管理解决方案
    组合计数3以及高斯消元
    自动驾驶中间件
    2023-10-11 python-windows平台-安装-记录
  • 原文地址:https://blog.csdn.net/qq_51625007/article/details/127935911