java 虚拟机学习笔记之一《运行时数据区》

时间:2022-12-27 15:41:35

java 虚拟机运行时数据

        Java虚拟机在执行Java程序的时候,会把它管理的内存划分为若干个不同的数据区域,这些区域有着各自用途,以及创建和销毁时间,有的区域随着虚拟机的启动而创建,有的区域则是依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范(Java SE 7版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如下图

java 虚拟机学习笔记之一《运行时数据区》

1.程序计数器

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

        由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。在任意一个确定的时刻一个处理器(多核的看成一个内核)都只会执行一个线程的中的指令。因此,为了保证线程切换后能够恢复到正确的位置,每一个线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存。

        如果线程执行的是Java方法,这个计数器记录的是当前正在执行的虚拟机字节码指令的地址,如果执行的是Java本地方法,则计数器的值为空(undefined)。此内存是Java虚拟机里面唯一一个没有规定任何 OutOfMemoryEorror情况的区域。

2.Java虚拟机栈

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

        经常有人把Java内存分为堆内存和栈内存,这是比较粗糙的。其中所指的栈就是虚拟机栈,或者说是虚拟机栈中的局部变量表部分。

        局部变量表存储了编译器可知的各种基本数据类型,(boolean、byte、char、short、int、float、龙、double)、对象引用和returnAddress类型。

        其中64位长度的龙和double类型的数据会占用2个局部变量空间,其余的数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,当进去一个方法时候,这个方法所需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

        这个区域规定两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度将会抛出*Error异常;如果虚拟机栈可以动态扩展时候,无法申请到足够的内存,就会抛出OutOfMemory异常。

3.本地方法栈

        本地方法栈和虚拟机栈发挥的作用很相似,区别是后者为虚拟机执行Java方法服务,后者为虚拟机使用Native方法服务。在虚拟机规范中堆本地方法栈中方法的使用,使用方式与数据结构并没有强制规定,因此具体的虚拟机可以*实现它,有的甚至把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样会抛出两个异常。

4.Java堆

        Java堆是虚拟机管理内存最大的一块,是被所有线程共享的一块区域,在虚拟机启动时候创建。它的唯一目的就是存放实例,几乎所有的对象实例都在这里存放。Java虚拟机规范中描述的是:所有的对象实例以及数组都要在对上分配,但是随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都是分配在堆上也渐渐变得没那么绝对了。

        Java堆是垃圾收集器管理的主要区域,因此很多时候也被叫做GC堆。从内存回收角度来看,由于现在收集器基本采用分代收集算法,所以Java堆中还可以细分为新生代和老年代,在细致一点可以分为Eden空间、From Survivor空间、To Survivor空间等。从内存分配的角度来看,线程共享Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。不过无论怎么划分都与存放内容无关,存放的都是实例,进一步划分的目的是为了更好地回收内存或者更快的分配内存。

        根据Java虚拟机规范规定:Java堆可以处于物理不连续的内存空间中,只要逻辑上是连续的即可。在实现的时候,既可以是固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照课扩展来实现的(通过-Xmx和-Xms控制)。如果没有足够的内存在分配实例,并且无法扩展时候,则会抛出OutOfMemoryError异常。

5.方法区

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

        对应习惯在HotSpot虚拟机开发的 部署的开发者来说,很多人都更愿意把方法区称为永久代,本质上两者有区别的,只是因为hotspot虚拟机设计者选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样hotspot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理的代码。

        Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续内存和可以选择固定或者扩大大小外,还可以选择不实现垃圾回收,相对而言垃圾回收再找个区域比较少出现的,但并非数据进入这个区域真的就永久存在,这个区域内存回收的目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收成绩难以令人满意,尤其对类型的卸载,条件相当苛刻,但是这部分回收确实是必要的。

        当方法区无法满足内存需求的时候,会抛出OutOfMemoryError异常。

5.1.运行时常量池

        运行时常量池是方法区的一部分。class文件中除了有类的版本、字段、方法、借口等描述信息外,还有一项信息就是常量池,用于存放编译期生成各种字面面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

        Java虚拟机对class文件每一部分(包含常量池)的格式有严格要求,每一个字节用于存放那种数据必须符合规范才被虚拟机认可、装载、执行,但对运行时常量池没有做任何细节规范要求。一般来说,除了保存class文件中描述的符号引用外,还会把翻译出来的直接引用也存在运行时常量池中。

        运行时常量池相对于class文件常量池的另外一个特征是动态性,Java语言并不要求常量一定在编译期产生,,也就是说并非只有常量池的的内容才能进入方法区的运行时常量池,运行期间也可以将新的常量放入池中。

        由于运行时常量池是方法区的一部分,自然受到方法区的内存的限制,当常量池无法再申请到内存时候会抛出OutOfMemoryError异常。

6.直接内存

        直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但这部分内存也被频繁使用,而且可能导致OutOfMemoryError异常

        在JDK1.4中新加入了NIO(new input/output)类,引入了一种基于通道(channel)与缓冲区(buffer)的I/O方式,它他可以使用native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样在一些场景中显著提高性能,因为避免了在Java堆和native堆中来回复制数据。

        显然本机的直接内存的分配不会受到Java堆大小的限制,但是既然是内存肯定受到本机总内存大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时候,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态内存扩展时候出现OutOfMemoryError异常。


总结:

属于线程私有的内存:程序计数器,Java虚拟机栈、本地方法栈

共享的内存:方法区、Java堆、

会抛OutOfMemoryError异常的内存:Java虚拟机栈、本地方法栈、方法区、Java堆

会抛*eError异常的内存:Java虚拟机栈、本地方法栈

随线程启动创建的内存:程序计数器、Java虚拟机栈、本地方法栈

随虚拟机启动创建的内存:方法区、Java堆