深入理解Java虚拟机 -- Java运行时数据区域

x33g5p2x  于2021-11-21 转载在 Java  
字(4.8k)|赞(0)|评价(0)|浏览(273)

本文参考于《深入理解Java虚拟机》

1. 运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。其包括:程序计数器、Java虚拟机栈、本地方法栈、Java堆和方法区

1.1 程序计数器

(1)、什么是程序计数器?

程序计数器(Program Counter Register) 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在物理层面上是由寄存器实现的它用于存储下一条所要执行的 JVM 指令的执行地址

(2)、为什么需要程序计数器?

在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。它的核心作用就是:用于存储下一条所要执行的 JVM 指令的执行地址。由执行引擎执行下一条JVM指令。

图示说明

(3)、程序计数器的相关特点

  1. 它是一块很小的内存空间几乎可以忽略不计,也是运行速度最快的存储区域
  2. 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程生命周期保持一致。
  3. 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地 址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)
  4. 此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

(4)、使用程序计数器存储字节码指令地址有什么用?为什么使用程序计数器记录当前线程的执行地址呢?

因为CPU需要不停的切换各个线程,这时候切换回来以后就得知道接着从哪儿开始继续执行。JVM的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令

(5)、程序计数器为什么是线程私有的?

为了线程切换后能恢复到正确的执行位置每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为 “线程私有”的内存

(6)、Java代码的执行过程

  1. 首先Java代码会被转换为JVM指令二进制字节码,进而Java可以实现跨平台运行
  2. 然后解释器会对JVM指令进行转换,从而转换为机器码给CPU执行
  3. 与此同时,程序计数器的中的值指向下一条需要执行的JVM指令的地址

1.2 Java虚拟机栈

(1)、什么是Java虚拟机栈?

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同(即随着线程的创建而创建,随着线程的消亡而消亡)。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

(2)、为什么需要虚拟机栈?

每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

(3)、虚拟机栈由什么组成?

虚拟机栈由一个又一个的栈帧组成,但是每个线程只有一个活动栈帧(即当前正在执行的方法),如果一个方法需要执行,则相应的栈帧需要入栈;如果一个方法执行结束,则相应的栈帧需要出栈。

一个栈帧包含以下几部分

  1. 局部变量表:局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的小。请读者注意,这里说的“大小”是指变量槽的数量,虚拟机真正使用多大的内存空间(譬如按照1个变量槽占用32个比特、64个比特,或者更多)来实现一个变量槽。
  2. 操作数栈:也可以称之为表达式栈(Expression Stack),在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)。某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用它们后再把结果压入栈,比如:执行复制、交换、求和等操作。操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间

  1. 动态链接
  2. 方法出口等信息

(4)、该区域常出现的两种异常状况

  1. StackOverflowError异常:如果Java虚拟机栈的容量不可以动态拓展,线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
  2. OutOfMemoryError异常:如果Java虚拟机栈的容量可以动态拓展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常

(5)、Java方法的结束方式

  1. return语句
  2. 抛出异常

1.3 本地方法栈

(1)、什么是本地方法栈?

Java虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务的。

补充概念

本地方法:当Java虚拟机需要和操作系统底层进行交互的时候需要调用C或者C++方法,而所调用的C或者C++方法就是本地方法

(2)、本地方法栈的作用

和Java虚拟机栈作用类似,本地方法栈用于存放相关的本地方法所被调用产生的栈帧当一个本地方法被调用时,相应的栈帧入栈;当一个本地方法调用结束时,相应的栈帧出栈。而本地方法栈的栈帧存放的是局部变量表、操作数栈、动态链接和方法出口等信息。

(3)、本地方法栈可能出现的异常状况

本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowErrorOutOfMemoryError异常

1.4 Java堆

(1)、什么是Java堆?

Java堆(Java Heap) 是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,所以此处的对象会涉及线程安全问题。在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。

几乎一词的说明
而这里笔者写的“几乎”是指从实现角度来看,随着Java语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了

(2)、GC堆说明

Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC堆”。从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆中经常会出现“新生代”“老年代”“永久代”“Eden空间”“From Survivor空间”“To Survivor空间”等名词。这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,而非某个Java虚拟机具体实现的固有内存布局,更不是《Java虚拟机规范》里对Java堆的进一步细致划分

(3)、常见的异常:OutOfMemoryError

Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩
展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

1.5 方法区

(1)、什么是方法区?

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载
类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据

(2)、方法区的实现方式

方法只是一个抽象概念只是定义了相关的规范就好比方法区是抽象类,定义了相应的规范,而它的实现方式(永久代或者元空间)就好比是实现了该抽象类的子类

  1. 永久代(JDK1.7之前)

  1. 元空间(JDK1.8之后)

(3)、永久代到元空间的变化

到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出到堆中,而到了JDK 8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)(主要是类型信息)来代替。

(4)、为什么要将永久代改换为元空间?

当年使用永久代来实现方法区的决定并不是一个好主意,这种设计导致了Java应用更容易遇到 内存溢出的问题(永久代有-XX:MaxPermSize的上限,即使不设置也有默认大小),无法进行调整;而元空间使用的是直接内存(操作系统的内存),虽然还是可能会导致元空间内存溢出,但是概率变低了。
元空间溢出时的错误:java.lang.OutOfMemoryError: MetaSpace

1.6 运行时常量池

(1)、什么是运行时常量池?

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

(2)、常见异常状况

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

(3)、运行时常量池的位置

  1. JDK1.7之前,方法区是由永久代实现的,所以运行时常量池也位于永久代内部
  2. JDK1.7时,方法区中的运行时常量池中字符串常量池从方法区移出到堆中,而运行时常量池的剩余部分还留在方法区中,方法区的实现方式为永久代。
  3. JDK1.8时,永久代被元空间替代,而此时字符串常量池还留在堆中,运行时常量池还留在方法区中

所以运行时常量池一直处于方法区中,只不过方法区的实现方式发生了改变,同时随着实现方式发生了改变,它的实际物理地址由堆中转移到了本地内存中

2. 直接内存

(1)、什么是直接内存?

直接内存(Direct Memory)不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中
定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制

(2)、新引入的NIO类

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

相关文章

微信公众号

最新文章

更多