深入理解Java虚拟机(第二版)

深入理解Java虚拟机阅读笔记

第一章

1.2java技术体系

Java技术体系包括:Java程序设计语言,各种平台上的Java虚拟机,class文件格式,JavaAPI类库
JDK: Java程序设计语言,Java的虚拟机,Java的api类库,(支持Java程序开发的最小环境)
JRE: JavaSE API,Java虚拟机(支持Java程序运行的标准环境)
Java技术体系所包含的内容

第二章Java的内存区域与内存溢出异常

2.2运行时数据区域

Java虚拟机运行时数据数据区

2.2.1程序计数器

Java字节码解释器工作时是通过改变计数器的值来选取下一条需要执行的字节码指令,每一个线程都需要有一个独立的线程计数器。

2.2.2 Java虚拟机栈

线程私有,生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法的调用和执行就对应着一个栈帧在虚拟机栈中的出栈与入栈。

2.2.3本地方法栈

本地方法栈为虚拟机使用到的native方法服务。

2.2.4 Java堆

存放实例对象。Java堆时Java垃圾收集器管理的主要区域,Java可以物理上处于不连续的内存,只要逻辑上是连续的即可。各个线程共享的区域

2.2.5方法区

它用于储存已被虚拟机加载的类的信息、常量、静态变量、即时编译后的代码的数据。各个线程共享的内存区域。本区域的来及回收针对的是常量池和对类型的卸载。

2.2.6运行时常量池

是方法区的一部分,class文件中储存的常量池信息,在类加载后放入方法区的运行时常量池。

2.2.7直接内存

并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
用于提高性能,避免在Java堆和native堆中来回复制数据。

2.3 Hotspot虚拟机

2.3.1 对象的创建

一、类的加载检查
1、检查是否能在常量池中定位到类的符号引用,以及符号引用代表的类是否已经被加载、解析、初始化过
2、如果没有加载就先执行类的加载过程
二、为新对象分配内存
1、假设Java堆的内存绝对规整(空闲空间在一边,使用过的空间在另一边),分配内存仅仅是把指针指向空闲空间那一边一一段与对象大小相同的距离。这种方式成为指针碰撞
2、如果内存是不连续的,Java虚拟机会维护一个列表记录可用内存。在分配是找到足够大的一块内存分配给对象。,这种分配方式成为空闲列表。
以虚拟机的角度看对象创建完成,但以Java程序的角度看没有,因为init方法还没执行,数据还未赋值。
保证多线程不会内存冲突的解决方案:一种是对分配的内存空间进行同步处理—实际上虚拟机采用cas配上失败重试的方式保证更新操作的原子性,另一种是把内存分配动作划分在不同空间中,即每个线程预先分配一小块内存,成为本地线程分配缓冲。线程分配内存是在本地线程分配缓冲上

2.3.2 对象的内存布局

对象在存储中的布局分为三个部分:对象头、实例数据和对齐填充
一、对象头
对象头包括两部分信息:第一部分用于储存对象自身运行时数据,另一部分是类型指针即指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例 ( 如果对象是数组,那么对象头还有一块用于记录数组长度的数据 )
二、实例数据
这是对象真正储存的有效信息,也是代码中所定义的各种类型字段的内容
三、对齐填充
起占位符的作用,对象的大小必须是8字节的整数倍。

2.3.3对象的定位访问

目前主流的访问对象方式有两种:
一、通过句柄访问,Java堆中会划分一块内存来作为句柄池,句柄中包含了对象实例数据与数据类型各自具体的地址信息。
优势:reference中储存的是稳定的句柄地址,对象被移动时只需要改变句柄中实例数据的指针,reference本身内容不需要更改
在这里插入图片描述
二、使用指针访问,那么Java堆的对象布局中就必须考虑如何放置访问类型数据相关信息,而reference中储存的就是对象地址
优势:减少一次指针定位的开销,访问速度快
在这里插入图片描述

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

3.2 对象已死吗

3.2.1引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器加1;每当引用失效,计数器减1;任何时刻技术器为0的对象就是不可能再被使用的。
缺点:难以解决对象之间的相互循环引用问题。

3.2.2 可达性分析算法

在Java中,是通过可达性分析(Reachability Analysis)来判定对象是否存活的。该算法的基本思路就是通过一些被称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
在这里插入图片描述
在Java中,可作为GC Root的对象包括以下几种:
——虚拟机栈(栈帧中的本地变量表)中引用的对象
——方法区中类静态属性引用的对象
——方法区中常量引用的对象
——本地方法栈中JNI(即一般说的Native方法)引用的对象

3.2.3 再谈引用

从可达性算法中可以看出,判断对象是否可达时,与“引用”有关。那么什么情况下可以说一个对象被引用,引用到底代表什么?
在JDK1.2之后,Java对引用的概念进行了扩充,可以将引用分为以下四类:
强引用(Strong Reference)
——强引用就是指在程序代码中普遍存在的,类似Object obj = new Object()这类似的引用,只要强引用在,垃圾搜集器永远不会搜集被引用的对象。也就是说,宁愿出现内存溢出,也不会回收这些对象。
软引用(Soft Reference)
——软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
弱引用(Weak Reference)
——弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
虚引用(Phantom Reference)
——虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

3.2.4 生存还是死亡

对象在经过两次标记方可判定死亡:
1、如果进行可达性分析时,不可达,则对象会被第一次标记,并进行一次筛选。(筛选条件为对象是否有必要执行finalize()方法:若对象没有覆盖finalize方法或者finalize以及被系统调用过了,虚拟机将视为没必要执行finalize方法)
2、如果对象判定为有必要执行finalize方法,那么将对象放进F-Queue队列中,虚拟机会自动建立一个优先级低的线程去执行它,但虚拟机并不会等待方法结束,稍后对GC会对F-Queue进行第二次标记,如果在finalize中建立了与引用链的联系,则在第二次标记过程中被移除“即将回收”的集合。如果未建立联系,基本他就真的被回收了
注:一个对象的finalize方法只可能被虚拟机调用一次,如果面临下次回收,绝不会再次执行finalize方法

3.2.5 方法回收区

方法区的垃圾回收主要回收两部分内容:废弃常量和无用的类。
回收废弃常量与回收Java堆中的对象非常相似。
无用的类需要满足3个条件:
(1)该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例;
(2)加载该类的ClassLoader已经被回收;
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

3.3 垃圾回收算法

3.3.1 标记清除算法

“标记-清除”算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。。
它的主要缺点有两个:
(1)效率问题:标记和清除过程的效率都不高;
(2)空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,碎片过多会导致大对象无法分配到足够的连续内存,从而不得不提前触发GC,甚至Stop The World。
在这里插入图片描述

3.2.2 复制算法

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
它的主要缺点有两个:
(1)效率问题:在对象存活率较高时,复制操作次数多,效率降低;
(2)空间问题:內存缩小了一半;需要額外空间做分配担保(老年代)
From Survivor, To Survivor使用的就是复制算法。老年代不使用这种算法
在这里插入图片描述

3.2.3标记-整理算法

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
在这里插入图片描述

3.3.4 分代收集算法

把Java堆分为新生代和老年代,根据年代特点采用合适的收集算法。

增量算法

增量算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

3.4 Hotspot的算法实现

3.4.1枚举根节点

从可达性分析中从GC Roots节点找引用为例,可作为GC Roots的节点主要是全局性的引用与执行上下文中,如果要逐个检查引用,必然消耗时间造成停顿。
另外可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行——这里的“一致性”的意思是指整个分析期间整个系统执行系统看起来就行被冻结在某个时间点,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果的准确性就无法得到保证。这点是导致GC进行时必须暂停所有Java执行线程的其中一个重要原因。

3.4.2安全点

在OopMap的协助下,HotSpot可以快速且准确的完成GC Roots的枚举。实际上,HotSpot只是在特定的位置记录了这些信息,这些位置被称为安全点(SafePoint)。所以安全点的设置是以让程序“是否具有让程序长时间执行的特征”为标准选定的。“长时间执行”最明显的特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生SafePoint。
对于SafePoint,另一个问题是如何在GC发生时让所有线程都跑到安全点在停顿下来。这里有两种方案:抢先式中断和主动式中断。
抢先式中断不需要线程代码主动配合,当GC发生时,首先把所有线程中断,如果发现线程中断的地方不在安全点上,就恢复线程,让他跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程来响应GC。
主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志的地方和安全点是重合的另外再加上创建对象需要分配的内存的地方。

3.4.3安全区域

使用安全点似乎已经完美解决了如何进入GC的问题,但实际情况却并不一定,安全点机制保证了程序执行时,在不太长的时间内就会进入到可进入的GC的安全点。但是程序如果不执行呢?所谓的程序不执行就是没有分配cpu时间,典型的例子就是线程处于sleep状态或者blocked状态,这时候线程无法响应jvm中断请求,走到安全的地方中断挂起,jvm显然不太可能等待线程重新分配cpu时间,对于这种情况,我们使用安全区域来解决。
安全区域是指在一段代码片段之中,你用关系不会发生变化。在这个区域的任何地方开始GC都是安全的,我们可以把安全区域看做是扩展了的安全点。
当线程执行到安全区域中的代码时,首先标识自己已经进入了安全区,那样当在这段时间里,JVM要发起GC时,就不用管标识自己为安全区域状态的线程了。当线程要离开安全区域时,他要检查系统是否完成了根节点枚举,如果完成了,那线程就继续执行,否则他就必须等待,直到收到可以安全离开安全区域的信号为止。

3.5垃圾收集器

在这里插入图片描述

3.5.1 Serial 收集器

在它运行的时候,必须暂停掉所有其他的线程,直到它工作结束.
Serial/Serial Old收集器运行示意图
对于serial收集器今天仍然在使用,因为有它存在的用处,它相对其他更复杂的收集器而言:有着简单而高效的优势。

3.5.2ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了Serial收集器外,目前只有它能与CMS收集器配合工作。
在这里插入图片描述
●并行(Parallel):指多条垃圾收集线程同时并排工作,运行在多个CPU上,但此时用户线程仍然处于等待状态。
●并发(Concurrent):指用户线程与垃圾收集线程同时交互执行,用户程序在继续运行,而垃圾收集程序运行于另一个CPU上,一个CPU负责多条垃圾收集线程,因为在不断的交替运行,时间很短,所以用户感觉不到。

3.5.3 Paraller Scavenger 收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
该收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

3.5.4 Serial Old 收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记整理算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。
如果在Server模式下,主要两大用途:
(1)在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用
(2)作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
在这里插入图片描述

3.5.6 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求
CMS收集器是基于“标记-清除”算法实现的。它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为4个步骤:
(1)初始标记
(2)并发标记
(3)重新标记
(4)并发清除
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”.
在这里插入图片描述
CMS收集器主要优点:并发收集,低停顿。
CMS三个明显的缺点:
(1)CMS收集器对CPU资源非常敏感。CPU个数少于4个时,CMS对于用户程序的影响就可能变得很大,为了应付这种情况,虚拟机提供了一种称为“增量式并发收集器”的CMS收集器变种。所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想
(2)CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。在JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,
(3)CMS是基于“标记-清除”算法实现的收集器,手机结束时会有大量空间碎片产生。空间碎片过多,可能会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前出发FullGC。

3.5.7G1收集器

G1收集器的优势:
(1)并行与并发
(2)分代收集
(3)空间整理 (标记整理算法,复制算法)
(4)可预测的停顿(G1处处理追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为一下步骤:
(1)初始标记
(2)并发标记
(3)最终标记
(4)筛选回收
在这里插入图片描述

3.6内存分配与回收策略

对象的分配主要是在新生代的Eden区上,如果启动了本地线程缓冲,就将按线程分配在TLAB上
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间分配时,虚拟机将发起一次Minor GC。
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现Major GC,经常会伴随至少一次的Minor GC(但并非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上

大对象就是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组。

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

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

JDK 6 Update 24之后的规则变化只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC

第六章类文件结构

class文件中的信息是一项一项排列的, 每项数据都有它的固定长度, 有的占一个字节, 有的占两个字节, 还有的占四个字节或8个字节, 数据项的不同长度分别用u1, u2, u4, u8表示。

6.2无关性的基石

各种不同平台的虚拟机与所有平台都统一使用的程序存储格式— 字节码( ByteCode ) 是构成平台无关性的基石。Java的规范分为了Java语言规范及Java虚拟机规范。

6.3 类文件的结构

在这里插入图片描述
SUN公司规定每个class文件都必须以一个word(四个字节)作为开始,这个数字就是魔数。class文件的名字还挺好听的的,其魔数就是0xCAFEBABE。
紧接着魔数的4个字节后,第5个和第6个表示次版本号。第7、8表示主版本号。
由于常量池中常量数量不固定,所以常量池入口放置了一项u2类型的数据,代表常量池容量计数。
在这里插入图片描述
常量池中的每一个常量都是一个表。表开始的第一位是一个u1类型的标志位,代表常量类型。
在常量池之后,紧接着的是两个字节代表访问标志,用于标识类或接口的访问信息。

之后是类索引,父类索引与接口集合。类索引和父类索引都是一个u2类型的数据,

Java单继承只能有一个父类索引。接口索引是一组u2类型的数据的集合。类索引和父类索引都指向类描述符常量。

接口索引集合的入口第一个u2类型的数据为接口计数器。

字段表用于描述接口或者类中声明的变量。

6.4字节码指令简介

Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。

在 Java 虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。

大部分的指令都没有支持整数类型 byte、char 和 short,甚至没有任何指令支持 boolean 类型。编译器会在编译器或运行期将byte 和 short 类型的数据带符号扩展为相应的 int 类型数据,将boolean 和 char 类型数据零位扩展为相应的 int 类型数据。

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括如下内容。
将一个局部变量加载到操作栈
将一个数值从操作数栈存储到局部变量表
将一个常量加载到操作数栈
扩充局部变量的访问索引的指令

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

类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显示类型转换操作,或者用来处理本节开篇所提到的字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。

Java 虚拟机提供了一些用于直接操作操作数栈的指令。

控制转移指令可以让 Java 虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改 PC 寄存器的值。

Java 虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。

第七章 虚拟机类的加载机制

7.2 类的加载时机

在这里插入图片描述

7.3 类的加载过程

加载阶段由三个基本动作组成:

  1. 通过类型的完全限定名,产生一个代表该类型的二进制数据流(根本没有指明从哪里获取、怎样获取,可以说一个非常开放的平台了)
  2. 解析这个二进制数据流为方法区内的运行时数据结构
  3. 创建一个表示该类型的java.lang.Class类的实例,作为方法区这个类的各种数据的访问入口。

7.3.2 验证

这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致上会完成4个阶段的校验工作:文件格式、元数据、字节码、符号引用

**文件格式:**验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。只有通过文件格式的验证后,字节流才会进入内存的方法区进行存储,所以后面的3个阶段的全部是基于方法区的存储结构进行的,不会再直接操作字节流。
元数据验证: 该阶段对字节码描述的信息进行语义分析,目的是保证不存在不符合Java语言规范的元数据信息。
字节码
第一阶段:通过数据流和控制流分析,确定程序语义是合法的,符号逻辑的。
第二阶段:对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。
符号引用验证
最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段——解析阶段中发生。符号引用验证的目的是确保解析动作能正常执行。

7.3.3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

7.3.4 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

7.3.5 初始化

初始化阶段是执行类构造器方法的过程。
在执行子类的的时候,会先调用父类的方法。也就意味着父类中定义的静态代码块要先于子类的类变量的赋值操作
方法不是必须的,如果一个类当中没有静态代码块,也没有对变量的赋值操作,编译器可以不为类生成方法

7.4类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。如果两个类来源于同一个Class文件,只要加载它们的类加载器不同,那么这两个类就必定不相等。

7.4.2 双亲委派模型

从Java虚拟机的角度分为两种不同的类加载器:启动类加载器其他类加载器

系统类加载器:启动类加载器、扩展类加载器、应用程序类加载器:
在这里插入图片描述

第八章 虚拟机字节码执行引擎

8.2 运行时栈帧结构

栈帧是用于支持虚拟机方法调用方法执行的数据结构,它是虚拟机运行时数据区中虚拟机栈的栈元素。
在这里插入图片描述

8.2.1 局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。
虚拟机通过索引定位的方式使用局部变量表。
为了节省栈帧空间,局部变量Slot可以重用。

8.2.2 操作数栈

操作数栈也常称为操作栈,它是一个后入先出栈。
在这里插入图片描述
让下一个栈帧的部分操作数栈与上一个栈帧的部分局部变量表重叠在一起,这样的好处是方法调用时可以共享一部分数据,而无须进行额外的参数复制传递。

8.2.3动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
字节码中方法调用指令是以常量池中的指向方法的符号引用为参数的,有一部分符号引用会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为 静态解析,另外一部分在每次的运行期间转化为直接引用,这部分称为动态连接。

8.2.4方法返回地址

当一个方法被执行后,有两种方式退出这个方法:
第一种是执行引擎遇到任意一个方法返回的字节码指令,这种退出方法的方式称为正常完成出口。
另外一种是在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理(即本方法异常处理表中没有匹配的异常处理器),就会导致方法退出,这种退出方式称为异常完成出口

8.3 方法调用

8.3.1 解析

在类加载的解析阶段解析成立的前提是:方法在程序运行前就有一个课确定的调用版本。
符合编译器可知,运行期不变的要求的主要包括静态方法和私有方法两大类。

8.4 基于栈低的字节码解释引擎

Java编译器输出的指令流,基本上是一种基于栈的指令集架构(Instruction Set Architecture,ISA),依赖操作数栈进行工作。
基于栈的指令集主要的优点就是可移植,寄存器是由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。

第十章早期(编译期)优化

10.2 Javac编译器

编译过程大致可以分为三个过程;
解析与填充符号表过程
插入式注解处理器的注解处理过程
分析与字节码生成过程
在这里插入图片描述

102.2.2解析与填充符号表

1.词法、语法分析
词法分析是将源代码的字符流转变为标记(Token)集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素。
语法分析是根据Token序列构造抽象语法树的过程,抽象语法树是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个阶段都代表着程序代码中的一个语法结构。
2、填充符号表
符号表中所登记的信息在编译的不同阶段都要用到。在语义分析中,符号表所登记的内容将用于语义检查(如检查一个名字的使用和原先的说明是否一致)和产生中间代码。在目标代码生成阶段,当对符号名进行地质分配时,符号表是地址分配的依据。

10.3 Java语法糖

10.3.1泛型与类型擦除

它的本质是参数化类型的应用,也就是说所操作的数据类型被指定为一个参数。
泛型技术在C#和Java之中的使用方式看似相同,但实现上却有着根本性的分析,C#里面泛型无论在程序源码中、编译后的IL中(中间语言,这时候泛型是一个占位符),或是运行期的CLR中,都是切实存在的,List< int > >与List< string >就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。
Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList与ArrayList就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型

第十一章晚期(运行期)优化

在部分的商用虚拟机中,java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”(Hot Spot Code)。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个过程的编译器称为即时编译器。

11.2.1解释器与编译器

HotSpot虚拟机采用解释器与编译器并存的架构,解释器与编译器两者各有优势:

当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。
在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获得更高的执行效率。
当程序运行环境中内存资源限制较大,可以使用解释执行节约内存,反之可以使用编译执行来提升效率。
解释器还可以作为编译器激进优化的一个“逃生门”,让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立时,可以通过逆优化退回到解释状态继续执行。
在这里插入图片描述
HotSpot虚拟机中内置了两个即时编译器,分别称为Client Compiler和Server Compiler,或者简称为C1编译器和C2编译器,虚拟机默认采用解释器与其中一个编译器直接配合的方式工作。
分成编译根据编译器编译、优化的模式与耗时,划分出不同的编译层次
第0层:程序解释执行,解释器不开启性能监控功能(Profiling),可触发第1层编译
第1层:也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑
第2层:也称为C2编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

11.2.2 编译对象与触发条件

运行过程中被即时编译的热点代码有两类:被多次调用的方法、被多次执行的循环体。
在这两种情况下,都是以整个方法作为编译对象,这种编译方式被称为栈上替换(即方法还在栈帧上就被替换了)。
判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为热点探测,目前主要的热点探测判定方式有两种:
基于采样的热点探测:采用这种方法的虚拟机会周期性地检查各个线程地栈顶,如果发现某个方法经常出现在栈顶,那这个方法就是“热点方法”
优点:实现简单、高效,还可以很容易地获取方法调用关系
缺点:很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测
基于计数器的热点探测:采用这个种方法的虚拟机会为每个方法建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”
优点:统计结果相对来说更加精确和严谨
缺点:实现复杂

Hotspot使用的是基于计数器的热点探测方法。因此它为每个方法准备了两类计数器:方法调用计数器和回边计数器。

方法调用计数器:用于统计方法被调用的次数,它的默认阈值在Client模式下是1500次,在Server模式在是10000次,方法被调用时,先检查该方法是否存在被JIT编译过的版本。
存在:优先使用编译后的本地代码来执行
不存在:将此方法的调用计数器值加1,执行下一步
判断方法调用计数器与汇编计数器值之和是否超过方法调用计数器的阈值
超过阈值:向即时编译器提交一个该方法的代码编译请求。默认不会同步等待编译请求完成,而是继续解释执行,当编译工作完成之后,这个方法的调用入口地址就会被系统自动改写成新的,下一次调用该方法时就会使用已编译版本
未超过:解释执行
如果不做任何设置,方法调用计数器统计的不是方法被调用的绝对次数,而是一个相对执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一般,这个过程称为方法调用计数器的热度衰减。
方法调用计数器触发即时编译如下图
在这里插入图片描述
回边计数器:用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”
回边计数器阈值计算公式:
Client模式:方法调用计数器阈值* OSR比率 / 100 ==> 默认值为13995
Server模式:方法调用计数器阈值 * (OSR比率 - 解释器监控比率 / 100) ==> 默认值为10700
当解释器遇到一条回边指令时,会先查找将要执行的代码片段是否有已经编译好的版本
有:优先执行已编译代码
无:把回边计数器的值加1,执行下一步
判断方法调用计数器与回边计数器值之后是否超过回边计数器的阈值
超过:提交一个OSR编译请求,并且把回边计数器的值降低一些,以便继续在编译器中执行循环,等待编译器输出编译结果
未超过:解释执行
与方法计数器不同,回边计数器没有计数热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数
回边计数器触发即时编译如下:
在这里插入图片描述

11.2.2编译过程

Server Compiler和Client Compiler两个编译器的编译过程是不同的,对于Client Compiler来说,它是一个简单快速的三段式编译器,主要的关注点在于局部性的优化,而 放弃了许多耗时较长的全局优化手段。
server compiler三个阶段如下
1、一个平台独立前段将字节码构造成一种高级中间代码表示(HIR),HIR使用 静态单分配的形式来表示代码值,这可以使得一些在JIR的构造过程之中和之后进行的优化动作更容易实现。在此之前,编译器会在字节码上完成一部分基础优化,如方法内联、常量传播等。
  2、一个平台相关的后端从HIR中产生低级中间代码表示,而在此之前,在HIR上完成另外一些优化, 如空值检查消除、范围检查消除等。
  3、在平台的后端使用线性扫描算法在LIR上分配寄存器,并在LIR上做窥孔优化,然后产生机器代码。

11.3 编译优化技术

虚拟机中的具有代表性的优化技术:
语言无关的经典优化技术之一:公共子表达式消除。
语言相关的经典优化技术之一:数组范围检查消除。
最重要的优化技术之一:方法内联
最前沿的优化技术之一:逃逸分析
公共子表达式消除
如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式。
优化仅限于程序的基本块内称为局部公共子表达式消除; 优化的范围涵盖了多个基本块称为 全局公共子表达式消除
方法内联
方法内联的优化行为只是把目标方法的代码“复制”到发起调用的方法之中,避免发生真实的方法调用而已。它更重要的意义是为其他优化手段建立良好的基础。
编译器在进行内联时,如果是非虚方法,那么直接进行内联就可以了,这时候的内联是有稳定前提保障的。如果遇到虚方法,则会向CHA查询此方法在当前程序下是否有多个目标版本可供选择,如果查询结果只有一个版本,那也可以进行内联,不过这种内联就属于激进优化,需要预留一个“逃生门”,称为守护内联
逃逸分析
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。

如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高效的优化。

栈上分配:如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存将会非常好 – 对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,不会逃逸的局部对象所占的比例很大,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,GC的压力将会小很多。
同步消除:线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以消除掉。
标量替换:标量(Scalar)是指一个数据已经无法再分解成更小的数据来表示了。相对的,如果一个数据可以继续分解,那它就称作聚合量(Aggregate)。如果把一个Java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上分配和读写之外,还可以为后续进一步的优化手段创建条件。

11.4 javac与C/c++编译器对比

Java虚拟机的即时编译器与C/C++的静态优化编译器相比,可能会由于下列这些原因而导致输出的本地代码有一些劣势。
第一,因为即时编译器运行占用的是用户程序的运行时间,具有很大的时间压力,它能提供的优化手段也严重受制于编译成本。
第二,Java语言是动态的类型安全语言,这就意味着需要由虚拟机来确保程序不会违反语言语义或访问非结构化内存。
第三,Java语言中虽然没有virtual关键字,但是使用虚方法的频率却远远大于C/C++语言,这意味着运行时对方法接收者进行多态选择的频率要远远大于C/C++语言,也意味着即时编译器在进行一些优化时的难度要远大于C/C++的静态优化编译器。
第四,Java语言是可以动态扩展的语言,运行时加载新的类可能改变程序类型的继承关系,这使得很多全局的优化都难以进行。
第五,Java语言中对象的内存分配都是堆上进行的,只有方法中的局部变量才能在栈上分配。而C/C++的对象则有多种内存分配方式,既可能在堆上分配,又可能在栈上分配,如果可以在栈上分配线程私有的对象,将减轻内存回收的压力。

第十二章Java内存模型与线程

12.3Java内存模型

12.3.1主内存与工作内存

Java内存模型主要目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
Java内存模型规定所有变量都存储在主存中(虚拟机内存的一部分)。每条线程还有自己的工作内存,线程的工作内存保存了被线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主存来完成。
在这里插入图片描述

12.3.2 内存间交互操作

主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、从工作内存同步回主内存之类的实现细节,Java内存模型中定义了以下8种操作来完成:
Lock(锁定):作用于主内存的变量,将主内存该变量标记成当前线程私有的,其他线程无法访问它把一个变量标识为一条线程独占的状态。
Unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,才能被其他线程锁定。
Read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
Load(加载):作用于工作内存中的变量,把read操作从内存中得到的变量值放入工作内存的变量副本中。
Use(使用):作用于工作内存中的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
Assgin(赋值):作用于工作内存中的变量,把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
Store(存储):作用于工作内存中的变量,把工作内存中一个变量的值传递到主内存中,以便随后的write操作使用。
Write(写入):作用于主内存中的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。

Java内存模型还规定在执行上述8种基本操作时必须满足如下规则:
不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况。
不允许一个线程丢弃它的最近assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
不允许一个线程无原因的(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,就是对一个变量执行use和store之前必须先执行过了assign和load操作。
一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
如果对一个变量执行lock操作,僵尸清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。
对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)

12.3.3 对于volatile型变量的特殊规则

第一是保证对所有线程的可见性
第二个语义是禁止指令重排序优化
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁来保证原子性:
1)运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
2)变量不需要与其他的状态变量共同参与不变约束。

12.3.4 对于long和double型变量的特殊规则

允许虚拟机将没有被volatile修饰的64位数据类型(long和double)的读取操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read和write这4个操作的原子性,这点就是long和double的非原子协定。

12.3.5 原子性、可见性与有序性

原子性(Atomicity):由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问具备原子性(long和double例外)
可见性(Visibility):指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
有序性(Ordering):Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

12.3.6 先行发生原则

1)程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地来说应该是控制流顺序而不是程序代码顺序,因为要考虑分支/循环结构。
2)管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一锁的lock操作。这里必须强调的是同一锁,而“后面”是指时间上的先后顺序。
3)volatile变量规则(Volatile Variable Rule):对一个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。

12.4 Java与线程

12.4.1 线程的实现

实现线程主要三种方式:

  1. 使用内核线程实现
  2. 使用用户线程实现
  3. 使用用户线程加轻量级进程混合实现
    使用内核线程实现
    内核线程(Kernel Thread, KLT)就是直接由操作系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程都可以看作是内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫多线程内核。
    而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换;每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程需要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程是有限的。
    在这里插入图片描述
    ** 使用用户线程实现**
    狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到线程存在的实现。用户线程的建立/同步/销毁和调度完全在用户态完成,不需要内核的帮助。
    使用用户线程加轻量级进程混合实现
    既存在用户线程,也存在轻量级进程。

12.4.2 Java线程调度

线程调度是指系统为线程分配处理器使用权的过程。主要调度方式两种:
使用协同调度的多线程系统,线程执行时间由线程本身控制,线程把自己的工作执行完后,要主动通知系统切换到另外一个线程上去。优点:实现简单。缺点:执行时间不可控制。
使用抢占调用的多线程系统,每个线程由系统分配执行时间,线程的切换不由线程本身决定。Java使用的就是这种线程调度方式。

12.4.3 状态转换

Java语言定义了5种进程状态,在任意一个时间点,一个线程只能有且只有其中一种状态。
新建、运行、无限期等待、限期等待、阻塞、结束

第十三章线程安全与锁优化

13.2 线程安全

线程安全:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的

13.2.1 Java语言中的线程安全

java中各种操作共享的数据分为以下5类:不可变, 绝对线程安全, 相对线程安全,线程兼容,线程对立;

  1. 不可变
    在Java语言中,不可变的对象一定是线程安全的,无论是对象的方法实现还是方法方法的调用者都不需要采取任何的线程安全保障措施。
  2. 绝对线程安全
    通常代价过大甚至不切实际。
  3. 相对线程安全
    相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的。
  4. 线程兼容
    线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用
  5. 线程对立
    线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。

13.2.2 线程安全的实现方法

  1. 互斥同步
    互斥同步是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。因此,在这 4 个字里面,互斥是因,同步是果;互斥是方法,同步是目的。
  2. 非阻塞同步
    互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
    基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,知道成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
  3. 无同步方案
    要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的,笔者简单地介绍其中的两类。
    可重入代码:这种代码也叫做纯代码,可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。相对线程安全来说,可重入性是更基本的特性,它可以保证线程安全,即所有的可重入的代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。
    线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。

13.3 锁优化

13.3.1 自旋锁与自适应自旋

为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

13.3.2 锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

13.3.3 锁粗化

如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,

13.3.4 轻量级锁

13.3.5 偏向锁

它的目的是消除数据在无竞争情况下的同步原语
它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

发布了79 篇原创文章 · 获赞 0 · 访问量 2659

猜你喜欢

转载自blog.csdn.net/qq_41017546/article/details/89458901