Java运行时数据区

1.2.Java运行时数据区

Java虚拟机在执行编译器编译后的字节码文件时会将自己划分多个内存区域,这些区域各执其责,用来存放程序执行顺序、变量、对象等。我们《Java虚拟机规范》规范中把Java虚拟机在运行时划分以下几个区域,如图 1-2所示。这里我们需要注意的是JDK8之后把方法区改名为Metaspace,Metaspace的叫法其实是 HotSpot的规定,并不是《Java虚拟机规范》的规定,因此我们要区分两者的关系。还有一点,JVM运行时数据区分为五块,不少网友看到的资料有说六块的,那是因为多了一个叫直接内存,后面我们会讲到。

图1-2 Java虚拟机运行时数区

1.2.1程序计数器

程序计数器(Program Counter Register)是一个记录着当前线程所执行的字节码的行号指示器,可以理解为一个指针,告诉程序按照我指示的顺序进行执行。

后面文章我会让大家看到Java文件翻译成字节码是什么样子的,就清楚知道程序计数器工作的环境。

程序计数器占用内存很小,可以忽略不计,是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的区域,并且此区域是线程独享。

JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,这时候就需要程序计数器来记录某个线程的字节码执行位置,如果虚拟机是单线程也就没必要用程序计数器记录每个线程的位置了。

 

1.2.2虚拟机栈

栈(Java Virtual Machhine Stack)是线程私有的,如图1-3所示,所以他的生命周期与线程相同,他描述的是Java的内存模型,方法执行就会创建一个栈帧(Stack Frame),栈帧是方法运行的基础数据结构,用于存放局部变量表、操作数栈、动态链接、方法出口等,方法调用与结束的过程就是栈帧入栈与出栈的过程。在编译代码的时候,栈帧需要多大的局部变量表,还有栈的深度就已经确定了,所以栈帧需要多大的内存不会受运行时数据改变而改变。栈是一个先进后出的结构,好比一个桶,一个人向桶里扔东西,一个人从桶口拿东西。在《Java虚拟机规范 》中该区域规定了两种异常,第一个是当线程请求栈深大于虚拟机允许的深度,会抛出StackOverflowError异常,这里可以这样理解,我们再次强调一下,栈帧属于方法,当一个线程通过调用方法执行程序时,就会在栈中多一个栈帧用来存储本次调用方法的一些信息,调用,越多,栈帧越多,占用的内存就越多,栈就越深,某一个调用时申请栈帧的时候内存不够了,就出出现我们之前所说的StackOverflowError异常,例如递归使用不当时。当然,栈无法扩展内存也会出现OutOfMemoryError异常。

图1-3 Java虚拟机栈

局部变量表

他是栈帧中重要的部分,存放着编译期各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用、方法返回类型地址。除long和double类型的数据,都可以使用32位或者更小的空间去存储。并且一个slot可以存放一个32位的数据类型,但是jvm并没有规定一个slot是多大,所以slot的长度可以随着处理器、虚拟机、操作系统的不同而发生变化。long和double是Java规定64位数据类型,采用分割存储的做法,把一次long和double的读写分割为两次32位的读写做法有些类型,由于栈是线程共享,即使分开读写也不会有线程安全问题。

 

操作数栈

操作数栈即方法操作使用的,32位数据类型占用的容量为1,64位数据类型占用的容量为2。方法刚开始是空的,随着方法执行会有各种字节码指令往操作数栈中写入和提取内容,也就是入栈和出栈操作。例如一个方法计算两个数相加,先把两个int类型的数据出栈执行iadd指令,然后把结果入栈。

另外这部分区域由于被JVM优化后会出现相邻两个栈帧重合区域,如图1-4所示。重合区域,其实是用来储存两个相邻栈公用的局部变量表的数据,免去重复数据来回复制,提高空间利用率和效执行率。

图1-4 优化后的操作数栈

动态链接

每个方法都会引用了很多变量,引用的这些变量首先是通过符号引用,符号引用的东西不能直接用啊,所以动态链接就是把这些符号引用的数据变成直接引用,这种转化叫动态分派。

我们知道JVM在类加载阶段的过程中有一步就是把符号引用变成直接引用,或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分也就是动态链接的作用就是将在每一次运行期间转化为直接引用,这部分称为动态连接。(静态分派,动态分派)后面详细讲解类加载。

 

方法返回地址

方法返回按字面意思就可以解释。一个方法结束有两种方法,一种是碰到返回字节码指令,例如return。另一种是碰到异常。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。一般来说方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,不会返回给调用者,栈帧中一般也不会保存这部分信息。

方法退出的过程等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

 

1.2.3本地方法栈

    本地方法栈(Native Method Stacks),他和虚拟机栈作用是一样的,区别在于虚拟机栈处理的是Java字节码的,而本地 方法栈处理虚拟机使用的方法,我们可以看到很多源码是 native修饰 的,这些方法就是本地方法,该区域StackOverflowError和OutOfMemoryError异常。

 

1.2.4堆

堆(Heap)这块区域是虚拟机管理内存最大的一块,是线程共享的区域,在虚拟机启动的时候创建,他主要是存放对象实例、数组、静态变量以及常量池的,这块区域也是GC最重要的区域(关于这部分GC后面会有讲解)。堆在生成时为了提高效率问题分配了缓冲区(Thread Local Allocation Buffer,TLAB),提前给对象预留了空间,如果空间不足会根据CAS算法扩充。堆的大小在启动前可以指定,通过 -Xmx和-Xms控制最大和最小内存,如果运行中超过了控制范围就会抛出OOM。

 

1.2.5方法区

方法区(Method Area)与堆一样,是线程共享区域,方法区用来存放虚拟机加载的类型信息、常量、静态变量 、即时编译器编译后的代码等数据。由于 Java虚拟机不断的更新,我们看到这块区域的名字总是变动,例如有“非堆”、“永久代”、“元数据区”等 ,所以 对于这个区域的概念会变得模糊。这完全是由于HotSpot更新带来的区别。下面我们来跟着HotSpot的更新历史分别来看一下这几个名字,首先“非堆”是一个别名,因为他和堆都是线程共享,所以逻辑上有所相似,只是为了区分堆,我们起的一个别名“非堆”。其次是“永久代”,这个名词是我们最为熟悉的,在JDK6的时候是有“永久代“的概念,也是由于虚拟机团队认为这部分内容受《Java虚拟机规范》管束,会更容易出现内存溢出的现象,在JDK6后,虚拟机开发团队试图把”永久代“这部分改为本地内存(Native Memory),使用了本地内存就意味着直接使用宿主机的内存,发展到JDK7的时候已经把字符串常量池、静态变量移出来了,到了JDK8的时候就已经完全放弃了“永久代”的概念,把之前在“永久代”的内容放置在本地内存中,这部分本地内存改名为元空间(Metaspace),现在我们应该清晰HotSpot中这些名字的演变。该区域内存分配不足会出现OutOfMemoryError异常。

讲到这里我们已经看到了Java虚拟机规范的运行时数据区大部分时都会出现内存溢出的,压倒骆驼的从来都不是最后一根稻草,其实在溢出之前程序也是有预兆的,在申请内存后无法释放自己已申请的内存空间会导致内存泄漏,多次内存泄漏会导致内存溢出。

 

1.2.6运行时常量池

运行时常量池(Runtime Constant Pool)是方法区中较为重要的部分。常量池技术在Java语言作用很重要,他可以快速创建对象,有则从池里直接取出,没有则创建,他不同于new关键字创建在堆中。在介绍运行时常量池之前我们要介绍一个叫class文件常量池,因为常量池存在的时机不同,所以class文件常量池又可以叫做静态常量池,理论上一个类一个静态常量池。按照我们运行Java程序的步骤来说,首先我们要生成class文件,class文件不仅存储着类的字段、方法、类的描述信息,还有一个重要的信息就是常量池(constant pool table),它用于存放编译器生成的字面量和符号引用,字面量就是常量,如文本字符串和final修饰的值。符号引用是用符号来描述目标值的引用,例如String str=“123”中的str。

接下来我们继续来执行类的创建过程,加载、连接、初始化,而连接又包括验证、准备、解析三个阶段,这时类已经被加载到内存中,也就是JVM已经将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。既然是方法区中的一部分,那么自然收到方法区内存的限制,当常量池无法申请到内存时会出现OutOfMemoryError异常。

 

1.2.7直接内存

直接内存(Direct Memory),也可以说是堆外内存,因为它不属于Java虚拟机运行时数据区,也不属于《Java虚拟机规范》中定义的内存。但这部分内存是真实存在的,而且内存分配不足时会出现OutOfMemoryError异常。他常出现的 原因是在JDK1.4时新增加的NIO包 (New input/output)引入了一种基于通道(channel)和缓冲区(buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过堆上的DirectByteBuffer对象对这块内存进行引用和操作。直接内存只受本机物理内存的限制,设计这一内存的好处是堆外内存是直接受操作系统管理。这样做的结果就是能保持一个较小的堆内内存,以减少垃圾收集对应用的影响,其次是堆内内存由JVM管理,属于“用户态”;而堆外内存由OS管理,属于“内核态”。如果从堆内向磁盘写数据时,数据会被先复制到堆外内存,即内核缓冲区,然后再由OS写入磁盘,使用堆外内存避免了这个操作。

我们在启动虚拟机时常常只关注堆内内存,而忽视了直接内存 ,如果你不是通过-XX:MaxDirectMemorySize参数设置直接内存的话,他默认为最大堆内存,即与-Xmx相同。这样使得同台机器部署很多Java程序时导致所有区域内存大于物理机内存,所以我们在设置启动参数 时要考虑到直接 内存,避免在程序运行期间可能出现OutOfMemoryError异常。

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值