美文网首页
当程序运行时,内存是如何进行分配的

当程序运行时,内存是如何进行分配的

作者: JackieZhu | 来源:发表于2022-07-12 17:28 被阅读0次

    Java虚拟机在执行JAVA程序的过程中,会把它所管理的内存划分为不同的数据区域。HelloWorld.java文件被JVM加载到内存的过程如下图:


HelloWorld.java文件被JVM加载到内存的过程

    Jamv中的内存可以划分为若干个不同的数据区域:程序计数器、虚拟机栈、本地方法栈,堆,方法区。

1.1 程序计数器 (Program Counter Register)

    它是虚拟机的一块较小的内存空间,主要用于记录当前线程执行的位置。


CPU切换线程时计数器的的恢复过程

    除恢复操作外,还有分支操作、循环操作,跳转操作,异常处理等也依赖计数品来完成。
注意点:

  1. Java虚拟机规范中,对程序计数器区域没有规定任何OutOfMemoryError情况
  2. 计数器是线程私有的,它的生命周期随线程创建而创建,结束而结束
  3. 计数器记录的是正在执行的虚拟机字节码指令的地址。执行Native时这个计数器的值为空(Undefined)

1.2 虚拟机栈

    虚拟机栈也是私有的,与线程生命周期同步。Java虚拟机规范对这个区域做了两种异常规范:

  1. StackOverflowError:当前线程请求栈深度超出虚拟机栈所允许的深度时抛出
  2. OutOfMemoryError:当前Java虚拟机动态扩展到无法申请足够内存时抛出

    JVM是基于栈的解释器执行的,DVM是基于寄存器解释器执行的。
“基于栈”指的就是虚拟机栈。虚拟机栈的初衷就是用来描述Java方法执行的内存模型,每个方法被执行的时间。JVM会在虚拟机栈中创建一个栈帧

栈帧(Stack Frame)

    栈帧是用于支持虚拟机用于支持方法调用和方法执行的数据结构,每一个线程在执行某个方法时,都会为这个方法创建一个栈帧。
一个线程包含多个栈帧,每一个栈帧都包含局部变量表、操作数栈、动态链接,返回地址等。

栈帧

局部变量表

    局部变量表是变量值的存储空间
    调用方法时传递的参数,以及方法内部创建的局部变量都保存在尽心竭力变量中。在Java编译成class文件的时候,会在方法的Code属性表中的max_locals数据项中确定该方法需要分配的最大局部变量表的容量。
如下代码:

publci static int add(int k){
    int i = 1;
    int j = 2;
    return i+j+k;
}

使用javap -v反编译之后,得到如下字节码指使:

publicstaticintadd(int);
    descriptor:(I)I
    flags:ACC_PUBLIC,ACC_STATIC
    Code:
    stack=2,locals=3,args_size=1
        0:icont_1
        1:istore_1
        2:icont_2
        3:istore_2
        4:iload_1
        5:iload_2
        6:iadd
        7:iload_0
        8:iadd
        9:ireturn

    注意:系统不舍友为局部变量赋予初始值,也就不存在类变量那样的准备阶级。

操作数栈(Operand Stack)

    操作数栈也常称为操作栈,它是一个后入先出栈(LIFO)
操作数栈的最大深度也在编译的时候写入方法的Code属性表中的max_stacks数据项中,栈中的元素可以是任意Java数据类型,包括long和double。
当一个方法刚刚开始执行时,这个方法的操作数栈是空的,在方法执行过程中,会有各种字节码指令被压入和弹出操作数栈。
比如:iadd指令就是将操作数栈栈中栈顶的两个元素弹出,执行加法运算,并将结果重新压回到操作数栈中。

动态链接(Dynamic Linking)

    动态链接主要目的是为了支持方法调用过程中的动态链接(Dynamic Linking),在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其所在内存地址中的直接引用,而符号引用存在于方法区中。
    Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用。持有这个引用的目的就是为了支持方法调用过程中的动态链接(Dynamic Linking)

返回地址

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

  • 正常退出: 指方法中的代码正常完成,或者遇到任意一个方法返回的字节码指令(如return)半退出没有抛出任何异常
  • 异常退出: 指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出

    虚拟机栈中的“返回地址”是用来帮助当前方法恢复它的上层方法执行状态。

  • 正常退出: 调用者的PC计数值可以作为返回地址,栈帧可能保存些计数值。
  • 异常退出: 返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。

实例讲解

public int add(){
    int i=1;
    int j=2;
    int result=i+j;
    return result+10;
}

    javap后的字节码指令如下:

    0: iconst_1     (把常量 1 压入操作数栈栈顶)
    1: istore_1     (把操作数栈栈顶的出栈放入局部变量表索引为 1 的位置)
    2: iconst_2     (把常量 2 压入操作数栈栈顶)
    3: istore_2     (把操作数栈栈顶的出栈放入局部变量表索引为 2 的位置)    
    4: iload_1      (把局部变量表索引为 1 的值放入操作数栈栈顶)
    5: iload_2      (把局部变量表索引为 2 的值放入操作数栈栈顶)
    6: iadd         (将操作数栈栈顶和栈顶下面一个进行加法运算后加入栈顶)
    7: istore_3     (将操作数栈栈顶的出栈放入局部变量表索引为 3 的位置)
    9: bipush_10    (把 10 压入操作数栈栈顶)
    10: iadd        (将操作数栈栈顶和栈顶下面一个进行加法运算后加入栈顶)
    11: ireturn     (结束)

    指令说明:

  • iconst和bipush 这两个指令都是将常量压入操作数栈栈顶。区别在于-1~5 用iconst,-128-127时用bipush
  • istore 把操作数栈栈顶的元素放入局部变量表的某索引位置
  • iload 把局部变量表中某下标下的值加载到操作数栈栈顶
  • iload 代表加法运算,将操作数栈最上方两个元素进行相加操作,然后将结果重新压入栈顶

    Java文件被编译class文件时,栈帧需要多大的局部变量表,多深的操作数栈已经完全确定,并且写入到了方法表的Code属性中。最后在执行return指令后,局部变量表和操作数栈也会相继销毁。

局部变量表与操作数栈

1.3 本地方法栈

    本地方法栈和虚拟机栈基本相同,只不过是针对本地(native)方法。在有些虚拟机的实现中已经二合一了(比如HotSpot)。

1.4 堆(Heap)

    Java堆(Heap)是JVM所管理内存中最大的一块,该区域唯一目的就是存放对象实例,几乎所有对象实例都是堆里分配。因此它也是 Java垃圾收集器(GC) 管理的主要区域,有时候也叫GC堆。同时它也是所有线程共享的内存区域。因此被分配在此区域的对象如果被多个线程访问的话,需要考虑线程安全的问题。

    按对象储存的时间不同,堆中的内存可划分为新生代(Young)老年代(old),其中新生代又被划分为Eden区Survivor区

堆内存的划分

    不同区域存放具有不同生命周期的对象,这样可以针对不同区域使用不同的垃圾回收算法,从而更具有针对性,进而提高垃圾回收效率。

1.5 方法区

    方法区(MethodArea)也是JVM规范里规定的一块运行时数据区。方法区主要是存储已经被JVM加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据。该区域同堆一样,也是被各个线程共享的内存区域。

注意:关于方法区,很多开发者会将其跟“永久区”混淆。二都概念对比如下:

  • 方法区是JVM规范中规定的一块区域,但是并不是实际实现,切忌将规范跟实现混为一谈。不同的JVM厂商可以有不同版本的“方法区”的实现。
  • HostSpot在JDK1.7以前使用“永久区”(或者叫Perm区)来实现方法区,在JDK1.8之后“永久区”就已经被删除了,取而代之的是一个叫作“元空间(metaspace)”的实现方式。

总结:

  • 方法区是规范层面的东西,规定了这一个区域要存放哪些数据。
  • 永久区或者是metaspace是对方法区的不同实现,是实现层面的东西

1.6 异常再现

StackOverflowError 栈溢出异常

    递归调用是造成StackoverflowError的一个常见场景

递归溢出

    在method方法中,递归调用了自身,并且没有设置递归结束条件。运行上述代码时,则会产生StackOverflowError。

溢出

    原因就是每调用一次method方法时,都会在虚拟机中创建出一个栈帧。因为是递归调用,method方法北不会退出,也不会将栈帧销毁,所以必然会导致StackOverflowError。因此当需要用递归时,需要格外谨慎。

OutOfMemoryError

    理论上,虚拟机栈、堆、方法区都有发生OutOfMemoryError的可能。但实际项目中,大多发生于堆当中。

堆内存溢出

    在一个无限循环中,动态的向ArrayList中添加新的HeapError对象。这会不断的占用堆中的内存,当堆内存不够是,必然会产生OutOfMemoryError,也就是内存溢出 异常。

溢出信息

总结

    对于JVM运行时内存布局,上面介绍的这5块内容都是在Java虚拟机规范中定义的规则,这些规则只是描述了各个区域是负责做什么事情、存储什么样的数据、如何处理异常、是否允许线程间共享等。不能将其理解为虚拟机的“具体实现”,虚拟机的具体实现有很多,如:HotSpot、JRocket、IMB J9、以及Android Dalvik和ART。

概括

    总结来说,JVM的运行时内存结构中一共有两个“栈”和一个“堆”,分别是:Java虚拟机栈和本地方法栈,以及“GC堆”和方法区,除此之外还有一个程序计数器。JVM内存中只有堆和方法区是线程共享的数据区域,其它区域都是线程私有的。并且程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

相关文章

网友评论

      本文标题:当程序运行时,内存是如何进行分配的

      本文链接:https://www.haomeiwen.com/subject/cpeabrtx.html