2024-10-26
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/1027320738

回答

JVM 的运行时内存区域是 Java 虚拟机在运行 Java 程序时用来存储各种数据的地方。它主要分为如下几个部分:

  1. 程序计数器(Program Counter Register):程序计数器是一块很小的内存区域,它可以看作是当前线程所执行的字节码的行号指示器。
  2. Java 虚拟机栈:每个线程都有一个独立的 Java 虚拟机栈,用于存储局部变量、操作数栈、动态链接、方法出口等信息。每当一个方法被调用时,都会创建一个新的栈帧(Stack Frame),用于存储方法的局部变量和操作数栈等信息。当方法调用结束时,该栈帧就会销毁。
  3. 本地方法栈:本地方法栈与 Java 虚拟机栈类似,只不过本地方法栈是为 JVM 使用的 Native 方法服务的。
  4. :堆是 JVM 管理的最大一块内存区域,用于存放对象实例和数组。
  5. 方法区:方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区在很多 JVM 实现中也被称为“永久代”(Permanent Generation),但在 JDK 8 之后,永久代被移除,取而代之的是“元空间”(Metaspace)。

扩展

程序计数器

程序计数器是一块很小的内存区域,它的主要作用是记录当前线程所执行的字节码指令地址。通过程序计数器,JVM能够知道下一条要执行的指令是什么,从而保证程序的正常执行流程。

每个线程都有一个独立的程序计数器,这是因为在多线程的环境下,JVM 在任意一个时刻只会执行一个线程的指令。程序计数器为每个线程维护了独立的执行路径,线程之间不会互相干扰。同时,程序计数器的生命周期与线程的生命周期相同,它随着线程的创建而创建,随着线程的结束而销毁。

注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域。

Java 虚拟机栈

与程序计数器一样,Java 虚拟机栈也是线程私有,其生命周期也和线程的生命周期一样。

Java 虚拟机栈由栈帧(Stack Frame)组成,每次方法的调用,JVM 会向 Java 虚拟机栈中压入一个新建的栈帧,方法结束时,栈帧会被弹出。

每个栈帧包含如下几个部分:

局部变量表

局部变量表用于存放方法参数和局部变量。局部变量表中的变量以索引的方式访问,索引值从0开始,其类型可以是各种基本数据类型(booleanbytecharshortintfloatlongdouble)、对象引用(reference)。

操作数栈

一个用于操作数据的先入后出的栈,我们可以把它认为是方法调用的中转站,用于存放方法执行过程中产生的中间计算结果。大多数字节码指令都要从操作数栈中弹出数据,进行计算,再将结果压回操作数栈。

动态链接

在 Java 中,方法的调用包括静态解析和动态链接。其中,静态解析是在编译时完成的,主要用于静态方法和私有方法调用。而动态链接是在运行时完成的,主要用于实例方法的调用。

每个栈帧都包含一个指向运行时常量池中该方法所属类的引用,而动态链接负责将常量池中的符号引用(一种指向类、方法或字段的逻辑地址)转换为直接引用(实际的内存地址)。具体指令:

  • invokevirtual:用于调用对象的实例方法,根据对象的实际类型进行动态方法分派。
  • invokespecial:用于调用实例的私有方法、构造方法(<init>)以及通过 super 调用父类的方法。
  • invokeinterface:用于调用接口方法,会在运行时搜索实现了这个接口的对象的实际方法。
  • invokestatic:用于调用类的静态方法。

动态链接是 Java 实现多态性的基础。在 Java 中,它允许一个父类引用指向不同的子类对象,通过动态链接,可以在运行时决定调用哪个具体的子类方法。

方法返回地址

当方法执行结束后,需要返回到调用它的方法。方法返回地址用于存放调用该方法的字节码指令的地址。

在程序运行过程中,Java 虚拟机栈可能会抛出两种错误:

  • StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度,会抛出 StackOverflowError。常见于递归调用过深导致的栈空间不足。
  • OutOfMemoryError:如果虚拟机栈可以动态扩展,但内存不足以支持栈的扩展时,会抛出 OutOfMemoryError

本地方法栈

本地方法栈和 Java 虚拟机栈非常相似,他们的区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。它的主要作用是:

  • 支持本地方法的调用和执行:当 Java 程序调用本地方法时,本地方法栈记录方法调用的状态和执行过程中所需的数据。
  • 与操作系统交互:本地方法通常用于与底层操作系统进行交互,执行一些 Java 语言无法直接完成的任务。

方法区

https://juejin.cn/post/6898119654233866248

方法区是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

但是**《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,方法区到底要如何实现,那就是虚拟机自己要考虑的事情了。也就是说,在不同的虚拟机实现上,方法区的实现是不同的**。例如我们常说的永久代,它其实是 HotSpot 对方法区的一种具体实现方式。也就是说,永久代是 HotSpot 的一个实现,而方法区则是 Java 虚拟机规范中的一个定义,一种规范,他们之间的关系类似于实现类和接口。

《深入理解Java虚拟机》中对方法区存储内容描述如下:它用于存储已被虚拟机加载的类信息、方法信息、域信息、常量、静态变量、即时编译器编译后的代码缓存等等。

方法区存储内容

类信息

每当一个类被加载到 jvm 中时,相关的类元数据就会存储在方法区,这些元数据包含了 JVM 执行类所需的所有重要信息。包括:

  • 类的完全限定名:包括包名和类名。例如,com.skjava.MyClass 是一个类的完全限定名。
  • 类的修饰符:如 public, private, abstract, final 等,定义了类的可访问性和特性。
  • 父类信息:存储该类的父类的引用。所有 Java 类最终都继承自 java.lang.Object
  • 接口信息:该类实现的所有接口的列表,JVM 使用这些信息来确保类在运行时的行为与其声明一致。
  • 类加载器引用:每个类在 JVM 中都有一个关联的类加载器引用,用于管理该类的加载过程。

方法信息

方法区还需要存储每个类中所有方法的详细信息,这些信息包括:

  • 方法签名:包括方法名、返回类型和参数类型。例如,public int add(int a, int b) 的签名为 add(int, int): int
  • 方法修饰符:如 public, private, static, final 等,定义了方法的可访问性和特性。
  • 方法字节码:方法的具体实现以字节码的形式存储在方法区中。JVM 通过解释这些字节码来执行方法。
  • 方法的异常表:存储了与方法相关的异常处理信息,用于异常抛出和捕获。包括每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引。

域信息

域信息描述了类中定义的所有字段。每个字段都有其名称、类型、修饰符等信息,JVM 需要这些信息来管理类的属性和对象的状态。具体内容包括:

  • 字段名称:如 private int age; 中的 age
  • 字段类型:如 int, String, double 等,定义字段的数据类型。
  • 字段修饰符:如 private, public, static, final 等,决定了字段的可访问性和特性。

常量

常量在类加载时解析并存储在运行时常量池中,具体内容包括:

  • 字面量:编译时确定的常量,如字符串、整数、浮点数等。例如,"Hello, World!"123
  • 符号引用:对类、方法、字段的符号化表示,需要在运行时解析成具体的内存地址。比如方法调用中的符号引用。

静态变量

我们知道静态变量是与类关联的变量,而不是与类的实例关联。所以,所有静态变量都存储在方法区中,这保证了它们在 JVM 中只有一份副本,且在所有实例间共享。

  • 静态字段名称:如 private static int counter; 中的 counter
  • 静态字段类型:如 int, String,定义了静态变量的数据类型。
  • 静态字段的初始值:在类加载时,静态变量通常会被初始化,初始值也存储在方法区中

方法区的变更历史

在 Java 中,方法区经历了多次变更

JDK版本 方法区实现 变化
JDK 1.6 永久代 字符串常量池、运行时常量池、静态变量都是在永久代中
JDK 1.7 永久代 字符串常量池和静态变量被移动到了堆当中,运行时常量池还是在永久代中
JDK 1.8 元空间 字符串常量池和静态变量仍然在堆当中;运行时常量池、类型信息、常量、字段、方法被移动都了元空间中

至于要换的原因,详情:JVM为什么使用元空间替换了永久代

在 Java 中,“几乎”所有的 Java 对象都是在堆中分配的,但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,所有 Java 对象都在堆上分配就不是这么绝对了,详情大明哥后面详细介绍。

Java 堆主要用于存放程序运行时创建的对象和数据,是 JVM 内存中最大的一块,通常会被划分为几个区域。

新生代

主要用于存放新建的对象,它通常较小,经过多次 GC 操作后,如果对象仍然存活,他们将会被移动到老年代。新生代被划分为三个:

  • Eden区

新建的对象首先被分配到 Eden区,当Eden区的内存被填满时,JVM会触发一次年轻代的垃圾回收。

  • Survivor区

新生代有两个 Survivor区,S0 和 S1,也叫From Survivor(From幸存区)和To Survivor(To幸存区)。S0 和 S1 是交替使用的,即 Eden 区的垃圾回收过程中,存活的对象会被移动到其中一个Survivor区,另一个Survivor区则会在下一次垃圾回收时使用。

老年代

老年代用于存放那些在新生代经过多次垃圾回收后仍然存活的对象,它主要用于存放生命周期较长的对象,且内存大小比新生代的内存大。

在大部分情况下,对象首先是在新生代的 Eden 区分配,在经历一次新生代垃圾回收后,如果对象存活,则会进入 S0 或者 S1 中,且对象的年龄会增加 1,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。为什么是 15 可以阅读这篇文章:GC年龄为什么默认为15,可以超过15吗?

在默认情况下,新生代与老年代占比为 1:2,Eden 区与 survivor 区的占比为 8:1:1。

关于堆,大明哥在后面文章做更加详细的介绍!

阅读全文