面试题之——JVM内存模型

 2022-09-10
原文地址:https://blog.csdn.net/qq_45901741/article/details/113738214

对于JVM的内存模型,面试中通常会直接问你对它的认识和了解多少,所以我们的回到应该由浅入深,逐层刨析。

一. Java 虚拟机内存模型

首先,我们得知道,JVM内存空间分为五部分,分别是:方法区、堆、Java虚拟机栈、本地方法栈、程序计数器。
这些数据区域可以分为两个部分:一部分是线程共享的,一部分则是线程私有的。其中,线程共享的数据区包括方法区和堆,线程私有的数据区包括虚拟机栈、本地方法栈和程序计数器。如下图所示:

202209102332579141.png

接下来我们挨个再说。

1.1、线程共享数据区域

1.1.1、方法区

方法区用于存储已被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据 。方法区通常和**永久区(Perm)**关联在一起,但永久代与方法区不是一个概念,只是有的虚拟机用永久代来实现方法区,这样就可以用永久代GC来管理方法区,省去专门内存管理的工作。根据Java虚拟机规范的规定,当方法区无法满足内存分配的需求时,将抛出 OutOfMemoryError 异常。

在这里要注意一个东西:“ 运行时常量池

运行时常量池 (Runtime Constant Pool)是方法区的一部分,用于存放编译期生成的各种 字面量 和 符号引用

1)、字面量比较接近Java语言层次的常量概念,如 文本字符串、被声明为final的常量值等
2)、符号引用则属于编译原理方面的概念,包括以下三类常量: 类和接口的全限定名、字段的名称和描述符 和 方法的名称和描述符 。因为运行时常量池(Runtime Constant Pool)是方法区的一部分,那么当常量池无法再申请到内存时也会抛出 OutOfMemoryError 异常。

运行时常量池相对于Class文件常量池的一个重要特征是具备动态性。Java语言并不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,比如字符串的手动入池方法intern()。

1.1.2、Java 堆

Java 堆的唯一目的就是存放对象实例,几乎所有的对象实例(和数组)都在这里分配内存 。Java堆是线程共享的,类的对象从中分配空间,这些对象通过new、newarray、 anewarray 和 multianewarray 等指令建立,它们不需要程序代码来显式的释放。

注意:由于Java堆唯一目的就是用来存放对象实例,因此其也是垃圾收集器管理的主要区域,故也称为称为 GC堆
从内存回收的角度看,由于现在的垃圾收集器基本都采用分代收集算法,所以为了方便垃圾回收Java堆还可以分为 新生代老年代
新生代用于存放刚创建的对象以及年轻的对象,如果对象一直没有被回收,生存得足够长,对象就会被移入老年代。
新生代又可进一步细分为 eden、survivorSpace0 和 survivorSpace1。刚创建的对象都放入 eden,s0 和 s1 都至少经过一次GC并幸存。如果幸存对象经过一定时间仍存在,则进入老年代。

1.2、线程私有的数据区

线程私有的数据区 包括 程序计数器、 虚拟机栈 和 本地方法栈 三个区域,它们的内涵分别如下:

1.2.1、程序计数器

在了解程序计数器之前,我们需要知道一个概念: 线程是CPU调度的基本单位

    也就是说,在多线程情况下,当线程数超过CPU数量或CPU内核数量时,线程之间就要根据 
    时间片轮询抢夺CPU时间资源。也就是说,在任何一个确定的时刻,一个处理器都只会执行一
    条线程中的指令。
    因此,为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器去记
    录其正在执行的字节码指令地址。

于是,程序计数器是 线程私有的一块较小的内存空间 ,其可以看做是当前线程所执行的字节码的行号指示器。

1、如果线程正在执行的是一个 Java 方法,计数器记录的是正在执行的字节码指令的地址。
2、如果正在执行的是 Native(本地)方法,则计数器的值为空。

他有两个作用:
1、字节码解释器通过改变程序计数器来一次读取指令,从而实现代码的流程控制,比如我们常见的 顺序、循环、选择、异常处理 等。
2、在多线程的情况下,程序计数器用来记录 当前线程执行的位置 ,当线程切换回来的时候仍然可以知道该线程上次执行到了哪里。

而且,程序计数器是唯一一个没有规定任何 OutOfMemoryError 的区域。

1.2.2、虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型。每个方法在执行的时候都会创建一个 栈帧 ,用于存储 局部变量表、操作数栈、动态链接、方法出口 等信息,而且 每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。

比如:我们方法执行过程中需要创建变量时,就会将局部变量插入到局部变量表中,局部变量的运算、传递等在操作数栈中进行,当方法执行结束后,这个方法对应的栈帧将出栈,并释放内存空间。

虚拟机栈有两种异常情况:StackOverflowErrorOutOfMemoryError。我们知道,一个线程拥有一个自己的栈,这个栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss 参数可以设置虚拟机栈大小)。
1、若线程请求的栈深度大于虚拟机允许的深度,则抛出 StackOverFlowError 异常。此外,栈的大小可以是固定的,也可以是动态扩展的,、。
2、若虚拟机栈可以动态扩展(大多数虚拟机都可以),但扩展时无法申请到足够的内存(比如没有足够的内存为一个新创建的线程分配栈空间时),则抛出OutofMemoryError 异常。

1.2.3、本地方法栈

本地方法栈与Java虚拟机栈非常相似,区别是:
1、虚拟机栈为虚拟机执行 Java 方法服务。
2、本地方法栈为虚拟机执行 Native(本地)方法服务,运行本地方法时也会创建栈帧,同样栈帧里也有局部变量表、操作数栈、动态链接和方法返回地址等,在本地方法执行结束后栈帧也会出栈并释放内存资源。

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