JVM内存模型

JVM不仅承担了Java字节码的分析(JIT compiler)和执行(Runtime),而且还内置了自动内存管理机制,负责内存的分配与回收。

JVM自动内存管理机制虽然有诸多好处,但其实是把双刃剑,它在提升Java开发效率的同时,也容易使开发人员过度依赖于这个机制,从而弱化对内存的管理能力。这样系统就很容易发生JVM堆内存异常、垃圾回收(GC)方式不合适以及GC次数过于频繁等问题,这些都将直接影响到应用服务器的性能。因此,在进行JVM调优前,需要深入了解JVM内存原理。

在深入了解JVM内原理前,我们先来看看JVM的内存模型。JVM内存主要被划分为了堆、方法区、程序计数器、虚拟机栈和本地方法栈。

image-20200920083402508

一、堆(Heap)

堆,是JVM内存中最大的一块空间,该空间被所有线程共享,几乎所有的对象和数组都被分配到了堆中。堆被划分为新生代和老年代,新生代又被进一步划分为Eden区和Survivor区,而Survivor又是由From Survivor和To Survivor组成的。

在Java6中,永久代在非堆内存区:

image-20200920113631544

在Java7中,永久代的静态变量和运行时常量池被合并到了堆中:

image-20200920113709761

在java8中,永久代则被元空间所取代:

image-20200920090352851

二、方法区

方法区,主要用来存放已经被虚拟机加载的类相关信息,这些信息源自class文件,其中主要包含类信息和常量池。

方法区也是一个共享内存区,被所有线程共享,当两个线程试图访问方法区中的同一个类信息时,如果这个类还没有装入JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待。

注:很多开发者习惯将方法区称为“永久代”,其实这两者并不是等价。

在HotSpot虚拟机中使用了永久代来实现方法区,但在其它类型的虚拟机中,例如,Oracle的JRockit、IBM的J9就不存在永久代一说。需要知道的是,方法区只是JVM规范中的一部分,可以说,在HotSpot虚拟机中,设计人员使用了永久代实现了JVM规范的方法区。

HotSpot虚拟机,在Java7中已经将永久代中的静态变量和运行时常量池转移到了堆中,其余部分则存储在JVM的非堆内存中;而在Java8中已经将永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置位于本地内存。之前永久代中类的元数据存放在了元空间,永久代中的静态变量(class static variables)以及运行时常量池(runtime constant pool)则转移到了堆中。

此时,我们可能会有这样的疑问,Java8为什么要使用元空间替代永久代?

官方给出的解释是:

  • 为了融合HotSpot JVM与JRockit VM而做出的努力。因为JRockit没有永久代,所以不需要配置永久代。
  • 永久代存在诸多问题。永久代经常因内存不够用或发生内存溢出,而引发java.lang.OutOfMemoryError: PermGen异常。因为在JDK1.7版本中,指定的PermGen区大小为8M,由于PermGen中的元数据信息在每次FullGC时都可能被收集,回收率都偏低,效率很难令人满意;还有,为PermGen分配多大的空间也很难确定,PermSize的大小依赖于很多因素,比如,JVM加载的class总数、常量池的大小和方法的大小等。

三、程序计数器

程序计数器,是一块很小的内存空间,该空间由线程独占,主要用来记录线程执行的字节码地址。分支、循环、跳转、异常、线程恢复等都依赖于程序计数器。

这里,我们需要知道的是,由于Java是多线程语言,当同时执行的线程数超过CPU核数时,线程之间就会根据CPU时间片轮询争夺CPU资源。如果一个线程的时间片用完了,或者因其它原因导致这个线程的CPU资源被提前抢夺,那么这个退出的线程就需要一个单独的程序计数器,来记录下一条运行的指令。

四、虚拟机栈

虚拟机栈,是线程私有的内存空间,和Java线程一起被创建,主要用于管理Java函数的调用。当创建一个线程时,会在虚拟机栈中为这个线程申请一个线程栈,用来保存方法的局部变量、操作数栈、动态链接方法和返回地址等信息,并参与方法的调用和返回。每个方法的调用都伴随着栈帧的入栈操作,方法的返回则是栈帧的出栈操作。

五、本地方法栈

本地方法栈,也是线程私有的内存空间,主要用于管理本地方法的调用。需要注意的是,本地方法并不是用Java实现的,而是由C语言实现的。

六、小结

在这篇文章中,我们主要熟悉了JVM的内存模型,JVM的内存空间被划分为了堆、方法区、程序计数器、虚拟机栈和本地方法栈。其中:

  • 堆,用于存放所有的对象和数组
  • 方法区,用于存放已经被虚拟机加载的类相关信息
  • 程序计数器,用于保存下一条执行指令的地址
  • 虚拟机栈,用于管理Java函数的调用
  • 本地方法栈:用于管理本地方法的调用

JVM内存模型
https://kuberxy.github.io/2020/09/20/JVM内存模型/
作者
Mr.x
发布于
2020年9月20日
许可协议