java虚拟机运行时数据区域-程序计数器,虚拟机栈,本地方法栈,堆等
内容摘抄自《深入理解Java虚拟机 第三版》
java虚拟机在运行java程序时会把内存划分为几个不同的数据区域,这些区域各有用途,创建及销毁时间。根据《Java虚拟机规范》规定,java虚拟机包含以下几个内存区域:程序计数器,虚拟机栈,堆,方法区,本地方法栈
1.1.1 程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,可以看做当前线程的执行字节码的行号指示器。java概念模型中,字节码解释器,就是通过改变这个计数器的值来获取下一条执行字节码指令。它是程序控制流的指示器,分支,循环,跳转,异常处理都依赖这个计数器完成。
多线程是通过线程轮流切换,分配处理器执行时间来实现的。在任意时刻,一个处理器都只会执行一条线程中的指令。为了线程切换后能够恢复正确的执行位置,每条线程都需要有一个独立的计数器,各条线程之间的计数器互不影响,独立存储。因此,这类内存区域为线程私有的内存。
如果线程执行的是java方法,那么计数器记录的是正在执行的虚拟机字节码的指令地址;如果是本地方法,这个计数器应为空。
程序计数器是规范中,唯一一个没有任何OutOfMemoryError的区域。
1.1.2 java虚拟机栈
与程序计数器一样,java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是java方法执行的线程的内存模型:方法被执行时,虚拟机会创建栈帧(stack frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
每一个方法被调用至执行完毕,都对应着一个栈帧在虚拟机从入栈到出栈过程。
通常情况下所说的栈就是指虚拟机栈,但大多数情况下指的是局部变量表
局部变量表存储了由编译期间可知的各种java虚拟机的基本数据类型(八种基本数据类型),对象引用(reference类型,不等同与对象本身,可能是指向对象其起始地址的指针引用,也可能是代表对象的句柄或其他)和returnAddress(指向了一条字节码指令地址)类型
这些变量在局部变量表的存储空间是以局部变量槽(slot)来表示的,64位的long和double占用2个变量槽,其余数据类型占用一个变量槽。局部变量表所需的空间在编译期间完成分配,当进入一个方法时,这个局部变量表在栈帧中分配多大的局部变量空间是确定的,运行期间不会改变表的大小。此处的大小,不是指实际内存大小,而是指局部变量槽的数量。
在《java虚拟机规范》中,对这个区域规定了两类异常状况:
1. 如果线程请求的栈的深度大于虚拟机所允许的最大深度,抛出StackOverflowError
2. 如果虚拟机的占容量可以动态扩容,当栈扩展时无法申请到足够的内存,将会抛出OutOfMemoryError异常
1.1.3 本地方法栈
本地方法栈(native Method Stack)与虚拟机栈的作用十分相似。其区别在于,虚拟机栈服务于虚拟机执行的java方法(也就是字节码),而本地方法栈则是为虚拟机使用到的本地方法(Native Method)服务
与虚拟机栈一样,本地方法栈也会抛出StackOverflowError,OutOfMemoryError异常。
java方法:是由java语言编写,编译成字节码,存储在class文件中的。java方法是与平台无关的。
本地方法:本地方法是由其他语言(如C、C++ )编写,编译成和处理器相关的代码。
本地方法保存在动态连接库中,格式是各个平台专用的,运行中的java程序调用本地方法时,虚拟机装载包含这个本地方法的动态库,并调用这个方法。
1.1.4 堆
堆(java heap)是虚拟机所管理的内存区域中最大的一块,jvm堆是被所有线程共享的一块内存,在虚拟机启动时创建。其唯一作用就是存储对象的实例。
《java虚拟机规范》中规定:“所有对象实例以及数组都应该在堆上分配”,但是对于现今java语言发展,对象实例都在堆上分配变得不是那么绝对了。
java堆可以处于物理上的不连续的内存空间中,但是在逻辑上应视为连续的。
java堆既可以被实现为固定大小,也可以被实现为可扩展的,当今主流的java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果java堆中没有内存完成实例分配,并且堆也无法扩展时,java虚拟机将抛出OutOfMemoryError异常
1.1.5 方法区
方法区(Method Area)与java堆一样,也是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译的代码缓存等。方法区虽然没有被描述为逻辑部分,但是通常称为“非堆”
说到方法区,与之相关的是另一个概念“永久代”,尤其在jdk1.8之前,许多java程序员都习惯在HotSpot虚拟机上开发,部署程序,很多人愿意将方法区称呼为“永久代”或两者混为一谈。
两者是不等价的,因为仅仅是当时的HotSpot团队把收集器的分代设计扩展到方法区,或者说用永久代来实现方法区。
在jdk1.6时HotSpot团队就有意放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划了,到了jdk1.7的HotSpot已经把字符串常量池,静态变量移出,而到了jdk1.8,终于放弃了永久代的概念,改用与JRockit,J9一样的本地内存中实现的元空间(Meta-space)来代替,把JDK1.7的剩余部分(类型信息)全部移到元空间中
《Java虚拟机规范》对方法区的要求是十分宽松的,除了和堆一样,不需要连续内存和可选固定大小或者可扩展外,还可以不实现垃圾收集。
相对而言,垃圾收集在方法区比较少出现,但不是数据进入了方法区就永久存在了。
方法区的内存回收主要对常量池的回收,和类型的卸载。
根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配的需求时,将抛出OutOfMemoryError的异常
1.1.6 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的已部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外。还有一部分叫常量池表(Constant Pool Table),用于存放编译期生成的字面量与符号引用,这部分在类加载后放到方法取得运行时常量池中。
虚拟机除了把Class文件中的符号引用保存外,也会把由符号引用翻译出来的直接引用也存储到运行时常量池中。
运行时常量池相对于Class文件的常量池的一个重要特征是具备动态性。Java不要求常量一定在编译期间产生,也就是说运行期间也能把新的常量放入常量池中。例如:String的inter()方法
作为方法区的有一部分,收到方法区的内存的限制,当常量池无法申请到内存时会抛出OutOfMemoryError异常。
1.1.7 直接内存
直接内存(DIrect Memory)并不是虚拟机的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也是被经常使用的,而导致OutOfMemory异常。
JDK1.4后加入了NIO(New Input/Output)类,并引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配对外内存,然后通过一个存储在Java堆里面的DirectByteBuffer的对象最为这块内存的直接引用进行操作。这样可以提高性能,kafka的mmap就是利用了这种机制,减少了数据的copy次数,减少了用户态和内核态的交互次数,避免了Java堆和Native堆的来回复制数据。
显然,本金直接内存的分配不会收到Java堆的限制,但是,既然是内存,肯定还是会受到本机总内存大小以及处理器寻址的限制。服务器管理员配置虚拟机参数时,会根据实际的内训去设置-Xmx等参数的信息,但是经常忽略直接内存,导致各个内存区域的总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。