标签搜索

目 录CONTENT

文章目录

JVM 之 内存管理以及内存区域划分

沙漠渔
2022-08-29 00:19:52 / 0 评论 / 0 点赞 / 1,375 阅读 / 4,167 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-08-29,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

Java的内存管理不像C/C那样具有至高无上的权力,对于Java来说,VM实现了GC(垃圾回收)机制,在我们每次创建java对象的时候,就不用去写配对的delete方法(在C中称之为析构函数),不容易出现内存泄漏和内存溢出的情况,有虚拟机去进行内存管理。

这样看起来非常的不错,开发人员不用再次去每次的手动的管理内存,但是也正是因为内存交给了虚拟机管理,这样的话,一旦出现内存的问题,如果不了解虚拟机的内存管理机制,那么对于这一类的问题我们一般无从下手去处理.

0、概述

VM在程序运行的时候,会将内存划分为若干的区域。这些区域都有自定的用途,有些内存区域在VM启动的时候就会创建,有些内存区域则是在线程启动的时候则会创建出来,根据《Java虚拟的规范SE7》的规定,VM会将内存划分为以下区域.

JVM的内存区域.png

每个区域的作用如下:

内存区域名称线程隔离作用
程序计数器线程字节码执行位置指示器
Java虚拟机栈线程执行方法的变量等信息
本地方法栈未严格规定线程执行Native方法的信息
Java堆存储所有对象的实例和数组
方法区

1、程序计数器

程序计数器(Program Counter Register,简称为PC,如异常表中的handler_pc) 是一块较小的内存区域。可以理解为记录每个线程的执行指令的行号指示器,在一些分支操作,如: 循环,判断,分支等操作,程序的下一条指令就是通过当前线程的程序计数器找到的。

在单核CPU中执行多线程的操作中,我们线程的执行与否都是由CPU划分时间片决定的,当CPU从线程A切换到线程B执行后,为了能恢复到正确的指令位置,所以需要从线程B的程序计数器中读取线程B的执行指令位置。所以说程序执行器是属于线程隔离的,也就是线程私有的内存区域。

在VM执行的是一个Java的方法的时候,程序计数器的值是当前执行虚拟机字节码的的指令地址;若VM执行的是Native方法,那么这个计数器的值是空(Underfined)(注意不是0)。此内存的区域是唯一一个在JAVA虚拟机规范中没有规定任何 OOM 异常的内存区域。

2、Java栈(Java虚拟机栈)

Java栈有人也称之为Java虚拟机栈(Java Virtual Machine Stacks ) ,和程序计数器一样,也是线程私有的内存区域。其生命周期和线程一致,线程创建,Java虚拟机栈也就创建,反之销毁也亦然.

线程在执行的时候都有创建一个栈帧(Stack Frame),用于存储局部变量表、操作数等信息,每个方法的执行也就意味着Java栈帧的入栈和出栈的过程。在一些文章中,有人喜欢把Java的内存区域大致分为堆内存(Heap)和栈内存(Stack),这种分发比较粗糙,实际的划分远比这复杂。同时也在侧面的说明,开发人员最关注的的也就是堆内存和栈内存。堆内存会在后面详细阐述, 这里讲的栈内存其实指的就是Java虚拟机栈,或者说指的是Java虚拟机栈的村部变量表部分。

局部变量表中存放了 编译期可知的基本类型(boolean,byte,char, short, int, float, long, double)、对象引用地址、也可能是指向代表某个对象的句柄以及returnAddress类型(指向了一条字节码指令的地址)**。

其中,64位长度的long和double类型的数据会占用2个局部变量的变量空间(Slot),其他类型均占用一个。也就是说局部变量的所占的内存空间,在编译时期也就确定下来,所以在运行时,一个线程的Java虚拟机栈占用多少内存空间是确定的,在方法运行期间不会改变这个大小。

在Java虚拟机规范中,对这个区域规定了两种异常情况:

  • 如果线程请求的栈的深度大于虚拟机所允许的最大栈深度,将会抛出StackOverFlowError异常,也就是平常所说的栈内存溢出异常
  • 如果Java虚拟机栈的内存可以扩展(Java虚拟机规范并没有对此内存区域是否可扩展做严格的规定,因此当前大部分VM都是允许拓展这一块的内存区域),当拓展无法申请到内存时,就会抛出OutOfMemoryError异常
  • Slot是可以被复用的,因此10个局部变量并不代表一定会有是个10个Slot

3、本地方法栈

本地方法栈和Java虚拟机栈类似,其区别在于Java虚拟机栈记录的是Java方法执行的信息,而本地方法栈为虚拟机使用Native方法服务(Natvie方法指的是使用C或者C++实现的代码)。同样的《Java虚拟机规范》中,也并未对这一块区域做严格限制,具体可由虚拟机自由实现。甚至有部分虚拟机(比如Sun HotSpot VM) 直接把本地方法栈和Java虚拟机栈合并。和Java虚拟机栈异常,本地方法栈也会抛出栈内存溢出异常以及内存溢出异常

4、Java堆

Java堆内存区域,是VM所管理的内存中最大的一块内存,属于线程共享的内存区域,在虚拟机启动的时候创建。其主要用于存放所有类的实例对象以及数组对象(The heap is the runtime data area from which memory for all class instances and arrays is allocated) 随着JIT编译器和逃逸分析技术的逐渐成熟,栈上分配(OSR)和标量替换等优化技术,也使得所有对象都在Java堆上配置不是那么的绝对了,关于逃逸分析,请参考第11章节的内容。
Java堆是VM中的垃圾收集器的主要工作区域,也因此这块内存区域也被称之为GC堆。由于现代的收集器基本采用分代回收机制,为了更好分回收内存或者更快分配内存,可以把Java堆内存细分为:新生代和老年代,甚至在细致一些,Eden空间、From Survivor空间、以及To Survivor空间。根据《Java虚拟机规范》Java堆可以是物理上不连续的内存空间,仅逻辑连续即可。在Java堆内存的是线上,可以允许拓展,也可以不允许拓展,现代主流的VM都实现了动态分配Java堆内存,可通过-Xmx和-Xms控制。如果堆内存没有完成内存分配并且堆内存也无法拓展的时候吗,会抛出OutOfMemoryError 异常,也就是我们常说的OOM。

5、方法区

方法区域和Java堆一样,也是各个线程共享的内存区域。它用于存储虚拟机加载的的信息、常亮、静态变量以及JIT编译后的代码等数据。在《Java虚拟中规范》中方法区属于Java堆的一个逻辑部分,但是为了和Java堆区分开来,方法区也称之为非堆(Non-Heap)。在HotSpot VM中,很多人更愿意把方法区称之为永久代,但从本质上说两者并不等价,仅是因为HotSpot的团队将垃圾回收收集器的工作范围拓展到方法区,将方法区作为永久代来执行垃圾回收而已,这样省去了专门为其开发回收器的工足量,但是在一些不存在永久代概念的虚拟机中,这种方式并不会是一个好主意。但是方法区的实例并非真的永久,比如 class文件的卸载等,就会导致进行垃圾回收, 之前就出现过因为方法区的内存回收的问题导致出现内存泄漏的BUG,从JDK1.8 开始就不在存在永久代的概念,而是使用元空间的方式实现(MetaData)。根据规范说明,当方法区无法满足内存分配的时候就会出现OutOfMemoryError的异常。

5.1 运行常量池

运行常量池(Runtime Constant Pool)属于方法区的一部分,保存着在编译器生成的各种字面量(CONSTANT_UTF8)和符号引用,这部分数据将在类加载后进入运行常量池中。Java虚拟机栈对Class文件的每一部分都有严格的规定,Class文件中每个字节存储什么内容都必须符合规范才能被虚拟机认可、装载并执行,但是对于运行时常量池并没有做任何细节的要求,不同的VM可以自由实现这一内存区域。运行时常量池也具有动态性,并非仅有预制在Class文件中的常量池的内容才可以进入此区域,在运行时候也可以将新的常亮置于运行时常量池中,比如String类的intern()方法。 一般来说,除了Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。当运行时常亮无法申请到内存的时候也会抛出OutOfMomeryError异常

5.2 直接内存(堆外内存)

直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中的定义的内存区域,但是这部分内存区域被频繁的使用,也有可能出现OOM异常, 直接内存的管理和操作其成本也是略高的。在Java1.4中,新加入了NIO类(New Input/Output),引入了一种基于通道(Channle)和缓冲区(Buffer)的IO方式,他可以使用Native函数直接分配内存,然后通过存储早Java堆中的DirectByteBuffer对象来进行操作。同样的只要是内存,都会受到RAM以及SWAP或者分页文件以及CPU寻址的限制,所以直接内存也会出现OOM异常

0
广告 广告

评论区