注意:Java内存结构 != Java内存模型
Java内存结构和java内存模型是是不同的两个重要概念,Java内存结构指JVM所管理的内存在执行java程序过程中被划分成若干个不同的数据区域结构。
Java内存模型(Java Memory Model,JMM)是指一种符合内存模型规范的,屏蔽了各种硬件和操作系统访问差异的,实现了Java程序在各种平台下对内存的访问都能保证效果一致的机制和规范。
Java内存结构(运行时数据区域)
- 程序计数器:线程私有,存放字节码指令的地址;
- 虚拟机栈:线程私有,与java方法调用有关,存储局部变量表,操作数栈,动态链接,方法接口等信息;
- 本地方法栈:与虚拟机栈类似,专为Native本地方法服务;
- java堆:线程共享,存放对象实例,垃圾收集(GC)的主要场所;
- 方法区:线程共享,存放类信息,常量、静态变量、即时编译器编译后的代码等。
《深入理解JVM虚拟机》一书中对上图的解释为:
一、程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,他可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
线程私有:由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器就是一个内核)都只会执行一条线程中的指令。java是支持多线程的,所以,为了线程切换后能够正确的回到上一次被切换时所执行的位置,每条线程都需要一个单独的程序计数器,各线程的查询计数器之间互不干扰,独立存储,即"线程私有"的内存。
如果线程正在执行一个Java方法,这个程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是Native方法(本地方法:java只提供方法接口,具体由如C,C++等其它语言实现),这个计数器的值为空。
程序计数器的内存区域是唯一一个在Java虚拟机规范中没有规定OutOfMemoryError情况的区域。
二、java虚拟机栈(栈)
Java虚拟机栈(Java Virtual Machine Stacks)描述的是Java方法执行的内存模型,每个方法每个方法在被调用执行的同时都会在虚拟机栈中创建一个栈帧(Stack Frame) 用于存储局部变量表,操作数栈,动态链接,方法接口等信息,每一个方法从调用到执行完成的过程都对应了一个栈帧在虚拟机栈中从入栈到出栈的过程。(我们常说的"栈"和"堆"中的栈就是指虚拟机栈,或者说虚拟机栈中的局部变量表部分)
虚拟机栈也是线程私有的,它的生命周期与线程相同。
局部变量表存放了编译期可知的各种基本数据类型(boolean byte char short int float long double)、对象引用和returnAddress类型(指向了一条字节码指令的地址)。其中64位的long和double类型的数据会占用两个局部变量空间,其他类型数据只占一个局部变量空间。局部变量表所需要的的内存大小在编译期就可以完成分配,当进入一个方法时,这个方法需要在虚拟机栈中分配多大的局部变量表空间是完全确定的,在运行阶段不会改变局部变量表的大小。
在Java虚拟机规范中,对虚拟机栈区域规定了两种异常状况
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
- 如果虚拟机栈可以动态扩展,当扩展时无法申请到足够内存,将抛出OutOfMemoryError异常。
三、本地方法栈
本地方法栈(Native Method Stack)顾名思义就是指为Native方法提供的方法栈,功能和虚拟机栈差不多,区别就在于虚拟机栈为虚拟机执行的java方法的字节码服务,本地方法栈则为虚拟机使用到的Native方法服务。
在虚拟机规范中对本地方法栈中方法使用的语言,使用方式和数据结构并没有强制性的规定,因此虚拟机可以自由的去实现它。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError异常和OutOfMemoryError异常。
四、Java 堆
Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块,此内存最大的目的在于存放对象实例,几乎所有的对象实例都在堆中分配内存(在虚拟机规范中是这样说的:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也逐渐变得不是那么"绝对"了)。
Java堆是所有线程共享的一块内存区域(线程共享),在虚拟机启动时被创建。Java堆可以处于物理上不连续的内存空间,只要逻辑上连续即可,就像我们的磁盘空间一样,它既可以实现固定大小,也可以是可拓展的,不过当前主流的虚拟机都是按照可拓展的方式来实现的,我们也可以通过"-Xmx"和"-Xms"来控制。
Java堆是垃圾收集器管理的主要区域,因此有时也被称为"GC堆"(Grbage Collected Heap)它向下再分区域可以从以下两个方面考虑:
- 从内存回收的角度(以主流的分代收集算法)可以分为新生代和老年代;再细致一点可以分为Edan空间,From Survivor空间和To Survivor空间。
- 从内存分配的角度来看,线程共享的Java堆可能分划为多个线程私有的分配缓冲区TLAB(Thread Local Allocation Buffer)。
如果在Java堆中没有内存来完成实例分配,并且堆也无法再扩展时,虚拟机将抛出OutOfMemoryError异常。
五、方法区
方法区(Met5hod Area)用于存储虚拟机加载的类信息(Class文件)、常量、静态变量、即时编译器编译后的代码等数据。虽然在Java虚拟机规范中的方法区被描述为堆的一个逻辑部分,但他也有一个别名叫做"Non-Heap"(非堆),所以我们通常把方法区和堆分开来解释。
方法区也是各个线程共享的内存区域。Java规范中对方法区的限制比较宽松,和Java堆一样也不需要严格的连续物理空间,也可以选择固定大小或者可拓展,此外还可以选择不实现垃圾收集。
对于习惯于在HotSpot虚拟机上开发和部署程序的开发中来说,很多人愿意将方法区称作"永久代"(Permanent Generation 分代垃圾收集中的概念),本质上二者并不等价,只是HotSpot虚拟机的垃圾收集器为了可以实现和管理堆内存一样来管理方法区内存,而把GC分代收集拓展到方法区一种表述。其它虚拟机是不存在永久代这一概念的。 其实相对而言,针对方法区的垃圾收集行为是比较少的,但也不是说数据"永久"存在而不处理,只是对于这部分的垃圾收集主要是针对常量池的回收和类型的卸载,尽管如此,回收效果也是不可靠的。
Java虚拟机规范规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。