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

Java 引觉得豪的就是它的自动内存管理机制。相比于 C++的手动内存管理、复杂难以理解的指针等,Java 程序写起来就方便的多。
然而这种呼之即来挥之即去的内存申请和释放方式,天然也有它的代价。为了管理这些快速的内存申请释放操做,就必须引入一个池子来延迟这些内存区域的回收操做。
咱们常说的内存回收,就是针对这个池子的操做。咱们把上面说的这个池子,叫做堆,能够暂时把它当作一个总体。java

JVM 内存布局

Java 程序的数据结构是很是丰富的。其中的内容,举一些例子:
静态成员变
动态成员变量
区域变量
短小紧凑的对象声明
庞大复杂的内存申请面试

咱们先看一下 JVM 的内存布局。随着 Java 的发展,内存布局一直在调整之中。好比,Java 8 及以后的版本,完全移除了持久代,而使用 Metaspace 来进行替代。这也表示着 -XX:PermSize 和 -XX:MaxPermSize 等参数调优,已经没有了意义。但大致上,比较重要的内存区域是固定的。
Cgq2xl4VrjWAPqAuAARqnz6cigo666.png
JVM 内存区域划分如图所示,从图中咱们能够看出:数组

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

虚拟机栈

Java 虚拟机栈是基于线程的。哪怕你只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程同样的。数据结构

栈里的每条数据,就是栈帧。在每一个 Java 方法被调用的时候,都会建立一个栈帧,并入栈。一旦完成相应的调用,则出栈。全部的栈帧都出栈后,线程也就结束了。每一个栈帧,都包含四个区域:多线程

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

咱们的应用程序,就是在不断操做这些内存空间中完成的。
Cgq2xl4VrjWABK2qAATDn4DQbvE629.png布局

本地方法栈是和虚拟机栈很是类似的一个区域,它服务的对象是 native 方法。你甚至能够认为虚拟机栈和本地方法栈是同一个区域,这并不影响咱们对 JVM 的了解。spa

这里有一个比较特殊的数据类型叫做 returnAdress。由于这种类型只存在于字节码层面,因此咱们日常打交道的比较少。对于 JVM 来讲,程序就是存储在方法区的字节码指令,而 returnAddress 类型的值就是指向特定指令内存地址的指针。
CgpOIF4VrjWAZvMCAAB9Uu8GKww546.png操作系统

  • 这里有一个两层的栈。第一层是栈帧,对应着方法;第二层是方法的执行,对应着操做数。注意千万不要搞混了。
  • 你能够看到,全部的字节码指令,其实都会抽象成对栈的入栈出栈操做。执行引擎只须要傻瓜式的按顺序执行,就能够保证它的正确性。

程序计数器

既然是线程,就表明它在获取 CPU 时间片上,是不可预知的,须要有一个地方,对线程正在运行的点位进行缓冲记录,以便在获取 CPU 时间片时可以快速恢复。线程

程序计数器是一块较小的内存空间,它的做用能够看做是当前线程所执行的字节码的行号指示器。这里面存的,就是当前线程执行的进度。下面这张图,可以加深你们对这个过程的理解。
Cgq2xl4VrjaANruFAAQKxZvgfSs652.png
能够看到,程序计数器也是由于线程而产生的,与虚拟机栈配合完成计算操做。程序计数器还存储了当前正在运行的流程,包括正在执行的指令、跳转、分支、循环、异常处理等。3d

咱们能够看一下程序计数器里面的具体内容。下面这张图,就是使用 javap 命令输出的字节码。你们能够看到在每一个 opcode 前面,都有一个序号。就是图中红框中的偏移地址,你能够认为它们是程序计数器的内容。
CgpOIF4VrjaAQSVlAAB8U3OQQR8670.jpg

堆 

Cgq2xl4VrjaAXnuQAANJIXDvNhI844.png
堆是 JVM 上最大的内存区域,咱们申请的几乎全部的对象,都是在这里存储的。咱们常说的垃圾回收,操做的对象就是堆。

堆空间通常是程序启动时,就申请了,可是并不必定会所有使用。

随着对象的频繁建立,堆空间占用的愈来愈多,就须要不按期的对再也不使用的对象进行回收。这个在 Java 中,就叫做 GC(Garbage Collection)。

因为对象的大小不一,在长时间运行后,堆空间会被许多细小的碎片占满,形成空间浪费。因此,仅仅销毁对象是不够的,还须要堆空间整理。这个过程很是的复杂。

那一个对象建立的时候,究竟是在堆上分配,仍是在栈上分配呢?这和两个方面有关:对象的类型和在 Java 类中存在的位置。

Java 的对象能够分为基本数据类型和普通对象。

对于普通对象来讲,JVM 会首先在堆上建立对象,而后在其余地方使用的实际上是它的引用。好比,把这个引用保存在虚拟机栈的局部变量表中。

对于基本数据类型来讲(byte、short、int、long、float、double、char),有两种状况。

咱们上面提到,每一个线程拥有一个虚拟机栈。当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其余状况,都是在堆上分配。

注意,像 int[] 数组这样的内容,是在堆上分配的。数组并非基本数据类型。
CgpOIF4VrjaAaILrAANJIXDvNhI630.png
这就是 JVM 的基本的内存分配策略。而堆是全部线程共享的,若是是多个线程访问,会涉及数据同步问题。

元空间

关于元空间,咱们仍是以一个很是高频的面试题开始:“为何有 Metaspace 区域?它有什么问题?”

说到这里,你应该回想一下类与对象的区别。对象是一个活生生的个体,能够参与到程序的运行中;类更像是一个模版,定义了一系列属性和操做。那么你能够设想一下。咱们前面生成的 A.class,是放在 JVM 的哪一个区域的?

想要问答这个问题,就不得不提下 Java 的历史。在 Java 8 以前,这些类的信息是放在一个叫 Perm 区的内存里面的。更早版本,甚至 String.intern 相关的运行时常量池也放在这里。这个区域有大小限制,很容易形成 JVM 内存溢出,从而形成 JVM 崩溃。

Perm 区在 Java 8 中已经被完全废除,取而代之的是 Metaspace。原来的 Perm 区是在堆上的,如今的元空间是在非堆上的,这是背景。关于它们的对比,能够看下这张图。
Cgq2xl4VrjaAIlgaAAJKReuKXII670.png
而后,元空间的好处也是它的坏处。使用非堆可使用操做系统的内存,JVM 不会再出现方法区的内存溢出;可是,无限制的使用会形成操做系统的死亡。因此,通常也会使用参数 -XX:MaxMetaspaceSize 来控制大小。

方法区,做为一个概念,依然存在。它的物理存储的容器,就是 Metaspace。如今,只须要了解到,这个区域存储的内容,包括:类的信息、常量池、方法数据、方法代码就能够了。

小结

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

因为常量池,在 Java 7 以后,放到了堆中,咱们建立的字符串,将会在堆上分配。

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

关于它们的关系,咱们能够看一张图。在个人感受里,堆是软绵绵的,松散而有弹性;而非堆是冰冷生硬的,内存很是紧凑。
CgpOIF4VrjaAOSx2AAJgrvself8711.png
你们都知道,JVM 在运行时,会从操做系统申请大块的堆内内存,进行数据的存储。可是,堆外内存也就是申请后操做系统剩余的内存,也会有部分受到 JVM 的控制。比较典型的就是一些 native 关键词修饰的方法,以及对内存的申请和处理。

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

若是你申请了一台系统内存为 2GB 的主机,可能 JVM 能用的就只有 1GB,这即是一个限制。

总结

JVM 的运行时区域是栈,而存储区域是堆。不少变量,其实在编译期就已经固定了。

相关文章
相关标签/搜索