alexpdh's blog

理解 JVM:JVM 内存模型

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,以及创建和销毁的时间。有的区域随着虚拟机进程的启动就存在了, 有的区域则是依赖用户线程。根据《Java虚拟机规范(第二版)》,Java 虚拟机所管理的内存包含以下的几个区域。

java-memory.jpg


运行时数据区(Runtime Data Area)

由上图可以看出,在运行时数据区中:虚拟机栈、本地方法栈、程序计数器属于线程隔离的数据区,是单个线程私有的,它们的生命周期与线程相同;而方法区和堆属于所有线程共享的数据区,是所有线程共享的


程序计数器

程序计数器(Program Counter Register)是最小的一块内存区域,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取吓一跳需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。在多线程环境下,当某个线程失去处理器执行权时,需要记录该线程被切换出去时所执行的程序位置。从而方便该线程被切换回来(重新被处理器处理)时能恢复到当初的执行位置,因此每个线程都需要有一个独立的程序计数器。各个线程的程序计数器互不影响,并且独立存储。

  1. 如果线程正在执行一个 java 方法时,这个程序计数器记录的时正在执行的虚拟机字节码指令的地址;
  2. 如果正在执行的是 Native 方法,这个计数器的值则为空(Undefined);
  3. 此内存区域是唯一一个在 java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

Java 虚拟机栈

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

局部变量表存放了编译期克制的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其它与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量表空间(Slot),其余的数据类型只占 1 个。局部变量所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定了的,在方法运行期间不会改变局部变量表的大小。

在 Java 虚拟机规范中,对这个区域规定了两种异常状况:

  1. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;
  2. 如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。

本地方法栈

本地方法栈(Native Method Stack)虚拟机栈所发挥的作用是非常相似的,它们的区别不过是虚拟机栈为虚拟机执行 Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如 Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。


Java 堆

也叫做java 堆(Java Heap)、GC 堆(Garbage Collected Heap)是 java 虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,在 JVM 启动时创建。该内存区域存放了对象实例及数组(所有 new 的对象)。其大小通过 -Xms(最小值)和 -Xmx(最大值)参数设置,-Xms 为 JVM启动时申请的最小内存,默认为操作系统物理内存的 1/64 但小于 1G,-Xmx 为 JVM 可申请的最大内存,默认为物理内存的 1/4 但小于 1G,默认当空余堆内存小于 40% 时,JVM 会增大 Heap 到 -Xmx指定的大小,可通过 -XX:MinHeapFreeRation= 来指定这个比列;当空余堆内存大于 70% 时,JVM 会减小 heap 的大小到 -Xms 指定的大小,可通过 XX:MaxHeapFreeRation= 来指定这个比列,对于运行系统,为避免在运行时频繁调整 Heap 的大小,通常 -Xms 与 -Xmx 的值设成一样。

Java 堆是垃圾收集器管理的主要区域,从内存回收的角度来看,由于现在收集器基本是采用分代收集算法,堆被划分为新生代和老年代。新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代 GC(Minor GC) 任然存活的对象。

  1. 新生代: 程序新创建的对象都是从新生代分配内存,新生代由 Eden Space 和两块相同大小的Survivor Space(通常又称 S0 和 S1 或 From 和 To)构成,在 Sun HotSpot 虚拟机中 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80% + 10%),可通过 -Xmn 参数来指定新生代的大小,也可以通过 -XX:SurvivorRation 来调整 Eden Space 及 Survivor Space 的大小。
  2. 老年代: 用于存放经过多次新生代 GC 任然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:1、大对象,可通过启动参数设置 -XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。2、大的数组对象,切数组中无引用外部对象。 老年代所占的内存大小为 -Xmx 对应的值减去 -Xmn 对应的值。

java-heap.jpg

  • Young Generation 即图中的 Eden + From Space + To Space
    • Eden 存放新生的对象
    • Survivor Space 有两个,存放每次垃圾回收后存活的对象
  • Old Generation Tenured Generation 即图中的 Old Space 主要存放应用程序中生命周期长的存活对象。

方法区

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于内存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆)。对于 HotSpot 虚拟机,也把方法区成为“永久代”(Permanent Generation),默认最小值为16MB,最大值为64MB,可以通过 -XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。在 JDK 1.7 以后已经逐步改为采用 Native Memory 来实现方法区。当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中处了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。对于 HotSpot 虚拟机,在 JDK 1.7 中,已经把原本放在永久代的字符串常量池移除。


直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常。jdk1.4 中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小。


参考文献

alexpdh wechat
欢迎扫一扫关注 程序猿pdh 公众号!