JVM的内存模型

概述

我们都知道Java语言较C/C++而言,拥有着完美的自动内存管理与垃圾回收机制,不容易出现内存泄漏和内存溢出的问题。但是,因为我们将控制内存的权力完全交给了JVM,所以一旦出现内存泄漏或溢出的问题,如果不了解JVM是怎样使用内存的话,对于问题的解决我们将无从下手。

内存泄漏

程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致系统无法分配内存给其他进程使用,最终导致程序运行速度变慢甚至系统崩溃。

内存溢出

系统中存在无法回收的内存或使用的内存过多,使得程序运行要用到的内存大于能提供的最大内存,最终导致程序无法运行。

运行时的数据区域

JVM在执行Java程序的过程中会把它所管理的内存区域划分成若干个不同的数据区域。这些数据区域有着各自的用途,以及创建和销毁时间。主要分为以下几个区域:
JVM的内存模型

程序计数器(线程私有)

是一块较小的内存空间,保存当前执行指令的地址。它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要这个计数器来完成。它是线程私有的,每个线程都需要有一个独立的程序计数器,各线程之间程序计数器互不影响,独立存储,生命周期与线程相同。

Java 虚拟机栈(线程私有)

Java虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个栈由多个栈帧(Fram)组成,对应着每次方法调用时所占的内存,每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。它也是线程私有的,生命周期与线程相同。

异常:在栈深度溢出抛出 *Error 和栈扩展失败会抛出 OutOfMemoryError异常

JVM的内存模型

局部变量表

存放局部变量的列表,它可以保存基本数据类型(byte,short,char,int,long,float,double,boolean),引用类型(reference)和returnAddress类型(指向了一条字节码指令的地址)。两个局部变量可以保存一个类型为long和double的数据。局部变量使用索引来进行定位访问,第一个局部变量的索引为0。

操作数栈

当一个方法刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出到局部变量表或返回给方法调用者。一个完整的方法在执行期间往往包含多个这样入栈/出栈的过程。

动态链接

简单的理解为指向运行时常量池的引用。在class文件中,一个方法调用了其他方法,或者访问其成员变量是通过符号引用来表示的,动态链接的作用就是将这些符号引用所表示的方法转换成实际方法的直接引用。

方法返回地址

方法调用的返回,包括正常返回(有返回值)和异常返回(没有返回值),不同的返回类型有不同的指令。无论采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法返回可能需要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态。

本地方法栈(线程私有)

本地方法栈的功能与特点类似于虚拟机栈,也是线程私有的。不同的是,虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机执行本地方法服务。

异常:在栈深度溢出抛出 *Error 和栈扩展失败会抛出 OutOfMemoryError异常

Java堆(共享)

Java堆是虚拟机管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。堆内存是用来存放对象实例的,因此也是垃圾收集器管理的主要区域,在有的资料里也被称为"GC"堆。

由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆中经常出现"新生代",“老年代”,“永久代”,“Eden区”,“SurvivorFrom区”,"SurvivorTo区"等名词。这只不过仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,并不是Java虚拟机具体实现的固有内存布局,更不是《Java虚拟机规范》中对Java对的进一步划分。

将Java堆细分的目的只是为了更好的回收内存或更快的分配内存。

异常:当堆没有完成实例分配,并且堆也无法在扩展时,Java虚拟机就会抛出OutOfMemoryError的异常。

方法区 (共享)

是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据 。

  • 在JDK 8 之前,由于HotSpot设计团队希望可以像Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作,所以使用永久代来实现方法区
  • 这种设计导致了Java应用程序更容易遇到内存溢出的问题(因为永久代有-XX:MaxPermSize的上限)
  • 到了JDK 6 的时候HotSpot的开发团队就有放弃永久代,逐步改用元空间的想法
  • 到了JDK 7 ,把原来放在永久代中的字符串常量池,静态变量等移出
  • 到了JDK 8 ,完全放弃了永久代的概念,改用本地内存中的元空间来代替,把JDK 7 中永久代剩余部分移到元空间中
    JVM的内存模型

异常:如果方法区无法满足新的内存分配的需求,就会抛出OutOfMemoryError的异常。

运行时常量池

是方法区的一部分。Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池表,用于存储编译时期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。不过一般来说,除了符号引用外,还会把符号引用翻译出来的直接引用也存储在运行时常量池中。

异常:当常量池无法在申请到内存时,就会抛出OutOfMemoryError的异常。