引言
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存会被划分成以下几个运行时数据区域,如下图所示。
如上图所示,JVM运行时数据区分为线程共享区和线程独占区。顾名思义,线程共享区是所有线程共用的内存,包括方法区和Java堆,而线程独占区是每个线程私有的,包含虚拟机栈、本地方法栈和程序计数器。每条线程被创建时,JVM会为每条线程开辟专属于该线程的虚拟机栈、本地方法栈和程序计数器。
方法区
方法区是各个线程共享的内存区域,在方法区中存储了每个被JVM加载的类的信息、常量、静态变量、即时编译器编译后的代码(即时编译是指JVM会对经常执行的方法编译成本地机器指令,后续调用该方法时执行的是编译后的机器指令,而不是字节码指令,以提高程序的运行效率)等数据。
方法区的特点
- 在方法区中,类class文件的加载是线程安全的,如果有两个线程同时访问一个未被加载的类A,但类A只会被加载一次。
- 方法区的内存不一定是连续的,大小可通过参数配置: -XX:PermSize=5M -XX:MaxPermSize=7M,说明:PermSize为方法区大小, MaxPermSize为最大的方法区大小。如果方法区中的数据超出MaxPermSize,就会抛出OutOfMemoryError异常。
Java堆
Java堆和方法区一样是各个线程共享的内存区域,也是Java虚拟机所管理的内存中最大的一块。Java堆的作用是存放对象实例。
由于Java堆是线程共享的,所以会存在内存争抢的问题:当两个线程同时创建对象实例时,把堆中的同一块内存分配给了两个不同的对象实例,比如Thread1申请创建对象a(大小2k),JVM把2k的堆内存分配给了对象a,但还没把这2k的内存区域标记为“已分配”,这时Thread2申请创建对象b(大小1k),由于未将对象a的2k内存标记,所以JVM把其中的1k内存分配给了对象b。
对于上述问题,了解过Java并发我们可能会想到给堆内存加锁:在给对象分配堆内存时,给Java堆加锁,分配完成后解锁。但是这种方案会使得同一时间只能有一个线程成功创建对象,效率非常低。对于这个问题JVM采用的是在Java堆中划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),每个线程都是在自己的TLAB中进行对象实例的创建,这就保证了各个线程在创建对象时使用的时不同的内存区域。
程序计数器
程序计数器是一块很小的内存区域,大小只有一个字长。程序计数器的作用是保存线程将要执行的下一条字节码执行的指针。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
程序计数器的特点
- 程序计数器是线程私有的,生命周期和线程相同,各个线程之间的程序计数器独立存储,互不影响。
- 如果线程正在执行的是一个Java方法,程序计数器记录的是下一条字节码指令的指针;如果正在执行的是native方法,程序计数器的值则为空(Undefined)。
- 程序计数器是惟一一个在Java虚拟机规范中没有定义任何OutOfMemoryError情况的内存区域。
虚拟机栈
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
虚拟机栈的特点
- 局部变量表所需的内存空间大小是固定不变的,而且在编译期间就确定了,在方法运行期间不会改变局部变量表的大小。
- 如果线程请求的栈深大于虚拟机所允许的深度,将抛出StackOverflowError异常(通常由无限递归调用引起的);如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常
本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。