浅析虚拟机内存管理模型

Java虚拟机在执行Java程序的过程当中会把Java程序所管理的内存划分为若干个不一样的数据区域,这些区域能够划分为5各部分:虚拟机栈、堆、方法区、本地方法栈、程序计数器,如图:java

2002319-20210203210949922-1046773488

虚拟机栈

Java虚拟机栈(Java Virtual Machine Stack)是线程私有的,它的生命周期与线程相同。也就是,每一个方法被执行的时候,Java虚拟机都会同步建立一个栈帧 (Stack Frame)用于存储局部变量表、操做数栈、动态链接、方法出口等信息。每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。下面讲解一下虚拟机栈中的内容:数组

2002319-20210203211037086-2046105052

局部变量表

局部变量表存放了编译期可知的各类Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象自己,多是一个指向对象起始地址的引用指针,也多是指向一个表明对象的句柄或者其余与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。缓存

这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其他的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法须要在栈帧中分配多大的局部变量空间是彻底肯定的,在方法运行期间不会改变局部变量表的大小。请读者注意,这里说的“大小”是指变量槽的数量,虚拟机真正使用多大的内存空间(譬如按照1个变量槽占用32个比特、64个比特,或者更多)来实现一个变量槽,这是彻底由具体的虚拟机实现自行决定的事情。安全

returnAddress类型目前已经不多见了,它是为字节码指令jsr、jsr_w和ret服务的,指向了一条字节码指令的地址。虽然long以及double是分配在两个变量槽中,可是因为在线程内部,因此不会有数据竞争和线程安全问题。多线程

操做数栈

操做数栈(Operand Stack)也常被称为操做栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表同样,操做数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。当一个方法刚刚开始执行的时候,这个方法的操做数栈是空的,在方法的执行过程当中,会有各类字节码指令往操做数栈中写入和提取内容,也就是出栈和入栈操做。譬如在作算术运算的时候是经过将运算涉及的操做数栈压入栈顶后调用运算指令来进行的,又譬如在调用其余方法的时候是经过操做数栈来进行方法参数的传递。举个例子,例如整数加法的字节码指令iadd,这条指令在运行的时候要求操做数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会把这两个int值出栈并相加,而后将相加的结果从新入栈。函数

写个小案例:spa

package com.courage;
public class DeOperandStack {
    public static void main(String[] args) {
        int i = 1;
        int j = 2;
        int k = i + j;
    }
}

此时DeOperandStack类中只有一个线程(main),局部变量表中拥有的变量:线程

默认args为0号变量,因此这个线程中有四个局部变量,那么是如何利用操做数栈进行加减的呢?指针

首先将第一个常数压入栈,而后存储局部变量表1号变量,而后将第二个常数压入栈,而后存储局部变量表2号变量,而后将局部变量表1,2两个数值加载进栈,弹出相加以后将结果压入栈,在将栈顶数据存储到3号变量。code

动态链接

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

方法出口

当一个方法开始执行后,只有两种方式退出这个方法:

  • 遇到方法返回的字节码指令
  • 遇到了异常

第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为“正常调用完成”(Normal Method Invocation Completion)。

另一种退出方式是在方法执行的过程当中遇到了异常,而且这个异常没有在方法体内获得妥善处理。不管是Java虚拟机内部产生的异常,仍是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会致使方法退出,这种退出方法的方式称为“异常调用完成(Abrupt Method Invocation Completion)”。

一个方法使用异常完成出口的方式退出,是不会给它的上层调用者提供任何返回值的。不管采用何种退出方式,在方法退出以后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能须要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。

Java堆是被全部线程共享的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,Java
世界里“几乎”全部的对象实例以及数组都在这里分配内存。

从分配内存的角度看,全部线程共享的Java堆中能够划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提高对象分配时的效率。不过不管从什么角度,不管如何划分,都不会改变Java堆中存储内容的共性,不管是哪一个区域,存储的都只能是对象的实例,将Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存。

方法区

方法区(Method Area)与Java堆同样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

和Java堆同样不须要连续的内存和能够选择固定大小或者可扩展外,甚至还能够选择不实现垃圾收集,这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,通常来讲这个区域的回收效果比较难使人满意,尤为是类型的卸载,条件至关苛刻,可是这部分区域的回收有时又确实是必要的,此区域未彻底回收会致使内存泄漏。

方法区、永久代、元空间的关系

之因此将这三个放一块儿,是这儿很容易混淆,对于Hotspot虚拟机,JDK六、JDK7 时方法区是 PermGen(永久代),JDK8 时,方法区是 Metaspace(元空间),怎么回事呢?

方法区 是JVM的规范,全部虚拟机必须遵照的。常见的JVM虚拟机Hotspot 、JRockit(Oracle)、J9(IBM)

PermGen space则是 HotSpot 虚拟机 基于 JVM 规范对 方法区 的一个落地实现, 而且只有 HotSpot 才有 PermGen space。而如 JRockit(Oracle)、J9(IBM) 虚拟机有 方法区 ,可是就没有 PermGen space。

PermGen space 是JDK7及以前, HotSpot 虚拟机 对 方法区 的一个落地实现,在JDK8被移除。

Metaspace(元空间)是 JDK8及以后, HotSpot 虚拟机对方法区 的新的实现。

永久代以及元空间,能够用来存放堆中存活好久的对象。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存

类信息

每个类有一个Class对象,编译期生成,保存在同名的.class文件中。这些Class对象包含了这个类型的父类、接口、构造函数、方法、属性等详细信息,这些class文件在程序运行时会被ClassLoader加载到JVM中,在JVM中就表现为一个Class对象,JVM使用该Class对象建立该类的全部常规对象,而这个对象的信息则保存在方法区的类信息中。

常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各类字面量与符号引用,这部份内容将在类加载后存放到方法区的运行时常量池中。既然运行时常量池是方法区的一部分,天然受到方法区内存的限制,当常量池没法再申请到内存时会抛出OutOfMemoryError异常。

静态变量区

静态变量也叫类变量,类的全部实例都共享,这个区专门存放静态变量和静态块。

static 修饰的 在JVM运行时就加载到内存中了 因此不须要实例类。

静态变量在类加载的准备阶段分配内存并设置类变量初始值的阶段,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区自己是一个逻辑上的区域,在JDK 7及以前,HotSpot使用永久代来实现方法区时,实现是彻底符合这种逻辑概念的;而在JDK 8及以后,类变量则会随着Class对象一块儿存放在Java堆中,这时候“类变量在方法区”就彻底是一种对逻辑概念的表述了,关于这部份内容,笔者已在4.3.1节介绍而且验证过。

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它能够看做是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工做时就是经过改变这个计数器的值来选取下一条须要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计数器来完成。

因为Java虚拟机的多线程是经过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个肯定的时刻,一个处理器(对于多核处理器来讲是一个内核)都只会执行一条线程中的指令。所以,为了线程切换后能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程私有”的内存。

本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的做用是很是类似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

相关文章
相关标签/搜索