JVM内存结构

188 阅读11分钟

JVM管理的内存主要包括以下几个运行时区域:PC寄存器(程序计数器)、Java虚拟机栈、本地方法栈、Java堆、方法区以及运行时常量池。

PC寄存器

程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。

为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。

如果线程执行 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是 Native 方法,计数器值为Undefined。

程序计数器不会发生内存溢出(OutOfMemoryError即OOM)问题。

Java虚拟机栈

Java虚拟机栈也是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)。 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常(当前大部分JVM都可以动态扩展,只不过JVM规范也允许固定长度的虚拟机栈)。Java虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时会创建一个栈帧。

栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的java虚拟机栈的栈元素。栈帧存储了方法的局部变量表操作数栈动态链接方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。

在编译程序代码的时候,栈帧中需要多大的局部变量表内存,多深的操作数栈都已经完全确定了。因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。

执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

1、局部变量表

栈帧中,由一个局部变量表存储数据。局部变量表中存储了基本数据类型(boolean、byte、char、short、int、float、long、double)的局部变量(包括参数)、和对象的引用(String、数组、对象等),但是不存储对象的内容(不考虑JIT优化)。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。

局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。对于64位的数据类型(long、double),JVM 会为其分配两个连续的变量槽来存储。以下简称 Slot 。

JVM 通过索引定位的方式使用局部变量表,索引的范围从0开始至局部变量表中最大的 Slot 数量。普通方法与 static 方法在第 0 个槽位的存储有所不同。非 static 方法的第 0 个槽位存储方法所属对象实例的引用。

Slot复用: 为了尽可能的节省栈帧空间,局部变量表中的 Slot 是可以复用的。方法中定义的局部变量,其作用域不一定会覆盖整个方法。当方法运行时,如果已经超出了某个变量的作用域,即变量失效了,那这个变量对应的 Slot 就可以交给其他变量使用,也就是所谓的 Slot 复用。

Slot 复用虽然节省了栈帧空间,但是会影响垃圾回收行为。如局部变量占用较多的Slot,执行完该局部变量的作用域后没有对Slot赋值或者清空设置null值,垃圾回收器便不能及时的回收该内存。

2、操作数栈

操作数栈是一个后进先出栈。操作数栈的元素可以是任意的Java数据类型。方法刚开始执行时,操作数栈是空的,在方法执行过程中,通过字节码指令对操作数栈进行压栈和出栈的操作。通常进行算数运算的时候是通过操作数栈来进行的,又或者是在调用其他方法的时候通过操作数栈进行参数传递。操作数栈可以理解为栈帧中用于计算的临时数据存储区。

一个方法调用另外一个方法时,可以通过操作数栈来进行方法参数的传递。虽然在Java虚拟机规范中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但是在大多Java虚拟机的实现时,都会进行一些优化:两个不同方法的栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样做不仅节约了一些内存空间,更重要的是在进行方法调用时就可以直接共用一部分数据,不需要进行额外的参数复制和传递。

3、动态链接

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

4、方法返回地址

方法返回时可能需要在栈帧中保存一些信息,用来于恢复调用者(调用当前方法的方法)的执行状态。一般来说,方法正常退出时,调用者的程序计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。

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

5、附加信息

在Java虚拟机规范中,允许Java虚拟机增加一些规范里没有描述的信息到栈帧之中,比如:调试、性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现。

本地方法栈

与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。

Java堆

也叫做GC堆,是java虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,在JVM启动时创建。该内存区域存放了对象实例及数组(所有new的对象)。其大小通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。

由于现在收集器都是采用分代收集算法,堆被划分为新生代和老年代。年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代GC(Minor GC)任然存活的对象。

当年轻内存占满后,会触发Minor GC,清理年轻代内存空间。

老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发Full GC。

注:Full GC是清理整个堆空间,包括年轻代和老年代。如果Full GC之后,堆中仍然无法存储对象,就会抛出OutOfMemoryError异常。

参数 描述
-Xms 堆内存初始大小
-Xmx(MaxHeapSize) 堆内存最大允许大小,一般不要大于物理内存的80%
-XX:NewSize(-Xns) 年轻代内存初始大小
-XX:MaxNewSize(-Xmn) 年轻代内存最大允许大小,也可以缩写
-XX:NewRatio 新生代和老年代的比值值为4 表示新生代:老年代=1:4,即年轻代占堆的1/5
-XX:SurvivorRatio=8 年轻代中Eden区与Survivor区的容量比例值,默认为8表示两个Survivor :eden=2:8,即一个Survivor占年轻代的1/10
-XX:+HeapDumpOnOutOfMemoryError 内存溢出时,导出堆信息到文件
-XX:+HeapDumpPath 堆Dump路径 -Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump
-XX:OnOutOfMemoryError 当发生OOM内存溢出时,执行一个脚本-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p%p表示线程的id pid
-XX:MaxTenuringThreshold=7 表示如果在幸存区移动多少次没有被垃圾回收,进入老年代

方法区

方法区同 Java 堆一样是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中。常量池是方法区的一部分。

注:JDK1.8 使用 元空间 MetaSpace 替代方法区,元空间并不在 JVM中,而是使用本地内存。元空间两个参数:

MetaSpaceSize:初始化元空间大小,控制发生GC阈值

MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存

常量池

常量池中存储编译器生成的各种字面量和符号引用。字面量就是Java中常量的意思。比如文本字符串,final修饰的常量等。符号引用则包括类和接口的全限定名,方法名和描述符,字段名和描述符等。

常量池避免了频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

注:JDK1.8 常量池在堆里。

直接内存

直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。在JDK 1.4中引入的NIO中,引入了一种基于Channel和Buffer的I/O方式,他可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的应用进行操作。


blog.csdn.net/rongtaoup/a… zhuanlan.zhihu.com/p/102065453 www.hollischuang.com/archives/80 www.hollischuang.com/archives/38…