美文网首页Java基础
JVM(一) 程序运行时内存分配

JVM(一) 程序运行时内存分配

作者: Timmy_zzh | 来源:发表于2020-11-07 10:22 被阅读0次
    1. 程序运行过程中,java文件的执行流程
    2. 内存分配区域:程序计数器,虚拟机栈,本地方法栈,堆,方法区
    3. 总结

    java虚拟机在执行java程序的过程中,会把它所管理的内存划分为不同的数据区域,下图描述了一个.java文件被JVM加载到内存中的过程

    • HelloWorld.java文件首先需要经过编译器编译,生成HelloWorld.class字节码文件

    • Java程序中访问到HelloWorld这个类时,需要通过ClassLoader(类加载器)将HelloWorld.class加载到JVM的内存中

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

    java文件加载运行过程.png

    1.程序计数器

    本质:程序计数器 是虚拟机中一块较小的内存空间,主要用于记录当前线程执行的位置

    • 背景

      • Java程序时多线程的,CPU在多个线程中分配执行时间片段(RR调度算法)。

      • 当一个线程被CPU挂起时,需要记录代码已经执行到的位置,当CPU重新执行此线程时,需要知道从那行指令继续执行,这就是程序计数器的作用

    • 注意点

      • 在Java虚拟机规范中,对程序计数器这一区域没有规定任何OutOfMemoryError情况(或许是感觉没有必要把)

      • 程序计数器是线程私有的,每条线程内部都有一个私有程序计数器。它的生命周期随着线程的创建和结束。

      • 当一个线程正在执行一个java方法的时候,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,这个计数器值则为空(Undefined)

    2.虚拟机栈

    • 经常听到JVM是基于栈的解释器执行的,DVM是基于寄存器解释器执行的,这里的“基于栈”值得就是虚拟机栈

    • 虚拟机栈设计的初衷是用来描述Java方法执行的内存模型,每个方法被执行的时候,JVM都会在虚拟机栈中创建一个栈

    • 虚拟机栈也是线程私有的,与线程的生命周期同步,在jvm规范中,对这个区域规定了两种异常状况:

      • StackOverflowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出

      • OutOfMemoryError:当jvm动态扩展到无法申请足够内存时抛出

    栈帧

    栈帧时用于支持虚拟机进行方法调用和方法执行的数据结构,线程中执行某一个方法时,方法内部可能会调用其他的方法,每调用一个方法,都会为这个方法创建一个栈帧

    • 可以这样理解,一个线程包含多个栈帧,每个栈帧内部包含局部变量表操作数栈动态连接返回地址
    2.虚拟机栈结构.png
    1. 局部变量表

      局部变量表是变量值的存储空间,我们调用方法时传递的参数,以及在方法内部创建的局部变量都保存在局部变量表中。

      • 在Java文件编译成class文件时候,就会在方法的Code属性表中的max_locals数据项中,确定该方法需要分配的最大局部变量表的容器,代码如下
     public static int add(int k) {
         int a = 1;
         int b = 2;
         return k + a + b;
         }
    

    先使用javac 编译成.class文件,再使用javap -v 反编译得到字节码如下:

     public static int add(int);
         descriptor: (I)I
         flags: ACC_PUBLIC, ACC_STATIC
         Code:
         stack=2, locals=3, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_0
         5: iload_1
         6: iadd
         7: iload_2
         8: iadd
         9: ireturn
         LineNumberTable:
         line 10: 0
         line 11: 2
         line 12: 4</pre>
    
    • 上面反编译的locals就是代表局部变量表长度为3,也就是经过编译后,局部变量表的长度已经确定为3,分别保存:入参k和局部变量i,j

    • 注意:系统不会为局部变量赋予初始值

    1. 操作数栈

      操作数栈也被称为操作栈,是一个后入先出栈,用于局部变量表中数据进行计算的场所。

      • 当一个方法刚刚开始执行的时候,这个方法的操作数栈式空的,当方法执行的过程中,会有各种字节码指令被压入和弹出操作数栈

      • 比如上面的 iadd指令就是将操作数栈中栈顶的两个元素弹出,执行加法运算,并将结果重新压入到操作数栈中

      • 同局部变量表一样,操作数栈的最大深度也在编译的时候写入方法的Code属性表中的max_stacks数据项中。栈中的元素可以式任意java数据类型,包括long和double

    2. 动态连接

      • 动态链接的主要目的是为了支持方法调用过程中的动态链接

      • 在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其所在内存地址中的直接引用,而符号引用存在于方法区中

    3. 返回地址

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

        • 正常退出:指方法中的代码正常完成,或者遇到任意一个方法返回的字节码指令(如return)并退出,没有抛出任何异常

        • 异常退出:指方法执行过程中遇到异常,并且这个异常再方法体内部没有得到处理,导致方法退出

      • 无论当前方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行。而虚拟机栈中的“返回地址”就是用来帮助当前方法恢复它的上层方法执行状态

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

    实例讲解

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

    对应的字节码和解释:

     public int add1();
     descriptor: ()I
     flags: ACC_PUBLIC
     Code:
     stack=2, locals=4, args_size=1
     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 的位置)
     8: iload_3          (把局部变量表索引为 3 的值放入操作数栈栈顶)
     9: bipush        10  (把常量 10 压入操作数栈栈顶)
     11: iadd          (将操作数栈出栈两个元素,进行加法运算后,结果放入栈顶)
     12: ireturn          (结束返回)
     LineNumberTable:
     line 16: 0
     line 17: 2
     line 18: 4
     line 19: 8</pre>
    
    • 从上面字节码指令可以看出,在代码执行期间,局部变量表和操作数栈式协同合作来完成某一运算效果的。

    • 各指令意思

      • iconstbipush:这两个指令都是将常量压入操作数栈顶,区别就是:当int取值-15采用iconst指令,取值-128127采用bipush指令

      • istote:将操作数栈顶的元素放入局部变量表的某索引位置,比如isote_5代表将操作数栈顶元素放入局部变量表下标为5的位置

      • iload表示将局部变量表中某下标的值加载到操作数栈顶中,比如iload_2代表将局部变量表索引为2上的值压入操作数栈顶。

      • iadd代表加法运算,具体式将操作数栈顶最上方的两个元素进行相加操作,然后将结果重新压入栈顶

    3.本地方法栈

    • 本地方法栈和虚拟机栈基本相同,只不过式针对本地(native)方法。在开发中如果涉及JNI调用可能会接触本地方法栈多一些。

    • 在有些虚拟机中的实现中已经将两个合二为一了(比如HotSpot)。

    4.堆

    • Java堆(Heap)是JVM所管理的内存中最大的一块,该区域唯一目的就是存放对象实例,几乎所有对象的实例都在堆内存分配。

    • 因此他也是Java垃圾收集器(GC)管理的主要区域,有时候也叫做“GC堆”

    • 同时他也是所有线程共享的内存区域,因此被分配在此区域的对象如果被多个线程访问的话,需要考虑线程安全问题

    • 按照对象存储时间的不同,堆中内存可以划分为新生代老年代,其中新生代又被划分为Eden和Survivor区

      • 图中不同的区域存放具有不同生命周期的对象,这样可以根据不同区域使用不同的垃圾回收算法,提高垃圾回收效率
    3.堆内存分配图.png

    5.方法区

    • 方法区是jvm规范里规定的一块运行时数据区

    • 方法区主要是存储已经被jvm加载的类信息(版本,字段,方法,接口),常量,静态变量,即时编译器编译后的代码和数据

    • 方法区数据区域同堆一样,也是被各个线程共享的内存区域

    • 注意方法区于永久区的区别

      • 方法区是规范层面的东西,规定了这一区域要存放那些数据

      • 永久区或者是metaspace(元空间)是对方法区的不同实现,是实现层的东西

    6.常见异常
    • StackOverflowError 栈溢出异常

    • OutOfMemoryError 内存溢出异常:大多发生在堆当中

    总结

    • JVM的运行时内存结构中一共又两个“栈”和一个“堆”,分别是:Java虚拟机栈和本地方法栈,以及“GC堆”和方法区,还有一个程序计数器。

    • JVM内存中只有堆和方法区是线程共享的数据区域,其他区域都是线程私有的。

    • 程序计数器是唯一一个在java虚拟机规范中没有归档任何OutOfMemoryError情况的区域

    4.总结图-程序运行时内存分配.png

    相关文章

      网友评论

        本文标题:JVM(一) 程序运行时内存分配

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