程序计数器
程序计数器是一块较小的内存空间,依赖于用户线程,随着用户线程的启动和结束而存在和销毁,它可以看作是当前线程所执行的字节码的行号指示器。多线程对于处理器来说属于串行执行,在任意时刻一个处理器只会执行一条线程中的指令,因为处理器的处理速度是超级快的,我们用多线程是可以充分的利用处理器资源,让它尽可能的满负荷运行。而Java虚拟机就是通过多个线程轮流切换,分配处理器执行时间的方式实现的。但是有一个问题,线程来回切换,等到再轮到本线程执行的时候如何知道上一次执行到哪里了呢,这里程序计数器就诞生了,线程之间是不能相互干扰,因此程序计数器是独立存储的,每个线程私有。
如果线程执行的是一段Java方法,计数器记录的是所执行的虚拟机字节码的指令地址;如果是执行的本地(Native)方法,计数器值应该为空(Undefined)。这一块内存区域是唯一一个在《Java虚拟机规范》中规定任何OutOfMemoryError情况的区域。
Java虚拟机栈
Java虚拟机栈也是线程私有的,它的生命周期与线程完全相同。每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧存储局部变量表,操作数栈,动态链接,方法出口等信息。方法被调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放的是编译阶段可知的各种Java虚拟机基本数据类型(long,double,int,short,char,byte,boolean),对象的引用(reference类型,它不等于对象本身,可能是指向对象起始位置的指针,也可能是指向对象的句柄或者其他指向该对象相关的位置)和returnAddress(指向一条字节码指令的地址)。
这些数据类型在局部变量表中存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型是两个变量槽,其余数据类型各占一个变量槽,局部变量表在编译期间完成分配,当进入一个方法时这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,而且在运行期间是不会发生改变的。这里所说的大小是指的变量槽的数量,虚拟机真正使用多大的内存空间是由具体的虚拟机实现决定的。
Java虚拟机栈中有两种异常,当线程请求的栈深度大于虚拟机允许的深度就会抛出StackOverflowError,(比如递归就会引发这个异常);如果Java虚拟机的栈容量可以动态扩展(HotSpot栈容量是不可以动态扩展的),当栈扩展到无法申请到足够的内存就会抛出OutOfMemoryError。
本地方法栈
本地方法栈和Java虚拟机栈的作用非常相似,不同的是Java虚拟机栈是为Java虚拟机执行Java方法(也就是Java字节码)服务,而本地方法栈是为虚拟机使用到的本地方法服务。
堆
堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,虚拟机启动时创建。此块区域的唯一目的就是存放对象实例。考虑到现在即时编译技术的进步,Java所有的对象都分配在堆上变得不是那么绝对了。
Java堆是垃圾收集管理器管理的内存区域,因此Java堆也被称作“GC堆”,现代的垃圾收集器都是基于“分代收集”理论设计的,因此Java堆又会被细分为“新生代”,“老年代”,“永久代”,Java堆中存储的只能是对象的实例。Java 堆可以是物理上不连续的内存空间,但是逻辑上应该是被视为连续的。Java堆大小可以实现扩展,通过参数(-Xmx和-Xms),如果在Java堆中没有空间完成对象实例的分配,就会出现OutOfMemoryError。
方法区-元空间
方法区也是被所有线程共享的一块内存区域,它用于存储已经被虚拟机加载的类信息,常量,类变量,静态变量,即时编译之后的代码缓存。Java1.8以前又叫做永久代,因为在1.8以前永久代是存在于Java虚拟机,受限于Java虚拟机的内存大小,比较明显是的是如果加载的JSP页面过多会出现永久代内存溢出,永久代中还是存在垃圾回收,只是比较少,1.8以后完全用元空间替代,元空间不受Java虚拟机内存的限制,受限与本地内存的大小,但是可以设置元空间的大小。
运行时常量池
运行时常量池是方法区的一部分,Class文件中除了类的版本,属性,方法,接口等信息之外,还存在一项信息是常量池表,用于存放编译期间生成的各种字面量和符号引用,这一部分在类加载完之后放到方法区的运行时常量池中。