JVM知识结构图
2021-07-21 22:54:45 3 举报
jvm知识结构一张图搞定
作者其他创作
大纲/内容
操作数栈
JVM按需动态加载采用双亲委派机制
静态变量
object7
语义分析
JIT动态编译
常驻异常对象
Yes
标记清除算法
HotSpot通过Heuristics这个类来统计某一时间段内某个类对象的撤销偏向和重偏向的次数。 那么如何对某一特定数据类型的所有对象进行统计?HotSpot刚开始采用的方案是:遍历对象堆。 但是当堆变得比较大的时候,其扩展性就比较差,为了解决这个问题,引入了epoch的概念, 用epoch这个字段来表示偏向是否有效。 每一个可偏向的数据类型(类)都有自己响应的epoch值。注意,这个epoch number属于类。 此时,一个线程拥有偏向锁,指的是:对象头中的Mark Word的Thread ID字段指向该想线程, 并且对象实例的epoch字段和类的epoch字段值相同,如果不相同,可以认为该对象处于可偏向但 未偏向的状态。这样就不需要遍历整个对象堆,提高了效率。 对于偏向锁,如果Thread ID = 0,表示未加锁 identity hashcode:如果我们没有重写默认的hashcode方法,默认的hashcode计算出的就是identity hashcode,这个值是被存储在对象头中的,如果重写之后,就不能叫identity hashcode了,计算出的值也不会存储在对象头中。 无锁状态下这些数据都是存储在对象头中的,但是当出现锁之后这行数据信息可能会发生变化,就比如轻量级锁下会将对象的Mark Word利用CAS替换到栈帧中用于记录锁记录的空间中,然后对象头中原有的位置变成了指向锁记录的指针。重量级锁也类似,不过他指向的是一个monitor对象,也能记录下原来的hashcode和分代年龄等。关键就是在于,原本存储Mark Word的位置被指针占据了,轻量级和重量级锁都会找个位置存放原有信息。但是偏向锁没有地方来存原来的位置了,所以如果你一个对象计算了hashcode之后,没地方放了怎么办,这就是hashcode与轻量级锁的冲突问题,一个对象如果计算了hashcode之后,锁就会膨胀,不能再做为偏向锁。
卸载
ParNew(并)复制
词法分析
方法出口
方法表
Serial Old 收集器Serial Old 收集器是Serial 收集器的老年代版本。其垃圾收集器的运行原理和Serial 收集器是一样的。
根可达算法
类加载器的引用
-XX:InitialHeapSize=2048m:初始化堆内存(-Xms2048m -简写)-XX:MaxHeapSize=2048m:最大堆内存(-Xmx2048m -简写)span style=\"font-size: inherit;\
object1
HelloWorld.class
根节点枚举算法
JIT 编译器
”加载“是”类加机制”的第一个过程,在加载阶段,虚拟机主要完成三件事:(1)通过一个类的全限定名来获取其定义的二进制字节流(2)将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构(3)在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。
类加载检查
准备阶段主要为类变量分配内存并设置默认值属性是被static所修饰的准备阶段之后是0,但是如果同时被final和static修饰准备阶段之后就是1了
字节码生成器
执行栈上替换
No
字面量
find in cache
分代收集理论的时候,会存在为了解决对象跨代引用所带来的的问题。垃圾收集器在新生代中建立了名字为记忆集的数据结构,用来避免把整个老年代加进GC roots扫描范围。事实上并不只是新生代、老年代之间才有跨代引用的问题,所有涉及部分区域收集(Partial GC)行为的 垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,都会面临相同的问题。记忆集其实是一种“抽象”的数据结构,抽象的意思是只定义了记忆集的行为意图,并没有定义其行为的具体实现。卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。卡表最简单的形式可以只是一个字节数组,而HotSpot虚拟机确实也是这样做的。字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page),每个页大小为512字节。只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。写屏障伪代码如下:CARD_TABLE [this address >> 9] = DIRTY 这里右移 9 位相当于除以 512,写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的则叫作写后屏障(Post-Write Barrier)。HotSpot虚拟机的许多收集器中都有使用到写屏障,但直至G1收集器出现之前,其他收集器都只用到了写后屏障。在JDK 7之后,HotSpot虚拟机增加了一个新的参数-XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断。开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损耗,是否打开要根据应用实际运行情况来进行测试权衡。伪代码如下:f (CARD_TABLE [this address >> 9] != DIRTY) CARD_TABLE [this address >> 9] = DIRTY;
对象头
Class Not Found Exception
方法返回
方法信息
JVM参数
是
运行时常量池
字节码校验器
使用
解释器
根可达算法分析
用于存放boolean、byte、char、short、int、float、long、double等类型的数据,以变量曹(slot)为最小单位(32位),long,double需要两个slot,所以线程不安全,基本数据类型会直接存值,引用类型会存放对象的引用
加载扩展jar包jre/lib/ext/*.jar或由 -Djava.ext.dirs指定
final常量值
Ext
在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化,执行<cinit>方法。对类变量进行初始值设定有两种方式:①声明类变量是指定初始值②使用静态代码块为类变量指定初始值JVM初始化步骤1、假如这个类还没有被加载和连接,则程序先加载并连接该类2、假如该类的直接父类还没有被初始化,则先初始化其直接父类3、假如类中有初始化语句,则系统依次执行这些初始化语句
语法分析
初始化
1、当遇到一个new指令时,虚拟机会首先进行类加载检查 - 检查new指令的参数是否能再常量池中定位到一个类的符号引用 - 检查这个字符引用的类是否被加载解析和初始化过,没有则先执行类加载2、对象所需内存的大小在类加载完成后可完全确定,于是分配空间等同于吧一块确定大小的内存从堆中划分出来,具体分配方式取决于堆内存是否规整,而是否规整又取决于所采用的的GC收集器是否带有压缩整理功能。 分配方式: - 指针碰撞(堆规整) - 空闲列表(堆不规整) 为保证并发情况下线程安全问题,在分配内存时有两种方案: - 同步:CAS+失败重试 - TLAB:-XX:+/-UseTLAB 分配步骤: - 首先尝试栈上分配 - 尝试TLAB分配 - 判断是否为大对象,是则在Old区分配,否则在Eden区。3、接着,虚拟机将分配到的内存空间(不包含对象头)都初始化为零值。4、设置对象头,如设置MarkWord、类型指针等。5、最后执行对象的<init>方法,既执行对象的构造方法,<init>方法首先执行父类的构造方法,然后初始化变量的初始值,然后执行构造方法内的代码。
连接
存活对象
TLAB:在Eden区开辟的每一个线程私有的很小的缓冲空间(Thread Local Allocation Buffer )。线程需要创建对象,只要TLAB空间足够就可以在此空间创建,不够再申请。
新生代GC时采用的算法是复制算法。老年代GC时是根据搜索算法,标记垃圾对象,若老年代满了会触发FullGC,系统会暂时停止,JVM调优就是调的FullGC发生的次数
硬件
类加载器
find Class并加载
类元信息
Custom
栈上替换
加载classpath指定内容
CMS(并发)清除
Serial 收集器Serial 收集器是最基础、历史最悠久的收集器,它在进行垃圾收集的时候会暂停所有的工作线程,直到完成垃圾收集过程。下面是Serial垃圾收集器的运行示意
主要是为了安全
类加载子系统
To Survivor s1(1/10)
HelloWorld.java
垃圾判断算法
From Survivor s0(1/10)
垃圾收集--GC
Custom ClassLoader - 自定义类加载器
类型的常量池
object3
栈帧
记忆集与卡表--解决跨代引用
JVM内部引用
三色标记算法
解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
验证的主要作用就是确保被加载的类的正确性。1、文件格式的验证2、元数据验证3、字节码验证4、符号引用验证对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,可以使用-Xverfity:none来关闭大部分的验证
方法/回边计数器+1
Bootstrap
初始化为零值
Java方法入口
JIT代码生成器(热点代码)
标记-清除算法: 分为标记和清除两个阶段:首先标记处所有需要回收的对象,标记完成后统一回收所有被标记的对象;是最基础的收集算法,其它的收集算法都是基于这种思路并对其不足进行改进而得到的。不足: a) 效率问题,标记和清除两个过程的效率都不高; b) 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存二不得不提前触发另一次垃圾收集动作。复制算法: 为了解决效率问题,复制收集算法出现了,他将可用的内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。不足: 这种算法的代价是将内存缩小为原来的一般,代价太高用法:存活区采用这种算法: 因为新生代中的对象98%是“朝生夕死”,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性的复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被“浪费”。不能保证每次回收都只有不多于10%的对象存活,当Survivor空间不够时,需要依赖老年代进行分配担保(Handle Promotion)标记-整理算法: 老年代对象存活率较高,一般不能选择复制算法,根据老年代特点,提出标记-整理算法,标记过程和“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。分代收集算法: 当前商业虚拟机的垃圾收集都采用“分代收集”算法,根据对象存活周期的不同将内存分为几块。一般是把java对分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中每次垃圾收集时都有大量对象死去,只有少量对象存活,所以就采用复制算法,只需要付出少了存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对他进行分配担保,就要使用“标记-清除”或“标记-整理”算法三色标记算法 我们将对象分成三种类型: 1、黑色:根对象,或者该对象与它的子对象都被扫描过(对象被标记了,且它的所有field也被标记完了)。 2、灰色:对象本身被扫描,但还没扫描完该对象中的子对象(它的field还没有被标记或标记完)。 3、白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,既垃圾对象(对象没有被标记到)。三色标记算法的整个过程: 1、初始时,所有对象都在 【白色集合】中; 2、将GC Roots 直接引用到的对象 挪到 【灰色集合】中; 3、从灰色集合中获取对象: 3.1. 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中; 3.2. 将本对象 挪到 【黑色集合】里面。 4、重复步骤3,直至【灰色集合】为空时结束。 5、结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收。而当需要支持并发标记时,即标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。 多标-浮动垃圾 当在标记过程中发生已标记过得变成白色,那应该被回收,这部分对象仍会被标记为存活,即本轮GC不会回收这部分内存,被称之为“浮动垃圾”。另外,针对并发标记开始后的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。 漏标-读写屏障 漏标只有同时满足以下两个条件时才会发生: 条件一:灰色对象 断开了 白色对象的引用;即灰色对象 原来成员变量的引用 发生了变化。 条件二:黑色对象 重新引用了 该白色对象;即黑色对象 成员变量增加了 新的引用。 漏标解决方案: 一、增量更新(Incremental Update):关注引用的增加,把黑色重新标记为灰色,下次重新扫描。 二、原始快照(Snapshot At The Beginning):当灰色断开白色引用时把这个引用推到GC堆栈,保证断开的白色还能被标记到。 重新标记是需要STW的,因为应用程序一直在跑的话,该集合可能会一直增加新的对象,导致永远都跑不完。 扫描所有GC Roots 这个操作(即初始标记)通常是需要STW的,否则有可能永远都扫不完,因为并发期间可能增加新的GC Roots。 (1) 写屏障 + SATB (2) 写屏障 + 增量更新 (3) 读屏障(Load Barrier)三色标记法与现代垃圾回收器现代追踪式(可达性分析)的垃圾回收器几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同:比如白色/黑色集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式可以是广度/深度遍历等等。对于读写屏障,以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下:CMS:写屏障 + 增量更新G1:写屏障 + SATB(SATB和RSet配合效率高)ZGC:读屏障CMS中使用的增量更新,在重新标记阶段,除了需要遍历 写屏障的记录,还需要重新扫描遍历GC Roots(当然标记过的无需再遍历了),这是由于CMS对于astore_x等指令不添加写屏障的原因
方法名称和描述符
自定义ClassLoader重写findClass方法,使用了模板方法模式
javac 编译器
后台执行编译
object4
Parallel Scavenge 收集器也是一款新生代垃圾收集器,同样的基于标记–复制算法实现的。它最大的特点是可以控制吞吐量。
引用计数
对齐填充-8字节倍数
基本数据类型
对象分配内存
方法区 (元空间 Metaspace)
本地方法栈
可以理解为用于计算的临时数据存储区,使用load指令将数据加载到此
Custom.loadClass
ParNew 收集器ParNew 垃圾收集器实则是Serial 垃圾收集器的多线程版本,这个多线程在于ParNew垃圾收集器可以使用多条线程进行垃圾回收。
字段信息
类装载子系统
常量池
两个计数器之和是否超过阈值
标记复制算法
Parallel Old 收集器Parallel Old 收集器同样是Parallel Scavenge 收集器的老年代版本,支持多线程并发收集。下面就是它的运行示意图
动态链接
Serial(串)复制
下面列举可以作为GC Roots的对象:Java虚拟机栈中被引用的对象,各个线程调用的参数、局部变量、临时变量等。方法区中类静态属性引用的对象,比如引用类型的静态变量。方法区中常量引用的对象。本地方法栈中所引用的对象。Java虚拟机内部的引用,基本数据类型对应的Class对象,一些常驻的异常对象。被同步锁(synchronized)持有的对象。
运行时数据区
字符串常量池
自底向上检查该类是否已经加载
Class实例引用
由于CPU执行指令是可中断的,会有线程切换,程序计数器会记录当前线程执行的字节码指令地址(行号),以便线程切换后能恢复到正确的执行位置是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
字节码抽象语法树
抽象语法树
与虚拟机栈非常相似,区别在于本地方法栈为虚拟机的Native方法服务;Hotspot将JVM栈和本地方法栈合二为一
基于计数器的热点探测方法调用计数器回边计数器
方法区常量
TLAB 线程私有
文本字符串
符号引用
字节码->JIT编译(代码膨胀10X)->执行->结果
否
GC Root
.class
验证
young
object2
堆 heap
old
解释执行
执行<init>
CodeCacheJIT编译代码产物
解析
对象
编译是已整个方法作为编译对象,编译完成后,方法的调用入口地址呗替换成编译后的方法地址
记录出栈地址即方法返回地址或者异常地址
字段名称和描述符
object6
类和结构的全限定名
Serial Old(串)整理
Extention ClassLoader - 扩展类加载器
数组
对象的创建过程
App
虚拟机栈
垃圾回收算法
加载
实例数据
自顶向下进行实际查找和加载
Client Compiler(C1) 速度
Bootstrap ClassLoader - 根类加载器
分代收集算法
程序计数器
双亲委派机制
大对象(可设置)会直接进入老年代,年龄达到15或者Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一般,大于等于该年龄的所有对象直接进入老年代
可回收对象
准备
对象头设置
G1(并发)整理+复制
返回结果
垃圾收集器 -- 连线代表可搭配使用
动态链接(多态,编译器没有指明运行时才指明对象),指向常量池中方法的引用
方法数据
Garbage First 收集器Garbage First(简称G 1)收集器是垃圾收集器发展史上里程碑式的成果,主要面向服务端应用程序。另外G 1收集器虽然还保留新生代和老年代的概念,但是新生代和老年代不在固定,它们都是一系列区域的动态集合。
局部变量表
老年代 (2/3)(MojorGC)
Parellel Old(并)整理
MarkWord
其他
年龄15
标记整理算法
类型信息
Java程序最初是通过解释器(interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器成为即时编译器(Just In Time Compiler下文统称为JIT编译器)。
线程1
栈中变量
数组长度(数组)
执行编译后的机器码
提交编译请求
方法区静态变量
Parellel Scavenge(并)复制
对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。1.Full GC会对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数。2.导致Full GC的原因1)年老代(Tenured)被写满调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 。2)持久代Pemanet Generation空间不足增大Perm Gen空间,避免太多静态对象 , 控制好新生代和旧生代的比例3)System.gc()被显示调用垃圾回收不要手动触发,尽量依靠JVM自身的机制
热点探测
Class字节码
Eden (8/10)
类加载过程
新生代 (1/3) -- MinorGCC
App ClassLoader - 应用类加载器
同步锁持有对象
Server Compiler(C2) 质量
类型指针
1、JVM使用一组OopMap的数据结构来标记对象引用的位置,在类加载完成的时候,JVM就会把对象在什么偏移量上是什么类型的数据计数出来。这样就可以快速且准确地完成GC Root的枚举2、如果为每一条指令都生产对应的OopMap,那就会需要大量的额外存储空间,所以只需要在特定的安全点添加存储空间,所以具有指令序列复用的指令才会产生Safepoint,如方法调用、循环跳转、异常跳转等。安全点暂停方式:抢先式中断、主动式中断。3、如果程序sleep或者blocked,就不会走到安全点,这时需要已用安全区域。
是否已编译
object5
解释器字节码->解释执行->结果
new
方法代码
收藏
收藏
0 条评论
回复 删除
下一页