异常实例构造昂贵
生成堆栈信息,逐一访问当前线程的Java栈帧,记录各种调试信息(方法名,类名,文件名,第几行)

静态绑定:解析时能识别具体的目标方法(重载)
动态绑定:运行过程中根据调用者的动态类型来识别目标方法的情况。
执行时根据实际类型在其方法表中根据索引值获取目标方法。索引值是链接时的指代符号。(重写)

动态绑定与静态绑定相比,仅多出几个内存解引用操作:

  • 访问栈上的调用者
  • 读取调用者的动态类型
  • 读取该类型的方法表
  • 读取方法表中某个索引值所对应的目标方法。

调用指令,5种

  1. invokestatic:用于调用静态方法。
  2. invokespecial:用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
  3. invokevirtual:用于调用非私有实例方法。
  4. invokeinterface:用于调用接口方法。
  5. invokedynamic:用于调用动态方法。

即时编译优化手段:内联缓存、方法内联

方法内联:将方法体编译进来取代方法调用,减少了压栈帧,访问,弹出栈帧操作

内联缓存

一种加快动态绑定的优化技术
缓存虚方法调用者类型及其目标方法,以后直接调用该类型对应方法(没有方法内联,还是方法调用),没有缓存则退化为方法表动态绑定。

  • 单态内联:只缓存了一种动态类型以及它所对应的目标方法
    命中,则直接调用对应的目标方法
    为了节省内存空间,Java 虚拟机只采用单态内联缓存
    大部分的虚方法调用均是单态的
  • 多态内联:
    最坏情况,两个实例交替执行调用,每次缓存都不会命中,执行动态链接且每次都会替换缓存。只有写缓存的额外开销,而没有用缓存的性能提升
  • 超多态内联:

总结

虚方法调用包括 invokevirtual 指令和 invokeinterface 指令。如果这两种指令所声明的目标方法被标记为 final,那么 Java 虚拟机会采用静态绑定。否则,Java 虚拟机将采用动态绑定,在运行过程中根据调用者的动态类型,来决定具体的目标方法。

Java 虚拟机的动态绑定是通过方法表这一数据结构来实现的。方法表中每一个重写方法的索引值,与父类方法表中被重写的方法的索引值一致。在解析虚方法调用时,Java 虚拟机会纪录下所声明的目标方法的索引值,并且在运行过程中根据这个索引值查找具体的目标方法。

Java 虚拟机中的即时编译器会使用内联缓存来加速动态绑定。Java 虚拟机所采用的单态内联缓存将纪录调用者的动态类型,以及它所对应的目标方法。

当碰到新的调用者时,如果其动态类型与缓存中的类型匹配,则直接调用缓存的目标方法。否则,Java 虚拟机将该内联缓存劣化为超多态内联缓存,在今后的执行过程中直接使用方法表进行动态绑定。

三大步骤:加载、链接、初始化
基本数据类型由Java虚拟机原先定义好,引用数据类型,泛型会被擦除,数组由虚拟机直接生成,类和接口对应字节流。

加载

定义:查找字节流并创建类结构
数据源:jar文件、class文件、网络数据源等

数组没有字节流由Java虚拟机直接生成,其他类需借助类加载器完成查找字节流过程。

类加载器

功能

功能:加载类,提供命名空间
类的唯一性是由类加载器实例以及类的全名确定。
同一串字节流由不同的类加载器加载,会得到两个不同的类。可借此,来运行同一个类的不同版本。

分类

  • 启动类加载器boot class loader
    最为基础、最为重要的类,如JRE lib目录下jar包中的类以及Xbootclasspath制定的类
  • 扩展类加载器 extension class loader
    boot class loader的子类
    负责相对次要又通用类的加载,如JRE lib/ext目录下jar包中的类以及由系统变量java.ext.dirs制定的类
    Java 9 改名为平台类加载器 platform class loader
  • 应用加载器 application class loader
    extension class loader的子类
    加载应用程序路径下的类,(应用程序路径指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径)
    默认应用程序中包含的类由应用类加载器加载
  • 自定义类加载器
    如实现class文件加密,加载时解密

双亲委派模型

类加载器接收到加载请求时,先将请求转发给父类加载器,附加在其没有找到该类时,它再尝试加载。避免重复加载。

链接

三个阶段:验证、准备、解析

  • 验证
    确保加载类满足JVM约束
  • 准备
    1. 为加载类的静态字段分配内存,静态字段初始化在初始化阶段进行
    2. 构造跟其他和类层次相关的数据结构,如动态绑定方法表
  • 解析
    将常量池中的符号引用替换为直接引语

.class文件加载到JVM前,类不知道其他类及其他类的方法、字段对应的具体地址,若引用其他类成员时,JAVA编译器会生成一个符号引用指代目标方法类名、方法名、参数类型、返回值类型
解析目的就是将指代符号解析为实际引用,若引用的类未加载则触发加载此类(但未必链接及初始化它)

JVM规范要求在执行前完成引用符号的解析,没有要求连接过程中完成解析

初始化

为常量值的字段赋值,执行clinit方法
clinit方法:Java编译器会把所有final静态字段赋值和静态代码块中的代码置于clinit方法中

初始化触发时机

  • 虚拟机启动时,初始化用户指定的主类;
  • 初始化 new 指令的目标类;
  • 调用静态方法的指令时,初始化该静态方法所在的类;
  • 访问静态字段的指令时,初始化该静态字段所在的类;
  • 子类的初始化会触发父类的初始化;
  • 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
  • 使用反射API对某个类进行反射调用时,初始化这个类;
  • 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

总结

加载是指查找字节流,并且据此创建类的过程。加载需要借助类加载器,在Java虚拟机中,类加载器使用了双亲委派模型,即接收到加载请求时,会先将请求转发给父类加载器。

链接,是指将创建成的类合并至Java虚拟机中,使之能够执行的过程。链接还分验证、准备和解析三个阶段。其中,解析阶段为非必须的。

初始化,则是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化。

问题

1
2
3
4
public static Object getInstance(boolean flag) {
if (flag) return new LazyHolder[2]; //会加载、链接、初始化LazyHolder吗?
return LazyHolder.INSTANCE;
}

会加载,不会链接、初始化。flag=true时链接、初始化。

为什么在虚拟机里运行?

  1. 高级语言语法复杂,抽象程度高,不能直接在硬件上运行(需转换成java字节码)。
    Java字节码指令的操作码(opcode)被固定为一个字节,所以取名java字节码
  2. 虚拟机带来了另一个好处:托管环境,可处理内存管理、垃圾回收,数组越界检查、动态类型、安全权限的动态检查,聚焦于业务代码处理。

Java虚拟机具体是怎样运行Java字节码的?

JVM内存模型

5部分

  • 方法区
    JVM加载class文件到方法区
  • 线程私有区域
    • 线程方法栈
    • 本地方法栈
    • PC寄存器(计数器) 存放线程执行位置(线程执行的字节码指令地址)

解释执行/编译执行

HotSpot为例,JVM需将方法去的字节码文件转换为机器码文件,两种方式

  1. 解释执行
    逐条将字节码翻译为机器码并执行
  2. 即时编译
    将方法中所有字节码编译成机器码后执行

解释执行快不需等待,即时编译实际运行速度快。HotSpot默认采用混合模式,结合两者有点,先解释执行字节码,后将热点代码以方法为代码即时编译。

Java虚拟机的运行效率究竟是怎么样的?

HotSpot JVM采用多技术提升启动性能及峰值性能,重要手段:即时编译。
20%代码占用80%计算资源,二八原则。80%不常用代码采用解释执行,20%热点代码即时编译。

HotSpot内置多个即时编译器:C1 、C2 、 Graal
在编译时间和生成代码的执行效率取舍

  • C1 Client编译器
    优化手段简,编译快,启动快,面向客户端程序
  • C2 Server编译器
    优化手段复杂,编译较慢,但生成代码执行效率高。面向对峰值性能有要求的服务器端程序。

JAVA7开始HosPot默认采用分次编译:热点代码首先C1编译,之后热点中的热点C2编译
HosPot即时编译在额外的编译线程中进行,为了不干扰应用正常运行。根据CPU数目设置线程数,按1:2配置C1、C2

计算资源充足的情况下,解释执行和编译执行同时进行,下次调用该方法时替换解释执行。
即时编译器因为有程序的运行时信息,优化效果更好,代码效率更高,程序峰值性更好

思考题

对于发布不频繁程序,运行时性能要求高,为什么不全部编译成机器码呢?

  1. AOT能够在线下将Java字节码编译成机器码,主要是用来解决启动性能不好的问题。
  2. 对于发布不频繁(也就是长时间运行吧?)的程序,选择线下编译和即时编译一样,因为至多一两个小时即时编译就完成了。
  3. 即时编译器有根据运行时信息优化,效果更好,峰值性能更好
  4. 线下编译和即时编译在峰值性能差不多的这个前提下,线下编译和即时编译就是两种选项,各有优缺点。JVM这样做,主要也是看重字节码的可移植性,而牺牲了启动性能。

主要内容
java并发编程核心原理
12并发工具类
9中并发设计模式
4大并发实战案例

学习目标
从单一的知识和技术中“跳出来”,高屋建瓴地看问题,并逐步建立自己的知识体系。
掌握java并发编程技术背后的逻辑关系以及应用场景。(建立并发问题的全景图,理解并发问题本质。)

开篇词

推荐《操作系统原理》,并发编程最早的应用领域就是操作系统

synchronized、wait()、notify() 不过是操作系统领域里管程模型的一种实现,条件变量 Condition也是管程里的概念。单独理解管中窥豹,站在理论模型高度会发现知识原来如此简单。

什么是管程?
管程作为一种解决并发问题的模型,是继信号量模型之后的一项重大创新,它与信号量在逻辑上是等价的(可以用管程实现信号量,也可以用信号量实现管程),但是相比之下管程更易用。

并发编程三个核心问题:分工、同步、互斥。
分工指的是如何高效地拆解任务并分配给线程,而同步指的是线程之间如何协作,互斥则是保证同一时刻只允许一个线程访问共享资源。
Java SDK 并发包很大部分内容都是按照这三个维度组织的,例如 Fork/Join 框架就是一种分工模式,CountDownLatch 就是一种典型的同步方式,而可重入锁则是一种互斥手段。

  1. 选择,永远比努力更重要。选择拼搏于细分行业里的夕阳产业,是多么愚蠢。

  2. 找到自己的问题,才是最重要的。
    这三年属于被“骂”的最多的三年,做的东西被同行“骂”,汇报被领导“骂”,被“骂”的多了,渐渐就意识到自己的问题了。

  3. 驱除虚妄,才能进步
    所有的失败都可以归结为“错估了形势,低估了敌人,高估了自己”。
    《驱除虚妄,才能进步》

一种轻量级的线程
协程是在用户态调度切换的成本更低,线程是在内核态中调度
协程比线程栈要小得多,协程栈的大小往往只有几K或者几十K,线程栈大小差不多有1M

线程同步意味着等待,线程等待是严重浪费。协程等待成本没那么高。

软件事务内存(Software Transactional Memory,简称STM)
数据库事务 MVCC Multi-Version Concurrency Control 多版本并发控制

读未提交,也就是允许读到未提交的数据,这种情况下查询是不会使用锁的,可能会产生脏读、不可重复读、幻读等情况。

读已提交就是只能读到已经提交的内容,可以避免脏读的产生,属于 RDBMS 中常见的默认隔离级别(比如说 Oracle 和 SQL Server),但如果想要避免不可重复读或者幻读,就需要我们在 SQL 查询的时候编写带加锁的 SQL 语句(我会在进阶篇里讲加锁)。

可重复读,保证一个事务在相同查询条件下两次查询得到的数据结果是一致的,可以避免不可重复读和脏读,但无法避免幻读。MySQL 默认的隔离级别就是可重复读。

可串行化,将事务进行串行化,也就是在一个队列中按照顺序执行,可串行化是最高级别的隔离等级,可以解决事务读取中所有可能出现的异常情况,但是它牺牲了系统的并发性。

Actor模型本质:以Actor作为基本计算单元的计算模型
在面向对象编程里面,一切都是对象;
在Actor模型里,一切都是Actor,并且Actor之间是完全隔离的,不会共享任何变量

Actor异步模型缺点:

  • 理论上不保证消息百分百送达
  • 不保证消息送达和发送顺序一致
  • 无法保证消息会被百分百处理

Java需要借助第三方类库使用Actor模型,如Akka、Vert.x

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//该Actor当收到消息message后,
//会打印Hello message
static class HelloActor extends UntypedActor {
@Override
public void onReceive(Object message) {
System.out.println("Hello " + message);
}
}

public static void main(String[] args) {
//创建Actor系统
ActorSystem system = ActorSystem.create("HelloSystem");
//创建HelloActor
ActorRef helloActor = system.actorOf(Props.create(HelloActor.class));
//发送消息给HelloActor
helloActor.tell("Actor", ActorRef.noSender());
}

消息机制

Actor模型异步消息机制:发送消息仅仅是发送出去,接收到后可能不会立即处理

内部可保存消息表,接收到消息放入消息表,如果消息表中有数据则新消息不会马上得到处理
单线程处理消息,无并发问题

Actor不仅适用于并发计算,且适用于分布式计算
发送消息和接收消息的Actor可以不在一个进程中,也可以不在同一台机器上,只需要知道对方的地址。

Actor的规范化定义???

基础的计算单元,具体来讲包括三部分能力

  1. 处理能力,处理接收到的消息。
  2. 存储能力,Actor可以存储自己的内部状态,并且内部状态在不同Actor之间是绝对隔离的。
  3. 通信能力,Actor可以和其他Actor之间通信。
    当一个Actor接收的一条消息之后,这个Actor可以做以下三件事:
  4. 创建更多的Actor;
  5. 发消息给其他Actor;
  6. 确定如何处理下一条消息。

Actor实现累加器

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
34
35
36
37
38
39
40
41
//累加器
static class CounterActor extends UntypedActor {
private int counter = 0;

@Override
public void onReceive(Object message) {
//如果接收到的消息是数字类型,执⾏累加操作,
//否则打印counter的值
if (message instanceof Number) {
counter += ((Number) message).intValue();
} else {
System.out.println(counter);
}
}
}

public static void main(String[] args) throws InterruptedException {
//创建Actor系统
ActorSystem system = ActorSystem.create("HelloSystem");
//4个线程⽣产消息
ExecutorService es = Executors.newFixedThreadPool(4);
//创建CounterActor
ActorRef counterActor =
system.actorOf(Props.create(CounterActor.class));
//⽣产4*100000个消息
for (int i = 0; i < 4; i++) {
es.execute(() -> {
for (int j = 0; j < 100000; j++) {
counterActor.tell(1, ActorRef.noSender());
}
});
}
//关闭线程池
es.shutdown();
//等待CounterActor处理完所有消息
Thread.sleep(1000);
//打印结果
counterActor.tell("", ActorRef.noSender());
//关闭Actor系统
system.shutdown();
}
0%