JVM系列 01 介绍
文章目录
一)简介
摘录
跨平台,“一次编写,到处运行”
相对安全的内存管理和访问机制,避免绝大部分的内存泄漏和指针越界问题
热点代码检测和运行时编译及优化
完善的API
Java技术体系 = Java程序设计语言,各平台JVM,Class文件结构,Java API,第三方类库
JDK = Java程序设计语言,JVM,Java API类库
JRE = Java API的SE,JVM
Java技术体系分为4个平台 = Card,ME,SE,EE
Java发展史/版本特征
JVM发展史:如HotSpot VM等
《深入理解Java虚拟机》Java待实现技术:模块化,混合语言,多核并行,丰富语法,64位虚拟机
二)关注
1. 运行时数据区
1.1 程序计数器
较小的内存空间
看作当前线程所执行的字节码的行号指示器
虚拟机的概念模型:字节码解释器,工作时通过改变这个计数器的值,选择下一条需执行的字节码指令,分支 循环 跳转 异常处理 线程恢复等基础功能,都需要依赖这个计数器来完成
JVM的多线程,通过线程轮流切换并分配CPU时间片的方式,来实现
单核如上,多核就是真正的并行
线程私有
线程执行Java方法,计数器记录正在执行的虚拟机字节码指令的地址
线程执行Native方法,计数器为空(Undefined)
1.2 Java虚拟机栈
同程序计数器,Java虚拟机栈也是线程私有,生命周期和线程相同
虚拟机栈描述的是:Java方法执行的内存模型。每个方法执行时,会创建一个栈帧用于存储“局部变量表、操作数栈、动态链接、方法出口等信息”;方法从调用至完成,对应着一个栈帧从入栈到出栈
局部变量表,存放了编译期可知的各种基本数据类型、对象引用类型、returnAddress类型。
64位的long和double类型数据,会占用2个局部变量空间(Slot),其余占用一个。
局部变量表所需内存空间,在编译期间完成分配,运行期不改变其大小。
JVM规范中,对这个区域规定了两种异常情况:栈溢出,内存溢出
1.3 本地方法栈
类Java虚拟机栈(为Java方法,字节码服务),而本地方法栈为Native方法服务
JVM规范中,对这个区域规定了两种异常情况:栈溢出,内存溢出
1.4 堆
堆,是JVM管理的内存中最大的一块。
线程共享。在JVM启动时创建,唯一目的就是存放对象实例。
JVM规范:所有对象实例及数组都要在堆上分配。
随着JIT编译期的发展以及逃逸分析技术逐渐成熟,栈上分配 标量替换优化技术导致“堆上分配对象”不再那么绝对
堆,是垃圾收集器管理的主要区域。
收集器基本采用分代收集算法,所以堆可细分为:新生代,老年代。
新生代可再分:Eden空间,From Survivor空间,To Survivor空间等
从内存分配角度,线程共享的堆可能划分出多个线程私有的分配缓冲区(TLAB)
JVM规范:堆可处于物理上不连续的内存空间,只要逻辑上连续即可。
固定大小或可扩展(主流JVM,通过-Xmx和-Xms控制堆大小)
堆无法扩展时,将会抛出内存溢出异常
1.5 方法区
线程共享。用于存储已被JVM加载的“类信息,常量,静态变量,即时编译器编译后的代码等数据”
习惯于HotSpot虚拟机开发部署的Programmer,称“方法区”为“永久代”,两者并不等价,原因是HotSpot将GC分代收集扩展至方法区。
永久代实现的方法区,更容易遇到内存溢出问题。
HotSpot使用元空间取代永久代,即方法区的实现——元空间
永久代,物理上是堆的一部分,和新生代、老年代是连续的
元空间,属于本地内存,存储内容不同(元空间存储类的元信息,静态变量和常量池等并入堆中)
运行时常量池
属于方法区的一部分。
Class文件除了有“类版本、字段、方法、接口等描述信息外,还有一项信息为常量池”
常量池:存放编译期生成的各种字面量和符号引用
类加载进入方法区,常量池内容(字面量,符号引用)存放到运行时常量池
符号引用翻译出来的直接引用,也存储在运行时常量池中
运行时进入常量池:如String类的intern()方法
1.6 其他
直接内存
不是JVM运行时数据区的一部分。
JDK 1.4引用了NIO类,基于通道与缓冲区的IO方式,可使用Native函数库直接分配堆外内存
堆外内存,通过一个存储在Java堆中的DirectByteBuffer对象,作为堆外内存的引用进行操作。
避免Java堆和Native堆来回复制数据
2. 对象深入
基于HotSpot虚拟机
2.1 对象的创建
创建对象(如克隆、反序列化)通常仅仅依赖一个new关键字
局限于普通Java对象,不包括数组和Class对象等
JVM如何处理?
-
new指令
-
类加载检查:JVM检查该指令的参数(类),是否能在(运行时)常量池中定位到一个类的符号引用
检测这个符号引用代表的类,是否已被加载、解析和初始化过 若没有,先执行相应的“类加载过程” 类加载会执行<client>,进行静态变量/代码块的执行
-
分配内存:
堆中内存:垃圾回收器是否带有压缩整理功能 指针碰撞:堆使用和未使用的内存泾渭分明,中间放着一个指针作为分界点的指示器 分配内存就是移动指针(指示器) 空闲列表:一个列表,记录所有可用的内存块 并发分配,不安全 CAS + 失败重试:保证更新操作的原子性 堆中的线程私有的分配缓冲区,TLAB + 同步锁定
-
初始化零值:不包括对象头
-
设置对象头:对象所属类、如何找到类的元数据信息、对象哈希码、GC分代年龄等
JVM当前运行状态不同,如是否启用偏向锁等,对象头会有不同的设置方式
-
执行
<init>
方法:由invokespecial指令决定
<client>和<init>见下文
2.2 对象的内存布局
对象头
包括两部分:
第一部分,Mark Word:存储对象自身的运行时数据(哈希码、GC分代年龄,锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等)
第二部分,类型指针:对象指向它的类元数据的指针
若对象是一个数组,对象头还有一块用于纪律数组长度的数据
实例数据
各种类型的字段:从父类继承而来,或子类定义
对齐填充
非必须。占位符,保证对象大小必须是8字节的整数倍
2.3 对象的访问定位
句柄
栈上引用
堆:句柄池(到实例数据的指针,到类型数据的指针),实例池
方法区:对象类型数据
直接指针
栈上引用
堆:到类型数据的指针,实例数据
3. 垃圾收集
3.1 垃圾收集区域
- 堆
- 方法区
3.2 垃圾对象探测
引用计数法
对象中添加一个引用计数器
可达性分析算法
一系列GC Roots
对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链。
若一个对象到GC Roots对象,没有任何引用链相连,证明此对象不可用
问题1:相互循环引用
Gc Roots对象
- 虚拟机栈引用的对象
- 方法区中类静态属性引用的变量
- 方法区中常量引用的对象
- 本地方法栈JNI(Native方法)引用的对象
四种引用类型
-
强:如
Object o = new Object()
。只要强引用存在,GC不会回收被引用的对象 -
软:有用但并非必需的对象。系统内存溢出前,会回收
-
弱:非必需对象。被弱引用引用的对象,只能生存到下一次GC发生前
JDK 1.2后,提供
WeakReference
类 -
虚:不影响引用对象的生存。目的是引用对象被回收时,能收到一个系统通知
JDK 1.2后,提供
PhantomReference
类
垃圾对象的二次标记
- 与GC Roots对象不相连,进行第一次标记
- 进行筛选:垃圾对象没有覆盖
finalize()
,或JVM已经调用过该对象的finalize()
- 没有必要执行
finalize()
,
- 没有必要执行
- 有必要执行
finalize()
,对象被放入F-Queue队列,稍后由一个JVM自动建立、低优先级的Finalizer
线程执行它 - 对F-Queue队列中的剩余对象进行第二次标记:执行
finalize()
,且重新与GC Roots
对象相连,可免去回收,重新变成可用对象
方法区回收
- 废弃常量
- 无用类
如String,常量池对象没有被引用——废弃String常量
无用类判断:满足下述3条件的无用类,可以回收,并非必然回收
1. 类所有实例,都被回收
2. 该类的ClassLoader已被回收
3. 该类的Class对象,没有被引用,无法在任何地方通过反射访问该类方法
3.3 垃圾收集算法
算法 | 描述 | 优缺 |
---|---|---|
标记—清除 | 1. 标记;2. 清除 | 效率低;产生内存碎片 |
复制 | 等分(优化:8-1-1) | 效率低;可用内存变少 |
标记-整理 | 1. 标记;2. 移动;3. 清理 | |
分代收集 | 新生代——复制;老年代——标记清除/整理 |
标记-清除算法
复制算法
标记-整理算法
3.4 垃圾收集器
理解不够,暂略
GC | 介绍 | 区域 |
---|---|---|
Serial | 单线程收集器,收集会暂停所有工作线程 | 新生代 |
ParNew | Serial的多线程版本,s-t-w | 新生代 |
Parallel | 并行 | 新生代 |
Serial Old | Serial的老年代版本 | 老年代 |
Parallel Old | ||
CMS | 并发收集器 | 老年代 |
G1 |
3.5 内存分配策略
- 对象优先在Eden
- 大对象直接进入老年代
- 长期存活的对象将进入老年代
- 动态对象年龄判定:Survivor空间中,相同年龄所有对象大小综合大于Survivor空间的一半,年龄大于等于该年龄的对象,都直接进入老年代
3.6 内存回收策略
Minor GC
老年代最大可用的连续空间,大于新生代所有对象总空间
Minor GC可确保是安全的
不成立,JVM查看"HandlePromotionFailure"是否允许担保失败
允许,检查老年代最大可用连续空间,是否大于“历次晋升到老年代对象的平均大小”
大于,尝试Minor GC(存在风险)
小于,Full GC
不允许,Full GC
Full/Major GC
回收老年代。
3.7 GC日志
一条日志大体包括如下:详细请自行查阅。
- GC发生的时间
- GC停顿类型:GC或Full GC(stop-the-world)
- GC发生区域:
- GC前已使用容量→GC后已使用容量(总容量)
- 堆
- GC前已使用容量→GC后已使用容量(总容量)
- 此次GC占用时间
4. 工具
4.1 JDK命令行工具
命令 | 介绍 |
---|---|
jps | JVM进程 |
jstat | JVM运行状态信息(类加载、内存、GC、JIT等) |
jinfo | JVM配置信息 |
jmap | 获取堆转储快照等 |
jhat | 堆转储快照分析工具 |
jstack | Java堆栈跟踪工具 |
4.2 JDK可视化工具
JConsole
JDK 5提供的虚拟机监控工具
VisualVM
JDK 6多合一故障处理工具(监控、故障、性能分析等)
4.3 反编译字节码
- javap
- JD-GUI
- jclasslib
- …
5. 类文件结构
底层系统→适配JVM→跨平台字节码→各种编译器→各种语言程序
5.1 Class是什么
Class文件是一组以8位字节为基础单位的二进制流,其数据按序紧凑排列
5.2 class文件结构
- 无符号数:属于基本数据类型,以"u1, u2, u4, u8"代表字节长度,可用来描述数字、索引引用、数量值、UTF-8编码的字节串值
- 表:由多个无符号数,或其他表作为数据项构成的复合数据类型
常量池
- 字面量:如文本字符串、声明为final的常量值等
- 符号引用:类和接口全限定名、字段的名称和描述符、方法的名称和描述符
访问标志
标识类、接口层次的访问信息,包括:
- Class是类还是接口
- 是否定义为public
- 是否定义为abstract
- 若是类,是否声明为final
- …
5.3 字节码指令
JVM的指令,由一个字节长度的操作码,零或多个操作数构成。
JVM面向操作数栈,而不是寄存器架构
- 加载和存储指令
- 运算指令
- 类型转换指令
- 对象创建和访问指令
- 操作数栈管理指令
- 控制转移指令
- 异常处理指令
- 同步指令
同步指令:管程
JVM支持方法级和方法内部的同步,都是用管程(Moitor)支持
方法级同步,隐式,即无须通过字节码指令来控制
方法常量池的方法表中的ACC_SYNCHRONIZED访问标志
方法内同步,显示,需Java语言中的synchronized语句块
JVM的指令集使用两条指令支持synchronized关键字的语义
- monitorenter
- monitorexit
6. 类加载机制
类加载:JVM把描述类的数据,从Class文件加载到内存,并对数据进行:
“校验、转换解析和初始化”。
最终形成可以被虚拟机直接使用的Java类型。
Class文件:一串二进制的字节流。(来自磁盘.class文件、网络、压缩包等)
6.1 生命周期
加载:
连接:包括“验证、准备、解析”三个部分
验证:
准备:
解析:
初始化
使用和卸载
6.1.1 各个阶段的顺序如何?
加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的。
而:解析阶段不一定,发生在“初始化前或后”
在初始化前开始,即一般流程
在初始化后开始,为支持“运行时绑定”(或动态绑定、晚期绑定)
另,开始即意味不是进行、完成,仅仅是开始解析阶段
6.2 类加载时机
有且只有5种情况的“初始化”——主动引用类
未发生初始化,遇到以下5种情况,需立即初始化
1)遇到new、getstatic、putstatic、invokestatic这四条字节码指令
new对象,调用类成员(包括静态变量、静态方法)
2)反射
3)初始化类时,发现父类未初始化,需要先初始化其父类
4)JVM启动,指定的主类——main(),先初始化
5)JDK 7的动态语言支持
说明:final static字段,在编译期被放入常量池,不会导致初始化
无初始化——被动引用类
1)子类引用,访问父类静态字段,不会导致子类初始化
2)数组定义引用的类,不会触发初始化
T[] arr = new T[n];
不会触发类T的初始化
3)调用一个类的常量字段,不会初始化该类
class A{final a..}
class B{ main(){ System.out.println(A.a);} }
A.a,实际上在编译阶段通过常量传播优化,常量值被存储到B类的常量池种
调用相当于B类对自身常量池的引用,与A类毫无关系
接口——初始化
主要在于第3)种情况:初始化类时,发现父类未初始化,需要先初始化其父类
在接口中,初始化子接口时,不要求其父接口已完成初始化,只要在真正使用到父接口,如:
引用父接口中定义的常量。才会初始化父接口
6.3 类加载过程
6.3.1 加载
- 类全限定名,获取此类二进制字节流
- 字节流,转化为方法区的运行时数据结构
- 内存中,生成对应
Class
对象,作为方法区该类各种数据的访问入口
java.lang.Class对象存储区域
JVM没有明确规定,Class对象存放在堆中
HotSpot虚拟机,Class对象存放在方法区
6.3.2 验证
- 目的:确保Class文件的字节流包含的信息,符合当前虚拟机的要求,不会危害虚拟机自身安全
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证:将符号引用转化为直接引用,发生在解析阶段
6.3.3 准备
-
目的:正式为类变量分配内存,并设置其零值(初始值)
传统方法区实现——永久代,静态变量和常量池存入方法区 新的实现——元空间,静态变量和常量池等并入堆中
6.3.4 解析
- 目的:常量池内的符号引用,替换为直接引用(运行时常量池)
6.3.5 初始化
-
目的:执行类构造器
<client>()
方法的过程<client>由下述语句合并: 类变量的赋值动作 静态语句块的语句 收集顺序,由语句在源文件出现顺序决定 1. 静态语句块,只能访问到定义在之前的变量 2. 静态语句块,对于定于在之后的变量,可以赋值,但是不能访问 3. 父类的<client>先于子类
<client>
例子
static class A {
static {
i = 0;
}
static int i = 1;
}
输出:i=1
6.4 类加载器
作用:实现类的加载动作。
类在JVM中的唯一性确定
- 类加载器
- 类本身
两个不同类加载器,加载同一个类,最终在JVM中是两个类
双亲委派模型
- 启动类加载器:
BootStrap ClassLoader
。C++实现,是JVM一部分 - 其他类加载器:由Java实现,独立于JVM
- 扩展类加载器:
Extension ClassLoader
- 应用程序类加载器:
Application ClassLoader
- 自定义类加载器:可选
- 扩展类加载器:
双亲委派模型的工作过程:
一个类加载器,收到类加载的请求,委派给父类加载器去完成,
当父类加载器反馈无法完成这个加载请求,子加载器才尝试自己加载
根类加载器,即启动类加载器,执行加载类
破坏双亲委派模型
- JDK 1.1和JDK 1.2的先后关系,具体略
- 线程上下文类加载器,接口提供者(SPI, Service Provider Interface)
- 动态性,如代码热替换、模块热部署等。导致双拼委派模型的树形结构→网状结构
7. 字节码执行引擎
7.1 运行时栈帧结构
栈帧,是JVM运行时数据区的虚拟机栈的栈元素。
栈帧存储了方法的局部变量表、操作数栈、动态链接、方法返回地址等信息
局部变量表
局部变量表,是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
-
最小单位:变量槽,Slot。一个Slot可存放一个32位以内的数据类型
- 32位以内,一个slot存储
- 64位的long和double,两个slot存储
局部变量表中第0位索引,默认用于传递方法所属对象实例的引用,方法中以this表示
操作数栈
操作数栈每个元素可以是任意的Java数据类型,包括long和double
动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。(运行期,符号引用转为直接引用,称为动态链接)
方法返回地址等
7.2 方法调用:摘录
确定被调用方法的版本(即调用哪一个方法)
类加载解析阶段:解析
方法调用中的目标方法,在Class文件里都是一个常量池的符号引用。
类加载的解析阶段,一部分符号引用直接转化为直接引用。前提条件:
- 方法在运行前,就有一个可确定的调用版本,且运行期不可改变
- 静态方法:与类相关
- 私有方法:外部不可访问
- …等非虚方法
分派:运行还是编译阶段?
- 静态分派:方法重载,编译阶段
- 动态分派:方法重写
执行引擎执行代码
- 解释执行:解释器执行字节码
- 编译执行:即时编译器产生本地代码执行
三)程序编译与代码优化
1. 概述
JVM在编译期,能干些什么
编译期是一段不确定的操作过程,可能执行如下操作:
前端编译器或编译器的前端,将.java转变成.class文件的过程
后端运行期编译器(JIT,Just In Time),将.class转变成本地机器码的过程
静态提前编译器(AOT,Ahead Of Time),直接把.java转变成本地机器码的过程
优化发生在哪个阶段
在编译期,运行效率几乎无优化。大部分都是发生在运行期,收益更多(.class跨平台,可能来自Java Scala等语言)。
Sun的Javac编译器,通过“语法糖”,使编码更简单。
2. 编译期优化
早期优化。
Javac编译器
由Java编写实现。
编译过程
1和2可能循环执行,主要看2是否对抽象语法树有更新
- 解析与填充符号表
- 词法、语法分析
- 填充符号表
- 插入式注解处理器的注解处理:编译期处理(大部分注解在运行期起作用)
- 分析与字节码生成
语法糖
- 泛型
- 自动拆装箱
- 遍历循环
- 条件编译
3. 运行期优化
运行期最初,由解释器进行解释执行。
- 热点代码:为提升效率,运行时,热点代码会被编译成本地机器码(由JIT编译器完成)
热点代码
- 多次调用的方法
- 多次执行的循环体:依然以方法为单位作为编译对象
热点探测判定方式
- 基于采样的热点探测
- 基于计数器的热点探测
4. 编译优化技术
略。
逃逸分析
四)高效并发
1. 了解
硬件架构
CPU,高速缓存,主存…
一致性问题
内存模型
2. Java内存模型
目的:屏蔽掉各种硬件和操作系统的内存访问差异,以实现Java程序在各平台下都能达到一致内存访问效果。
主内存与工作内存
主内存类比硬件主存
工作内存类比硬件高速缓存
工作内存:保存对应线程,使用到的变量的主内存副本拷贝
线程对变量操作(读写),在工作内存进行
线程间无法直接访问对方工作内存中变量,需借助主存传值
内存间交互操作——协议
内存间交互操作,都是原子、不可再分
lock
unlock
read
load
use
assign
store
write
举例:线程A,B。
线程A “lock”主存变量x,使用,使用完“unlock”
先“read” x,传到线程A自己的工作内存
再“load",把“read”的值放入工作内存的“副本x"中
JVM使用变量,即“副本x”:“use”,执行引擎获取“副本x”
...//执行引擎处理
JVM回写变量,即“副本x”:“assgin”,执行引擎将新值,写给“副本x”
“store”:将“副本x”从工作内存传递到主存
“write”:将“store”的值,写到主存变量x
线程B执行如上
举例2:并发执行
线程A,从主存读取x,工作内存存储副本x1
线程B,从主存读取x,工作内存存储副本x2
volatile的特殊规则
-
保证可见性。执行引擎获取变量,每次都直接从主存获取,而不是工作内存,避免一致性问题
-
禁止指令重排序优化。
读,写,读,写... 普通变量,仅保证再该方法的执行过程中,所有依赖赋值结果的地方,都能获取到正确的结果,而不能保证 变量赋值操作的顺序 与程序代码中的执行顺序一致。 某一个读过程,依赖上一个写过程,它们相对顺序不会发生改变
原理:volatile修饰的变量,其字节码中,赋值后多执行一个“lock …”——内存屏障
内存屏障:指令重排序时,不能将后面的指令重拍到内存屏障之前的位置。
long和double的特殊规则
long和double的非原子性协定:读写共4个操作,非原子性
long和double,未被volatile修饰的读写会被划分为2次32位操作,即JVM可以不保证64位数据类型的“load、store、read、write”的原子性。
原子性,可见性,有序性
原子性,原子性变量操作——主存到工作内存的读 赋值,工作内存和执行引擎的读写,工作内存到主存的 读 赋值
可见性,线程间共享变量x,线程A修改x 其他线程立即获取这个修改
有序性,
线程内,所有操作有序
线程间,所有操作无序:指令重排序,工作内存与主存同步延迟(不一致)
volatile,synchronized保证
先行发生原则作了一些约定
先行发生原则
- 程序次序规则
- 管程规定规则
- volatile变量规则
- 线程启动规则
- 线程终止规则
- 线程中断规则
- 对象终结规则
- 传递性
3. Java线程
线程实现方式
- 内核线程
- 用户线程
- Java线程:基于操作系统原生线程模型实现
线程调度
操作系统为线程分配CPU使用权的过程。
- 协同式
- 抢占式
线程状态及迁移
- 新建
- 运行
- 无限期等待
- 限期等待
- 阻塞
- 结束
4. 线程安全与锁优化
线程安全如何体现
- 不可变
- 绝对线程安全:锁,如Vector批操作的原子性
- 相对线程安全:如Vector
- 线程兼容:同步
- 线程对立:同步也无法实现并发安全
线程安全如何实现
- 互斥同步
- 非阻塞同步
- 无同步方案:如不适用共享变量(狭义)、线程本地存储
锁优化
适应性自旋
锁消除
锁粗化
轻量级锁
偏向锁
等等
自旋锁和自适应自旋
自旋,执行一个忙循环
- 自旋时间|次数限制
- 自适应,自旋时间不定:前一次在同一个锁上的自旋时间及锁的拥有者的状态
- 自旋时间
- 锁,自旋很少获取,或经常成功
锁消除
不存在共享数据的锁——消除
实现:逃逸分析的数据支持
锁粗化
连续加锁解锁,加锁同步的范围扩展到整个操作序列的外部——粗化
轻量级锁
无竞争 + CAS
偏向锁
无竞争 + 连续由同一个线程获取锁(无同步)