深入理解JVM之Java虚拟机(JVM)内存区域划分

来源:互联网 发布:淘宝网商城男装秋装 编辑:程序博客网 时间:2024/05/22 06:41

运行时数据区域

java虚拟机(JVM)在执行java程序时会将所管理的内存区域划分为若干个区域,这些区域有各自的用途,以及创建和销毁时间。如下图所示: 注*图片来自深入理解java虚拟机第二版

图片来自深入理解JAVA虚拟机第二版

java虚拟机运行时数据区

1.程序计数器

程序计数器是一块很小的内存空间,他可以看作当前线程所执行的字节码的行号指示器。Java是一门解释型的语言,.java文件被javac指令编译成.class的字节码文件。字节码解释器会将编译好的字节码文件解释执行,这也真是java语言可以很好的实现的跨平台的真正原因。字节码解释器工作时就是通过改变程序计数器的值来选择下一条需要执行的字节码指令,分支,跳转,循环,异常处理等基础功能都需要依赖这个计数器来完成。

java是一门支持多线程的语言,所谓的多线程实则是通过CPU的时间片轮转调度算法来实现的,在一个时间片内,处理器只会执行一个线程中的指令。为了保证线程切换过程时可以恢复到原来的执行位置,每条线程都会有一个独立的程序计数器,各线程之间互不影响,独立存储。

java程序中程序计数器记录字节码指令的地址。Native方法程序计数器则为空。这个区域不会有OutOfMemoryError异常。

2.Java虚拟机栈

java虚拟机栈描述的是java方法执行的内存模型,方法的执行的同时会创建一个栈祯,用于存储局部变量表、操作数栈、动态链接、方法的入口、出口等信息,每个方法从调用直到执行完成的过程,就对应着一个栈祯在虚拟机栈中入栈到出栈的过程。

上学的时老师常把java虚拟机的内存分为堆内存(heap)和栈内存(stack),这种分配方式实际上非常的粗糙,实际的内存划分远比这复杂。这么划分其实是为了让程序员理解对象内存分配最密切的这两个区域。所说的栈就是指虚拟机栈,或者说是虚拟机栈中的栈祯的局部变量表。堆部分下文会详细讲解。

(1)局部变量表

局部变量表中存放了编译时期可知的基本数据类型、reference类型的对象引用(它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与本对象相关的位置)和returnAddress类型(指向一条字节码指令的地址)。

returnAddress类型是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用在方法中通过this访问。Slot是可以重用的,当Slot中的变量超出了作用域,那么下一次分配Slot的时候,将会覆盖原来的数据。Slot对对象的引用会影响GC(要是被引用,将不会被回收)。 局部变量表所需的内存空间在编译时期完成分配,当进入一个方法时,这个方法需要在栈祯中分配多大的局部变量空间是完全确定的。在方法的运行期间不会改变局部变量表的大小。

(2)操作数栈

Java虚拟机的解释执行引擎被称为”基于栈的执行引擎”,其中所指的栈就是指-操作数栈。操作数栈也常被称为操作栈。 和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。
虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int。
虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,

(3)动态链接

每个栈帧都包含一个指向运行时常量池(方法区的一部分)中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中有你大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期间转化为直接应用,这部分称为动态连接。

(4)方法入出口信息

通过动态链接可以在运行时常量池中找到该方法的入口、出口信息。

在java虚拟机规范中,对这个区域规定了两种异常,一种是StackOverFlowError异常:请求栈深度大于虚拟机栈低指针。另一种是OutOfMemoryError异常:虚拟机栈在扩展时,如果无法申请到足够的内存。

3.本地方法栈

本地方法栈和虚拟机栈很相似,区别在于而本地方法栈是为Native方法服务而虚拟机栈是为java方法服务,在本地方法栈也会抛出StackOverFlowError异常、OutOfMemoryError异常。

4.Java堆

java堆是Java虚拟机所管理的内存中最大的一块。java堆是被所有线程所共享的一块内存区域,虚拟机启动时创建,几乎所有对象的实例都存储在堆中,所有的对象和数组都要在堆上分配内存。
java堆是垃圾收集器(GC)管理的主要区域,java堆中可以划分出多线程私有的缓冲区,但是无论怎么划分对象的实例仍然存储在堆中。java堆允许处于不连续的物理内存空间中,只要逻辑连续即可。堆中如果没有空间完成实例分配无法扩展时将会抛出OutOfMemoryError异常。

5.方法区

方法区与堆一样所有线程所共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。java虚拟机对方法区的限制非常宽松,除了和堆一样不需要连续的内存和可扩展,还可以不实现垃圾收集,相对而言,垃圾收集机制在这个区域出现的较少,当方法区无法分配足够内存时,将会抛出OutOfMemoryError异常。

6.运行时常量池

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

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

运行时常量池是方法区的一部分,自然当方法区无法分配足够内存时,将会抛出OutOfMemoryError异常。

7.直接内存

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

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

显然,直接内存不会受到Java堆内存的大小的影响,但是既然是内存,肯定还是受到本机内存大小以及处理器寻址范围限制。当各个内存区域的总和大于物理存储限制,从而导致动态扩展是出现OutOfMemoryError异常。

到此有关Java虚拟计的内存区域以及各个区域的会抛出的异常就介绍完了,如果你认真读并且仔细思考相信你一定能有所收获,你的理解一定不再局限于大学时老师所讲的堆和栈的理解,对Java内存的区域的深入理解对于今后的学习一定会有很大的帮助。同时对OutOfMemoryError异常也会有更为深刻的认知,对今后的工作中遇到的问题解决的更为轻松。

欢迎留言指出问题。期待一起进步!

如有疑问欢迎大家留言指正。祝大家生活愉快。

最后欢迎对Android开发感兴趣的老哥一起讨论。


1 0
原创粉丝点击