java虚拟机
1 意义
- 屏蔽各个硬件平台和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果
2 运行时数据区组成
2-1 线程私有
程序计数器
- 当前线程所执行的字节码的行号指示器:<ol><li>如果正在执行的是java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;</li><li>如果正在执行natvie方法,计数器值为空(undefined)</li></ol>
作用
- java虚拟机字节码解释器通过改变这个计数器的值来选取下一条要执行的字节码指令
- 没有规定任何outofmemoryerror的情况
虚拟机栈
- 用途:为jvm执行java方法服务
- 编译期间完成分配
<p>结构:栈帧:<b><u>局部变量表</u></b>、操作数栈、动态链接、方法出口</p>
- 基本类型变量,(boolean,byte,char,short,int,float,long,double)
- 对象句柄
- 方法参数
- 方法的局部变量
- 两种异常:stackoverflowerror、outofmemoryerror; -xss设置栈大小
本地方法栈
- 用途:为虚拟机使用到的native方法服务
- 两种异常:stackoverflowerror、outofmemoryerror
2-2 线程共有
方法区(自用)
- 用途:用于存储已被jvm加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
运行时常量池
内容:存放编译产生的字面量(<b>常量final</b>)和<b>符号引用</b>
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
特点:运行时常量池相对于class文件常量池的一个重要特征是具备<b>动态性</b>
- java语言并不要求常量一定只有编译期才能产生,运行期间也能将新的常量放入池中
- 异常:(无法扩展)outofmemoryerror
方法区的gc
- 参考垃圾对象的判定
gc堆(可及)
- 目的:存放对象实例和数组
分代处理
- 目的:更好的回收内存和更快的分配内存
新生代
- eden空间
- from survivor空间
- to survivor空间等
- 老年代
- 空间结构:逻辑连续的空间,物理可不连续
- 优先分配tlab(thread local allocation buffer),减少加锁,提高效率
- gc管理的主要区域
异常:如果在堆中没有完成内存分配,并且堆也无法扩展时,会抛出outofmemoryerror
- -xms初始堆大小 -xmx最大堆大小
3 对象的创建
3-1 检查参数是否在常量池中定位到一个类的符号引用;该类是否被加载、解析、初始化过
- 若没有做进行类加载
3-2 若有则分配内存
内存绝对规整
- 用“指针碰撞”来分配内存
内存不规整
- 用“空闲列表”来分配内存
线程安全
对分配内存空间的工作进行同步处理
- 采用cas+失败重试的方式保证更新操作的原子性
每个线程分配一块本地线程分配缓冲区
tlab
- -xx:+/-usetlab
- 3、始化已分配内存为零值(保证类型不赋初值可以使用)
- 4、上面工作完成后,执行init方法按照程序员意愿初始化对象
4 对象创建流程图
5 对象的内存布局
5-1 对象头
- 存储运行时数据
- 存储类型指针
5-2 实例数据
- 是对象真正存储的有效信息
5-3 对齐填充
- 起占位符的作用
6 对象的访问定位
6-1 使用句柄
- 堆中有句柄池,存储到实例数据和类型数据的指针;栈中的引用指向对象的句柄地址
优点
- <ol><li>reference中地址相对稳定;</li><li>对象被移动(gc时)时只会改变句柄中的实例数据指针</li></ol>
6-2 直接指针
-
栈中的引用直接存储对象地址,到方法区中类型数据的指针包含在对象实例数据中
优点
- 访问速度快,节省了一次指针定位的开销
7 oom异常
7-1 虚拟机栈和本地方法栈溢出
线程请求栈深度大于最大深度stackoverflowerror
- 设置-xss128k,在单线程下,通过不断调用递归方法。
新线程拓展栈时无法扩展出现outofmemoryerror错误
- 不断创建新线程,并让创建的线程不断运行
- -xss
7-2 方法区和运行时常量池溢出
java.lang.outofmemoryerror后会跟permgen space
- 不断创建新的字符串常量,并添加到list中
- -xx:permsize和-xx:maxpermsize
7-3 堆溢出
java.lang.outofmemoryerror:java heap space
内存泄漏
- 通过不断创建新对象,并放入list中,保证gcroots到对象之间路径可达
- 内存溢出
- -xms -xmx
7-4 本机直接内存溢出
- 在heap dump文件中没有明显异常
- -xx
8 垃圾对象的判定
8-1 对象的引用
强引用
- 存在就不回收
软引用
将要发生内存溢出之前
- 实现缓存
弱引用
下一次垃圾回收
- 回调函数中防止内存泄露
虚引用
对对象的生存时间没有影响
- 能在这个对象被收集器回收时收到一个系统通知
8-2 引用计数法
- 难以解决对象之间相互循环引用的问题
8-3 根搜索算法
- 从gc roots向下搜索建立引用链;一个对象如果到gc roots没有任何引用链相连时,证明对象不可用
gc roots
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
8-4 堆中垃圾回收过程
- 1、如果对象在进行可达性分析后发现没有与gc roots相连接的引用链,那它将会被第一次标记
- 2、断对象是否有必要执行finalize()方法,(没有覆盖,被调用过,都没有必要执行),放入f-queue队列
- 3、放入f-queue中,进行第二次标记
- 4、被拯救的移除队列,被两次标记的被回收
8-5 方法区中垃圾回收
废弃常量
- 没有任何一个对象引用常量池中的“abc”常量
无用的类(满足条件可以被回收,非必然)
- 1、该类所有的实例都已经被回收
- 2、加载该类的加载器被回收
- 3、该类对应的javalang.class对象没有在任何地方被引用,无法通过反射访问该类的方法
9 垃圾回收算法
9-1 标记-清除算法
- 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
- <ul><li>效率问题,标记和清除两个过程的效率都不高</li><li>空间问题,产生大量不连续的内存碎片,连续内存不足会再次触发gc</li></ul>
9-2 复制算法
- 将内存等分,每次用一块,当这块内存用完了,就将活着的对象复制到另一块,然后把前者清空
- <ol><li>对象存活率较高时就要进行较多的复制操作,效率将会降低 </li><li>空间利用率低</li></ol>
9-3 标记-整理算法
- 所有存活的对象移向一端,然后直接清理掉端边界以外的内存
9-4 分代收集算法
新生代
- 复制算法
老年代
- 标记-整理或标记-清除
9-5 hotspot算法
枚举根结点
- 当执行系统停顿下来后,并不需要一个不漏的检查完所在执行上下文和全局的引用位置,在hotspot的实现中,使用一组称为oopmap的数据结构来存放对象引用
安全点
- 在这些特定的位置,线程的状态可以被确定
位置
- 方法调用指令
- 循环跳转指令
- 异常跳转指令
中断方式
抢占式
- gc发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑在安全点上
主动式
- 设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起
安全区域
- 背景:线程sleep状态或者blocked状态的时候,无法响应jvm中断,走到安全的地方,jvm也不能等他们,这样就无法进行gc
- 安全区域是指在一段代码中,引用关系不会发生变化,这个区域中的任何地方开始gc都是安全的。
10 垃圾收集器
10-1 serial收集器
特点
- 新生代收集器
- 采用复制算法
- 单线程收集
- 进行垃圾收集时,必须暂停所有工作线程,直到完成
应用场景
- 是hotspot在client模式下默认的新生代收集器
- 简单高效(与其他收集器的单线程相比)
- 对于限定单个cpu的环境来说,serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
参数设置
- "-xx:+useserialgc":添加该参数来显式的使用串行垃圾收集器;
10-2 parnew收集器
特点
- 新生代收集器
- 采用复制算法
- 除了多线程外,其余的行为、特点和serial收集器一样
应用场景
- server模式下,parnew收集器是一个非常重要的收集器
- 单个cpu环境中,不会比serail收集器有更好的效果,因为存在线程交互开销
参数设置
- "-xx:+useconcmarksweepgc":指定使用cms后,会默认使用parnew作为新生代收集器;
- "-xx:+useparnewgc":强制指定使用parnew;
- "-xx:parallelgcthreads":指定垃圾收集的线程数量,parnew默认开启的收集线程与cpu的数量相同;
10-3 parallel scavenge收集器
特点
- 新生代收集器
- 采用复制算法
- 多线程收集
- cms等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间; 而parallel scavenge收集器的目标则是达一个可控制的吞吐量(throughput)
应用场景
- 高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间
- 当应用程序运行在具有多个cpu上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互
参数设置
- "-xx:maxgcpausemillis"控制最大垃圾收集停顿时间
- "-xx:gctimeratio" 设置垃圾收集时间占总时间的比率
- "-xx:+useadptivesizepolicy"
10-4 serial olc收集器
特点
- 针对老年代
- 采用"标记-整理"算法(还有压缩,mark-sweep-compact)
- 单线程收集
应用场景
- 主要用于client模式
在server模式中
- 在jdk1.5及之前,与parallel scavenge收集器搭配使用(jdk1.6有parallel old收集器可搭配)
- 作为cms收集器的后备预案,在并发收集发生concurrent mode failure时使用
10-5 parallel old收集器
特点
- 针对老年代
- 采用"标记-整理"算法
- 多线程收集
应用场景
- jdk1.6及之后用来代替老年代的serial old收集器
- 特别是在server模式,多cpu的情况下
- 在注重吞吐量以及cpu资源敏感的场景,就有了parallel scavenge加parallel old收集器的"给力"应用组合
参数设置
- "-xx:+useparalleloldgc":指定使用parallel old收集器
10-6 cms收集器
特点
- 针对老年代
- 基于"标记-清除"算法(不进行压缩操作,产生内存碎片)
- 以获取最短回收停顿时间为目标
- 并发收集、低停顿
- 需要更多的内存(看后面的缺点)
应用场景
- 与用户交互较多的场景
- 希望系统停顿时间最短,注重服务的响应速度
- 以给用户带来较好的体验
- 如常见web、b/s系统的服务器上的应用
参数设置
- "-xx:+useconcmarksweepgc":指定使用cms收集器
运行过程
- 初始标记
- 并发标记
- 重新标记
- 并发清除
缺点
- 对cpu资源非常敏感
- 无法处理浮动垃圾,可能出现"concurrent mode failure"失败
- 产生大量内存碎片
10-7 g1收集器
特点
并行与并发
- gc收集线程并行
- 用户线程与gc线程并发
- 分代收集,收集范围包括新生代和老年代
- 空间整合:结合多种垃圾收集算法,空间整合,不产生碎片
- 可预测的停顿:低停顿的同时实现高吞吐量
应用场景
- 面向服务端应用,针对具有大内存、多处理器的机器
- 最主要的应用是为需要低gc延迟,并具有大堆的应用程序提供解决方案
运行过程(不计remembered set操作)
初始标记
标记gc root能直接关联到的对象
- 需要停顿用户线程
并发标记
对堆中对象可达性分析
- 并发执行
最终标记
修正并发标记中因用户线程运行发生改变的标记记录
- 需要停顿线程
筛选回收
对region的回收价值和成本排序,根据参数指定回收计划
- 可以并发
参数设置
- "-xx:+useg1gc":指定使用g1收集器
- "-xx:initiatingheapoccupancypercent":当整个java堆的占用率达到参数值时,开始并发标记阶段;默认为45
- "-xx:maxgcpausemillis":为g1设置暂停时间目标,默认值为200毫秒
- "-xx:g1heapregionsize":设置每个region大小,范围1mb到32mb;目标是在最小java堆时可以拥有约2048个region
10-8 内存分配和回收策略
对象优先在eden分配
- 大多数情况下,对象在新生代eden区中分配,当eden区没有足够空间进行分配时,虚拟机将发起一次minor gc
大对象直接进入老年代
- 所谓大对象,就是需要大量连续内存空间的对象,最典型的大对象就是那种很长的字符串以及数组
- 长期存活的对象将进入老年代
动态对象年龄判定
- 如果在survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象就可以进入老年代,无须等到maxtenuringthreshold中要求的年龄
空间分配担保
- minor gc之前,jvm检查老年代最大连续空间是否大于新生代所有对象的空间,成立则确保minor gc安全
- 不成立,参看参数handlepromotionfailure是否允许担保失败,允许则检查老年代最大连续空间是否大于历次晋升的对象的平均大小,大于则尝试minor gc
- 否则,进行full gc
- 内存管理机制
- 垃圾收集器
- 内存分配策略
11 虚拟机类加载机制
11-1 类加载的时机
声明周期
- 加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中验证、准备、解析3个部分统称为连接
以下情况对类进行初始化
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类
- 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.methodhandle实例最后的解析结果bef_getstatic、bef_putstatic、bef_invokestatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
11-2 类加载的过程(5)
加载
完成3件事
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口
类的来源
- 从zip包中读取,这很常见,最终成为日后jar、ear、war格式的基础
- 从网络中获取,这种场景最典型的应用就是applet
- 运行时计算生成,这种场景使用得最多的就是动态代理基础
- 由其它文件生成,典型场景就是jsp应用,即由jsp文件生成赌赢的class类
- 从数据库中读取,这种场景相对的少些,例如有些中间件服务器可以选择把程序安装到数据库中来完成程序代码在集群间的分发
验证
- 验证是连接阶段的第一步,这一阶段的目的就是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
验证项
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
- 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都讲在方法区汇总进行分配
解析
- 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
解析动作分类
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
初始化
- 类初始化是类加载过程中的最后一步,这时才真正开始执行类中定义的java程序代码
11-3 类加载器
- 虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为类加载器
- 比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个class文件,被同一个虚拟机加载,只要它们的类加载器不同,那这两个类就必定不相等
11-4 双亲委派模型
分类
启动类加载器
- 这个类加载器使用c++语言实现,是虚拟机自身的一部分
其他类加载器
- 这些类加载器由java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.classloader
- java内存模型与线程
- 线程安全
12 java内存模型与线程
12-1 硬件效率和一致性
硬件使用了很多手段, 目的是提高硬件执行效率
- 为了解决处理器和内存速度的矛盾, 加多级缓存
- 为了使处理器内部运算单元尽量充分使用, 处理器对输入代码进行乱序执行(out-of-order execution)
实际jvm虚拟机中也是用了类似的技术手段来提升性能
- 线程的工作内存 - 硬件多级缓存
- 编译器指令重排序(instruction reorder) - 乱序执行
使用优化问题所带来的问题
多级缓存引入了缓存一致性问题
- 使用缓存一致性协议来解决, 如: msi/ mesi/ mosi/ synapse/ firefly/ dragon protocol 等
12-2 java内存模型(jave memory model, jmm);
可参考jsr-133(java memory model and thread specification revision)
- 主要目标是定义程序中各个变量的访问规则, 这里的变量指的是实例字段/ 静态字段/ 数组对象的元素 等, 可以被线程共享的变量
- @jvm没有直接像c/c++一样直接使用物理硬件和操作系统的内存模型, 增加了跨平台能力;@内存模型的定义必须足够严谨, 保证并发内存操作不会产生奇异;@但是定义也必须足够宽松, 使得虚拟机的实现有足够的自由空间去利用硬件的各种特性(寄存器/ 高速缓存/ 指令集中某些特有指令)/;
主内存和工作内存
主内存
- jvm堆中的一部分
工作内存
- 每个线程都有自己的工作内存, 工作内存中保存被该内存使用的变量的主内存副本
- 线程的读写操作必须对工作内存操作, 无法对主内存直接进行读写操作
- 线程无法直接读写其他线程的工作内存;线程间的变量值的传递需要通过主内存来完成
- jvm虚拟机栈的一部分
- 对应于硬件, 很可能对应了高速缓存甚至寄存器
内存间的交互操作
- 主要讲了java内存模型的原子性
- read/load需要顺序执行;store/write需要顺序执行;但是在指令间可以插入其他指令, 如: read a; read b; load a; use a; load b;
8中原子操作还需要遵从以下规则
- 不允许read和load / store和write单独出现;即不允许变量从主存读取了工作内存不接受 或者 从工作内存回写了主存不接受的情况
- 不允许一个线程丢弃它最近的assign操作;即变量在工作内存中改变了之后必须把该变化同步到主存
- 不允许一个线程无原因地(没有发生过assign操作)把数据从工作内存同步到主存;???防止对主存的无意义刷新更新其他线程已经修改的值(大雾);即对一个变量使用store之前必须经过assign操作
- 变量只能在主存中"诞生", 不允许在工作内存中使用一个未经初始化(load/assign)的变量;即对一个变量使用use/ store之前必须经过 load/assign操作
- 一个变量在同一时刻只允许一个线程对其进行lock操作, 但lock可以被同一线程重复执行多次(可重入), 执行多次lock后必须执行相同次数的unlock操作, 变量才能被解锁
- 如果对一个变量执行lock操作, 将会清空工作内存中此变量的值, 在执行引擎使用该变量前, 需要重新执行load或者assign操作初始化变量的数值
- 如果一个变量事先没有被lock操作锁定, 那么就不允许对它执行unlock操作;不允许一个线程去unlock一个被其他线程锁住的变量
- 对一个变量执行unlock操作之前, 必须将该变量同步回主存中(执行store write操作)
- 这些规则(加上对volatile的一些特殊规定)确定了那些内存访问操作在并发下是安全的
volatile的特殊规定
保证volatile对所有线程的可见性;即 当某线程改变变量值后, 新值对其他线程可立即得知;
- 可见性不等同于一致性;
- volatile在符合以下所有情况时可以保证并发安全:1. 运算结果并不依赖变量的当前值;2. 确保只有一个线程可以修改变量;3.变量不需要与其他的状态变量同时参与不变约束;
volatile变量可以禁止指令冲排序优化
- 利用了内存屏障, 指令重排序无法越过内存屏障
对long/double的特殊规定
- long/double的非原子性协议:允许虚拟机将非volatile修饰的64位数据的读写操作划为两次32位的操作来进行
- 在目前商用虚拟机中64位数据的读写也是原子性的
原子性/可见性/有序性
- 原子性:由java内存模型直接保证的原子性操作包括lock/unlock/read/load/use/assign/store/write
- 可见性:当一个线程修改了共享变量的值, 其他线程能够立即得知这个修改;java内存模型通过在变量修改后将新值同步回主存, 在变量读取前从主存刷新变量值这种依赖主存为传递媒介的方式来实现可见性的; 普通变量和volatile变量都是如此;不过volatile变量的特殊规定保证了新值能够立即同步到主存, 以及每次使用前立即从主存刷新; 因此可以说volatile保证了多线程操作时变量的可见性, 但普通变量无法保证这一点;除了volatile变量外, sync同步代码块和final也可以保证可见性;
- 有序性:如果在本线程内观察, 所有操作都是有序的; 如果在一个线程中观察另一个线程, 所有操作都是无序的;前半句指线程内表现为串行的语义(whthin-thread as-if-serial semantics);后半句指"指令重排序"现象和"工作内存与主内存同步延迟"现象;volatile和sync同步代码块可以保证有序性;
先发性原则
- 先行发生是java内存模型中定义的两项操作间的偏序关系;如果操作a先行发生于操作b, 实际指 在发生b之前, a产生的影响会被b观察到;
满足以下任意一规则, 则不可指令重拍
- 程序次序规则
- 管理锁定规则
- volatile变量规则
- 线程启动规则
- 线程终止规则
- 线程中断规则
- 对象终结规则
- 传递性
- 时间先后顺序和先行发生原则之间基本没有太大关系, 所以衡量并发安全问题时只按先行发生原则为准即可;
12-3 java与线程
线程的实现
使用内核线程实现
- 内核线程定义:由操作系统内核(kernel)直接支持;线程切换/调度/映射到处理器由线程调度器(thread scheduler)完成;支持多线程的kernel叫做多线程内核;
- 轻量级进程定义:程序一般不会直接使用内核线程, 而是使用内核线程的一种高级接口: 轻量级进程(light weight process);轻量级进程即指我们通常意义所讲的线程;轻量级线程和内核线程是的关系是 1 - 1;
- 内核线程/轻量级进程的优点:成为独立的调度单元, 某内核线程/轻量级进程阻塞不会影响整个进程(不影响其他线程);
- 内核线程/轻量级进程的缺点:线程创建/析构/同步都需要系统调用, 代价较高, 需要在用户态和内核态间切换;会消耗内核资源(如 内核线程的栈空间), 因此一个系统支持内核线程/轻量级进程数是有限的;
使用用户线程实现
- 用户线程定义:广义上 - 非内核线程, 如果这样看轻量级线程也属于用户线程;狭义上 - 完全建立在用户空间的线程库上, kernel无法感知用户线程存在, 线程的创建/销毁/同步/调度也全部在用户态中完成;和进程的关系是n-1;
- 用户线程优点:如果实现得当, 速度快且低消耗;支持规模更大的线程数量(部分高性能数据库的多线程使用用户线程实现);
- 用户线程缺点:实现极复杂;某个线程阻塞会对整个进程造成影响;
使用用户线程加轻量级进程实现
- 结合使用两者优点, 用户线程和轻量级线程数量比不定, 为n-m;
java线程的实现
- 1.2前, 用户线程
- 从1.2开始, 使用操作系统原生线程模型来实现, 所以现在使用那种线程和操作系统有关;
- 对于sun jdk来说: windows和linux使用1 - 1线程模型;solaris因为支持n - m模型, 所以可以使用专有虚拟机参数设置;
java线程调度
协同式调度
- 调度问题由线程自身控制
- 可以避免并发问题
- 如果某线程阻塞, 可能会造成整个无法运行
抢占式调度
- 调度由系统(内核线程/轻量级进程)或者其他线程调度程序(用户线程)控制
- 线程可主动让出调度时间, 但无法主动获取调度时间; (java使用thread.yield()让出)
抢占式调度可以设置线程执行优先级
- java中有10个优先级
- 操作系统中线程优先级并非10个, 所以有不对称的对应关系;多的如solaris中有2^32个优先级, 但是windows中只有7个;
- 不应该太多,java中的优先级, 操作系统对系统调度有优化, 如某个线程特别"勤奋", 会被越过优先级分配额外的执行时间;
线程状态和转换
新建
- 创建未启动
运行
- java中的运行中, 对应kernel中的runing和ready, 即运行中或者等待kernel分配执行时间
无限期等待
- 需要被显示唤醒才能继续执行
切换到此状态的方法
- object.wait()
- thread.join()
- locksupport.park()
限期等待
切换到此状态的方法
- thread.sleep()
- object.wait timeout
- thread.join timeout
- locksupport.parknanos()
- locksupport.parkuntil()
阻塞
- 在等待获取排它锁, 其他线程放弃排它锁时才可能结束阻塞状态
- 结束
13 线程安全和锁优化
13-1 线程安全
- 定义:当多个线程访问一个对象时, 如果不用考虑这些线程在运行时环境下的调度和交替执行, 也不需要进行额外的同步, 或者在调用方法行进行任何其他的协调操作, 调用这个对象的行为都可以获得正确的结果, 那这个对象是线程安全的.
java语言中的线程安全
- 需要考虑线程安全的前提是: 多个线程间存在共享数据
按照线程的安全程度分类
不可变
- final修饰的基础类型
- 属性全部为final修饰的对象
绝对线程安全
- 完全满足<上面对线程安全的定义>
- java中大部分声明线程安全的对象并非"绝对"线程安全;如: vector两个线程分别进行增和删的操作, 会抛出arrayindexoutofboundsexception
相对线程安全
- 保证对该对象单独的操作是线程安全的, 但是对特定顺序的连续调用, 就可能需要在调用时使用额外的同步手段来保证正确性
- 如: vector/hashtable/collections.synchronizedcollection()包装的集合 等
线程兼容
- 对象并非线程安全, 但可以通过在调用端正确地使用同步手段来保证安全
- 如: arraylist/hashtable 等
线程对立
- 无论调用端是否采取同步措施, 都无法保证多鲜橙环境下的安全
- 在java中, 这种代码很少出现, 而且通常有害, 应避免
- 如: thread的suspend()和resume();如果两线程同时持有一个线程对象, 一个尝试去中断线程, 另一个尝试恢复线程, 即便使用同步手段, 目标线程都是存在死锁的风险;
线程安全的实现方法
互斥(阻塞)同步
- 属于悲观锁
synchronized关键字 - 需要reference类型参数来指明锁定/解锁的对象
- 编译后会生成monitorenter和monitorexit两个字节码指令, 来运行加锁/解锁操作;monitorenter: 首先尝试获取锁, 如果对象未被锁定, 或者当前线程已经拥有该对象的锁(可重入特点), 把锁计数器+1;monitorexit: 锁计数器-1, 当计数器为0时, 锁被释放;
如果没有明确指定哪个对象来加锁 - static方法: 类的class对象
- 普通方法: 当前实例, 即this
- sync映射到了kernel中的轻量级进程, 加锁/解锁操作实际都需要在 内核态和用户态来回切换, 需要耗费较多的处理器时间, 因此也叫做重量级锁;虚拟机本身对重量级锁会进行一些优化, 比如: 在通知操作系统前加入一段自旋等待过程, 避免频繁切入内核态;
reentrantlock - 属于轻量级锁, 在api层面进行互斥, 相比sycn的优点:1. 等待可中断;2. 公平锁: 会根据申请锁的时间顺序来依次得到锁, 默认构造是非公平的;3. 绑定多条件: 可以绑定多个condition对象;4. 1.6之前效率比sync高, 从1.6之后效率相差不大;
非阻塞同步
- 属于乐观锁:基于冲突检测的乐观并发策略, 即: 先进行操作, 如果没有其他线程争夺资源, 修改成功;如果有资源争夺, 产生了冲突, 进行补偿措施(最常见的是不断重试, 直到成功为止);
- 这种并非策略不需要将线程挂起, 因此也被称为 非阻塞同步;
- 使用乐观锁的必须条件: 操作和冲突检测 是原子性的(ia64和x86指令集通过cas指令来实现);
- java中原子性操作都是通过sun.misc.unsafe的compareandswap来实现的
- cas指令会引入aba问题, 比如: x线程修改前探测变量值为a, 后其他线程将变量先改为b, 再改为a, 这样x进行修改时无法探测到变量实际已经被修改过了;但是并不影响并发的正确性;
无同步方案
- 可重入代码:不依赖存储在堆上的数据和公共的系统资源;用到的状态都通过参数传入;不调用非可重入方法;
- 线程本地存储???
13-2 锁优化
自旋锁/自适应自旋
- 作为阻塞锁的补充, 避免频繁在用户态和内核态之间切换;
- 在进入系统阻塞前, 进行一段时间的自旋, 自旋一般为次数;自旋次数也可以通过前一次在同一个锁上的自旋时间和锁的拥有者的状态来决定, 这种自旋锁叫自适应自旋锁;
锁消除
- 即时编译器(jit)在运行时会对不可能发生共享资源争夺的锁进行消除
- jit对资源是否可能产生争夺的判断 是 通过逃逸分析技术来进行的:如果堆上的所有数据都不会逃逸出去而被其他线程访问到, 那么就可以把他们当做栈上的数据对待, 认为是线程私有的;程序员一般可很清楚的明白数据是否可以逃逸, 但是在jdk的api中默认存在很多锁, 比如字符串"+"拼接在编译后变成使用stringbuffer的append(), 而stringbuffer是带锁的, 锁消除主要针对这些锁进行消除;
锁消除
- 前提:在编写代码时的原则是尽量减小同步代码块的范围: 使锁占用时间尽量小, 其他阻塞线程尽快拿到锁, 以增加效率;
- 但如果一系列的连续操作都对同一对象反复加锁/解锁, 甚至加锁操作出现在循环体内, 即使没有线程竞争, 频繁加锁/解锁也会造成不必要的性能损耗;jvm在探测到这种一连串零碎操作使用同一个对象作锁的情况时, 会将锁进行粗化到整个操作序列外部;如: stringbuffer的连续append()操作;
轻量级锁
- 在没有锁竞争时, 将通知kernel进入互斥状态的操作替换为了对于对象头锁标示位的cas操作;
- 如果存在锁竞争, 轻量级锁会膨胀为重量锁;在这种情况(存在锁竞争)下, 轻量级锁因为增加了额外的cas操作, 会比重量级锁更慢;
偏向锁
- 将对象头标示位设置为偏向锁, 在没有锁竞争时, 再次执行时将同步操作消除掉, 连cas也不做了
14 java内存模型定义了8种原子性的操作
14-1 lock
- 作用于主内存;把一个对象标识为一条线程独占的状态
14-2 unlock
- 作用于主内存;把处于锁定状态的对象释放
14-3 read
- 作用于主内存;将主内存中的变量传输到工作内存
14-4 load
- 作用于工作内存;把read操作从主内存得到的变量值放入工作内存的变量副本中
14-5 use
- 作用于工作内存;把工作内存中的一个变量值传递给执行引擎;每当虚拟机遇到一个需要使用变量的值的字节码指令时执行该操作
14-6 assign
- 作用于工作内存;把从执行引擎接收到的值赋给工作内存的变量;每当虚拟机遇到一个给变量赋值的字节码指令时执行该操作
14-7 store
- 作用于工作内存;把工作内存中的一个变量传出到主内存
14-8 write
- 作用于主内存;把store操作从工作内存中得到的变量值放入主内存的变量中
15 jvm调优
15-1 常见参数
- xms
- xmx
- xmn
- xss
- -xx:survivorratio
- -xx:newratio
- -xx:+printgcdetails
- -xx:parallelgcthreads
- -xx:+heapdumponoutofmemoryerror
- -xx:+useg1gc
- -xx:maxgcpausemillis
15-2 调优思路
确定是否有频繁full gc现象
1.1 如果full gc频繁,那么考虑内存泄漏的情况
内存泄露角度
- 1.使用jps -l命令获取虚拟机的lvmid
- 2.使用jstat -gc lvmid命令获取虚拟机的执行状态,判断full gc次数
- 3.使用jmap -histo:live 分析当前堆中存活对象数量
4.如果还不能定位到关键信息,使用 jmap -dump打印出当前堆栈映像dump文件 - jmap -dump:format=b,file=/usr/local/base/02.hprof 12942
- 5.使用mat等工具分析dump文件,一般使用的参数是histogram或者dominator tree,分析出各个对象的内存占用率,并根据对象的引用情况找到泄漏点
1.2 如果full gc并不频繁,各个区域内存占用也很正常,那么考虑线程阻塞,死锁,死循环等情况
线程角度
- 1.使用jps -l命令获取虚拟机的lvmid
- 2.使用 jstack 分析各个线程的堆栈内存使用情况,如果说系统慢,那么要特别关注blocked,waiting on condition,如果说系统的cpu耗的高,那么肯定是线程执行有死循环,那么此时要关注下runable状态。
- 3.如果还不能定位到关键信息,使用 jmap -dump打印出当前堆栈映像dump文件
- 4.使用mat等工具分析dump文件,一般使用的参数是histogram或者dominator tree,分析出各个对象的内存占用率,并根据对象的引用情况找到泄漏点。
1.3 如果都不是,考虑堆外存溢出,或者是外部命令等情况
- runtime.getruntime.exec()