优秀的编程知识分享平台

网站首页 > 技术文章 正文

JVM内存布局(jvm内存区域划分)

nanyue 2024-10-28 16:41:36 技术文章 4 ℃

经典JVM内存布局(JDK8以上)

一、Heap(堆区)

Heap存储着几乎所有的实例对象,堆由垃圾收集器自动回收,堆区由各个子线程共享使用。 通常情况下,它占用的空间是所有内存区域中最大的,同时也是OOM(Out Of Memory)故障最主要的发源地。

堆的内存空间既可以固定大小,也可以在运行时动态地调整。

通过如下参数设置初始值和最大值,比如-Xms256M -Xmx1024M (-X表示它是JVM运行参数,ms是memory start(最小堆容量)的简称,mx是memory max(最大堆容量)的简称)。由于服务器在不断运行过程中,堆空间不断地扩容与回缩,势必形成不必要的系统压力,所以在线上生产环境中,JVM的Xms Xmx 设置成一样大小,避免在GC调整堆大小时带来的额外压力。

堆分为两大块:新生代和老年代。 对象产生之初在新生代,步入暮年进入老年代,但是老年代也接纳在新生代无法容纳的超大对象。

新生代 = 1个Eden区 + 2个Survivor区。 绝大部分对象在Eden区生成,当Eden区填满的时候,后触发Young Garbage Collection(YGC)。垃圾回收的时候在Eden区实现清除策略,没有被引用的对象则直接回收。仍然存活的对象会被移送到Survivor区。Survivor区被分为S0和S1两块内存空间,每次YGC的时候,他们将存活的对象复制到未使用那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。如果YGC要移送的对象大于Survivor区容量的上限,则直接移交给老年代。

每个对象都有一个计数器,每次YGC都会加1。-XX:MaxTenuringThreshold参数能配置计数器的值到达某个阈值的时候,对象从新生代晋升到老年代。默认值是15,可以在Survivor区交换14次之后,晋升至老年代。 如果参数配置为1,那么从新生代的Eden区直接移至老年代。

如果Survivor区无法放下,或者超大对象的阈值超过上限,则尝试在老年代进行分配;如果老年代也无法放下,则会触发Full Garbage Collection,即FGC。如果依然无法放下,则抛出OOM。可设置参数 -XX:+HeapDumpOnOutOfMemoryError,让JVM遇到OOM时输出堆内信息。

二、方法区

  • 方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 元空间的前身为永久代(Perm),在JDK8之后被淘汰,在JDK7及之前的版本中,只有Hotspot才有Perm区,译为永久代,它在启动的时候固定大小,很难进行调优,并且FGC时会移动类元信息。
  • 区别于永久代,元空间在本地内存中分配。在JDK8里,Perm区中的所有内容中 字符串常量移至堆内存,其他内容包括类元信息,字段,静态属性,方法,常量等移动至元空间内。

三、JVM Stack(虚拟机栈)

  • java虚拟机栈是线程私有的,生命周期与线程相同。
  • 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存储局部变量表,操作数栈,动态连接,方法出口等信息。每个方法从调用到执行完成的过程,都对应着一个栈帧在虚拟机栈入栈到出栈的过程。
  • Java虚拟机规范中对这个区域规定两种异常情况:
  1. OutOfMemoryError(在虚拟机栈可以动态扩展的情况下,扩展时无法申请到足够的内存);
  2. StackOverflowError(线程请求的栈深度 > 虚拟机所允许的深度);
  • 虚拟机栈通过压栈和出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上。在执行过程中,如果出现异常,会进行异常回溯,返回地址通过异常处理表确定。

1. 局部变量表

局部变量表是存放方法参数和局部变量的区域。相对于类属性变量的准备阶段和初始化阶段,局部变量没有准备阶段,必须显式化初始化。如果是非静态方法,则在index[0]位置上存储的是方法所属对象的实例引用,随后存储的是参数和局部变量。

2. 操作栈

操作栈是一个初始状态为空的桶式结构栈。JVM的执行引擎是基于栈的执行引擎,其中的栈指的就是操作栈。字节码指令集的定义都是基于栈类型的,栈的深度在方法元信息的stack属性中。

3. 动态连接

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

4. 方法返回地址

方法执行有两种退出情况:第一,正常退出;第二,异常退出。无论哪种退出情况,都将返回至方法当前被调用的位置。方法退出相当于弹出当前栈帧。

退出的三种方式:

  • 返回值压入上层调用栈帧
  • 异常信息抛给能够处理的栈帧
  • PC计数器指向方法调用后的下一条指令

四、本地方法栈

  • 本地方法栈描述的是Native方法执行的内存模型
  • 可能抛出的异常:与 Java 虚拟机栈一样。

本地方法栈在JVM内存布局中,也是线程对象私有的,但是虚拟机栈“主内”,而本地方法栈“主外”。本地方法栈为Native方法服务。

线程开始调用本地方法时,会进入一个不再受JVM约束的世界。本地方法可以通过JNI(Java Native Interface)来访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和JVM相同的能力和权限。当大量本地方法出现时,势必会削弱JVM对系统的控制力,因为他的出错信息比较黑盒。对于内存不足的情况,本地方法栈还是会抛出native heap OutOfMemory。

五、程序计数器(Program Counter Register,PC)

  • 程序计数器是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。
  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。
  • JVM多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,所以,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程互不影响,独立存储,线程私有。
  • 线程执行Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址。如果执行Native方法,这个计数器值为空(Undefined)。
  • 此区域是唯一一个JVM规范中没有规定任何OOM(OutOfMemoryError)情况的区域。
  • 每个线程创建后,都会产生自己的程序计数器和栈帧,程序计数器都用来存放执行指令的偏移量和行号指示器等,线程执行和恢复都要依赖程序计数器。程序计数器在各个线程之间互不影响,此区域也不会发生内存溢出异常。

从线程共享的角度来看,堆和元空间是所有线程共享的,而虚拟机栈、本地方法栈、程序计数器是线程内部私有的。

Tags:

最近发表
标签列表