02 | 你不得不掌握的 JVM 内存管理

本文详细介绍了JVM内存的管理,包括虚拟机栈、程序计数器、堆和元空间等区域。堆是最大的内存区域,用于存储对象实例;元空间取代了Java8之前的永久代,避免了内存溢出问题,但仍需通过参数控制其大小。程序计数器记录线程执行进度,而本地方法栈服务于native方法。理解这些概念对于优化Java应用的性能至关重要。
摘要由CSDN通过智能技术生成


你不得不掌握的 JVM 内存管理

面试题:

  • JVM 是如何进行内存区域划分的?
  • JVM 如何高效进行内存管理?
  • 为什么需要有元空间,它又涉及什么问题?

JVM 内存布局

随着 Java 的发展,内存布局一直在调整之中。比如,Java 8 及之后的版本,彻底移除了持久代,而使用 Metaspace 来进行替代。 这也表示着 -XX:PermSize 和 -XX:MaxPermSize 等参数调优,已经没有了意义。但大体上,比较重要的内存区域是固定的。

在这里插入图片描述

JVM 内存区域划分如图所示,从图中我们可以看出:

  • JVM 堆中的数据是共享的,是占用内存最大的一块区域。
  • 可以执行字节码的模块叫作执行引擎。
  • 执行引擎在线程切换时怎么恢复?依靠的就是程序计数器。
  • JVM 的内存划分与多线程是息息相关的。像我们程序中运行时用到的栈、本地方法栈和程序计数器,它们的维度都是线程。
  • 本地内存包含元数据区和一些直接内存。

虚拟机栈

Java 虚拟机栈是基于线程的。 哪怕你只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。 栈里的每条数据,就是栈帧。在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。每个栈帧,都包含四个区域:

  • 局部变量表
  • 操作数栈
  • 动态连接
  • 返回地址

在这里插入图片描述
在这里插入图片描述

  • 这里有一个两层的栈。第一层是栈帧,对应着方法;第二层是方法的执行,对应着操作数栈。
  • 所有的字节码指令,其实都会抽象成对栈的入栈出栈操作。执行引擎只需要傻瓜式的按顺序执行,就可以保证它的正确性。

本地方法栈是和虚拟机栈非常相似的一个区域,它服务的对象是 native 方法。

程序计数器

设想一下,如果我们的程序在线程之间进行切换,凭什么能够知道这个线程已经执行到什么地方呢? 既然是线程,就代表它在获取 CPU 时间片上,是不可预知的,需要有一个地方,对线程正在运行的点位进行缓冲记录,以便在获取 CPU 时间片时能够快速恢复。 就好比你停下手中的工作,倒了杯茶,然后如何继续之前的工作?

程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。这里面存的,就是当前线程执行的进度。

在这里插入图片描述
程序计数器也是因为线程而产生的,与虚拟机栈配合完成计算操作。程序计数器还存储了当前正在运行的流程,包括正在执行的指令、跳转、分支、循环、异常处理等。使用 javap 命令输出的字节码。可以看到在每个 opcode 前面,都有一个序号。就是图中红框中的偏移地址,你可以认为它们是程序计数器的内容。

在这里插入图片描述

在这里插入图片描述

堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。 随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)。由于对象的大小不一,在长时间运行后,堆空间会被许多细小的碎片占满,造成空间浪费。所以,仅仅销毁对象是不够的,还需要堆空间整理。

那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在 Java 类中存在的位置。 Java 的对象可以分为基本数据类型和普通对象。 对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。 比如,把这个引用保存在虚拟机栈的局部变量表中。 对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。 当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。 注意,像 int[] 数组这样的内容,是在堆上分配的。数组并不是基本数据类型。

元空间

为什么有 Metaspace 区域?它有什么问题?.class,是放在 JVM 的哪个区域的?

对象是一个活生生的个体,可以参与到程序的运行中;类更像是一个模版,定义了一系列属性和操作。

在 Java 8 之前,这些类的信息是放在一个叫 Perm 区的内存里面的。更早版本,甚至 String.intern 相关的运行时常量池也放在这里。Perm区有大小限制,很容易造成 JVM 内存溢出,从而造成 JVM 崩溃。 Perm 区在 Java 8 中已经被彻底废除,取而代之的是 Metaspace。 原来的 Perm 区是在堆上的,现在的元空间是在非堆上的。

在这里插入图片描述

元空间的好处也是它的坏处。使用非堆可以使用操作系统的内存,JVM 不会再出现方法区的内存溢出;但是,无限制的使用会造成操作系统的死亡。所以,一般也会使用参数 -XX:MaxMetaspaceSize 来控制大小。

问题补充

我们常说的字符串常量,存放在哪呢?

由于常量池,在 Java 7 之后,放到了堆中,我们创建的字符串,将会在堆上分配。

堆、非堆、本地内存,有什么关系?

在这里插入图片描述

JVM 在运行时,会从操作系统申请大块的堆内内存,进行数据的存储。但是,堆外内存也就是申请后操作系统剩余的内存,也会有部分受到 JVM 的控制。比较典型的就是一些 native 关键词修饰的方法,以及对内存的申请和处理。

在 Linux 机器上,使用 top 或者 ps 命令,在大多数情况下,能够看到 RSS 段(实际的内存占用),是大于给 JVM 分配的堆内存的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

久违の欢喜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值