一文看懂JVM运行时内存分布

 前言

繁忙的一年即将过去,由于若干种原因,下定决心开始写一些基础系列,主要包含Java基础、Android基础、设计模式与算法等,目前还没给这个系列想到一个好听的名字。

虚拟机的实现有很多,比如HotSpot、Android Dalvik 、 ART等,不同虚拟机具体实现方式不同但都符合Java虚拟机规范中的规则。

从1+2来看JVM运行时内存分布

新建一个Test类,定义一个静态方法sum,代码如下所示:

public class Test {
    public static void main(String[] args) {
        System.out.println(sum());
    }
    public static int sum() {
        int a = 1;
        int b = 2;
        return a + b;
    }
}

一文看懂JVM运行时内存分布

运行程序,打印结果为3。那么运行Test文件的流程是怎样的呢?

JVM内存分布

首先Test.java文件经过编辑器编译生成Test.class文件。当运行Test类时,通过ClassLoader将Test.class加载到JVM内存中,如图1所示。

一文看懂JVM运行时内存分布

一文看懂JVM运行时内存分布

图1 Test.java 执行流程

 JVM运行时内存主要分为:程序计数器、虚拟机栈、本地方法栈、堆、方法区五个部分,如图2所示。

一文看懂JVM运行时内存分布

一文看懂JVM运行时内存分布

图2 JVM运行时内存分布

其中方法区和堆是线程间共享的 ,虚拟机栈、本地方法栈和程序计数器是线程私有的,依次来看这些区域各自的作用。

程序计数器

程序计数器用来记录当前线程执行的位置。CPU可以在多个线程中分配执行时间,当某个线程被挂起时,程序计数器用来记录代码已经执行的位置,当线程恢复执行时继续从记录位置开始执行。常见的异常处理、分支操作等都是通过通过程序计数器来完成的。

每个线程内部都有一个程序计数器,随着线程的创建而创建,随着线程的销毁而销毁。计数器记录的是正在执行的虚拟机字节码指令的地址,如果当前执行的是Native方法,计数器值为空。

虚拟机栈

虚拟机栈用来描述Java方法执行的内存模型,我们都知道,JVM是基于栈的解释器执行的,这里的栈指的就是虚拟机栈,更确切的说是虚拟机栈栈帧中的操作数栈。

线程在执行方式时会为每个方法创建一个栈帧,栈帧内部又包含局部变量表、操作数栈、动态链接与返回地址。线程中栈帧分布如图3所示。

一文看懂JVM运行时内存分布

一文看懂JVM运行时内存分布

 图3 栈帧结构

局部变量表

局部变量表是变量值的存储空间,调用方法传递的参数、方法内部创建的变量都会保存在局部变量表中。java文件经过编译后局部变量表的大小已经确定,会写在Code属性表中max_locals属性中。

以上面两数相加的代码为例,查看Test文件的字节码代码如下所示:

public static int sum();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: iconst_1
         1: istore_0
         2: iconst_2
         3: istore_1
         4: iload_0
         5: iload_1
         6: iadd
         7: ireturn
      LineNumberTable:
        line 16: 0
        line 17: 2
        line 18: 4

一文看懂JVM运行时内存分布

从字节码文件中可以看出locals属性的值是2,说明局部变量表的大小为2 分别用来存储变量a和变量b。args_size 表示是参数的个数,这里参数是0,stack表示操作数栈的最大值,首先来看操作数栈是什么。

操作数栈

操作数栈中可以存储任意的Java数据类型。字节码code表中stack=2表示操作数栈的最大深度为2,方法执行的时候会有字节码指令压入或弹出,以上面的字节码操作为例,来看一下操作数栈和局部变量表的变化。

首先开看下各指令值的含义:

iconst:将常量压入操作数栈栈顶,与此类似的还有bipush指令,当 int 取值 -1~5 采用 iconst 指令,取值 -128~127 则使用 bipush 指令。

istore:将操作数栈栈顶元素出栈放入局部变量表的索引位置,istore_n表示将栈顶元素放在局部变量表下标为n的位置。

iload:iload_n表示将局部变量表中下标为n的值压入栈顶

iadd:将操作数栈最上面的两个元素相加,将结果压入栈顶

以1+2的字节码方法为例 

0: iconst_1
 1: istore_0
 2: iconst_2
 3: istore_1
 4: iload_0
 5: iload_1
 6: iadd
 7: ireturn

一文看懂JVM运行时内存分布

刚开始执行sum方式时字局部变量表与操作数栈下图4所示。

一文看懂JVM运行时内存分布

一文看懂JVM运行时内存分布

           图4 局部变量表和操作数栈初始状态

  执行0: iconst_1之后,如图5所示。

一文看懂JVM运行时内存分布

一文看懂JVM运行时内存分布

  图5 

执行 1: istore_0之后,如图6 所示。

一文看懂JVM运行时内存分布

一文看懂JVM运行时内存分布

 图6

同样的执行 

 2: iconst_2

 3: istore_1

 4: iload_0

 5: iload_1

 6: iadd

依次变化如图7所示。

一文看懂JVM运行时内存分布

一文看懂JVM运行时内存分布

                  图7 第2步到第6步局部变量表与操作数栈变化

最后执行return,将操作数栈中的元素3返回,由此1+2=3的操作边完成了,方法执行完成后局部变量表和操作数栈会被销毁。

我们经常会遇到*Error的异常,这就是因为我们上面所说的每调用一个方法时都会在虚拟机栈中创建一个栈帧,当遇到异常导致方法无法退出时,栈帧就不会销毁从而导致*Error的异常。

动态链接

动态链接是为了支持方法调用过程中的动态链接。一个方法若要调用另一个方法,需要将方法的符号引用转化为内存地址的应用,符号引用存储在方法区中。

返回地址

返回地址可以使当前方法恢复上层方法执行状态,便于在方法退出后返回到方法被调用的位置继续执行。

方法退出方式无非就是两种:正常退出和异常退出,正常退出时程序计数器可以作为返回地址,异常退出时返回地址需要通过异常处理器表来确定。

本地方法栈

本地方法栈与虚拟机栈基本相同,主要用来管理native方法,如在Android中使用JNI。这里就不对本地方法栈单独介绍了。

方法区

方法区主要用来存储已被加载的类、静态变量、常量等信息。方法区仅仅是JVM规范中规定的区域,不同的JVM厂商实现方式是不同的。这一点是需要注意的。

堆在JVM管理管理的内存中是最大的一块,堆用来存在对象的实例,也是GC管理的主要区域。

按照存储对象时间不同可以划分为新生代和老年代,其中新生代又分为Eden区和Survivor区,不同的存放区域存放不同生命周期的对象,这样每个区域就可以使用不同的垃圾回收算法,以此来提高垃圾回收率。堆的划分如图8所示。

一文看懂JVM运行时内存分布

一文看懂JVM运行时内存分布

图8 堆区域划分

堆和方法区都是线程间共享的内存区域。

总结

JVM运行时内存主要有程序计数器、虚拟机栈、本地方法栈、堆和方法区,只有堆和方法区是线程间的数据共享区域。

上一篇:阿里云镜像站支持ipv6啦!


下一篇:2021年总结:前路有光,初心莫忘