java虚拟机
运行时的数据区域
程序计数器
- 内存空间较小,可视为当前线程执行的字节码行号指示器。当字节码解释器工作时,通过改变计数器的值来选择下一个字节码指令。
- 这里唯一的内存区域java虚拟机规范中没有规定outOfMemoryError情况的区域
- 线程私有
虚拟机栈
- 线程私有
- 在执行每种方法的同时,将创建一个栈帧来存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 会出现栈溢出异常
- 虚拟机栈可以动态扩展。如果在扩展过程中不能申请足够的内存,也会发生OOM
本地方法栈
- 用于虚拟机native方法服务
堆
-
线程共享
-
这个内存区域的唯一目的是存储对象的实例和数组
- 由于逃逸分析技术的发展,所有对象逐渐分配在堆上,变得不那么绝对。
-
通过-Xmx和-Xms控制大小
-
可细分为新生代和老年代
- 再细分为Eden空间、From Survivor空间、To Survivor空间
-
GC管理主要区域
方法区
-
线程共享
-
存储虚拟机加载的信息、常量、静态变量、即时编译代码等数据
-
该区域的内存回收主要针对常量池的回收和无用卸载
-
无用类条件
- 1.所有这子都被回收了, 也就是Java这种类型的例子在堆中不存在。
- 2.加载此类Classboader已经被回收
- 3.这类对应javalang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
-
常量池运行
-
是方法区的一部分
- Class除了类似的版本、字段、方法、界面和其他描述信息外,还有一个常量池信息(Constant Pool Table),用于存储编译期间生成的各种字面量和符号引用,这部分内容将存储在类加载后进入方法区常量池中
直接内存
- 在JDK1.4中新加人了NIO引入基于通道的类别(Channel)与缓冲区(Buffer)的I/O可以使用方法Native通过存储,函数库直接分配堆外内存Java堆中的DirectByteBuffer对象作为该内存的引用进行操作。由于避免了在某些场景中显著提高性能Java堆和Native堆中来回复制数据
逃逸分析
-
逃逸分析的基本行为是分析对象的动态作用域:当一个对象在方法中被定义时,它可能会被外部方法引用,例如作为调用参数传输到其他方法,称为法逃逸。它甚至可能被外部线程访问,如赋值给类变量或可以访问其他线程的实例变量,称为线程逃逸。 如果能证明一个对象不会逃离方法或线程,即其他方法或线程不能通过任何方式访问该对象,则可以有效地优化该变量。
-
栈上分配(Stack Allocation)
- 虚拟机的垃圾收集系统可以回收堆中不再使用的对象,但回收动作需要时间来筛选可回收对象,回收和整理内存。如果确定一个对象不会逃避方法,那么让对象在栈上分配内存将是一个很好的想法,对象占用的内存空间可以被栈帧销毁。在一般应用中,不会逃跑的局部对象占很大比例。如果可以在栈上分配,大量对象会随着方法的结束自动销毁,垃圾收集系统的压力会小很多。
-
同步消除(SynchronizationElimination)
- 线程同步本身是一个相对耗时的过程。如果逃逸分析可以确定一个变量不会逃离线程,不能被其他线程访问,那么这个变量的读写肯定不会有竞争,可以消除这个变量的同步措施。
-
标量替换(Scalar Replacement)
- 这意味着一个数据不能再分解小的数据,Java虚拟机中的原始数据类型(int、long等数值类型和reference类型等。)不能进一步分解,可以称为标量。如果一个数据能够继续分解,则称为聚合量(Aggregate),Java 对象是最典型的聚合量。如果把一个Java根据程序访问情况,将使用的成员变量恢复到原始类型,称为标量替换。如果逃逸分析证明一个对象不会被外部访问,并且该对象可以分散,则该程序可能不会创建该对象,而是直接创建该方法使用的几个成员变量。拆分对象后,除了将对象的成员变量分配到堆栈上(堆栈上存储的数据很有可能被虚拟机分配到物理机器的高速寄存器中)外,还可以为后续的进一步优化手段创造条件。
-
-
一系列需要数据流敏感的分析
- 如有必要,并确认有利于程序运行,用户可以使用参数-xx: DoEscapeAnalysis手动打开逃逸分析,打开后可通过参数-XX: PrintEscapeAnalysis查看分析结果。在逃生分析的支持下,用户可以使用参数-xx: EliminateAllocations 打开标量替换,使用 XX: EliminateLocks使用参数打开同步消除-XX: PrintEliminateAllocations检查标量的替换情况。
虚拟机对象
对象创建过程
-
1.遇到一个虚拟机会new在指令中,首先检查指令的参数是否可以定位在常量池中的个别符号引用,并检查该符号引用代表的类是否已加载、分析和初始化。如果没有,则必须首先执行相应的类加载过程
-
2.为新对象分配内存
-
3.将分配的内存空间初始化为零
-
4.设置对象头信息
- 对象哈希码,对象GC分代年龄,锁标志
-
5.执行<init>初始化方法
对象的内存布局
-
对象头
-
Mark Word
- 哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏时戳等
-
类型指针
- 也就是说,对象指向其类元数据的指针,虚拟机通过这个指针确定对象是哪个类的实例。
-
如果对象是一个数组,则对象头中还有一个记录数组长度的数据
-
-
实例数据
- 对象存储的储的有效信息
-
对齐填充
- 这部分不一定存在,也没有特殊意义,起着占位符的作用
对象访问定位
-
Java程序需要堆栈reference数据操作堆上的特定对象。reference类型在Java虚拟机规范只规定了指向对象的引用,并没有定义引用应该如何定位和访问堆中对象的具体位置,因此对象的访问也取决于虚拟机的实现。目前,主流的访问方法有两种:使用句柄和直接指针
- 使用句柄
- 直接指针
性能监控和故障处理工具
命令行工具
-
bin目录中的命令行工具大多是jdk/lib/tools.jar只是类库的一层薄包装,它们的主要功能代码是tools在类库中实现。
-
jps
- JVMProcessStatusTool,显示指定系统中的所有显示HotSpot虚拟机进程
-
jstat
- JVM Statistics Montoring Tool用于收集 HotSpot 虚拟机各方面的运行数据
-
jinfo
- Configuration Info for Java,显示虚拟机配置信息
-
jmap
- Memory Map for Java, 生成虚拟机的内存转储快照(heapdump文件)
-
jbat
- JVM Heap Dump Browser. 用于分析heapdump文件,它会建立一个HTTP/HTML服 务器,让用户可以在浏览器上查看分析结果
-
jistack
- Stack Trace for Java, 显示虚拟机的线程快照
可视化工具
- JConsole
- visualvm
用户态/内核态
由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 :用户态 和 内核态
内核态
- CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡. CPU也可以将自己从一个程序切换到另一个程序
用户
- 只能受限的访问内存, 且不允许访问外围设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获取
垃圾收集器
对象已死判断
-
引用计数法
-
定义:
- 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值就减1,任何时刻计数器为0的对象就是不可能再被使用的
-
未使用
- 主流虚拟机没有使用这种方法,其中最主要原因是它很难解决对象之间的相互循环引用的问题,例如objA.instance=objB,obj.instance=objA
-
-
可达性分析算法
-
主流实现中,都是通过该方法判断对象存活的
-
基本思路
- 就是通过一系列的称为“GCRoots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots 没有任何引用链相连(用图论的话来说,就是从GCRoots到这个对象不可达)时,则证明此对象是不可用的。
-
GC Roots对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
-
-
引用
-
强引用
- 是指在程序代码之中普遍存在的,类似“Objectobj=newObject()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
-
软引用
-
是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了 SoftReference类来实现软引用
- SoftReference<String> sr = new SoftReference<String>(new String("hello")); System.out.println(sr.get());
-
-
弱引用
-
是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK12之后,提供了 WeakReferenee类来实现弱引用
- WeakReference<String> sr = new WeakReference<String>(new String("hello")); System.out.println(sr.get());
-
-
虚引用
-
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK12之后,提供了PhantomReferenee 类来实现虚引用
- PhantomReference<String> pr = new PhantomReference<String>(new String("hello")); System.out.println(pr.get());
-
-
垃圾收集算法
-
标记清除算法(Mark-Sweep)
-
定义
- 为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。
-
不足
- 效率问题,标记和清除两个过程的效率都不高
- 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
-
-
复制算法
-
定义
- 它将可用内存按容量划分为大小相等的两块,每次只使用其中的块。当这一块的内存用完了,就将还存活着的对象复制到另外块上面,然后再把已使用过的内存空间一次清理掉。
-
优点
- 不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
-
不足
- 算法的代价是将可用内存缩小为了原来的一半
-
升级版
-
定义
- 不按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块 Survivor空间上,最后清理掉Eden和刚才用过的Suvivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1。
-
优点
- 每次新生代中可用内存空间为整个新生代容量的90%
-
不足
- 对象存活率较高时会进行较多的复制操作,效率将会变低
-
-
-
标记整理算法(Mark-Compact)
-
定义
- 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
-
-
分代收集算法
-
新生代
- 采用复制算法
-
老年代
- 标记-清除或者标记-整理算法
-
垃圾收集器
-
serial收集器
- 单线程收集器,STW时间过长
-
parNew收集器
- 多线程收集器
-
CMS收集器
-
定义
- 是一种以获取最短回收停顿时间为目标的收集器
-
采用标记-清除算法实现
-
过程
-
初始标记
-
仅仅只标记GC roots能直接关联到的对象
- 发生STW
-
-
并发标记
- 进行GC Roots Tracing
-
重新标记
-
为了修正并发标记时因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
- 发生STW
-
-
并发清除
-
-
优点
- 并发收集、低停顿
-
不足
- 占用cpu资源,默认启动的回收线程数是(cpu数量+3)/4
- 无法收集浮动垃圾,即用户线程在运行过程中不断产生的新的垃圾,这部分垃圾CMS无法在当次收集中处理掉它们,只好等待下次GC时清理,因此也要预留一部分空间提供并发收集时程序运行使用。
- 由于采用标记-清除算法,会产生大量空间碎片。为解决这个问题,CMS提供开关参数用于在进行FullGC时开启内存碎片整理,此时SWT时间不得不变长,CMS又提供另外一个参数设置执行多少次不压缩的FullGC后来一次带压缩的。
-
-
G1收集器
-
特点
-
并行与并发
- 利用多CPU、多核来缩短STW时间
-
分代收集
-
空间整合
- 与CMS标记-清除算法不同,G1从整体上来看是采用标记-整理算法,从局部(两个region之间)上来看是基于复制算法实现的,所以不会产生内存空间碎片
-
可预测停顿
- 能让使用者指定在一个长度为m毫秒的时间段内,消耗在垃圾收集上的时间不得超过n毫秒
-
内存布局
- 它将整个java堆划分为多个大小相等的独立区域(region),虽然还保留了新生代和老年代的概念,但它们已不是物理隔离的了,都是一部分region的集合,
-
优先回收(Garbage-First)
- G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
-
避免全堆扫描
- 虚拟机都是使用remembered set来避免全堆扫描的,如果发现reference引用的对象处于不同的region之中(在分代中就是在不同的代中)就把相关信息记录到被引用对象所在的region的remembered set中,在进行内存回收可达性分析时,将remembered set加入根节点范围。
-
-
过程
-
初始标记
- 与CMS一致
-
并发标记
- 与CMS一致
-
最终标记
- 合并remembered set
-
筛选回收
-
-
内存分配及回收策略
-
对象优先在新生代eden区中分配,如果该区没有足够的空间分配时,虚拟机将发起一次Minor GC
-
大对象直接进入老年代
- 虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(复习一下:新生代采用复制算法收集内存)。
-
长期存活的对象进入老年代
- 虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在 Eden 出生并经过第一次 Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold 设置。
类文件结构
无符号数和表
- 根据java虚拟机规范的规定,class文件格式采用一种类似于c语言结构体系的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表
- 无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1字节、2字节、4字节、8字节
- 表由多个无符号数或其他表作为数据项构成的复合数据类型
class类文件结构
-
class文件是一组以8位字节为基础单位的二进制流
-
魔数与class文件版本
- 每个class文件的头4个字节被称为魔数,它的唯一作用是确定这个文件是否是一个能被虚拟机接受的class文件,使用魔数而不使用扩展名,是因为扩展名可以随意修改,class文件的魔数值为0xCAFEBABE
- 紧接着魔数的4个字节存储的是class文件的版本号,5、6字节是次版本号,7、8字节是主版本号
-
常量池
- 紧接着主次版本号之后的是常量池入口
-
访问标志
- 在常量池结束之后,紧接着两个字节代表访问标志,这个标志用于识别一些类或接口层次的访问信息,包括:这个class是类还是接口;是否定义为public类型;是否定义为abstract类型;是否被申明为final
-
类索引、父类索引与接口索引集合
-
字段表集合
-
字段表用于描述接口或者类中声明的变量
-
-
方法表集合
-
属性表集合
字节码指令
- 加载和存储指令
- 运算指令
- 类型转换指令
- 对象创建与访问指令
- 操作数栈管理指令
- 控制转移指令
- 方法调用和返回指令
- 异常处理指令
- 同步指令
类加载机制
虚拟机规范严格要求有且只有5中情况必须立即对类进行“初始化”(加载、验证、准备自然需要在此前开始):
- 1.使用new关键字实例化对象的时候、调用类的静态方法、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)
- 2.反射调用的时候
- 3.当初始化一个类的时候,如果父类还没有初始化,则需要先出发父类的初始化
- 4.虚拟机会先初始化主类
- 5.当使用JDK1.7的动态语言支持时,如果一个java.langinvokeMethodHandle实例最后的解析结果REF_getStaticREF_puStaticREF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
加载
- 1.通过一个类的全限定名来获取定义此类的二进制字节流
- 2.将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
- 正式为类变量(被static修饰的变量,不包括实例变量)分配内存并设置变量初始值,这些变量所使用的内存都将在方法区中进行分配。
解析
- 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
初始化
- 初始化阶段是执行类构造器<clinit>()方法的过程
使用
卸载
类加载器
分类
-
启动类加载器:使用C++语言实现,是虚拟机自身的一部分
-
其他类加载器:由java语言实现,独立于虚拟机外部,并且全部都继承自抽象类java.lang.ClassLoader
-
扩展类加载器
-
应用程序类加载器
- 也称为系统类加载器,加载classpath路径上所指定的类库
-
双亲委派模型
- 过程:如果一个类加载器收到了类加载请求,它首先不会尝试自己去加载这个类,而是把这个请求委派给父类加载器去完成,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈不能完成这个加载请求时,子加载器才会尝试自己去加载
- 好处:因为最终都是委派给顶端的启动类加载器进行加载,因此类在程序的各种类加载器环境中都是同一个类,保证了java类型体系中最基础的行为
自定义类加载器">自定义类加载器
-
继承ClassLoader,重写loadClass方法
-
使用
- MyClassLoader myClassLoader=new MyClassLoader(); Object obj=myClassLoader.loadclass("org.test.classloading.ClassLoaderTest").newInstance();
java语法糖
泛型与类型擦除
- Java中泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型了,泛型被擦除,并且在相应的地方插入强制类型转换
自动装箱、拆箱与遍历循环
- 自动装箱、拆箱在编译之后被转换成了对应的包装和还原方法,而遍历循环则把代码还原成了迭代器的实现
高并发
硬件的效率与一致性
java内存模型
内存间交互操作
-
一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存的实现细节,java内存模型定义了以下8种操作来完成,虚拟机实现时必须保证每一种操作都是原子的、不可再分的
-
lock
- 作用于主内存的变量,它把变量标识为一个线程独占的状态
-
unlock
- 作用于主内存变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
-
read
- 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
-
load
- 作用于工作内存的变量,它把read操作从主内存得到的变量值放入工作内存的变量副本中
-
use
- 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
-
assign(赋值)
- 作用于工作内存的变量,它把一个从执行引擎接受到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
-
store(存储)
- 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write使用
-
write
- 作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中
-
8种基本操作时必须满足如下规则: 1.不允许read和 loadstore和 write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现 2.不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。 3.不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内行同步回主内存中。 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个术被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store 操作之前,必须先执行过了assign 和 load操作。 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock 操作可以被同一条线程重复执行多次,多次执行ock后,只有执行相同次数的unlock 操作,变量才会被解锁。 4.如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。 5.如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。 6.对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
-
volatile变量特殊规则
- 1.当一个变量定义为volatile之后,它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见。
- 2.使用volatile变量的第二个语义是禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的方法执行过程中无法感知到这点,这也就是Java内存模型中描述的所谓的“线程内表现为串行的语义”(Within- Thread As-If-Serial Semantics)
原子性
- 原子性(Atomicity):由Java内存模型来直接保证的原子性变量操作包括readload、 assign、usestore 和 write,我们大致可以认为基本数据类型的访问读写是具备原子性的(例外就是long和double的非原子性协定,读者只要知道这件事情就可以了,无须太过在意这些几乎不会发生的例外情况)。 如果应用场景需要一个更大范围的原子性保证(经常会遇到),Java内存模型还提供了 lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块--synchronized关键字,因此在 synchronized 块之间的操作也具备原子性。
可见性
- 可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。上文在讲解volatile变量的时候我们已详细讨论过这一点。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。 除了volatile 之外,Java 还有两个关键字能实现可见性,即synchronized 和 final。同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行 storewrite操作)”这条规则获得的,而 final关键字的可见性是指:被 final 修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见final字段的值
有序性
- Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性, volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一企变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。
线程
-
Thread所有关键方法都是声明为Native的
-
实现线程的方式
-
使用内核线程实现
- 局限性:由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(UserMode)和内核态(Kernel Mode)中来回切换。
-
使用用户线程实现
- 现在使用用户线程的程序越来越少,java放弃使用它
-
使用用户线程加轻量级进程混合实现
-
-
java线程
-
基于操作系统原生线程模型来实现,使用一对一线程模型
-
线程调度
-
协同式调度
- 线程执行时间由线程本身控制,线程把自己的工作执行完之后,要主动通知系统切换到另外的线程上。 缺点:线程执行时间不可控,如果线程编写有问题,一直不告诉系统切换线程,将发生程序阻塞。
-
抢占式调度
- 每个线程将由系统来分配执行时间,线程切换不由线程本身决定
-
Java线程调度采用后者
-
线程优先级
- 10个级别,不过线程优先级不太靠谱,因为java线程是通过映射到系统的原生线程来实现的,线程调度最终取决于操作系统
-
-
线程状态
-
新建
- 创建后尚未启动的线程
-
运行
- 正在运行
-
无限期等待waiting
- 处于这种状态的线程不会被分配cpu执行时间,它们要等待被其他线程主动唤醒
-
限期等待
- 在一段时间后它们由系统自动唤醒
-
阻塞
- 阻塞与等待的区别:阻塞是等待获取到一个排他锁,而等待则是在等待一段时间或者唤醒动作的发生
-
结束
-
-
线程安全的实现方法
-
互斥同步(阻塞同步) 悲观并发策略
- 1.互斥是实现同步的一种手段,临界区、互斥量、信号量都是主要的互斥实现方式。 2.在Java中,最基本的互斥同步手段就是synchronized关键字,synchronized 关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果 Java程序中的 synchronized明确指定了对象参数,那就是这个对象的reference,如果没有明确指定,那就根据synchronized 修饰的是实例方法还是类方法,去取对应的对象实例或Class 对象来作为锁对象。 3.根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的在执行monitorexit指令时会将锁计数器减当计数器为0时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。 4.由于线程阻塞或唤醒需要操作系统完成,要从用户态转换到核心态,所以synchronized是一个重量级操作。
-
非阻塞同步 乐观并发策略
-
乐观并发策略:通俗讲,就是先进行操作,如果没有其他线程争用共享数据,则操作成功,如果争用共享数据,产生了冲突,那就再采取其他补偿措施(最常见的补偿措施就是不断重试,直到成功为止),乐观并发策略的许多实现都不需要把线程挂起,因此这种同步操作成为非阻塞同步
-
非阻塞同步使用CAS等这种处理器指令完成
-
CAS指令需要有3个操作数,分别是内存位置(在Java中可以简单理解为变量的内存地址,用V表示)、日的预期值(用A表示)和新值(用B表示)。CAS指令执行时,当且仅当V符合旧预期值A时处理器用新值B更新V的值,否则它就不执行更新,但是无论是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作
- CAS中的ABA问题:但A的值曾被修改为了B,又修改为了A,那CAS就会误任务它从来没有被改变过,这个漏洞称为CAS操作的ABA问题。大部分情况下,ABA问题不会影响程序并发的正确性,如果要解决ABA问题,使用传统的互斥同步比使用原子类更高效
-
-
-
-
无同步方案
-
不使用共享数据
-
线程本地存储
- Java语言中,如果一个变量要被多线程访问,可以使用volatile 关键字声明它为“易变的”,如果一个变量要被某个线程独享,可以通过java.langThreadLocal类来实现线程本地存储的功能,每一个ThreadLocal 对象都包含了一个独一无二的 threadLocalHashCode 值,使用这个值就可以在线程 K-V 值对中找同对应的本地线程变量。
-
-
-
锁优化
-
自旋锁与自适应锁
-
自旋锁
- 互斥同步对性能最人的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
- 如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次,用户可以使用参数-XX:PreBlockSpin米更改。
-
自适应锁
- 自适应意味着自旋的时间不再固定了,而是前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
-
-
锁消除
- 锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁门然就无须进行。
-
锁粗化
- 如果对一个对象反复加锁解锁,甚至加锁操作出现在循环体中,将会把加锁同步的范围扩展到整个操作序列的外部。
-
轻量级锁
- 加锁过程: 在代码进入同步块的时候,如果同步对象没有被锁定(即对象头MarkWord锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(LockRecord)的空间,用于存储锁对象目前的 MarkWord的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word)。 然后,虚拟机将使用CAS操作尝试将对象的MarkWord更新为指向LockRecord 的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象MarkWord的锁标志位(MarkWord的最后2bit)将转变为“00”,即表示此对象处于轻量级锁定状态。 如果这个更新操作失败了,虚拟机首先会检查对象的MarkWord 是否指向当前线程的栈帧,如果只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
-
偏向锁
-
如果说轻量级锁是在无竞争的情况下使用 CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步
- 过程: 当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的 Mark Word 之中,如果 CAS 操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如 LockingUnlocking及对Mark Word 的 Update 等)。 当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(RevokeBias)后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为”00“)
-
-
-