一、JVM的运行时数据区
概览
JVM运行时数据区主要包括以下几个部分:程序计数器、虚拟机栈、本地方法栈、方法区、堆;其中栈是运行时的单位,而堆是存储的单位!
1.程序计数器
程序计数器可以看作是当前线程所执行的字节码的 行号指示器
可以通过javap -c xxx.class(也可以使用javap -v 查看附加信息)执行查看反汇编文件;
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。
对应后方的 aload_0、invokespecial等指令,需要查看java字节码指令手册查看;#1 对应执行常量池中的方法,需要通过javap -v指令查看!如下图显示:
2.虚拟机栈
2.1 虚拟机栈
虚拟机栈描述的是 Java方法执行的内存模型:
在同一个线程中,比如main线程,其中调用的每个方法在执行的同时都会创建一个栈帧,并以栈的数据结构,存储于虚拟机栈中(请自行脑补为什么方法的调用是用栈结构的FILO),栈帧中这用局部变量表、操作数栈、动态链接、方法出口等信息存储和操作对应的内容。
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机栈的栈元素。
2.2 局部变量表
局部变量表是一组变量和值的存储空间,用于存放方法参数和方法内部定义的局部变量。在字节码执行过程中,会从放入操作数栈的数据,按栈的方式取出(弹栈),然后赋值给对应的局部变量,对应指令istore_1,
(1)编译时就确定最大容量
在Java程序编译为Class文件时,就在Class文件的方法表的Code属性的 max_locals 数据项中确定了该方法所需要分配的局部变量表的最大容量。
(2)局部变量表的容量以变量槽(Slot)为最小单位
一个Slot可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有8种: boolean 、byte、char、short、int、float、reference 和 returnAddress; 对于 64位的数据类型(long/double),虚拟机会以高位对齐的方式分配两个连续的Slot空间
(3)索引定位
虚拟机通过索引定位的方式使用局部变量表,索引值范围是从0至最大Slot数量
对32位数据类型的变量来说,索引n就代表了使用第n个Slot;而对64位数据类型的变量来说,则会同时使用n和n+1两个Slot;
(4)索引分配
在方法执行时,虚拟机是使用局部变量表来完成参数值到参数变量列表的传递过程的。
如果执行的是实例方法,则局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数;其余参数按照参数表顺序排列,占用从1开始的局部变量Slot;参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。
(5)Slot重用
局部变量表中的Slot是可以重用的,如果当前字节码PC计数器的值,已经超过了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。
2.3 操作数栈
在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,这就是入栈和出栈。
操作数栈是为字节码指令服务的:
例如,虚拟机执行字节码指令 iload ,会将一个int类型的局部变量从局部变量表加载到操作数栈,iadd会将操作栈顶的两个元素相加 ,并将相加后的结果入栈;详情见下图:
2.4 动态连接
动态连接就是指向常量池中该栈帧所属方法的引用
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用 (#号部分)作为参数。
这些符号引用一部分会在类加载阶段或者第一次使用的时候转化为直接引用,这种转化称为静态解析
另一部分将在每一次运行期间转为直接引用,这部分称为动态连接;请注意下方参数对应的 #数字 的跳转!
class MethodA{
public int addMethod(int a,int b){
return a+b;
}
}
public class lomaMath {
public static void main(String[] args){
int a=2;
int b=3;
MethodA methodA = new MethodA();
int i = methodA.addMethod(a, b);
System.out.println(i);
}
}
X:\workspaceforthird\concurrent\out\production\concurrent\com\woniuxy>javap -v lomaMath.class
Classfile /X:/workspaceforthird/concurrent/out/production/concurrent/com/woniuxy/lomaMath.class
Last modified 2020-7-3; size 703 bytes
MD5 checksum 671bd2aad8ee531dd2bf3ce6bebb014f
Compiled from "lomaMath.java"
public class com.woniuxy.lomaMath
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool: //常量池
#1 = Methodref #8.#28 // java/lang/Object."<init>":()V
#2 = Class #29 // com/woniuxy/MethodA
#3 = Methodref #2.#28 // com/woniuxy/MethodA."<init>":()V
#4 = Methodref #2.#30 // com/woniuxy/MethodA.addMethod:(II)I
#5 = Fieldref #31.#32 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Methodref #33.#34 // java/io/PrintStream.println:(I)V
#7 = Class #35 // com/woniuxy/lomaMath
#8 = Class #36 // java/lang/Object
#9 = Utf8 <init>
#10 = Utf8 ()V //()表示方法参数,V表示返回值void
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/woniuxy/lomaMath;
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V //[符号表示是数组 L表示引用类型
#18 = Utf8 args
#19 = Utf8 [Ljava/lang/String;
#20 = Utf8 a
#21 = Utf8 I
#22 = Utf8 b
#23 = Utf8 methodA
#24 = Utf8 Lcom/woniuxy/MethodA;
#25 = Utf8 i
#26 = Utf8 SourceFile
#27 = Utf8 lomaMath.java
#28 = NameAndType #9:#10 // "<init>":()V
#29 = Utf8 com/woniuxy/MethodA
#30 = NameAndType #37:#38 // addMethod:(II)I
#31 = Class #39 // java/lang/System
#32 = NameAndType #40:#41 // out:Ljava/io/PrintStream;
#33 = Class #42 // java/io/PrintStream
#34 = NameAndType #43:#44 // println:(I)V
#35 = Utf8 com/woniuxy/lomaMath
#36 = Utf8 java/lang/Object
#37 = Utf8 addMethod
#38 = Utf8 (II)I
#39 = Utf8 java/lang/System
#40 = Utf8 out
#41 = Utf8 Ljava/io/PrintStream;
#42 = Utf8 java/io/PrintStream
#43 = Utf8 println
#44 = Utf8 (I)V
{
public com.woniuxy.lomaMath();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 35: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/woniuxy/lomaMath;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: iconst_2
1: istore_1
2: iconst_3
3: istore_2
4: new #2 // class com/woniuxy/MethodA 跳转到常量池中#2的位子
7: dup
8: invokespecial #3 // Method com/woniuxy/MethodA."<init>":()V
11: astore_3
12: aload_3
13: iload_1
14: iload_2
15: invokevirtual #4 // Method com/woniuxy/MethodA.addMethod:(II)I
18: istore 4
20: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
23: iload 4
25: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
28: return
LineNumberTable:
line 37: 0
line 38: 2
line 39: 4
line 40: 12
line 41: 20
line 42: 28
LocalVariableTable: //局部变量表
Start Length Slot Name Signature
0 29 0 args [Ljava/lang/String;
2 27 1 a I
4 25 2 b I
12 17 3 methodA Lcom/woniuxy/MethodA;
20 9 4 i I
}
SourceFile: "lomaMath.java"
2.5 方法返回地址
一个方法的退出方式有两种:
正常完成出口:执行引擎遇到任意一个方法返回的字节码指令
异常完成出口:在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,也就是在本方法的异常表中没有搜索到匹配的异常处理器,这时就会导致方法异常退出。
方法返回地址:一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。
而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
方法退出的过程实际上就等同于把当前栈帧出栈.
因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等.
3.堆
Java堆在虚拟机启动时创建,唯一目的就是存放对象实例(new 的对象)以及数组。
(1)从内存回收的角度来看,Java堆是垃圾收集器管理的主要区域:
由于目前的垃圾收集器都采用分代收集算法,因此Java堆中还可细分为:新生代和老年代,默认占比为Young:Old = 1:2。
同时新生代中采用复制算法(survivor s0复制给s1,两者只会有一块有数据),将新生代分为三个区域,默认占比为:Eden:from:to=8:1:1
(2)从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer)
new的对象中的对象头,等单独专题讲解!对其填充:保证对象是8个字节的整数倍!
4.方法区(元空间)
方法区用于存储已被虚拟机加载的类信息(即Class对象)、常量(位于常量池)、静态变量、即时编译器编译后的代码等数据。
对于HotSpot虚拟机来说,也称为“永久代”(Permanent Generation)。
方法区的变化:
jdk1.6及以前:有永久代,常量池在方法区中
jdk1.7:有永久代,但已逐步“去永久代”,常量池转移到堆中
jdk1.8及之后:无永久代,常量池在元空间中,同时元空间属于jvm以外的内存部分
堆外内存的好处:
可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间
理论上能减少GC暂停时间(节约了大量的堆内内存)
可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现
它的持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据
堆外内存能够提升IO效率
堆内内存由JVM管理,属于“用户态”;而堆外内存由OS管理,属于“内核态”。
如果从堆内向磁盘写数据时,数据会被先复制到堆外内存,即内核缓冲区,然后再由OS写入磁盘,使用堆外内存避免了数据从用户内向内核态的拷贝。