JAVA虚拟机
类加载过程
加载
- 取二进制流:通过一个类的全限定名获取定义此类的二进制流。
- 转换为运行时的数据结构:将字节流静态存储结构转化为方法去运行时的数据结构
- 生成class:在内存中生成一个代表这个类的class对象 ,作为方法区这个类的各种数据的访问入口
验证
- 文件格式验证:字节流进入嫩村的方法区进行存储
- 元数据验证
- 字节码验证:使用类型检查来完成数据流分析校验
- 符号引用验证:
准备
正式为类变量分配内存并设置类变量初始值,都将在方法区中进行分配
解析
虚拟机将符号引用替换为直接引用的过程
初始化
<clinit>
<init>
六种触发初始化的情形如下:
- new创建类实例
- 访问某个类的或者接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射class.forName("com.xxx.xxx")
- 初始化一个类的子类
- java虚拟机启动时被标名为启动类的类
动态调用
静态分配
对应方法的重载:依赖静态类型来定位方法执行的版本,属于多分派
动态分配
对应方法重写,属于单分派。
确认方法的步骤:
- 找到操作数栈的第一个元素所指向的对象的实际类型,即new 关键字后面类型
- 如果在常量池中找到描述符合简单方法名都相符的方法,则进行访问权限的校验通过,则直接引用
- 否则按照继承关系自下而上进行第二部操作,重新查找其他子类
- 如果始终没有找到则抛出异常
几个相关名词的定义:
宗量:方法接受者与方法的参数 单分派:根据一个总量对目标方法进行选择 多分派:根据多个宗量对目标方法进行选择
JAVA语言是一门静态多分派 动态单分派语言
锁
自旋:自旋是指某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁。
偏向锁
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令。 当只有一个线程去竞争锁的时候,我们不需要阻塞,也不需要自旋,因为只有一个线程在竞争,我们只要去判断该偏向锁中的ThreadID是否为当前线程即可。如果是就执行同步代码,不是就尝试使用CAS修改ThreadID,修改成功执行同步代码,不成功就将偏向锁升级成轻量锁。
轻量锁
获取轻量锁的过程与偏向锁不同,竞争锁的线程首先需要拷贝对象头中的Mark Word到帧栈的锁记录中。拷贝成功后使用CAS操作尝试将对象的Mark Word更新为指向当前线程的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁。如果更新失败,那么意味着有多个线程在竞争。 当竞争线程尝试占用轻量级锁失败多次之后(使用自旋)轻量级锁就会膨胀为重量级锁,重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒他。
重量锁
重量级锁的加锁、解锁过程和轻量级锁差不多,区别是:竞争失败后,线程阻塞,释放锁后,唤醒阻塞的线程,不使用自旋锁,不会那么消耗CPU,所以重量级锁适合用在同步块执行时间长的情况下
synchronized 和 volatile
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值不确定,需要从主内存中读取;synchronize锁定当前变量,只有当前线程可以当问该变量,其他线程被阻塞住
- volatile仅能使用在变量级别 synchronize可以使用在变量、方法、类
- volatile仅实现变量的修改可见性,不能保证原子性;而synchronized则都可以保证
- volatile不会造成线程的阻塞;synchronized会造成线程的阻塞
- volatile变量不会被编译器优化;synchronized标记的变量可以被编译器优化
synchronize 和 ReentrantLock
ReentrantLock扩展了synchronized ReentrantLock可以对锁的等待事件进行设置,这样就避免了死锁 ReentrantLock可以获取各种锁的信息 ReentrantLock可以灵活地实现多路通知
两者加锁机制不一样: ReentrantLock底层调用的是Unsafe的park方法加锁 synchronized操作对象头中的mark word信息进行加锁