JVM 内存模型
运行时数据区域
程序计数器(Program Conunter Regisiter)
程序计数器是一个比较小的内存空间,可以看作是当前线程执行的字节码行号指示器。本质就是记录字节码执行顺序。 在《Java 虚拟机规范》中没有任何 OutOfMemoryError 情况的区域。
虚拟机栈(JVM Stack)
虚拟机栈,存放的是线程运行时内部的局部变量,也可以理解为线程栈。
每个方法被执行的时候, 虚拟机会创建一个栈帧(Stack Frame)用于存放局部变量表(local variable),操作数栈(operand stack),动态连接,方法出口等信息。
栈帧(Stack Frame)随着方法的调用而创建,随着方法的结束而销毁(不论是正常结束还抛出异常)。
字节码指令分析(描述 JVM Stack 操作过程)
public int add() { int a = 1; int b = 2; int c = b - a; return c; } 0 iconst_1 //将 a 压入局部变量表栈顶 1 istore_1 //对 a 进行赋值 1 2 iconst_2 //将 b 压入局部变量表栈顶 3 istore_2 //对 b 进行赋值 2 4 iload_2 //读取 b 到操作数栈 5 iload_1 //读取 a 到操作数栈 6 isub //执行 b - a 7 istore_3 //将 int 类型的值存入局部变量表 3 8 iload_3 //读取 c 到操作数栈 9 ireturn //返回
局部变量表(local variable)
局部变量表存放了各种编译期 Java 虚拟机基本数据库类型(boolean、byte、char、short、int、float、long、dubble)和对象引用(reference 类型,即对象的起始位置指针或者对象句柄), 对象的真实数据通常存放在堆空间。
局部变量表中的存储空间通过变量槽(slot) 来表示,其中 64 位长度的 long 和 double 占 2 个变量槽。
操作数栈(operand stack)
每个栈帧都包含一个操作数栈的先进先出(FIFO)栈,栈帧中操作数栈的深度由编译期决定,并且通过方法的 code 属性保存以及提供给栈帧使用。
动态链接
每个栈帧都包含一个指向当前方法所在类型的运行时常量池的引用。以便对当前方法的代码实现动态链接。
在 class 文件中,一个方法如果要调用其它方法, 或者访问局部成员变量,则需要将符号引用(synbolic reference)来表示,动态链接的作用就是将这些符号引用转换为对实际方法的直接引用。
方法出口
方法正常完成,当前栈帧恢复调用者的责任,包括恢复调用者的局部变量表,操作数栈,以及正确的程序计数器递增。跳过刚才执行的方法调用指令等,低哦啊用着的代码被调用的方法正返回值压入调用者操作数栈后,会继续正常执行。
方法一场完成,某些指令导致了 JVM 虚拟机抛出异常,或者用户显示的通过 thorw
关键字跑出一场,同时在改该方法中没有捕获异常。如果方法异常调用完成,那不一定有方法返回值返回给调用者。
本地方法栈(Native Method Stack)
为本地方法所分配的内存空间,就是为 native
关键字修饰的方法提供服务的。
本地方法主要是 Java 来调用 C/C++ 函数库的调用方法。
方法区(Method Area)
主要存放数据有:常量,静态变量,类信息。
方法区存放的是静态变量的内存地址, 方法区里面有一个元空间, 在JDK1.8 之前叫永久代。
堆(Heap)
JVM 管理的最大的一块内存空间。与堆相关的一个重要概念是垃圾收集器。几乎所有的垃圾收集器都是采用分代收集算法,所以对内存空间也是基于这一点进行相应的划分:新生代和老年代,新生代分为Eden 空间、From Survivor 空间、To Survivor 空间。
对象创建的过程中首先会存在 Eden 区,然后经过 minor gc 过后进入 survivor ,进过 15 次 survivor 转移过后,进入老年代。
如果内存都不够用了就触发 full gc, 再次触发 GC 过后无法分配申请内存,JVM 就会抛出 OOM。
分析对象是否存在引用,是否被回收采用的是 GC ROOT 可达性分析。
直接内存(Direact Memory)
直接内存,不是 JVM 来管理,是通过操作系统来管理的, 与 Java NIO 密切相关。 Java 通过DirectByteBuffer来操作直接内存。