了解 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运行时数据区)的主要内容,如果未能解决你的问题,请参考以下文章