理解Java虚拟机第三版框架教案
2022-10-26 15:29:09 0 举报
AI智能生成
理解Java虚拟机第三版框架教案
作者其他创作
大纲/内容
七. 虚拟机类加载机制
类的生命周期
加载, 验证, 准备, 解析, 初始化, 使用, 卸载; 其中验证, 准备, 解析统称为连接
6种情况必须立即对类进行初始化
1). 使用如下字节码指令的时候
遇到new, getstatic, putstatic, invokestatic指令时, 类型没有进行初始化, 则需要先触发其初始化阶段
对应的Java代码中的使用场景
使用new关键字实例化对象的时候
读取或设置一个类型的静态字段的时候(被final修饰, 已经编译期把结果放入常量池的静态字段除外)
调用一个类型的静态方法的时候
遇到下面的字节码指令的时候
2). 使用java.lang.reflect包的方法的对类型进行反射调用的时候
3). 当类型初始化的时候, 发现其父类还没有进行初始化,则需要先触发其父类的初始化
4). 当虚拟机启动的时候, 用户需要指定一个启动类, 虚拟机会先出货后这个主类
5). 当使用JDK7新加入的动态语言支持时, 部分方法句柄对应的类型没有进行初始化,则需要进行初始化
6). 一个接口定义了JDK8新加的默认方法(default修饰)时, 如果这个接口对应的实现类发生初始化, 那么这个接口要先被初始化
类加载的过程
1. 加载
1). 通过一个类的全限定名来获取定义此类的二进制字节流
2). 通过字节流代表的静态存储结构转化为方法区的运行时数据结构
3). 在内存中生成一个代表这个类的Class对象, 作为方法区这个类的数据访问的入口
注意
该阶段用户程序可以通过自定义类加载器的方式进行局部参与<br>
2. 验证
确保Class文件的字节流中包含的信息符合Java虚拟机规范的约束要求
4个阶段
1). 文件格式验证
字节流是否符合Class文件的格式规范, 并且是否能被当前版本的虚拟机处理
验证点
是否以魔数开头: 0xCAFEBABE
主,次版本号是否在当前的Java虚拟机接受范围内
常量池中的常量是否有不被支持的类型(检查常量的tag)
...
2). 元数据验证
对字节码描述信息进行语义分析并对元数据信息进行语义校验
验证点
这个类是否有父类
这个类是否继承了不允许被继承的类
如果这个类是抽象类是否实现了其父类或接口中所要求的所有的抽象方法
...
3). 字节码验证
通过数据流分析和控制流分析, 确定程序语义的合法性, 符合逻辑性. 对类的方法体进行校验分析
验证点
保证在任何时刻操作数栈的数据类型与质量代码序列都能配合工作
保证任何跳转指令都不会跳转到方法体以外的字节码指令上
...
4). 符号引用验证
虚拟机把符号引用转化为直接引用的时候, 这个动作会在"解析"阶段中发生, 检查类型是否缺少或者禁止访问他依赖的某些外部类, 方法, 字段等资源
验证点
符号引用中通过自费重描述的全限定名是否能找到对应的类
在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
符号引用的类, 字段, 方法 的可访问性(private, protected, public)是否可被当前类访问
...
3. 准备
类的静态变量(static)被分配内存并设置初始值的过程
注意
静态成员变量在准备阶段过后初始设置为零值(不同数据类型都对应各自的零值, 如boolean: false; reference: null)
静态成员变量被赋值为Java代码中的值是在putstatic指令执行时完成, 这个指令存放与类构造器<clinit>()方法之中
静态成员变量被final修饰, 即讲台常量, 那么在准备阶段就会直接赋值为Java代码中的值
4. 解析
Java虚拟机将常量池内的符号引用替换为直接引用的过程
包括类或接口的解析, 字段的解析, 方法的解析
5. 初始化
在初始化阶段,则会根据程序编写指定的主观计划去初始化类变量和其他资源. 简单的说其实就是执行类构造器<clinit>()方法的过程
<clinit>()是编译器自动收集类中的所有类变量赋值动作和静态语句块中的语句的合并
静态语句块中只能访问定义在他之前的变量,定义在他之后的变量只能进行赋值操作,不能访问
JVM会保证子类的<clinit>()方法执行之前, 父类的<clinit>()方法已经执行完毕, 也就意味着父类定义的静态语句块要优先于子类的类变量赋值操作
一个类中没有类变量的赋值也没有静态代码块, 那么编译器可以不为这个类生成<clinit>()方法
接口中不能使用静态语句块, 单仍然有变量初始化的赋值操作, 因此接口也会生成<clinit>()方法
JVM会保证一个类的<clinit>()方法只被执行一次, 使用了CAS同步锁机制
类加载器
类加载阶段"通过一个类的全限定名来获取描述该类的二进制字节流"放在虚拟机外部去实现, 得以让应用程序自己可以决定如何获取所需要的类, 例如: 类层次划分, OSGi, 程序热部署, 字节码加密等
类与类加载器
类加载器用于实现类的加载动作
对于任意一个类, 都必须由加载他的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性
双亲委派模型
要求除了顶层的启动类加载器之外, 其余的类加载器都应有自己的父类加载器, 当一个类加载器收到加载类的请求, 先把请求委托给父类加载器, 一直把请求发到底层, 只有当父类加载器反馈自己无法加载这个类(它的搜索范围中找不到所需的类)时, 子类加载器才会尝试自己完成加载
在JVM视角, 只有两种类加载器, 一种是启动类加载器(Bootstrap ClassLoader), 是虚拟机的一部分; 另一种就是其他的加载器, 使用Java语言实现, 全部继承自抽象类ClassLoader
好处: Java中类随着他的类加载器仪器具备了一种带有优先级层次的关系
3类系统类加载器
启动类加载器(Bootstrap ClassLoader)
负责加载<JAVA_HOME>\lib目录, 或者-Xbootclasspath指定路径中存放可被JVM识别的类库加载到JVM内存中(JVM按照文件名识别, 例如: rt.jar, tools.jar)
扩展类加载器(Exension ClassLoader)
负责加载<JAVA_HOME>\lib\ext目录中, 或者java.ext.dirs变量指定的路径中的类库
应用程序类加载器(Application ClassLoader)
负责加载用户类路径(ClassPath)上所有的类库
八. 虚拟机字节码执行引擎
运行时栈帧结构
概念
JVM以方法作为最基本的执行单元, 栈帧则是用于支持虚拟机允许方法调用和方法执行背后的数据结构; 每个方法的调用到执行结束, 都对应着一个栈帧的入栈到出栈的过程
每个栈帧都包括: 局部变量表, 操作数栈, 动态连接, 方法返回地址, 额外的附加信息
局部变量表
一组变量值的存储空间, 用于存放方法的参数和方法内部定义的局部变量, 在编译期就确定了最大容量,在Code属性的max_locals数据项中
包括方法的参数, 实例方法的隐藏参数"this", catch定义的异常, 方法体中的声明的变量
变量槽
局部变量表分配内存所使用的最小单位, 长度不超过32位的数据类型(byte, char, float,int, short, boolean, returnAddress), 每个局部变量占用一个变量槽
64位的数据类型long和double放在两个连续的变量槽中, reference跟虚拟机的实现有关, 32位的占32位, 64位的还需要看是否开启指针压缩
局部变量表中的变量槽可以重用, 当PC计数器的值已经超过了某个变量的作用域, 那么他对应的变量槽可以交给其他变量来重用
操作数栈
操作数栈的最大深度也在编译期就确定了最大深度 写入了Code属性的max_stacks数据项中
32位数据类型所占的栈容量为1, 64位的数据类型占用的栈容量为2
动态连接
每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用, 为了支持在方法的调用的过程中的动态连接
方法返回地址
当前方法完成调用退出时, 必须返回到方法被调用的地方, 程序才能继续执行, 方法返回时需要在栈帧中保存一些信息, 用来帮助恢复他的上层主调方法的执行状态
附加信息
允许增加一些规范里面没有描述的信息到栈帧中, 例如: 调试, 性能收集的相关信息, 跟虚拟机的实现有关
方法的调用
解析
所有方法调用的目标在Class文件中都有一个常量池中的符号引用, 在类加载的解析阶段把符号引用转化为了直接引用
Java源文件编译完成之后,就确定可唯一调用的版本, 可以在类加载中的解析阶段把符号引用直接解析为直接引用. 包括静态方法, 私有方法, 实例构造器, 父类方法, 被final修饰的方法
分派
分派的调用过程将会揭示一些多态的最基本的体现
静态类型
静态类型的变化仅仅在使用的时候发生, 最终静态类型是在编译期可知的
实际类型
变化的结果只有在运行期才可确定
静态分派
所有依赖静态类型来决定方法的执行版本的分派动作叫做静态分派, 经典的应用就是在编译期进行的静态分派, 进而通过参数确定使用哪个重载版本, 并生成对应的字节码指令
选择哪个重载版本
虚拟机在重载时, 通过参数的静态类型作为判断依据, 静态类型在编译器可知的, 所以在编译阶段, 根据参数的静态类型决定了会使用哪个重载版本
动态分派
在运行期间根据实际类型确定方法执行版本的分派过程称为动态分派
选择哪个重写版本
invokevirtual指令把常量池中的方法的符号引用解析到直接引用上, 并根据方法的接收者的实际类型来选择版本
单分派和多分配
单分派: 根据一个宗量对目标方法进行选择
多分派: 根据多于一个宗量对目标方法进行选择
动态类型语言
定义
类型检查的主体过程是在运行期而不是在编译期进行的, 例如: JavaScript, PHP, Python
那么在编译期就进行类型的检查过程的语言叫做静态类型语言, 例如: Java, C++
java.lang.invoke
除了单纯地依靠符号引用来确定调用的目标方法这条路之外, 提供一种新的动态确定目标方法的机制, 称为"方法句柄"(Method Handle)
MethodHandle则设计为可服务于Java虚拟机之上的语言, 包括了Java语言
invokedynamic指令
每个含有invkedynamic指令的位置都被称为"动态调用点", 这条指令的代表方法符号引用是CONSTANT__InvkeDynamic_info常量
可以获得3项信息
引导方法
有固定的参数, 并且返回值是CallSite对象, 这个对象代表了真正要执行的目标方法调用
方法类型
名称
字节码解释执行
传统编译过程: 编写的源码程序-> 词法分析-> 语法分析-> 抽象语法树->中间代码-> 生成器-> 目标代码
解释执行过程: 编写的源码程序-> 词法分析-> 语法分析-> 抽象语法树->指令流-> 解释器-> 解释执行
javac编译器完成了程序代码经过词法分析, 语法分析到抽象语法树,再编译整个语法树生成线性字节码指令流的过程
Javac编译器输出的字节码指令流, 基本上是采用基于栈的指令集架构, 大部分的字节码指令都是零地址指令, 即他们的指令不带参数, 依赖于操作数栈进行工作
九. 类加载及子系统案例
Tomcat 正统的类家在架构
解决的问题
部署在同一个服务器上的两个web应用程序所使用的Java类库可实现相互隔离
部署在同一个服务器上的两个web应用程序所使用的Java类库可实现相互共享
服务器需要保证自身的安全不受部署的web应用程序的影响
支持jsp的web服务器
目录结构和类加载器
/common目录, 可被Tomcat和素有的web应用程序共同使用, CommonClassLoader
/server目录, 可被Tomcat使用, 对所有的web应用程序都不可见 CatalinaClassLoader
/shared目录, 被所有的web应用程序共同使用, 但对Tomcat自己不可见 SharedClassLoader
/WebApp/WEB-INF目录, 仅仅可被web应用使用 WebappClassLoader
单独处理jsp文件, JasperLoader
OSGi
基于Java语言的动态模块化规范
字节码生成技术
javac, Javassist, CGLib, ASM
动态代理: 代理类的处理逻辑可以在原始方法进行环绕修饰, 即在调用原始方法之前或之后添加自己的代码逻辑
Backport工具
把高版本的JDK编写的代码放到低版本JDK环境中部署运行
十. 前端编译与优化
代表性编译器产品
前端编译器: JDK的javac, Eclipse JDT的增量式编译器(ECJ)
即时编译器: HotSpot虚拟机的C1, C2, Graal编译器
提前编译器: JDK的Jaotc, GCJ, Excelsior JET
Javac编译器
编译过程
1). 准备阶段: 初始化初始化插入式注解处理器
2). 解析与填充符号表的过程
词法,语法分析, 构造出抽象语法树
填充符号表, 产生符号地址和符号信息
3). 插入式注解处理器的注解处理过程
4). 分析与字节码生成的过程
标注检查, 对语法的静态信息进行检查
数据流和控制流分析
解语法糖
语法糖能减少代码量, 增加程序的可读性; 解语法糖就是在编译阶段还原回原始的基础语法结构
字节码生成
把前面步骤所生成的信息转为字节码指令写到磁盘中, 并进行了少量的代码添加和转化工作, 例如: 实例构造器<init>() 和类构造器<clinit>()
Java语法糖
泛型
本质是参数化类型或者参数化多态的应用, "类型擦除式泛型"
由于jdk5引入泛型时, Java已经问世十余年,遗留的代码规模非常大, 为了保证Java5之后引入泛型以前的编译的Class文件能继续执行. 最终选择了 直接把现有的类型原地泛型化, 不添加新的类型.
类型擦除
让所有的泛型化的实例类型 如ArrayList<Integer> 自动成为ArrayList的子类型或者还原回它本身, 否则类型转换就是不安全的
缺陷
类型擦除实现的泛型直接导致了对原始类型数据支持成了麻烦, 因为不支持int, long 与Object之间强制转型
运行期间无法获取泛型的类型信息
带来了模棱两可的模糊情况, 例如方法的重载参数是两个不同的类型的List, 却不能被编译通过, 因为类型被擦除了
结论
从Signature属性的出现, 可以看出所谓类型擦除, 仅仅是对方法的Code属性中的字节码进行擦除, 实际上元数据中还是保留了泛型信息, 这也是我们能通过反射手段获取到参数化类型的根本依据
值类型与未来的泛型
Valhalla项目中规划了多种泛型的实现方案, 其中包括具现化
提供"值类型"的语言层面的支持
自动拆装箱
"==" 运算在遇到算术运算符时会自动拆箱
equals()方法不处理数据转型的关系, 或者说数量类型一样并且值一样, 才为真
条件编译
条件为常量的if语句可以实现条件编译
十一. 后端编译与优化
即时编译器
Java程序最初都是通过解释器进行解释执行的,当时JVM发现某个代码块执行的特别频繁,就会把这些代码认定为热点代码, 为提高热点代码的执行效率, JVM将会把这些代码编译成本地机器码. 完成这个任务的后端编辑器叫做 即时编译器
解释器与编译器
当程序要想迅速启动和执行的时候, 适合使用解释执行, 省去了编译的时间, 立即执行
程序启动之后, 随着时间的推移, 编译器会把把越来越多的热点代码编译成本地机器码,减少解释器的中间损耗, 获取更高的而执行效率
热点代码
被多次调用的方法
被多次执行的循环体
不管是哪种情况, 编译的目标对象都是整个方法体
热点探测判定
基于采样的热点探测
周期性的检查各个线程的调用栈顶, 如果某个方法经常出现在栈顶, 那这个方法就是热点方法
基于计数器的热点探测
为每个方法创建计数器, 统计方法的调用次数, 执行次数超过一定的阀值则认为他是热点方法
编译过程
默认条件下, 无论采用哪种编译执行方式, 虚拟机在编译器还未完成编辑之前, 都仍然按照解释方式继续执行代码, 而编译动作则交给编译线程中进行
提前编译器
提前把字节码编译为本地机器码, 但这跟具体的硬件平台息息相关, 无法做到"一次编译, 到处运行"的理念.
ART使用提前编译, 在Android的世界里大放异彩, 干掉了即时编译器Dalvik
编译器优化技术
方法内联
把目标方法的代码原封不动的的"复制"到发起调用的方法中, 避免发生真实的方法调用
方法内联的条件
被调用方法是否是热点代码
被调用法法是否大小合适
运行时方法是否可唯一确定
逃逸分析
分析对象的动态作用域, 当一个对象在方法里面被定义后, 他可能被外部方法所引用(例如: 调用参数传递到其他方法中), 这称之为"方法逃逸"
当前线程中的对象赋值给其他线程中访问的实例对象, 这称之为"线程逃逸"
代码优化
如果能证明一个对象不会逃逸到方法或者线程之外或者逃逸程度比较低, 则可以为这个对象采取不同程度的优化
栈上分配
如确定一个对象不会逃逸出线程之外, 那么这个对象可以在栈上分配内存, 这样对象所占用的内存空间就会随着栈帧的出栈而销毁
标量替换
标量
若一个数据无法再分解成更小的数据来表示(int , long ... reference类型等),不能进步一步拆分, 那么这些数据称之为标量
聚合量
相反地, 一个数据可以进一步被分解, 那么这些数据称之为聚合量
标量替换
把一个Java对象拆散, 根据程序的访问情况, 把用到的成员变量恢复为原始类型来访问, 这个过程称为标量替换
如果一个对象不会被方法外部访问, 并且这个对象可以被拆散, 那么程序真正执行的时候将可能不去创建这个对象
同步消除
如果一个对象不会逃逸出线程, 无法被其他线程访问, 那么这个对象的读写肯定不会有竞争, 那么对这个对象的同步措施就可以安全地消除<br>
公共子表达式消除
如果一个表达式E已经被计算过了, 并且从先前计算到现在E中所有的变量的值都没有发生改变, 那么E的这次出现称为公共子表达式
数组边界检查消除
在访问数组元素的时候, 系统将自动会检查上下界限.编译器通过数据流分析可以得知操作元素的下标不会超过数据的范围, 则可以进行数组上下界检查的消除
Graal编译器
JDK10增加 JVMCI, 虚拟机编译器接口
响应HotSpot的编译请求, 并将该请求分发给Java实现的即时编译器
允许编译器访问HotSpot中与即时编译相关的数据结构, 包括类, 字段, 方法, 性能监控数据等
提供HotSpot代码缓存的Java端抽象, 以便于部署编译完成的二进制机器码
代码中间层表示
中间层表示也被等价的称之为理想图, 从编译器内部来看整个过程: 字节码-> 理想图-> 优化-> 机器码的转变过程
代码优化与生成
生成理想图
理想图本身的数据结构: 一组不为空的节点集合, 它的节点都是用ValueNode的不同类型的子类节点表示
字节码生成理想图: 可以按照字节码解释器的思路去理解它
理想图的操作
规范化: 如何缩减理想图的规模, 在理想图的基础上优化代码所要采取的措施, 对于理想图的规范化不局限于单个操作码范围内, 很多都是立足于全局进行的
理想图转化为机器码
先生成低级中间表示LIR, 然后再有HotSpot统一后端来产生机器码
十二. Java内存模型与线程
Java内存模型
主内存和工作内存
Java内存模型规定了所有的变量都存在主内存中
每条线程都有自己的工作内存, 线程的工作内存中保存了被该线程使用的变量的副本, 线程对变量的操作多发生在工作内存中, 线程之间的变量值的传递需要通过主内存来完成
内存间的交互操作
一个变量如何从主内存拷贝到工作内存, 又如何从工作内存同步到主内存中, Java内存模型定义了8中操作来完成
lock 锁定
作用于主内存的变量, 把一个变量标志为一条线程独占的状态
unlock 解锁
作用于主内存的变量, 把一个处于锁定状态的变量释放出来, 此时才可以被其他线程锁定
read 读取
作用于主内存的变量, 把一个变量从主内存中传输到工作内存中
load 载入
作用于工作内存的变量, 把从主内存read过来的变量放入工作内存变量副本中
use 使用
作用于工作内存的变量, 工作内存中的一个变量值传递给执行引擎
assign 赋值
作用于工作内存的变量, 把执行引擎接受的值赋值给工作内存的变量
store 存储
作用于工作内存的变量, 工作内存中的一个变量的值传送到主内存中
write 写入
作用于主内存的变量, 把store操作的变量放入主内存的变量中
针对8中操作的规则
不允许read和load, store和write 操作之一单独出现
不允许一个线程丢弃它最近的assign操作
不允许一个线程没有经过assign操作的值同步会主线程
一个新的变量只能在主内存中产生
一个变量同一个时刻值运行一条线程对其进行lock操作
如果对一个变量执行lock操作,那将会清理掉工作内存中此变量的值
如果一个变量事先没有被lock操作锁定,那么不允许使用unlock操作
对一个变量执行unlock操作之前, 必须先把此变量同步回主内存
volatile型变量的特殊规则
两项特性
保证此变量对所有线程的可见性, 当一个线程修改了这个变量的值, 那么其他线程都可以立即得知
禁止指令重排序
volatile规则
工作内存中, 每次使用被volatile修改的变量前都先从主内存中刷新最新的值到本地内存中
工作内存中, 每次修改给volatile修饰的变量都必须立即同步回主内存中
要求被volatile修改变量不会被指令重排序优化, 从而保证代码的执行顺序与程序的顺序相同
原子性, 可见性 有序性
原子性
一系列操作是一个整体, 要么成功, 要么失败. 在Java内容模型中, 大致可以认为, 基本数据类型的访问, 读写都是具备原子性(除了long和double的非原子性协定)
可见性
当一个线程修改了共享变量, 其他线程能够立即得知这个修改
Java内存模型中通过主内存作为传递媒介, 不同的线程可修改或读取主流程的变量, 而被volatile修改的变量的特殊之处在于, 被修改的变量能立即同步到主内存, 每次使用变量之前都从主内中读取.
Java中能保证可见性有三个关键字: volatile, synchronized, final
有序性
线程内表现为串行语义, 指令重排序现象和工作内存与主存同步延迟的现象
先行发生原则
判断数据是否存在竞争, 线程是否安全的非常有用的手段, 说操作A先行发生于操作B, 那么是说操作B发生之前, 操作A产生的影响能被操作B观察到
程序次序规则
在一个程序内, 按照控制流顺序, 书写在前面的操作先行发生于书写在后面的操作
管程锁规则
一个unlock操作先行发生于后面对同一个锁的lock操作
volatile变量规则
对一个volatile变量的写操作先行发生于后面对这个变量的读操作
线程启动规则
Thread对象的start()方法先行发生于此线程的每个动作
线程终止规则
线程中所有发生的操作都先行发生于线程的终止操作
线程中断规则
对线程的interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
对象的直接规则
一个对象的初始化完成先行发生于它的finalize()方法的开始
传递性
操作A先行发生于操作B, 操作B先行发生于操作C, 那么就可以得出操作A先行发生于操作C的结论
Java的线程
Java的每一个线程都直接映射到一个操作系统原生线程上来实现的
线程的状态
新建 New
新建: 创建后尚未启动
运行 Running
运行: 包括操作系统线程状态中的Running和Ready, 处于此状态的线程有可能正在执行, 也有可能等待操作系统给他分配执行时间
无限等待 Waiting
无限等待: 不会被分配处理器的执行时间, 需要等待其他线程的显示地唤醒[包括 Object::wait(), Thread::join(), LockSupport::park()]
限期等待 Timed Waiting
s限期等待: 不会被分配处理器执行时间, 达到等待时间之后有系统自动唤醒[包括: Thread::sleep(s), Object:wait(s), Thread::join(s), LockSupport::parkNanos(), LockSupport::parkUntil()]
阻塞 Blocked
阻塞: 等待着获取到一个排它锁, 这个事件将在另外一个线程放弃这个锁的时候发生
结束 Terminated
已经终止的线程状态<br>
十三. 线程安全与锁优化
线程安全
线程安全的定义
当多个线程同时访问一个对象时, 如果不用考虑这些线程在运行时环境下的调度和交替执行, 也不需要进行额外的同步, 在调用方也不需要进行其他的协调操作, 调用这个对象可以获取正确的结果, 那这个对象是线程安全的
Java语言中的线程安全
共享操作数据分为5类
不可变
不可变的对象一定是线程安全的, 例如: final关键字, 被final修饰的String, Intger, 就不可变
绝对线程安全
不管运行时环境如何, 调用者都不需要额外的同步措施
相对线程安全
通常意义上讲的线程安全
线程兼容
对象本身并不是线程安全的, 但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中可以安全使用
线程对立
不管调用端是否采用了同步措施, 都无法再多线程环境中并发使用代码
线程安全的实现方法
互斥同步(悲观锁)
同步
多个线程并发访问共享数据时, 保证共享数据在同一个时刻只被一条线程使用
缺点
无论共享的数据是否出现竞争, 先认为有竞争的线程, 进行加锁, 这将会导致用户态到和心态转换, 维护锁计数器和检查是否有被阻塞等开销
互斥
实现同步的一种手段
互斥是因, 同步是果; 互斥是方法, 同步是目的
synchrionized
最基本的互斥同步手段, Javac编译之后生成monitorenter, monitorexit 两个字节码指令, 这两个指令都需要制定一个reference类型的参数来指明要锁定的和解锁的对象
执行monitorenter指令时, 首先尝试获取对象的锁.如果这个对象没有被锁定, 或者当前线程已经持有了这个对象的锁. 就把锁的计数器的值增加一, 而执行monitorexit指令时把锁的计数器减一, 计数器值为0, 锁就会被释放. 如果获取对象锁失败, 那当前线程就应当被阻塞等待, 直到请求锁定的对象被持有他的线程释放为止.
注意
被synchronized修饰的同步块对同一条线程来说是可重入的
被synchronized修饰的同步块在持有锁的线程执行完释放锁之前, 会无条件的阻塞后面其他的线程进入
Lock接口
ReentrantLock 可重入锁
等待可中断
长时间等待锁释放的线程,可以选择放弃等待, 处理其他事情
公平锁
多个线程在等待同一个锁时, 必须按照申请锁的时间顺序来一次获取锁
锁绑定多个条件
指一个ReentrantLock对象可以同时绑定多个Condition对象
ReentrantReadRriteLock
非阻塞同步(乐观锁)
先不管风险, 直接进行操作, 出现了冲突再进行一段时间补偿性的重试,直到出现没有竞争的共享数据为止, 不再需要把线程阻塞挂起
CAS: 比较并交换, 硬件处理器直接通过一条指令完成, 属于原子操作
需要三个参数, 修改一个变量V的值为B, 先检查符合旧的预期值A的话, 则更新成功, 否则更新失败
ABA问题
无同步方案
如果一个方法本来就没有涉及到共享数据, 那么它自然就不需要添加任何的同步措施
可重入代码: 一个方法输入相同的数据, 都能返回相同的结果, 则该方法的返回值是可预测的
线程本地存储: 共享数据的可见范围限制在一个线程内部, 通过ThreadLocal类实现线程本地存储的功能
锁优化
JDK6实现了各种锁优化技术, 当使用synchronized加锁时, 在锁定对象之前, 先进行了一系列的锁优化
自旋锁
自旋等待避免了线程切换的开销, 但是却要占用处理器的时间, 如果占用时间过长反而带来的收益会降低. 所以自旋有次数的限制, 达到这个次数之后任然没有成功获得锁, 就使用传统的方式挂起线程
JDK6对自旋锁做了优化, 引入了自适应, 意味着自旋的限制次所不再固定, JVM会根据同一个对象的上次获得锁次数和拥有者的运行状态判断, 允许自旋次数的适当增加; 另一方面, 自旋很少成功获得锁, 那在以后获取锁将有可能直接省掉自旋过程, 以避免浪费处理器资源
锁消除
JVM即时编译器在运行时, 对一些代码要求同步, 但是对被检测到的不可能存在的共享数据竞争的锁进行消除, 锁消除的主要判定来源于逃逸分析的数据支撑
锁粗化
如果JVM探测到有一串零碎的操作都是对同一个对象加锁, 将会把加锁同步的范围扩展到整个操作序列的外部
轻量级锁
在没有多线程竞争的前提下, 减少传统的重量级锁使用操作系统互斥量产生的性能消耗
加锁工作过程
进入同步代码块的时候, 若同步对象没有被锁定(对象头中锁标记位是"01"), 则JVM在当前线程的栈帧中建立一个名为锁记录的空间, 存储锁对象的Mark Work 的拷贝
JVM使用CAS尝试把对象的Mark Wrod更新为锁记录的指针
更新成功, 则表示该线程拥有了对象的锁, 同时锁标记位改成了"00", 表示对象处于轻量级锁状态
如果更新失败, 则表示至少有一条线程跟当前线程在竞争这个对象的锁, 检查对象的Wark Word 是否指向当前线程的栈帧, 如果是, 说明当前的线程已经拥有了这个对象的锁, 直接执行同步块的代码
检查对象头若不是当前线程的栈帧, 说明锁对象已经被其他线程给占用了, 必须要膨胀为重量级锁, 标记位改为"10", 此时Mark Word中存储的是重量级锁的指针
偏向锁
消除数据在无竞争情况下的同步原语,进一步提升程序的性能
这个锁会偏向于第一个获取它的线程, 如果在接下来的执行过程中, 该锁一直未被其他的线程获取, 则持有偏向锁的线程将永远不需要进行同步
一. 走进Java
Java技术体系
组成部分
Java程序设计语言
不同硬件平台的Java虚拟机
Class文件格式
Java类库API
第三发Java类库
产品线
Java Card
Java小程序Applets小内存平台
Java ME
支持Java程序运行在移动终端上的平台
Java SE
支持桌面级应用的Java平台
Java EE
支持使用多层架构的企业应用的Java平台
Java发展史
分支主题
Java虚拟机
相当的原始, 虚拟机的鼻祖: Sun Classic
使用范围最广的JVM: HotSpot
面向移动和嵌入式市场: Mobile VM
BEA System 公司的 JRockit
IBM 的 IBM J9
......
展望Java技术的未来
无语言倾向
Graal VM
任何语言通过解释器转化的中间表示层, 然后通过该虚拟机加载运行
先进的即时编译器
Graal编译器
采用更加先进的优化方案,及时编译热点代码等.
向Native迈进
JVM可直接加载预编译成二进制并调用, 降低了内存占用和启动时间, 实现"Run Programs Faster Anywhere"
Java虚拟机不断优化增强
Java体系提供更多的接口
jdk1.4时, 已开放了Java虚拟机信息监控接口 JVMPI 和 Java虚拟机调试接口 JVMDI
jdk5时, 抽象了Java虚拟机工具接口 JVMTI
jdk6时, JVMTI不断增强替换了原来的 JVMPI 和 JVMDI
jdk9时, 开放了Java言语级别的编译器接口 JVMCI
jdk10时, 重构了Java虚拟机垃圾收集器接口 JVMGI, 统一了内部垃圾收集器的公共行为
语言语法持续增强
如: jdk10中, 本地类型变量的推导; jdk13中, switch支持表达式, 字符串文本块; 基本类型的泛型支持
二. 内存区域和内存溢出
运行时数据区域
程序计数器
线程私有, 当前线程锁执行的字节码的行号指示器, 不会出现OOM
Java虚拟机栈
概念
线程私有, Java方法执行的线程内存模型,每个方法被执行时 JVM会同步创建一个栈帧,用于存储: 局部变量表, 操作数栈,动态链接, 方法出口等
会出现两类异常
若线程请求的栈深度超过JVM所允许的深度, 抛出StackOverflowError
栈扩展时无法申请到足够的内存, 抛出OutOfMemoryError
本地方法栈
线程私有,服务于本地方法
Java堆
概念
线程共享, JVM启动时创建, 存放对象的实例; 随着即时编译器的进步, Java对象也可以再栈上分配
组成
新生代, 老年代, 永久代, Eden, Survivor ... 把Java堆细分只要是为了更好地回收内存, 或者更快的分配内存
方法区
概念
线程共享, 存储被JVM加载的类型信息, 常量, 静态变量, 即时编辑器编译后的代码缓存等数据
变化
JDK7的HotSpot, 把原来放在永久代的字符串常量池, 静态变量移动到了堆中; JDK8废弃了永久代的概念, 使用了 本地内存中的元数据区进行了替换
HostSpot虚拟机对象
对象的创建
过程
在常量池中检查类的符号引用
该符号引用代表的类进行加载, 解析和初始化
堆中分配内存
指针碰撞
内存分配是规整的, 每次分配内存时, 使用指针位移的方式进行
空闲列表
JVM维护一个列表, 记录堆内存哪些内存块是可用的, 每次分配完内存之后更新该列表
并发问题
JVM采用CAS保证更新操作的原子性; 另一种方式: 把内存分配动作按照线程划分在不同的空间中进行
对象头设置
对象是哪个类的实例, 哈希码, GC分代年龄...; 至此JVM视角,一个对象已创建完成
从Java程序的视角, 构造函数尚未执行(Class文件中的<init>), 字段默认是0值, 此时程序对象还不能使用
执行实例构造函数<init>()方法
此时该对象才算完全构造出来
对象的内存布局
对象头
运行时数据
官方称之为 "Mark Word", 动态定义的数据结构, 包括: 哈希码, GC分代年龄,锁状态标志, 线程持有的锁, 偏向线程id, 偏向时间戳等等
指向类型元数据的指针
通过这个指针确定该对象是哪个类的实例
如是数组,则还有个数组长度
实例数据
程序代码中所定义的各种类型的字段内容, 无论是父类继承下来的字段还是本类自己的都会记录下来,按照默认的分配策略,相同宽度的字段总是分配到一起
对齐填充
JVM要求对象的起始地址必须是8字节的整数倍, 所以如果对象实例数据没有对齐的话, 就需要通过对齐填充
对象的访问定位
通过栈上的reference数据来操作堆上的具体对象
访问方式
使用句柄访问
Java堆中划分一块内存作为句柄池, reference保存的就是对象的句柄地址,而句柄保存的是实例地址和对应的类型数据地址
优势
reference中存放的是稳定的句柄, 对象被移动的时候, 修改的是句柄中实例对象的指针, reference不必修改
使用直接指针访问
栈上直接存放的就是堆中对象的内存地址, 这种方案就需要考虑类型数据如何存放
优势
访问速度快, 节省了一次指针定位的开销
OutOfMemoryError异常
Java堆溢出
-Xmx 最大堆内存; -Xms初始堆内存
虚拟机栈和本地方法栈溢出
-Xss 设置栈内存容量
字符串常量池
String::intern()
JDK7之前, 把首次出现的字符串实例对象复制到[永久代]中的字符串常量池中, 并返回永久代的实例地址
JDK7以及后面的版本, 字符串常量池移动到了堆中, 那么在需要在常量池中记录字符串首次出现的实例引用即可, 并返回常量池中的地址
方法区溢出
-XX:MaxMetaspaceSize 最大元空间, 以字节为单位; -XX:MetaspaceSize 初始元空间
三. 垃圾收集区与内存分配策略
GC要解决的问题
哪些内存需要回收
什么时候回收
如何回收
哪些对象可以回收
引用计数法
定义
在对象中添加一个引用计数器,当该对象被引用一次,计数器就加1, 引用失效一次,计数器减1, 计数器为0那么该对象就没有被使用
优点
原理简单容易实现,判断效率也很高
缺点
无法解决对象之间互相引用的问题
可达性分析算法
定义
通过一系类称之为"GC Roots" 的对象作为起始节点根据引用关系进行遍历,没有被遍历到的对象则列为可被回收范围
GC Roots
JVM栈中引用的对象. 例如: 方法中使用的参数, 局部变量, 临时变量等
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象
JVM内部的引用, 流入: 基本数据类型对应的Class对象, 系统类加载器
被同步锁持有的对象
反映JVM内部情况的JMXBean, JVMTI中注册回调, 本地代码缓存等
引用的分类
强引用
程序中普遍存在的引用赋值, 只要存在这种关系, 被引用的对象就不会被回收
软引用
一些有用但非必须对象, 将要发生内存溢出异常前,会把这些对象列入回收范围进行第二次回收
常常被用来实现缓存技术, 例如: 图片缓存, 网页缓存
弱引用
非必须对象, 只能生存到下一次GC发生为止
虚引用
无法通过一个虚引用获取一个对象的实例, 唯一的目的只是为了这个对象被GC时可以收到一个系统通知
finalize()方法
finalize()何时可以被执行, 1. 对象的finalize方法没有被复写; 2. 对象的finalize方法已经被JVM调用过. 这两种情况都视为 "没有必要执行"
对象经过可达性分析 进行第一次标记之后, 若需要执行finzlize()方法, 则该对象被放置在F-Queue队列中
稍后会被JVM自建的, 低优先级的Finalizer线程执行对象的finalize()方法,在该方法中可以进行自救(例如把自己的引用赋值给其他变量)
finalize()方法是对象逃脱被GC的最后一次机会, 随后收集器会对F-Queue中的对象进行第二次小规模的标记
任何对象的finalize()方法都只能被JVM自动调用一次
方法区的GC
废弃的常量: 常量池中的对象没被引用, 那么GC该区域的时候, 判定要被回收的话, 则会被清理掉
不再使用的类型
该类所有的实例都被回收
加载该类的类加载器已被回收
该类对应的Class对象没有被其他地方引用
垃圾收集算法
分代收集理论
在大多数程序运行的实际情况的经验上提出的该理论, 建立在两个分带假说
弱分代假说
绝大多数对象是朝生夕灭
强分代假说
熬过多次GC过程的对象就越难消亡
跨代引用假说
跨代引用相对于同代引用来说仅占极少数
GC收集器设计原则: 把Java堆分出不同的区域,然后把回收的对象按照年龄分配到不同的区域中存储
标记-清除算法
首先标记出所有需要回收的对象, 统一回收掉所有被标记的对象
优点
实现简单
缺点
执行效率不稳定
内存空间碎片化问题
标记-复制算法
内存分为两个大小相等的区域, 每次只是用其中一块. 某一块使用完了之后, 就把还存活的对象复制到另一块区域上. 清理掉已经使用的那块
优点
实现简单, 运行高效; 当然若存活对象较多时, 效率也很低
缺点
浪费内存空间
标记-整理算法
把所有的存活对象都移动到内存空间的一端, 然后直接清理掉边界以外的对象
优点
解决了标记-清除的 内存碎片化问题, 和 标记-复制的 浪费内存的问题
缺点
算法相对复杂
内存地址的移动, 增加了GC的停顿时间
HotSpot算法细节
并发和并行
并发: 拥有处理多个任务的能力, 例如 一个单核的CPU, 可以交替执行任务, 那么就是并发
并行: 同一时刻可以执行多个任务, 比如多核CPU就可以在同一时刻执行多个任务, 每个CPU同一时刻各干一件事情
根节点枚举
根节点枚举的过程中, 需要保证对象的引用关系不会发生变化, 存在 "Stop The World"
OopMap
为了解决 避免遍历所有的执行上下文的引用位置, 使用OopMap的结构存放对象的引用
一旦类加载器动作完成时, HotSpot就会把对象中的某偏移量对应的类型计算出来, 使用OopMap保存这些对象的引用
安全点
为了解决 快速准确完成GC Roots的枚举, 引入了安全点, 安全区域
用户线程执行到安全点才会暂停, 安全点位置是为编译器自动插入, 一般地具备"让程序长时间执行的特征", 例如: 方法的调用, 循环跳转, 异常跳转等指令序列复用的地方
用户线程达到安全点并暂停的方案
抢先式中断
系统先把所有的用户线程都中断, 然后筛选出不在安全点上的线程恢复, 然后过一会在对他进行中断, 直到到达安全点
主动式中断
GC收集器需要中断线程的时候, 设置一个标记, 用户线程在执行的过程中不断的轮询这个标记, 发现标记为真则执行到最近的安全点上中断挂起
HotSpot使用内存保护陷阱的方式, 通过一条汇编指令完成轮询和触发线程中断 [test %eax, 0x..... ; {poll}]
安全区域
安全区域能确保在某一段代码片段之中, 引用关系不会发生变化, 例如: 用户线程处于Sleep状态
当用户线程要离开安全区域时, 需要检查JVM是否已经完成了根节点枚举等需要暂停用户线程场景
记忆集和卡表
为了解决对象跨代引用带来的问题, 引入了记忆集, 用于记录非收集区域指向收集区域的指针集合的抽象数据结构
卡表: 针对"记忆集" 这种定义的一个具体实现
HotSpot中设计了 CARD_TABLE 字节数组这个卡表, 大小为512字节, 数组的每个元素标识为内存区域中的一块特定大小内存块
只要这个特定的内存区域中的对象的字段存在跨代指针,则把对应数组元素值标记为1, 该元素称之为变脏, GC时, 只要扫码下卡表中变脏的元素, 然后找出对应的内存区域中的对象加入到 GC Roots中进行扫描
写屏障
为了解决 如何维护卡表元素状态
这里的写屏障可以看做JVM层的"引用类型字段赋值" 这个动作的AOP切面, 引用对象的赋值前后都在写屏障的覆盖范畴内
并发的可达性分析
为了解决 并发遍历对象图, 降低用户线程的停顿. 引入三色标记
按照"是否被GC收集器访问过"这个条件标记为三种颜色
白色: 未被收集器访问过
黑色: 已经被收集器访问过, 并且这个对象的所有引用都已经被扫描过
灰色: 已经被收集器访问过, 但这个对象至少存在一个引用没有被扫描过
对象消失问题, 即原本已经被GC扫码过的对象引用发生了变化 两个条件
赋值器插入至少一条黑色对象到白色对象的引用
赋值器删除了全部从灰色对象到白色对象的直接或者间接引用
解决对象小时问题, 破坏这两个中任何一个条件即可
增量更新
黑色对象一旦新插入了指向白色对象的引用之后, 他就变成灰色对象
原始快照(SATB)
无论引用关系删除与否, 都会按照刚开始扫描的时的对象图快照来进行搜索
垃圾收集器
Serial 收集器
单线程工作的 收集新生代内存的收集器, 使用标记-复制算法
实现简单,高效; 但是用户线程停顿时间较长
ParNew 收集器
其实是一个多线程版本的Serial收集器, 支持多线程并行收集, 使用标记-复杂算法
目前仅有他能跟CMS收集器配合
Parallel Scavenge 收集器
达到一个可控的吞吐量, 支持多线程
吞吐量 = 运行用户代码时间/ (运行用户代码时间+运行垃圾收集的时间)
通过具体的参数可以进行精确控制吞吐量, 使用标记-复杂算法
Serial Old 收集器
是Serial收集器的老年代版本, 使用标记-整理算法
Parallel Old 收集器
是Parallel Scavenge 的老年代版本, 支持多线程并行收集,使用标记-整理算法
CMS 收集器
Concurrent Mark Sweep 以获取最短回收停顿时间为目标的收集器
步骤
初始标记: 单线程执行, 暂停用户线程, 标记一下GC Roots能直接关联的对象, 速度很快
并发标记: 多线程并发从GC Roots的直接关联的对象遍历整个对象图的过程
重新标记: 多线程执行, 暂停用户线程, 修正并发标记过程中已经被标记却发生变动的对象
并发清除: 并发清理掉已经被标记的对象
优点
并发收集, 低停顿
缺点
对处理器资源比较敏感, 跟用户线程存在竞争关系
无法处理"浮动垃圾" 只能等到下次GC时被回收
采用"标记-清除"算法, 会产生内存空间的碎片化
只能跟 Serial , ParNew 收集器配合使用, 不能跟新生代的 Scavenge收集器一起使用
G1 收集器
概念
面向局部收集的设计思路和基于Region的内存布局形式, 追求应用的分配速率,而非把Java堆一次性清理干净,是垃圾收集器技术发展史上的里程碑
按照职责分离的原则, 在JDK10中, 提出了 统一垃圾收集器接口(JVMGI)
停顿时间模型: 能够支持在一个长度为M毫秒的时间片段内, 垃圾回收所用的时间不超过N毫秒的目标
Region: 把连续的Java堆划分为多个大小相等的独立区域, 每个Region都可以根据需要, 扮演Eden区, Survivor区, 老年代, 收集器可针对不同角色的Region使用不同的策略进行垃圾回收
Humongous: 存储大对象, 合并多个Region存放这些大对象, 只要超过Region的一半可判定为大对象, 每个Region大小可通过参数设定
G1 会跟踪统计每个Region的回收的"价值" 和 "收益", 根据用户设定的收集停顿时间, 优先处理回收价值最大的那些Region
待解决的问题
跨Region引用对象的处理 : 记忆集和卡表 解决
并发标记阶段如何保证收集线程和用户线程互不干扰: 采用 原始快照算法实现
如何建立可靠的停顿预测模型: 通过衰减均值理论实现, Region的统计状态越新越能决定他的回收价值
4个步骤
初始标记: 单线程执行, 暂停用户线程, 标记GC Roots 直接关联的对象, 并修改TAMS只针对的值
并发标记: 多线程并发从GC Roots的直接关联的对象遍历整个对象图的过程
最终标记: 短暂暂停用户线程, 并行处理并发标记阶段留下的SATB记录
筛选回收: 更新Region的统计数据, 对各个Region的会价值和成本进行排序, 根据设定的期望停顿时间来制定回收计划, 可选择多个Region组成回收集, 把其中存活的对象复制到空的Region中,回收掉旧的Region, 这个过程需要移动存活对象, 需要暂停用户现场
优点
可指定最大停顿时间
分Region的内存布局, 按收益动态确定回收的创新设计带来巨大优势
G1 从整体看是基于"标记-整理"算法实现的, 从局部(两个Region)看又是基于"标记-复制"算法实现的.
缺点
G1 收集器执行, 更加消耗内存, 额外负载也比较高
Shenandoah 收集器
目标是能在任何堆大小下都能把垃圾收集的停顿时间限制在十毫秒以内
9个步骤
初始标记: 单线程执行, 暂停用户线程, 标记GC Roots 直接关联的对象, 并修改TAMS只针对的值
并发标记: 多线程并发从GC Roots的直接关联的对象遍历整个对象图的过程
最终标记: 短暂暂停用户线程, 并行处理并发标记阶段留下的SATB记录
并发清理: 清理掉那些整个区域中都不存在一个存活对象的Region
并发回收: 多线程并发把收集的存活对象复制到未被使用的Region中, 使用读屏障和"BrooksPointers" 的转发指针来处理用户线程还在改变引用的问题
初始引用更新: 短暂暂停用户线程,建立一个线程集合点, 确保所有的并发回收阶段中收集线程都已经完成分配给他们的对象移动任务.
并发更新引用: 多线程并发执行 真正把堆中所有指向旧对象的引用修正到复制后的新地址
最终引用更新: 暂停用户线程, 修正存在于 GC Roots 中的引用, 暂停时间和GC Roots中的数量成正比
并发清理: 整个回收集中的Region已无存活对象, 清理回收掉这些Region即可
ZGC 收集器
基于Region内存布局,不设置分代, 使用了读屏障, 染色指针和内存多重映射等技术实现的可并发标记-整理算法的, 以低延迟为首要目的收集器
4大步骤
并发标记: 跟G1一样, 需要经过初试标记, 最终标记
并发预备重分配: 根据特定的查询条件得出收集过程要清理哪些Region, 把这些Region组成 重分配集
并发重分配: 把重分配集中存活对象复杂到新的Region中,并为重分配集中的每个Region维护一个转发表, 记录从旧对象到新对象的转向关系
并发重映射: 修正整个堆中指向重分配集中旧对象的所有引用
如何选择合适的收集器
收集器的权衡
应用程序关注是什么? 例如: 数据分析, 科学计算, 那就关注吞吐量; 若是客户端应用程序需要关注内存的占用
运行应用的基础设施佮? 例如: 硬件规格
使用JDK的版本发行商?
JVM收集器日志
查看GC基本信息
< JDK9 使用 -XX:+PrintGC
>= JDK9 使用 -Xlog:gc
查看GC详细信息
< JDK9 使用 -XX:+PrintGCDetails
>= JDK9 使用 -Xlog:gc*
查看GC前后堆, 方法区可用容量变化
< JDK9 使用 -XX:+PrintHeapAtGC
>= JDK9 使用 -Xlog:gc+heap=debuge
内存分配与回售策略
自动管理内存的目标
自动给对象分配内存
自动回收掉无用对象的内存
对象的生命旅程
对象优先分配在Eden区域
大多数情况下, 对象在新生代Eden区中分配, 当Eden区域没有足够的空间时, 则发送一次Minor GC
大对象直接进入老年代
创建的大对象, 需要连续的内存空间, 指定大于-XX:PretenureSizeThreshold 参数的对象, 直接在老年代进行分配
长期存活的对象进入老年代
诞生于Eden中的对象, 经过Minor GC 一次之后, 对象的年龄就会+1 (年龄保存在对象头中), 当年龄增加到 -XX:MaxTenuringThreshold 参数(默认:15)设置的值时,晋升到老年代
动态对象年龄判定
在Survivor空间中的相同年龄的对象大小总和大于单个Survivor空间的一半, 则年龄>=该年龄的对象直接进入老年代, 无须关心年龄阀值
空间担保
发生Minor GC之前, JVM先检查老年代的最大可用的连续空间是否大于新生代对象的总和或者是历次晋升的平均大小, 就会进行Minor GC, 否则直接Full GC
四. JVM性能分析工具
jps: JVM进程状况工具
列出正在运行的虚拟机进程, 常用命令: jps -lvm
jstat: JVM统计信息监控工具
监视虚拟机各种运行状态信息的命令行工具, 常用命令: jstat -gcutil pid 1000
jinfo: Java配置信息工具
实时查看和调整虚拟机参数, 常用命令: jinfo -flags pid
jmap: Java内存映像工具
生成堆转储快照,常用命令: jmap -dump:format=b,file=xxx.bin pid
jhat: 堆内存快照分析工具
内置了一个web服务器, 生成的堆快照可以使用浏览器进行分析
jstack: Java堆栈跟踪工具
生成当前时刻JVM的线程快照
五. 调优案例分析域实战
Java虚拟机管理大内存
回收大块堆内存导致的长时间停顿
大内存必须要有64位Java虚拟机的支持
必须保证应用程序的足够稳定
相同的程序再64位虚拟机中消耗的内存比32位要大
若干虚拟机独立部署应用
节点竞争全局资源
很难高效率利用某些资源池
32位Java虚拟机收到系统的内存限制
大量使用本地缓存, 造成内存浪费
六. 类文件结构<br>
两种基础数据类型
无符号数
基本的数据类型: 以u1, u2, u4, u8 来分别表示1个字节, 2个字节, 4个字节, 8个字节的无符号数
表
多个无符号或者其他表作为数据项构成的复合数据类型, 便于区分, 命名习惯性地以_info 结尾
Class文件内容剖析
魔数与Class文件的版本
每个Class文件的前4个字节称为魔数
唯一的作用就是确定是否为一个能被虚拟机接受的Class文件, 值固定为: 0xCAFEBABE
次版本号
第5, 6 两个字节, JDK1.2-JDK12 之间这个值一直为0 , JDK12后又再次启用这个 次版本号
主版本号
第7,8 两个字节 对应JDK的版本号, 每个JDK都对应一个版本号, 例如: JDK8 = 52
WinHex 可以打开16进制的Class文件
常量池
常量池容量计数值
主版本号后面是常量池的入口,放置的是一个u2类型的数据, 从1开始计数,代表常量池常量数量
把0项空出来的目的在于若果后面某些指向常量池的索引值的输在在特定的情况下需要达到"不引用任何一个常量池项目"的含义,可以把索引值设置为0<br>
常量池
可以比喻为Class文件的资源仓库
字面量
接近Java语言层面的常量的概念, 如文本字符串, final修饰的常量
符号引用
被模块导出或者开放的包
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
方法句柄和方法类型
动态调用点和动态常量
项目类型
常量池中每个项目都是一个表, 截止JDK13 共有17种常量类型
17种常量类型的表结构第一个字节都是u1类型的标志位(tag) , 标记属于哪种常量类型
图示
分支主题
访问标志
常量池结束之后, 跟着两个字节是访问标志, 类或者接口的访问信息, 包括 Class还是Interface, public类型, abstract类型, final类型
ACC_PUBLIC
0x0001 : 是否为public类型
ACC_FINAL
0x0010: 是否被声明final
ACC_SUPER
0x0020: 是否允许使用invokespecial指令新语义, 1.0.2后编译的此标记都为真
ACC_INTERFACE
0x0200: 标识是个接口
ACC_ABSTRACT
0x0400: 是否为abstract类型
ACC_SYNTHETIC
0x1000: 标识这个类并非用户代码生成
ACC_ACCOTATION
0x200: 标识这是一个注解
ACC_ENUM
0x4000: 标识是个枚举
ACC_MODULE
0x8000: 标识是个模块
类索引, 父类索引, 接口索引集合
类索引
用于确定这个类的全限定名
父类索引
用于确定这个类的父类的全限定名, 除了Object类之后其他所有的类的父类索引都不为0
接口索引集合
入口有一项u2类型的数据为接口计数器, 若该类没有实现任何接口, 则该计数器为0, 后面的接口集合不占用任何字节
类索引和父类索引各使用一个u2类型的索引值表示, 各自指向一个类型为CONSTANT_Class_info的类描述符常量, 通过该常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中断呃全限定名字符串
字段表
概念
用于描述接口或者类中声明的变量, 包括类级别的变量和实际级别的变量
字段表结构
包括字段的作用域(private, protected , public), 实例变量还是类变量(static), 是否可被序列化(transient), 可变性(final), 并发可见性(volatile), 字段类型(基本类型, 对象, 数组), 字段名
字段表集合中不会列出从父类和父接口中继承而来的字段, 但有可能出现原本Java代码中不存在的字段
方发表集合
用于描述类中的方法的相关描述
依次包括 访问标志, 名称索引, 描述符索引, 属性表集合 等
方法里面的Java代码, 编译为字节码指令之后, 存放在方法属性表中一个名为"Code"的属性里面
如果没有重写父类的方法, 在方法表集合中就不会出现来自父类的方法信息, 但是有可能出现有编辑器自动添加的方法, 例如: 类构造器<clinit>() 方法, 实例构造器<init>() 方法
属性表集合
每个属性, 它的名称都要从常量池中引用一个CONSTANT_Uft8_info类型的常量来表示,属性值的结构则是完全自定义,通过一个u4长度的属性说明属性值占用的位数即可
Code属性
Java程序中方法体中的代码被编译后, 最终的字节码指令存储在Code属性中
但是并不是所有的方法表都存在这个属性, 例如接口或者抽象类中的方法就不存在Code属性
Exceptions属性
在方法表与Code属性平级的一项属性, 作用是列举出所有的可能抛出的手受查异常, 即在throws关键字后面列举的异常
LineNumberTable属性
描述Java源代码行号与字节码行号(字节码偏移量)之间的关系
LocalVariableTable属性
描述栈帧中局部变量表的变量和Java源码中定义的变量之间的关系
LocalVariableTable属性
JDK5之后引入了泛型,增加了这个属性, 由于描述符中的泛型参数化类型被擦除, 所以使用了字段的特征签名来完成泛型的描述
SourceFile属性
用于记录生成这个Class文件的源码文件名称
SourceDebugExtension属性
用于存储额外的代码调试信息
ConstantValue属性
通知虚拟机自动给static的变量进行赋值
InnerClasses属性
记录内部类和宿主类之间的关联
Deprecated属性
属于标志性的布尔属性, 只有存在和不存在的区别, 表示某个类, 字段或者方法已不对推荐使用
Synthetic属性
表示某字段或者方法并不是有Java源代码生成的, 而是编译器自己添加的
StackMapTable属性
JDK6新增的, 这个属性在虚拟机类加载的字节码验证阶段被新类型检查验证器使用, 目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器
Signature属性
JDK5新增, 任何类,接口, 初始化方法, 成员的泛型签名如果包含了类型变量或参数转化类型, 则Signature属性会记录泛型签名信息
Bootstrapmethods属性
JDK7新增, 用于保存invokedynamic指令引用的引导方法限定符
MethodParameters属性
JDK8新增, 记录方法的形参名称和信息
字节码指令
定义
指令由一个字节长度的, 代表着某种特定的操作含义的数字(操作码)以及跟随其后的0至多个代表此操作所需的参数(操作数)构成
JVM采用面向操作数栈而不是面向寄存器的架构, 所以大多指令都不包含操作数, 仅有操作码, 指令的参数放在操作数栈中
字节码与数据类型
大多数的指令都包含其操作对应的数据类型信息
例如: iload指令用于从局部遍历表中加载int型的数据到操作数栈中
fload指令则是把float类型的数据加载到操作数栈
操作码助记符中都有特殊的字符
i代表对int类型的数据操作, l表示long, s表示short, b表示byte, c表示char, f表示float, d表示double, a表示reference
9大类
加载和存储指令
加载和存储指令用于把数据在栈帧中的局部遍历表和操作数栈之间传递
将一个局部变量加载到操作数栈
iload,iload_<n>,lload, lload_<n>, fload, fload_<n>, dload, dload_<n>, aload, aload_<n>
将一个数值从操作数栈存储到局部变量表
istore,istore_<n>,lstore, lstore_<n>, fstore, fstore_<n>, dstore, dstore_<n>, astore, astore_<n>
将一个常量加载到操作数栈
pipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, iconst_ml, iconst_<i>, loconst_<l>, fconst_<f>, dconst_<d>
扩充局部变量表的访问索引指令
wide
说明
上面的指令中有一部分是<n>, 实际上表示的是一组命令, 例如: iload_<n> 代表了iload_0, iload_1, iload_2, iload_3 这几个指令, 这种表示只是iload的一种特殊形式,后面的数字其实表示的是 操作数. 例如: iload_0 等价于 iload 0
运算符指令
算术指令用于对两个操作数栈上的值进行某种运算,并把结果重新存入操作数栈顶
加减乘除指令
iadd, ladd, fadd , dadd, isub, imul, idiv ...
求余取反位移
irem, lrem, frem, drem, ineg, ishl
按位或
ior, lor
按位与
iand, land
按位异或
ixor, lxor
局部变量自增
iinc
比较指令
dcmpg, dcmpl, fcompg, fcmpl, lcmp
类型转换指令
可以把两个不同类型的数值类型进行互相转化
宽化类型转换
小范围类型向大范围类型安全的转化, 无须显示第转化指令
int -> long, float , double
long -> float, double
float -> double
窄化处理转换
必须显示地使用转化指令来完成
i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l, d2f
可能导致转化结果产生不同的正负号, 不同的数量级的情况, 以及精度缺失
对象创建与访问指令
创建类实例指令
new
创建数组的指令
newarray, anewarray, multianewarray
访问类字段, 和实例字段的指令
getfield, putfield, getstatic, putstatic
把一个数组元素加载到操作数栈的指令
bdload, caload, saload, iaload, laload, faload, daload, aaload
把一个操作数栈的值存储到数组元素中的指令
bastore, castore, sastore, iastore, fastore, dastore, aastore
取数组的长度的指令
arraylength
检查类实例类型的指令
instanceof, checkcast
操作数栈管理命令
栈顶一个或两个元素出栈
pop, pop2
复制栈顶一个或两个元素, 并把复制的元素压入栈顶
dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2
将栈顶的两个元素互换
swap
控制转移指令
有条件或无条件地从指定位置指令的已下条指令开始执行
条件分支
ifeq, iflt, ifle, ifne, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_ifmpgt, if_icmple, if_icmpge, if_acmpeq, if_acmpne
复合条件分支
tableswitch, lookupswitch
无条件分支
goto, goto_w, jsr, jsr_w, ret
方法调用和返回指令
invokevirtual
用于调用对象的实例方法, 根据对象的实际类型进行分派
invokeinterface
用于接口方法的调用, 在运行时搜索一个实现了这个接口的方法对象, 找出适合的方法调用
invokespecial
用于调用一些特殊处理的实例方法, 包括: 实例初始化方法和私有方法, 父类方法
invokestatic
调用类的静态方法
invokedynamic
用于在运行时动态解析出调用点限定符所引用的方法
异常处理指令
athrow指令来完成实现, 在Java虚拟机中处理异常catch采用异常表类完成
同步指令
Java语言中的synchronized语句块来表示的, 指令集中有monitorenter和moitorexit两条指令来支持者语义的<br><br>
方法级别的同步是隐式的, 无需通过字节码指令来控制, JVM可以通过常量池中的方法表结构ACC_SYNCHRONIZED访问标志符确定一个方法是否声明为同步, 方法级别的同步与代码块级别的都使用管程(Monitor)来实现
0 条评论
下一页