JVM内存模型
2025-04-21 23:17:36 0 举报
JVM内存模型
作者其他创作
大纲/内容
语义分析器
自定义类加载器
线程B
CPU socket-0
执行引擎执行字节码的方式 -Xint 纯字节码解释器 -Xcomp 纯模板解释器 -Xmixed 混合模式 字节码解析器(JVM针对不同系统做相同的实现) java代码(字节码) -》 C++代码 -》 硬编码(二进制) 模板解析器(JIT的一部分) 字节码 -> 硬编码 触发即时编译 放在方法区:热点代码缓存模板解析器的底层原理:1. 将 new 方法的硬编码拿过来2.申请一块可读可写可执行的内存区域(MAC 不支持 JIT)3.将硬编码写入4.声明一个函数指针指向这个区域5.通过这个函数指针掉用这块内存
运行时数据区
运行时常量池
寄存器1
JAVA源文件
方法区
计算的临时数据存储区方法入栈时数据会先加载到这里,再根据需要加载到局部变量表里
本数据类型会直接存值,引用数据类型会存放对象的引用
HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。 JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。 JDK 9 引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。 JDK 支持分层编译和 AOT 协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。
类装载子系统
Survivor区
field references
load
本地库接口
Application ClassLoader
。。。
方法()...栈帧
initFlag = true
Epsilon收集器(适用于微服务):控制内存分配,但是不执行任何垃圾回收工作。一旦java的堆被耗尽,jvm就直接关闭。目标提供一个完全消极的GC实现,分配有限的内存分配,最大限度降低消费内存占用量和内存吞吐时的延迟时间。
线程A
寄存器n
即时编译器 C1编译器 -client(32位) C2编译器 -server(64)1.需要收集的数据比较少 1.需要收集的数据比较多2.编译,优化方面都比较浅 2.优化更彻底3.编译生成的代码执行效率比C2低 3.编译时耗费CPU资源4.编译时耗费的CPU资源比C2 低 4.执行效率更高 即时编译触发条件 方法的执行次数 C2 N 默认值为 10000 C1 N 默认值为 1500 循环的执行次数 热度衰减 执行引擎执行某个方法后一段时间未执行,则会发生 2 倍数的 衰减 热点代码缓存 方法区 默认大小 C2:2496KB C1:160K 最大大小 C2: C1: 分层编译技术 系统启动初期优化用 C1 ,热机后采用 C2
工作内存(线程A)initFlag = false
时钟
initFlag= true
寄存器
字节码执行引擎
JMM 8大数据原子操作1. lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态2. unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定3. read(读取):把一个变量值从主内存传输到线程的工作内存中,以便后随后的load动作使用4. load(载入):它把read操作从主内存中得到的变量值放入工作内存的变量副本中5. use(使用):把工作内存中的一个变量值传递给执行引擎6. assign(赋值):它把一个从执行引擎接收到的值赋给工作内存的变量7. store(存储):把工作内存中的一个变量的值传递到主内存中,以便随后的 write 的操作8. write(写入):把 store 操作从工作内存中的一个变量的值传送到主内存的变量中
core-1
线程 B
二级级缓存
寄存器...
修改行号
100M
java -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar microservice-eureka-server.jar
线程共享数据区
堆
1M
元空间调优规则最小:20.75,最大:2的48次方1.像调节堆一样,最大,最小设置成一样大小(防止内存忽大忽小,造成内存抖动)2.具体设置成 物理内存的 1/323.visualVM,arthas监控元空间的使用情况的4. 20%-30%的空间预留
词法分析器
线程栈
内存总线
jvm执行方法流程 ( 方法1执行完后返回 )1.修改线程的 操作数栈 开始指针 为 main方法 的2.修改线程的 局部表的 开始指针 为 main方法 的3.恢复 main方法 的 程序计数器4.判断有了没有返回值,如果有,则压入main方法的操作数栈5.释放栈帧
线程隔离数据区
工作内存
内存
工作内存(线程B)initFlag = false
use
执行
硬盘
public class Main { public static final int initData = 666; public static Object obj = new Object(); public int compute() { int a = 1; int b = 2; int c = (a + b) * 10; return c; } public static void main(String[] args) { Main ma = new Main(); ma.compute(); System.out.println(\"test\"); }}
Eden(8/10)
c=30
系统总线
加载
标记-整理 算法(标记压缩)(Mark Compact)
JVM内存模型
numberic constants
动态链接
core-3
虚拟机栈
探测器(Profiler)
栈帧4
双亲委派模型是描述类加载器之间的层次关系,模型要求除了Bootstrap ClassLoader外,其余的类加载器都要有自己的父加载器。子加载器通过组合来复用父加载器的代码,而不是使用继承。在某个类加载器加载class文件时,它首先委托父加载器去加载这个类,依次传递到顶层类加载器(Bootstrap)。如果顶层加载不了(它的搜索范围中找不到此类),子加载器才会尝试加载这个类。双亲委派模型好处:安全性像 java.lang.Object 这些存放在 rt.jar 中的类,无论使用哪个类加载器加载,最终都会委派给最顶端的启动类加载器加载,从而使得不同加载器加载的 Object 类都是同一个。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放在 classpath 下,那么系统将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无法保证。如何打破双亲委托机制?答案是:使用 Thread.getContextClassLoader() - 当前线程的上下文加载器,该加载器可通过 Thread.setContextClassLoader() 在代码运行时动态设置。
一级缓存
方法信息
双亲委派模型
对象实例数据
serial old收集器
注解语法树
I/O总线
c=30
G1收集器(span style=\"font-size: inherit;\
控制器
为了解决效率问题,使用了 “复制”收集算法 ,它可以将内存分为大小相同的两块,每次使用一块,每次使用其中的一块。当这一块的内存使用完后,就将存活的对象复制到另一块,然后再把使用的空间一次清理掉。这样就使每次的内存回收都对内存区间的另一半进行回收
一些附加信息
Volatile(底层)可见性实现原理底层实现:通过汇编 lock前缀指令触发底层缓存锁定机制(缓存一致性协议&总线锁)例如触发MESI协议,lock指定会触发 锁定变量缓存行 区域并写回主内存,这个操作称为 ”缓存锁定“ ✔ 缓存一致性机制阻止同时修改被两个以上处理器缓存的内存区域数据(MESI协议)✔ 一个处理器的缓存回写到内存会导致其它处理器的缓存无效(MESI协议)IA-32架构
方法1()-栈帧
应用类加载器
CMS收集器
CPU-1
方法4
initFlag = false
本地方法栈
局部变量表:1.一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。2.class文件中方法表code属性的max_locals数据项中定义了该表的容量最大值。3.该表的容量计量单位为slot,一个slolt可以存放32位以内的数据。4.虚拟机通过索引的方式使用局部变量表。索引从0开始。方法执行时,索引为0的slot默认用于传递方法所属对象的引用,方法中可以用 this 关键字来访问这个隐含的参数5.为了节省空间,表中slot是可以重用的
store
其它I/O 设备
initFlag= false
线程(执行引擎)while(! initFlag)
网卡
向上委托
jdk9不再支持
主线程
512M
CPU-0
方法1
垃圾回收器
字节码解析器
句柄池
复制算法(Copying)
Class文件
ALU
老年代
新生代
所有的 类 都是在 Eden 区 new出来的。Survivor 区 有2个,from 和 to(会互相交换) 。当Eden的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对Eden区进行垃圾回收(Minor GC),将Eden园区中不再被其他对象引用的对象进行销毁。然后将Eden中剩余的对象移动到Survivor区。若Survivor的from区满了,再对该区进行垃圾回收,然后移动到to区【当对象大于Survivor设置内存大小的50%,会移动到 Old Gen】
串行收集器serial
Extension ClassLoader
Bootstrap ClassLoader
堆内存模型
ZGC 收集器:ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
token流
验证
线程(执行引擎)initFlag = true
类型信息
算法 分为“标记”和“清除”阶段: • 标记:从根节点开始标记引用的对象 清除:未被标记引用的对象就是垃圾对象,可以被清理 但是1.效率效率差(2个阶段都需要遍历)2.空间问题(碎片化严重)
方法出口
初始化
垃圾收集 Minor GC
三级级缓存
磁盘控制器
ZGC 收集器(jdk11)
串行收集器:使用单线程进行垃圾回收,只有一个线程在工作,并且java应用中所有线程都要暂停等待垃圾回收的完成, STW现象(Stop-The-World)Javaweb应用中不会采用该收集器
垃圾收集器(Garbage Collection)
总线接口
s0(1/10)
程序计数器=10
new Object();
新生代(1/3)
字节码生成器
编译器(JIT Compiler)
新生代(Minor GC)
方法返回地址
电商网站线上某个方法经常产生60M/s的大对象,频繁地Full GC,针对这种情况如何调优?
程序计数器
局部变量表
标记-清除 算法(Mark Sweep)
800M
域信息
本地方法库
s1(1/10)
系统层面
Young区经过过次CG仍存活的对象(分代年龄 = 15)会移动到Old Gen,若 Old Gen 满了,则会产生 Major GC(Full GC),会发生STW(Stop the world)。若 Full GC 后还无法进行对象的保存则会产生 OutOfMemoryError
Shenandoah收集器:只有OpenJDK才会包含的收集器,最开始由RedHat公司独立发展后来贡献给了OpenJDK,相比G1主要改进点在于:支持并发的整理算法,Shenandoah的回收阶段可以和用户线程并发执行;Shenandoah 目前不使用分代收集,也就是没有年轻代老年代的概念在里面了;Shenandoah 摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率。
硬件层面
read
把 initFlag = false写回线程栈中
运算器
老年代(2/3)
JMM
I/O桥
逃逸分析变量的作用域全局变量,就会发生逃逸局部变量,不会发生逃逸栈上分配标量替换 标量:不可再分,基本数据类型 聚合量:可再分,引用类型锁消除 当发现锁的对象是局部变量,就会把锁消除掉
汇编语言
操作数栈:1.操作数栈的最大深度在编译的时候已经写入方法表的 code 属性的 max_stacks数据项中。2.操作数栈的每一个元素都可以是任意的java数据类型,32位的数据占用栈空间为1,64位占用为2。3.方法刚开始执行时,操作数栈是空的,方法执行过程中,会有各种字节码指令往操作数栈中存储数据。4.操作数栈中元素的数据类型必须是与字节码指令的序列严格匹配。 例如 iadd指令用于执行整数加法,一定不能用于操作一个long类型的情况
启动类加载器
引用ma
各种控制器
方法2
程序计数器(线程私有):就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),又执行引擎读取吓一条指令,是一个非常小的内存空间,几乎可以忽略不记
类加载生命周期口诀:加、连(验、准、解)、初、使、卸加载: 负责通过类全限定名可获取二进制字节码文件 并将其加载到虚拟机中连接: 验证:魔数开头,版本,字节码,符号引用 准备 int i => 0 解析 符号引用 转换成 直接引用初始化: 执行静态初始化代码 1.new 2.反射 3.子类调用初始化 4.指定初始化类
本地方法栈
操作数栈
方法出口:1.当一个方法开始执行后,只有两种方式可以退出这个方法。2.第一种方式是执行引擎遇到任意一个方式返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,是否有返回值以及返回值类型将根据遇到方法返回字节码指令来决定,这种退出方式成为正常完成出口3.另一种退出方式是在方法的执行过程中出现了异常,并且这个异常没有在方法体内得到处理,无论是jvm内部产生的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口。这种情况下,方法是不会给上层调用者返回任何值。4.无论采用何种退出方式,在方法退出后,都需要返回到方法被调用的位置继续执行,方法返回时可能需要在战阵中保存一些信息,用来帮助恢复他的上层方法的执行状态。5.方法退出时可能执行操作有:恢复上层方法的局部变量表和操作数栈,把返回值,压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方向调用指令后面的一条指令地址
当Old Gen 满后会做对整个堆做 Full GC
Epsilon 收集器
连接
分代收集算法
为什么标记清理算法会产生内存碎片!但是CMS仍采用这种算法呢?答案是:因为CMS作为第一款实现用户线程和收集线程并发执行的收集器!当时的设计理念是减少停顿时间,最好是能并发执行!但是问题来了,如要用户线程也在执行,那么就不能轻易的改变堆中对象的内存地址!不然会导致用户线程无法定位引用对象,从而无法正常运行!而标记整理算法和复制算法都会移动存活的对象,这就与上面的策略不符!因此CMS采用的是标记清理算法!
局部变量表
Shenandoh收集器(openJDK)
Full GC参数优化
线程 A
assign
obj
Main.class
CPU socket-1
其他线程
准备
string constants
栈帧2
Old 和 Eden 分配得内存大小为 3:1Eden 和 Survivor分配的内存大小为 8 :1:1
根据老年代的特点推出 标记算法,标记过程仍与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一段移动,然后直接清理掉端边界以外的内存。
core-2
compute()-栈帧
b=2
a=1
对象类型数据
老年代(Major GC/Full GC)
栈帧3
方法3
2G
java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar
class references
core-4
附加信息虚拟机规范允许具体的虚拟机实现增加一些规范中没有描述的信息到栈帧之中,例如和调试相关的信息,这部分信息完全取决于不同的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其他附加信息一起归为一类,称为栈帧信息。
CPU缓存
帧数据区(三者统称)
parallel old收集器
执行引擎1.字节码解析器2.模板解析器3.JIT4.逃逸分析5.栈上分配
案例:热机切冷机故障在运行长的JVM系统集群中添加新的机器, 并把热机的流量切到冷机,导致并发过大,冷机崩溃。因为热机采用的是 C2 编译器,而冷机开始用的是C1 ,字节码还未生成完全,并发处理比热机要低。
并行收集器:ParNew同样用于新生代,是Serial的多线程版本。它的并行仅仅指的是收集多线程并行,并不是收集和原程序可以并行进行。ParNew也是需要暂停程序一切的工作,然后多线程执行垃圾回收。
元空间(1.8之前叫永久代,1.8后由本地内存实现)
栈帧1
JVM调优案例
解释器(interpreter)
并行收集器Parlnew
JAVA源码级编译器
。。。
扩展类加载器
总线bus访问主内存(MESI 缓存一致性协议)
write
method references
JMM缓存一致性底层实现机制总线锁(性能很低) CPU从主存读取数据到缓冲区当中,总线会加锁锁定该缓存对应的主存区域,来自其它CPU或者总线代理的控制请求将被阻塞,无法读写内存直到锁定被释放MESI缓存一致性协议 M:修改,E:独占,S:共享,I:失效 多CPU从主存读取同一数据到各自缓存区中,该数据在lock前缀指令执行期间已经在处理器内部的缓存中被锁定,缓存被锁定期间其它CPU无法读写该数据,直到该缓存数据被修改同步回主存后,其它CPU通过 总线嗅探机制 感知数据变化及时失效自己缓存中的数据,在下一指令周期从主存重新 load 数据
其他方法栈帧
二进制机器码
抽象语法树
栈帧的内部结构
代码优化器(Code Optimizer)
语法分析器
new Main();
Java虚拟机栈(线程栈):Java线程执行方法的内存模型,一个线程对应一个栈,每个方法在执行的同时都会创建一个栈帧(用于存储局部变量表,操作数栈,动态链接和进出口信息)不存在垃圾回收问题,只要线程一结束该栈就会释放,生命周期和线程一致JVM对该区域规范了两种异常:1.线程请求的栈深度大于虚拟机栈所允许的深度,将抛出StackOverflowError异常。2.若虚拟机栈可动态扩展,当无法申请到足够内存空间时将抛出OutOfMemoryError, 通过jvm参数 -Xss指定栈空间,空间大小决定函数调用的深度
G1 收集器
目标代码生成器(Target Code Generator)
Parallel scavenge收集器
解析
主内存
main()-栈帧
FILO 栈:线程1(私有)
当前虚拟机的垃圾收集都采用分代收集算法,这种算法只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样就可以根据各个年代的特点选择合适的垃圾收集算法。新生代的中每次垃圾收集中会发现有大批对象死区,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。老年代中因为对象的存活率高,没有额外的控件对它进行分配担保,就必须使用“标记-清扫”或者“标记-整理”算法来进行回收。
中间代码生成器(Imtermediate Code Generator)
0 条评论
下一页