读书笔记:《深入理解Java虚拟机》——(一)JVM内存结构

每一个Java程序员都知道,Java虚拟机有自动管理内存机制,这个机制可以让我们不用为每个对象去分配内存和回收内存,可以更专注于业务实现。但是,这并不代表着可以不用去关心内存分配,不恰当的编码可能导致内存泄漏等问题,而要想避免这些问题,就需要去理解Java虚拟机是如何去管理内存的。

Java虚拟机会将内存(运行时数据区)划分为若干个不同的区域,这些区域有各自不同的用途:

其中方法区(Method Area)和堆(Heap)为所有线程共享的数据区;虚拟机栈、本地方法栈以及程序计数器为每个线程独立的数据区,且生命周期与线程相同,随着线程的启动而产生,线程结束而消失。

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。[1]

我们知道,线程是CPU调度的最小单元,在任何一个确定的时间,一个处理器(多核处理器为一个核心)只会执行一个线程的指令。当一个挂起线程被唤起的时候,为了让CPU可以知道应该如何继续执行,就需要每个线程自己记录执行指令的位置,这就是程序计数器。

上面说了,程序计数器可以看作是当前所执行的字节码的行号指示器,但是Native方法大多是由C实现并编译成机器码的。所以当JVM执行到Native方法的时候程序计数器为空(undefined)。

那么Native方法的多线程是如何实现的呢?以HotSpot JVM为例,目前它在大多数平台上都是使用1:1(原生线程)模型,也就是每个Java线程都对应一个系统原生线程,因此,当执行到Native方法的时候是由原生线程执行的,所以并不需要JVM中的程序计数器。也就是Native方法的线程切换和其他原生应用一样,由CPU上的PC计数器实现。

这个内存区域是Java虚拟机规范中唯一一个没有规定任何OutOfMemoryError的区域

Java虚拟机栈

Java虚拟机栈和程序计数器一样,也是线程私有的。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会创建一个栈帧,每个方法被调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

栈帧中包含了局部变量表、操作数栈、动态链接、方法出口等信息。

局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,可以理解为对象的指针,而不是对象本身)和returnAddress类型(指向一条字节码指令的地址)。

在32位虚拟机中,一个局部变量空间(Slot)为32位,因此64位长度的long和double类型的数据会占用2个Slot,其余数据类型占用1个。局部变量表的大小在编译器就可知,当进入一个方法的时候,需要在栈帧中分配多大的空间给局部变量表是完全确定的,在运行时不会改变。

在Java虚拟机规范中,在这个区域规定了两种异常:
1.当线程请求的栈深度大于虚拟机允许的深度,抛出StackOverflowError;
2.如果虚拟机栈可以动态扩充(现在大部分虚拟机默认可扩充,不过Java虚拟机规范中允许固定大小的虚拟机栈),当扩展无法申请到足够的内存时,抛出OutOfMemoryError。

本地方法栈

本地方法栈与虚拟机栈的作用非常相似,只不过本地方法栈是为Native方法服务的。

虚拟机规范中没有对本地方法栈的实现方式做任何强制规定,由虚拟机自由实现。因此,像HotSpot虚拟机就直接把本地方法栈和虚拟机栈合二为一。

和虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError。

Java堆

通常来说,Java堆是虚拟机中最大的一块内存,在虚拟机启动是创建,唯一的作用是存放对象实例。Java虚拟机规范中描述:所有的对象实例以及数组都要在堆上分配[2],但是随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配和标量替换优化技术将导致这一点变得不那么绝对。

Java堆是垃圾收集器工作的主要区域,因此很多时候也叫做"GC堆"。从内存回收的角度来看,由于现在的收集器都采用分代收集算法,所以Java堆还可以分为新生代和老年代,而新生代又可以分为Eden空间、From Survivor空间、To Survivor空间(默认情况下按8:1:1的比例分配)。从内存分配角度来看,Java堆可以划分出多个线程私有的分配缓冲区。

Java虚拟机规范中规定,Java堆在物理空间上可以不连续,可以是固定大小,也可以是可扩展的,当前主流的虚拟机都是按可扩展来实现的(通过-Xmx(最大)和-Xms(最小)控制,两个参数设成一样就相当于固定的)。

在Java虚拟机规范中,在这个区域规定了一种异常:
当Java堆中没有足够的内存完成分配,且无法再扩展时,抛出OutOfMemoryError。

方法区

方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然方法区Java虚拟机规范把方法区描述为堆的一个逻辑部分,但它却有一个别名叫做Non-Heap(非堆),目的应该是为了与Java堆进行区分。

Java虚拟机堆方法区的限制非常宽松,除了不需要物理连续外,还可以选择不进行垃圾回收。在HotSpot虚拟机中,使用"永久代"(Permanent Generation)来实现方法区,为了可以让垃圾收集器像管理Java堆一样管理方法区。对于其他虚拟机(如BEA JRockit、IBM J9等),是不存在永久代的概念的。现在看来,使用永久代来实现方法区更容易遇到内存溢出问题(永久代有-XX:MaxPermSize上限,而J9和JRockit只要没超过系统可用内存就没有问题)。而且垃圾收集在这个区域出现情况比较少,但并不是数据一但进入方法区就永远存在了,针对这个区域的回收目标主要是针对常量池的回收和类型卸载,一般来说这个区域的回收"成绩"比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但对这个区域进行回收确实是有必要的。

在Java虚拟机规范中,在这个区域规定了一种异常:
当方法区中没有足够的内存完成分配时,抛出OutOfMemoryError。


  1. 摘自《深入理解Java虚拟机》,作者周志明。

  2. Java虚拟机规范中的原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated。