JVM
2022-03-25 16:31:09 0 举报
jvm
作者其他创作
大纲/内容
Java的体系结构
JVM的架构模型
栈式架构
指令大多都是零指令地址,执行依赖操作数栈
优点
设计简单
不需要硬件支持,可移植性可跨平台性更好
缺点
性能下降(实现同样的功能需要更多的指令)
寄存器式架构
地址+寄存器地址
优点
性能优秀,执行更高效
缺点
指令集依赖硬件,可移植性差
JVM的发展历程
sun Classic VM(第一款商用)->纯解释器
hotspot虚拟机
最初由Longview Technologies的小公司设计,1997年被sun公司收购,2009年sun公司被甲骨文收购
JDK1.3时,HotSpot VM成为默认虚拟机
HotSpot就是他的热点代码探测技术
通过计数器找到最具编译价值代码,触发即时编译或栈上替换
通过编译器与解释器协同工作,在优化响应时间和最佳执行性能中取得平衡
JRockit(EBA)
专注服务器端应用-不太关注程序启动速度,引起JRockit内部不包括解析器实现,全部代码靠即时编译器编译后执行
全面的Java运行时解决方案组合
提供毫秒或微秒级的JVM响应时间,适合财务、军事指挥,电信网络的需要
MissionControl服务套件,极低的开销,来监控、管理和分析生成环境中的应用程序的工具
被oracle收购,JDK8中,在HOTSPOT的基础上,移植JRockit的优秀特性
IBM J9
广泛应用于IBM的各种Java产品
Grall VM->GraalVM想成为一统天下的“最终”虚拟机
跨语言全栈虚拟机,可以作为任何语言的运行平台使用。
2018年4月,Oracle labs公开了GraalVM。
JAVA的类加载机制
加载
主要是将Class文件中的二进制数据读到内存(方法区)中去,然后将其转化为方法区中的运行时数据(包括常量、静态变量、静态代码块),同时在堆中创建一个java.lang.Class对象,用于外部访问这个Class类的入口。
链接
验证
文件格式验证(如是否是cafe babe开头)
元数据验证
对字节码描述的信息进行语义分析,保证描述符合Java规范
字节码验证
通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的。
在字节码的执行过程中,是否会跳转到一条不存在的指令
函数的调用是否传递了正确类型的参数
变量的赋值是不是给了正确的数据类型等
符号引用验证
通过字符串描述的全限定名是否能找到对应的类(NoClassDefFoundError,NoSuchFieldError,NoSuchMethodError)
权限访问验证
准备
为类变量分配内存,并且设置该类变量的初始值,即零值。不包含final的值,final在编辑成。class文件直接指定了符号引用
解析
将常量池内的符号引用转换为直接引用的过程
符号引用:间接的引用方式,如类中的一个方法引用了另一个类
直接引用:直接将方法通过指针的方式指向目标对象在内存中的位置
直接引用:直接将方法通过指针的方式指向目标对象在内存中的位置
以方法为例,Java虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法。通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。
初始化
初始化阶段是执行类构造器方法<clinit>()的过程(初始赋值)
Java内存结构
虚拟机栈
每个线程创建都会创建一个虚拟机栈,内部保存着一个个栈帧
java栈的操作(出栈,入栈)
出栈的两种方式
方法return
方法异常
栈的内部结构
局部变量表
c定义为一个数字数组,主要用于存储方法参数,定义在方法体内部的局部变量,数据类型包括各类基本数据类型,对象引用,以及return address类型
操作数栈
Java虚拟机的解释引擎是基于栈的执行引擎,其中栈就是操作数栈
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
方法返回地址
本地方法栈
JAVA调用非JAVA代码的接口
为什么使用本地方法
与操作系统底层或硬件交换信息时使用
程序计数器(不会oom,没有垃圾回收)
作用是指示当前线程方法执行的字节码的行号指示器
如果正在执行的本地方法,这个计数器值则应为空。(undefined)
堆
方法区(元数据空间,永久代)
直接内存
直接内存是在java堆外的,直接向系统申请的内存区间(NIO)
JAVA的垃圾回收器
垃圾回收算法
标记阶段
引用计数法
对每个对象保存一个整型的引用计数器属性,用于记录被对象引用的情况
优缺点
优点
实现简单,垃圾便于辨识,判断效率高,回收没有延迟性
缺点
需要单独的字段存储计数器,增加了存储空间的开销
每次赋值需要更新计数器(加锁),伴随加减法操作,增加了时间开销
无法处理循环引用的情况,致命缺陷
根结点标记
通过定义一系列称为 GC Roots 根对象作为起始节点集,从这些节点出发,穷举该集合引用到的全部对象填充到该集合中(live set)。
GC Roots 包括
虚拟机栈内引用对象
线程调用的方法使用的参数和局部变量等
方法区中的静态引用对象
Java虚拟机内部的引用
基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。
所有被同步锁synchronized持有的对象
两大问题
误报(本该被回收的对象,被标记为存活):已死亡对象被标记为存活,垃圾收集不到。多占用一会内存,影响较小。
在标记的时候,由于需要标记的对象比较多,需要一定的时间,假设这个时候已经标记了一半了,还在标记剩余的一半,这个时候有一个线程将自己的一个引用置为null了,并且这个引用是在已经标记完成的那一半中的,这个时候就会出现误报——本该是垃圾被回收的,现在标记成了存活。
漏报:引用的对象(正在使用的)没有被标记为存活,被垃圾回收了。那么直接导致的就是JVM奔溃。(STW可以确保可达性分析法的准确性,避免漏报)
其实就是标记已经结束了,这个时候其他线程创建了对象,但是这个对象的引用没有被标记,然后在垃圾回收的时候,这个对象就被回收掉了。
STW
Java虚拟机是利用可达性算法判断对象是否需要回收的,由于在GC进行时,必须暂停所有的Java执行线程(Sun称之为“Stop The World”),所以,虚拟机必须尽量的优化GC过程的效率,减少暂停的时间。
GC 停顿会拖慢应用程序,在外界看来,它就像冻住了一样。在 GC 停顿期间发给服务器的请求会更晚收到响应,根据停顿时间的不同(传统的 GC 停顿有可能达到几十秒),客户端有可能会出现超时。如果客户端进行重试,服务器端就会有更多待处理的请求,这个时候需要使用断路器。
长时间的 GC 停顿也可能造成服务的健康检测失效,并导致服务被重启。而在一个服务重启期间,其他服务需要承担更多的负载,它们所经历的停顿会更长,这就像是一个恶性循环。
不可预测的 GC 停顿给系统带来的影响远远超过了应用程序本身。客户端出现回压,请求队列溢出,监控控制台满是各种超时异常,运维人员忙得团团转。对于一个可以应对各种情况的系统来说,需要在 CPU 时间、队列长度、可接受的响应时间方面具备缓冲能力。
三色标记算法(在对象头里面进行标记)
三色
黑色
表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。
灰色
表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过
白色
表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。
问题
浮动垃圾
漏标问题
增量更新
黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。
原始快照
灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)
简单说就是在并发标记阶段,当引用关系发生变化的时候,通过pre-write barrier函数会把这种这种变化记录并保存在一个队列里,在JVM源码中这个队列叫satb_mark_queue。在remark阶段会扫描这个队列,通过这种方式,旧的引用所指向的对象就会被标记上,其子孙也会被递归标记上,这样就不会漏标记任何对象,snapshot的完整性也就得到了保证。
清除阶段
标记-清除算法
标记-根节点可达分析标记被引用对象,不是垃圾对象
清除-把需要清除的地址保存在空闲列表里面(搜索活动对象(标记阶段)所花费的时间和搜索整体 堆(清除阶段)所花费的时间之和)
缺点
碎片化
分配速度
stw(标记+清除)
标记-压缩算法
标记-和清除一样
压缩-将所有的对象压缩在内存的一端,此时创建对象的方式使用指针碰撞
优点
没有内存碎片
也没有内存的浪费
缺点
效率低于两种算法
移动对象的同时需要调整引用地址
移动的过程中,需要全程停止用户线程(标记+整理)
复制算法
搜索并复制活动对象到另一块区域
缺点
多余的内存空间
stw(移动期间不能产生新的对象)
优点
简单高效
没有内存碎片
垃圾回收机制
分代收集机制
思想
80%以上的对象都是朝生夕死的(IBM得出的结论是98%的对象),如string对象。
经历过几轮垃圾回收都未被回收的对象,可能会贯穿整个jvm生命周期,如spring的ioc单例对象
不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
具体实现
新生代(复制算法)
eden
survivor0(TO)
survivor1(FROM)
老年代(标记压缩或者标记整理)
不同区域的垃圾回收
新生代GC(young GC)
当年轻代(Eden)满了就会触发young gc
老年代(Old GC)
当前只有CMS这个垃圾会收齐会有单独收集老年代
全堆GC(FULL GC)-堆空间和方法区
fullgc 为啥那么讨厌呢
一次收集的空间大(全堆和方法区)
方法区的收集比较慢(废弃常量和无用的类)
判断无用的类的条件
该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例;
加载该类的ClassLoader已经被回收;
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾回收注意点(JVM优化)
频繁收集Young区
较少收集Old区
基本不收集Perm区(元空间)
较少收集Old区
基本不收集Perm区(元空间)
增量收集算法
让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
缺点
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
分区收集算法
为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
缺点
实现过程复杂
占用更多的额外内存
GC性能的指标
吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)
垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
收集频率:相对于应用程序的执行,收集操作发生的频率。
内存占用:Java堆区所占的内存大小。
快速:一个对象从诞生到被回收所经历的时间。
垃圾回收器 ZGC的命名 ZFS(文件系统)的启发或向其致敬,ZFS(文件系统)在它刚问世时在许多方面都是革命性的。
Serial回收器(串行)
Parallel回收器(并行-吞吐量优先)
CMS回收器(低延迟)
PerNew(并行)
前6种的总结
如果你想要最小化地使用内存和并行开销,请选Serial GC(如手机端)
如果你想要最大化应用程序的吞吐量,请选Parallel GC(如kafka(异步收集日志),大数据 等应用,)
如果你想要最小化GC的中断或停顿时间,请选CMS GC。
G1(区域化分代式)
使用Remembered Sets用来记录外部指向本Region的所有引用,每个Region维护一个RSet。
JVM将内存划分成了固定大小的Card。这里可以类比物理内存上page的概念。
ZGC
标记
初始, 只标记gcroot 有stw 时间特别短
并发 没有stw
在标记 有stw 时间也很短
准备
看需要回收那些区域
转移(复制算法)
初始转移(只转移Gc ROOT) 有stw 时间特别短
并发转移(转发表记录新旧地址,读屏障更改地址)
染色指针
标记方案
标记直接记录在对象头(Serial,perNew)
标记记录在与对象相互独立的数据结构(如G1、Shenandoah使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息)
直接把标记信息记在引用对象的指针上(如ZGC)
读屏障(AOP)
调优
调优的目的
减少stw。达到停顿时间和吞吐量的一个平衡。
ZGC(基本不用调优)
G1
不要设置新生代老年代的大小
不断调优暂停时间目标
调大堆内存的大小
分代垃圾回收器的调优
减少full gc,最好没有full gc
full gc产生的条件
System.gc()方法的调用
发生Young GC之前进行检查,如果“老年代可用的连续内存空间” < “新生代历次Young GC后升入老年代的对象
总和的平均大小”,说明本次Young GC后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间。
总和的平均大小”,说明本次Young GC后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间。
执行Young GC之后有一批对象需要放入老年代,此时老年代就是没有足够的内存空间存放这些对象了,此时必
须立即触发一次Old GC
须立即触发一次Old GC
方法区满了
那些对象会进入老年代
大对象(超过1M)
避免大对象在新生代,屡次躲过GC,还得把他们来复制来复制去的,最后才进入老年代,这么大的对象来回复制,是很耗费时间的。
分代年龄超过15
躲过15次gc都没被回收,可以认为他是生命周期比较长或者是贯穿整个生命周期的对象。
对象动态年龄判断
年龄1 + 年龄2 + 年龄n的多个对象总和超过Survivor区的50%,那就会把年龄n以上的对象都放入老年代。
0 条评论
下一页