前言

对于java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄漏和内存溢出的问题,由虚拟机管理内存这一切看起来都很美好。不过,也正是Java程序员把内存控制的权利交给了Java虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会成为一项异常艰难的工作。

运行时数据区域

JVM在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。根据规定,JVM管理内存将会包括以下几个运行时的数据区域

v2-abefb713de46f1e6dd241246c0afe263

  • 线程私有:程序计数器、虚拟机栈、本地方法区
  • 线程共享:堆、方法区

程序计数器

程序计数寄存器(Program Counter Register),Register 的命名源于 CPU 的寄存器,寄存器存储指令相关的线程信息,CPU 只有把数据装载到寄存器才能够运行。

这里,并非是广义上所指的物理寄存器,JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。主要用来记录各个线程执行的字节码的地址,例如,分支、循环、线程恢复等都依赖于计数器。

如果正在执行的是Native方法,这个计数器值则为空

虚拟机栈

Java虚拟机栈(Java Virtual Machine Stacks)是线程私有的,它的生命周期与线程相同。虚拟机栈的内部保存着一个个的栈帧(Stack Frame),对应着一个个的Java方法,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至完成的过程,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

Java 虚拟机规范允许 Java虚拟机栈的大小是动态的或者是固定不变的,有以下两种异常情况:

  • 如果采用固定大小的 Java 虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈深度大于 Java 虚拟机栈允许的深度,Java 虚拟机将会抛出一个 StackOverflowError 异常
  • 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个OutOfMemoryError异常

本地方法栈

本地方法栈(Native Method Stack)类似于虚拟机栈,它们的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

  • 当某个线程调用一个本地方法的时候,它就进入了一个全新的并且不再受虚拟机限制的世界,它和虚拟机拥有同样的权限。
  • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。

对于大多数应用,Java 堆是 Java 虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”(Garbage Collected Heap)。

为了进行高效的垃圾回收,虚拟机把堆内存逻辑上划分成两块区域(分代的唯一理由就是优化 GC 性能):

  • Young(新生代):新对象和没达到一定年龄的对象都在新生代

    • Eden
    • Survivor(有S0和S1两个区)

    这三个区的比例一般为8:1:1

  • Old(老年代):被长时间使用的对象,老年代的内存空间应该要比年轻代更大

对象在堆中的生命周期

当创建一个对象时,对象会被优先分配到Eden区,此时JVM会给对象定义一个对象年轻计数器

当 Eden 空间不足时,JVM 将执行新生代的垃圾回收(Minor GC)

  • JVM 会把存活的对象转移到 Survivor 中,并且对象年龄 +1
  • 对象在 Survivor 中同样也会经历 Minor GC,每经历一次 Minor GC,对象年龄都会+1

如过对象的年龄超过了年龄值(一般为15),对象会直接被分配到老年代


Java 虚拟机规范规定,Java 堆可以是处于物理上不连续的内存空间中,只要逻辑上是连续的即可,像磁盘空间一样。实现时,既可以是固定大小,也可以是可扩展的,主流虚拟机都是可扩展的(通过 -Xmx-Xms 控制),如果堆中没有完成实例分配,并且堆无法再扩展时,就会抛出 OutOfMemoryError 异常。

方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。

下面这张图片解释了栈、堆、方法区的交互关系

db050d0052a44605a13043a0bec204f0

运行时常量池(Java8叫元空间)

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息之外,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载之后进入方法区的运行时常量池中存放。

一个 Java 源文件中的类、接口,编译后产生一个字节码文件。而 Java 中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候用到的就是运行时常量池。

常量池可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

  • 在加载类和结构到虚拟机后,就会创建对应的运行时常量池
  • 常量池表(Constant Pool Table)是 Class 文件的一部分,用于存储编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
  • 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则 JVM 会抛出 OutOfMemoryError 异常。