* 运行时数据区执行引擎
本篇我们介绍第二大模块——运行时数据区(JVM内存模型)。
-
其实虚拟机的这些模块并不是独立的,都是相互联系的。java 文件编译为 class 文件,通过类加载子系统加载,信息再到 JVM 托管的内存中(部分操作会与本地内存交互)的流转,再到垃圾回收等等,都是一系列的操作。
本系列的博客为了更加清晰的描述清楚功能和原理,将其分为几个章节写作。
概览
运行时数据区分为几大模块(如上图所示):
线程共享区:
- JAVA堆
- 方法区
线程私有区:
- JAVA栈
- 本地方法栈
- 程序计数器
本文中,我们将从以下几个方法面来分析各个区域:
- 功能
- 存储的内容
- 是否有内存溢出和内存泄露
- 是否进行垃圾回收
对应的垃圾回收算法- 垃圾回收流程
性能调优
线程私有区
程序计数器
程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过该计数器的值来选择选取下一条需要执行的字节码的指令,分支、循环、跳转、异常处理、线程恢复都需要依赖该区域。
通俗点讲,该区域存放的就是一个指针,指向方法区的方法字节码,用来存储指向下一条指令的地址,也就是即将要执行的指令代码。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。
当执行完一行指令码,JVM执行引擎会更新程序计数器的值。
由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。(方法的调用,方法中又调用另外一个方法,正式满足栈的“先进先出,后进后出”的模型)。
OutOfMemoryError:无
虚拟机栈
它描述的是java方法执行的内存模型,其生命周期与线程相同。
每个方法在执行的同时都会创建一个栈帧(StackFrame),每一个栈帧又包括局部变量表、操作数栈、动态链接、方法出口等。方法的调用,方法中又调用另外一个方法,正式满足栈的“先进先出,后进后出”的模型。即每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
以上都只是几个很机械的概念,难以深入理解。下面我通过一个示例,来分析虚拟机栈的存储内容。
首先创建一个简单的程序:
package com.sunwin.robotcloud.test;
/**
* Created by 追梦1819 on 2019-11-01.
*/
public class CalculateMain {
public int calculate(){
int a = 3;
int b=4;
int c = a+b;
return c;
}
public static void main(String[] args) {
CalculateMain main = new CalculateMain();
int d = main.calculate();
System.out.println(d);
}
}
对于以上程序,线程启动时,虚拟机会给主线程 main 分配一个大的内存空间,然后给main方法分配一个栈帧,存放该方法的局部变量;
执行calculate()方法时又分配一个calculate()的栈帧,存放对应方法的局部变量。
要注意的是,一个方法分配一个单独的内存区域,即栈帧。
Java 属于高级语言,难以直接通过代码看出它的执行过程。我们通过底层的字节码,反解析出执行的指令码,来分析底层执行过程。
进入 CalculateMain.class 文件目录,执行命令:
将指令码直接输出到文件 CalculateMain.txt:
Compiled from "CalculateMain.java"
public class com.sunwin.robotcloud.test.CalculateMain {
public com.sunwin.robotcloud.test.CalculateMain();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int calculate();
Code:
0: iconst_3
1: istore_1
2: iconst_4
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: iload_3
9: ireturn
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/sunwin/robotcloud/test/CalculateMain
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method calculate:()I
12: istore_2
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: iload_2
17: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
20: return
}
先看看calculate()方法,根据以上指令,查询JVM指令手册,可以得到以上程序的执行流程:
0.将int类型常量3压入(操作数)栈;
1.将int类型值3存入局部变量1(1是数组下标),也就是在局部变量表中给a分配一块内存(用以存储3);
2.将int类型常量4压入(操作数)栈;
3.将int类型值4存入局部变量2;
4.从局部变量1中装载int类型值,也就是将局部变量表的值3,拿出来加载到操作数栈;
5.从局部变量2中装载int类型值;
6.两值相加;
7.(将数存入到操作数栈?)将int类型值7存入局部变量3;
8.从局部变量3中装载int类型值;
9.返回计算值。
以上是方法执行时的局部变量在内存中的流转过程。总结就是:
操作数栈相当于数据在操作时的临时中转站
局部变量表:局部变量存放空间。是一个字长为单位、从0开始计数的数组。类型为int、float、reference、retrueAddress的值,只占据一项。类型为byte、short、char的值存入数组前都被转化为int值。类型为long、double的值在其中占据连续的两项。索引指向第一个值即可。
不过需要注意的是,虚拟机对byte、short、char是直接支持的,只不过在局部变量表和操作数栈中是被转化为了int值,在堆和方法区中,依然是原来的类型。
操作数栈:数据操作的临时空间。与局部变量表类似。唯一不同的是,它并非是通过索引来访问的,而是通过压栈和出栈来访问的。
动态链接:存放的是方法的jvm指令码的内存地址,运行时动态生成的。
对象有对象头,其中一个类型指针指向方法区的类元信息
最后
2020年在匆匆忙忙慌慌乱乱中就这么度过了,我们迎来了新一年,互联网的发展如此之快,技术日新月异,更新迭代成为了这个时代的代名词,坚持下来的技术体系会越来越健壮,JVM作为如今是跳槽大厂必备的技能,如果你还没掌握,更别提之后更新的新技术了。
更多JVM面试整理:
个时代的代名词,坚持下来的技术体系会越来越健壮,JVM作为如今是跳槽大厂必备的技能,如果你还没掌握,更别提之后更新的新技术了。
[外链图片转存中…(img-bDghJ8Au-1628633753284)]
更多JVM面试整理:
[外链图片转存中…(img-1MmdTk0h-1628633753287)]