虚拟机读书笔记

参考

深入理解Java虚拟机:JVM高级特性与最佳实践

介绍操作系统内存管理的微信公众号文章
https://www.zhihu.com/question/27871198

Java内存区域与内存溢出

程序计数器

Java虚拟机栈
线程私有的,它的生命周期与线程相同。是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储变量表,操作数栈,动态链接,方法出口等信息。

本地方法栈
与虚拟机栈的区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

Java堆
堆是被所有线程共享的一块内存区域,此内存区域唯一目的就是存放对象实例

方法区
是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

运行时常量池
是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项常量池,用于存放编译生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行是常量池中存放

直接内存

对象的创建

对象的内存布局
对象在内存中存储的布局可以分为3块区域:对象头(Header)、实时数据(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机的对象头包含两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态、线程持有的锁、偏向锁ID、偏向时间戳,这部分数据在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称之为(Mark Word)。
另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

OutOfMemoryError异常
Java堆溢出
Java堆用于存储对象实例,只要不断创建对象,并且保证GCRoots到对象之间有可达路径来避免垃圾回收机制清楚这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常

虚拟机栈和本地方法栈溢出
1 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
2 如果虚拟机在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
当栈空间无法继续分配内存时,到底是内存太小,还是已使用的栈空间太大,其本质是对同一件事情的两种描述。

方法区和运行时常量池溢出

本机直接内存溢出

垃圾回收器与内存分配策略

概述

程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭。

堆区和方法区
一个接口的多个实现类需要的内存不一样
一个方法中的多个分支需要的内存也不一样
我们只有在程序处于运行期间时才能直到会创建那些对象,这部分内存分配和回收都是动态的

对象已死吗

在堆里面存放着Java世界中几乎所有的对象实例,垃圾回收器在对堆会回收前,第一件事情就是要确定这些对象之中那些还存活着,哪些已经死去(即不可能在被任何途经使用的对象)

引用计数

给对象中添加一个引用计数器,每当有一个地方引用他时,计数器的值就加1;当引用失效时,计数器的值就减1;任何时刻计数器为0的对象就是不可能再被使用的
Java虚拟机里面没有选用引用计数算法来管理内存,其中主要原因是它很难解决对象之间的循环引用问题
对象ObjA和ObjB都有字段instance,赋值令ObjA.instance = ObjB,ObjB.instance = ObjA

可达性分析

用途:判断对象是否存活
算法基本思想:GC Roots 对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径称之为Reference Chain,当一个对象到GC Roots 没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象不可用。
可以作为GC Roots的对象包含以下几种
1 虚拟机栈(栈帧中的本地变量表)中引用对象
2 方法区中的类静态属性引用对象
3 方法区中常量引用的对象
4 本地方法栈中JNI引用的对象

再谈引用

1 强引用就是指在程序代码中普遍存在的,类似 Object obj = new Object() 这类引用,只要强引用还在,垃圾收集器永远不会回收掉对象的引用
2 软引用时用来描述一些有用但并非必须的对象,对与软引用关联着的对象,在系统发生内存溢出异常之前,会将这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出雷村溢出异常。
3 弱引用也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存在下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否存足够,都会回收掉只被弱引用关联的对象
4 虚引用,他是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用的唯一目的就是能在这个对象被垃圾回收时受到一个系统通知。

生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是非死不可,要宣告一个对象的死亡,至少要经历两次标记过程:
1 如果对象在进行可达性分析后没有GC Roots相连接的引用连,那它将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况视为没有必要执行。如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列中,并在稍后由一个虚拟机自动建立的,优先级的Finalizer线程去执行它。
GC对F-Queue中的对象进行第二次小规模标记

回收方法区

永久代的垃圾回收主要回收两部分内容
1 废弃常量
2 无用的类
判定一个类是否是无用类
1 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
2 加载该类的ClassLoader已经被回收
3 该类对应的java.lang.Class对象没有任何地方被引用到,无法在任何地方通过反射访问该类的方法

垃圾回收算法

标记清楚算法
不足
1 效率问题
2 标记清除后产生大量不连续的内存碎片

复制算法
将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块使用完,就将还存活着的对象赋值到另一块上面

标记整理
让所有存活的对象都向一端移动,然后清楚掉边界以外的内存

分代收集算法
根据对象的存活周期不同将内存划分为几块。一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾收集都发现偶大批对象死去,只有少量存活,选用复制算法。老年代中因为对象存活率高,没有额外的空间对它进行担保,就必须使用标记-清理或者标记-整理来进行回收

HotSpot 的算法实现

枚举根结点

安全点
HotSpot使用一组称为OopMap的数据结构来存放对象的引用,在OopMap的协助下,HotSpot可以快速准确的完成GC Roots枚举。
HotSpot只是在特定的位置记录,这些位置称之为安全点
如何在GC发生时让所有线程都跑到最近的安全点
1 抢先中断 不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让他跑到安全点上
2 主动中断 当GC需要中断的时候,不直接堆线程操作,仅仅简单的设设置一个标志,各个线程执行时主动去轮询这个标志。发现中断标志为真时就自己中断挂起。轮询标记的地方和安全点重合。

安全区
安全区是指在一段代码片段之中,引用关系不会发生变化,在这个区域中任意地方开始GC都是安全的。

垃圾收集器
Serial 收集器
单线程收集器,使用一个CPU或一条收集线程区完成垃圾收集工作。必暂停其他所有的工作线程,直到它收集结束

ParNew 收集器
多线程
并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
并发:指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上

Parallel Scavenge 收集器
复制算法的收集器
并行的多线程收集器
CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程停顿的时间,而 Parallel Scavenge 收集器的目表则是达到一个可控的吞吐量
吞吐量:运行用户代码时间/(运行用户代码时间+垃圾收集时间)

Serial Old 收集器
使用标记-整理算法

Parallel Old 收集器
标记-整理
多线程

CMS(Concurrent Mark Sweep)收集器
获取最短回收停顿时间为目标的收集器,基于标记-清除,整个过程分为4个步骤
1 初始化标记 仅仅只标记GC Roots 能直接关联的对象,速度很快
2 并发标记 进行GC Roots Tracing(追踪)的过程
3 重新标记 修正并发标记期间因用户程序继续运作导致标记产生变动的那一部分对象的标记记录,比并发标记时间短
4 并发清除
缺点
CMS收集器对资源非常敏感
CMS收集器无法处理浮动垃圾,可能出现(Concurrent Mode Failure)失败导致另一次Full GC的产生
CMS是一款基于标记-清除算法实现的收集器,结束时会产生大量碎片

G1收集器
特点
并行与并发
分代收集
空间整合 整体上是基于标记-整理算法实现的收集器,局部 两个 Region(区域) 上是基于复制算法实现的
可预测的停顿 建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒

将整个Java堆划分为多个大小相等的独立区域(Region) ,G1跟踪各个Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需的时间的经验值),在后台维护一个优先列表,每次允许的收集时间,优先回收价值最大的Region
虚拟机使用 Remembered Set 避免全堆扫描,G1 中每个Region 都有一个与之对应的 Remembered Set 虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中

内存分派与回收策略

对象主要分配在新生代的Eden区上,当Eden区没有足够空间进行分配时,虚拟机发起一次Minor GC

大对象直接进入老年代
大对象是指连续内存空间的Java对象

长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年纪(Age)计数器,如果对象在Eden出生并经过第一次Minor GC 后仍然存活,并且能被Survivior容纳的话,将被移动到Survivor空间中,并且对象年龄设为1 ,对象在Survivor区中每熬过一次Minor GC 年龄就增加1岁,当它的年龄达到一定程度时(默认15岁),将会被晋升到老年代中

动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一般,年龄大于或等于该年龄对象就可以直接进入老年代

空间分配担保
在发生Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象空间,如果这个条件成立,那么Minor GC 可用确保是安全的,如果不成立,则须立即会看HandlePromotionFailure设置的值是否允许担保失败

类文件结构

Class类文件的结构
两种数据结构
无符号数,属于基本的数据类型,可用来描述数字,索引引用,数量值或按照UTF-8编码构成字符串
u1 1个字节
u2 2个字节
u4 4个字节
u8 9个字节
表 是由多个无符号数或者其他表作为数据项的符合数据类型
习惯以 _info 结尾
使用一个前置的容量计数器加若干个连续的数据项的形式,称这一系列连续的某一个类型数据为某一类型的集合

字节对应的含义
1~4 标识
5~6 次版本号
7 主版本号

常量池 数量不固定主要存放字面量和符号引用

访问标志 两个字节,用于识别一些类或接口层次的访问信息 一共有16个标志位可以使用,当前之定义了其中8个,没用到的标志位要求一律为0

类索引 一个u2类型的数据
父类索引 一个u2类型的数据
接口索引 一组u2类型的数据集合

字段表集合 用于描述接口或者类中声明的变量

方法集合

属性表集合
code属性

字节码指令简介
虚拟机的指令由一个字节长度的,代表这某种特定操作含义的数字(称为操作码),以及跟随其后的零至多个代表此操作所需参数(操作数)而构成

Java字节码与数据类型

加载和存储指令
用于将数据在栈帧中的局部变量表和操作数栈
将一个局部变量加载到操作栈: iload
将一个数值从操作栈存储到局部变量表:istore
将一个常量加载到操作栈:bipush
扩充局部变量表的访问索引的指令:wide

运算指令

类型转换指令

类的加载过程

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括

加载
在加载阶段虚拟机需要完成以下3件事
1 通过一个类的全限定名称来获取定义此类的二进制文件流
2 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
Class对象比较特殊,它虽然是对象,但是存放在方法区里面,这个对象将作为程序访问方法区中的这些类型数据的外部接口.
加载阶段与连接阶段的不部分内容是交叉进行的,加载尚未完成,连接阶段可能已经开始

验证
验证是连接阶段的第一步,这个阶段的目的是为了确保Class文件和字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全.
验证阶段大致上完成下面4个阶段的检验动作
文件格式验证
元数据验证
字节码验证
符号引用验证

准备
准备阶段是正式成为类变量分配内存并设置类变量初始话值的阶段
基本数据类型的零值

解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用 用一组符号来描述所引用的目标
直接引用 直接指向目标指针,相对偏移量或是一个能间接定位到目标的句柄
虚拟机可以对第一次的解析结果进行缓存,从而避免重复解析动作重复进行

初始化
类的初始化阶段是类加载过程的最后一步
初始化阶段是执行类构造器()方法的过程
()方法是由编译器自动手机类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量
()方法与类构造函数不同,它不需要显示地调用父类构造器
父类的()方法先执行
()方法对类或接口来说并不是必须的
()接口中不能使用静态语句块,但任有变量初始化的赋值操作,执行()不需要先执行父类的方法

使用

卸载

验证,准备,解析部分统称为Linking

只有5种情况必须立即对类进行初始化
1 遇到new getstatic putstatic invokestatic
2 使用java.lang.reflect 包的方法对类进行反射调用的时候
3 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发器父类的初始化
4当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个类
5 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,
REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化

类加载器
从虚拟机的角度讲,只存在两种不同的类加载器
1 启动类加载器,C++语言实现,是虚拟机的一部分
2 所有其他的类加载器,Java语言实现,独立于虚拟机外部并且都继承自抽象类,java.class.loader
启动加载器(Bootstrap ClassLoader)
拓展类加载器(Extension ClassLoader)
应用程序类加载器(Application ClassLoader)

虚拟机字节码执行引擎

运行时栈帧结构

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行数据中的虚拟机栈的栈元素
栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息
每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程

局部栈帧
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量

操作数栈
后入先出栈
当一个方法开始执行的时候,这个方法的操作栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容

动态连接
每个栈都包含一个指向运行时常量池中该栈帧所属方法的引用

方法返回
只有两种方式可以退出这个方法
1 遇到任意一个方法返回的字节码指令
2 在方法执行过程中遇到了异常

附加信息

方法调用

确定被调用方法的版本(即调用哪一个方法)

解析
所有方法调用中的目标方法Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,调用目标在程序代码写好,编译进行编译时就必须确定下来
主要包括静态方法和私有方法两大类
前者与类型直接关联
后者在外部不可访问

invokestatic 调用静态方法
invokespecial 调用实例构造器方法,私有方法和父类方法
invokevirtual 调用所有的虚方法
invokeinterface 调用接口方法,会在运行时再确定一个实现此接口的对象
invokedynamic 先在运行时动态解析出调用点限定符所引用的方法,然后在执行该方法
invokestatic 和 invokespecial 指令调用的方法都可以在解析阶段中确定唯一的调用版本
虽然final方法使用invokevirtual指令来调用的,但是由于无法被覆盖,没有其他版本,所以也无需对犯法接受者进行多态选择

分派
解析调用一定是静态的过程,在编译期间就完全确定
分派调用则可能是静态的也可能是动态的,根据分派一句的宗数量()可分为单分派和多分派

静态单分派
静态多分派
动态单分派
动态多分派

静态分派
所有依赖静态类型来定位方法执行版本的分派动作称之为静态分派.静态分派的典型应用就是方法的重载
静态分派发生在编译阶段,因此确定静态分配的动作实际上不是由虚拟机来执行的

动态分派
重写
运行期间根据实际类型确定方法执行版本的分派过程称为动态分配
invokevirtual指令的运行解析过程大致分为以下步骤
1 找到操作数的栈顶的第一个元素所指向的对象的实际类型,记作C
2 如果在类型C中找到与常量中的描述和简单名称都相符的方法,则进行权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常
3 否则,按照继承关系从下往上一次对C的各个父类进行第2不的搜索和验证
4 如果始终没有找到何时的方法,则排除java.lang.AbstractMethodError异常

单分派和多分派
方法的接受者与方法的参数统称为方法的宗量
根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种
单分派是根据一个宗量对目标方法进行选择
多分派则是根据多于一个宗量对目标方法进行选择

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

动态分配是非常频繁的动作
动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的方法

稳定优先 就是为类在方法区中建立一个虚方法表(接口方法表)
如果子类没有被重写,那子类的虚方法表里的地址入口和父类相同方法的地址入口是一致的
为了程序实现上的方便,具有相同签名的方法,在父类,子类的虚拟方法表中都应当具有一样的索引

动态类型语言支持
什么是动态语言?
动态类型语言的关键特征是它的类型检测的主体过程是在运行期而不是编译期
变量无类型而变量值才有类型

invokedynamic 指令的位置都称作动态调节点
CONSTANT_InvokeDynamic_info常量,从这个新常量可以得到3项信息
引导方法 有固定参数 并且返回值是java.lang.invoke.CallSite对象
方法类型
名称

Refletion(反射)和MethodHandle机制

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

Java内存模型与线程

内存间交互操作

lock(锁定) : 作用于主内存的变量,它把一个变量标识为一条线程独占状态
unlook(解锁) : 作用于主内存的变量,它把一个处于锁状态的变量释放出来,释放后的变量才可以被其他线程锁定
read(读取) : 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入) : 作用于工作内存的变量,它把read操作从主内存中得到的变量放入工作内存的变量副本中
use(使用) : 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引起,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
assign(赋值) : 作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
store(存储) : 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
write(写入) : 作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中

volatile
volatile专门定义了一些特殊的访问规则
当一个变量定义为volatile之后,它将具备两种特性
第一是保证此变量对所有线程的可见性
第二个语义是禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有的依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致

对于long和double型变量的特殊规则

原子性
synchronized块之间的操作具备原子性
可见性
volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新
synchronized和final 实现可见性
synchronized对一个变量执行unlock操作之前,必须先把变量同步回主内存中
被final修饰的字段在构造器中一旦初始话完成,并且构造器没有把 this 的引用传递出去
有序性
volatile和synchronized保证线程之前操作的有序性

先行发送原则

程序次序规则
在一个线程内,按流程代码顺序,书写在前面的操作先发生于书写后面的操作

管程锁定规则 一个unlock操作先行发生于后面对同一个锁的lock操作

volatile 对一个volatile变量的写操作先行发生于后面对这个变量的读操作

线程启动规则 Thread对象的start()方法先行发生与此线程的每一个动作

线程终止 线程中所有操作都先发生于对此线程的终止检测

线程中断规则 对线程interruput()方法的调用先发生于被中断线程的代码检测到中断事件的发生

对象终结规则 一个对象的初始化完成先发生于他的finalize()方法开始

传递性 如果操作A先发行于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生与操作C的结论

java线程
线程是比进程更轻量级的调度执行单位
实现线程主要有3种方式
1 使用内核线程实现
2 使用用户线程实现
3 使用用户线程加轻量级线程进行混合实现
4 java线程的实现

java线程调度
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种
1 协同式线程调度
2 抢占式线程调度

状态转换
5种状态
1 新建 (New) : 创建后尚未启动的线程处于这种状态
2 运行(Runable) :
3 无限等待(Waiting) : 处于这种状态的线程不会被分配CPU执行时间,它们要等待其他线程显示的唤醒
Object.wait()
Thread.join()
LockSupport.pack()
4 有限等待(Time Waiting) 处于这种状态的线程不会分配CPU执行时间,在一定时间后它们会由系统自动唤醒
Thread.sleep()
LockSupport.packNanos()
LockSupport.packUntil()
5 阻塞(Blocked) 线程被阻塞了,在等待获取一个排他锁
6 结束(Terminated)

线程安全与锁优化

线程安全 当多个线程访问一个对象时,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的欣慰都可以获取正确的结果,那这个对象是线程安全的
1 不可变
不可变的对象一定是线程安全的
2 绝对线程安全
3 相对线程安全
4 线程兼容
对象本身并不是线程安全的,但是可以通过调用端正确地使用同步手段来保证对象在并发环境中可以安全的使用
5 线程对立
线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码

线程安全的实现方法
1 互斥同步
synchronized
执行 monitorenter 指令时,首先尝试获取对象锁
如果这个对象没有被锁定,或者当前线程已经拥有了这个对象锁,把锁的计数器加1
相应的monitorexit指令将会锁计数器减一
当计数器为0时,锁就释放了
synchronized 同步线程是可重入的
等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助
公平锁
多个线程在等待同一个锁,必须按照申请锁的时间顺序来依次获得锁;
非公平锁不保证这一点,在锁释放时,任何一个等待的线程都有机会获得锁
synchronized 中的锁是非公平的
ReentrantLock 默认情况下也是非公平的
锁绑定多个条件
是指一个ReentrantLock对象可以同时绑定多个Condition对象

2 非阻塞同步
测试并设置
获取并增加
交换
比较并交换(Compare-and-Swap CAS)
加载链接/条件存储

CAS
CAS指令需要有3个操作数,分别是内存位置(在Java中可以简单理解为变量的内存地址,用V表示)
旧的预期值(用A表示)
新值(用B表示)
CAS指令执行时,当且仅当V符合旧预期值A时,处理器用新值B更新V的值,否则它就不执行更新,但是无论是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作
ABA问题
3 无同步方案
可重入代码
线程本地存储
ThreadLocal

锁优化

自旋锁与适应性自旋
自旋次数默认值是10次
自适应的自旋锁
自适应意味着自旋的时间不在固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就认为这次自旋也有可能再次成功

锁清除
虚拟机即时编译器在运行时,对一些代码上要求同步,但是检测到不可能存在共享数据竞争的锁进行消除

锁粗化
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量的小

轻量级锁
没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的新能消耗
HotSpot虚拟机的对象头(object Header)分为两部分信息
第一部分用于存储对象自身的运行时数据,如哈希码(HashCode),GC分代年龄(Generational GC Age)等
另一部分用于存储指向方法区对象类型数据的指针

偏向锁
消除数据在无竞争情况下的同步语言

Dalvik和ART虚拟机

Dalvik虚拟机的特点
生成Dex文件时,会把所有Class文件整合到一个Dex文件中,同时对所有类进行优化合并,去除冗余的信息
Dex文件会把每个类文件的常量池进行分解合并,最后放在一个常量池中
Dex文件中所有的字符串常量也会整合并成一份
Dex文件中的签名只有一份,验证也只有一次
Dalvik虚拟机为应用创建的线程都是Linux线程,通过Linux的内核来管理线程,不但效率高,也简化了Dalvik虚拟机的设计

即时编译JIT(运行时编译)
是一种在运行时将字节码翻译成机器码的技术
JIT技术的要点是把翻译好的机器码缓存起来,而不是每次都解释,以节约时间

Dalvik启动流程
Dalvik虚拟机在Zygote进程中启动和初始话 AndroidRuntime.cpp的startVM()函数开始启动Dalvik
jni.cpp路径

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值