深入理解JAVA虚拟机
2020-05-27 21:42:48 0 举报
AI智能生成
登录查看完整内容
深入理解Java虚拟机
作者其他创作
大纲/内容
深入理解JAVA虚拟机
走近Java
Java技术体系
Java发展史
Java虚拟机发展史
Sun Classic / Exact VM
Sun HotSpot VM
展望Java技术的未来
模块化
混合语言
多核并行
进一步丰富语法
64位虚拟机
自己编译JDK
OpenJDK源代码
自动内存管理机制
Java内存区域
运行时数据区域
程序计数器Program Counter Register
当前线程所执行的字节码的行号指示器
Java虚拟机栈Java Virtual Machine Stacks
描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
本地方法栈Native Method Stack
与虚拟机栈相似,为Native方法服务
堆(Heap)
存放对象实例
分代收集算法
新生代
老年代
方法区Method Area
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
实现
1.8以前:font color=\"#0076b3\
1.8以后:元空间(MateSpace)
运行时常量池Runtime Constant Pool
方法区的一部分
用于存放编译期生成的各种字面量和符号引用
运行期间也可将新的常量放入常量池中String.intern()
直接内存Direct Memory
不在Java虚拟机规范中定义
Native函数库直接分配堆外内存
NIO:DirectByteBuffer
概要
Hotspot虚拟机对象探秘
对象的创建
创建过程
首先检查new指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有,则先执行相应的类加载过程
类加载检查通过后,为新生对象分配内存
初始化零值
设置对象头信息
分配内存策略
指针碰撞(Bump the Pointer)
用于Serial/ParNew等带Compact整理过程的收集器
空闲列表(Free List)
用于CMS这种基于Mark-Sweep算法的收集器
对象的内存布局
对象头Header
font color=\"#0076b3\
类型指针
指向它的类元数据的指针
实例数据 Instance Data
是对象真正存储的有效信息
对齐填充 Padding
对象的访问定位
使用句柄
直接指针
内存溢出异常
OutOfMemoryError
StackOverflowError
垃圾收集器
对象存活确定
引用计数算法Reference Counting
给对象添加引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效是,计数器值就减1
很难解决对象之间相互循环引用的问题
可达性分析算法Reachability Analysis
算法的基本思路就是通过一系列名为\"GC Roots\"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的
可作为GC Roots的对象包括
虚拟机栈中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中引用的对象
引用
强引用(StrongReference)
软引用(SoftReference)
弱引用(WeakReference)
虚引用(PhantomReference)
垃圾回收算法
标记-清除算法Mark-Sweep
标记出所有需要回收的对象统一回收所有被标记的对象
效率低产生大量不连续的内存碎片
复制算法Copying
内存分为两块,每次只使用其中一块这一块内存用完,将还存活的对象复制到另一块上一次清理已使用过的内存空间
缺点:内存使用率低
优点:只需移动堆顶指针,按顺序分配内存即可,实现简单,运行高效
用于回收新生代
一块较大的Eden空间
两块较小的Survivor空间
From Survivor
To Surivivor
标记-整理Mark-Compact
标记出所有需要回收的对象让所有存活对象都向一端移动最后直接清理掉端边界以外的内存
用于回收老年代
分代收集算法Generational Collection
新生代使用:复制算法
老年代使用:标记-清除 或者 标记-整理 算法
Hotspot的算法实现
枚举根节点
安全点Safepoint
分析过程中对象引用关系不会发生变化的点
产生Safepoint的地方:方法调用;循环跳转;异常跳转等
安全点数据得适中
安全区域SafeRegion
Serial
单线程,进行垃圾收集时,必须暂停其他所有的工作线程
简单高效,适用于Client端
-XX:+UseSerialGC
Serial Old
Serial的老年代版本
标记-整理算法
-XX:+UseSerialOldGC
单线程,适用于Client端
ParNew
新生代,Serial的多线程版本,并行
适用于Server端
-XX:+UseParNewGC
Parallel Scavenge
控制吞吐量(Throughput)
吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值
吞吐量 = 运行用户代码的时间/(运行用户代码的时间 + 垃圾收集时间)
-XX:+UseParallelGC
自适应调节策略
无法与CMS收集器配合工作
Parallel Old
Parallel Scavenge 的老年代版本
多线程 标记-整理
-XX:+UseParallelOldGC
搭配 Parallel Scavenge
吞吐量优先
CMSConcurrent Mark Sweep
以获取最短回收停顿时间为目标
+XX:+UseConcMarkSweepGC
标记-清除算法
运作过程
1、初始标记(CMS initial Mark): stop-the-world,需要停顿虚拟机2、并发标记(CMS concurrent mark):并发追溯标记,程序不会停顿 并发欲清理:查找执行并发标记阶段从年轻代晋升到老年代的对象3、重新标记 (CMS remark) :暂停虚拟机,扫描CMS堆中的剩余对象4、并发清除(CMS concurrent sweep):清理垃圾对象,程序不会停顿并发重置:重置CMS收集器的数据结构
优点
1、并发收集2、低停顿
缺点
1、对CPU资源非常敏感2、无法处理浮动垃圾3、标记-清除 算法收集结束后会有大量空间碎片
G1 Garbage First
特点
并行与并发
分代收集
无需其他收集器配合
空间整合
可预测的停顿
-XX:+UseG1GC
把内存“化整为零”
将整个Java堆划分为多个大小相等的独立区域(Region)
年轻代和老年代不再物理隔离
1、初始标记(Initial Marking)2、并发标记(Concurrent Marking)3、最终标记(Final Marking)4、筛选回收(Live Data Counting and Evacuation)
jdk11 : EpsilonGC 和 ZGC
内存分配策略
内存分配与回收策略
对象优先在Eden分配。Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC
大对象直接进入老年代
长期存活的对象将进入老年代
动态对象年龄判定
空间分配担保
参数
常用参数
虚拟机性能监控
JDK命令行工具
jps
JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程
jstat
JVM Statistics Monitoring Tool,用于收集HotSpot虚拟机各方面的运行数据
jinfo
Configuration Info for Java ,显示虚拟机配置信息
jmap
Memory Map for Java ,生成虚拟机的内存转储快照(heapdump)文件
jhat
JVM Heap Dump Brower,用于分析heapdump文件
jstack
Stack Trace for Java ,显示虚拟机的线程快照
JDK的可视化工具
JConsole:Java监视与管理控制台
VisualVM:多合一故障处理工具
JMC(Java Mission Control)
虚拟机执行子系统
类文件结构
无关性的基石
基础是虚拟机和字节码存储格式
Java虚拟机不和包括Java在内的任何语言绑定
只与Class文件关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息
Java语言中的各种变量、关键字和运算符号的语义最终都是有font color=\"#0076b3\
Class类文件的结构
8字节为基础单位的二进制流,无分隔符,8字节以上空间的数据按照高位在前的方式分割成若干个8位字节进行存储
任何一个Class文件都对应着唯一一个类或者接口的定义信息,但反过来说,类或者接口并不一定都得定义在文件里(譬如通过类加载器直接生成)
数据类型
无符号数
基本的数据类型
u1,1个字节
u2,2个字节
u4,4个字节
u8,8个字节
表
多个无符号数或者其他表作为数据项构成的符合数据类型
一般以_info结尾
Class文件本质就是一张表
集合
前置容量计数器加若干个连续的数据项
文件格式
魔数与Class文件的版本
magic(u4),魔数
0xCAFEBABE
minor_version(u2),次版本号
major_version(u2),主版本号
从45开始,45:JDK1.1,52:JDK1.8
常量池
constant_pool_count(u2),常量池容量计数器
constant_pool(cp_info),常量池
字面量(Literal)
比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等
符号引用(Symbolic References)
属于编译原理方面的概念,包括了下面三类常量:类和接口的全限定名;字段的名称和描述符;方法的名称和描述符
访问标志
access_flags(u2),访问标志
用于识别类或者接口层次的访问信息
类索引、父类索引与接口索引集合
this_class(u2),类索引
super_class(u2),父类索引
interfaces_count(u2),接口索引计数器
interfaces(u2),接口索引集合
字段表集合
fields_count(u2),字段计数器
fields(field_info),字段表集合
方法表集合
methods_count(u2),方法计数器
methods(method_info),方法表集合
属性表集合
attributes_count(u2),属性计数器
attributes(attribute_info),属性表集合
字节码指令
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(操作数,Oprands)而构成
优势
小数据量,高效率传输
劣势
操作码总数不能超过256条
放弃操作数长度对齐,损失性能
指令
加载和存储指令
用于将数据在栈帧中的局部变量表和操作数栈之间来回传输
运算指令
用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶
类型转换指令
将两种不同的数值类型进行相互转换
对象创建与访问指令
方法调用和返回指令
异常处理指令
同步指令
虚拟机类加载机制
类加载的过程
加载Loading
完成3件事
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
获取二进制字节流的方式
从网络获取,比如Applet
运行时获取,如 动态代理技术
有其他文件生成,如 jsp应用
从数据库中读取
连接Linking
验证(Verification)
确保Class文件的字节流种包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
检验动作
文件格式验证
元数据验证
字节码验证
符号引用验证
准备(Preparation)
正式为类变量分配内存并设置类变量初始值(零值),这些变量所使用的内存都将在方法区中进行分配
解析(Resolution)
是虚拟机将常量池内的符号引用替换为直接引用的过程
初始化Initialization
初始化阶段是执行类构造器<clinit>()方法的过程
<clinit>()是有编译器自动收集类中的所有类变量(static修饰的变量)的赋值动作和静态语句块(static{}块)中的语句合并产生的
虚拟机进行类初始化的5种情况(主动引用)
遇到new、getstatic、putstatic、invokestatic字节码指令时
对类进行反射调用时
初始化一个类时,先初始化它的父类
虚拟机启动时,会先初始化启动类
当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_puStatic、REF_invokeStatic的方法句柄
loadClass和forName的区别
CLass.forName得到的class是已经初始化完成的
CLassLoader.loadClass得到的class是还没有链接的
类加载器
双亲委派模型
启动类加载器BootrapClassLoader
扩展类加载器ExtClassLoader
应用程序类加载器AppClassLoader
避免多份同样字节码的加载
破坏双亲委派模型
OSGI
虚拟机字节码执行引擎
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
运行时栈帧结构
局部变量表(Local Variable Table)
操作数据栈(Operand Stack)
动态链接(Dynamic Linking)
方法返回地址(Return Address)
附加信息
方法调用
解析
分派
动态类型语言支持
基于栈的字节码解释执行引擎
类加载及执行子系统的案例与实战
Tomcat:正统的类加载器架构
OSGI:灵活的类加载器架构
字节码生成技术与动态代理的实现
Retrotranslator:跨越JDK版本
程序编译与代码优化
早期(编译期)优化
Javac编译器
Javac的源码与调试
解析与填充符号表
注解处理器
语义分析与字节码生成
Java语法糖的味道
泛型与类型擦除
自动装箱、拆箱与遍历循环
条件编译
晚期(运行期)优化
HotSpot虚拟机内的即时编译期
解释器与编译器
编译对象与触发条件
编译过程
查看及分析及时编译结果
编译优化技术
优化技术概览
公共子表达式消除
数组边界检查消除
方法内联
逃逸分析
高效并发
Java内存模型与线程
概述
并发处理的广泛应用使得Amdahl定律代替摩尔定律成为计算机性能发展源动力
Amdahl定律
通过系统中并行化与串行化的比重来描述多处理期系统能获得的运算加速能力
摩尔定律
用于描述处理器晶体管数量与运行效率之间的发展关系
硬件的效率与一致性
内存模型
在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象
Java内存模型
主内存与工作内存
主内存
工作内存
内存间交互操作
8种内存操作
作用于主内存变量
lock(锁定)
把一个变量标识为一条线程独占的状态
unlock(解锁)
作用于主内存的变量,把一个处于锁定状态的变量释放出来
read(读取)
把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
write(写入)
把store操作从工作内存中得到的变量的值放入主内存的变量中
作用于工作内存变量
load(载入)
把read操作从主内存中得到的变量值放入工作内存的变量副本中
use(使用)
把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
assign(赋值)
把一个从执行引擎接收到值赋给工作内存的变量
store(存储)
把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
8种操作必须满足的规则
对volatile型变量的特殊规则
特性
可见性
当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的
volatile变量在各个线程的工作内存中不存在一致性问题
禁止指令重排
指令重排意义:JVM能够根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能
对于long和double型变量的特殊规则
long和double的非原子协定
允许虚拟机将没有被volatile修饰的64位数据结构的读写操作划分为两次32位的操作
原子性可见性有序性
原子性Atomicity
由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、write,可以认为基本数据类型的访问读写是具备原子性的
可见性Visibility
指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改
有序性Ordering
如果在本线程内观察,所有的操作都是有序的
线程内表现为串行语义
如果在一个线程中观察另一个线程,所有的操作都是无序的
指令重排
工作内存和主内存同步延迟
先行发生原则happens-before
先行发生原则是判断数据是否存在竞争、线程是否安全的主要依据
先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息。调用了方法等
天然的先行发生关系
程序次序规则(Program Order Rule)
管程锁定规则(Monitor Lock Rule)
volatile变量规则(Volatile Variable Rule)
线程启动规则(Thread Start Rule)
对象终结规则(Finalizer Rule)
传递性(Transitivity)
线程终止规则(Thread Termination Rule)
Java与线程
线程的实现
使用内核线程实现
使用用户线程实现
Java线程的实现
使用用户线程加轻量级进程混合实现
Java线程调度
线程调度是指系统为线程分配处理器使用权的过程
调度方式
协同式线程调度
抢占式线程调度(Java使用的方式)
状态转换
新建(New)
创建后尚未启动的线程的状态
运行(Runable)
包含Running和Ready
无限期等待(Waiting)
不会被分配CPU执行时间,需要显式被唤醒
没有设置Timeout参数的Object.wait()方法
没有设置Timeout参数的Thread.join()方法
LockSuport.park()方法
限期等待(Timed Waiting)
在一定时间后会被系统自动唤醒
Thread.sleep()
Object.wait(timeout)
Thread.join(millis)
LockSuport.parkNanos(nanos)
LockSuport.parkUntil(deadline)
阻塞(Blocked)
等待获取排它锁
结束(Terminated)
已终止线程的状态
线程安全与锁优化
线程安全
定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的
Java语言中的线程安全
线程共享数据类型
不可变
final
绝对线程安全
相对线程安全
Vector、HashTable、Collections的synchronizedCollection()
线程兼容
ArrayList、HashMap
线程对立
Thread类的suspend()和resume()(已废弃)
线程安全的实现方法
互斥同步
实现手段
synchronized关键字
可重入锁(ReetrantLock)
重入
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有的对象锁的临界资源时,这种情况属于重入
需要进行线程阻塞和唤醒,性能低
非阻塞同步
乐观锁
CAS
基于冲突检测的乐观并发策略,就是先进行操作,如果没有 其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止)
不需要把线程挂起
无同步方案
可重入代码
线程本地存储(ThreadLocal)
锁优化
自旋锁
自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,\"自旋\"一词就是因此而得名。
缺点:若锁被其他线程长时间占用,会带来更多性能上的开销
自适应自旋锁
自旋的次数不再固定
由前一次再同一个锁上的自旋时间及锁的拥有者的状态来决定
锁消除
锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除主要判定依据来源于逃逸分析的数据支持
例如:StringBuffer.append(\"a\").append(\"b\")
锁粗化
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部,这样就只需要加锁一次就够了
通过扩大加锁的范围,避免反复加锁和解锁
偏向锁
偏向锁,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
轻量级锁
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;若在同一时间访问同一锁的情况,就会到时轻量级锁膨胀为重量级锁
适应的场景:线程交替执行同步块
0 条评论
回复 删除
下一页