jvm
2022-11-23 09:35:28 0 举报
jvm
作者其他创作
大纲/内容
内存分布
线程私有
虚拟机栈
解释
早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame)
每一个方法被调用直到执行完毕的过程,就对应着虚拟机栈从入栈到出栈的过程
生命周期与线程相同
结构
局部变量表(Local Variables)
局部变量表称为局部变量数组或者本地变量数组
局部变量表存放了编译期可知的各种Java数据类型(boolean、byte、short、int、long、float、double),对象引用(reference类型,只是指向对象起始位置的一个引用指针)以及returnAddress类型(指向了一条字节码指令的地址)
局部变量表是线程私有的数据,不存在数据安全问题
局部变量表所需的容量大小是在编译期确定下来的,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定后的,在方法运行期间不会改变局部变量表的大小。这边具体就体现在方法Code属性的maximum local variables数据项中。
局部变量表最基本的存储单元是Slot(变量槽),变量槽也都是从索引为0开始计数。
局部变量表存储的数据类型中,64位长度的double,long类型会占据两个Slot,其他对象引用包括returnAddress都占据一个Slot。
JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引就可以成功访问到局部变量表中指定的局部变量值
当前帧是由构造方法或实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列
需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。
表现方式
操作数栈(Operand Stack)
操作数栈也被称为操作栈,它是一个后进先出(Last In First Out LIFO)栈。和局部变量表一样,操作数栈的最大深度也是编译的时候写入到Code属性的max_stacks数据项中
操作数栈可以包含任意的Java数据类型,和局部变量表一样,long和double占两个栈容量,其它占一个栈容量。
当一个方法开始执行的时候,这个操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容。
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译期要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。
表现方式:
动态链接(Dynamic Linking)
指向运行时常量池的方法引用
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
表现方式:查看字节码时 右边的#11等显示
方法返回地址(Return Address)
当一个方法开始执行的时候只有两种方式退出方法,一种是执行引擎遇到了任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,另一种就是方法执行过程中遇见了异常,并且异常没有被处理。
无论采用哪种退出方式,方法退出之后,都必须返回到最初方法被调用时的位置。一般来说,方法正常退出时,主调用方法的PC计数器的值就可以作为返回地址,即调用该方法的指令的下一条指定的地址。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧一般不保存这部分信息。
方法退出的过程就是出栈的过程,出栈的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈,调整PC寄存器的值指向方法调用指令的后面一条指令。
操作方式
结构
当前栈帧(Current Frame)
当前方法(Current Method)
当前类(Current Class)
局部变量表(Local Variables)
操作数栈(Operand Stack)
每次方法的执行都对应着栈帧入栈、出栈的操作
执行引擎运行的所有字节码指令只针对当前栈帧进行操作
如果在当前方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈顶,成为新的当前栈
可能的问题
当前线程请求的栈深度超过虚拟机栈最大深度的时候,会出现StackOverFlow异常,可以通过-Xss来设置栈空间的内存大小
本地方法栈
解释
方法栈用于管理本地方法的调用。使用Java代码调用非Java代码的接口称作为Native Method
程序计数器
解释
存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行本地方法,则是未指定值(undefined)
线程共享
堆
定义
类,方法,常量,变量,保存我们所有引用类型的真实对象
参数定义
-Xms
表示堆的起始内存
等价于 -XX:InitialHeapSize
-Xmx
表示堆的最大内存
等价于 -XX:MaxHeapSize
设定技巧
通常会将 -Xmx 和 -Xms 两个参数配置为相同的值,其目的是为了能够在垃圾回收机制清理完堆区后不再需要重新分隔计算堆的大小,从而提高性能
默认情况下,初始堆内存大小为:电脑内存大小/64
默认情况下,最大堆内存大小为:电脑内存大小/4
存放内容
存放对象实例
详细分类
新生代
eden
survivor
from survivor
to survivor
新生代分配规则
当创建一个对象时,对象会被优先分配到新生代的 Eden 区
此时 JVM 会给对象定义一个对象年轻计数器(-XX:MaxTenuringThreshold)
当 Eden 空间不足时,JVM 将执行新生代的垃圾回收(Minor GC),JVM 会把存活的对象转移到 Survivor 中,并且对象年龄 +1,对象在 Survivor 中同样也会经历 Minor GC,每经历一次 Minor GC,对象年龄都会+1
新生代:Minor GC回收规则
eden->from survivor->to survivor->from survivor->to survivor...
默认是15 次回收标记后去老年代
老年代
分配规则
大对象直接进入老年代
避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝
如果分配的对象超过了-XX:PetenureSizeThreshold设置的大小,对象跳过新生代和新生代gc会直接被分配到老年代
旧的一代内存包含那些经过许多轮小型 GC 后仍然存活的对象
老年代:Major GC回收规则
内存不足后进行Major GC
Major GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常
分类参数
默认情况下新生代和老年代的比例是 1:2,可以通过
–XX:NewRatio
来配置 新生代中的 Eden:From Survivor:To Survivor 的比例是 8:1:1,可以通过 -XX:SurvivorRatio 来配置
JDK 8 是默认开启-XX:+UseAdaptiveSizePolicy,会动态调整 JVM 堆中各个区域的大小以及进入老年代的年龄,开启后每次 GC 后都会重新计算 Eden、From Survivor、To Survivor 的大小,计算依据是GC过程中统计的GC时间、吞吐量、内存占用量; –XX:NewRatio会失效;
查看方式
jmap -heap 进程号
方法区(元数据区)/永久代/元空间
方法区
运行时常量池
直接内存
jit
GC
回收区域
堆
类,方法,常量,变量,保存我们所有引用类型的真实对象和实例
方法区
GC类型
Major GC
触发条件
老年代内存满时执行
Full GC
Minor GC
GC描述
新生代GC
一个Eden区和两个Survivor
触发条件
当 Eden 区的空间耗尽了怎么办?这个时候 Java虚拟机便会触发一次 Minor GC来收集新生代的垃圾,存活下来的对象,则会被送到 Survivor区。
当新生代的Eden区满的时候触发 Minor GC
参考链接:
https://zhuanlan.zhihu.com/p/163602877
GC回收触发
触发算法
引用计数算法
因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
可达性分析算法
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收
方法区的回收
主要是对常量池的回收和对类的卸载
该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:
该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
禁用类的垃圾收集(GC)
finalize()
该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。
自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法
相关知识
引用类型
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关
1.强引用
使用 new 一个新对象的方式来创建强引用
被强引用关联的对象不会被回收
2. 软引用
使用 SoftReference 类来创建软引用。
被软引用关联的对象只有在内存不够的情况下才会被回收
3. 弱引用
使用 WeakReference 类来实现弱引用。
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
4.虚引用
使用 PhantomReference 来实现虚引用。
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。
垃圾回收算法
标记 - 清除
将存活的对象进行标记,然后清理掉未被标记的对象。
不足:
标记和清除过程效率都不高
会产生大量不连续的内存碎片,导致无法给大对象分配内存
标记 - 整理
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
复制
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半
分代收集
根据对象存活周期将内存划分为几块,不同块采用适当的收集算法
一般将堆分为新生代和老年代
一般使用
新生代使用: 复制算法
老年代使用: 标记 - 清除 或者 标记 - 整理 算法
参考资料
https://pdai.tech/md/java/jvm/java-jvm-gc.html#_3-%E5%A4%8D%E5%88%B6
垃圾收集器
新生代
Serial
ParNew
Parallel Scavenge
G1
老年代
Serial Old
Parallel Old
CMS
G1
相关知识
单线程与多线程: 单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程;
串行与并行: 串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并形指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
参考资料:
https://pdai.tech/md/java/jvm/java-jvm-gc.html#_3-%E5%A4%8D%E5%88%B6
分类
HotSpot VM
J9 VM
JRockit
jdk1.8
HotSpot VM+JRockit VM
JRockit内部不包含解释器实现,全部代码都靠及时编译器(JIT)编译后执行
虚拟机介绍
基于栈式的虚拟机
基于栈式指的是虚拟机在执行指令时,采用的方式是基于栈的指令集,会将需要执行的指令一条条的压入栈中,主要有入栈和出栈两种操作
从移植性上看,栈式的虚拟机会远强于基于寄存器模型的虚拟机
可以无视硬件、物理架构。当然,缺点也非常明显,因为无论什么操作都要经过操作数栈,所以性能会低一些
java虚拟机实现
a+b=7步
传输到操作数栈
基于寄存器模型的虚拟机
Android的Dalvik、Lua5.0的RegisterBased等虚拟机都是基于寄存器模型实现的
用于模拟CPU中真实的PC寄存器
这些虚拟的寄存器也并不是直接在CPU中的,而是和操作数栈一样,位于运行时栈中,通过一个数组(运行时栈帧中的连续内存空间)存储所有的虚拟寄存器。
a+b=3步
传输到CPU后刷回寄存器
0 条评论
下一页