深入理解Java虚拟机(一)——JVM内存模型

时间:2022-05-05 21:21:19

Java虚拟机的内存空间分为五个部分:

  • 方法区
  • 虚拟机栈
  • 本地方法栈
  • 程序计数器

深入理解Java虚拟机(一)——JVM内存模型

程序计数器

定义

程序计数器是一个块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指令器。程序计数器记录的是当前线程正在执行的那一条字节指令的地址。

如果当前线程正在执行的是一个本地方法,那么程序计数器为空。

作用

程序计数器有两个作用:

  1. 字节码解释器可以通过改变程序计数器的值,从而实现堆代码执行顺序的控制,如顺序、循环;
  2. 可以线程切换的情景下,记录当前线程的运行位置。

特点

  1. 占用空间小;
  2. 线程私有;
  3. 唯一一个不会出现OutOfMemoryError的内存区域。
  4. 生命周期伴随线程的生死。

Java虚拟机栈

定义

Java虚拟机栈是描述Java方法执行线程内存模型。
在方法被执行的时候,Java虚拟机栈会同步创建一个栈帧用于存储方法运行时所需的信息,包括:

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

特点

  1. 每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
  2. 经常有人把Java内存区域笼统地分为堆内存和栈内存,这样划分过于粗糙。栈通常指地就是Java虚拟机栈,或者更多情况下指的是虚拟机栈中局部变量表部分。
  3. 局部变量表存放了编译器客之的各种Java虚拟机基本数据类型、对象引用和returnAddress类型(指向了条字节码指令的地址)。
    这些数据类型在局部变量表中的存储空间以局部变量槽Slot在表示,其中64位长度的数据类型会占用两个变量槽,其余只占用一个。所以,一个变量槽的大小是32位的。局部变量表所需的内存空间在编译期间完成分配,进入方法是时,这一个方法子啊栈帧中分配的局部变量大小不会改变。
  4. 虚拟机栈会抛出两种异常:
    • 如果线程请求的栈深度大于虚拟机所允许的深度,就会抛出*Error异常;
    • 如果虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
  5. 线程私有。

注:*Error和OutOfMemoryError的异同?
*Error表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。
而OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。

本地方法栈

定义

本地方法栈为虚拟机使用的本地方法服务,功能与虚拟机栈类似。

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

Java堆

定义

堆是用来存放对象实例的空间,还包括数组、字符串。
几乎所有对象实例都存放在堆中。现值类型的支持,通过即时编译技术,对象可能会分配到其他内存中。

特点

  1. 是各个线程共享的内存区域。(整个Java虚拟机只有一个堆,所有的线程都访问同一个堆。而程序计数器、Java虚拟机栈、本地方法栈都是一个线程对应一个的。)
  2. 在虚拟机启动时候创建。
  3. 垃圾回收的主要场所。
  4. 可以细分为:新生代和老年代。新生带可以分为Eden、From Survior、To Survior。所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区,用于提升分配时的效率。将堆细分知识为了更好地回收内存,或者更快地分配内存。
  5. 堆的在逻辑内存空间应当是连续的。
  6. 堆的大小可以是固定的,也可以是可扩展的。主流的虚拟机是按照可扩展的来实现的(通过参数-Xmx和-Xms设定)。如果堆中内存大小无法满足实例的分配,而且也无法扩展时,虚拟机将会抛出OutOfMemoryError异常。

方法区

定义

方法区是用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
《Java虚拟机规范》 中把方法区描述为堆的一个逻辑部分,从逻辑上可以与堆区分开。

特点

  1. 线程共享:是堆的逻辑部分,因此和堆一样是线程共享的。虚拟机中方法区唯一。
  2. 永久代:方法区的数据与需要长期存在,它是堆的逻辑部分,所以,用堆的划分方法,可以把方法区称为老年代。JDK8,HotSpot用元空间取代了永久代。
  3. 内存回收效率低,因为其中数据需要长期存在,所以一次回收只能回收少量数据。
    对方法区的内存回收目标是对常量池的回收和对类型的卸载。
  4. 虚拟机规范对方法去要求比较宽松,可以不实现垃圾回收。

运行常量池

运行常量池用于存储方法区中常量的这一部分。

在一个类中通过public static final来声明一个常量。这个类被编译后便生成Class文件,这个类的所有信息都存储在这个class文件中。

运行常量池相对于Class文件常量池具有动态性,在运行期间也可以将新的常量放入池中,例如String类的intern()方法。

当常量池无法申请到内存时会抛出OutOfMemoryError异常。

常量池中的某些常量没有被引用时就需要被回收。

直接内存

直接内存并不属于Java虚拟机的内存,但是也会被频繁地使用。

在NIO类,引入了一种基于通道与缓冲区的IO方式。它可以Native函数库直接分配堆外内存,然后通过一个存储在Java堆里的DirectByteBuffer对像作为这块内存的引用进行操作。避免了Java堆和Native堆之间的数据复制,从而提高了内存效率。

直接内存的分配不会收到Java虚拟机堆大小的限制,但是,既然是内存,还是会收到本机总内存的限制,不够的时候就会抛出OOM。

总结

  1. 线程私有的区域:程序计数器、虚拟机栈、本地方法栈。生命周期和其所属线程一样。
  2. 线程共享的区域:堆和方法区。虚拟机启动就创建,停止就销毁。
  3. 只有程序计数器不会抛出OOM异常,其他内存区域如果无法向虚拟机申请到最后的空间,都会抛出OOM。
  4. Java虚拟机内存模型中的两个栈分别是虚拟机栈和本地方法栈。两个栈的区别是:虚拟机栈描述的是Java方法运行过程的内存模型,而本地方法栈式描述Java本地方法过程的内存模型。
  5. Java虚拟内存模型中的两个堆分别是Java堆和方法区。方法区属于Java堆的逻辑部分。区别是:堆中存放实例对象,而方法区存放类信息、常量、静态变量、即时编译器编译的代码数据。
  6. 堆是Java虚拟机中最大的一块内存区域,也是垃圾收集器的主要工作区域。