JVM内存结构

什么是Java虚拟机


  • Java Virtual Machine(JVM):java虚拟机,用来保证Java语言跨平台
  • Java虚拟机可以看做是一台抽象得计算机,如同真实的计算机那样,它有自己的指令集以及各种运行时内存区域
  • Java虚拟机与Java语言并没有必然的联系,它只于特定的二进制文件格式(class文件格式)所关联
  • Java虚拟机就是一个字节码翻译器,它将字节码文件翻译成各个系统对应的机器码,确保字节码文件能在各个系统正确运行

JVM体系结构图


JVM体系结构图

JVM内存结构


1.程序计数器

1.1 概念

程序计数器是计算机处理器中的寄存器,它包含当前正在执行的指令的地址(位置)。当每个指令被获取,程序计数器的存储地址加一。在每个指令被获取之后,程序计数器指向顺序中的下一个指令。当计算机重启或复位时,程序计数器通常恢复到零。
冯 ·诺伊曼计算机体系结构的主要内容之一就是“程序预存储,计算机自动执行”!处理器要执行的程序(指令序列)都是以二进制代码序列方式预存储在计算机的存储器中,处理器将这些代码逐条地取到处理器中再译码、执行,以完成整个程序的执行。为了保证程序能够连续地执行下去,CPU必须具有某些手段来确定下一条取指指令的地址。程序计数器(PC )正是起到这种作用,所以通常又称之为‘指令计数器’。
摘自百度百科

1.2 特点

  • 程序计数器是一个以线程私有的一块较小的内存空间,用于记录所属线程所执行的字节码行号指示器;字节码解释器工作时,通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳准、异常处理、线程恢复等基础功能都需要依赖程序计数器来完成。

  • 程序计数器具备线程隔离性,每个线程在运行是都会有处于自己独特的程序计数器。在多线程中,会存在线程上下文切换(CPU 时间片)执行,为了线程切换后能恢复正确的执行位置,所以需要从程序计数器中获取该线程需要执行的字节码的偏移地址

  • 如果线程执行 Java 方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址。如果执行 Navtive 方法,程序计数器值则为空(Undefined)。

  • 由于是线程私有的,生命周期随着线程,线程启动而产生,线程结束而消亡。

  • Java 虚拟机规范里面, 唯一 一个没有规定任何 OutOfMemoryError 情况的区域,由于保存的是线程需要执行的字节码的偏移地址,当执行下一条指令的时候,改变的只是程序计数器中保存的地址,并不需要申请新的内存来保存新的指令地址,因此,不会产生内存溢出。


2.虚拟机栈

2.1 特点

  • 虚拟机栈是每个线程运行时所需要的内存空间,是线程私有的,即每个线程都有自己独立的虚拟机栈。他的生命周期于线程相同
  • 虚拟机栈描述的是Java方法执行的线程内存模型:每个java方法在执行时,会创建一个**“栈帧(stack frame)”**,每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
  • 每个线程只有一个活动栈帧,对应着当前正在执行的那个方法。

2.2 栈帧

2.2.1 组成

局部变量表,操作数栈,动态连接,方法出口

2.2.1.1 局部变量表
  • 存放局部变量的列表,用于存放方法参数和方法内部定义的局部变量
  • 一个局部变量可以保存的类型为boolean、byte、char、short、float、reference(对象引用类型)和returnAddress(指向了一条字节码指令的地址)的数据
  • 局部变量表中的存储空间以**局部变量槽(Slot)**来表示。
  • 局部变量表的所需要的内存空间在编译期间完成分配。当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的空间
2.2.1.2 操作数栈
  • 也称为操作栈,它是一个后进先出(Last In First Out)的栈
  • 当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用这,也就是出栈/入栈操作,一个完整的方法执行期间往往包含多个这样的入栈/出栈的过程
  • 简单理解,操作数栈是线程实际的操作台
2.2.1.3 动态连接
  • 简单的理解为指向运行时 运行时常量池(JVM 运行时数据区域) 的引用
  • 在class文件里面,描述一个方法调用了其他方法,或者访问其他成员变量是通过符号引用来表示的,动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用
2.2.1.4 方法出口
  • 方法调用的返回,包括正常返回(由返回值)和异常返回(没有返回值),不同的返回类型由不同的指令
  • 无论方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法返回是可能需要在当前栈帧中保存一些信息,用来帮他恢复他的上层方法执行状态
2.2.2 栈帧的生命周期

方法调用时,创建栈帧,并压入虚拟机栈;方法执行完毕,栈帧出栈并被销毁
一个栈帧的生命周期

2.2.3 虚拟机栈异常
  • 如果线程请求分配的栈容量超过了 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出 StackOverflowError 异常
  • 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将抛出一个 OutOfMemoryError 异常

3 本地方法栈

3.1 特点

  • 本地方法栈是一个后进先出(Last In First Out)栈
  • 由于是线程私有的,生命周期随着线程,线程启动而产生,线程结束而消亡。
  • 本地方法栈于虚拟机栈所发挥的作用非常相似,其区别是虚拟机栈为执行虚拟机Java方法(编译后字节码)提供服务,而本地方法栈则是为虚拟机使用到的 Native 方法提供服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
  • Native 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。

3.2 本地方法栈异常

同虚拟机栈一样本地方法栈会抛出 StackOverflowErrorOutOfMemoryError 异常。


4 Java堆

4.1 特点

  • Java堆(Java Heap)是虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的一块区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例
  • 几乎所有的对象实例都在Java堆中分配内存,由于即时编译技术的进步,尤其是逃逸分析技术的日益强大, 栈上分配、标量替换优化手段已经导致发生了一些变化,所以说Java对象实例在堆上分配也变得不那么绝对了
  • Java堆是垃圾收集器管理的内存区域
  • 从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率
  • Java堆可以处于物理上不连续的内存,但在逻辑上它必须被视为连续的。但对于大对象的(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。
  • Java堆既可以被实现成固定大小,也可以是扩展的,不过当前主流的JVM都是按照可扩展来实现的(通过参数 -Xmx和 -Xms设定)

4.2 Java堆异常

如果在Java堆中没有内存完成对象实例分配,并且堆也无法扩展时,Java虚拟机将会抛出OutOgMemoryError异常。


5 方法区

5.1 特点

  • 方法区是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即使编译器编译后的代码缓存等数据
  • JDK7以前Hotspot 使用永久代来实现方法区,这种设计导致Java应用更容易遇到内存溢出的问题(永久代有 -XX:MaxPermSize的上限,即使不设置也有默认大小),而且有极少数的方法(例如String::intern())会因为永久代的原因而导致不同虚拟机下有不同的表现。
  • 到了JDK7的Hotspot,已经把原本存放在永久代的字符串常量池,静待变量等移除
  • 到了JDK8,完全废弃了永久代的概念,将JDK7中永久代的剩余内容(主要包括类型信息)全部移到了在内地内存中实现的元空间中(Meta-space)
  • 《java虚拟机规范》对方法区的约束比较宽松,不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以不选择实现垃圾收集

5.2 方法区异常

如果方法区无法满足新的内存分配需求,将抛出OutOfMemoryError异常


6 运行时常量池

6.1 特点

  • 运行时常量池(Runtime Constant Pool)是方法区的一部分
  • Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一部分信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量于符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中。
  • 运行时常量池相对于Class文件常量池另外一个重要的特征时具备动态性,Java语言并不要求常量一定只有编译期才能产生,这种特性利用的比较多的便是String类的intern()方法;

6.2 运行时常量池异常

因为时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常


7 直接内存

7.1 特点

  • 在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
  • 本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小

7.2 直接内存异常

配置虚拟机参数时,如果各内存区域总和大于物理内存限制(包括物理和操作系统级的限制) 从而导致动态扩展时出现OutOfMemoryError异常

参考

本内容基本来自《深入理解Java虚拟机》第三版 –周志明。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值