JVM内存结构介绍

文章目录

  • JVM内存结构
    • 本地方法栈
    • PC寄存器
    • 方法区
    • 直接内存
    • 总结
    • 相关面试题

JVM内存结构

JVM内存结构介绍_第1张图片

JVM内存结构介绍_第2张图片

PS:下文说到的栈、堆都是指 JVM 中的,并不是传统意义上的栈、堆数据结构

  • 本地内存:是指JVM在运行时分配和管理的内存,它是为了支持Java程序和Java虚拟机本身的运行而存在的。在JVM中,本地内存用于存放JVM自身的数据结构、线程栈、方法区、堆外内存(Off-Heap Memory)等。

    本地内存是直接由操作系统分配和管理的,不受Java堆内存大小的限制,并且不会受到Java垃圾回收机制的控制。

  • 运行时内存:运行时内存是指在程序运行时分配的内存空间,用于存储程序的数据和执行过程中所需的临时数据。

  • 什么是堆

    (Heap)用于存储对象的实例,包括各种类型的对象。堆是JVM内存管理中最大的一块区域,被所有线程共享。在堆中,又可进一步细分为新生代老年代,用于管理不同生命周期的对象。

  • 什么是新生代和老年代

    • 新生代(Young Generation):新创建的对象被分配在新生代中。新生代又可以细分为伊甸区(Eden Space)和两个幸存者区(Suvivor Space)。大部分对象在新生代被创建和销毁,只有经过多次垃圾回收的对象才会被晋升到老年代。
      • 伊甸区(Eden Space):伊甸区是对象的初始分配区域,新创建的对象首先在伊甸区分配内存。当伊甸区的内存空间不足时,会触发 Minor GC(新生代垃圾回收),回收伊甸区中的垃圾对象,同时将存活的对象移动到幸存者区。通常情况下,大部分对象在新生代分配的内存中很快就会变为垃圾对象,所以伊甸区的回收频率相对较高。
      • 幸存者区(Survivor Space):幸存者区是用来保存伊甸区中经过一次垃圾回收后存活的对象。幸存者区分为两个部分:From 区To 区。在任何时候,一个区用于存活对象,另一个区为空。当进行 Minor GC 时,存活的对象会从伊甸区移动到其中一个幸存者区,而不是直接回收。经过多次垃圾回收后,仍然存活的对象会被移到老年代(Old Generation)区域。
    • 老年代(Old Generation):经过多次垃圾回收后仍然存活的对象会被晋升到老年代。老年代主要存储长时间存活的对象。
  • 堆内存的大小设置方式:可以使用 -Xmx-Xms 命令行参数来设置堆内存的最大和初始大小。例如,-Xmx1024 表示设置最大堆内存为 1024MB。你可以根据自己的需求来指定合适的参数值,如果堆内存剩余的内存不足以满足于对象创建,JVM会抛出OutOfMemory异常。

    默认情况堆的大小是动态变化的,但是可以通过设置 -XX:-UseAdaptiveSizePolicy JVM 参数来实现堆大小的固定,但是堆大小固定是会存在很多问题的,比如:如果设置的堆大小过小,将可能导致频繁的垃圾回收和内存容量不足;如果设置的堆大小过大,将会占用更多的系统资源,导致内存资源浪费。所以一般直接采用默认配置即可

  • 栈存储什么类型的数据

    堆一般存储引用类型的数据(也就是对象,而不是对象的引用),常见的引用类型数据有:类的对象、基本类型的包装类、数组、字符串……

  • 如何理解对象和对象的引用

    对象(Object)是存储在堆内存中的一块数据区域,而对象的引用是指向这块堆内存的地址。

    个人理解:Java中并没有指针这个概念,但是这个对象的引用本质就相当于是一个指针,可以理解为对象是存储数据的具体内存空间,而对象的引用是指向这个数据的地址,比如: A a = new A(); 其中这个变量 a 就是对象的引用,而 new A() 是对象,a 存储的是 new A() 这个对象的地址。

    备注:对象是存储在堆(Heap)中的,而对象的引用是存储在栈(Stack)中

  • 什么是字符串常量池

    字符串常量池(String Constant Pool):字符串常量池是字符串对象的存储区域,用于存储字符串字面量常量(由双引号引起来的字符串)。为了提高性能和节省内存,Java中的字符串常量会被放入一个专门的字符串常量池。当程序中出现相同的字符串字面量时,JVM会重复使用已存在于字符串常量池中的字符串对象,而不会重复创建新的字符串对象。

  • 什么是运行时常量池

    运行时常量池(Runtime Constant Pool):运行时常量池是每个类或接口的常量池表项的运行时表示形式。它包含了编译时期生成的各种字面量常量(如字符串、数字等)、符号引用(如类和方法的全限定名)等。运行时常量池是每个类的一部分,用于存储类的常量信息,并且在类加载时被创建并保存在方法区。

  • 字符串常量池和运行时常量池的关系

    • 在早期的Java版本中,字符串常量池和运行时常量池彼此独立,没有直接关联。字符串常量池用于存储字符串常量,运行时常量池用于存储其他类型的常量和符号引用。

      字符串常量池(String Constant Pool)位于方法区(Method Area)中,其中包含了所有的字符串常量。字符串常量池是一个特殊的数据结构,用于存储字符串常量的引用,并且这些字符串常量在编译时就已经确定。

      而运行时常量池(Runtime Constant Pool)则也位于方法区,存储了每个类或接口的常量池表的副本,包括各种常量(如字符串、整数、浮点数等)和符号引用(类、方法、字段等)。

    • 在较新的Java版本(Java 7及之后)中,运行时常量池(Runtime Constant Pool)确实包含了字符串常量池(String Constant Pool),它们都位于堆内存中。

      在这些版本中,运行时常量池被作为方法区的一部分,存放着每个类或接口的常量池表的副本。这个常量池表包含了各种常量(例如字符串、整数、浮点数、符号引用等),其中就包含了字符串常量。

      字符串常量池是运行时常量池的子集,用于存储由字符串字面量或String对象的intern()方法产生的字符串常量。当字符串常量被创建或调用intern()方法时,它们会被添加到运行时常量池中的字符串常量池部分。

  • 什么是栈

    (Stack)也可以称作虚拟机栈(VM Stack),每个线程在JVM中都会有一个对应的栈,用于存储方法调用的栈帧,栈的生命周期和线程相同,随着线程的创建二创建,随着线程的消亡而消亡。每个栈帧包含局部变量、方法参数、操作数栈、动态链接等信息。栈的大小是固定的,通过-Xss参数可以调整。

  • 什么是栈帧

    栈由一系列的元素组成,每个元素称为栈帧(Stack Frame),也被称为活动记录(Activation Record)或函数帧(Function Frame)。每当一个函数被调用时,一个新的栈帧就会被创建并被推入栈顶,当函数执行完毕后,该栈帧会被弹出。栈帧的生命周期和方法的调用是相同的,随着方法的调用而产生,随着方法调用完毕而消亡

  • 栈帧的组成

    • 局部变量表(Local Variables):用于存放方法中定义的局部变量和参数值。局部变量表的槽位以及槽位中存放的数据类型在编译时确定,主要存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)和对象引用。

      除了上述主要的组成部分,栈帧还可能包含一些附加信息,如异常处理信息、调试信息等

    • 操作数栈(Operand Stack):用于存放方法执行过程中的操作数和中间结果。操作数栈是一个后进先出(LIFO)的栈结构。

    • 动态链接信息(Dynamic Linking):包含了指向运行时常量池中该方法的引用以及其他动态链接所需的信息。

      动态链接的作用是在程序运行时提供对其他类、字段或方法的访问能力,而不需要事先知道它们的具体位置。这使得Java程序更加灵活和可扩展,允许在运行时动态加载和链接类以及调用其他类和方法,从而提供了更强大的动态性和可重用性。

      动态链接是指将Java字节码中的符号引用解析为实际的内存地址的过程,这样程序在运行时可以直接跳转至正确的内存位置,主要用于方法之间的调用。动态链接主要分为两个阶段

      • 解析(Resolution)阶段是指将符号引用转换为实际的内存地址。对于类的解析,JVM会通过类加载机制找到类的定义并进行解析。对于字段和方法的解析,JVM会在符号引用的类中找到对应的字段或方法,并确定其内存地址。
      • 绑定(Binding)阶段是将解析后的内存地址绑定到符号引用所在的指令或代码中。这使得程序在运行时可以直接使用内存地址调用对应的类、字段或方法。
    • 方法返回地址(Return Address):指向方法被调用后,执行完毕后需要返回的地址。

  • 栈内存大小的设置方式

    • 固定大小:栈的大小是固定的某一个值,类似于数组,栈中数据超过固定分配大小,则会报StackOverflow异常

    • 动态分配:栈的大小是动态变化的,类似于集合,栈中数据超过当前可用内存,则会报OutOfMemory异常

  • 栈存储什么类型的数据

    栈一般存储基本数据类型对象的引用

本地方法栈

  • 什么是本地方法栈

    本地方法栈(Native Method Stack)与 Java 栈类似,但是用于存储在应用程序中调用的本地方法栈帧

  • 本地方法栈的大小如何设置

    本地方法栈一般是固定的,但是可以通过-Xss设置线程的本地方法栈大小。 参数可以接受一个数字,后面可以跟着 kmg 来指定单位(分别表示千字节、兆字节和千兆字节)。例如,-Xss256k 表示设置本地方法栈大小为 256 KB。如果本地方法栈的数据超过当前最大分配空间,就会报StackOverFlow异常

  • 本地方法栈存储什么类型的数据

    方法参数局部变量(临时变量或者表达式计算结果)、返回值对象引用

PC寄存器

  • 什么是PC寄存器

    PC寄存器(Program Counter Register),也称程序计数器,用于存储当前线程正在执行的 JVM 指令的地址。

    在通用的计算机体系中,程序计数器用来记录当前正在执行的指令,在 JVM 中也是如此。程序计数器是线程私有,所以当一个新的线程创建时,程序计数器也会创建。由于Java是支持多线程,Java中的程序计数器用来记录当前线程中正在执行的指令。如果当前正在执行的方法是本地方法,那么此刻程序计数器的值为undefined。

    注意:这个区域是唯一一个不抛出OutOfMemoryError的运行时数据区。

  • PC寄存器存储什么类型的数据

    存储当前正在执行的 JVM 指令的地址

  • PC寄存器的作用

    • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理
    • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了

方法区

  • 什么是方法区

    方法区(Method Area)也称为永久代(PermGen),在Java8中,被元空间(Metaspace)替代,用于存储类的结构信息,如类的字段、方法、常量池、静态变量等。

    在 JDK 8及以前的版本中,方法区是一个逻辑上的概念,直接对应于堆内存中的一块内存区域。但在 JDK 8之后,方法区的实现被替换为元空间(Metaspace),它不再位于堆中,而是直接使用本地内存。

  • 元空间相较于永久代有如下优势

    • 省去了永久代常见的问题,如永久代溢出、类加载器泄漏等。

    • 元空间的大小可以根据应用程序的需求进行动态调整。

    • 元空间的内存分配和回收交由操作系统管理,减少了 JVM 对内存的管理开销。

  • 方法区的大小设置

    在 Java 8 及之前的版本中,Java 虚拟机的方法区大小是固定的,由虚拟机内部决定,无法通过命令行参数进行直接设置。在 Java 8 及之后的版本中,方法区已被元数据区(Metaspace)所取代。元数据区不再是固定大小的,而是使用本机内存来存储元数据,并且可以动态地根据应用程序的需求进行扩展,可以通过以下 JVM 参数来调整元数据区(方法区)的大小:

    • -XX:MetaspaceSize:用于设置初始元数据区的大小,默认值为 21MB。

    • -XX:MaxMetaspaceSize:用于设置元数据区的最大大小,默认值为不限制。

    一旦元数据区达到最大大小,JVM 将会触发垃圾回收,清理无用的元数据。

  • 方法区存储什么类型的数据

    主要用于存储类的元数据信息,如类的结构、方法和字段的描述符,以及常量池等。除了元数据和常量池之外,方法区还可以存储一些其他类型的数据:静态变量字符串常类型信息(方法区中存储了类的结构信息,包括方法的字节码、类的字段信息等)、符号引用(方法区还存储了符号引用,包括类、方法和字段的符号引用)

直接内存

  • 什么是直接内存

    直接内存(Direct Memory)直接内存是通过NIO(New Input/Output)库来直接操作系统的内存空间,绕过了Java堆内存。它主要用于提供高性能的I/O操作,例如文件读写和网络传输等。直接内存的申请和释放由操作系统管理。

  • 直接内存一般存储什么类型的数据

    直接内存主要用于存储IO缓冲区和大型数据结构,以及支持Java NIO中的零拷贝操作

    注意:直接内存虽然不受Java堆大小的限制,但它并不会被自动回收。因此,在使用直接内存时,需要谨慎管理和手动释放内存,以避免内存泄漏问题。

总结

  • 线程私有的:程序计数器、虚拟机栈、本地方法栈

  • 线程共享的:堆、元空间、直接内存

  • 可以人为操作的:堆、虚拟机栈、元空间

  • 操作系统操作的(不可人为操作):本地方法栈、程序计数器、直接内存

JVM内存结构介绍_第3张图片

相关面试题

  • 程序计数器为什么是私有的?

    程序计数器私有的主要目的是为了在线程切换后能够恢复到正确的执行位置。

    因为程序计数器的作用是记录当前线程执行的位置,从而能够在线程切换后恢复现场,如果是共享的,线程切换后将十分困难。

    备注:如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址

  • 虚拟机栈和本地方法栈为什么是私有的?

    虚拟机栈和本地方法栈私有的目的就算为了线程隔离,保障线程中的局部变量不能被其它线程访问,同时能够提高资源的利用率,一个线程结束对应的虚拟机栈和本地方法栈就会被立即回收。

    虚拟机栈存储栈帧,栈帧中是 Java 方法运行时的局部变量表、方法参数返回地址、常量池引用等数据,JVM 通过控制虚拟机栈的入栈和出栈实现方法之间的调用;本地方法栈和虚拟机栈十分类似,他记录的本地方法(也就是 Native 修饰的方法)的相关局部参数和局部变量数据,JVM 通过入栈和出栈实现本地方法之间的相互调用。

  • 堆和方法区的比较?

    堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

你可能感兴趣的:(#,Java,后端开发,#,面试总结,jvm,java)