JVM慢慢磨
2018-11-07 15:30:34 0 举报
AI智能生成
JVM虚拟机的基本实现原理和高效实现
作者其他创作
大纲/内容
模块二:高效实现
Java 语法糖 与Java 编译器
自动装箱、自动拆箱
拆箱——调用Integer.intValue
装箱——调用Integer.valueOf
泛型与类型的擦除
泛型在JVM中变成所限定的继承类
泛型存在性:Java编译器可以根据泛型参数判断程序中的语法是否正确
桥接
泛型父类与之类之间的方法重写
子类方法参数类型为父方法子类
子类方法返回值类型为父方法子类
其他语法糖
变成参数、try-with-resource关闭资源,同一个catch代码块中捕获多种异常,finally代码总被执行设计
foreach 循环允许java 再循环中遍历数组与Iterable对象
数组
Iterable对象
字符串switch 转换成int switch
即时编译,有c1、c2、最新的Graal即时编译器
作用:一项提升应用程勋运行效率的技术、代码一般会被JVM解释执行,之后反复执行的热点代码会被即时编译为机器码,直接运行在底层硬件上
分层编译模式,分为五层 1、4为终止状态
0:解释执行
1、执行不带profiling的c1代码
2、执行仅带方法调用次数以及循环回边执行次数profiling的C1代码
3、执行带有所有profiling的c1代码
4、执行c2代码
编译器线程分布、触发条件
c1:c2 线程数=1:2
对于四核及以上的机器,总的编译线程的数目为:
n = log2(N) * log2(log2(N)) * 3 / 2
其中 N 为 CPU 核心数目。
n = log2(N) * log2(log2(N)) * 3 / 2
其中 N 为 CPU 核心数目。
即时编译器触发条件
除了以方法为调用单位外,还可以利用OSR技术以循环为单位的即时编译
OSR:一种能够在非方法入口处进行解释执行和编译后代码之间切换的技术
即时编译器优化
基于分支profile的优化
根据条件跳转指令的分支profie,即时编译器可以将从未执行的分支减掉,以便节约资源
即时编译器还会比较各个分支的执行概率,以便优化概率高的路径
即时编译器假设仅执行某一分支
基于类型的profile优化
即时编译器假设动态类型仅为类型profile中的那几个
即时编译器去优化
当以上两个假设失败后,JVM就会去优化,即从执行即时编译生成的机器码切换回解释执行
即时编译器的中间表达式
中间表达式(IR)
上边的基于profile的优化,并非实际的优化过程
编译器前端进行词法分析、语法分析、语义分析,生成中间表达式
编译器后端对IR进行优化,生成目标代码
由于Java字节码本身剥离高级的语法,并且基于栈的计算模型容易建模,即时编译器直接将字节码当做一种IR
现代编译器采用一种静态赋值IR(SSA IR),每个变量只能被赋值一次,只有赋值后才能使用
SSA IR 作用:
查找赋值但是没有被使用变量,识别荣誉赋值
在对常量折叠、常量传播、强度消减、死代码删除优化有很大帮助
Java字节码
Java字节码是JVM所使用的指令集,与JVM基于栈的计算模型密不可分
操作数栈
字节码的基本指令
duo:复制栈顶元素,经常用来调用new指令生成位初始化的引用,然后用之调用构造函数
pop:舍弃栈顶元素
swap:交换栈顶两个元素
加载int类型的值:iconst加载-1~5,bipush、sipush加载一个字节、两个字节代表的int值
Idc 加载常量池的值
字节码解释执行过程中,为Java分配栈帧时,会分配操作数栈,存放参数与返回结果
局部变量区
字节码程序可以将计算结果缓存到局部变量区中
实质:JVM将局部变量当成数组,一次存放this指针、参数、字节码中的局部变量
局部变量区的指令
整型对加载指令—iload
整型的存储指令——istore
方法内联
定义:在编译过程中,将目标方法纳入编译范围中,取代原方法调用的优化手段,消除调用带来的开销
调用方法步骤:1、创建并压入方法的栈帧 2、访问字段 3、弹出栈帧 4、恢复当前执行的代码
C2在解析字节码过程中进行方法内联
方法内联实现原理
将被调用方法的IR图节点复制到调用者方法的IR图中
方法内联的条件
内联越多导致生成机器码的时间越长,编译器不会无限制进行方法内联
会被内联
自动拆箱
被@ForceInline 注解 inline指令指定方法
不会被内联
Throwable类方法不能被其他类方法
由dontinline | exclude指令指定,以及由@DontInline注解
调用字节码对应的符号引用未被解析、类未初始化、目标方法时native
动态绑定的虚方法调用需要去虚化
完全去虚化
通过类型推导,类层次分析,识别需方法调用的唯一目标方法,将其转换成直接调用的一种优化方式
条件去虚化
虚方法调用转换成若干个类型测试以及直接调用的一种优化方式,关键在于需要济宁比较的类型
HotSpot虚拟机instrinsic
JVM中使用@HotSpotIntrinsicCandidate 注解的都使用了 HotSpot intrinsic,会被HotSpot虚拟机替换成高效的CPU指令序列
StringLatin1.indexof方法将在一个字符串数组中查找另一个字符串数组,对应CPU指令PCMPESTRI:在16字节以下的字符串中查找另一个16字节以下字符串
整数加法的溢出处理,在cpu 中状态寄存器 FLAGS register 中就有溢出标志位
统计二进制中1的个数,popcnt指令
intrinsic实现
一种独立的桩程序,既可以被解释器利用,直接替换成原方法的调用,也可被即时编译器利用,它把代表原方法IR节点,替换成对桩程序调用IR节点
另一种是编译器在方法内联中将特殊的编译器IR节点,只能被即时编译器利用,即时编译器根据替换的特殊IR节点,生成指定的CPU指令
例如Thread.currentThread 方法时native方法,同时也是HotSpot instrinsic 在对方法调用被即时编译替换成特殊的IR点,生成读取R13寄存器存放的当前线程指针
逃逸分析
逃逸:在某个方法内创建对象,除了在方法体内被引用,还会在其他方法引用、其他线程访问,在当前方法执行结束后,无法对该对象进行GC
逃逸判断依据
该对象是否存在堆中
对象是否被传入未知代码
逃逸分析的作用:开启逃逸分析即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者其其它线程所引用。
基于逃逸分析的优化
锁消除
锁对象不逃逸出当前的编译方法
栈上分配
方法退出,弹出当前方法栈帧自动回收分配的空间
标量替换
对象的字段访问替换成一个个局部变量的访问
字段访问相关优化
场景:Java对象是逃逸的、或者被即时编译器当成逃逸的,无法济宁标量替换
字段读取优化
字段存储优化
死代码的消除
循环优化
循环无关代码外提
循环展开
循环判断外提
循环剥离
向量化
模块一:基本原理
Java代码的运行
Java怎么运行?
运行方式
双击jar文件
开发工具中
命令行
网页中运行
环境
JRE-Java虚拟机以及Java核心类库
JDK —JRE ,一系列开发诊断工具
虚拟机
提供一个托管环境——“一次编写,到处运行”
内存管理,垃圾回收,数组越界、动态类型、安全权限等动态检测
Java字节码
字节码示例
Java字节指令操作码(opcode)固定一个字节
虚拟机运行字节码
方法区
包括常量池、静态变量、构造函数、等信息
java方法栈
本地方法栈
为Native方法服务,由c++语言实现
堆
存储Java实例或者对象
HotSpot 字节码翻译成机器码
解释执行
无需等待
即时编译
运行速度快
即时编译器种类
C1,C2 ,Graal
HotSpot采用混合模式
先解释执行,热点代码以方法为单位即时编译
Java的基本类型
Java虚拟机的Boolean类型
在虚拟机中boolean会被映射成int类型,true=1,false=0,boolean=2 做了掩码操作 取最后一位
Java的基本类型
Java基本类型的大小
byte、short、int、long、float、double 内存中默认值都是0
浮点float 的 0 正无穷 NaN
解释器使用的解释栈帧
局部变量区
局部变量区等价于一个数组,可用正整数索引,float,double 需要两个数组单元存储
也就是说,boolean、byte、char、short 这四种类型,在栈上占用的空间和 int 是一样的,32位虚拟机占用4字节、64位 占用8字节
除float、double外,其他基本类型与引用类型在解释执行的方法栈帧中占用的大小一致
字节码操作数栈
操作数的加载
boolean、byte、char 以及 short 从内存加载到操作数栈,当成int 类型运算
char、boolean 无符号扩展 以0填充高二字节
byte、short 有符号扩展,正数0填充,负数1填充高字节
Java 虚拟机加载Java类
类型分类
基本类型
引用类型
类
JVM 通过字节流读取
接口
JVM 通过字节流读取
数组
虚拟机直接生成
泛型参数
虚拟机擦除
加载
加载器,双亲委派模式
boot class loader 启动类加载器
其他都是ClassLoader子类
扩展类加载器 extensionclass loader
比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)
应用类加载器 application class loader
加载应用程序下的类, -cp.-classpath 系统变量java.vclass.path 环境变量CLASSPATH
链接
验证
准备
解析
初始化
为被标记为常量字段赋值——被fina修饰的基本类型&字符串
执行<client>方法——直接赋值操作,静态代码块中代码
JVM 如何实现方法调用
重载与重写
重载
Java编译器根据传入参数声明选取重载方法
1、不允许装拆箱,不允许变成参数
2、在1方法没找到,允许拆装箱,不允许可变长参数
3、2方法没找到,皆允许
4、如果两个方法相同,会调用更加贴切的方法,根据参数类型的继承关系
重写
返回类型,方法参数都与父类非私有方法相同
如果两个方法静态,子类隐藏了父类方法
JVM 的静态绑定和动态绑定
重载为静态绑定&编译时多态
重写称为动态绑定
调用指令符号的引用
编译过程中,不知道目标方法的实际内存地址,会用符号来表示目标方法(目标方法所在的类或接口名字,目标方法名,方法描述(犯法参数类型&返回类型))
非接口符号引用查找路径——类—父类—object—直接&间接实现接口
接口符号引用查找路径——接口—Object公有实例——超接口
静态绑定方法引用指向方法指针,动态绑定的方法调用指向一个方法表索引
方法表
本质是一数组,指向一个当前类及其祖先类的非私有实例方法
子类方法表包含父类方法表所有方法
子类方法在方法表的索引值,与重写的父类方法索引值相同
静态绑定与动态绑定:动态绑定多出内存解引用操作(访问栈上调用者,读取调用者动态类型)读取相应的方法表
内联缓存(与内联没啥关系)
解释:一种加快动态绑定的优化技术
缓存调用者的动态类型
执行时遇到已缓存类型,直接调用对应方法
执行时缓存中没有该类型,缓存退化成基于方法表的动态绑定
(单态内联缓存、多态内联缓存和超多态内联缓存)JVM采用单态内联缓存,如果类型不匹配,劣化为超多态内联缓存,在以后执行中直接使用方法表
JVM 如何处理异常
字节码中,每个方法附带异常表,每条记录由from 、to 、target 指针 以及捕获的异常类型构成
当捕获异常触发新的异常,finally抛出的是后者异常,前者异常被忽略
解决方法 Java7构造try-with-resources 语法糖 字节码中使用的是Supressed 避免异常消失
JVM怎么实现反射
反射调用(Method.invoke)实际委托给MethodAccessor,该接口有两个实现,本地实现(c++语言),委派实现(本地实现的父类)
实质:委派实现开始都是调用本地实现,那为什么还需要委派实现呢?
原因:在调用本地实现方法15次后,本地实现中会动态生成Java版本的invoke实现,以后委派就会调用Java版本,效率提升20倍(无需c++ 与java 转换,第一次调用c++,由于生成字节码相当耗时)
反射开销及其优化
Class.forName
Class.getMethod
返回结果的一份拷贝,在实际程序中应该缓存Class.forName,Class.getMethod结果
Method.invoke
关闭反射调用Inflation机制,取消委派实现
关闭反射调用方法检查权限
方法内联—使得编译器通过逃逸分析
内联瓶颈:在调用动态实现的时候,不同反射调用生成不同类型的GeneratedMethodAccessor 实现,增加虚拟机记录调用者具体类型数量,使得反射调用被内联:虚拟机设置参数——XX:TypeProfileWidth 默认2
JVM 怎么实现Lambda表达式(invokedynamic实现)
invokedynamic
解决只知道方法参数、返回类型情况下调用不同类型的同名方法:抽象出调用点的概念,允许程序将调用点链接到任意符合条件的方法上
引入 MethodHandle 方法句柄概念:
Java7 引入的“现在反射”比反射更加简洁,可以获取方法的句柄,类似方法的指针,功能更强大,支持增删改参数操作,生成适配方法句柄实现
一个强类型,直接执行的引用,仅仅关心参数类型&返回类型,在创建时检查方法权限,节省开销,通过invokeExact or invoke调用
可通过反射API中Method查找,也可根据类、方法名、句柄类型查找
Lambda表达式实现:MethodHandle.bindTo 方法句柄增操作,往里参数中传入额外参数,再调用另外句柄
会对invokeExact进行处理,调用到一个共享与方法句柄类型相关的特殊适配器中——LambdaForm
调用Invokers.checkExactType 检查参数类型
调用invokeBasic方法,获取句柄中MemberName 字段,以它为参数调用linkToStatic 调用目标方法
调用Invokers.checkCustomized 优化调用,方法调用超过127,生成特殊适配器,它把方法句柄作为常量,直接获取Membername字段,以它为参数调用linkToStatic 调用目标方法
invokedynamic 指令
指令的实例dynamic call site (动态调用点)
JVM调用BootstrapMethod 生成调用点对象
call site speicifier 调用者说明符
指令操作数指向 CONSTANT_InvokeDynamic_info
JVM 解析调用符得到 MethodHandle 指向 bootstrap method 、方法名、方法描述、提供给启动方法参数
JVM 调用启动方法,传入参数
返回一个调用点CallSite 对象
这个CallSite对象将永久和这个动态调用点关联
调用CallStite 关联的Methodhandle 指向的方法
Lambda表达式原理
启动方法生成一个适配器类实现了函数式接口,返回CallSite ,链接对象:适配器类实例的方法句柄
Java 对象的内存布局
创建对象的方式
new 、反射、Object.clone 、反序列化、Unsafe.allocateInstance( 不用构造函数实现,基于JVM 底层c++,直接操作内存创建对象,(分配内存空间,返回内存地址))
创建对象的步骤:
为对象分配内存空间,将对象的实例变量初始化为其变量类型的默认值
如果实例变量在声明时被显式的初始化则将初始化值赋给实例变量
调用构造方法,返回对象的引用
new 指令
生成的对象,对象中涵盖了所有父类的实例字段,也会对父类字段分配内存
JAVA内存布局分为三部分
对象头:由标记字段、类型指针所构成(64位JVM ,标记字段,类型指针各自占用64位)
标记字段:对象运行数据,如hashcode、GC info、锁信息
类型指针:指向对象的类
实例数据
实例数据部分是对象真正存储的有效信息,也是在程序代码中定义的各种类型的字段内容。无论是父类继承下来的,还是本类中定义的
对齐填充
压缩指针
压缩指针:减少内存占用量,将类型指针的占用量减少一半,占用32位。所以对象头占用12字节,另外类属性、对象引用类型、对象数组类型
原理:解释器解释字节码,植入压缩命令、进行编码解码
零基压缩技术
涉及相关术语:对象间填充,内存对齐 ——(堆内存起始地址为8N) 例如long、double 引用地址为8倍数,目的是使字段出现在同一CPU缓存行中。
字段重排列
主要是JVM重新分配字段定义的顺序,达到内存对齐的目的
JVM垃圾回收
判断对象存活机制
引用计数法
原理:每添加一个对象添加一个引用计数器
BUG:无法处理循环引用对象问题
可达性分析
原理:通过GC Roots 作为存活对象集,从该集合开始,检索所有能被集合引用的对象,并将其加入到其中。
GC Roots :从堆外指向堆内的引用
多线程下,可能造成误报,漏报,造成引用对象被回收
Stop-the-world 暂停其他所有线程
实现原理:安全点机制,等待所有线程都达到安全点,才允许请求Stop-the-world
安全点
安全点是使得其他线程找到一个稳定执行状态,这个执行状态下,JVM的堆栈不会发生变化
产生安全点位置:执行JNI(Java Native interface) 方法返回时、解释执行字节码、执行即时编译器生成的机器码、线程阻塞
在Graal 中还会在计数循环回边插入安全点,目的是避免机器码长时间不进入安全点,间接减少垃圾回收暂停时间
垃圾回收三种方式
清除(sweep)
原理:把死亡对象标记为空闲内存
缺点:内存碎片化、分配效率低
压缩(compact)
原理:把存活的对象聚集到内存区域起始位置
优缺点:能解决碎片化问题,但性能开销大
复制(copy)
原理:把内存区域分为两分,用from、to 维护,复制from 中存活对象到to,交换位置
优缺点:解决内存碎片化问题,堆空间使用效率低
分代回收思想
新生代
Eden
当Eden空间耗尽、JVM触发一次Minor GC 收集新生代垃圾,存活对象被送到第一个survivor区
Survivor
JVM触发一次Minor GC 收集新生代垃圾,Ede区和from指向的Survivor区存活的对象被复制到to指向的Survivor,然后交换from 与 to 指针
Survivor
如果一个对象被复制15次,该对象就会被提到老年区
单个Survivor 被占用50%,较高复制次数的对象也会被晋升到老年代
老年代
bug:老年代引用新生代对象的时候,我们需要扫描老年代中的对象
解决方式:卡表
1、将整个堆分为512字节的卡,有一个卡表存储每张卡的一个标志位,标识对应卡是否存在指向新生代对象引用,视为脏表
2、当扫描堆的时候只需要读脏表就行,读完后标志位清零
3、更新引用,修改卡表,就需要截获每个引用实例变量的写操作,并操作对应的写标识位
垃圾回收器
新生代
Serial 、Parallel Scavenge 、Parallel New(Serral多线程版本)
标记复制算法
老年代
Serial Old、Parallel Old 、CMS
前两个标记压缩算法、后一个标记清除算法
G1
一个跨新生代、老年代垃圾回收器——采用标记压缩算法,打破之前的对结构,每个区域都可以充当eden、survivor、老年区的一个
Java 内存模型
与happen-before之间的关系
happen-before 描述两个操作的内存可见性
同一线程中,字节码的先后顺序也暗含happens-before关系,控制流前的字节码先执行
如果没有观测到前者的运行结果,后者数据没有依赖前者,他们可能会被重排序
happen-before具备传递性
解决重排序问题:给a,b设置volatile字段
解决数据竞争问题关键在于构造一个跨线程的happens-before关系,操作x happens-before 操作y ,使得操作x 之前的字节码结果对于操作y之后字节码可见
volatile 关键字
保证不同线程对变量操作的可见性,即时修改内存值与刷新线程缓存,但是不能保证程序的原子性
保证原子性可以加锁
禁止进行指令重排序
1、当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行
2、在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
加了关键字的字段在汇编代码中多出lock前缀
内存屏障
1、它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2、它会强制将对缓存的修改操作立即写入主存
3、如果是写操作,它会导致其他CPU中对应的缓存行无效
使用规则,约束原则性
1、对变量的写操作不依赖当前值 x++
2、该变量没有包含在具有其他变量的不变式中
使用场景:状态标记量、在单例中双重检查
JVM怎么实现synchronized,上图为32位JVM中对象头标志位
锁的类型
乐观锁
思想:认为读多写少,遇到并发写的可能性低,每次读数据认为别人不会修改,不会上锁,更新的时候根据数据版本号判断数据是否修改,如果版本号一致,更新,不同会重复读-比较-写操作
一般通过CAS 进行原子操作,CAS(compare and swap) 比较并操作,原子地更新某个位置的值
悲观锁
思想:认为写多,并发性高,每次拿数据都会认为别人修改了,所以在每次读写操作的时候都要上锁
字节码中主要通过monitorenter 和 monitorexit 指令实现synchronized
一般有一个monitorenter 和多个monitorexit 确保在异常情况下能进行解锁操作
锁对象
对于实例方法:锁的是对象this
对于静态方法:锁的是所在类的classs实例
这两个指令相当于 每个锁对象的计数器,和指向持有该锁的线程指针
重量级锁Synchronized
阻塞加锁失败的线程,在目标锁被释放的时候,唤醒这些线程
该锁是非公平锁,在线程进入等待队列前会进行自旋,尝试获取锁 ,可能和ONDeck线程抢夺锁资源
自旋锁
场景:线程阻塞后会进入内核调度状态,避免线程在用户态与内核态之间频繁切换,采用自旋锁,达到延时阻塞目的,在线程在进入等待队列时首先进行自旋尝试获得锁,如果不成功再进入等待队列
原理:在争用线程没有获取资源时,不会立即进入阻塞状态,而是会等一段时间(对应cpu中执行一些空操作,比如执行for 循环、执行空汇编指令),等待线程释放锁资源
不足:等待时间过长 影响系统整体性能, 等待时间过短达不到延时阻塞
偏向锁,只有一个线程使用这个锁
定义:同步锁只有一个线程使用,在不存在线程争用时,线程不需要触发同步,这时候就会加一个偏向锁
偏向锁的获取过程
偏向锁的释放:
有了竞争后就会升级为轻量级锁,这时候需要撤销偏向锁
等待全局安全点、暂停拥有偏向锁的线程,判断对象是否处于被锁定状态,撤销偏向锁最后两位标志位恢复到未锁定、轻量级状态
使用场景:始终只有一线程在执行同步块,在他没有释放锁之前,没有其他线程执行同步块,在有锁无竞争下使用
轻量级锁,多线程不同时间请求同一把锁
由偏向锁升级而成
加锁过程
1、进入同步块时,锁对象的状态为无锁状态时,会在当前线程中建立一个锁记录(Lock Record) 空间,存储锁对象标志位的拷贝
2、拷贝成功,虚拟机通过CAS原子操作将锁对象的标志位更新为指向锁记录的指针
3、更新成功就代表该线程拥有该锁,锁对象的锁标志位变为00,表示该对象处于轻量级锁
4、如果更新失败,JVM判断该对象是否拥有指向当前线程的指针,如果有就表示当前线程拥有锁,可以继续执行,否则膨胀为重量级锁,锁标志为10,标志位中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程
轻量级锁的释放
在释放时发现在持有锁期间有其他线程尝试获取锁,并该线程对标志位进行修改,发现当前线程之前拷贝的标志位与锁标志位不同,就膨胀为重量级锁
锁优化
适应性自旋(Adaptive Spinning),JVM自己会根据历史自旋的次数,自动调节
锁粗化(Lock Coarsening),将多次连接在一起的加锁解锁合并为一次,将连续的锁扩展成更大范围的锁
锁消除(Lock Elimination),删除不必要的加锁操作,根据代码逃逸,判断一段代码中堆上的数据不会逃逸出当前线程,则认为这段代码安全,不需要加锁
0 条评论
下一页
为你推荐
查看更多