JVM 内存模型划分
JVM 运算时数据区详细介绍
- 运行时数据区的作用:JVM 在运行写好的代码时,会使用到多块内存空间,不同的内存空间用来放不同的数据,然后配合我们写的代码流程,才能让我们的系统运行起来
方法区(Method Area)
- 在
JDK1.8
之前称之为方法区
- 主要是放从
“.class”
文件里加载进来的类,还会有一些类似常量池的东西放在这个区域里 -
JDK1.8
以后,这块区域的名字改了,叫做“Metaspace”
,可以认为是“元数据空间”
这样的意思 - 方法区是被所有线程共享的内存区域,用来存储已被虚拟机加载的类信息、常量、静态变量、JIT(just in time,即时编译技术)编译后的代码等数据
- 运行时常量池是方法区的一部分,用于存放编译期间生成的各种字面常量和符号引用
- 通过反射获取到的类型、方法名、字段名称、访问修饰符等信息就是从方法区获取到的
- 在使用到
CGLib
对类进行增强时,增强的类越多,就需要越大的方法区类存储动态生成的Class
信息,当存放方法区数据的内存溢出时,会报OutOfMemoryError
异常 - 可以通过参数
JVM
参数-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
设置Metaspace
的空间大小
程序计数器(Program Counter Register)
- 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器
- 程序计数器就是用来记录当前执行的字节码指令的位置的,也就是记录目前执行到了哪一条字节码指令
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令:分支、跳转、循环、异常处理、线程恢复等基础操作都会依赖这个计数器来完成
- 每个线程都有独立的程序计数器,用来在线程切换后能恢复到正确的执行位置,各条线程之间的计数器互不影响,独立存储
- 字节码执行引擎在执行代码时,会修改程序计数器的值
- 它是一个
“线程私有”
的内存区域
虚拟机栈(VM Stack)
- Java 在执行代码时,是以线程的方式帮我们执行代码
- 每一个线程都有自己的
Java虚拟机栈
,用来存放自己执行那些方法的局部变量 - JVM 栈是线程私有的内存区域,它描述的是 java 方法执行的内存模型
- 每个方法执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
- 每个方法从调用直至完成的过程,都对应着一个栈帧从入栈到出栈的过程
- 每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧
本地方法栈( Native Method Stack)
- 本地方法栈和虚拟机栈所发挥的作用是很相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(字节码)服务
- 本地方法栈则为虚拟机使用到的
Native
方法服务
堆(Heap)
- 对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块
- Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建
- 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存
字节码结构
- Class 文件是一组以
8
位字节为基础单位的二进制流
CLass 结构
- 无符号数:基本类型,u1,u2,u4,u4 分别代表一个字节、两个字节、四个字节、八个字节的无符号数
- 多个无符号数或者其他表作为数据项构成的复合数据类型,习惯以
_info
结尾,整个 class 文件本质上就是一张表
Class 具体结构
魔数
- 每个 class 文件的头 4 个字节称为魔数
- 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件
- 大部分文件存储标准都是通过魔数来进行身份验证的
- 对 Class 文件来说魔数值为
0xCAFEBABE
版本号
- 在魔数之后的 4 个字节存储的是 class 文件的版本号
- 前两个字节是次版本号,后两个是主版本号
常量池
- 常量池可以理解为 class 文件中的资源仓库,它是 class 文件结构中与其他项目关联最多的数据类型
常量池主要存放两大类常量
字面量
- Java 语言的常量概念,final 修饰的关键字,字符串等等
符号引用
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
访问标志
- 常量池之后是
access_flags
- 这个标志用于识别一些类或者接口层次的访问信息
- 例如这个 class 是类还是接口,是 public 还是 abstract 等等
- flags:(0x0021) ACC_PUBLIC,ACC_SUPER
类索引、父类索引、接口索引集合
- 类索引和父类索引各自指向一个类型为
CONSTANT_Class_info
的类描述符常量 - 通过这个类描述符常量中的索引值可以找到定义在
CONSTANT_Utf8_info
类型的常量中的全限定名字符串
字段表集合
-
field_info
用于描述接口或者类中声明的变量 - 字段包括类级变量以及实例级变量,不包括方法中的临时变量
- 字段表的结构用
access_flags
来表示作用域、是否 final、是否 static 等等
方法表集合
- 类似于字段表集合,由于方法没有
volatile
和transient
关键字,因此access_flags
中没有ACC_VOLATILE
标志和ACC_TRANSIENT
标志 - 而增加了
synchronized
、native
等修饰方法的关键字
栈帧
局部变量表
- 局部变量表是用来存储一组变量值的内存空间,用于存放方法参数和方法内部定义的局部变量
- 在已经编译好的 Class 文件中,方法的 Code 属性的
max_locals
数据项中,就确定了该方法所需分配的局部变量表的最大容量
变量槽(Variable Slot)
- 局部变量表的容量以变量槽(Variable Slot)为最小单位
- 每个变量槽存放一个 32 位数据类型
- 如 boolean、byte、char、short、int、float 和 reference 这几种类型
reference
- reference 类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针
- reference 类型表示对一个对象实例的引用
- 通过这个引用做到两件事情:根据引用直接或间接地查找到实例在 Java 堆中的数据存放的起始地或索引
- 根据引用直接或间接地查找到在方法区中的存储的类信息
当一个方法被调用时,会使用局部变量表来完成参数值到参数变量列表的传递过程。如果执行的是对象实例的成员方法,那么局部变量表中第 0 位索引的变量槽默认就是该对象实例的引用,其余参数则按照参数表顺序排列,参数表分配完毕后,再根据方法体内部定义的局部变量顺序和作用域分配其余的变量槽。
操作数栈
- 操作数栈是一个后入先出(Last In First Out,LIFO)栈。和局部变量表一样
- 在已经编译好的 Class 文件中,方法的 Code 属性的
max_stacks
数据项中,就确定了该方法所需分配的操作数栈的最大深度 - 在方法执行的任何时候,操作数栈的深度都不会超过在
max_stacks
数据项中设定的最大值 - 当一个方法刚刚开始执行的时候,该方法的操作数栈是空的
- 在该方法的执行过程中,会有各种字节码指令对操作数栈进行出栈和入栈的操作
- 比如,整数加法的字节码指令
iadd
,在该指令执行前必须保证操作数栈中最接近栈顶的两个元素已经存入了两个 int 型的数值 - 当该指令执行时,会把这两个 int 值出栈并相加,然后将相加的结果重新入栈
动态链接
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用
- 包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接 (Dynamic Linking)比如:
invokedynamic
指令 - 不是所有方法调用都需要动态链接的,有一部分符号引用会在类加载解析阶段
- 比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的
- 那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
方法出口
- 当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址
当一个方法开始执行时,可能有两种方式退出该方法
- 正常完成出口
- 异常完成出口
一般来说,方法正常退出时,调用者的 PC 计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。
栈帧调用流程详细分析
堆内存结构
- 堆是 Java 虚拟机所管理的内存中最大的一块存储区域。
堆内存被所有线程共享
- 主要存放使用
new
关键字创建的对象。所有对象实例以及数组都要在堆上分配 - 垃圾收集器就是根据 GC 算法,收集堆上对象所占用的内存空间
Java 堆
年轻代(Young Generation)
- 年轻代又分为伊甸园(Eden)
幸存区(Survivor区)
幸存区又分为 From Survivor 空间,To Survivor 空间,年轻代存储 “新生对象”
,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发 Minor GC
,清理年轻代内存空间。
老年代(Old Generation)
老年代存储长期存活的对象和大对象,年轻代中存储的对象,经过多次 GC 后仍然存活的对象会移动到老年代中进行存储,老年代空间占满后,会触发 Full GC
,Full GC 是清理整个堆空间,包括年轻代和老年代。如果 Full GC 之后,堆中仍然无法存储对象,就会抛出 OutOfMemoryError
异常。
GC 过程
- 当发出一个
Minor GC
时,垃圾回收器就会对Eden
区的内容进行回收- 如果某一个对象没有被任何指针引用,刚会被清理如果对象有指针引着,就会把该对象移到幸存区
s0
并且对该对象的分代年龄+1
- 再一次再发出
Minor GC
时,会同时对Eden
区和S0
区进行回收,没有被回收的对象,全部转移到s1
区,对象的分代年龄+1
- 再下一次时,同时对
Eden
区和s1
区进行回收,没有被回收的对象,会被转移到s0
区,对象分代年龄+1
- 当对象的分代年龄到达
15
时,就会把该对象转移到老年代- 当老年代中的内容存满时,就会触发
full GC
,清理所有的内容