本篇博客主要针对 Java 虚拟机的晚期编译优化,Java 内存模型与线程,线程安全与锁优化进行总结,其余部分总结请点击 Java 虚拟总结上篇 ,Java 虚拟机总结中篇。
一. 晚期运行期优化
即时编译器 JIT
即时编译器 JIT 的作用就是热点代码转换为平台相关的机器码,并进行优化,它并不是一个虚拟机所必须的部分,只能说有它是锦上添花。
热点代码
热点代码分类
- 被多次调用的方法
- 被多次调用的循环体
热点探测判定方法
- 基于采样的热点探测,虚拟机周期性地检查栈顶,发现某个方法经常出现在栈顶,那么这个方法就是热点方法,简单高效但不精确
- 基于计数器热点探测,为每个方法建立计数器来统计执行次数,超过阈值就是热点方法,Hotpot 就是采用这种方法。分为方法计数器(统计方法),回边计数器(统计循环)
编译过程(Client Complier)
- 第一阶段
- 将字节码构造成高级中间代码表示(HIR)
- 第二阶段
- 将 HIR 变为 LIR
- 第三阶段
- 使用线性扫描算法,在 LIR 上分配寄存器,产生机器代码
优化方法
公共子表达式优化
当一个表达式 A 的结果已经计算过了,且 A 中的所有变量都没有发生过变化,那么下一次要用到 A 时就不用计算了,而是直接取之前 A 的结果。
数组边界检查消除
方法内联
逃逸分析
逃逸的定义:一个在方法里定义的变量,作为参数传递给其他方法(方法逃逸),或者赋值给类变量(线程逃逸)。
优化方法:
- 栈上分配:不会逃逸的对象就不在堆上分配了,就在栈上分配,那么对象所占的空间就可以随栈帧的出栈而销毁,减少垃圾收集系统的压力。
- 同步消除:如果一个变量肯定不会逃逸出线程,那么关于这个变量的同步措施就可以去掉。
二. Java 内存模型与线程
内存模型
说了这么多的内存模型,到底什么是内存模型呢?
特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。
它的作用是定义程序中各个共享的变量的访问规则,即如何将变量写入内存和从内存中取出变量。Java 内存模型有主内存与工作内存之分,所有变量存在主内存中,线程则是拥有自己的工作内存,它是主内存的副本拷贝,线程只能读写工作内存。
8 种原子操作
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
- load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。
- write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
volatile 变量的特殊规则
volatile 的特性是保证此变量对所有线程的可见性,即当变量的值修改后,其他线程可以立即知道发生的变化。普通变量则是修改完值后,需要写回主内存,然后其他线程再从主内存读取该数据。volatile 还可以通过内存屏障来禁止指令的重排序。综合来讲它的读操作和普通变量差不多,写操作慢一点。
long 和 double 变量的特殊规则
8 种操作一般都是原子性的,但是对于 64 位的数据,内存模型允许将没有被 volatile 修饰的 64 位数据的读写操作划分为两次 32 位的操作进行—-> 非原子协定但一般我们不需要将 long 和 double 声明为 volatile。
先行发生原则
- 程序次序规则
- 管程锁定规则
- volatile 变量规则
- 线程启动规则
- 线程终止规则
- 线程中断规则
- 对象终结规则
- 传递性
Java 与线程
Java 的 Thread 类大多 API 都是 Native 方法,是与平台相关的。
实现线程的三种方式
- 使用内核线程实现:内核线程即直接由操作系统内核支持的线程,由内核来完成线程切换,程序使用轻量级进程接口与内核线程一对一的关系,内核线程再经由线程调度器分派给 CPU。
- 使用用户线程实现:用户线程的建立同步销毁调度完全在用户态中完成,不需切换到内核态,一对多的关系。
- 用户线程 + 轻量级进程:多对多的关系。
线程的调度
- 协同式调度
- 线程的执行时间由线程自己控制,执行完后再主动通知系统切换线程,可能会导致一个线程长时间地阻塞
- 抢占式调度
- 由系统分配时间,线程可以主动让出时间但是不能主动获得时间,通过设置优先级确定顺序
线程的状态
- 新建:刚刚创建还未启动
- 运行:正在执行或者等待分配时间
- 无限等待:不会被 CPU 分配时间,需要其他线程显式唤醒
- 有限等待:在一段时间后由系统自动唤醒
- 阻塞:等待一个排他锁
- 结束
三. 线程安全与锁优化
线程安全的程度,依次减弱
- 不可变,将对象中带状态的变量都置为 final
- 绝对线程安全,完全符合线程安全定义
- 相对线程安全,对这个对象的单独的操作是线程安全的,如 Vector,HashTable 等
- 线程兼容,对象本身不是线程安全的,但是可以在调用端正确地使用同步手段才能保证在并发环境下正常使用。
- 线程对立,无论调用端如何努力,都不可能实现线程安全
线程安全的实现方法
互斥同步
synchronized 关键字会在代码块的前后分别形成 monitorenter 和 monitorexit 指令,这两个指令需要一个 reference 对象参数,该锁有一个计数器以实现同步,进入时将计数器 + 1,退出时 - 1,本线程可重入,其他线程需阻塞等待。synchronized 的缺点是由于 Java 线程是映射到操作系统的,所以唤醒阻塞一个线程都需要系统帮忙,需要从用户态转到内核态,耗费很多处理器时间。
ReentrantLock 对 synchronized 的优势:
- 等待可中断
- 公平锁:必须按照申请锁的时间顺序来一次获得锁
- 锁绑定多个条件
非阻塞同步
为了解决线程阻塞和唤醒所带来的性能问题,先对共享数据进行操作,如果没有竞争就成功了,否则就补偿(不断重试直到成功)
无同步方案
- 可重入代码
- 线程本地存储,把共享数据的范围限制到线程内,ThreadLocalMap 以 ThreadLocalHashMap 为键, 以本地线程变量为值的 K-V 对
锁优化
锁优化的方案有以下几种:
- 自旋锁:为了减少线程阻塞与唤醒的消耗,线程在被阻塞时可以执行一个忙循环(自旋)
- 锁消除:对不存在共享数据竞争的锁进行消除
- 锁粗化:在一个代码块内对一个对象连续的地加锁解锁,就对整个代码块一次性加锁减少性能损耗
- 轻量级锁:无竞争地情况下使用 CAS 操作去消除同步使用地互斥量
- 偏向锁:锁会偏向于第一个获得它地线程