JVM之运行时数据区与内存溢出异常【一】

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。


根据虚拟机规范的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域(基于1.7):
在这里插入图片描述
每个区域中详情见下图(基于1.8):
在这里插入图片描述

1. 程序计数器

程序计数器是一块较小的内存空间,主要作用是存储线程执行的字节码行号

因为Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在一个时刻,一个处理器只会执行一个线程中的指令。所以为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。

此内存区域是唯一一个Java虚拟机规范中没有规定任何OOM情况的区域

2. 虚拟机栈

虚拟机栈也是线程私有的,对于每一个线程,JVM都会在线程创建的时候,创建一个属于该线程的栈。虚拟机栈描述的是方法执行的内存模型:栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,每个方法在执行的同时会在线程栈中创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从开始执行到执行结束就对应一个栈帧在虚拟机中入栈到出栈。

栈对应线程,栈帧对应方法

在活动线程中,位于栈顶的栈帧称为当前栈帧,正在执行的方法称为当前方法。在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。虚拟机栈通过压栈和出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,会跳转到另一个栈帧上。如果这个方法被调用,就会跳转到调用这个方法的栈帧上,这就是方法出口;如果出现了异常,会进行异常回溯,返回地址通过异常处理表确定。

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

操作数栈是一个栈结构,当JVM为执行方法创建栈帧的时候,会在栈帧中为方法创建一个操作数栈,保证方法内的指令可以完成工作。

动态链接:每个栈帧中包含一个常量池中对当前方法的引用,目的是支持方法调用过程的动态链接。

方法返回地址:方法执行时有两种退出情况:1.正常退出(遇到返回字节码指令,如RETURN、IRETURN、ARETURN);2.异常退出。无论哪种退出,都要返回到方法被调用的位置。方法退出的过程相当于弹出当前栈帧,退出有三种方式:1.返回值压入上层调用栈帧;2.异常信息抛给能够处理的栈帧;3.程序计数器执行方法调用后的下一条指令。

在虚拟机规范中,对这个内存区域规定了两种异常:1.如果线程请求的栈深度大于虚拟机所允许的深度,会抛出StackOverflowError异常;2.如果虚拟机栈动态扩展时无法申请到足够内存,就会抛出OOM。

3. 本地方法栈

由于现在java已经很成熟了,native方法基本使用不到了,所以HotSpot干脆直接把本地方法栈和虚拟机栈合二为一。

4. 堆

堆是Java虚拟机所管理的内存中最大的一块。它被所有线程共享,在虚拟机启动时创建。它的唯一目的是存放对象,几乎所以对象实例都在堆里分配内存(随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术会导致对象未必会在堆上分配),Java虚拟机规范中描述:所有对象实例以及数组都要在堆上分配。

堆是垃圾收集器(推荐阅读 JVM之垃圾收集器【三】)管理的主要区域:1.从内存回收的角度看,现在的收集器基本都采用分代收集算法(推荐阅读 JVM之垃圾回收算法和策略【二】),所以堆中还可以细分为新生代和老年代,再细致一点分为Eden区、From Survivor区、To Survivor区;2.从内存分配的角度看,堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB):对象优先分配在新生代Eden区,多线程环境下为了避免加锁等机制影响对象分配速度,JVM可以为每个线程分配一个私有的缓存区域(TLAB),这部分空间在分配时是线程独享的,在使用时是线程共享的,所以堆并不全是线程共享的。从本质上说,TLAB的管理依靠三个指针:start、end、top,start与end标记了Eden中被该TLAB管理的区域,该区域不会被其他线程分配内存时使用,top是分配指针,开始时指向start的位置,随着内存分配的进行,慢慢向end靠近,当等于end时触发TLAB refill。无论如何,堆中存放的都是对象。

TLAB的结构:
在这里插入图片描述
Eden区的结构:
在这里插入图片描述
堆内存结构:
在这里插入图片描述

堆内存的查看工具:(JConsole和VisualVm—我使用的就是这个,可以安装插件Visual GC,导航栏工具->插件里面)
在这里插入图片描述

延伸知识点:top命令查看cpu使用情况,load average 代表1分钟、5分钟、15分钟的系统平均负载,从这三个数字,可以判断系统负荷是大还是小。当CPU完全空闲的时候,平均负荷为0;当CPU工作量饱和的时候,平均负荷为1。
在这里插入图片描述

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续,就像磁盘空间一样。在实现时,HotSpot虚拟机是按照可扩展实现的(通过最小堆空间-Xms、最大堆空间-Xmx、新生代大小-Xmn等 参数控制)。如果在堆中没有空间完成对象实例分配,并且也无法再扩展,就会OOM。

5. 方法区

方法区和Java堆一样也是所有线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等元数据,每当一个类初次被加载的时候,他的元数据就会被放到方法区中。

方法区是虚拟机规范定义的运行时数据区域中管理最宽松的,Java虚拟机规范没有规定如何实现方法区,并且对方法区位置没有明确要求。HopSpot中(JDK1.8以前),方法区只是逻辑上的独立区域,在物理上并没有独立于堆而存在,而是位于永久代中。所以这时候方法区也是可以被垃圾回收的。

因为HotSpot虚拟机设计团队把GC分代收集扩展到了方法区,这样HotSpot的垃圾收集器就可以像管理堆一样管理这部分内存,所以很多人把方法区称作“永久代”。但使用永久代来实现方法区很容易导致内存溢出。在JDK1.7中,HotSpot把原本放在方法区的字符串常量池移到了堆中。JDK1.8中永久代被元空间(在本地内存中)替代,存储类元数据信息(字段、方法)、静态属性、常量。

元空间的本质和方法区类似,都是对JVM规范中方法区的实现,不过元空间和方法区之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

这部分和堆一样不需要连续的内存和可以选择固定大小或可扩展,也可以选择不实现垃圾收集。方法区的内存回收目标主要是针对常量池的回收和对类的卸载,但是类的卸载条件很苛刻,不过这部分区域进行内存回收是有必要的。

对应的JVM调参:
在这里插入图片描述
根据Java虚拟机规范规定,当方法区无法满足内存分配需求时,会OOM。

6. 运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池。每个Class文件的头四个字节称为Magic Number,它的作用是确定这是否是一个可以被虚拟机接受的文件;接着四个字节存储的是Class文件的版本号;紧挨着版本号的就是常量池入口。常量池用于存放编译器生成的各种字面量(如文本字符串、final常量值)和符号引用(字段方法这些符号符号引用在运行期就要进行转换,以得到真正的内存入口地址),这部分内容将在类加载后存放进方法区的运行时常量池中。

Java虚拟机规范对运行时常量池没有任何细节要求,但对于Class文件常量池的格式有严格规定。一般来说,除了保存Class文件中描述的符号引用,还会把翻译出来的直接引用也存储在运行是常量池中。

运行时常量池对于Class文件常量池的另外一个重要特征是具备动态性。常量不一定在编译期才能产生,运行期间也可以将常量放入池中(String类的intern()方法)。

7. 直接内存

直接内存并不是JVM运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是也会OOM。

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

在这里插入图片描述

直接内存不会受到Java堆大小的限制,但会受到物理内存的限制,所以动态扩展时也可能会出现OOM。

总结:
1.除了程序计数器,其他所有内存区域都会发生OOM;
2.java运行进程的内存占用情况
在这里插入图片描述

发布了11 篇原创文章 · 获赞 0 · 访问量 611

猜你喜欢

转载自blog.csdn.net/fei1234456/article/details/105037544