性能文章>面试必问,JVM内存模型扫盲>

面试必问,JVM内存模型扫盲原创

10月前
185933

JVM简介

JVM(Java Virtual Machine,Java虚拟机)是Java语言的核心,是一个用于解释Java字节码的虚拟计算机。它可以在运行Java程序时自动管理内存、处理异常等。Java程序员不需要关心底层硬件和操作系统的细节,只需要编写符合Java语法规范的代码,就可以实现跨平台的编程。

当我们编写Java程序时,Java源代码会被编译成为Java字节码( .java 文件被编译成 .class 文件)。这些字节码可以在任何安装了Java虚拟机的平台上运行。JVM在执行Java字节码时,将其转换成特定于底层CPU和操作系统的机器代码。

运行时数据区简介

为了执行字节码,JVM在内存中定义了一系列的数据区,用于在运行时存储各类数据,即运行时数据区(Runtime Data Areas)。理解这些数据区及其作用,是掌握Java性能调优和错误排查的关键。

JVM 运行时数据区是 Java 虚拟机在执行 Java 程序时用于数据存储的内存区域,这些区域各司其职,确保了 Java 程序的正确执行。JVM 运行时数据区主要分为五个部分:程序计数器(Program Counter Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、堆(Heap)、方法区(Method Area)。JVM运行时数据区在程序运行时动态地分配和释放内存,内存管理由JVM自动完成。不同的数据区域有不同的内存管理机制和垃圾回收算法,以保证程序运行的效率和稳定性。

其中程序计数器、虚拟机栈、本地方法栈属于线程私有区域,跟随线程的启动和结束而建立和销毁。堆和方法区是线程共享区域,跟随虚拟机进程的启动而存在。

程序计数器(Program Counter Register) 是一块较小的内存空间,作用是指示当前线程正在执行的 JVM 字节码指令地址。

虚拟机栈(VM Stack) 存放的是一些基本类型的变量(如int, long)和对象引用。Java 方法执行的内存模型是以栈帧(Stack Frame)为基础的,每个方法在执行的时候都会创建一个栈帧,栈帧中存放了局部变量表、操作数栈、动态链接、方法出口等信息。

本地方法栈(Native Method Stack) 与虚拟机栈类似,其主要服务于 JVM 使用到的 Native 方法。

堆区(Heap) 是 JVM 所管理的最大一块内存空间,主要用于存放所有线程共享的 Java 对象实例。这也是垃圾回收器主要活动区域。

方法区(Method Area) 是用来存储加载的类信息、常量、静态变量等数据的。这个区域是线程共享的。

1. 程序计数器

程序计数器(Program Counter Register)是线程私有区域,生命周期与线程一致,也是 JVM 内存中唯一一个没有任何 OutOfMemoryError 的区域。

程序计数器的作用是记录当前线程正在执行的指令地址,换句话说,它指向了下一条将要被执行的 JVM 字节码指令。在 JVM 的概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

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

程序计数器对于现代多线程而言至关重要,因为在 CPU 切换各个线程时,需要将各个线程的程序计数器记录下来,以便在下一次切换回这个线程时,能知道该从哪里继续执行。

总结:

  • 程序计数器是一块很小的内存空间,也是运行速度最快的存储区域。

  • 在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期一致。

  • 如果当前线程正在执行的是 Java 方法,程序计数器记录的是 JVM 字节码指令地址,如果是执行 native 方法,则是未指定值(undefined)

  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成

  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令

  • 它是唯一一个在 JVM 规范中没有规定任何 OutOfMemoryError 情况的区域

2. 虚拟机栈

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,生命周期与线程相同。描述的是Java方法执行的内存模型。

在 JVM 中,每当一个新的线程被创建,都会创建一个与之关联的私有 JVM 栈。这个栈会随着线程的运行而进行入栈(push)和出栈(pop)操作。它主要用于存储局部变量、操作数堆栈以及方法调用的情况。

JVM 栈是由一系列栈帧(Stack Frame)组成的。每当一个方法被调用,一个新的栈帧就会被压入栈中,每当一个方法调用结束,一个栈帧就会被弹出栈。每个栈帧中都包含了局部变量表、操作数栈、动态链接和方法返回地址等信息。

局部变量表主要存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于指针,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。

操作数栈则是在执行字节码指令时用到的临时存储区,比如在进行算数运算时,操作数栈就会用来存放操作数和接收结果。

Java虚拟机栈可能会抛出以下异常:

  1. 如果线程请求的栈深度大于 JVM 所允许的深度,将抛出 StackOverflowError

  2. 如果 JVM 栈可以动态扩展,当扩展时无法申请到足够的内存,会抛出 OutOfMemoryError

3. 本地方法栈

本地方法栈(Native Method Stack)也是线程私有,生命周期与线程相同。作用是与虚拟机栈类似,虚拟机栈是为Java 方法服务的,而本地方法栈是为 Native 方法服务的。

和虚拟机栈一样,本地方法栈的大小可以是固定的也可以是动态的。如果是固定的,当线程请求的栈深度超过最大深度时,会抛出 StackOverflowError。如果是动态的,并且在尝试扩展时无法申请到足够的内存,会抛出 OutOfMemoryError。

4. 堆

堆(Heap)是 JVM 所管理的最大一块内存空间,也是所有线程共享的一块内存区域,在虚拟机启动时创建。堆主要用于存储对象实例和数组,这也是 Java 垃圾回收器主要活动的区域。

在物理上,堆区可以处于分散的内存空间中,但在逻辑上它被视为连续的。堆区在 JVM 启动时创建,如果堆区的空间不足,将会抛出 OutOfMemoryError

堆分为新生代(Young Generation)和老年代(Old Generation)。新生代又分为 Eden 区、From Survivor 区(简称 S0)、 To Survivor 区(简称 S1)。划分这么多区域的目的是为了更好地回收内存,或者更快地分配内存。

新生代中各个区域的内存占比分别是,Eden : S0 : S1 = 8 : 1 : 1

新创建的对象优先在  Eden 区进行分配。当 Eden 区满时,会触发一次 Minor GC(新生代垃圾回收,也叫 Young GC),将仍然存活的对象从 Eden 区和 S0 区移动到 S1 区,下次 Minor GC 处理情况类似,把存活的对象从 Eden 区和 S1 区移动到 S0 区。当 Survivor 区也满了,还存活的对象会被移动到老年代。如果老年代也满了,将会触发 Major GC(老年代垃圾回收,也叫 Old GC)。当老年代满了,也可能触发 Full GC,Full GC 会对整个堆内存进行垃圾回收,包含新生代、老年代和方法区。Full GC 会导致较长的停顿时间,并且会消耗大量的系统资源。

5. 方法区

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

方法区只是 JVM 规范中定义的一个概念,针对 Hotspot 虚拟机,JDK8 之前使用永久代(Permanent Generation,简称 PermGen)实现,JDK8 使用元空间(Metaspace)实现。

JDK8 之前可以通过 -XX:PermSize-XX:MaxPermSize 来设置永久代大小,JDK8 之后,使用元空间替换了永久代,改为通过 -XX:MetaspaceSize-XX:MaxMetaspaceSize 来设置元空间大小。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区中的一部分,用于存储编译期间生成的各种字面量和符号引用。在Java程序运行时,JVM将编译期生成的class文件中的常量池内容读取到运行时常量池中。

运行时常量池存储了类和接口中的常量,包括字符串字面量、被声明为final的常量值等。它还存储了类和接口中的符号引用,如类和接口、字段和方法的引用等。

在JVM中,运行时常量池是线程安全的。每个线程都有一个自己的线程栈,其中包含了局部变量表,而这些局部变量表中所引用的对象都位于堆中。当一个线程需要引用运行时常量池中的常量时,JVM会先将常量值从运行时常量池中复制到线程栈的局部变量表中,然后再进行引用。

需要注意的是,在JDK8中,运行时常量池已经被移动到元空间(Metaspace)中。元空间是在本地内存中分配的,与JVM的堆内存是分离的,因此不会受到Java堆大小的限制。

 

点赞收藏
一灯架构

只分享有趣的技术干货

请先登录,查看3条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步
3
3