3、探索JVM内存区域的神秘用途

本文探讨了JVM内存区域的划分,包括方法区(在JDK 1.8后称为Metaspace)、程序计数器、Java虚拟机栈和Java堆等。方法区存储类信息,程序计数器记录执行指令位置,虚拟机栈存放方法局部变量,堆内存存储对象实例。理解这些内存区域对于Java开发者和面试准备至关重要。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在深入了解了JVM的类加载机制之后,我们来探讨一下JVM的内存区域划分。这一主题是互联网公司面试中常会涉及到的知识点,因此对求职者来说,掌握其细节显得尤为重要。

3.1、你真的了解内存是如何被划分的吗?

其实这个问题非常简单,JVM在运行我们写好的代码时,他是必须使用多块内存空间的,不同的内存空间用来放不同的数据,然后配合我们写的代码流程,才能让我们的系统运行起来。
举个最简单的例子,比如咱们现在知道了JVM会加载类到内存里来供后续运行,那么我问问大家,这些类加载到内存以后,放到哪儿去了呢?想过这个问题吗?
所以JVM里就必须有一块内存区域,用来存放我们写的那些类。

在JVM的运行过程中,内存管理是至关重要的一部分。实际上,这个过程并不复杂。当我们编写的代码被执行时,JVM需要使用多个不同的内存区域,每个区域都用于存储特定类型的数据。这些内存区域与我们的代码流程紧密配合,共同确保系统的正常运行。

让我们通过一个简单的例子来理解这个概念。我们知道,JVM会将类加载到内存中以供后续运行。那么,这些被加载的类在内存中的何处存放呢?这是一个值得我们思考的问题。

为了解决这个问题,JVM内部必须有一个专门的内存区域,用于存储我们编写的类。我们来看下面的图:
在这里插入图片描述

当我们的代码开始运行时,我们是否要执行我们编写的每一个方法? 在运行这些方法时,方法内部包含的众多变量等元素,是否需要被存储在某个内存区域中? 进一步地,如果我们的代码中创建了一些对象,那么这些对象是否也需需要分配相应的内存空间来储存呢?同样的,大家看下图:

在这里插入图片描述

这就是为什么在JVM中,我们必须划分不同的内存区域,这是为了让我们编写的代码在运行过程中能够根据需要使用。接下来,我们将逐一探讨JVM中的不同内存区域。

3.2、方法区

在JDK 1.8之前,JVM中有一个特定的区域,被称为方法区。这个区域的主要作用是存储从".class"文件中加载的类信息,同时也会存放一些类似于常量池的数据。

然而,在JDK 1.8及以后的版本中,这个区域的名称发生了变化,被重新命名为"Metaspace",即元数据空间。虽然名字改变了,但是其主要功能并未发生变化,仍然是用于存储我们自己编写的各种类相关的信息。

例如,如果我们有两个类,一个是"User.class",另一个是"UserManager.class",那么这两个类的相关信息就会存储在Metaspace中,就像下面的代码所展示的那样。

public class User {
    public static void main() {
        UserManager UserManager = new UserManager();
    }
}

在这个例子中,"User.class"和"UserManager.class"的类信息就会被加载到Metaspace中。这两个类加载到JVM后,就会放在这个方法区中,大家看下图:
在这里插入图片描述

3.3、程序计数器

继续假设我们的代码是如下所示:

public class User {
    public static void main() {
        UserManager UserManager = new UserManager();
        UserManager.loadUserInfoFromDB();
    }
}

之前我们讨论过,实际上,Java源代码首先存在于以“.java”为后缀的文件中,这种文件就是我们所说的Java源代码文件。

然而,这些文件是面向我们程序员的,计算机本身无法理解你写的代码。因此,我们需要通过编译器,将“.java”为后缀的源代码文件编译成以“.class”为后缀的字节码文件。

这个“.class”为后缀的字节码文件,其中存储的就是经过编译的你的代码所生成的字节码。字节码是一种可以被计算机理解的语言,而不是我们最初编写的源代码。

字节码的样貌大致如下,它与上述的代码无关,只是一个示例,旨在让大家对字节码有一个直观的感受。

public java.lang.String getName();
     descriptor: ()Ljava/lang/String;
     flags: ACC_PUBLIC
     Code:
         stack=1, locals=1, args_size=1
             0: aload_0
             1: get_field    #2
             4: areturn

这段字节码的主要目的是让大家了解,当我们的Java源代码文件(以“.java”为扩展名)被编译后,会生成对应的字节码文件(以“.class”为扩展名),这些字节码文件的内容大致是什么样子的。

例如,“0: aload_0”这样的字符串,就是一条字节码指令。这些字节码指令对应着底层的机器指令,计算机通过读取这些机器码指令,才能知道具体应该执行什么操作。这些指令可能包括从内存中读取数据,或者将数据写入内存等,各种不同的指令会指导计算机执行各种不同的任务。

因此,我们首先需要明白的是:我们编写的Java代码会被编译成字节码,这些字节码对应着各种字节码指令。

接下来,当我们的Java代码在JVM上运行时,第一步就是将Java代码编译成字节码指令,然后这些字节码指令会被逐条执行,从而实现我们编写的代码的预期效果。

所以,当JVM将类信息加载到内存后,实际上会使用其内置的字节码执行引擎,去执行我们编写的代码编译出来的字节码指令。这个过程可以如下图所示。
在这里插入图片描述

在执行字节码指令的过程中,JVM需要一个特殊的内存区域,这就是“程序计数器”。

程序计数器的主要功能是记录当前正在执行的字节码指令的位置,换句话说,它负责跟踪目前执行到哪一条字节码指令。为了更直观地解释这个概念,我们可以借助一张图来说明。
在这里插入图片描述

JVM支持多线程并发执行。这意味着当你编写的代码在JVM上运行时,可能会启动多个线程来同时执行不同的代码块。

为了实现这种并发性,每个线程都会拥有一个独立的程序计数器。这个计数器的主要功能是跟踪当前线程正在执行的指令。具体来说,它会记录下一条要执行的字节码指令的位置,确保线程能够准确地按照预定的顺序执行相应的操作。

下面提供的图示更直观地展示了线程、程序计数器以及它们之间的关联关系。通过观察这张图,你可以更清楚地理解多线程环境下的线程管理和指令执行过程。
在这里插入图片描述

3.4、Java虚拟机栈

在Java代码执行过程中,无论是哪种方法中的代码,都是由线程来负责执行的。即便是简单的代码,也会由一个主线程(main线程)来执行main()方法中的代码。当主线程执行main()方法的代码指令时,它会通过对应的程序计数器(Program Counter)来记录当前正在执行的指令位置。

public class User {
    public static void main() {
        UserManager UserManager = new UserManager();
        UserManager.loadUserInfosFromDB();
    }
}

在方法中,我们经常会在方法内部定义一些局部变量。例如,在上述的main()方法中,就有一个名为UserManager的局部变量,它引用了一个UserManager实例对象。不过,我们先不关注这个对象的细节,而是来了解一下方法和局部变量的概念。

因此,JVM必须有一个特定的区域来存储每个方法内的局部变量等数据,这个区域就是Java虚拟机栈。每个线程都有自己的Java虚拟机栈,比如这里的main线程就会有自己的一个Java虚拟机栈,用来存放自己执行的方法的局部变量。

当线程执行一个方法时,会为这个方法创建一个对应的栈帧。栈帧包含了该方法的局部变量表、操作数栈、动态链接和方法出口等信息。在这里,我们可以暂时不用全部理解这些概念,先关注局部变量。

main线程为例,当它执行main()方法时,就会为这个main()方法创建一个栈帧,并将该栈帧压入main线程的Java虚拟机栈中。同时,在main()方法的栈帧中,会存放相应的UserManager局部变量。上述过程,如下图所示:
在这里插入图片描述

然后假设main线程继续执行UserManager对象里的方法,比如下面这样,就在“loadUserInfosFromDB”方法里定义了一个局部变量:“hasFinishedLoad”

public class UserManager {
    public void loadUserInfosFromDB() {
        boolean hasFinishedLoad = false;
    }
}

那么main线程在执行上面的“loadUserInfosFromDB”方法时,就会为“loadUserInfosFromDB”方法创建一个栈帧压入线程自己的Java虚拟机栈里面去。
然后在栈帧的局部变量表里就会有“hasFinishedLoad”这个局部变量。整个过程如下图所示:
在这里插入图片描述

接着如果“loadUserInfosFromDB”方法调用了另外一个“isLocalDataCorrupt()”方法 ,这个方法里也有自己的局部变量,比如下面这样的代码:

public class UserManager {
    public void loadUserInfosFromDB() {
        boolean hasFinishedLoad = false;
        if (isLocalDataCorrupt()) {
            // 
        }
    }
    public boolean isLocalDataCorrupt() {
        boolean isCorrupt = false;
        return isCorrupt;
    }
}

那么这个时候会给“isLocalDataCorrupt”方法又创建一个栈帧,压入线程的Java虚拟机栈里。
而且“isLocalDataCorrupt”方法的栈帧的局部变量表里会有一个“isCorrupt”变量,这是“isLocalDataCorrupt”方法的局部变量。整个过程,如下图所示:
在这里插入图片描述

在JVM中,当一个方法执行完毕后,与之对应的栈帧会从Java虚拟机栈中被弹出。以“isLocalDataCorrupt”方法为例,一旦该方法执行完毕,其对应的栈帧便会从Java虚拟机栈中出栈。同样的,如果“loadUserInfosFromDB”方法也执行完毕,那么它对应的栈帧也会从Java虚拟机栈中出栈。

这就是JVM中“Java虚拟机栈”这一组件的主要作用:在调用并执行任何方法时,都会为该方法创建一个新的栈帧并将其压入栈中。在这个栈帧中,存放了该方法的局部变量以及其他与该方法执行相关的信息。一旦方法执行完毕,对应的栈帧便会从栈中弹出。

为了更好地理解这一过程,我们可以借助一张图来展示。在这张图中,我们可以看到,每个线程在执行代码时,除了程序计数器之外,还会配备一个Java虚拟机栈内存区域,用于存放每个方法中的局部变量表。
在这里插入图片描述

3.5、Java堆内存

在Java中,当main线程执行main()方法时,它会有自己的程序计数器,这是用来记录下一条需要执行的指令。

同时,Java虚拟机会按照代码的执行顺序,将main()方法,loadUserInfosFromDB()方法,isLocalDataCorrupt()方法的栈帧依次压入Java虚拟机栈。这些栈帧用于存放每个方法的局部变量,以及相关的运行信息。

然而,除了上述的结构,Java虚拟机还有一个非常关键的区域,那就是Java堆内存。Java堆内存是用于存放我们在代码中创建的各种对象的存储区域。这些对象在被创建后,会被分配在Java堆内存中,直到它们不再被使用,才会被垃圾回收器回收。比如下面的代码:

public class User {
    public static void main() {
        UserManager UserManager = new UserManager();
        UserManager.loadUserInfosFromDB();
    }
}

通过执行"new UserManager()"这行代码,我们创建了UserManager类的一个对象实例。这个对象实例将包含一些数据,如下面的代码所示。

在UserManager类中,"UserInfoCount"是该对象实例的一个属性。类似UserManager这样的对象实例,会被存储在Java堆内存中。

public class UserManager {
    private long UserInfoCount;
    public void loadUserInfosFromDB() {
        boolean hasFinishedLoad = false;
        if (isLocalDataCorrupt()) {
            // 
        }
    }
    public boolean isLocalDataCorrupt() {
        boolean isCorrupt = false;
        return isCorrupt;
    }
}

在Java的堆内存区域,会存储诸如UserManager这样的对象。当我们在main方法中创建了一个UserManager对象时,当线程执行到main方法的代码,会在与main方法对应的栈帧的局部变量表中,创建一个引用类型的局部变量“UserManager”,用于存储UserManager对象的地址。

换句话说,你可以理解为局部变量表中的“UserManager”变量,是指向了Java堆内存中的UserManager对象的。还是给大家来一张图,更加清晰一些:
在这里插入图片描述

3.6、核心内存区域全流程解码

其实我们把上面的那个图和下面的这个总的大图一起串起来看看,还有配合整体的代码,我们来捋一下整体的流程,大家就会觉得很清晰。
在这里插入图片描述

public class User {
    public static void main() {
        UserManager UserManager = new UserManager();
        UserManager.loadUserInfosFromDB();
    }
}

public class UserManager {
    private long UserInfoCount;
    public void loadUserInfosFromDB() {
        boolean hasFinishedLoad = false;
        if (isLocalDataCorrupt()) {
            // 
        }
    }
    public boolean isLocalDataCorrupt() {
        boolean isCorrupt = false;
        return isCorrupt;
    }
}

首先,当你的JVM进程启动时,它会先加载你的User类到内存中。随后,一个名为main的线程开始执行User中的main()方法。这个main线程与一个程序计数器关联,该计数器会记录线程当前正在执行的指令行数。

接下来,当main线程执行main()方法时,它会在与该线程关联的Java虚拟机栈中压入一个main()方法的栈帧。然后,它会发现需要创建一个UserManager类的实例对象,此时会将UserManager类加载到内存中。

接着,会在Java堆内存中分配一个UserManager的对象实例,并在main()方法的栈帧的局部变量表中引入一个名为“UserManager”的变量,使其引用UserManager对象在Java堆内存中的地址。

然后,main线程开始执行UserManager对象中的方法,会依次将自己执行到的方法对应的栈帧压入自己的Java虚拟机栈。一旦方法执行完毕,就会将方法对应的栈帧从Java虚拟机栈中出栈。

通过理解这个过程,你就能彻底理解JVM中的各个核心内存区域的功能以及它们与我们的Java代码之间的关系。

3.7、其他内存区域

在JDK的底层API中,例如与IO、NIO和网络Socket相关的部分,当我们深入研究其内部源码时,会发现许多地方并非纯粹的Java代码。实际上,它们通过native方法调用了本地操作系统的一些功能,这些功能可能是用C语言编写的,或者是一些底层类库。

以下面这段代码为例:

public native int hashCode();

在调用这种native方法时,会涉及到线程对应的本地方法栈。这个栈与Java虚拟机栈类似,用于存储各种native方法的局部变量表等信息。

此外,还有一个不属于JVM的区域。通过NIO中的allocateDirect这类API,可以在Java堆外分配内存空间。然后,可以通过Java虚拟机中的DirectByteBuffer来引用和操作这些堆外内存空间。

实际上,许多技术都会采用这种方式,因为在一些场景下,堆外内存分配可以提升性能。

3.8、本文小结

本文已经详细介绍了JVM中的核心内存区域功能,重点包括方法区、程序计数器、Java虚拟机栈和Java堆等。这些内存区域的作用对于我们理解JVM的运行机制至关重要。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无法无天过路客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值