1 前言
什么是JVM?我们来看一下维基百科的答案
A Java virtual machine (JVM) is a virtual machine that enables a computer to run Java programs as well as programs written in other languages and compiled to Java bytecode. The JVM is detailed by a specification that formally describes what is required of a JVM implementation. Having a specification ensures interoperability of Java programs across different implementations so that program authors using the Java Development Kit (JDK) need not worry about idiosyncrasies of the underlying hardware platform.
Java虚拟机(JVM)是一种虚拟机,它使计算机能够运行Java程序以及用其他语言编写的程序(如Groovy,Scala),并编译成Java字节码。JVM由规范描述了JVM实现所需的规范。使用规范确保Java程序在不同实现之间的互操作性,以便使用Java开发工具包(JDK)的程序作者不必担心底层硬件平台的特质。
其中虚拟机指的是
指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。
我们常见的虚拟机有VM Ware
,Virtual Box
,JVM
而今天我们要介绍的内容是虚拟机中很重要的一个概念,虚拟机的内存结构。
2 虚拟机内存结构
其中最重要的概念是 方法区,JAVA堆,JAVA栈,指令计数器(PC)
指令计数器(PC)
每个线程都有独立的程序计数器,各线程的互不影响,用于存储下一条执行的虚拟机指令地址(对于Native方法则为空undefined)-
JAVA栈(VM Stack)
我们知道栈是一种先进后出
的数据结构,它有点像机关枪的弹夹先被放进去的子弹最后被打出来。
JVM会为每一个线程创建一个栈,JAVA中的栈是以栈帧为基本的数据单元,在一个线程栈里面会有很多个栈帧(Frame
),每一个栈帧对应一个方法。例如:我们一般在main方法写的程序,叫做主线程中main方法的栈帧。而这个main方法的栈帧存储着,该方法运行时所需要的数据。当main方法调用其他方法例如max()方法。那么max方法所对应的栈帧就会进行压栈操作,成为当前的栈帧。当max()方法执行结束之后,当前的栈帧就会出栈,main方法重新成为当前的栈帧。
那么一个栈帧主要存储着哪些数据呢?主要包含下面的数据- 局部变量表
- 操作数栈
- 方法返回地址
- 动态链接
2.1 局部变量表:
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,其中存放的数据的类型是编译期可知的各种基本数据类型、对象引用(reference)和(returnAddress)类型(它指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成计算的,即在Java程序被编译成Class文件时,就确定了所需分配的最大局部变量表的容量。当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。2.2 操作数栈:
操作数栈又常被称为操作栈,操作数栈的最大深度也是在编译的时候就确定了。32位数据类型所占的栈容量为1, 64位数据类型所占的栈容量为2。当一个方法开始执行时,它的操作栈是空的,在方法的执行过程中,会有各种字节码指令(比如:加操作、赋值元算等)向操作栈中写入和提取内容,也就是入栈和出栈操作。Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。因此我们也称Java虚拟机是基于栈的,这点不同于Android虚拟机,Android虚拟机是基于寄存器的。基于栈的指令集最主要的优点是可移植性强,主要的缺点是执行速度相对会慢些;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的优点是执行速度快,主要的缺点是可移植性差。2.3 动态链接:
每个栈帧都包含一个指向运行时常量池(在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如 final、static 域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。2.4 方法返回地址:
当一个方法被执行后,有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令或遇到了异常,并且该异常没有在方法体内得到处理。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。方法退出的过程实际上等同于把当前栈帧出站,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。
-
JAVA堆(Heap)
所有线程共享的内存区域,用于存放对象实例 -
方法区(Method Area)
线程共享,用于存放已加载的类、常量、静态变量、JIT编译后的代码等数据。
对于HotSpot虚拟机用户而言,经常将方法区称为永生代(Permanent Generation),是因为HotSpot虚拟机用永生代实现方法区,用GC管理方法区。 -
综合案例
综合例子.jpg
public class Sample {
private String name;
public Sample(String name) {
this.name = name;
}
public void printName() {
System.out.println(name);
}
}
public class appMain {
public static void main(String[] args) {
Sample test1 = new Sample("测试1");
Sample test2 = new Sample("测试2");
test1.printName();
test2.printName();
}
}
程序的执行流程如下。
- appMain类信息(即appMain.class对象)通过类加载器加载进方法区。
-
test1,test2
是自定义类Sample
类的两个引用,放置在主线程main
方法对应的栈帧中。 - Sample类的两个
实例
放置在堆区中。 - 分别调用在
test1,test2
,关于Sample存储在方法区中的printName()
方法。 - 返回
main
方法,程序结束。