总体架构:
各部分介绍:
程序计数器:
每个线程通过程序计数器记录当前要执行的的字节码指令的地址。
作用:多线程工作时,A线程执行到第2句切换到B线程,A线程的程序计数器保存到第二句位置。B线程切换回来的时候CPU通过A线程的程序计数器知道A要从第3句开始执行。
JAVA虚拟机栈:
JAVA中每调用一个方法都生成一个栈帧。
局部变量表:
存放方法的入参、方法内的局部变量。
特点:
以slot槽的形式存放变量,读到一个新的变量就把它按照顺序插到槽里。
槽可以复用:指的是如果随着程序的执行,到了某个局部变量已经超出了其作用域的时候(就是用不到它了)的时候,新的变量可以插入它所在的槽取而代之。
以这段代码举例:
public int add(int a, int b) {
int c = a + b;
return c;
}
使用jclasslib工具分析编译后得到字节码文件如下图所示:
具体长这样(使用jclasslib工具分析编译后的字节码文件):
第一列Nr代表编号,只起到个编号作用。
第二列叫起始PC,代表的意思是上上张图代表的字节码中从哪一句话开始这个变量开始生效。
第三列叫长度,代表的是这个变量的生效范围,就是上上张图中这个变量生效的区域总共有多少行。
第四行是这个槽里变量的名字。
第五行是描述符。
操作数栈:
就是一个栈,类似于后缀表达式计算数值。以上上张图为例,具体过程如下:
读取变量a到操作数栈,
读取变量b到操作数栈,
弹出a,b,进行加法操作得到c存储到局部变量表,
将c的值压回栈,
返回c的值。
动态连接:
保存了编号到运行时常量池的内存地址的映射关系。(直接引用)
方法返回地址:
正就是当前方法执行完成后需要将返回值传递给上层的方法调用者。
异常表
异常表存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。
以下图代码为例:
public int divide(int a, int b) {
int c;
try {
c = a / b;
} catch (ArithmeticException e) {
c = 0;
}
return c;
}
字节码文件,异常表如下图所示:
这张异常表表达的意思从起始PC(字节码第0行)到结束PC(字节码第4行)之内进行异常捕获。如果捕获到异常,程序就跳转到跳转PC(字节码第7行)。
本地方法栈:
帮助JVM使用本地方法的栈,功能与JAVA虚拟机栈类似。本地方法是指使用非JAVA语言(如C、C++、汇编等)编写的代码。
方法区:
方法区是一个逻辑概念,可能会使用到直接内存和堆内存,所以从物理概念上来说并不是JVM单独的一个区域。主要包括类元信息、运行时常量池和字符串常量池。
类元信息:
指的是在类加载的第一个阶段加载阶段,JVM会将编译好的字节码文件通过二进制流加载到内存中,同时会在方法区中生成一个instanceKlass类,保存类的所有信息,包括字节码文件中的常量池、字段、方法等等。
JDK<=7时,这部分在永久代里。
JDK>=8时,这部分在元空间里,元空间是直接内存的一部分。
运行时常量池:
字节码文件中的常量池在经过类加载阶段之后,放入运行时常量池,符号引用会替换为直接引用。
JDK<=7时,这部分在永久代里。
JDK>=8时,这部分在元空间里,元空间是直接内存的一部分。
字符串常量池:
存储代码中定义的字符串常量内容。
JDK<7时,这部分是运行时常量池的一部分,存放在永久代里。
JDK=7时,运行时常量池存放在永久代,字符串常量池存放到堆中。
JDK>=8时,运行时常量池存放在元空间,字符串常量池存放到堆中。
静态变量:
JDK<=6时:静态变量存储在永久代中。
JDK>=7时,静态变量存储到了堆中java.lang.class对应实例类的对象中。
堆:
JAVA创建出来的对象都存在于堆上。《深入理解JAVA虚拟机》书中提到,曾经对堆的分类例如:年轻代,老年代,Eden,Survivor等等诸如此类的划分是针对垃圾回收器分代收集理论设计的,现如今有新的垃圾收集器并不需要分代,所以对堆进行更细致的划分没有太大意义。
直接内存:
直接内存不属于JVM,是直接向操作系统申请的内存区间。