jvm内存结构
JVM内存结构
我们称的内存结构,在虚拟机规范中称为运行时数据区。
看下原文解释:
Java虚拟机定义了在程序执行过程中使用的各种运行时数据区域。其中一些数据区域在Java虚拟机启动时创建,只有在Java虚拟机退出时才被销毁。其他数据区域是每个线程。每个线程的数据区域在创建线程时被创建,并在线程退出时被销毁。
在JVM虚拟机规范中,规范了jvm中逻辑上的内存区域。jvm规范中共有6大块:
- 程序计数器(The pc Register)。
- java虚拟机栈(Java Virtual Machine Stacks)。
- 堆(Heap)。
- 方法区(Method Area)。
- 运行时常量池(Run-Time Constant Pool)。
- 本地方法栈(Native Method Stacks)。
程序计数器(The pc Register)
原文机翻一波:
Java虚拟机可以同时支持许多执行线程(JLS17)。每个Java虚拟机线程都有它自己的pc(程序计数器)寄存器。 在任何时候,每个Java虚拟机线程都在执行一个单一方法的代码,即该线程的当前方法(2.6)。如果该方法不是本机的,则pc寄存器包含当前正在执行的Java虚拟机指令的地址。如果该线程当前要执行的方法是本机的,则未定义Java虚拟机的pc寄存器的值。 Java虚拟机的pc寄存器足够宽,可以在特定的平台上保存一个返回地址或一个本机指针。
当前线程所执行的字节码的行号指示器,它会指出下一条将要执行的指令的地址,字节码解释器就是通过改变计数器的值来选取程序接下来执行的操作。
程序计数器是线程私有的一小块内存,每个线程都有独立的程序计数器,保证线程切换后回到正确的位置。
保存内容:
- 执行的如果是是Native方法,则为空。
- 线程正在执行java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址。
唯一一个不会出现OOM的内存区域。
java虚拟机栈(Java Virtual Machine Stacks)
机翻一波:
每个Java虚拟机线程都有一个私有的Java虚拟机堆栈,它与线程同时创建。 Java虚拟机堆栈可存储帧(2.6)。Java虚拟机堆栈类似于传统语言如C的堆栈:它包含局部变量和部分结果,并在方法调用和返回中发挥作用。 因为Java虚拟机堆栈永远不会被直接操作,除非是推送和弹出帧,所以帧可以被堆分配。Java虚拟机堆栈的内存不需要是连续的。
和程序计数器一样,Java虚拟机栈也是线程私有的,在线程创建时Java虚拟机栈会被创建,每个方法在执行的同时都会创建一个栈帧,用于存放局部变量表,操作数栈,动态链接,方法出口等信息。调用方法时压栈,方法返回时出栈。
- 局部变量表:是存放方法参数和局部变量的区域,存放了各种基本类型。对象引用,returnAddress类型,局部变量表所需空间在编译期间就已经确定并完成分配,在方法运行期间不会被改变。
虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中通过this访问。
- 操作数栈:是个初始状态为空的桶式结构栈。
虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int。
虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的。 - 动态链接:每个栈帧中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态连接。
- 方法出口:
- 正常退出:遇到方法的返回字节码指令。
- 异常退出:
- 栈溢出:StackOverFlowError,如果线程中的计算需要比允许的更大的 Java 虚拟机堆栈。
- 内存溢出:如果 Java 虚拟机堆栈可以动态扩展,并且尝试扩展,但无法提供足够的内存来实现扩展,或者如果无法提供足够的内存来为新线程创建初始 Java 虚拟机堆栈。
堆(Heap)
来一波:
Java虚拟机有一个由所有Java虚拟机线程共享的堆。堆是用来分配所有类实例和数组的内存的运行时数据区域。
该堆是在虚拟机启动时创建的。对象的堆存储由自动存储管理系统(称为垃圾回收器)回收;永远不会显式释放对象。Java虚拟机假设没有特定类型的自动存储管理系统,并且可以根据实现者的系统要求来选择存储管理技术。堆可能是固定的大小,也可以根据计算的要求进行扩展,如果没有必要使用更大的堆,则可以进行收缩。堆的内存不需要是连续的。
本规范允许Java虚拟机堆栈具有固定的大小,或者按照计算的需要动态扩展和收缩。如果Java虚拟机堆栈是固定大小的,则在创建每个Java虚拟机堆栈时,可以独立选择每个Java虚拟机堆栈的大小。
这里是JVM管理的内存里最大的一块,存放对象实例,在虚拟机启动时创建。
根据对象存活的周期不同,JVM把堆内存进行分带管理,由垃圾收集器进行对象的回收管理。
方法区(Method Area)
机翻:
Java虚拟机具有在所有Java虚拟机线程之间共享的方法区域。该方法区域类似于传统语言编译代码的存储区域,或类似于操作系统过程中的“文本”段。它存储每个类的结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括在类和实例初始化和接口初始化中使用的特殊方法(2.9)。
方法区域是在虚拟机启动时创建的。虽然方法区域是在堆的逻辑部分,简单实现可以选择不垃圾收集或压缩。此版本的Java虚拟机规范不强制指定方法区域的位置或用于管理的策略编译的代码。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果方法区域变得更大,则可以收缩没必要。方法区域的内存不需要是连续的。
与堆一样是线程共享的,存储被类加载器加载的类信息,常量,静态变量等,就是编译器编译后的代码等数据。
如果方法区域中的内存无法满足分配请求,则Java虚拟机将抛出OutOfMemoryError。
方法区在jdk1.7和1.8之间,落地的物理实现有了区别。
运行时常量池(Run-Time Constant Pool)
机翻搞起来:
运行时常量池是类文件中constant_pool表的每个类或每个接口的运行时表示形式(4.4)。它包含几种常量,从编译时已知的数字文本到在运行时必须解析的方法和字段引用。运行时常量池提供的函数类似于传统编程语言的符号表的函数,尽管它包含了比典型的符号表更广泛的数据范围。
每个运行时常量池都从Java虚拟机的方法区域(2.5.4)进行分配。类或接口的运行时常量池将在由Java虚拟机创建(5.3)时构建。
通俗来说,运行时常量池用于存放编译期生成的各种字面量和符号引用。并不是编译器才产生常量,运行期间也有可能将新的常量放入常量池。
创建类或接口时,如果构造运行时常量池需要的内存超过Java虚拟机的方法区域可以提供的内存,则Java虚拟机将抛出OutOfMemoryError。
constant_pool表,指的是类在编译过程中生成的常量表。
本地方法栈(Native Method Stacks)
机翻再来:
Java虚拟机的实现可以使用传统的堆栈,俗称“C堆栈”,来支持本地方法(用Java编程语言以外的语言编写的方法)。本地方法堆栈也可以使用解释器的实现Java虚拟机的指令集的语言如C.Java虚拟机实现不能加载本地方法,本身不依赖于传统的栈不需要提供本地方法堆栈。如果提供了这些,则通常在创建每个线程时为每个线程分配本机方法堆栈。
本规范允许本地方法堆栈具有固定的大小,或者按照计算的要求动态地扩展和收缩。如果本机方法堆栈的大小是固定,则在创建每个本机方法堆栈时,可以独立选择每个本机方法堆栈的大小。
本地方法栈与虚拟机栈类似,不过本地方法栈保存本地方法。
如果线程中的计算需要比允许的更大的本机方法堆栈,则Java虚拟机将抛出StackOverFlowError。
如果可以动态扩展本机方法堆栈,并且尝试进行本机方法堆栈扩展,但可用内存不足,或者如果没有足够的内存可以为新线程创建初始本机方法堆栈,则Java虚拟机将抛出OutOfMemoryError。
总结
JVM规范中,逻辑上就规范了这6中内存区域,但并没有规范实际的虚拟机该如何实现,不同的虚拟机可以安照该规范有不同的实现,相同虚拟机不同版本也可能有不同的实现,按照规范即可。
规范中规定了一些内存区域是线程共享的,这种区域就随着虚拟机的创建销毁而进行分配回收,而一些线程独享的内存区域就随着线程的创建销毁而分配回收。
- 线程共享的区域:
- 堆
- 方法区(非堆)
- 运行时常量池
- 线程独享的区域:
- java虚拟机栈
- 本地方法栈
- 程序计数器