对Java虚拟机知识点的复习,参照《深入理解JVM虚拟机》。
JVM虚拟机
1 Java 内存区域
1.1 Java 内存区域模型
1.1.1 程序计数器
记录程序字节码执行到某个位置的功能,方便线程切换之后再切换回来能从上次停止的地方继续执行。此区域为线程独有的内存区域。此区域为 Java 虚拟机规范中定义的唯一不会出现 OOM 的区域。
1.1.2 Java虚拟机栈
每执行一个 Java 方法,虚拟机就会创建一个栈帧。栈帧中包含局部变量表,操作数栈,动态链接和方法出口。局部变量表中包含基本数据类型和对象引用。此区域为线程独有的内存区域。当栈帧超过虚拟机可容纳的最大值时,会出现 StackOverFlow 异常。
局部变量表中以变量槽(slot)为最小单位,只有long和double占据2个slot,其余基本数据类型均占1个slot。虚拟机通过索引定位局部变量表。正常方法从索引0出开始,第0位为this。静态方法从索引1出开始定位。
1.1.3 本地方法栈
本地方法栈和Java虚拟机作用相似。一个是针对Java方法,一个是针对native方法。
1.1.4 Java堆
Java堆中存放着对象实例,为线程公有的内存区域。
1.1.5 方法区
线程公有的内存区域,存储着类加载器加载的类信息,常量及静态变量。
1.2 创建对象
- 检测类加载器是否加载了这个类,没有加载则进行类加载过程。
- 为对象实例在Java堆上分配空间。 空间如果是规整的,采用指针碰撞进行分配,如果不规整,采用空闲列表分配。
- 虚拟机将分配的内存空间初始化零值。
- 执行
方法对对象实例进行赋值。 - 对象引用指向对象实例。
1.3 对象的内存布局
对象包含对象头,实例数据和对齐填充。
对象头包含(Mark Word) 哈希值信息,分代年龄,GC标志,线程持有的锁(偏向锁),锁状态(用来记录当前处于锁升级的何种阶段),锁偏向id(上一个持有偏向锁的线程Id)。 对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
1.4 对象的访问定位
句柄和直接指针。直接指针访问是速度快,只有一次定位时间。但是GC时对象移动需要重新修改,而句柄只需要修改句柄中的实例数据指针,reference本身不需要修改。
2. 垃圾收集器和内存分配策略
三个问题。
- 哪些对象需要回收?
- 什么时候回收?
- 如何回收?
2.1 判断对象是否需要回收
可采用 引用计数法和可达性分析算法。
2.1.1 引用计数法
对象被引用一次那么数量加1,移除引用的时候数量减1,当计数为0的时候说明对象可回收。但没办法解决相互引用的问题。
2.1.2 可达性分析算法
对象从GC Root出发,在引用链上的对象都不能进行回收。当对象不在引用链上,那么对象属于可回收状态。 GC Root: 1. Java虚拟机栈中引用的对象。 2. 常量引用的对象 3. 静态变量引用的对象 4. 本地方法栈中引用的对象
2.2 GC算法
GC算法分为三种,其中针对新生代老年代采用不同算法。
2.2.1 标记清除算法
对回收对象进行标记,然后将标记的对象进行GC。
2.2.2 复制算法
将存活的对象复制到内存的另一侧,然后将这一侧的内存回收。
2.2.3 标记整理算法
第一步和标记清除的第一步一样,标记出需要回收的对象,然后将存活的对象统统移动到内存的一侧,然后清理端边界之外的内存。
2.2.4 分代收集算法
由于新生代的特性是对象回收的多,存活的少。而老年代的对象经过GC后存活的多。所以新生代适用复制算法,老年代适合标记清除或标记整理算法。
3. 虚拟机类加载机制
3.1 类加载过程
一共有七步。分别是 加载,验证,准备,解析, 初始化, 使用 和 卸载。
3.2 双亲委派模型
引导类加载器,扩展类加载器,应用程序类加载器,自定义加载器。
双亲委派模型,要求先查该类是否已经被加载过了,如果没有加载过,那么首先子加载器不加载,传递给父加载器进行加载。如果到最上层引导类加载器还没有加载过该类,那么引导类加载器试图加载此类,如果父类加载器无法加载时,再传递给子加载器进行加载。
4. Java内存模型与线程
4.1 Java 内存模型
- 主内存和工作内存
所有变量都存储在主内存中,每个线程有自己的工作内存。线程从主内存中将变量值取到工作内存中,然后在工作内存中进行使用,赋值等,最后将工作内存中值再写入到主内存中。其中一共涉及到八个步骤,每个步骤操作均为原子性。
Lock,Read, Load, Use, Assign, Store, Write, UnLock。
4.2 volatile
有两个特性,一个是 可见性,保证此变量对所有线程的可见性。也就是被 volatile 修饰的变量被一个线程修改之后,对于其他线程是立刻可以得知的。第二个是禁止指令重排。
4.3 Java 线程
实现线程有3个方式,内核线程、用户线程、用户线程和轻量级进程实现。Java 线程是映射到操作系统线程的。
5. 线程安全与锁优化
5.1 线程安全的实现方法。
- 互斥同步
Synchronized就是互斥同步。 synchronized 是可重入的。互斥同步是悲观的。
synchronized 和 ReentrantLock 区别。
- ReentrantLock 是等待可中断,而Synchronized不可以。等待可中断是线程在等待持有锁的时候可以选择放弃等待,改做其他事情。
- ReentrantLock 可实现乐观锁。而Synchronized 是悲观锁。
- ReentrantLock 可绑定多个锁条件。Synchronized 中,锁对象的wait() notify()或 notifyAll() 只能实现一个。
- 非阻塞同步 CAS 就是非阻塞同步。CAS为线程中变量的值V,旧的预期值为A,新值为B。当且仅当V如何旧值预期A时,将V值更新为B。否则就不执行操作。CAS会出现ABA问题,解决的方式可以通过添加版本号。
Synchronized是可重入的。可重入的意思是在代码执行的任何时候中断它,转而去执行另外一段代码,在控制权返回后,原来的程序不会出现错误。
可重入一定线程安全,但线程安全不一定可重入。
5.2 锁优化
首先锁是无锁,碰到多线程时升级为偏向锁,如果当前线程和Mark Word中存储的锁偏向id不一致时,升级为轻量级锁,也即CAS去进行更新,失败的话则自选等待。自璇失败到一定次数时,升级为重量级锁。