JVM内存模型

 

 

1.程序计数器

线程私有的一小块内存空间,存储当前线程正在执行的Java方法的JVM指令偏移地址,即当前线程所执行的字节码的行号(说行号只是为了方便理解,实际上不是行号)。字节码解释器就是通过改变该值来选择下一条要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都是依赖该内存完成。

★ 每个线程有自己独立的程序计数器。因为JVM的多线程机制是  通过轮流切换线程并且分配线程执行时间,所以在一个确切的时间点一个处理器(如果是多核,将其看做一个内核)能够执行的命令只有一条。因此每次线程切换后(CPU执行权发生切换时),为了让处理器能恢复到该线程的正确位置,就依赖程序计数器存的地址来恢复之前的线程。各线程有独立的程序计数器,互不影响,独立存储,称之为“线程私有”内存。栈 也是线程私有内存。

一个线程在任何时刻都可能失去执行权,所以,程序计数器的内存空间必须在创建线程时就分配好

程序计数器是唯一一个JVM规范中没有规定任何OOM情况的内存区域

因为它只需要存储当前线程执行字节码指令的偏移地址,当执行下一条指令的时候只需要改变地址值,不需要重写申请新的内存空间,所以永远不会发生内存溢出。

它的生命周期随着线程的创建而创建,随着线程的死亡而死亡。

★ 如果正在执行native方法,则该线程程序计数器为空(Ubdifined)。因为native方法都是C/C++编写,不会产生字节码,所以也不会有指令地址。

2.虚拟机栈(线程栈)

Java方法执行的内存模型,线程私有,与其对应的线程生命周期相同。主要作用是用来存局部变量。

线程栈由一个个栈帧组成。数据结构中的栈,是一种FILO的数据结构。而线程栈就是由“栈”这种数据结构维护着一个个栈帧。

何为栈帧?线程中每调用一个方法,都会给这个方法在线程栈中分配一小块方法自己的内存,就是栈帧。

JVM对栈帧的操作只有两种,压栈和出栈。每次方法调用,都会压栈,调用方法结束后出栈。

栈帧的组成部分:

1. 局部变量表:用来存局部变量(编译器可知的的基本数据类型变量的值 引用数据类型变量的引用)、方法参数成员变量的引用,JVM通过索引定位的方式使用局部变量表。

局部变量表所需的内存空间在编译器完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

局部变量表的容量以槽(slot)为最小单位,32位虚拟机中可以存放一个32位以内的数据类型(boolean,byte,char,shot,int,float,reference和returnAddress八种),存64位长度的long和double型的数据会占用两个局部变量空间(Slot)。

★reference类型,对象引用,可能是一个执行对象起始地址的引用指针,也可能是指向一个代表对象的局部或其他于此对象相关的位置。

★returnAddress类型,指向一条字节码指令的地址。

2. 操作数栈

 Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指-操作数栈。

JVM把操作数栈作为它的工作区,大多数指令都要从操作数栈弹出数据,执行运算,然后压会操作数栈。

是一个后入先出的栈,数据结构为线性表。其大小、当前帧的最大栈深度在编译时就已确定。

用于存储局部变量表、操作栈、动态链接、方法出口等信息。

int a = 1;
int b = 2;
int c = a + b;

这行代码的class文件通过javap -c 反汇编成JVM指令后:

0: iconst_1        //将int类型常量1压入操作数栈 
1: istore_0        //将int类型值存入局部变量0
2: iconst_2        //将int类型常量2压入操作数栈 
3: istore_1        //将int类型值存入局部变量1
4: iload_0         //从局部变量0中装载int类型值 
5: iload_1         //从局部变量1中装载int类型值 
6: iadd            //执行int类型的加法 
7: istore_2        //将int类型值存入局部变量2 
8: iload_2         //从局部变量2中装载int类型值 
9: ireturn         //从方法中返回int类型的数据 

可以看出,Java方法中每一行代码,都要经过操作数栈的处理。

3. 常量池引用:正在执行的方法对应对象的常量池的引用。

4. 动态链接

5. 返回结果地址。方法返回分两种情况,正常完成和异常完成。

如果执行过程中没有引起异常,就是正常完成。此时当前帧会携带着操作结果返回到操作树栈中,随后栈帧被销毁。

如果执行过程中JVM抛出异常,意外完成的方法执行行不会返回结果给操作树栈。

 

★ 编译时就确定了成员变量表的大小、操作树栈、与当前帧相关联的方法的字节码。

(成员变量不管是基本数据类型和引用数据类型都是存在堆里面的,在类创建对象的时候初始化。当该类方法被调用的时候压入栈)

(静态变量存在方法区)

在JVM规范中,对栈这个区域规定了两种异常情况:StackOverflowError异常,OutOfMemoryError异常。

StackOverflowError:如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常。

OutOfMemoryError:如果虚拟机栈支持动态扩展(当前大部分虚拟机支持动态扩展,不过Java虚拟机规范也允许固定长度的虚拟机栈),且扩展时无法申请到足够内存,就会抛出OutOfMemoryError异常。

 

3. 本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。

Java虚拟机规范中对本地方法栈中方法使用的语言、使用方式、数据结构没有强制规定,因此由具体的虚拟机自由实现。甚至有些虚拟机(Sun HotSpot虚拟机)直接把本地方法栈和虚拟机栈合二为一。

与虚拟机栈一样,本地方法栈也会抛出StackOverflowError、OutOfMemoryError异常。

 

4. 堆

所有线程共享的一块内存,是大多数应用的JVM中最大一块,在JVM启动时创建。堆区唯一的作用就是存放实例,几乎所有的对象实例都是在这里分配内存。

堆内存会被垃圾回收器进一步划分,分为新生代、老年代、持久代。

根据Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。

 

5. 方法区

各个线程共享的内存区域,用于存储被已经加载的元(meta)数据:类信息、常量、静态变量、即时编译器编译后的代码等数据。另外,常量池也在方法区中。

JVM规范规定,当方法区无法满足内存分配时,会抛出OutOfMemoryError异常。

方法区中,每个数据类型都对应一个常量池,

 

6. 运行时常量池

是方法区的一部分。

Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant PoolTable),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

每个class文件头四个字节称为Magic Number,作用是确定是是否是一个可以被JVM接受的文件;

接着的四个字节存储的是class文件的版本号;

挨着版本号的就是常量池入口。

常量池主要存放两类常量:

1. 字面量(Literal),如String, final常量值。

2. 符号引用,存放了一些与编译相关的常量,因为Java不像C++那样有连接的过程,因此字段、方法等这些符号引用在运行期就需要进行转换,以便得到真正的内存入口地址。

class文件中的常量池称为静态常量池,JVM完成class装载后,会把静态常量池加载到内存中,存放在运行时常量池。

7. 直接内存

直接内存不属于Java规范规定,属于JVM运行时数据区的一部分。Java的NIO可以使用native方法直接在JVM堆外分配内存,使用DirectByteBuffer对象作为堆外内存的引用。

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值