04_运行时数据区

运行时数据区

一、数据结构

在运行Java程序时,Java虚拟机需要使用内存来存放各式各样的数据。Java虚拟机规范把这些内存区域叫作运行时数据区。

运行时数据区可以分为两类:一类是多线程共享的,另一类则是线程私有的。

  • 多线程共享的运行时数据区需要在Java虚拟机启动时创建好,在Java虚拟机退出时销毁
  • 线程私有的运行时数据区则在创建线程时才创建,线程退出时销毁

多线程共享的内存区域主要存放两类数据:类数据和类实例(也就是对象):

  • 类实例对象数据存放在堆(Heap)中
  • 类数据存放在方法区(Method Area)中,类数据包括字段和方法信息、方法的字节码、运行时常量池等

线程私有的运行时数据区用于辅助执行Java字节码。每个线程都有自己的pc寄存器(Program Counter)和Java虚拟机栈(JVMStack)。

Java虚拟机栈又由栈帧(Stack Frame,后面简称帧)构成,帧中保存方法执行的状态,包括局部变量表(Local Variable)和操作数栈(Operand Stack)等。

二、数据类型

Java虚拟机可以操作两类数据:基本类型(primitive type)和引用类型(reference type)。

基本类型的变量存放的就是数据本身,引用类型的变量存放的是对象引用,真正的对象数据是在堆里分配的。这里所说的变量包括类变量(静态字段)、实例变量(非静态字段)、数组元素、方法的参数和局部变量,等等。

基本类型可以进一步分为布尔类型(boolean type)和数字类型(numeric type),数字类型又可以分为整数类型(integral type)和浮点数类型(floating-point type)。

引用类型可以进一步分为3种:类类型、接口类型和数组类型。类类型引用指向类实例,数组类型引用指向数组实例,接口类型引用指向实现了该接口的类或数组实例。引用类型有一个特殊的值——null,表示该引用不指向任何对象。

三、线程

线程都有自己的pc寄存器(Program Counter)和Java虚拟机栈(JVMStack)。

定义一个线程类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Thread {

/** 最大栈深度 */
private static int maxStackSize = 1024;

/** 程序计数器 */
private int pc;
/** 虚拟机栈 */
private JvmStack stack;

public Thread() {
stack = new JvmStack(maxStackSize);
}

public int getPc() {
return pc;
}

public void setPc(int pc) {
this.pc = pc;
}

public void pushFrame(Frame frame) {
stack.push(frame);
}

public Frame popFrame() {
return stack.pop();
}

public Frame currentFrame() {
return stack.top();
}
}

其中,JvmStack 是虚拟机栈,定义在后面给出。

四、虚拟机栈

虚拟机栈就是一个栈结构,数据先入后出。虚拟机栈里面放的就是栈帧,表示每一个被调用的方法。

和堆一样,Java虚拟机规范对Java虚拟机栈的约束也相当宽松:

  • Java虚拟机栈可以是连续的空间,也可以不连续;可以是固定大小,也可以在运行时动态扩展
  • 如果Java虚拟机栈有大小限制,且执行线程所需的栈空间超出了这个限制,会导致StackOverflowError异常抛出
  • 如果Java虚拟机栈可以动态扩展,但是内存已经耗尽,会导致OutOfMemoryError异常抛出

因此可以采用单向链表的形式来实现虚拟机栈,它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class JvmStack {

/** 最大栈深度 */
private int maxSize;
/** 当前栈深度 */
private int size;
/** 栈顶对象 */
private Frame top;

public JvmStack(int maxSize) {
this.maxSize = maxSize;
}

public void push(Frame frame) {
if (size >= maxSize) {
throw new StackOverflowError("jvm stack is overflow: " + maxSize);
}
frame.setLower(top);
top = frame;
size++;
}

public Frame pop() {
if (size == 0) {
throw new IllegalStateException("jvm stack is empty!");
}
Frame val = top;
top = top.getLower();
val.setLower(null);
size--;
return val;
}

public Frame top() {
if (size == 0) {
throw new IllegalStateException("jvm stack is empty!");
}
return top;
}

}

其中,栈帧 Frame 就作为单向链表的节点存在,栈顶就是单向链尾。

五、栈帧

栈帧表示的一个方法调用,里面包括了执行方法所需要的局部变量表和操作数栈。

执行方法所需的局部变量表大小和操作数栈深度是由编译器预先计算好的,存储在 class 文件 method_info 结构的 Code 属性中。

栈帧的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Frame {

/** 局部变量表 */
private LocalVars localvars;
/** 操作数栈 */
private OperandStack operandStack;
/** 指向下一个栈帧 */
private Frame lower;

public Frame(int maxLocals, int maxStack) {
localvars = new LocalVars(maxLocals);
operandStack = new OperandStack(maxStack);
}

public Frame getLower() {
return lower;
}

public void setLower(Frame lower) {
this.lower = lower;
}

}

六、局部变量表

局部变量表是按索引访问的,可以把它想象成一个数组。

根据Java虚拟机规范,这个数组的每个元素至少可以容纳一个 int 或引用值,两个连续的元素可以容纳一个 longdouble 值。

为了能够同时容纳一个 int 和引用值,这里采用以下结构作为局部变量表的数组元素类型,同时增加了一些静态方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class Slot {

/** 索引 */
private long num;
/** 引用 */
private Object ref;

public Object getRef() {
return ref;
}

public void setRef(Object ref) {
this.ref = ref;
}

public static void setInt(Slot slot, int num) {
slot.num = num;
}

public static int getInt(Slot slot) {
return (int) slot.num;
}

public static void setFloat(Slot slot, float num) {
int bits = Float.floatToIntBits(num);
setInt(slot, bits);
}

public static float getFloat(Slot slot) {
int bits = getInt(slot);
return Float.intBitsToFloat(bits);
}

public static void setLong(Slot highSlot, Slot lowSlot, long num) {
// long类型占用2个插槽
highSlot.num = num >>> 32;
lowSlot.num = num & 0x0FFFFFFFFL;
}

public static long getLong(Slot highSlot, Slot lowSlot) {
// long类型占用2个插槽
long lowerBits = lowSlot.num;
long highBits = highSlot.num;
long val = highBits & 0x0FFFFFFFFL;
val = (val << 32) | (lowerBits & 0x0FFFFFFFFL);
return val;
}

public static void setDouble(Slot highSlot, Slot lowSlot, double num) {
// 把double的bit转成long保存
long bits = Double.doubleToLongBits(num);
setLong(highSlot, lowSlot, bits);
}

public static double getDouble(Slot highSlot, Slot lowSlot) {
// 把long的bit解析成double
long bits = getLong(highSlot, lowSlot);
return Double.longBitsToDouble(bits);
}

}

然后局部变量表的定义则是这样的,实际就是对数组进行了一层封装,并提供了一些接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class LocalVars {

/** 局部变量数组的最大大小 */
private static final int defaultMaxLocals = 65536;
/** 局部变量表数组 */
private Slot[] slots;

public LocalVars() {
this(defaultMaxLocals);
}

public LocalVars(int maxLocals) {
if (maxLocals <= 0) {
maxLocals = defaultMaxLocals;
}
// 一开始就把插槽初始化好
slots = new Slot[maxLocals];
for (int i = 0; i < slots.length; i++) {
slots[i] = new Slot();
}
}

public void setInt(int index, int num) {
Slot.setInt(slots[index], num);
}

public int getInt(int index) {
return Slot.getInt(slots[index]);
}

public void setFloat(int index, float num) {
Slot.setFloat(slots[index], num);
}

public float getFloat(int index) {
return Slot.getFloat(slots[index]);
}

public void setLong(int index, long num) {
// long类型占用2个插槽
Slot.setLong(slots[index + 1], slots[index], num);
}

public long getLong(int index) {
// long类型占用2个插槽
return Slot.getLong(slots[index + 1], slots[index]);
}

public void setDouble(int index, double num) {
// double类型占用2个插槽
Slot.setDouble(slots[index + 1], slots[index], num);
}

public double getDouble(int index) {
// double类型占用2个插槽
return Slot.getDouble(slots[index + 1], slots[index]);
}

public void setRef(int index, Object ref) {
slots[index].setRef(ref);
}

public Object getRef(int index) {
return slots[index].getRef();
}

}

其中,longdouble 这2种类型是占用了2个插槽的。

long 类型可以拆成2个 int 类型保存。

float 可以转成 int 类型保存,double 可以转成 long 类型保存。

七、操作数栈

操作数栈和局部变量表类似,实际上操作数栈操作的数据,就是局部变量表里面的数据。

操作数栈的大小是编译器已经确定的,所以可以用 Slot[] 实现栈结构,其中栈顶就是 slots[size - 1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class OperandStack {

/** 最大栈深度 */
private static final int defaultMaxStack = 65536;
/** 操作数栈数组 */
private Slot[] slots;
/** 当前栈大小 */
private int size;

public OperandStack() {
this(defaultMaxStack);
}

public OperandStack(int maxStack) {
if (maxStack <= 0) {
maxStack = defaultMaxStack;
}
slots = new Slot[maxStack];
for (int i = 0; i < slots.length; i++) {
slots[i] = new Slot();
}
}

public void pushInt(int val) {
Slot.setInt(slots[size], val);
size++;
}

public int popInt() {
size--;
return Slot.getInt(slots[size]);
}

public void pushFloat(float val) {
Slot.setFloat(slots[size], val);
size++;
}

public float popFloat() {
size--;
return Slot.getFloat(slots[size]);
}

public void pushLong(long val) {
// long类型占用2个插槽
Slot.setLong(slots[size + 1], slots[size], val);
size += 2;
}

public long popLong() {
// long类型占用2个插槽
size -= 2;
return Slot.getLong(slots[size + 1], slots[size]);
}

public void pushDouble(double val) {
// double类型占用2个插槽
Slot.setDouble(slots[size + 1], slots[size], val);
size += 2;
}

public double popDouble() {
// double类型占用2个插槽
size -= 2;
return Slot.getDouble(slots[size + 1], slots[size]);
}

public void pushRef(Object ref) {
slots[size].setRef(ref);
size++;
}

public Object popRef() {
size--;
Object ref = slots[size].getRef();
slots[size].setRef(null);
return ref;
}

}

操作数栈的实现和局部变量表的实现差不多,都是采用数组 Slot[],所以操作数栈也需要对 longdouble 进行特殊处理。

八、单元测试

局部变量表测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class LocalVarsTest {

@Test
public void testLocalVars() {
LocalVars localVars = new LocalVars(100);
localVars.setInt(0, 100);
localVars.setInt(1, -100);
localVars.setLog(2, 2997924580L);
localVars.setLog(4, -2997924580L);
localVars.setFloat(6, 3.1415926F);
localVars.setDouble(7, 2.71828182845);
localVars.setRef(9, this);
System.out.println(localVars.getInt(0));
System.out.println(localVars.getInt(1));
System.out.println(localVars.getLong(2));
System.out.println(localVars.getLong(4));
System.out.println(localVars.getFloat(6));
System.out.println(localVars.getDouble(7));
System.out.println(localVars.getRef(9));
}

}

操作数栈测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class OperandStackTest {

@Test
public void testOperandStack() {
OperandStack stack = new OperandStack(100);
stack.pushInt(100);
stack.pushInt(-100);
stack.pushLong(2997924580L);
stack.pushLong(-2997924580L);
stack.pushFloat(3.1415926F);
stack.pushFloat(-3.1415926F);
stack.pushDouble(2.71828182845);
stack.pushDouble(-2.71828182845);
stack.pushRef(this);
System.out.println(stack.popRef());
System.out.println(stack.popDouble());
System.out.println(stack.popDouble());
System.out.println(stack.popFloat());
System.out.println(stack.popFloat());
System.out.println(stack.popLong());
System.out.println(stack.popLong());
System.out.println(stack.popInt());
System.out.println(stack.popInt());
}

}

总结

数据结构:

  • 运行时数据区可以分为两类:一类是多线程共享的,另一类则是线程私有的

  • 多线程共享的运行时数据区需要在Java虚拟机启动时创建好,在Java虚拟机退出时销毁

  • 线程私有的运行时数据区则在创建线程时才创建,线程退出时销毁

  • 多线程共享的内存区域主要存放两类数据:类数据和类实例(也就是对象)

  • 类实例对象数据存放在堆(Heap)中

  • 类数据存放在方法区(Method Area)中,类数据包括字段和方法信息、方法的字节码、运行时常量池等

数据类型:

  • Java虚拟机可以操作两类数据:基本类型(primitive type)和引用类型(reference type)
  • 基本类型的变量存放的就是数据本身,引用类型的变量存放的是对象引用,真正的对象数据是在堆里分配的
  • 基本类型可以进一步分为布尔类型(boolean type)和数字类型(numeric type),数字类型又可以分为整数类型(integral type)和浮点数类型(floating-point type)
  • 引用类型可以进一步分为3种:类类型、接口类型和数组类型。类类型引用指向类实例,数组类型引用指向数组实例,接口类型引用指向实现了该接口的类或数组实例

线程结构:

  • 线程都有自己的pc寄存器(Program Counter)和Java虚拟机栈(JVMStack)
  • 虚拟机栈就是一个栈结构,数据先入后出。虚拟机栈里面放的就是栈帧,表示每一个被调用的方法
  • Java虚拟机规范对虚拟机栈的约束相当宽松:
    • Java虚拟机栈可以是连续的空间,也可以不连续;可以是固定大小,也可以在运行时动态扩展
    • 如果Java虚拟机栈有大小限制,且执行线程所需的栈空间超出了这个限制,会导致StackOverflowError异常抛出
    • 如果Java虚拟机栈可以动态扩展,但是内存已经耗尽,会导致OutOfMemoryError异常抛出
  • 局部变量表是按索引访问的,可以把它想象成一个数组
  • 根据Java虚拟机规范,局部变量表的每个元素至少可以容纳一个 int 或引用值,两个连续的元素可以容纳一个 longdouble
  • 操作数栈的大小是编译器已经确定的
  • 操作数栈实际操作的数据就是局部变量表里面的数据
作者

jiaduo

发布于

2022-01-15

更新于

2023-04-03

许可协议