内存区域划分
- Java虚拟机所管理的内存区域分为如下部分:方法区、GC堆、虚拟机栈、本地方法栈、PC程序计数器。
-
其中方法区、GC堆是所有线程共享的;虚拟机栈、本地方法栈、PC程序计数器是各个线程独占的。
image.png
PC程序计数器
- 程序计数器可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。每条线程都一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
- 如果线程正值执行的是一个java方法,这个计数器记录的是正在执行的的虚拟机字节码指令的地址;如果正在执行的是native方法,这个计数器则为空(undefined)。
- 此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
java虚拟机栈
- 与程序计数器一样,java虚拟机栈也是线程私有的,它的生命周期和线程相同。
- 虚拟机栈描述的是java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧 用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
下面我们详细介绍下虚拟机栈的栈帧,包括局部变量表、操作数栈、动态链接、方法出口四部分。
image.png
符号引用和直接引用
- 符号引用即用用字符串符号的形式来表示引用,其实被引用的类、方法或者变量还没有被加载到内存中。而直接引用则是有具体引用地址的指针,被引用的类、方法或者变量已经被加载到内存中。
- 符号引用要转换成直接引用才有效,这也说明直接引用的效率要比符号引用高。那为什么要用符号引用呢?这是因为类加载之前,javac会将源代码编译成.class文件,这个时候javac是不知道被编译的类中所引用的类、方法或者变量他们的引用地址在哪里,所以只能用符号引用来表示。
- 我们都知道,类加载过程分为加载—>验证—>准备—>解析—>初始化这 5个阶段,符号引用转换为直接引用就发生在解析阶段,解析阶段可能在初始化前,也可能在初始化之后.
局部变量表
- 局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
- 在编译为Class文件时就在方法的code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量
- 一个Slot变量槽可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有boolean、byte、char、short、float、reference和returnAddress8种类型。第7种reference类表示对一个对象实例的引用。
- 如果是实例方法(非static的方法),那么局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用"this"。其余参数则按照参数表的顺序来排列。比如方法method(int a1,inta2),局部变量表索引0、1、2则分别存储了this指针、a1、a2,如果方法内部有其他内部变量,则在局部变量表中存在a2之后的位置。
- 为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。
- 局部变量不像的类成员变量那样存在"准备阶段"。我们知道类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始值。因此,即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样,如果一个局部变量定义了但没有赋初始值是不能使用的,不要认为Java中任何情况下都存在诸如整型变量默认为0,布尔型变量默认为false等这样的默认值。
操作数栈
- 操作数栈(Operand Stack)也常称为操作栈,它是一个后人先出(Last In First out,LIFO)栈。
- 同局部变量表一样,操作数栈的最大深度也在编译的时候写人到code属性的max_stacks数据项中。
- 当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写人和提取内容,也就是出栈/入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。举个例子,整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存人了两个int型的数值,当执行这个指令时,会将这两个int值出栈并相加,然后将相加的结果入栈
- Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。如果当前线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
动态链接
- 每个栈帧都包含一个指向运行常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
- Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
- Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存人口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
方法的返回地址
- 当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,这种退出方法的方式称为正常完成出口。
- 另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。
- 在方法退出之后,需要返回到方法被调用的位置,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。
- 而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压人调用者栈帧的操作数栈中,调整pc计数器的值以指向方法调用指令后面的一条指令等。
本地方法栈
- 本地方法栈是一个后入先出(Last In First Out)栈.
- 本地方法栈与 Java 虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。
- Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。
方法区
- 方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 垃圾收集行为在这个区域是比较少出现的,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
- 方法区存储的类型信息包括(这个类型的完整有效名、这个类型直接父类的完整有效名、这个类型直接接口的一个有序列表、这个类型的修饰符;类型的常量池、域(Field)信息、方法(Method)信息、除了常量外的所有静态(static)变量)
类型的常量池
- jvm为每个已加载的类型都维护一个常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量(string, integer, 和floating point常量)和对类型,域和方法的符号引用。池中的数据项象数组项一样,是通过索引访问的。
- 因为常量池存储了一个类型所使用到的所有类型,域和方法的符号引用,所以在java程序的动态链接中起了核心的作用
运行时的常量池
- 运行时的常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等消息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时的常量池中。一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
- 运行时常量池相对于Class文件常量池的另外一个重要特征就是具备动态性。Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
GC堆
- Java堆是java虚拟机所管理内存中最大的一块。Java堆是被所有线程所共享的一块内存区域,在虚拟机启动的时候创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
- Java堆是垃圾回收器管理的主要区域,因此很多时候也被称为“GC堆”。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
网友评论