JVM学习
什么是JVM
定义
JVM: Java Virtual Machine -java程序的运行环境
好处
- 一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态等
组成
内存结构
程序计数器
定义
Program Counter Register 程序计数器(寄存器)
作用
记住下一条jvm指令的执行地址
特点
- 线程私有
- 不会存在内存溢出
虚拟机栈
栈:线程运行需要的空间结构
栈帧:每个方法运行时所需要的内存
活动栈帧:每个线程只有一个活动栈帧,对应正在执行的方法
栈内存溢出
- 栈帧过多
- 栈帧过大
线程运行诊断
- cpu占用过多
- 用top定位那个进程占用过高
- ps H -eo pid,tid,%cpu | grep 用ps命令进一步定位
- jstack
- 程序运行很长时间没有结果
- jstack 定位
本地方法栈
Native Method Stacks
指不用java代码编写的方法,用C或C++编写的本地方法
堆
通过new关键字,创建对象都会使用堆内存。
特点
- 线程共享,堆中的对象都许哟啊考虑线程安全的问题
- 有垃圾回收机制
堆内存诊断
jps工具
查看当前系统有哪些java进程
jmap工具
查看堆内存占用情况
jconsole工具
图形界面的,多功能的监测工具,可以连续监测
方法区
定义
Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。方法区域类似于用于传统语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但简单的实现可能不会选择垃圾收集或压缩它。此规范不强制指定方法区的位置或用于管理已编译代码的策略。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的!
组成
方法区内存溢出
1.8 之前会导致永久代内存溢出
使用 -XX:MaxPermSize=8m
指定永久代内存大小
1.8 之后会导致元空间内存溢出
使用 -XX:MaxMetaspaceSize=8m
指定元空间大小
运行时常量池
二进制字节码包含(类的基本信息,常量池,类方法定义,包含了虚拟机的指令)
常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
运行时常量池:常量池是 *.class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
StringTable
- 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder
- 字符串常量拼接的原理是编译器优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中
intern方法 1.8
调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中
- 如果串池中没有该字符串对象,则放入成功
- 如果有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象
注意:此时如果调用 intern 方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象
StringTable 的位置
jdk1.6 StringTable 位置是在永久代中
1.8 StringTable 位置是在堆中。
StringTable 垃圾回收
-Xmx10m
指定堆内存大小-XX:+PrintStringTableStatistics
打印字符串常量池信息-XX:+PrintGCDetails
-verbose:gc
打印 gc 的次数,耗费时间等信息
StringTable 性能调优
因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
-XX:StringTableSize=桶个数(最少设置为 1009 以上)
考虑是否需要将字符串对象入池可以通过 intern 方法减少重复入池
直接内存
定义
Direct Memory
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
使用直接内存的好处
文件读写流程
因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。
使用了 DirectBuffer 文件读取流程
直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。
直接内存回收原理
直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。
一般用 jvm 调优时,会加上下面的参数:
-XX:+DisableExplicitGC // 静止显示的 GC
意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。所以我们就通过 unsafe 对象调用 freeMemory 的方式释放内存。
垃圾回收
如果判断对象可以回收
引用计数法
当一个对象被引用时,就当引用对象的值加一,当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。
这个引用计数法听起来不错,但是有一个弊端,如下图所示,循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。
可达性分析算法
- JVM 中的垃圾回收器通过可达性分析来探索所有存活的对象
- 扫描堆中的对象,看能否沿着 GC Root 对象为起点的引用链找到该对象,如果找不到,则表示可以回收
- 可以作为 GC Root 的对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(即一般说的Native方法)引用的对象
四种引用
强引用
只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
软引用(SoftReference)
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
可以配合引用队列来释放软引用自身
弱引用(WeakReference)
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
可以配合引用队列来释放弱引用自身
虚引用(PhantomReference)
必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,
由 Reference Handler 线程调用虚引用相关方法释放直接内存
终结器引用(FinalReference)
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象。
演示软引用
回收算法
标记清除
定义
Mark Sweep
- 速度较快
- 会产生内存碎片
标记整理
定义
Mark Compact
- 速度慢
- 没有内存碎片
复制
定义
Copy
- 不会有内存碎片
- 需要占用两倍内存空间
分代垃圾回收
- 新创建的对象首先分配在 eden 区
- 新生代空间不足时,触发 minor gc ,eden 区 和 from 区存活的对象使用 - copy 复制到 to 中,存活的对象年龄加一,然后交换 from to
- minor gc 会引发 stop the world,暂停其他线程,等垃圾回收结束后,恢复用户线程运行
- 当幸存区对象的寿命超过阈值时,会晋升到老年代,最大的寿命是 15(4bit)
- 当老年代空间不足时,会先触发 minor gc,如果空间仍然不足,那么就触发 full fc ,停止的时间更长!
相关 JVM 参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
垃圾回收器
相关概念
- 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
- 并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个 CPU 上
- 吞吐量:即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99% 。
串行
- 单线程
- 堆内存较少,适合个人电脑
-XX:+UseSerialGC=serial + serialOld
安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象
因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态
Serial 收集器:Serial 收集器是最基本的、发展历史最悠久的收集器
特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)!
ParNew 收集器:ParNew 收集器其实就是 Serial 收集器的多线程版本
特点:多线程、ParNew 收集器默认开启的收集线程数与CPU的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题
Serial Old 收集器:Serial Old 是 Serial 收集器的老年代版本
特点:同样是单线程收集器,采用标记-整理算法
吞吐量优先
- 多线程
- 堆内存较大,多核 cpu
-XX:+UseParallelGC ~ -XX:+UsePrallerOldGC
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio // 1/(1+radio)
-XX:MaxGCPauseMillis=ms // 200ms
-XX:ParallelGCThreads=n
Parallel Scavenge 收集器:与吞吐量关系密切,故也称为吞吐量优先收集器
特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与 ParNew 收集器类似)
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与 ParNew 收集器最重要的一个区别)
GC自适应调节策略:
Parallel Scavenge 收集器可设置 -XX:+UseAdptiveSizePolicy
参数。
当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、
晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为 GC 的自适应调节策略。
Parallel Scavenge 收集器使用两个参数控制吞吐量:
XX:MaxGCPauseMillis=ms 控制最大的垃圾收集停顿时间(默认200ms)
XX:GCTimeRatio=rario 直接设置吞吐量的大小
Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本
特点:多线程,采用标记-整理算法(老年代没有幸存区)
响应时间优先
- 多线程
- 堆内存较大,多核 cpu
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark
CMS 收集器:Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器
特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务
CMS 收集器的运行过程分为下列4步:
- 初始标记:标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。
- 并发标记:进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
- 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题
- 并发清除:对标记的对象进行清除回收,清除的过程中,可能任然会有新的垃圾产生,这些垃圾就叫浮动垃圾,如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会退化为 serial Old 收集器,将老年代垃圾进行标记-整理,当然这也是很耗费时间的!
CMS 收集器的内存回收过程是与用户线程一起并发执行的,可以搭配 ParNew 收集器(多线程,新生代,复制算法)与 Serial Old 收集器(单线程,老年代,标记-整理算法)使用。
G1 收集器
定义: Garbage First
适用场景:
- 同时注重吞吐量和低延迟(响应时间)
- 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域
- 整体上是标记-整理算法,两个区域之间是复制算法
相关参数:
JDK8 并不是默认开启的,所需要参数开启
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
G1 垃圾回收阶段
- Young Collection:对新生代垃圾收集
- Young Collection + Concurrent Mark:如果老年代内存到达一定的阈值了,新生代垃圾收集同时会执行一些并发的标记。
- Mixed Collection:会对新生代 + 老年代 + 幸存区等进行混合收集,然后收集结束,会重新进入新生代收集。
Young Collection
分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间!
新生代收集会产生 STW !
Young Collection + CM
在 Young GC 时会进行 GC Root 的初始化标记
老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的 JVM 参数决定
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
Mixed Collection
会对 E,S,O 进行全面的回收
- 最终标记会 STW
- 拷贝存活会 STW
-XX:MaxGCPauseMills=xxms
用于指定最长的停顿时间!
问:为什么有的老年代被拷贝了,有的没拷贝?
因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)
Full GC
G1 在老年代内存不足时(老年代所占内存超过阈值)
如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理
如果垃圾产生速度快于垃圾回收速度,便会触发 Full GC,然后退化成 serial Old 收集器串行的收集,就会导致停顿的时候长。
Young Collection 跨代引用
- 新生代回收的跨代引用(老年代引用新生代)问题
- 卡表 与 Remembered Set
- Remembered Set 存在于E中,用于保存新生代对象对应的脏卡
- 脏卡:O 被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡
- 在引用变更时通过 post-write barried + dirty card queue
- concurrent refinement threads 更新 Remembered Set
Remark
重新标记阶段
在垃圾回收时,收集器处理对象的过程中
黑色:已被处理,需要保留的
灰色:正在处理中的
白色:还未处理的
但是在并发标记过程中,有可能 A 被处理了以后未引用 C ,但该处理过程还未结束,在处理过程结束之前 A 引用了 C ,这时就会用到 remark 。
过程如下
- 之前 C 未被引用,这时 A 引用了 C ,就会给 C 加一个写屏障,写屏障的指令会被执行,将 C 放入一个队列当中,并将 C 变为 处理中状态
- 在并发标记阶段结束以后,重新标记阶段会 STW ,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它,由灰色变成黑色。
JDK 8u20 字符串去重
过程
- 将所有新分配的字符串(底层是 char[] )放入一个队列
- 当新生代回收时,G1 并发检查是否有重复的字符串
- 如果字符串的值一样,就让他们引用同一个字符串对象
- 注意,其与 String.intern() 的区别
- String.intern() 关注的是字符串对象
- 字符串去重关注的是 char[]
- 在 JVM 内部,使用了不同的字符串标
优点与缺点
- 节省了大量内存
- 新生代回收时间略微增加,导致略微多占用 CPU
-XX:+UseStringDeduplication
JDK 8u40 并发标记类卸载
在并发标记阶段结束以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类
JDK 8u60 回收巨型对象
- 一个对象大于region的一半时,就称为巨型对象
- G1不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉
JDK 9 并发标记起始时间的调整
- 并发标记必须在堆空间占满前完成,否则退化为 FulGC
- JDK 9 之前需要使用
-XX:InitiatingHeapOccupancyPercent
- JDK 9 可以动态调整
-XX:InitiatingHeapOccupancyPercent
用来设置初始值- 进行数据采样并动态调整
- 总会添加一个安全的空挡空间
垃圾回收调优
查看虚拟机参数命令
D:\JavaJDK1.8\bin\java -XX:+PrintFlagsFinal -version | findstr "GC"
可以根据参数去查询具体的信息
调优领域
- 内存
- 锁竞争
- cpu 占用
- io
- gc
确定目标
低延迟/高吞吐量? 选择合适的GC
- CMS G1 ZGC
- ParallelGC
- Zing
最快的 GC是不发生gc
首先排除减少因为自身编写的代码而引发的内存问题
- 查看 Full GC 前后的内存占用,考虑以下几个问题
- 数据是不是太多?
- resultSet = statement.executeQuery(“select * from 大表 limit n”)
- 数据表示是否太臃肿
- 对象图
- 对象大小 16 Integer 24 int 4
- 是否存在内存泄漏
- static Map map …
- 软
- 弱
- 第三方缓存实现
- 数据是不是太多?
新生代调优
- 新生代的特点
- 所有的 new 操作分配内存都是非常廉价的
- TLAB thread-lcoal allocation buffer
- 死亡对象回收零代价
- 大部分对象用过即死(朝生夕死)
- Minor GC 所用时间远小于 Full GC
- 所有的 new 操作分配内存都是非常廉价的
- 新生代内存越大越好么?
- 不是
- 新生代内存太小:频繁触发 Minor GC ,会 STW ,会使得吞吐量下降
- 新生代内存太大:老年代内存占比有所降低,会更频繁地触发 Full GC。而且触发 Minor GC 时,清理新生代所花费的时间会更长
- 新生代内存设置为内容纳[并发量*(请求-响应)]的数据为宜
- 不是
- 幸存区需要能够保存 当前活跃对象+需要晋升的对象
- 晋升阈值配置得当,让长时间存活的对象尽快晋升
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistrubution
老年代调优
以 CMS 为例:
- CMS 的老年代内存越大越好
- 先尝试不做调优,如果没有 Full GC 那么已经,否者先尝试调优新生代。
- 观察发现 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
-XX:CMSInitiatingOccupancyFraction=percent
类加载与字节码技术
类文件结构
执行一个简单的 HelloWorld.java
// HelloWorld 示例
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
执行javac -parameters -d . HellowWorld.java
编译为HelloWorld.class
得到的字节码
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
0001120 00 00 02 00 14
根据JVM规范,类文件结构如下
u4 magic
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
魔数
0-3字节,表示它是否是[class]类型文件。(ca fe ba be)
版本
4-7字节,表示类的版本 (00 00 00 34) 前三个是小版本,最后一个是大版本16进制
常量池
8 -9字节,表示常量池长度,00 23 (十进制下35) 表示常量池有#1-#34项,注意#0项不计入,也没有值。
第#1项 0a 表示第一个Method 信息, 00 06 和 00 15 (21)表示它引用了常量池#6和#21项来获得这个方法的所属类和方法名
第#2项09表示一个Field信息, 00 16 (22) 和 00 17 (23) 表示他引用了常量池#22 和 #23 项来获得这个成员变量的所属类和方法名
….
字节码指令
javap工具
Oracle 提供了 javap 工具来反编译 class 文件
javap -v 类名.class
图解方法执行流程
准备
原始java代码
/**
* 演示 字节码指令 和 操作数栈、常量池的关系
*/
public class Demo3_1 {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
编译后的字节码文件
[root@localhost ~]# javap -v Demo3_1.class
Classfile /root/Demo3_1.class
Last modified Jul 7, 2019; size 665 bytes
MD5 checksum a2c29a22421e218d4924d31e6990cfc5
Compiled from "Demo3_1.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#26 // java/lang/Object."<init>":()V
#2 = Class #27 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #28.#29 //
java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #30.#31 // java/io/PrintStream.println:(I)V
#6 = Class #32 // cn/itcast/jvm/t3/bytecode/Demo3_1
#7 = Class #33 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcn/itcast/jvm/t3/bytecode/Demo3_1;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
#22 = Utf8 c
#23 = Utf8 MethodParameters
#24 = Utf8 SourceFile
#25 = Utf8 Demo3_1.java
#26 = NameAndType #8:#9 // "<init>":()V
#27 = Utf8 java/lang/Short
#28 = Class #34 // java/lang/System
#29 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#30 = Class #37 // java/io/PrintStream
#31 = NameAndType #38:#39 // println:(I)V
#32 = Utf8 cn/itcast/jvm/t3/bytecode/Demo3_1
#33 = Utf8 java/lang/Object
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 java/io/PrintStream
#38 = Utf8 println
#39 = Utf8 (I)V
{
public cn.itcast.jvm.t3.bytecode.Demo3_1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t3/bytecode/Demo3_1;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field
java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method
java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 8: 0
line 9: 3
line 12: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
MethodParameters:
Name Flags
args
}
常量池载入运行时常量池
方法字节码载入方法区
main线程开始运行,分配栈帧内存
执行引擎开始执行字节码
bipush 10:
- 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
- sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
- ldc 将一个 int 压入操作数栈
- ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
- 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
istore 1:
- 将操作数栈栈顶元素弹出,放入局部变量表的slot 1中
ldc #3:
- 从常量池加载 #3 数据到操作数栈
- 注意
Short.MAX_VALUE
是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的
iload1:
将局部变量表中1号位置的元素放入操作数栈中
…
…
return
- 完成main方法调用,弹出main栈帧
- 程序结束
多态的原理
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用invokevirtual指令
在执行invokevirtual指令时,经历了以下几个步骤
- 先通过栈帧中对象的引用找到对象
- 分析对象头,找到对象实际的Class
- Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
- 查询vtable找到方法的具体地址
- 执行方法的字节码
异常处理
(1)try-catch
public class Main {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
}
}
}
对应的字节码文件(为了抓住重点,下面的字节码省略了不重要的部分):
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
Exception table:
from to target type
2 5 8 Class java/lang/Exception
- 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测2~4行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
- 8行的字节码指令 astore_2 是将异常对象引用存入局部变量表的2号位置(为e)
(2)多个 single-catch 块的情况
public class Main {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (ArithmeticException e) {
i = 20;
} catch (Exception e) {
i = 30;
}
}
}
对应的字节码文件:
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 19
8: astore_2
9: bipush 20
11: istore_1
12: goto 19
15: astore_2
16: bipush 30
18: istore_1
19: return
Exception table:
from to target type
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/Exception
因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用
(3)finally
public class Main {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
对应的字节码文件:
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1
//try块
2: bipush 10
4: istore_1
5: bipush 30
//try块执行完后,会执行finally
7: istore_1
8: goto 27
//catch块
11: astore_2
12: bipush 20
14: istore_1
//catch块执行完,会执行finally
15: bipush 30
17: istore_1
18: goto 27
//出现异常,但未被Exception捕获,会抛出其他异常,这时也需要执行finally块中的代码
21: astore_3
22: bipush 30
24: istore_1
25: aload_3
26: athrow //抛出异常
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any //剩余的异常类型,比如 Error
11 15 21 any //剩余的异常类型,比如 Erro
被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
注意:
虽然从字节码指令看来,每个块中都有finally块,但是finally块中的代码只会被执行一次
(4)finally中的return
public class Main {
public static void main(String[] args) {
int result = test();
System.out.println(result);//20
}
public static int test() {
try {
return 10;
} finally {
return 20;
}
}
对应的字节码文件:
Code:
stack=1, locals=2, args_size=0
0: bipush 10
2: istore_0
3: bipush 20
5: ireturn // 返回栈顶 int(20)
6: astore_1
7: bipush 20
9: ireturn // 返回栈顶 int(20)
Exception table:
from to target type
0 3 6 any
由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准
跟前一个中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常
运行下面的代码,不会抛出异常:
public class Main {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
try {
int i = 1 / 0;
return 10;
} finally {
return 20;
}
}
}
(5)finally不带return
public class Main {
public static void main(String[] args) {
int i = Main.test();
System.out.println(i);//输出为10
}
public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
}
对应的字节码文件:
Code:
stack=1, locals=3, args_size=0
0: bipush 10
2: istore_0 //赋值给i 10
3: iload_0 //加载到操作数栈顶
4: istore_1 //加载到局部变量表的1号位置
5: bipush 20
7: istore_0 //赋值给i 20
8: iload_1 //加载局部变量表1号位置的数10到操作数栈
9: ireturn //返回操作数栈顶元素 10
10: astore_2
11: bipush 20
13: istore_0
14: aload_2 //加载异常
15: athrow //抛出异常
Exception table:
from to target type
3 5 10 any
synchronized
public class Main {
public static void main(String[] args) {
int i = 10;
Object lock = new Object();
synchronized (lock) {
System.out.println(i);
}
}
}
对应的字节码文件:
Code:
stack=2, locals=5, args_size=1
0: bipush 10
2: istore_1
3: new #2 // class java/lang/Object
6: dup
7: invokespecial #1 // Method java/lang/Object."<init>":()V
10: astore_2
11: aload_2
12: dup
13: astore_3
14: monitorenter //加锁
15: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_1
19: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
22: aload_3
23: monitorexit //解锁
24: goto 34
//异常操作
27: astore 4
29: aload_3
30: monitorexit //解锁
31: aload 4
33: athrow
34: return
Exception table:
from to target type
15 24 27 any
27 31 27 any
编译期处理
所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃嘛)
默认构造器
public class Candy1 {
}
编译成class后的代码:
public class Candy1 {
// 这个无参构造是编译器帮助我们加上的
public Candy1() {
super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."<init>":()V
}
}
自动拆装箱
这个特性是 JDK 5 开始加入的, 如下代码 :
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
泛型集合取值
泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:
public class Candy3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10); // 实际调用的是 List.add(Object e)
Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
}
}
所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作:
// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);
如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:
// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();
可变参数
可变参数也是 JDK 5 开始加入的新特性: 例如:
public class Candy4 {
public static void foo(String... args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo("hello", "world");
}
}
可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。 同样java 编译器会在编译期间将上述代码变换为:
public class Candy4 {
public static void foo(String[] args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo(new String[]{"hello", "world"});
}
}
注意: 如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个空的数组,而不会 传递 null 进去
foreach循环
仍是 JDK 5 开始引入的语法糖,数组的循环:
public class Candy5_1 {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦
for (int e : array) {
System.out.println(e);
}
}
}
会被编译器转换为:
public class Candy5_1 {
public Candy5_1() {
}
public static void main(String[] args) {
int[] array = new int[]{1, 2, 3, 4, 5};
for(int i = 0; i < array.length; ++i) {
int e = array[i];
System.out.println(e);
}
}
}
如果是集合呢?
public class Candy5_2 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5);
for (Integer i : list) {
System.out.println(i);
}
}
}
实际被编译器转换为对迭代器的调用:
public class Candy5_2 {
public Candy5_2() {
}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Iterator iter = list.iterator();
while(iter.hasNext()) {
Integer e = (Integer)iter.next();
System.out.println(e);
}
}
}
注意 :foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其 中 Iterable 用来获取集合的迭代器( Iterator )
switch 字符串
从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:
public class Candy6_1 {
public static void choose(String str) {
switch (str) {
case "hello": {
System.out.println("h");
break;
}
case "world": {
System.out.println("w");
break;
}
}
}
}
注意: switch 配合 String 和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清楚
会被编译器转换为:
public class Candy6_1 {
public Candy6_1() {
}
public static void choose(String str) {
byte x = -1;
switch(str.hashCode()) {
case 99162322: // hello 的 hashCode
if (str.equals("hello")) {
x = 0;
}
break;
case 113318802: // world 的 hashCode
if (str.equals("world")) {
x = 1;
}
}
switch(x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
}
以看到,执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较。
问:为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢?hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突。
例如 BM 和 C. 这两个字符串的hashCode值都是 2123 ,如果有如下代码:
public class Candy6_1 {
public static void choose(String str) {
switch (str) {
case "BM": {
System.out.println("h");
break;
}
case "C.": {
System.out.println("w");
break;
}
}
}
}
会被编译器转换为:
public class Candy6_1 {
public Candy6_1() {
}
public static void choose(String var0) {
byte var2 = -1;
switch(var0.hashCode()) {
case 2123:
if (var0.equals("C.")) {
var2 = 1;
} else if (var0.equals("BM")) {
var2 = 0;
}
default:
switch(var2) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
}
}
类加载阶段
加载
- 将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
- _java_mirror 即 java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用
- _super 即父类
- _fields 即成员变量
- _methods 即方法
- _constants 即常量池
- _class_loader 即类加载器
- _vtable 虚方法表
- _itable 接口方法
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
- instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在堆中
- instanceKlass和 java_mirror(java镜像类)互相保存了对方的地址
- 类的对象在对象头中保存了 *.class 的地址。让对象可以通过其找到方法区中的instanceKlass,从而获取类的各种信息
链接
(1)验证
验证类是否符合 JVM规范,安全性检查
(2)准备
为 static 变量分配空间,设置默认值
- static变量在JDK 7以前是存储与instanceKlass末尾。但在JDK 7以后就存储在_java_mirror末尾了
- static变量在分配空间和赋值是在两个阶段完成的。分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
解析
将常量池中的符号引用解析为直接引用
初始化
(1)<cinit>()V
方法
初始化即调用 ,虚拟机会保证这个类的【构造方法】的线程安全
(2)发生的时机
类的初始化的懒惰的,以下情况会初始化:
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
以下情况不会初始化:
- 访问类的 static final 静态常量(基本类型和字符串)
- 类对象.class 不会触发初始化
- 创建该类对象的数组
- 类加载器的.loadClass方法
- Class.forName的参数2为false时
加载器
以 JDK 8 为例:
名称 | 加载的类 | 说明 |
---|---|---|
Bootstrap | ClassLoader(启动类加载器) | JAVA_HOME/jre/lib 无法直接访问 |
Extension ClassLoader(拓展类加载器) | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,显示为null |
Application ClassLoader(应用程序类加载器) | classpath | 上级为Extension |
自定义类加载器 | 自定义 | 上级为Application |
启动类加载器
用 Bootstrap 类加载器加载类:
package cn.itcast.jvm.t3.load;
public class F {
static {
System.out.println("bootstrap F init");
}
}
执行
package cn.itcast.jvm.t3.load;
public class Load5_1 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
System.out.println(aClass.getClassLoader());
}
}
输出
E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:.
cn.itcast.jvm.t3.load.Load5
bootstrap F init
null
- -Xbootclasspath 表示设置 bootclasspath
- 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
- 可以用这个办法替换核心类
- java -Xbootclasspath:
- java -Xbootclasspath/a:<追加路径>(后追加)
- java -Xbootclasspath/p:<追加路径>(前追加)
- java -Xbootclasspath:
扩展类加载器
如果classpath和JAVA_HOME/jre/lib/ext 下有同名类,加载时会使用拓展类加载器加载。当应用程序类加载器发现拓展类加载器已将该同名类加载过了,则不会再次加载
双亲委派模式
- 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
- 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则
自定义类加载器
使用场景
想加载非 classpath 随意路径中的类文件
通过接口来使用实现,希望解耦时,常用在框架设计
这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤
继承ClassLoader父类
要遵从双亲委派机制,重写 findClass 方法
不是重写loadClass方法,否则不会走双亲委派机制
读取类文件的字节码
调用父类的 defineClass 方法来加载类
使用者调用该类加载器的 loadClass 方法
运行期优化
即时编译
分层编译
JVM 将执行状态分成了 5 个层次:
0层:解释执行,用解释器将字节码翻译为机器码
1层:使用 C1 即时编译器编译执行(不带 profiling)
2层:使用 C1 即时编译器编译执行(带基本的profiling)
3层:使用 C1 即时编译器编译执行(带完全的profiling)
4层:使用 C2 即时编译器编译执行
profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等
即时编译器(JIT)与解释器的区别:
解释器
将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
是将字节码解释为针对所有平台都通用的机器码
即时编译器
将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
根据平台类型,生成平台特定的机器码
对于大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由 来),并优化这些热点代码
逃逸分析:
发现新建的对象是否逃逸。可以使用 -XX:- DoEscapeAnalysis 关闭逃逸分析
内存模型JMM
JMM 即 Java Memory Model,它定义了主存(共享内存)、工作内存(线程私有)抽象概念
原子性 - 保证指令不会受到线程上下文切换的影响
可见性 - 保证指令不会受 cpu 缓存的影响
有序性 - 保证指令不会受 cpu 指令并行优化的影响
volatile与synchronized 区别:
- volatile(易变的)保证可见性、有序性,不保证原子性!
- 可见性:每次读取变量值都是去主存中读取,保证的是在多个线程之间,一个线程对volatile 变量的修改对另一个线程可见。
- 有序性:禁用指令重排,禁止的是加 volatile 关键字变量之前的代码重排序。
- synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低。
volatile 原理
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
对 volatile 变量的写指令后会加入写屏障
对 volatile 变量的读指令前会加入读屏障
CAS(CompareAndSwap)与原子类
1、CAS流程
1)首先从地址 V 读取旧的预期值 A;
2)根据 A 计算目标值 B;
3)通过 CAS 以原子的方式将地址 V 中的值从 A 修改为 B。
特点:
更新一个变量的时候,只有当旧的预期值A和内存地址V当前实际值相同时,才会将内存地址V对应的值修改为B。(整个比较并替换的操作是一个原子操作)
一般与volatile一起使用,使得可以从主存中获取变量的最新值。
CAS的缺点:
并发量高时,CPU开销过大
CAS机制只能保证一个变量的原子性
ABA问题
CAS(乐观锁)—synchronized(悲观锁)
2、原子类
原子操作类,指的是以Atomic开头的包装类。如AtomicBoolean,AtomicUInteger,AtomicLong。而Atomic操作类的底层正是用到了CAS机制”。
synchronized 锁优化
- 轻量级锁(假设多个线程错开访问,用课本占座)–> 用MarkWord记录加锁(00),无锁(01)
- 锁膨胀:若有其他线程访问同步块,轻量级锁膨胀为重量级锁(10)。
- 重量级锁:monitorEnter、monitorExit。重量级锁竞争时使用自旋来优化(等红灯不熄火)。
- 偏向锁:第一次CAS将线程ID存入Mark Word头,之后若访问线程为自己ID就不用再CAS。
加锁优化方面:
- 同步代码块尽量短
- 减少锁的粒度(拆分锁)
- 锁粗化
- 锁消除(逃逸分析)
- 读写分离(CopyOnWriteArrayList)
评论区
欢迎你留下宝贵的意见,昵称输入QQ号会显示QQ头像哦~