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

简介: 一文看懂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;
    }
}

image.gif

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

JVM内存分布

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

image.gif

图1 Test.java 执行流程

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

image.gif

图2 JVM运行时内存分布

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

程序计数器

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

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

虚拟机栈

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

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

image.gif

图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

image.gif

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

image.gif

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

image.gif

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

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

image.gif

 图5

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

image.gif

图6

同样的执行

2: iconst_2

3: istore_1

4: iload_0

5: iload_1

6: iadd

依次变化如图7所示。

image.gif

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

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

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

动态链接

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

返回地址

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

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

本地方法栈

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

方法区

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

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

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

image.gif

图8 堆区域划分

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

总结

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

目录
相关文章
|
25天前
|
存储 Java 程序员
【JVM】——JVM运行机制、类加载机制、内存划分
JVM运行机制,堆栈,程序计数器,元数据区,JVM加载机制,双亲委派模型
|
1月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
1月前
|
Java
JVM运行时数据区
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一
28 2
|
2月前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
2月前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
29 3
|
2月前
|
存储 缓存 监控
Elasticsearch集群JVM调优堆外内存
Elasticsearch集群JVM调优堆外内存
61 1
|
2月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
499 1
|
3月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
51 4
|
2月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
2月前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
30 1