深入理解JVM 2.2运行时数据区

java虚拟机我在执行java程序过程中会把它所管理的内存划分为若干个不同的数据区域。这些数据区域有各自的用途,以及创建和销毁的时间。根据java虚拟机规范中的规定,java虚拟机所管理的内存将会包含以下几个运行时数据区

2.2.1程序计数器

程序计数器是一块较小的内存空间,它可以看做是当前线程执行的字节码的行号指示器。在java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支,循环,调准,异常处理,线程恢复等都需要以来程序计数器来完成。

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

如果线程正在执行的是一个java方法,这个计数器记录的是真该执行的虚拟机字节码的地址:如果在指定的是本地native放啊,这个计数器的值为空(Undefind)次内存区域是唯一一个在java虚拟机规范中没有规定任何outOfMemoryError情况的区域

java虚拟机栈

与程序计数器一样,java虚拟机栈也是线程私有的我,它 生命周期与线程相同我。虚拟机描述的是java放啊执行的线程内存模型:每个方法被执行的时候,java虚拟机都会同步创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口灯信息。每一个方法被调用直至完毕的过程,救队友这一个栈帧在虚拟机中从入栈到出栈的过程。

经常有人把java虚拟机的内存区域笼统的划分为堆栈,,这种划分方式直接继承自传统的c++、c的内存不去,在JAVA语言里就显得有点粗糙了,实际的内存区域划分要比这个更复杂。不过这种划分方式也间接说明了程序员最关注的,与对象关系最密切的区域是堆和栈俩块。很多情况下啊栈指的是虚拟机栈中局部变量表的部分。

局部变量表存放了编译期克制的那个java虚拟机的基本数据类型,对象引用和returnAddress类型(指向了一条字节码的地址)。

这些数据类型在局部变量表中用槽slot表示,其中64位长度的long和double类型的数据会占用俩个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分分配多大的布局变量空间是完全确定的,仔方法运行期间不会改变局部变量表的大小。(这里说的大小是指变量槽的数量)

在java虚拟机规范中,对这个内存区域规定了俩类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将跑出StackOverflowEerror异常,如果虚拟机容量可以动态扩展,当栈扩展时,无法申请到足够的内存时,会抛出

OutofMemoryError异常

本地方法栈

本地放啊栈与虚拟机栈发挥的作用是非常相似的,其区别只是虚拟机为我虚拟机执行java方法(也就是字节码),而本地方法 栈是则是为虚拟机使用到本地native方法服务。

java虚拟机规范对本地方法栈中使用的语言,使用方式与数据结构没有任何强制规定。甚至有的java虚拟机(比如hotspot)直接把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在深度溢出的时候分别抛出StacOverflowError和OutOfMemoryError异常

java堆

对应用程序来说,jaava堆是虚拟机所管理的内存最大的一块,java堆是被所有线程所共享的一块区域,在虚拟机启动时创建。因此内存区域的唯一目的就是存放对象实例,java世界里几乎所有的对象实例都在这分配内存。这里的几乎是从现实角度来看,随着java语言的发展,现状已经能看到些许吉祥表明日后可能出现类型的支持,即便只考虑现状,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配,标量替换优化烧断导致一些微妙的变化悄然发生我,所以说java对象实例都分配在堆上也变得不是那么绝对了

如果从分配内存的角度看,所有线程共享的java堆中科院划分出多个线程私有的分配缓冲区,艺体生对象分配时的效率。不过无论从什么角度,无论如何划分,都不会改变java堆中存储内容的共性,无论是那个区域,存储的都只能是对象的实例。

根据java虚拟机规范的规定,java堆科院处于物理上不连续的空间中,但在逻辑上,它应该被视为连续的,这点就想我们用磁盘空间去存储大文件一样,并不要求每个文件都连续存放,但对于大对象(典型的就如数组对象),多数虚拟机实现处于简单,存储高效的考虑,很有可能要求使用连续的内存空间

java堆即可以被实现成固定大小的,也可以是可扩展的,不过当前主流java虚拟机都是按照可扩展来实现的。通过参数-Xmx和-Xms设定。

方法区

方法区域java堆一样,是个线程共享的内存区域,它用来存储已被虚拟机加载的类型信息,常量,静态变量,及时编译器编译后的代码缓存等数据。

说到方法区,不得不提一下永久代这个概念,尤其是jdk8以前。许多java程序员都习惯在hotspot虚拟机上开发部署程序,很多人都更愿意把方法区称呼为永久代,或者将俩者混为一谈。本质上这俩这并不是等价的,因为仅仅是hotspot虚拟机设计团队选择把收集器的分带设计扩展至方法区,或者说使用永久代来实现方法区而已,这样使得hotspot的垃圾收集器能够像管理java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。但是对于其他虚拟机实现,譬如BEA Jrockit IBM J9等来说,是不存在永久代的概念的,原则上如何实现方法区的细节,不受java虚拟机规范的约束,并不要求统一。但现在来看,当年使用永久代老实现方法区的决定并不是一个好主意,这导致了java应用程序更容易遇到内存溢出的问题考虑到hotspot未来的房展,在jdk6的时候hotspot开发团队就有放弃永久代,逐步改为采用与Jrockit J9一样在本地内存中实现的元空间来代替,把jdk7中永久代还剩余的内容(主要是类型信息)全部移到了元空间中

运行时常量池

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

运行时常量池相对于class文件常量池的另外一个重要特性是具备动态性,java语言并非不要求常量一定只有编译期才能产生,也就是说,并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性比较多的被用在string类的intern()方法。

既然运行时常量池也是方法区的一部分,自然受到方法区的内存限制,当常量池无法再申请到内存时会炮锤oom异常

直接内存(direct memory)并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但是这部分内存也被频繁使用,而且也看导致oom 异常的出现,所以我们放到这里一起讲解。

在jdk1.4中新加入了NIO类,引入了一种基于通道缓冲器的I/O方式,它可以使用native函数库直接分配堆外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些厂家中显著提高性能,因为避免了再java堆和native堆中来回复制数据。

显然本机直接内存的法恩培不会受到java堆大小的限制,但是既然是内存,肯定还是会受到本机总内存限制,从而导致oom异常

Last modification:April 19, 2022
如果觉得我的文章对你有用,请随意赞赏