写点什么

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

作者:黄林晴
  • 2022 年 3 月 09 日
  • 本文字数:2566 字

    阅读完需:约 8 分钟

前言


繁忙的一年即将过去,由于若干种原因,下定决心开始写一些基础系列,主要包含 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;    }}
复制代码


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

JVM 内存分布

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


图 1 Test.java 执行流程


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


图 2 JVM 运行时内存分布

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

程序计数器

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

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

虚拟机栈

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

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



 图 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
复制代码


从字节码文件中可以看出 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        
复制代码


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


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

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



  图 5 


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



 图 6

同样的执行 

 2: iconst_2 3: istore_1 4: iload_0 5: iload_1 6: iadd

依次变化如图 7 所示。


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


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

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

动态链接

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

返回地址

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

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

本地方法栈

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

方法区

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

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

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



图 8 堆区域划分

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

总结

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

发布于: 刚刚阅读数: 4
用户头像

黄林晴

关注

但行好事,莫问前程~ 2018.12.28 加入

Android开发工程师,公众号:"Android技术圈"

评论

发布
暂无评论
一文看懂JVM运行时内存分布_JVM_黄林晴_InfoQ写作平台