jvm学习笔记
jvm学习笔记
一、编译和解释
编译器:是把源程序的每一条语句都编译成机器语言,并保存成二进制文件,这样运行时计算机可以直接以机器语言来运行此程序,速度很快;
解释器:则是只在执行程序时,才一条一条的解释成机器语言给计算机来执行,所以运行速度是不如编译后的程序运行的快的.
Java:先编译为字节码文件(类文件)(Class文件),再在java虚拟机上解释运行,所以有跨平台性,不管是什么语言编译生成的Class文件,都可以在java虚拟机上解释运行
二、jvm内存管理
Java不像c/c++语言,程序员可以对程序对象内存进行管理,Java通过将权力交给Java虚拟机来让程序员脱离这种痛苦,但是也会带来一个问题,即当发生内存溢出或者泄露时,需要理解jvm内存的管理方式,才能排查错误
内存结构图:
。。。。。。图暂无
运行时数据区
程序计数器:
-
线程私有
-
因为cup实现程序并发执行采用的是时间片轮转的方式来实现,所以,每次cpu切换到一个新的线程的时候,需要判断上一次程序的执行位置,程序计数器解释解决了这个问题,也叫行号计数器
-
当运行的是Java方法,程序计数器记录字节码指令的地址
-
当运行的是native方法,程序计数器记录的是空值
-
内存大小:一块较小的内存区域
-
异常:
- 此内存区域是唯一一个没有OutOfMemoryError的区域
Java虚拟机栈:
- 线程私有
- 与线程的生命周期相同,描述的是Java方法执行的内存模型,每个方法执行时都会创建一个栈帧,栈帧在虚拟机栈中的入栈出栈,就是一个方法的调用和执行完成
- 栈帧(大小在编译期间确定,运行期间不改变,因为编译生成的Class文件内部的方法表Code属性已经保存。):
- 局部变量表:
- 局部变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位。 一个Slot可以存放一个32位以内(boolean、byte、char、short、int、float、reference和returnAddress)的数据类型,reference类型表示一个对象实例的引用。对于64位的数据类型(long和double),虚拟机会以高位对齐的方式为其分配两个连续的Slot空间,是线程安全的。
- 操作数栈:
- 操作数栈(Operand Stack)是一个后入先出栈。当一个方法执行开始时,这个方法的操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。
- 动态连接:
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接,动态链接,就是指类加载过程中的准备阶段,不是有常量池中的符号引用转化为直接引用吗?这个操作叫静态连接,而在运行期间,将常量池中的符号引用转化为直接引用,就叫动态连接
- 方法返回地址:
- 不管是方法正常调用结束,还是方法异常调用结束,方法都要返回到调用地址
- 局部变量表:
- 内存大小:可以是固定大小,也可以扩展
- 异常:
- OutOfMemoryError异常:线程请求的栈深度超过允许的深度,抛出这个异常
- StackOverflowError异常:当虚拟机栈扩展时也申请不到足够内存,就抛出这个异常
本地方法栈:
- 线程私有
- 和虚拟机栈差不多,是不过这个区域时运行本地方法的
- 本地方法:
- 非java语言能实现的方法
- 内存大小:可以是固定大小,也可以扩展
- 异常:
- OutOfMemoryError异常:同虚拟机栈原理
- StackOverflowError异常:同虚拟机栈原理
java堆:
- 线程共享
- 所有的对象实例都在这里分配内存,但是也不绝对,逃逸分析,标量替换技术会导致例外
- 内存空间在逻辑上连续,物理上可以不连续
- 内存大小:可以是固定大小,也可以扩展
- 异常:
- OutOfMemoryError异常:堆中没有内存能够分配给实例,或者无法扩展
方法区:
- 线程共享
- 逻辑上是堆的一部分,但是要和堆区分开,他用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等
- hotspot虚拟机对这部分的内存管理采用了永久代来实现方法区
- intern()本地方法的解释:
- jdk1.6及之前,返回的是首次出现的字符串常量复制到永久代之后的引用
- jdk1.7之后,返回的是首次出现的字符串常量实例引用的引用
- 异常:
- OutOfMemoryError异常:无法满足内存分配
三、Java类文件(Class文件)结构
-
8字节大小的二进制流,中间没有任何分隔符号,采用两种数据结构来表示,即:无符号数、表。
- 无符号数:属于基本数据类型,用u1,u2,u4,u8表示1,2,4,8字节的无符号数
- 表:多个无符号数和其他表作数据项组成的复合数据类型,习惯以“_info”结尾,Class文件实际上也是一张表
- 在无符号数的前面放置一个计数器来表示此种无符号数的集合大小
-
Class文件结构图
-
。。。。。图暂无
-
魔数:u4 magic 1,表示4字节大小的魔数有1个,作用是确定文件是否是一个可以被虚拟机接受的Class文件
-
次版本号:u2 minor_version 1
-
主版本号:u2 major_version 1
-
常量池入口:u2 constant_pool_count 1
-
常量池:cp_info constant_pool constant_pool_count -1,表类型数据结构,从1开始计数,后面连续的constant_pool_count -1个字节中,每一个字节都代表一种常量类型的引用,且都有自己的表结构,存放编译器生成的字面量和符号引用
- 字面量:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
- 符号引用:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。
- 常量池在jvm加载Class文件到内存的时候变身成为,运行时常量池,最主要的运用便是String类的intern()方法
- 运行时常量池:它的字面量可以动态的添加,符号引用可以被解析为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。
- 字符串常量池:就是专门记录字符串的常量类型,是常量池的一部份区域,他有以下特征:
- +号运算的常量,jvm优化,直接放入字符串常量池中
- 事前声明的常量,放入常量池中
-
访问标志:u2 access_flags 1,2字节大小,有16个标志位,因为标志位不会随意改变,所以不用去常量池描述,只需要用0和1表示即可,用于标识一些在类和接口层次上的访问信息,如是否为public类型,是否是final的,是否是接口,是否是注解等
-
类索引:u2 this_class 1,确定类的全限定名
-
父类索引:u2 super_class 1,确定父类的全限定名
-
接口索引集合入口:u2 interfaces_count 1
-
接口索引集合:u2 interfaces interfaces_count ,确定接口的全限定名
- 拿着类索引、父类索引、接口索引的值先去常量池中按常量池的下标找到对应的常量类型,必都为字符串结构类型,然后进一步可在常量池中查到具体的字符串,即全限定名
-
字段表集合入口:u2 fields_count 1
-
字段表:field_info fields fields_count ,定义字段包括类变量(static修饰)和实例变量(无static修饰)
- 其中的修饰符使用标识位来表示(0和1),字段名和定义的数据类型使用常量池来描述
- 修饰符有:public,private,protected,static,final,volatile,transient
-
方法表集合入口:u2 methods_count 1
-
方法表集合:methods_info methods methods_count ,定义类中方法
- volatile,transient不能修饰方法
- 方法的修饰符有:public,private,protected,static,final,synchronized,native,abstract,strictfp
-
属性表集合入口:u2 attributes_count 1
-
属性表集合:attribute_info attributes attributes_count ,用于描述某些场景专有的信息
- 常见属性如下:
- Code,ConstantValue,Exceptions等
-
使用javap -verbose命令可以输出字节码内容
-
四、jvm中的对象
- 对象的创建
- 虚拟机遇到new关键字,先去常量池检查是否有一个类的符号引用,并检查该类是否被加载、解析、初始化,如果没有则执行类加载过程
- 类加载通过后,虚拟机为对象分配内存
- 如果堆内存是规整的,即一边是使用过的内存,一边是未使用过的内存空间,中间用一个指针作为分界点,这种分配方式叫“指针碰撞”
- 如果对内存不是规整的,空闲区和使用区相互交错,则需要维护一个表,记录可用内存块,这种分配方式叫“空闲列表”
- Java堆是否规整取决于垃圾收集器是否带有压缩功能
- Serial、parNew等带Compact过程的收集器时,采用指针碰撞分配
- CMS这种基于Mark-Sweep算法的收集器,通常采用空闲列表
- 由于分配内存的频繁性,并发情况下不是线程安全的,有两种解决方案:
- 对内存分配动作采取同步处理
- 为每个线程分配TLAB,即本地线程分配缓存,现在TLAB上分配,用完TLAB并分配新的时候再锁定
- 内存分配后,将内存空间初始化为零值
- 之后,在对对象头进行设置
- 此时,虚拟机层面的对象已经生成,程序层面还需要init方法为字段赋值
- 对象的内存布局
- 对象在内存中可分为三部分:对象头、实例数据、对齐填充
- 对象头:
- 运行时数据(Mark Word):拿32位操作系统未加锁的对象举例,有25bit哈希码、4bitGC分代年龄、2bit锁状态标志、1bit固定为零,线程持有的锁、偏向线程ID、偏向时间戳等
- 类型指针:指向类元数据的指针
- 如果是数组对象,还要有一块用于记录数组长度的数据
- 实例数据:
- 定义的各种字段的内容,相同宽度(如long和double)的字段被分配在一起,父类属性在子类属性之前
- 对齐填充:
- 对象大小必须为8字节整数倍,所以要填充
- 对象的访问定位
- 句柄访问
- 虚拟机栈中的栈帧中的reference,指向Java堆中的句柄池,句柄池中的句柄包含该reference引用所指向对象的实例数据地址和类型数据地址,实例数据在堆中,类型数据在方法区中
- 直接访问
- 虚拟机栈中的栈帧中的reference,指向Java堆中的实例数据,实例数据中又包含了指向方法区对象类型数据的指针
- 两者区别:
- 句柄访问,在对象移动时,只需要改变实例数据的地址,不需要改变reference
- 直接访问速度快,节省了一次指针定位的开销,hotspot采用直接访问
- 句柄访问
- OutOfMemoryError实战
- jdk/bin目录下有Java自带的可视化工具jconsole
- -Xmx 设置堆的最大值
- -Xms 设置堆的初始大小
- -XX : +HeapDumpOnOutOfMemoryError 可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆 转储快照
- -XX : MaxPermSize 设置方法区最大值
- -XX : PermSize 设置方法去的初始大小
- -XX : MaxDirectMemorySize 本机直接内存,默认为Java堆最大值
五、垃圾回收器
说起垃圾回收器,有以下几个问题讨论,为什么要垃圾回收?什么是垃圾?什么时候回收?怎么回收?
-
为什么要垃圾回收?
- 线程私有的内存区域在程序编译期间就是可知的,所以很少需要管理,但是方法区和堆空间不一样,内存的分配和回收都是动态的,所以需要一种机制来管理
-
什么是垃圾?
-
在堆中讨论:
- 对象死了就是垃圾,那怎么判断对象的死活呢?
-
不被引用了就是死了呗
3. 有下面几种方法判断对象生死: -
引用计数法
- 给对象一个引用计数器,有一个引用,计数器就加1,为0时候就代表对象死了
- 判定效率高,但是无法解决循环依赖(循环引用)问题
- 给对象一个引用计数器,有一个引用,计数器就加1,为0时候就代表对象死了
-
可达性分析
- 通过一系列的叫做GC ROOTS的对象为起始节点,判断任何一个对象是否与一个GC ROOTS关联,关联则活,不关联则死
- 可达性分析会导致stop the world发生,因为引用链不能不断变化,需要停顿所有java线程来查找引用,在查询的时候不需要遍历整个执行上下文,在类加载的时候,使用OopMap数据结构早已记录引用的位置,所以在线程到达安全点时,或抢占式中断,或主动式中断,而对于本来就已经阻塞的线程来说,是无法实现中断的,所以需要在任何地方都允许GC的安全区域来负责此种情况
- GC ROOTS对象有以下几种情况:
- 虚拟机栈中栈帧中本地变量表引用的对象
- 本地方法栈中引用的对象
- 方法区中常量引用的对象
- 方法区中类静态属性引用的对象
- 引用的分类:
- 引用也有高低贵贱,一些引用食之无味弃之可惜,内存充足时最好保留,不足时再杀死对象,所以对引用也进行了分类:
- 强引用:new出来的对象引用,引用在,对象在,不回收
- 软引用:内存足够,就不回收,内存不足,就回收
- 弱引用:内存足够,也回收
- 虚引用:特别弱的一种关系,连对象实例也获取不到,唯一作用就是在对象被回收时,收到一条系统通知
- 虚拟机栈中栈帧中本地变量表引用的对象
- 通过一系列的叫做GC ROOTS的对象为起始节点,判断任何一个对象是否与一个GC ROOTS关联,关联则活,不关联则死
-
在方法区讨论:
-
废弃常量和无用的类就是垃圾
2. 废弃常量:没有其他地方引用这个字面量(只是一种字面上的量,和值不是一个概念),也没有其他地方的String对象引用这个常量
3. 无用的类:- 堆中不存在该类的任何实例
- 该类的类加载器已经被回收
- 该类的Class对象任何地方没被引用,无法反射访问类中方法
- 需要同时满足上面三个条件才叫无用类
-
-
什么时候回收?
- 堆上的讨论:
- 不可达,或者计数器为0时回收,判死刑
- 但是也不是直接判死刑,会给缓刑,过程如下:
- 第一次发现不可达,先标记筛选,看有没有覆盖finalize方法,或者已经调用过finalize方法
- 调用过,直接删除对象,没调用过,对象进入F队列等待二次标记(F队列的执行时异步的,不会等待finalize方法的状态)
- 二次标记时,finalize方法中没有自救成功,则真正死亡
- 对象的finalize方法系统只会执行一次,也就是说只能自救一次
- 方法区的讨论:
- 和对象回收差不多,常量废弃时,类无用时
- 堆上的讨论:
-
怎么回收?
-
堆上的讨论:
-
垃圾收集算法:
- 标记清除算法:标记并清除回收对象
- 缺点:标记清除动作效率不高,产生内部碎片,大对象进入会提前触发垃圾回收
- 复制算法:内存空间分为相同的两部分,每次使用其中一部分,按顺序分配内存,内存使用完将剩余存活对象复制到另一部分,将此部分内存回收
- 缺点:空间缩小一半
- 优点:实现简单,运行高效
- 优化:分为Eden和Survivor和Survivor,8:1:1比例
- 标记整理算法:标记对象移动到一端,清除整理边界外的内存
- 分代收集算法:分为老年代和新生代,新生代有大量对象生存毁灭,故采用复制算法,老年代对象成活率高,采用标记清除算法或者标记整理算法进行回收
- 标记清除算法:标记并清除回收对象
-
垃圾收集器:
- 垃圾回收器就是实现垃圾回收的具体实现
- Serial收集器
- 运行在新生代,单线程收集器,采用复制算法,他在垃圾回收时必须暂停其他所有的工作线程
- ParNew收集器
- 运行在新生代,Serial的多线程版本,采用复制算法,目标是更少的停顿时间
- Parallel Scavenge收集器
- 运行在新生代,并行的多线程收集器,采用复制算法,目标是高吞吐量
- Serial Old收集器
- 运行在老年代,单线程收集器,采用标记整理算法
- Parallel Old收集器
- 运行在老年代,多线程收集器,采用标记整理算法
- CMS收集器
- 运行在老年代,是一种以获取最短回收停顿时间为目标的收集器,采用标记清除算法
- 运作过程:
-
- 初始标记:单线程,标记GC roots能直接关联的对象
- 并发标记:和用户线程并发,从初始标记的对象开始,找出所有的存活对象
- 重新标记:修正并发标记中用户线程新产生的引用
- 并发清除:清除没有标记的对象
- G1收集器
- 并行与并发,新生代老年代区分不明显,分代收集算法
- 步骤:初始标记、并发标记、最终标记和筛选回收
-
垃圾收集相关参数使用
。。。。。。以后再说
-
-
方法区上的讨论:
- 大量使用反射、动态代理、自定义类加载器的场景,使用类卸载功能
-
六、内存分配(serial/serialold收集器)
- 分为新生代和老年代
- 新生代又有8:1:1的eden区和survivor区和survivor区
- 对象优先在eden区分配,当空间不足时,新生代发生minor GC
- 如果minor GC也不能满足即将分配的大对象时,比较个-XX :PretenureSizeThreshold参数,大于这个参数,大对象直接在老年代分配
- 虚拟机会给每个出生在eden区的对象设置一个年龄计数器,经历一次minor GC并进入survivor区则年龄增加一岁,年龄生长到15岁晋升到老年代,也可以通过-XX :MaxTenuringThreshold改变晋升阈值
- 当在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
- 老年代有担保的作用,当minor GC发生survivor区不能复制容纳存活对象的时候,老年代来担保提供空间,决策思路有求新生代存活对象总和以及求历次晋升老年代对象的平均大小这两种方式
- 老年代担保失败,或者空间不足,会触发full GC
七、虚拟机性能监控与处理
- jps:虚拟机进程状态工具
- jps -m 运行时传入主类的参数;
- jps -v 虚拟机参数;
- jps -l 运行的主类全名 或者jar包名称;
- 也可以一块使用 jsp -mlv
- jstat:虚拟机统计信息监视工具
- 监视虚拟机运行时的状态信息,包括监视类装载、内存、垃圾回收、jit编译信息
- jinfo:Java配置信息工具
- jmap:Java内存映像工具
- jhat:虚拟机堆转储快照分析工具
- 屏幕显示“Server is ready.”的提示后,用户在浏览器中键入http://localhost:7000/就可以 看到分析结果
- jstack:Java堆栈跟踪工具
- JConsole是一种基于JMX的可视化监视、管理工具可进行内存管理、线程管理、查看死锁等。
八、字节码指令简介
。。。。。。
这部分知识对应Java代码到字节码指令的转换过程,学会了可以阅读Class文件
九、虚拟机的类加载机制
在Java中,类型的加载、连接和初始化过程都是在程序运行期间完成的,所以,java具有高度的灵活性,可以在java程序运行时从其他地方加载进来一段其他代码的二进制流作为程序代码的一部分
- 加载
- 将外部二进制流数据加载到内存中的方法区,作为程序访问的类型数据的外部接口
- 这一步用户可以自己决定方法,所以可以自定义类加载器
- 验证
- 确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 大致上会完成4个阶段的校验工作:文件格式、元数据、字节码、符号引用。
- 准备
- 为类变量赋零值,为常量赋值
- 解析
- 常量池中的符号引用替换为直接引用的过程
- 符号引用(Symbolic References): 符号引用以一组符号来描述所引用的目标,符号可以是符合约定的任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用(Direct References): 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用与虚拟机实现的内存布局相关,引用的目标必定已经在内存中存在。
- 初始化
- 静态变量和静态块的初始化
- 静态块种只能赋值,不能引用,否则编译报错
- 使用
- 卸载
其中,解析阶段不一定在初始化之前,也可能在初始化之后,因为,一下5种情况下会立即初始化:
- 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化
- new:使用new关键字实例化对象的时候
- getstatic和putstatic:读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
- invokestatic:调用一个类的静态方法的时候
- 对类进行反射调用时,如果类没有进行过初始化,则需要先触发其初始化
- 初始化子类时,父类也要初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个 类),虚拟机会先初始化这个主类。
- 当使用JDK 1.7的动态语言支持时
类加载器:
- 一个类是否唯一,取决于类本身和类加载器共同拥有的类名称空间
双亲委派模型:
-
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而都是使用组合的关系复用父类加载器的代码。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
好处:可以防止类重复加载,防止产生同样的但是本质不同的两个类
这么重要的功能,代码实现却十分简单
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { //1 首先检查类是否被加载 Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { //2 没有则调用父类加载器的loadClass()方法; c = parent.loadClass(name, false); } else { //3 若父类加载器为空,则默认使用启动类加载器作为父加载器; c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { //4 若父类加载失败,抛出ClassNotFoundException 异常后 c = findClass(name); } } if (resolve) { //5 再调用自己的findClass() 方法。 resolveClass(c); } return c; } -
破坏双亲委派模型
- 为什么要破坏?因为,基础类常常被用户类调用,但有时候,基础类也要去调用用户类(jndi、jdbc等),那这时候怎么办呢?根加载器是找不到用户代码的,所以就没办法加载,因为用户代码在用户的类路径下,java设计团队只能引入一种不优雅的设计,线程上下文类加载器!!!这个加载器的出现就是破坏了双亲委派机制
十、Java内存模型
-
java内存模型分为工作内存和主内存
工作内存:一般指方法内的区域,即虚拟机栈这种
主内存:一般指对象的实例数据部分,也就是堆中的那部分
执行引擎:我认为就是进行操作数栈这种位置,用来运算的
-
读取写出过程:
- 读取实例变量的值一般是这个过程:从主内存read,后load到工作内存,再从工作内存use到执行引擎
- 写出实例变量的值一般是这个过程:从执行引擎assign到工作内存,再从工作内存store,后write到主内存
- 此过程顺序不能发生改变
- 另外,还有lock和unlock,分别为加锁和解锁,加锁为主内存一条线程独占,解锁为线程共享
- 这些命令属于内存模型的底层协议命令,不是java命令,具有原子性
-
volatile命令
- 可以让实例变量的改变在线程之间立即可见,因为他对上面的读取写入过程规定了更严格的规则,可以确保读取前必刷新主内存内的值,修改后必写入到主内存中,且不会被重排序优化
-
重排序:
- 这其实就是在jvm还要底层的cup指令级别的一种优化操作,在这里指令是会被优化不按顺序执行,但是并不影响对同一值的操作,比如(a+1)×3,也可以3×(a+1),但是有时候上升到代码逻辑里就会导致一些问题
- 线程内部是有序的,而两个线程之间互看却是无序的交替运行,这就是重排序在起作用
-
变量的可见性:
- volatile、final、synchronized修饰的变量
- 其中final的可见性是指,初始化完成的final变量,是多线程可见的
十一、线程状态
- Java线程是映射到操作系统的线程模型上运行的,Java线程有5种状态:
- 新建:线程被创建出来
- 运行:start()方法执行后,从新建态变为运行态
- 无限期等待:执行wait()等方法,从运行态变为等待态,执行notify()、notifyAll()方法,从等待态变为运行态
- 限期等待:执行wait()等方法,从运行态变为等待态,执行notify()、notifyAll()方法,从等待态变为运行态
- 阻塞:synchronized排他锁,从运行态变为阻塞态
- 结束:run()方法结束,从运行态到结束态
- 等待态和阻塞态的区别:
- 等待态是等待一段时间,或者唤醒动作的发生
- 阻塞态是等待其他线程放弃锁
- java线程状态转换不同于操作系统线程状态转换
- Java线程状态转换是都集中于运行态变化的
- 两者运行态不同,Java是可能正在运行也可能等待时间片,os是获取时间片
- 两者等待态不同,Java是等待被唤醒或者等待时间超时,os是等待时间片
- 两者阻塞态不同,Java是等待排他锁,os是等待i/o等设备
十一、线程安全
-
什么叫线程安全?
- 通俗解释,就是调用一个多线程访问的对象行为时,不需要额外的同步,不需要考虑线程调度,那这个对象就是线程安全的
-
怎么判断线程安全:
- 先行发生原则:
- 操作A的影响,能被操作B观察到,就说明A先行发生于B
- 和时间先后无关
- 这个原则能确定变量是否是线程安全的
- 先行发生原则:
-
线程安全的分类:
-
由强到弱
-
不可变
-
不可变的类是线程安全的,不用再采取任何的线程安全保障措施保障。只要一个不可变的对象被正确地构建出来,永远也不会在多线程之中处于不一致的状态。
不可变有如下几种:
final 关键字修饰的基本数据类型
String类
枚举类型
基本数值的包装类,如 Long,Integer 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。
对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。
-
-
绝对线程安全
-
不管运行时环境如何,调用者都不需要任何额外的同步措施。 该类的对象被多个线程访问时仍然有效,不管运行时环境如何排列,线程都不需要任何额外的同步。
非常严格的固定,很多平常说的线程安全类,如HashTable及Vector等都打不到标准。
-
-
相对线程安全
-
有条件线程安全是指只能保证单个线程调用的准确的,多个线程只有部分操作线程安全,有些则不满足。
举个栗子:
Vector、HashTable等集合类,普通单个方法可以保证线程安全,但在使用过程中,如果包含了多个方法,则不能保证线程安全
-
-
线程兼容
- 线程兼容是指对象本身并不是线程安全的,但是可以使用同步手段来保证对象在并发环境中可以安全地使用。如添加synchronized关键字修饰方法或代码块等。
-
线程对立
-
线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于 Java 语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。
如:suspend()和resume(),相互执行,会发生死锁
-
-
-
-
线程安全的实现
- 互斥来实现同步(阻塞同步):synchronized和可重入锁
- 非阻塞同步:乐观锁,CAS
- 无同步方案:天生就不涉及共享数据,自然天生线程安全
十二、锁优化
- 自旋锁:避免了反复短暂的线程上下文切换
- 自适应自旋锁:自适应的判断需不需要自旋,自旋多久
- 锁消除:虽然加了锁,但是即时编译器判断出不会产生非线程安全问题,所以优化忽略
- 锁粗化:通常锁范围会控制的尽量小,但有时候对同一个对象的加锁解锁也会导致性能问题,所以会适当扩大加锁的范围
- 轻量级锁:。。。。。
- 偏向锁:。。。。
上面这些内容可以重点看看这个老哥写的内容:字节面试官:synchronized能保证可见性吗
synchronized能实现原子性、有序性(通过实现拿到锁的线程对cup的独占)、可见性(在退出监视器对象前会重新将值从工作内存改到主内存)
volatile能实现有序性(对读写操作加内存屏障来避免指令重排序实现有序性)、可见性(该值的更改会立即赋到主内存,并对其他线程可见)
大家都知道 synchronized 是锁。那怎么会实现可见性和有序性。volatile也能实现对吧。
java内存模型是这么规定的
关于主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存。如何从工作内存同步到主内存中的实现细节。java内存模型定义了8种操作来完成。这8种操作每一种都是原子操作。8种操作如下:
lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:
不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。
可以看到最后一条synchronized 实现其实是靠lock和unlock实现的。在对一个变量unlock操作前。必须也先把此变量同步回主内存中。这就实现了可见性
而有序性是一个变量在同一个时刻只允许一个线程对其进行lock。这就说明了持有一个锁的两个同步块只能串行进行。
再来说下volatile。
他的可见性是在对变量进行use动作前必须在它前面进行load,read动作
在对变量进行assign动作后必须连续进行store,write动作。
也就是说,使用前先读取变量在内存的值,并且载入到工作内存副本中
赋值后要马上写回内存
而有序性是通过内存屏障来实现的。(重排序时不能把后面的指令重排序到内存屏障之前)
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。(首先保证了正确性,再去追求执行效率)
1.在每个volatile写操作前插入StoreStore屏障;对于这样的语句Store1; StoreLoad; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
2.在每个volatile写操作后插入StoreLoad屏障;对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
3.在每个volatile读操作前插入LoadLoad屏障;对于这样的语句Load1;LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
4.在每个volatile读操作后插入LoadStore屏障;对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
如果编译器无法确定后面是否还会有volatile读或者写的时候,为了安全,编译器通常会在这里插入一个StoreLoad屏障
————————————————
版权声明:本文为CSDN博主「小炫剑指大厂」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xu505928168/article/details/99292084
十三、其他
至于其他有关java多线程的概念,参考我的另一个博客《java多线程学习》

浙公网安备 33010602011771号