Java虚拟机
2018-03-06 14:59:08 527 举报
AI智能生成
本思维导图是我在读《深入理解Java虚拟机》时整理的,几乎涵盖书中所有重要的知识点,全面清晰明了,对于想学习虚拟机的同学有很大的帮助
作者其他创作
大纲/内容
Java虚拟机
自动内存管理机制
运行时数据区域
程序计数器
线程私有
当前线程所执行的字节码的行号指示器
Java虚拟机栈
每个方法在执行的同时都会创建一个栈帧用于存储变量表,操作数栈,动态链接,方法出口等信息
本地方法栈
本地方法栈为虚拟机使用到的Native方法服务
Java堆
线程共享
存放对象实例
再细致一点的:eden、from survivor、to survivor,从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB)
通过-Xmx和-Xms控制
方法区
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
运行时常量池
用于存放编译器生成的各种字面量和符号引用,这部分内容将在加载后进入方法区的运行时常量池中存放
直接内存
直接内存并不是虚拟机运行时数据区的一部分,它根本就是本机内存而不是VM直接管理的区域
Java对象探秘
对象创建
虚拟机遇到一条new指令,首先确保类加载过程已经完成
内存分配方式
指针碰撞(Java堆内存规整)
所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅把指针向空闲空间那边挪动一段与对象大小相等的距离
空闲列表(Java堆内存不规整)
虚拟机维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
线程安全解决
对分配内存空间的动作进行同步处理
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
把内存分配的动作按照线程划分在不同的空间之中进行
每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,哪个线程要分配内存,就在哪个线程的TLAB上分配
虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定
<init>方法
执行new指令之后接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来
对象的内存布局
对象头
自身运行时数据(Mark Word)
哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
类型指针
对象指向它的类元数据的指针,虚拟机通过这个指针来确定是哪个类的实例
实例数据
对象真正存储的有效信息
HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops,从分配策略可以看出,相同宽度的字段总是被分配到一起
对齐填充
并不是必然存在的,也没有特别的含义,仅仅骑着占位符的作用
对象的访问定位
访问方式
使用句柄
Java堆中划出一块内存作为句柄池,reference中存储的就是对象的句柄地址
直接指针
reference中存储的直接就是对象地址
优点
使用句柄访问
对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改
直接指针访问
访问速度快,节省一次指针定位的的时间开销
OutOfMemoryError异常
Java堆溢出
参数:-XX:+HeapDumpOnOutOfMemoryError
虚拟机栈和本地栈溢出
栈容量由-Xss 参数设定
方法区和运行时常量池溢出
通过-XX:PermSize和-XX:MaxPermSize限制方法区大小
本机直接内存溢出
通过-XX:MaxDirectMemorySize指定
垃圾收集器与内存分配策略
概述
了解GC目的目的
当我们需要排查各种内存溢出,内存泄漏问题时,当垃圾收集称为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节
程序计数器,虚拟机栈,本地方法不需要考虑GC
对象已死?
判断算法
引用计数算法
给对象中添加一个引用计数器,每当一个地方引用它,计数器加1,当引用失效时,计数器值就减1
存在的问题
主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互引用的问题
可达性分析算法
通过一系列的“GC Roots”的对象作为起始点,从这些起始点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的
可作为GC Roots的对象
虚拟机栈中(栈帧中)引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象
引用分类
强引用
类似于“Object obj=new Object()”
垃圾收集器永远不会回收掉被强引用的对象
软引用
还有用但非必须的对象
在系统将要发生内存溢出异常之前,将会对这些对象列进回收范围之中进行第二次回收,如果这次没有足够的内存,才会抛出内存溢出异常
弱引用
用来描述非必须对象
被弱引用关联的对象只能生存到下一次垃圾收集发生之前
虚引用
也称为幽灵引用或者幻影引用,它是最弱的一种引用关系
目的就是能在这个对象被收集器回收时收到一个系统通知
生存还是死亡
要宣告一个对象死亡,至少要经历两次标记过程
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记
判断对象是否有必要执行finalize()方法
对象有没有覆盖finalize()方法
finalize()方法是否已经被虚拟机调用过
finalize()方法
任何一个对象的finalize()方法都只会被系统自动调用一次
不建议用finalize()方法,它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序
回收方法区
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类
无用的类
条件
该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
加载该类的ClassLoader已经被回收
该类对应的javalang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
参数控制
-Xnoclassgc
垃圾回收算法
标记-清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
不足
效率问题,标记和清除两个过程的效率都不高
空间问题,标记清除之后会产生大量不连续的内存碎片,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
复制算法
将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
对象存活率较高时就要进行较多的复制操作,效率将会降低
标记-整理算法
所有存活的对象移向一端,然后直接清理掉端边界以外的内存
分代收集算法
新生代
老年代
“标记清理”或者“标记整理”
HotSpot的算法实现
枚举根节点
当执行系统停顿下来后,并不需要一个不漏的检查完所在执行上下文和全局的引用位置,在HotSpot的实现中,使用一组称为OopMap的数据结构来存放对象引用
安全点
虚拟机在特定的位置记录下栈和寄存器中哪些位置时引用,这些位置称为安全点
选择标准
是否具有让程序长时间执行的特征
中断方式
抢占式中断
GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑在安全点上
主动式中断
设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起
安全区域
引入原因
程序不执行时,也即没有分配CPU时间,JVM显然也不可能等待线程重新分配CPU时间,这种情况就需要安全区域来解决
垃圾收集器
Serial收集器
特点
针对新生代
采用复制算法
单线程收集
进行垃圾收集时,必须暂停所有工作线程,直到完成
应用场景
是HotSpot在Client模式下默认的新生代收集器
简单高效(与其他收集器的单线程相比)
对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
参数设置
\"-XX:+UseSerialGC\":添加该参数来显式的使用串行垃圾收集器;
ParNew收集器
除了多线程外,其余的行为、特点和Serial收集器一样
Server模式下,ParNew收集器是一个非常重要的收集器
单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销
\"-XX:+UseConcMarkSweepGC\":指定使用CMS后,会默认使用ParNew作为新生代收集器;
\"-XX:+UseParNewGC\":强制指定使用ParNew;
\"-XX:ParallelGCThreads\":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
Parallel Scavenge收集器
新生代收集器
多线程收集
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间; 而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput)
高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间
当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互
\"-XX:MaxGCPauseMillis\"控制最大垃圾收集停顿时间
\"-XX:GCTimeRatio\" 设置垃圾收集时间占总时间的比率
\"-XX:+UseAdptiveSizePolicy\"
Serial Olc收集器
针对老年代
采用\"标记-整理\"算法(还有压缩,Mark-Sweep-Compact)
主要用于Client模式
在Server模式中
在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配)
作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
Parallel Old收集器
采用\"标记-整理\"算法
JDK1.6及之后用来代替老年代的Serial Old收集器
特别是在Server模式,多CPU的情况下
在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的\"给力\"应用组合
\"-XX:+UseParallelOldGC\":指定使用Parallel Old收集器
CMS收集器
基于\"标记-清除\"算法(不进行压缩操作,产生内存碎片)
以获取最短回收停顿时间为目标
并发收集、低停顿
需要更多的内存(看后面的缺点)
与用户交互较多的场景
希望系统停顿时间最短,注重服务的响应速度
以给用户带来较好的体验
如常见WEB、B/S系统的服务器上的应用
\"-XX:+UseConcMarkSweepGC\":指定使用CMS收集器
运行过程
初始标记
并发标记
重新标记
并发清除
缺点
对CPU资源非常敏感
产生大量内存碎片
G1收集器
并行与并发
分代收集,收集范围包括新生代和老年代
结合多种垃圾收集算法,空间整合,不产生碎片
可预测的停顿:低停顿的同时实现高吞吐量
面向服务端应用,针对具有大内存、多处理器的机器
最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案
\"-XX:+UseG1GC\":指定使用G1收集器
\"-XX:InitiatingHeapOccupancyPercent\":当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45
\"-XX:MaxGCPauseMillis\":为G1设置暂停时间目标,默认值为200毫秒
\"-XX:G1HeapRegionSize\":设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region
内存分配和回收策略
对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
大对象直接进入老年代
所谓大对象,就是需要大量连续内存空间的对象,最典型的大对象就是那种很长的字符串以及数组
长期存活的对象将进入老年代
动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以进入老年代,无须等到MaxTenuringThreshold中要求的年龄
空间分配担保
虚拟机性能监控与故障处理工具
JDK的命令行工具
jps
虚拟机进程状况工具
命令格式
jps [option][hostid]
主要选项
-q 只输出LVMID,省略主类的名称
-m 输出虚拟机进程启动时传递给主类main()函数的参数
-l 输出主类的全名,如果进程执行的是jar包,输出jar路径
-v 输出虚拟机进程启动时JVM参数
jstat
虚拟机统计信息监视工具
jstat [option vmid [interval[s|ms] [count]]]
-class 监视类装载,卸载数量,总空间以及类装载所消耗的时间
-gc 监视Java堆状况,包括Eden区、两个survivor区,老年代,永久代等的容量、已用空间,GC时间合计等信息
-gccapacity 监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间
-gcutil 监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
-gccause 与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因
-gcnew 监视新生代GC状况
-gcnewcapacity 监视内容与-gcnew基本相同,输出主要关注使用到的最大,最小空间
-gcold 监视老年代GC状况
-gcoldcapacity 监视内容与-gcold基本相同,输出主要关注使用的最大,最小空间
-gcpercapacity 输出永久代使用到的最大、最小空间
-compiler 输出JIT编译器编译过的方法、耗时等信息
-printcompilation 输出已经被JIT编译的方法
jinfo
Java配置信息工具
jinfo [option] pid
jmap
Java内存映像工具
jmap [option] vmid
-dump 生成Java堆转储快照
-finalizerinfo
显示在F-Queue中等待Finalizer线程执行finalize方法的对象
-heap 显示Java堆详细信息,如使用哪种回收器,参数设置,分代情况等
-histo 显示堆中对象统计信息,包括类、实例数量、合计容量
-permstat 以ClassLoader为统计口径显示永久在内存状况
-F 当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照
jhat
虚拟机堆转储快照分析工具
jstack
Java堆栈跟踪工具
HSDIS
JIT生成代码反汇编
类文件结构
概念
Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class之中,中间没有添加任何分隔符
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表
无符号数
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数
无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
表
表是由多个无符号数或者其他表作为数据项构成的复合数据类型
所有表都习惯的以_info结尾,表用于描述有层次关系的复合结构的数据,整个class本质上就是一张表
魔数
每个Class文件的头4个字节称为魔数,它的唯一作用是确定整个文件是否为一个能被虚拟机接受的Class文件
Class文件版本
紧接着魔数的四个字节存储的是Class文件的版本号:第5个和第6个字节是次版本号,第7个和第8个字节是主版本号
常量池
紧接着主次版本号之后的常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型
Class文件结构中只有常量池的容量计数是从1开始的,对于其他集合类型,包括接口索引集合,字段集合,方法表集合等的容量计数都是从0开始的
常量池主要存放两大类常量:字面量和符号引用
访问标志
在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类和接口层次的访问信息
包括:这个Class是类还是接口;是否定义为pulic类型,是否定义为abstract类型;如果是类的话,是否被声明为final等
类索引、父类索引与接口索引集合
类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合
类索引用于确定整个类的全限定名
父类索引用于确定这个类的父类的全限定名
接口索引集合就用来描述这个类实现了哪些接口
字段表集合
字段表用来描述接口或者类中声明的变量
方法表集合
方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项
属性表集合
在Class文件、字段表、方法表都可以携带自己的属性表集合以用来描述某些场景专有的信息
字节码与数据类型
在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息
虚拟机指令
加载和存储指令
运算指令
类型转换指令
对象创建于访问指令
操作数栈管理指令
控制转移指令
方法调用和返回指令
异常处理指令
同步指令
共有设计和私有设计
虚拟机类加载机制
类加载的时机
声明周期
加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中验证、准备、解析3个部分统称为连接
以下情况对类进行初始化
遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化
使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发父类的初始化
当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类
当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果BEF_getStatic、BEF_putStatic、BEF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
类加载的过程
加载
完成3件事
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
类的来源
从ZIP包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础
从网络中获取,这种场景最典型的应用就是applet
运行时计算生成,这种场景使用得最多的就是动态代理基础
由其它文件生成,典型场景就是JSP应用,即由JSP文件生成赌赢的Class类
从数据库中读取,这种场景相对的少些,例如有些中间件服务器可以选择把程序安装到数据库中来完成程序代码在集群间的分发
验证
验证是连接阶段的第一步,这一阶段的目的就是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
验证项
文件格式验证
元数据验证
字节码验证
符号引用验证
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都讲在方法区汇总进行分配
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
解析动作分类
类或接口的解析
字段解析
类方法解析
接口方法解析
初始化
类初始化是类加载过程中的最后一步,这时才真正开始执行类中定义的Java程序代码
类加载器
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为类加载器
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要它们的类加载器不同,那这两个类就必定不相等
双亲委派模型
分类
启动类加载器
这个类加载器使用C++语言实现,是虚拟机自身的一部分
其他类加载器
这些类加载器由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader
虚拟机字节码执行引擎
执行引擎是Java虚拟机中最核心的组成部分之一,“虚拟机”是一个相对于“物理机”的概念
所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果
解释执行
编译执行
运行时栈帧结构
栈帧存储了方法的局部变量表,操作数栈,动态链接和方法返回地址等信息
方法调用
方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本
所有方法调用中的目标方法在Class文件里面都是一个常量池的符号引用,在类加载的解析过程中,会将一部分符号引用转化为直接引用
方法分类
虚方法
除了非虚方法,其它方法都为虚方法
非虚方法
静态方法,私有方法,实例构造器,父类方法4类,他们在类加载的时候会把符号引用解析为该方法的直接引用,这些方法称为非虚方法
分派
动态分派
在运行期根据实际类型确定方法执行版本的分派过程称为动态分派
静态分派
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用就是方法重载
早期(编译期)优化
因为Javac这类编译器对代码的运行效率几乎没有任何优化措施,虚拟机设计团队把对性能的优化集中到了后端的即时编译器中,这样可以让那些不是由Javac产生的文件也同样能享受到编译器优化所带来的好处
编译过程
解析与填充符号表过程
词法,语法分析
填充符号表
插入式注解处理器的注解处理过程
在JDK1.6中实现了JSR-269规范,提供了一组插入式注解处理器的标准API在编译期间对注解进行处理,我们可以把它看做是一组编译器的插件,在这些插件里面,可以读取,修改,添加抽象语法树中任意元素
分析与字节码生成过程
标注检查
检查变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配
数据及控制流分析
对程序上下文逻辑更近一步的验证,可以检查出诸如程序局部变量使用前是否有赋值,方法的每条路径是否都有返回值,是否所有的受检异常都被正确处理了等问题
解语法糖
Java中最常用的语法糖主要是泛型,变长参数,自动装箱/拆箱等,虚拟机运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖
字节码生成
字节码生成阶段不仅仅是把前面各个步骤所生成的信息转化成字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作
晚期(运行期)优化
即时编译器
当虚拟机发现某个方法或者代码块的运行特别频繁时,就会把这些代码认定为“热点代码”,为了提高热点代码的执行效率,在运行时,虚拟机就会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器
编译器
在程序运行后,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率
server Compiler
Client Compiler
热点代码
被多次调用的方法
被多次执行的循环体
解释器
当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行
当程序运行环境内存资源限制较大,可以使用解释执行节约内存
最有代表的优化技术
公共子表达式消除
数组边界检查消除
方法内联
逃逸分析
栈上分配
同步消除
标量替换
Java内存模型与线程
Java内存模型
主内存和工作内存
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存
线程的工作线程中保存了被线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量
不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成
主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域
内存间交互操作
lock(锁定): 作用于主内存的变量,它把一个变量标识为一条线程独占的状态
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量放入工作内存的变量副本中
use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
assign(赋值):作用于工作内存的变量,它把一个执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
volatile变量
特性
可见性:保证此变量对所有线程的可见性,“可见性”是指一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的
禁止指令重排序优化
long和double型变量
对于64位的数据类型,在模型中特别定义了一条相对宽松的规定,允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为32位的操作来进行,这就是所谓的long和double的非原子性协定
线程特性
原子性
Java内存模型提供了lock和unlock操作来满足原子性,并且提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块-synchronized关键字
可见性
就是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改
Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递 媒介的方式实现可见性
除了volatile之外,Java还有两个关键字能实现可见性,即synchronize和final
同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中”这条规则获得的
被final修饰的字段在构造中一旦被初始化完成,并且构造器没有把“this”的引用传递出去
有序性
如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是有序的
保证有序性
volatile关键字本身就包含指令重排序的语义
synchronize则由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则获得的
先行发生原则
这是判断数据是否存在竞争,线程是否安全的主要依据
依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作之间是否可能存在冲突的所有问题
它是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在操作B之前,操作A产生的影响能被操作B观察到,“影响”包括了修改了内存中共享变量的值、发送了消息、调用了方法等
“天然的”先行发生关系
程序次序规则
管程锁定规则
volatile变量规则
线程启动规则
线程终止规则
线程中断规则
对象终结规则
传递性
Java与线程
线程的实现
使用内核线程实现
使用用户线程实现
使用用户线程加轻量级进程混合实现
Java线程调度
协同式线程调度
线程的执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另一个线程上
抢占式线程调度
每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定
状态转换
新建
运行
等待
阻塞
结束
线程安全与锁优化
线程安全
Java语言中各种操作共享的数据
不可变
绝对线程安全
相对线程安全
线程兼容
线程对立
线程安全的实现方法
互斥同步
同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用,而互斥是实现同步的一种手段
实现互斥的手段
临界区、互斥量和信号量都是主要的互斥实现方式
非阻塞同步
先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就采用其他的补偿措施,这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步
无同步方案
可重入代码
线程本地存储
锁优化
适应性自旋
锁消除
锁粗化
轻量级锁
偏向锁
0 条评论
回复 删除
下一页