大部分内容都是《深入理解Java虚拟机上的内容》的总结,少部分内容是来自于网上或者自己的理解。读完应该会把没笔记的markdown文件放在 github上。
本部分笔记对应的是《深入理解Java虚拟机》最后几章。
早期优化
编译器
- 解析
生成抽象语法树的阶段
- 填充符号
符号表是由一组符号地址和符号信息构成的表格
- 注解处理器
提供一组插入式注解处理的标准API在编译期间对注解进行处理,我们可以把它看做一组编译器的插件,在这些插件里面,可以读取,修改,添加抽象语法树中的任意元素。如果这些插件在注解期间对语法书进行了修改,编译器将会到解析及符号表填充的过程重新处理,知道所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环称为一个Round。
- 标注检查
- 数据及控制流分析
- 解语法糖
- 生成字节码
语法糖
泛型
public class GenericTypes{
public static void method(List list){
}
public static void method(List list){
}
}
这段代码不能被编译,因为类型擦除后都变成原生类型List
public class GenericTypes{
public static int method(List list){
}
public static String method(List list){
}
}
这段代码是可以被编译运行的。方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重载选择,但是在Class文件格式之中,只要描述符不是完全一致的两个方法就可以共存。
晚期优化
HotSpot中有两个编译器,分别称为Client Compiler和Server Compiler。
解释器与编译器
热点代码
- 被多次调用的方法
- 被多次执行的循环体
编译器都是以整个方法作为编译对象
对于方法调用的热点数据判定
- 基于采样的热点数据探测(Sample Based Hot Spot Detection),采用这种方式的虚拟机会周期性的检查各个线程的栈顶,如果发现某个线程方法经常出现在栈顶,就编译。
-
基于计数器的热点探测(Count Based Hot Spot Detection),采用这种方法的虚拟机会为每个方法建立一个计数器,统计方法的执行次数,如果执行次数超过一定的阙值,就编译。
- 方法计数器,记录方法的相对频率,一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用仍然不足以让它提交给编译器,这个方法的调用计数器就会被减少一半,这个过程被称为方法调用计数器热度的衰减(Count Decay),这段时间就被称为半衰周期(Counter Half Life Time)
- 回边计数器,统计一个方法中循环体代码的执行次数,当初过一定阙值就会触发编译。
逃逸分析
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中称为方法逃逸。如果被外部线程访问,称为线程逃逸。
- 栈上分配(Stack Allocation),Java堆中的对象对于各个线程都是共享可见的,只要持有这个线程的引用,就可以访问堆中存储的对象数据。如果确定一个对象不会逃逸出一个方法之外,那就让这个对象在栈上分配,减少垃圾回收的压力
- 同步消除(Synchronization Elimination)如果一个变量不会逃逸出线程,那么同步措施就可以取消
- 标量替换(Scalar Repalace)如果逃逸分析证明一个对象不会被外部访问,并且这个对象,可以被拆散的话,那么程序真正执行的时候将可能不创建这个对象,而改为直接创建这个若干个被这个方法使用到的成员变量来代替。
Java内存模型与线程
Java内存模型
其实屏蔽的是操作系统的多级内存模型
主内存和工作内存
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每个线程还有自己的工作内存(Working Memory),线程的工作内存保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
内存间交互操作
Java的内存模型中定义了8个操作来完成,虚拟机实现时必须要保证下面提及的每一种操作都是原子的。
- lock(锁定)作用于主内存的变量,它把一个变量标志为一条线程独占的状态。
- unlock(解锁)作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放之后才能被其他的线程锁定。
- read(读取)作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入)作用于工作内存的变量,它把read操作从主内存中获取得到的变量值放入工作内存的副本中
- use(使用)作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令将会执行这个操作。
- assign(赋值)作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储)作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- wirte(写入)作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中。
基本规则
- read和load,store和write必须顺序执行,但是没有保证连续执行。
- 变量在工作内存中改变了之后必须把该变化同步回主内存中
- 线程不能无原因(无assgin操作)把数据从线程的工作内存同步回主内存中
- 一个变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化的变量。(必须要先申明?)
- 一个变量在同一个时刻只允许被一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行好多次,但是必须要执行相同次数unlock
- 对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量
- 如果没有被lock操作锁定,就不允许对它执行unlock操作
- 对一个变量执行unlock操作之前,必须要此变量同步回主内存中
volatile
volatile,依然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如果直接在主内存中读写访问一般。
符合以下两个规则,则可以不加锁
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变的约束
可见性
当一个线程修改了这个变量的值,新值对于其他线程是可以立即得到的。普通变量在线程间传递均需要通过主内存来完成。
禁止指令重排
先行原则
- 程序次序原则(应该是控制流顺序)
- 管程锁定原则
- volatile变量规则
- 线程启动规则
- 线程终止规则
- 线程中断规则
- 对象终结规则
- 传递性
Java与线程
线程的实现
内核线程实现
内核线程(Kernel-Level Thread)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射各个处理器上。支持多线程的内核叫做多线程内核
轻量级进程(Light Weight Thread),轻量级进程就是通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核之间1:1的关系称为一对一的线程模型。
会经常在用户态和内核台切换。
用户线程实现
用户线程指的是完全建立用户空间的线程库上,系统内核不能感知线程的存在。
用户线程加轻量级进程实现
内核线程和用户线程一起使用的方式
线程调度
协同式线程调度(Cooperative Threads-Scheduling)
线程的执行时间由线程本身控制,线程把自己的工作执行完成之后,要主动通知操作系统切换到另一个线程
抢占式线程调度(Preemptive Threads-Scheduling)
每个线程将由操作系统来分配执行时间,线程切换不有线程本身来决定。Java使用的线程调度方式就是抢占式调度。Java的线程是通过调用操作系统的api来实现的。所以Java提供的线程优先级也是要依赖于系统。
状态转换
就绪,运行,等待,阻塞,结束
线程安全与锁优化
线程安全的级别
不可变
绝对线程安全
不管运行时环境如何,调用者都不需要任何额外的同步措施
相对线程安全
实现
互斥
synchronize的是可重入锁
ReentrantLock
- 等待可中断,当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待。
- 公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
- 锁可以绑定多个条件
非阻塞同步
CAS
无同步方案
- 可重入代码(Reentrant Code),可以在代码执行的任何时刻中断,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。
- 线程本地存储(Thread Local Storage),ThreadLocal类
锁优化
自旋锁和自适应自旋
共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。为了让线程等待,只需要让线程执行一个忙循环体,这就是共享自旋锁。
锁消除
锁消除是指在虚拟机即使编译在运行时,对一些代码上要求同步,但是检测到不可能存在共享数据竞争的锁进行消除。有的时候会编译优化产生锁
锁粗化
对于一段代码,在不同代码块反复加锁,不如直接给这段代码加锁。
轻量级锁
HotSpot虚拟机的对象头(Object Header)分为两部分信息,第一部分用于存储对象自身的运行时数据,如HashCode,GC分代年龄(Generational GC age),这部分数据的长度在32位和64位的虚拟机分别为32bit,64bit,官方称为“Mark Word”。另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象,还会有一个额外的部分存储数组长度。
加锁
在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”),虚拟机首先将当前线程的栈帧中建立一个名为锁记录(Lock Record)空间,用于存储对象目前的Mark Work的拷贝。
虚拟机将使用CAS才做尝试将对象的Mark Word更新为指向Lock Record的指针。如果更新成功,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志编程 “00”,表示此对象处于轻量级锁定状态
如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,则说明当前线程已经拥有锁了。如果不是那轻量级线锁就不再有效,要膨胀为重量级锁。锁的标志变为“10”。
解锁
通过CAS操作进行,如果对象的Mark Word仍然指向该线程的锁记录,那么就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来,如果替换成功,同步过程就完成了。如果失败,说明有其他线程尝试获取该锁,那就释放该锁的同时,唤醒被挂起的线程。
先用CAS看看能不能修改对象锁的状态,如果不能就用挂起和阻塞的方式去修改,以表明该对象是被锁定的。从轻量级锁膨胀为重量级锁。
偏向锁
当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即为偏向模式。同时使用CAS操作把获取到这个锁的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每一次进入这个锁相关的同步块,都不需要进行任何同步操作。
当另外一个线程去尝试获取这个锁的时候,偏向模式结束,后续按照轻量级锁的方式进行处理。
如果只有一个线程进入临界区,那么就是偏向锁。如果有第二线程想要进入,那么就会锁膨胀为轻量级锁。