【JVM】JVM内存结构&Java内存模型

1 JVM内存结构

根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存包括以下几个运行时数据区域。

1.1 程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。其主要作用有两个方面:

1.字节码解释器在工作时通过改变技术器的值来选取下一条需要执行的字节码指令,包括分支、循环、跳转等逻辑都要依靠该计数器来完成;

2.使多线程环境下线程切换后能恢复到正确的执行位置。因为java多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,同一时刻、同一处理器(或内核)只会执行一条线程中的指令。在线程切换时,依靠程序计数器记录当前线程目前执行的指令位置。

为了每个线程能正确执行,程序计数器是线程私有的。如果线程正在执行的是一个Java方法,线程计数器记录的是正在执行的虚拟机字节码指令位置;如果正在执行的是naive方法,则为空。

1.2 Java虚拟机栈

Java虚拟机栈的生命周期与线程的生命周期相同,其描述的是Java方法执行时的内存模型。每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。(我们平时所说的栈相当于Java虚拟机栈中局部变量表部分,用以存放基本数据类型和对象引用)
栈帧由局部变量表、操作数栈、动态链接、方法返回地址及附加信息等部分组成。

局部标量表是一组变量值的存储空间,用于存放 方法参数 和 局部变量。在Class 文件的方法表的Code属性的max_locals指定了该方法所需局部变量表的最大容量。

变量槽(Variable Slot)是局部变量表的最小单位,没有强制规定大小为 32 位,虽然32位足够存放大部分类型的数据。一个 Slot 可以存放 boolean、byte、char、short、int、float、reference 和 returnAddress 8种类型。其中reference表示对一个对象实例的引用,通过它可以得到对象在Java堆中存放的起始地址的索引和该数据所属数据类型在方法区的类型信息。returnAddress则指向了一条字节码指令的地址。对于64位的 long 和 double 变量而言,虚拟机会为其分配两个连续的 Slot 空间。

虚拟机采取类似于数组索引定位的方式获取每个slot中的值,下标0储存的是“this"指向的对象,即方法所属实例。

1.3 本地方法栈

本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务。有的虚拟机(如Hot Spot虚拟机)直接将本地方法栈和虚拟机合二为一。

1.4 Java堆

对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,用以存放对象实例,几乎所有的对象实例(包括数组)都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,因此也称之为GC堆。由于现在垃圾收集器基本都采用分代收集算法,Java堆从内存回收角度可分为新生代和老年代,还可进一步细分。

1.5 方法区

方法区用于储存已被虚拟机加载的类信息、常量、静态常量、及时编译器编译后的代码等数据。

1.5.1 常量池

Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字面量(字符串,数字等) ,还包含类、方法的信息,占用了class文件绝大部分空间。

运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

1.5.2 静态常量池(Static Constant Pool)

静态常量池主要用于存放两大类常量:字面量和符号引用量,字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值和基本数据类型的值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

  • 类和接口的全限定名
  • 字段名称和描述符
  • 方法名称和描述符

1.5.3 运行时常量池(Runtime Constant Pool)

运行时常量池相对于Class文件常量池的一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。

1.5.4 静态常量池和运行时常量池的区别

  • 静态常量池是class文件结构中的一个区域,它不存在虚拟机内存中,而运行时常量池存在于方法区中
  • 静态常量池是在编译阶段产生的,用于存放class文件信息的常量池,不具备被执行的能力,而运行时常量池是在class文件被JVM装载完成之后,静态常量池中的内容将被解析,并放到运行时常量池中以供程序运行使用的。

1.6 直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。在JDK1.4中新加入的NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用natative函数库直接分配堆外内存,通过一个储存在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中提升性能,因为避免了在Java堆和native堆中来回复制数据。

直接内存不受Java堆大小的限制,但是受本机总内存的限制。

1.7 异常

在JVM内存区域定义了两种异常,分别是OutOfMemoryError异常、StackOverflowError异常。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError,该异常出现在Java虚拟机栈区;如果虚拟机可以动态扩展,在动态扩展时无法申请到足够的内存,则抛出OutOfMemeoryError,除程序计数器外其他区域都有可能抛出该异常。

2 Java内存模型

Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。JMM是和多线程相关的,他描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。JMM定义了一些语法集,这些语法集映射到Java语言中就是volatile、synchronized等关键字。

2.1 主内存与工作内存

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory)。

  • 共享变量(类变量以及对象的全局实例变量等都是共享变量)存储于主内存中,每个线程都可以访问,这里的主内存可以看成是堆内存。
  • 每个线程都有私有的工作内存,这里的工作内存可以看成是栈内存。
  • 工作内存只存储该线程对共享变量的副本。
  • 线程不能直接操作主内存,只有先操作了工作内存之后才能通过工作内存写入主内存。
  • 不同的线程之间也不能直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

线程、主内存、工作内存三者的交互关系如图所示:

2.2 内存间的交互操作

关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。

  • 1、lock(锁定):作用于主内存中的变量,它把一个变量标识为一条线程独占的状态。
  • 2、unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • 3、read(读取):作用于主内存中的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • 4、load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。.
  • 5、use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • 6、assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • 7、store(存储):作用于工作内存中的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • 8、write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中。

流程图如图所示:

同时,JMM还规定了执行上述8种基本操作时必须满足的规则:

  • 1、不允许read和load、store和write操作之一单独出现
  • 2、不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存
  • 3、不允许一个线程无原因地把数据从线程的工作内存同步回主内存中
  • 4、一个新的变量只能从主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量
  • 5、一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁
  • 6、如果对同一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值
  • 7、如果一个变量事先没有被lock操作锁定,那就不允许对它进行unlock操作,也不允许去unlock一个被其他线程锁定的变量
  • 8、对一个变量执行unlock操作之前,必须先把此变量同步回主内存中

2.3 对于volatile型变量的特殊规则

volatile关键字是Java虚拟机提供的最轻量级的同步机制,有两层语义:

  • 保证此变量对所有线程的可见性,此处的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程立即可知;
  • 禁止指令重排序优化。

参考内容:

  • 《深入理解Java虚拟机》
  • Java 内存模型:
    http://www.importnew.com/28456.html
    https://www.jianshu.com/p/84bc9bcf7492
  • 线程与内存交互:
    https://www.cnblogs.com/hongwz/p/5948308.html
  • Java虚拟机运行时栈帧结构:
    https://blog.csdn.net/zq602316498/article/details/38926607
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值