了解 JVM和JVM内存结构(JVM运行时数据区)

Posted XeonYu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了了解 JVM和JVM内存结构(JVM运行时数据区)相关的知识,希望对你有一定的参考价值。

上一篇:Java 线程池使用详解

之前的文章中,我们大多是了解并发是怎么回事儿,怎么解决并发问题,juc给我们提供锁都有什么效果,是如何使用的。实际上,了解完前面的知识,日常的并发问题大多都可以应付了,但是,也只是停留在会用的水平上。
至于底层是怎么实现的,这个代码到底是怎么运行的,就不是很清楚了。
作为一名有追求的程序员,我们不仅要会用,还要知道为什么这么用,它是怎么实现的。这样,才能走的更高,看的更远。

JVM

官方参考文档:

https://docs.oracle.com/javase/specs/jvms/se16/html/index.html

首先,JVM就是 Java虚拟机,是一个抽象的计算机,可以通过指令集去操作不同的内存区域。

我们所编写的Java程序最终就是编译成 .class 字节码文件运行在Java虚拟机中的。

我们都知道 Java语言的一大特性就是跨平台。原因就是JVM可以在不同的操作系统中运行,而我们的程序是在JVM中运行的,所以,自然而然的我们的程序也就可以跨平台了。

大致执行过程如下:

我们可以写个简单的例子看一下:

随便写一个简单的类,如下

public class YZQ {
    public static void main(String[] args) {
        String name = "yzq";
        name = "Xeon";

        System.out.println("name = " + name);
    }
}

下面我们来用命令编译一下,然后运行看看

首先简单说一下后面用到的命令:

  • javac 文件名.java : 是将java文件编译成class文件
  • java 文件名(class文件) :运行文件
  • javap -c 文件名(class文件):反汇编class文件,就是把class文件转成我们能看懂的指令

更多的命令去官方文档看:

https://docs.oracle.com/en/java/javase/16/docs/specs/man/index.html

操作步骤如下:

如图所示,我们先通过javac 文件名.java 将YZQ.java文件编译,编译结束后就多了一个class文件。
然后通过java class文件名 命令就可以运行了。

我们来看一下class文件是啥样的。

如图所示,class文件里面都是16进制的数据。这玩意我们肯定看不懂,下面我们反汇编看看是什么样的。

javap -c 文件名


可以看到,我们的程序实际上就是一步一步执行的,至于这些指令的含义是什么,看官方文档即可。

https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-2.html#jvms-2.11

这里我们就不深究了,我们这里只需要知道从Java代码到JVM运行这个过程是什么样的就可以。

了解完Java代码运行的流程之后,我们来看看JVM的组成部分

如图所示,主要有以下5个组成部分

组成部分作用
类加载器 (Class Loader)加载字节码文件到内存(运行时数据区)中
运行时数据区 (Runtime Data Area)是JVM核心的内存控件结构模型
执行引擎(Execution Engine)也叫做命令解释器,将字节码文件翻译成操作系统认识的指令集去运行
本地方法接口 (Native Methord Interface)Java语言和其他语言(一般是c或c++)相互调用的一种协议
本地方法库Java本地方法的具体实现

而我们常说的JVM内存结构,就是指JVM的运行时区域


JVM 内存结构

上面也说了,JVM 内存结构实际上就是JVM中的运行时数据区,是JVM程序在操作系统上运行时分配的内存区域。

运行时数据区主要分为5个部分,如下图

组成部分作用
方法区线程共享,主要用来存放类信息、常量、静态变量等数据
线程共享,主要用来存放新创建的对象以及数组等数据,占用空间较大,是GC主要管理的区域
虚拟机栈线程私有,存放的是栈帧,每个栈帧对应一个方法
程序计数器线程私有,用来记录当前线程执行的指令行号,主要是为了解决线程切换的问题
本地方法栈线程私有 本地方法执行时所属的区域

需要注意的是:

方法区和堆是线程共享
虚拟机栈和程序计数器以及本地方法栈是线程私有

上面我们说到了每个线程中都会有各自的虚拟机栈、程序计数器以及本地方法栈。
图示如下

我们主要来看下线程内部的虚拟机栈的结构

一个栈由多个栈帧组成,每个方法都对应一个栈帧,栈帧中主要分为四个区域

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法出口

图示如下:

我们来看下这四个区域分别的作用是什么

区域作用
局部变量表存放方法中的产生的局部变量,以及对象类型的引用地址
操作数栈主要负责存值,取值,做运算等操作,是一个临时区域
动态链接简单理解就是当前栈帧的入口位置,该位置的值存放在方法区,其他方法想要调用当前方法就可以通过入口位置信息找到该方法
方法出口简单理解就是调用该方法时其他方法的执行位置,执行完毕后返回到该位置继续执行

举个例子:
有如下代码

public class TestJVM {
    public static void main(String[] args) {
        String name = "yzq";
        int sum = sum();
        System.out.println("name = " + name);
        System.out.println("sum = " + sum);
    }
    public static int sum() {
        int a = 10;
        int b = 20;
        int sum = a + b;
        return sum;
    }
}

我们javap -c TestJVM 看一下, 指令如下

Compiled from "TestJVM.java"
public class yzq.jvm.TestJVM {
  public yzq.jvm.TestJVM();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String yzq
       2: astore_1
       3: invokestatic  #3                  // Method sum:()I
       6: istore_2
       7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: new           #5                  // class java/lang/StringBuilder
      13: dup
      14: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
      17: ldc           #7                  // String name =
      19: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: aload_1
      23: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      26: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      29: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      32: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      35: new           #5                  // class java/lang/StringBuilder
      38: dup
      39: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
      42: ldc           #11                 // String sum =
      44: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      47: iload_2
      48: invokevirtual #12                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      51: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      54: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      57: return

  public static int sum();
    Code:
       0: bipush        10
       2: istore_0
       3: bipush        20
       5: istore_1
       6: iload_0
       7: iload_1
       8: iadd
       9: istore_2
      10: iload_2
      11: ireturn
}

我们以sum方法为例,来简单看看执行的过程:

指令作用
0: bipush 10向操作数栈中压入一个值 10
2: istore_0从操作数栈中取出值10,存入局部变量表 a=10
3: bipush 20向操作数栈中压入一个值 20
5: istore_1从操作数栈中取出值20,存入局部变量表中 b=20
6: iload_0从局部变量表中取出变量a的值 10,压入操作数栈中
7: iload_1从局部变量表中取出变量b的值 20,压入操作数栈中
8: iadd从操作数栈中取出最上面的两个值,分别为20和10,做加法操作,得出30 ,压入操作数栈中
9: istore_2将值30从操作数栈中取出,存到局部变量表中,sum=30
10: iload_2从局部变量表中取出sum的值 30
11: ireturn返回int类型的数据到方法出口

通过上面的步骤我们可以看到,局部变量表中就是存放当前方法产生的局部变量以及对应的值,操作数栈就是用来临时存储值,是一个入栈,出栈的过程。

一个非常简单的方法代码的执行过程就是这样。


如果你觉得本文对你有帮助,麻烦动动手指顶一下,可以帮助到更多的开发者,如果文中有什么错误的地方,还望指正,转载请注明转自喻志强的博客 ,谢谢!

以上是关于了解 JVM和JVM内存结构(JVM运行时数据区)的主要内容,如果未能解决你的问题,请参考以下文章

了解 JVM和JVM内存结构(JVM运行时数据区)

深入理解JVM虚拟机:JVM运行时数据区

Java -----JVM运行时数据区

深入理解java:1.3.1 JVM内存区域的划分(运行时数据区)

20张图助你了解JVM运行时数据区,你还觉得枯燥吗?

JVM运行时内存区域结构