JVM

一、概况

JDK(Java Development Kit): 支持Java开发的最小环境。包括:Java程序设计语言、Java虚拟 机、Java API类库
JRE(Java Runtime Environment): 支持Java运行的标准环境。包括:Java虚拟机、Java API类库中的Java SE API子集
JVM:Java虚拟机

内存分析工具: Memory
Analyer 1.7.0 Release
JVM监控工具:jdk è bin è
jconsole.exe

二、内存区域与内存溢出

  1. 运行时数据区域
    (1) 程序计数器
    当前线程所执行字节码的行号指示器,所占内存较小
    每个线程一个,相互独立
    如果执行Java方法,记录正在执行的虚拟机字节码指令的地址;如果是Native方法(本地方法,意味着和平台有关),值为空(Undefined)
    唯一一个没有规定任何OutOfMenoryError的区域
    (2) Java虚拟机栈
    描述的是Java方法执行的内存模型:每个方法执行时创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等。
    线程私有,生命周期同线程
    规定了两种异常情况:线程请求的栈深度大于虚拟机允许,抛出StackOverflowError

                                动态扩展无法申请到足够内存,抛出OutOfMenoryError
    

(3) 本地方法栈

为Native方法服务(HotSpot将虚拟机栈和本地方法栈合二为一)

抛出OutOfMenoryError和StackOverflowError异常

(4) Java堆

线程共享区域,虚拟机启动时创建

存放的是对象实例

垃圾收集器的主要区域

堆中没有内存完成实例分配且无法扩展时,抛出OutOfMenoryError

(5) 方法区

线程共享区域

存放的是对象类型数据,存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码

无法满足内存分配下需求时,抛出OutOfMenoryError

(6) 运行时常量池

方法区的一部分,具备动态性

Class文件中有常量池用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池存放(new出来的对象在Java堆中)

无法满足内存分配下需求时,抛出OutOfMenoryError

(7) 直接内存

直接内存不是Java虚拟机规范中定义的区域,也不是虚拟机运行时数据区的一部分

NIO(New Input/Output)引入基于通道与缓存区的I/O方式,使用Native函数库直接分配堆外内存

被频繁使用,抛出OutOfMenoryError

  1. 对象创建

指针碰撞: 要求Java堆规整。使用的内存在一边,空闲的内存在一边,中间使用指针作 为分界点的指示器

空闲列表: 虚拟机维护一个表,记录哪些内存可用

线程安全问题:可能出现同时需要给两个对象分配内存

解决方法: 1. 对分配动作进行同步处理

  1. 把分配动作按线程划分到不同空间,每个线程预先分配一小块内存(本地线程分配缓冲)

  2. 对象内存布局

(1)对象头

包括两部分:第一部分用于存储对象自身的运行时数据(Mark Word)如哈希码、GC分代 年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳

第二部分为类型指针(非必须),即对象指向它的类元数据的指针,虚拟机通 过这个指针来确定对象是哪个类的实例

(Java数组在对象头中还会记录数组长度)

(2)实例数据

存储对象的有效信息,也是在程序代码中所定义的各种类型的字段内容。

(3)对齐填充

对象的起始地址必须是8字节的整数倍。(非必须)

  1. 访问定位(使用)

主流的访问方式:句柄: Java堆中划分出句柄池,reference存储句柄地址,句柄中包括对 象实例数据(Java堆)与类型数据(方法区)的具体地址信息。 对象移动reference本身不需改变,只改变句柄中的实例数据指针

直接指针(Hot Spot):reference直接存放对象地址

速度快,节省了一次指针定位的时间开销

三、垃圾收集器与内存分配策略

(主要包括Java堆和方法区)

  1. 判断对象是否已死

(1) 引用计数算法

添加引用计数器,被引用是+1,引用失效时-1,计数器为0的对象表示不再被引用。

(问题:当有两个对象相互引用时,计数器均不为0,无法回收)

(2) 可达性分析算法

以一系列的“GC Roots”对象为起点,向下搜索(走过的路径称为引用链),当一个对象到GC Roots没有引用链相连时,判断为已死。

GC Roots包括:虚拟机栈中的引用对象、方法区类静态属性的引用对象、方法区常量的引用对象、本地方法栈JNI(native方法)的引用对象。

(3) 引用强度

JDK1.2之后定义了4种强度的引用

强引用:类似new,只要强引用在就不会回收

软引用:有用但不是必须的对象,内存溢出前回收

弱引用:非必须的对象,生存到下次垃圾回收

虚引用:唯一的目的是能在对象被回收时收到一个系统通知

(4) 标记过程

要宣告一个对象死亡需要经历两次标记:

可达性分析没有与“GC Roots”的引用链,第一次标记并且筛选,筛选条件为是否有必要执行finalize()方法,当对象没有覆盖此方法或者已经被虚拟机调用过此方法,(虚拟机只会调用一次)则认为没有必要执行。

有必要执行的对象放置在F-Quene队列,虚拟机自动建立一个低优先级的Finalizer线程去执行。如果对象在finalize()中建立了引用链关联,第二次标记时将被移出此队列。

(5) 方法区回收

在永久代的垃圾收集率效率非常低,主要回收内容为:废弃常量、无用的类。

在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

  堆=新生代+老年代,比例的值为 1:2 ,不包括永久代(方法区)。
  1. 垃圾收集算法(思想)

(1)标记-清除算法

分为“标记”和“清除”两个阶段:标记出所有需要回收的对象,标记完成后统一收回。

缺点: 效率低,标记和清除过程效率都不高

容易产生大量不连续的内存碎片

(2)复制算法

将内存分为两个大小相同的部分,每次使用其中一块,用完后将存活的对象复制到另一块,并且清理已经使用过的部分。

缺点: 内存缩小为一半,代价太大

改进: 将内存分为Eden空间和两块Survivor空间(8:1:1)

每次使用Eden和其中一个Survivor,回收时将存活的对象复制到另一个Survivor空间,清理掉原来的Eden和其中一个Survivor空间,将两个Survivor角色互换。Survivor空间不够时,进行分配担保(从老年代分配内存空间)。

(3)标记-整理算法

标记后让存活的对象向一端移动,然后清理端边界以外的内存。

(4)分代收集算法

将Java堆分为新生代和老年代,采用不同的算法。新生代——“复制”,老年代——“标记清除”或者“标记整理”

  1. HotSpot的算法实现

(1)枚举根节点

问题:GC Roots节点多,逐个检查消耗时间多;GC停顿(stop the world,可达性分析需要 确保一致性,分析过程引用关系不变化)

解决:准确式GC,使用一组OopMap的数据结构让虚拟机什么地方存放着对象引用。

(2)安全点(Safepoint)

问题:可能导致引用关系(OopMap)变化的指令非常多,需要大量内存生成OopMap

解决:安全点,只在特定的位置记录信息。程序执行时,只有达到安全点才能暂停开始GC

(安全点选定以程序“是否具有让程序长时间执行的特征”为标准,如方法调用、循环跳转)

抢先式中断:(一般不采用)中断所有线程,恢复没有到达安全点的线程,等待其到安全点

主动式中断:设置一个标志,线程主动轮询这个标志,发现为真时自己中断挂起。

轮询标志位置:安全点+创建对象需要分配内存的地方

(3)安全区域(Safe Region)

安全区域:在一段代码中,引用关系不发生变化。(可以看作安全点的扩展)

程序执行到Safe Region首先标识,此时虚拟机如果发起GC,就无需管已经标识了的线程。离开Safe Region时,检查系统是否已经完成根节点枚举,否则等待可以安全离开的信号。

  1. 垃圾收集器

(1)Serial收集器

最基本、发展历史最久远的单线程收集器。虚拟机运行在Client模式下的默认新生代收集器。工作时必须暂停其他所有工作线程直到收集结束。简单高效。

新生代——“复制”,老年代——“标记整理”

(2)ParNew收集器

Serial的多线程版本,使用多条线程进行垃圾收集。运行在Server模式下的虚拟机首选的新生代收集器,能与CMS配合使用。

(3)Parallel Scavenge收集器

使用“复制”算法的新生代、并行多线程收集器。(吞吐量优先处理器)

目的:达到一个可控的吞吐量(运行用户代码时间与总消耗时间的比值),高效率的利用CPU时间。

可以使用GC自适应的调节策略。

*停顿时间短适合需要与用户交互的程序,良好的相应速度能提升用户体验

高吞吐量高效利用CPU时间,尽快完成程序运算,适合后台不需要太多用户交互的程序。

(4)Serial Old收集器

Serial的老年代版本,使用单线程和“标记-整理”算法。主要适合Client模式下的虚拟机。在Server模式下两个用途:JDK1.5之前与Parallel Scavenge配合使用;作为CMS的后备预案。

(5)Parallel Old收集器

Parallel
Scavenge的老年代版本,使用多线程和“标记-整理”算法。

(6)CMS收集器

CMS(Concurrent Mark Sweep)以获取最短回收时间停顿为目标。很大部分应用在互联网或者B/S系统的服务端上,以给用户带来较好的体验。用于老年代,基于“标记-清除”算法,分为4个步骤:

初始标记:标记GC Roots能直接关联的对象,速度快(停顿)

并发标记:进行GC Roots Tracing(并发)

重新标记:修正并发标记期间用户程序运行导致标记发生变动的对象的标记(停顿)

并发清除:(并发)

其中,初始标记、重新标记需要“Stop the world”,并发标记、并发清除耗时长,但是可以和用户线程一起工作。

优点: 并发收集、低停顿

缺点: ①对CPU资源要求高,并发阶段占用部分线程导致程序缓慢

②无法处理浮动垃圾(清理阶段用户程序还在运行),需要预留一部分内存满足程序需要,否则抛出“Concurrent Mode Failure”失败,启动Serial Old重新收集老年代的垃圾。

③“标记-清除”算法容易产生大量碎片,可设置碎片整理开关(默认开启)

(7)G1收集器

面向服务端,将整个Java堆划分为多个大小相同的独立区域(Region),新生代和老年代不再物理隔离。G1跟踪各个Region里的垃圾堆积的价值大小,在后台维护一个列表,每次根据允许的时间回收价值最大的Region。具有以下特点:

●并行和并发:充分利用多CPU、多核环境,缩短停顿时间,通过并发的方式让java程序继续执行。

●分代收集:对不同对象使用不同的收集方式

●空间整合:整体看“标记-整理”,局部看“复制”,不会产生内存碎片

●可预测的停顿:建立可预测的停顿时间模型,让使用者明确指定在一段时间内,消耗在GC上的时间不得超过限定值。

Region之间同样存在对象引用,使用Remembered Set来避免全堆扫描。每个Region都有一个对应的Remembered Set,虚拟机发现程序在对Reference类型数据写操作时,产生一个Write Barrier暂时中断写操作,检查引用的对象是否在不同的Region之间,是的话通过CardTable将相关引用信息记录到被引用对象所属Region的Remembered Set中。

G1收集器的运行步奏(不计算维护Remembered Set):

初始标记:标记GC Roots能直接关联到的对象,修改TAMS值(停顿)

并发标记:可达性分析(并发)

最终标记: 虚拟机并发标记期间产生标记变化的对象记录在线程的Remembered Set Logs里,将Remembered Set Logs数据合并到Remembered Set中(停顿)

筛选回收:对各个Region回收价值和时间成本进行排序,根据停顿时间制定回收计划(停顿)

  1. 内存分配与回收策略

●对象优先在Eden中分配

大多数情况下,对象在新生代Eden中分配,Eden没有足够空间时虚拟机发起一次Minor GC。

●大对象直接分配在老年代

设置参数,使得大于设置值的对象(长字符串、数组)在老年代分配,避免Eden和Survivor之间发生大量复制。

●长期存活的对象进入老年代

对象年龄计数器,从Eden进入Survivor中年龄设为1,每经过一次Minor GC年龄+1,到达一定值(默认15,可设置)时复制到老年代中。

●动态对象年龄判断

在Survivor中,如果相同年龄的对象大小总和大于Survivor空间的一般,年龄大于或者等于此年龄的对象直接进入老年代。

●空间分配担保

在Minor GC前虚拟机会检查老年代可用的连续空间是否大于新生代对象总空间,条件成立则Minor GC是安全的。如果不成立,查看是否允许担保失败(HandlePromotionFailture)。允许的话继续检查最大可用连续空间是否大于历次晋升到老年代的平均值,如果大于将尝试一次Minor GC,小于或者不允许冒险时改为Full GC。

否

















是








                            


















是否大于历次晋升到老年代的对象平均值

四、Class类文件结构

平台无关性和语言无关性的基石是虚拟机和字节码存储格式。

Class类文件:以8位字节为基础单位的二进制流,采用类似c语言的伪结构,只有两种数据类型:

无字符数:基本数据类型,可以描述数字、索引引用、数量值、UTF-8字符串等

表:由多个无符号数或者其他表作为数据项构成的复合数据类型

  1. 魔数与Class文件版本

魔数:每个Class文件的头4个字节,确定文件是否能被虚拟机接受。

Class文件的魔数为:0xCAFEBABE

Class版本: 紧接着魔数的4个字节为Class文件的版本号

5、6字节为次版本号,7、8字节为主版本号

  1. 常量池

Class文件中的资源仓库,常量池的入口设置u2类型的数据代表容量计数器(从1开始)

常量池主要放置两大类:字面量、符号引用

常量池中每一项常量都是一个表,JDK1.7定义了14种结构的表,可以使用javap工具来解析Class文件字节码。

  1. 访问标志

常量池后面两个字节代表访问标志(access_flags),用于表示一些类或者接口层次的访问信息。包括:类还是接口、是否public、是否abstract、类是否为final等。

一共16个标志位,使用了其中8个,其余为0。

  1. 继承关系

类索引和父类索引是u2类型的数据,接口索引集合为一组u2类型数据的集合。

类索引用于确定类的全限定名称,父类索引用于确定其父类的全限定名称,接口索引集合用来描述实现了哪些接口。

(索引值指向常量池)

  1. 字段表集合

字段表用于描述接口或者类中声名的变量。可以包括:字段的作用域(public/…)、实例变量还是类变量(static)、可变性(final)、并发可见性(volatile)可否序列化、字段数据类型(基本类型、对象、数组)、字段名称。

布尔类型使用标志位,名称、数据类型等引用常量池。

  1. 方法表集合

与字段表集合类似。方法中的代码编译后存放在方法属性表中名为“Code”的属性里。

  1. 属性表集合

Class文件、字段表、方法表都可以带自己的属性表集合。最新的《Java虚拟机规范(Java SE 7)》中,属性有21项。

(1)Code属性

方法体中的代码经过编译以后变为字节码指令存储在Code属性中。

在字节码之后是方法的显式异常处理表集合

(2)Exception属性

与Code属性平级,列举方法中可能抛出受查异常(throws后面列举的异常)

(3)LineNumberTable属性

描述Java源码行号与字节码行号的对应关系

(4)LocalVariableTable属性

描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系

(5)SourceFile属性

记录生成这个CLass文件源码文件的名称

(6)ConstantValue属性

通知虚拟机自动为静态变量赋值

(7)InnerClasses属性

记录内部类和宿主类之间的关联

。。。。。。

五、虚拟机类加载机制

虚拟机的类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成被虚拟机直接使用的Java类型。

Java动态扩展的语言特性,依赖运行期动态加载和动态连接的特点。

类的生命周期:

加载 → 验证 → 准备 →
解析 → 初始化
→ 使用 → 卸载

连接
  1. 类加载的时机

加载、
验证、 准备、
初始化和卸载按顺序开始(相互交叉的混合式进行)。解析阶段可以在初始化阶段之后(动态绑定)。

虚拟机规定有且只有五种情况必须立即对类初始化(主动引用):

·遇到new、getstatic、putstatic或者invokestatic字节码指令

·使用java.lang.reflect包的方法反射调用

·初始化类时,其父类没有初始化

·虚拟机启动时,需要初始化主类

·使用JDK1.7的动态语言支持,java.lang.invoke.MethodHandle实例的解析结果为REF_getStatic、REF_putStatic、REF_invokeStaticd的方法句柄,并且所对应的类没有进行过初始化。

(如果是接口,第三条不要求父接口全部完成初始化)

  1. 类加载的过程

(1)加载

虚拟机完成三件事情:通过类的全限定名获取定义此类的二进制字节流;将字节流代表的静态存储结构转化为方法区的运行时数据结构;在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类各类数据的访问入口。

(非数组类的加载阶段可控性最强;数组类不通过类加载器,由虚拟机直接创建)

(2)验证

确保Class文件的字节流中包括的信息符合虚拟机的要求。包括4个检验动作:

·文件格式验证:验证字节流是否符合Class文件规范

·元数据验证:对字节码描述的信息语义分析,确保符合Java语言规范

·字节码验证:对类的方法体进行验证

·符号引用验证:对类自身外(常量池中的符合引用)的信息进行匹配性校验(解析)

(3)准备

为类变量分配内存并设置类变量初始值。

注意: 只有类变量分配内存(static修饰),实例变量将在对象实例化时再分配内存;

初始值通常为零值(除了在字段属性表里ConstantValue属性中已经指定了初始值)

(4)解析

虚拟机将常量池内的符号引用替换为直接引用。解析动作主要针对7类符号引用:类和接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符。

除了invokedynamic指令外,可以对解析结果进行缓存。

(5)初始化

执行类构造器()方法。()方法由编译器自动收集类中所有类变量的赋值动作和静态代码块(static{
})中的语句合并产生。虚拟机保证线程安全,父类方法先于子类。

  1. 类加载器

类加载器:把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到虚拟机外部去实现,让应用程序自己决定如何获取需要的类。(加载阶段)

类加载器的分类:

(默认)

(1)类与类加载器

(2)双亲委派模型

六、虚拟机字节码执行引擎

物理机和虚拟机都有执行代码的能力,物理机的执行引擎建立在处理器、硬件、指令集和操作系统层面;虚拟机的执行引擎由自己实现,可以自行制定指令集与执行引擎的结构体系,能执行不被硬件直接支持的指令集格式。

不同虚拟机在执行代码时可能会有解释执行,编译执行或者两者兼具,甚至包含几个不同级别的编译器执行引擎。但是从外观看,Java虚拟机规范制定了虚拟机字节码执行引擎的一致性:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

  1. 运行时栈帧结构

栈帧是虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区的虚拟机栈的栈元素,包括:局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。

(1)局部变量表

用于存放方法参数和方法内部定义的局部变量。(Java程序编译为Class文件时在Code属性的max_locals中确定了该方法需要的最大内存)

局部变量表的容量为变量槽Slot为单位,一个Slot可以存放32位以内的数据,对于64位的数据类型以高位对齐的方式分配2个连续的Slot空间(线程私有数据,读写两个连续的Slot不会造成线程安全问题)。

虚拟机通过索引定位的方式使用局部变量表。

为节约栈帧空间,Slot可以重用。当前字节码计数器超过了某个变量的作用域,那么这个Slot就可以交给别的变量使用。(可能会对垃圾回收产生影响)

局部变量必须赋值后才能使用。(类变量会在准备阶段初始化为零值,不赋值也能使用)

(2)操作数栈

即操作栈,后入先出,最大深度在编译时写入Code属性的max_stacks中。方法开始执行时操作栈为空,方法执行过程中字节码指令向操作栈中写入/读取内容,即出栈/入栈。

在概念模型里,栈帧完全独立。但是虚拟机实现时会将两个栈帧的一部分重叠(下面栈帧的部分操作栈与上面栈帧的部分局部变量表重叠),共用部分数据,无需额外的参数复制。

以两个整数相加为例:

(3)动态连接

每个栈帧包含一个指向运行时常量池中该栈帧所属方法的引用,以支持方法调用过程中的动态连接。

(4)方法返回地址

一个方法开始后只有两种方法可以退出:①正常完成出口:执行引擎遇到方法返回的字节码指令,可能有返回值传递给上层调用者;②异常完成出口:在方法执行过程中遇到异常并且在方法体内没有得到处理。

方法退出后需要返回到方法被调用的位置,一般来说方法正常退出时调用者的PC计数器可以作为返回地址,异常退出时,返回地址要通过异常处理器表确定。

  1. 方法调用

确定被调用方法的版本(调用哪个方法),不等于方法执行,暂时不涉及方法内部执行过程。

(1)解析

方法在程序运行前就有确定的调用版本,在运行期不可改变,这类方法的调用称为解析。

例如:静态方法、构造器方法、私有方法、父类方法、final修饰方法(不能重写其他版本)

(2)分派

分派调用可能是静态的也可能是动态的,分为静态单分派、静态多分派、动态单分派、动态多分派。

Java语言是一门静态多分派、动态单分派的语言。

① 静态分派

编译阶段,Javac编译器根据参数的静态类型决定使用哪个版本的重载版本。所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。

编译器能确定方法的重载版本,但不是唯一的,选择“更加合适”的版本。

② 动态分派

动态分派:运行时期根据实际类型确定方法执行版本的分派过程。(重写)

③ 单分派和多分派

方法的接受者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以分为单分派和多分派。

(3)动态语言支持

JSR-292
invokeDynamic 对非Java语言的调用支持

  1. 基于栈的字节码解释执行引擎

(1)解释执行

Java中Java编译器完成了源码到抽象语法树,再遍历语法树生成线性的字节码指令流的过程,这部分动作在虚拟机外完成。解释器在虚拟机内部,Java程序的编译属于半独立的实现。

(2)基于栈的指令集

Java编译器输出的指令流基本上是一种基于栈的指令集架构。指令流中的指令大部分都是零地址指令,依靠操作数栈进行工作。(x86等支持指令集基于寄存器,依赖寄存器工作)

基于栈的指令集的优点是可移植,不依赖硬件提供的寄存器;缺点是执行速度相对稍慢。

七、Java内存模型与线程

并发应用场景: 充分利用处理器速度(CPU和存储速度差距大)

一个服务端同时对多个客户端提供服务(每秒事务处理数TPS)

硬件效率: 增加高速缓存(缓存一致性问题:处理器访问缓存协议)

处理器对输入代码进行乱序执行优化

内存模型:可以理解为在特定操作协议下对特定内存或者高速缓存进行读写访问的抽象过程

  1. Java内存模型

Java的并发使用的是共享内存模型,线程之间的通信隐式的进行。

(1)主内存与工作内存

Java内存模型规定所有的变量存储在主内存(Java内存的一部分,类比硬件内存),每个线程有自己的工作内存(类比高速缓存)。工作内存保存该线程使用到变量的主内存拷贝,线程对变量的操作在工作内存中完成。

注:这里的变量指的是实例字段、静态字段和构成数组对象的元素(堆内存),不包括局部变量、方法参数、异常处理器参数,后者是线程私有,不存在共享问题。

(2)内存间的相互操作

关于主内存和工作内存之间具体的交互协议,Java内存模型定义了8种操作,虚拟机需要保证每一种操作的原子性:

Lock(锁定):主内存,把变量标识为线程独占

Unlock(解锁):主内存,将处于锁定的变量释放

Read(读取):主内存,将一个变量值从主内存传输到线程的工作内存以便load操作

Load(载入):工作内存,把read操作从主内存得到的变量值放入工作内存变量副本

Use(使用):工作内存,把工作内存中变量值传递给执行引擎

Asssign(赋值):工作内存,把执行引擎收到的值赋给工作内存

工→主

Store(存储):工作变量,把工作内存变量值传送到主内存,以便write操作

Write(写入):主内存,把store操作从工作内存获得的变量值放入主内存中的变量

执行8种基本操作时需满足一些规则:

(3)volatile型变量的特殊规则(略)

Java虚拟机提供的最轻量级的同步机制。

Volatile变量具备两种特性: 保证此变量对所有线程的可见性(不保证原子性)

禁止指令重排序优化(会干扰程序的并发执行)

   ·对单个volatile变量的读写具有原子性,不包括volatile变量的组合

·Volatile与锁的选择依据:volatile能否满足场景需求

·64位的long和double一般不需要声明为volatile,在64位JVM中double和long的赋值操作是原子操作(JDK5开始:写操作可以拆分2个32位,读操作必须具有原子性)

·除了volatile,synchronized和final也能实现可见性

·volatile和synchronized可以保证线程之间的有序性

·并发中的3个重要特性:原子性、可见性、有序性

① Volatile的实现原理:

有volatile修饰共享变量进行读写操作时会生成lock前缀的指令,完成两件事:

1)将当前处理器缓存行的数据写回系统内存

2)使在其他CPU缓存了该内存地址的数据无效

② 追加字节性能优化:使用追加到64字节的方式来填满高速缓存行,避免头节点和尾节点加载到同一行,使用时不会相互锁定。

(如果队列的头尾节点都不足64字节,处理器会将他们读到同一个高速缓存行,一个处理器修改头节点时会将整行锁定,其他处理器无法操作尾节点。)

(4)先行发生原则(happens-before)

JDK5以后,判定数据是否存在竞争、线程是否安全的主要依据。先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其实就是说在操作B之前,操作A产生的影响能被B观察到。

Java内存模型中“天然”存在的先行发生关系,无须任何同步

         ·程序次序规则:一个线程内按照控制流顺序,写在前面的操作先于写在后面的

·管程锁定规则:同一个锁的unlock先于时间在后面的lock

·volatile变量规则:对volatile的写操作先于时间在后面的读操作

·线程启动规则:Thread对象的start方法先于此线程的每个动作

·线程终止规则:线程所有的方法先于此线程的终止检测

(可以通过join方法结束、isAlive的返回值等检测到线程已经终止)

·线程中断规则:对线程的interrupt方法的调用先于被中断线程的代码检测到中断

·对象终结规则:一个对象的初始化完成先于finalize方法

·传递性:A先于B,B先于C,则A一定先于B

例一: 以下三个操作分别在三个线程中执行,A、B之间具有先行关系

i=1;
//操作A

j=i;
//操作B

i=2;
//操作C

结论:A先于B执行,但是B和C之间没有先行关系,C可能会对B有影响(j=2)

例二: 线程A和B分别调用setValue(1)和getValue方法。

private int value =0;

public void setValue(int value){

this.value=value;

}

public int getValue(){

return value; }

结论:A时间上在B之前,不满足先行发生原则,线程不安全,不能保证读到的是1.

解决:getter/setter方法定义为synchronized方法;value定义为volatile变量

例三: 以下操作在同一线程中执行

int i=1; //操作A

int j=2; //操作B

结论:满足先行发生原则,但是A不一定时间上在B之前发生(重排序优化)

(5)重排序

编译器优化重排序、指令级并行重排序、内存系统重排序。前者属于编译器重排序,后面两个属于处理器重排序。

Java内存模型通过禁止特定类型的编译器和处理器重排序(插入内存屏障)为程序员提供一致的内存可见性保证。在不改变(正确同步的)程序执行结果的前提下,尽可能的为编译器和处理器优化打开方便之门。

  1. Java与线程

并发不一定要依赖多线程,但是在Java中大多数都与线程相关。

(1)线程的实现

线程是CPU调度的基本单位。Java语言提供了在不同硬件和操作系统平台下对线程操作的统一处理,每个已经执行start()方法且还未结束的java.lang.Thread类的实例代表一个线程,它的所有关键方法声明为Native(使用平台相关的方法实现)。实现线程主要有3种:

① 使用内核线程实现

内核线程就是直接由操作系统内核支持的线程,由内核来完成线程切换,通过操纵调度器对线程进行调度等。

程序一般使用轻量级进程(即线程,内核线程的高级接口),每个轻量级进程都有一个内核线程支持,这种轻量级进程和内核线程1:1关系称为一对一的线程模型。

优点:每个轻量级进程成为一个独立的调度单元

缺点:基于内核线程,需要系统调用的代价相对较高,需要在用户态和内核态切换

轻量级进程需要内核支持,消耗一定的内核资源

② 使用用户线程实现

广义上只有不是内核进程的线程都是用户线程,狭义上指完全建立在用户空间的进程库上。不需要内核帮助,不切换到内核态,速度快且低消耗。可以支持更大规模的线程数量,一般也较为复杂。进程与用户线程之间1:N的关系称为一对多的线程模型。

③ 使用用户线程加轻量级进程混合实现

轻量级进程作为用户线程和内核线程的桥梁。多对多的线程模型。目前JDK支持什么样的线程模型在虚拟机规范中并未规定。

(2)Java线程调度(Java使用抢占式线程调度)

线程调度指系统为线程分配处理器使用权,主要有协同式线程调度、抢占式线程调度。

协同式线程调度,线程的执行时间由线程本身决定不可控,执行完成主动切换到另外的线程,没有线程同步问题,容易阻塞;抢占式线程调度,每个线程将由系统分配执行时间。

Java语言设置10个级别的线程优先级,当两个线程同时处于Ready状态时,优先级越高的线程越容易被系统选择执行。(线程优先级和系统优先级不能对应时,两个线程的优先级可能会变成相同;系统可能被系统自行改变,例如Windows的“优先级推进器”)

(3)状态转换

Java定义了5(6)种线程状态:

新建(new):创建后尚未启动

运行(Runable):包括系统线程状态的Running、Ready,即运行或者等待CPU分配执行时间

无限期等待(Waiting):等待被其他线程显示地唤醒

限期等待(Timed Waiting):在一定时间后自动唤醒

阻塞(Blocked):等待获取排它锁(另一个线程放弃这个锁是发生)

(结束(Terminated):已经终止的线程状态)

八、线程安全与锁优化

  1. 线程安全

当多个线程访问一个对象时,如果不需要考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行其他协调操作,调用这个对象的行为都是可以获得正确的结果,那么这个对象就是线程安全的。

(1)Java语言中的线程安全

按照线程安全的“安全程度”由强到弱,将Java语言中操作的共享数据分为:

不可变:一定线程安全(如基本数据类型使用final修饰,java.lang,String类)

绝对线程安全:严格定义。Java API标注线程安全的类大多数不是绝对的线程安全,例如Vector、HashTable等。

相对线程安全:通常意义上的线程安全,对于特定顺序的连续调用可能需要在调用端使用额外的同步手段。

线程兼容:对象本身线程不安全,但是在调用端使用额外的同步手段保证对象在并发环境中安全使用。

线程对立:无论在调用端是否使用同步手段都无法在多线程环境中并发使用。

(2)线程安全的实现方法

  除了代码编写,虚拟机提供的同步和锁机制也起到非常重要的作用。

① 互斥同步(阻塞同步)

实现手段:synchronized关键字(JVM)、ReentrantLock重入锁(Lock的实现类,JDK)

synchronized关键字是Java语言中重量级的操作,synchronized同步块对同一线程可重入,同步块在已经进入的线程执行完成前会阻塞后面的线程(阻塞线程需要系统协助,从用户态切换到核心态,消耗处理器时间)。虚拟机优化:在阻塞前加入一段自旋等待过程。

Synchronized用的锁存在Java对象头中。Java中每个对象都可以作为锁,具体为:

·普通同步方法,锁是当前实例对象

·静态同步方法,锁是当前类的Class对象

·同步方法块,锁是Synchronized括号里配置的对象

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步使用monitorenter和monitorexit指令实现,方法同步使用另一种方法(也可以使用这两个指令)。

编译后,monitorenter指令插入到同步代码块的开始位置,monitorexit插入到方法结束处和异常处,每个monitorenter必须要对应的monitorexit与之匹配,任何对象有一个monitor与之关联,当且仅当monitor被持有后,它将处于锁定状态。线程执行monitorenter时将会尝试获取所对应的monitor所有权(锁)。

ReentrantLock重入锁在基本用法上和synchronized类似,增加了高级功能:

等待可中断:正在等待的线程可以选择放弃等待

公平锁:通过构造函数实现,多个线程等待同一个锁时按申请时间顺序依次获得锁

锁可以绑定:ReentrantLock对象可以同时绑定多个Condition对象

注:优先考虑synchronized(synchronized隐式的支持重入)

② 非阻塞同步

基于冲突检测的并发策略。先进行操作,如果没有其他线程竞争就成功了;如果共享数据有争用产生冲突再采取补偿措施(比如不断重试)。

需要操作和冲突检测具备原子性,依赖硬件指令集的发展。

③ 无同步方案

安全的代码:

可重入代码:不依赖存储在堆上的数据和公共系统资源,不调用非可重入方法等

线程本地存储:使用java.lang.ThreadLocal类

  1. 锁优化

① 自旋锁与自适应自锁

自旋:在请求锁的线程“稍等”但不放弃处理器时间,看看持有锁的线程是否很快释放锁,即让线程执行一个忙循环(自旋)。自旋次数默认为10,对于锁被占用时间很短的情况效果很好,避免用户态和内核态切换。

自适应自旋:自旋时间不固定,根据前一次同一个锁的自旋时间和锁拥有者的状态决定。

② 锁消除

虚拟机即时编译器在运行时,对代码上要求同步但是被检测到不可能存在数据共享的锁进行消除。主要判断依据来源于逃逸分析的数据支持。

③ 锁粗化

原则上对同步块的范围尽可能小,但是如果对同一个对象反复加锁和解锁可以把加锁同步的范围扩展(粗化)到整个操作序列外部。

④ 轻量级锁

没有多线程竞争的情况下,减少传统重量级锁使用操作系统互斥量产生的性能消耗。

加锁过程:对象头中有2bit用于存储锁标志位,代码进入同步块后对象没有被锁(01),虚拟机在线程的栈帧中创建一个Lock Record空间,存储对象头中MarkWord的拷贝。然后使用CAS尝试将对象的MarkWord更新为指向LockRecord的指针,如果成功则拥有锁,标志位置位00,失败则检查对象的MarkWord是否指向当先线程的栈帧,是则说明当前线程已经拥有锁,否则说明这个锁对象已经被其他线程抢占。

如果有两个以上线程竞争同一个锁,则膨胀为重量级锁,标志位置10,MarkWord存储指向重量级锁的指针,后面的线程进入阻塞状态。

⑤ 偏向锁

消除数据在无竞争情况下的同步原语(操作系统或计算机网络用语范畴,是由若干条指令组成的,用于完成一定功能的一个过程),把整个同步去除,进一步提高程序的运行性能。

锁偏向于第一个获得它的线程,如果该锁没有被其他线程获得,则持有偏向锁的线程永远不需要再进行同步。是一种带有效益权衡性质的优化,并不一定总对程序有利,如果总是被多个线程访问则偏向模式就是多余的。

·Java SE1.6锁一共4个级别:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。

锁可以升级但是不能降级

·三种锁的比较

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值