第五章 用Java实现JVM之运行时数据区
用Java实现JVM目录
第零章 用Java实现JVM之随便说点什么
第一章 用Java实现JVM之JVM的准备知识
第二章 用Java实现JVM之命令行工具
第三章 用Java实现JVM之查找Class文件
第四章 用Java实现JVM之解析class文件
第五章 用Java实现JVM之运行时数据区
第六章 用Java实现JVM之指令集和解释器
第七章 用Java实现JVM之类和对象
第八章 用Java实现JVM之方法调用和返回
第九章 用Java实现JVM之数组和字符串
第十章 用Java实现JVM之本地方法调用
第十一章 用Java实现JVM之异常处理
第十二章 用Java实现JVM之结束
文章目录
- 用Java实现JVM目录
- 前言
- 运行时数据区
- 栈
- 方法栈和局部变量表
- 操作栈
- 方法返回
- pc计数器
- 堆
- 代码实现
- 局部变量表
- 操作栈
- 栈帧
- 方法栈
- 线程
- 堆
- 测试
- 总结
前言
上一篇我们已经实现了`class文件`的解析,今天开启新的征程,继续往下运行时数据区
折腾了这么久,现在可以进入核心的地方了——运行时数据区。这玩意,基本算是老生常谈了,面试时候经常聊到。一说到JVM
模型,大家不会不加思索的说出来:堆栈。这些词都快听吐了,这里就不在污染大家的耳朵了。现在先把什么JVM
规范扔到一旁,我们换个角度来聊这么问题。如果让你来设计JVM
内存模型你会这么设计?
让我来设计啊?那我就直接操作整个内存。什么堆栈,不熟!分配对象直接分配就行了。方法调用这么办?在内存里直接模拟出来一个公共的栈!所有方法统一往里压,执行完了弹出,简单粗暴、效率还高!嗯。效率确实是高,问题也有不少:
-
内存这么回收?
-
这个公共栈,多线程咋办?方法一多咋办,都挤在一个栈里面,出栈是不是还有等其他线程先出栈了才行
-
还有也是关于多线程的问题。公共栈只有一个,线程安全谁来保证,开发吗?那开发指定是不干了。我就是个写代码的,你让我来研究编译原理、由
JVM
来保证线程安全吗?严格意义上来说,确实是。但是加锁的话,会严重削减性能
栈
那这不对啊,这么多问题,不能硬上啊,得改改。3个问题中有两个和线程有关,那平时是如何解决线程安全问题的?锁?不合适,性能不好。那就谁也别争,每个线程一份吧。线程各自有一份问题不大,那线程中要有哪些结构?或者换一个问题:线程执行需要哪些东西才行
方法栈和局部变量表
线程的执行本质上来说就是,方法的调用,而方法的调用必然设计到栈结构。好,方法栈有了。在细想一下,方法执行的过程中有可能传递参数,并且方法体也可以定义变量,这些变量要要存放到哪里?诶,这时候就需要有个数据结构用来存放这些变量了,这就是 局部变量表。那 局部变量表 应该是个什么样的数据结构呢?数组?链表?还是Map?看来看去,好像是Map
更为合适一点,key
是变量名,value
是值。可以是可以,不过嘛,忽略了编译器的作用,要知道jvm
解析的是Class
文件,而不是Java
文件,Class
文件中涉及到访问局部变量,会提供索引,这样就没什么好纠结的了。那指定是数组了
局部变量表 既然是个数组,那数组存放的元素应该是什么?无非就两种,基本类型,和对象。或者说,数字和对象,因为对于计算机来说什么Boolean
类型,true
啊、false
的,真的不熟悉,编译时候统统用0、1来表示了
操作栈
方法中遇到的变量问题解决了。在考虑下,方法中还有那些情况。有可能还有涉及到一些运算,比如说int a = b + c
之类的。这些要这么计算?我们现在可是在做jvm
,可没有现成的东西去运算。说来也简单,还是栈。如果要深究的话,要涉及到 逆波兰表达式,不过我们有万能的编译器,他会帮我们优先级弄好,我们只需要根据指令,进行对应的操作就行了。这里《自己动手写java虚机机》书上有个例子。这里就直接引用了:
方法返回
现在单个方法需要的东西基本准备妥当,那如果A方法调用了B方法,那么在B方法执行完成之后,A方法怎么回到原来执行的位置,这时候也需要保存执行到哪一行
pc计数器
方法执行已经执行完成了,接下来在考虑下多线程的场景,多线程情况下,有可能发现线程上下文切换。这时候线程也需要保存执行到哪一行,方便后续调度
堆
线程私有的内存区域基本已经定义完毕了,接下来就是公共区域——堆。堆这个区域没有明确的规定,可以连续,也可以不连续,给了实现这足够大的自由,主要问题在于内存管理这块。最简单的方式就是依托于Java
本身的管理机制,这样我们啥也不用操心,不过嘛,这样子做对我们帮助不大,起不到一个学习的作用。还有就是自己实现一个算法了,既然本着学习的态度来做这个项目的,那就自己实现吧。现在先不涉及复杂的内容,只要有个堆来分配就行了
代码实现
经过上面的分析,代码其实基本上就已经出来了。接下来进入实战环节
局部变量表
局部变量表 就是一个数组,但是数组内的元素是主要包含对象和数字,那基本框架其实就已经出来了。先定义个基础类,然后分别实现。Slot
代码如下:
@Setter
@Getter
public class Slot<T> {public T getVal() {return null;}public void setVal(T val) {}
}
再来就是两个实现了,一个是数字类型,虽说是数字类型,但是也分为很多:整型、长整型、浮点型。幸运的是,Java
已经提供了公共父类,那就是Number
类,这就好办了,我们直接利用他来实现。NumberSlot
代码如下:
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class NumberSlot extends Slot<Number> {private Number num;@Overridepublic Number getVal() {return num;}@Overridepublic void setVal(Number val) {this.num = val;}
}
说完数字类型,接下来就是对象了,对象那就更多了,不可以一一实现,只能考虑多态来实现。在Java
中,我们都知道Object
是所有对象的父类。为此,我们也定义个所有对象的起始类——JObject
,他现在就是个空对象,什么都没有,就不贴代码了。有了这个起始类之后,引用类型也就出来了。RefSlot
代码如下:
@NoArgsConstructor
@AllArgsConstructor
public class RefSlot extends Slot<JObject> {private JObject ref;@Overridepublic JObject getVal() {return this.ref;}@Overridepublic void setVal(JObject val) {this.ref = val;}
}
元素有了,局部变量表 也就出来了,新增LocalVars
类,代码如下:
@Getter
public class LocalVars {private Slot[] slots;public LocalVars(int size) {slots = new Slot[size];}public Float getFloatVal(int index) {NumberSlot numberSlot = (NumberSlot) slots[index];return numberSlot.getVal().floatValue();}public void setFloatVal(int index, Float val) {slots[index] = new NumberSlot(val);}public Long getLongVal(int index) {NumberSlot numberSlot = (NumberSlot) slots[index];return numberSlot.getVal().longValue();}public void setLongVal(int index, long val) {slots[index] = new NumberSlot(val);slots[index + 1] = null;}//省略其他代码。。。
}
操作栈
操作栈也比较简单,就一些出入栈的操作,我们可以借用Java
自带的栈来实现,稍微封装下即可。OperandStack
代码如下:
/*** 操作栈*/
@Getter
public class OperandStack {private int size;private Stack<Slot> stack;public OperandStack(int size) {if (size > 0) {stack = new Stack();}}public void pushInt(int val) {push(new NumberSlot(val));}public Integer popInt() {return ((Number) pop().getVal()).intValue();}//省略其他代码。。。
}
栈帧
方法运行需要用到的元素基本差不多了,还剩下一个方法执行行数记录。把这些数据合起来就是 栈帧 了,也就是Jvm
方法栈中的基本元素了。StackFrame
代码如下:
@Getter
@Setter
@AllArgsConstructor
public class StackFrame {private LocalVars localVars;private OperandStack operandStack;private AtomicInteger returnAddress;public StackFrame(OperandStack stack, Integer maxLocals) {this.operandStack = stack;this.localVars = new LocalVars(maxLocals);this.returnAddress = new AtomicInteger(0);}public StackFrame(Integer maxStackSize, Integer maxLocals) {this.operandStack = new OperandStack(maxStackSize);this.localVars = new LocalVars(maxLocals);this.returnAddress = new AtomicInteger(0);}
}
方法栈
栈帧 完成之后,就可以写 方法栈 了,方法栈 除了 栈帧,还需要对栈的深度、局部变量表的长度进行控制,毕竟内存不是无限的,不能随意使用。JvmStack
代码如下:
package com.hqd.jjvm.rtda;import lombok.Getter;import java.util.Stack;/*** 虚拟机栈*/
@Getter
public class JvmStack extends Stack<StackFrame> {private Integer maxStackSize;private Integer maxLocalSize;private StackFrame top;public JvmStack(Integer maxStackSize,Integer maxLocalSize) {this.maxStackSize = maxStackSize;this.maxLocalSize = maxLocalSize;this.top = new StackFrame(new OperandStack(maxStackSize),maxLocalSize);this.push(top);}public Integer getLocalVarsIntVal(int index) {return this.getTop().getLocalVars().getIntVal(index);}//省略其他代码。。。
}
线程
剩下还有个程序计数器,归属于线程底下。我们也把他提取出来,作为一个对象。JThread
代码如下:
@Getter
@Setter
public class JThread {private AtomicInteger nextPc;private JvmStack jvmStack;public JThread( ) {this.nextPc = new AtomicInteger(0);this.jvmStack = new JvmStack(1024, 1024);}private void recordCurrentStackStatus(StackFrame newFrame) {/*** 记录当前方法返回地址*/getJvmStack().getTop().setReturnAddress(getNextPc());getJvmStack().push(newFrame);setNextPc(new AtomicInteger(-1));}
}
堆
最后一个内存区域就是堆了,不过现在不需要什么复杂的算法,先创建出来一个byte[]
组成的类就行了。JHeap
代码如下:
@Data
public class JHeap {private byte[] heap; // 模拟堆内存的字节数组private int heapSize; // 堆大小private int top;/*** 简单的内存分配,返回分配起始地址* @param size 需要分配的字节数* @return 分配起始地址,-1表示内存不足*/public int allocate(int size) {if (top + size > heapSize) {return -1; // 堆内存不足}int allocAddress = top;top += size;return allocAddress;}
}
测试
好了,搞定,现在测试一下今天的成果。最后对CmdCommand#startJVM
方法进行改造。代码如下:
private void startJVM(CmdArgs cmdArgs) {ClasspathClassResource cp = new ClasspathClassResource(cmdArgs.getXJreOption(),cmdArgs.getCpOption());ClassParse parse = new ClassParse(cp);try {StackFrame sf = new StackFrame(100,100);testLocalVars(sf.getLocalVars());testOperandStack(sf.getOperandStack());} catch (Exception e) {e.printStackTrace();}}
testLocalVars
方法代码如下:
public void testLocalVars(LocalVars vars) {vars.setIntVal(0, 100);vars.setIntVal(1, -100);vars.setLongVal(2, 2997924580L);vars.setLongVal(4, -2997924580L);vars.setFloatVal(6, 3.1415926F);vars.setDoubleVal(7, 2.71828182845);vars.setRefVal(9, null);System.out.println(vars.getIntVal(0));System.out.println(vars.getIntVal(1));System.out.println(vars.getLongVal(2));System.out.println(vars.getLongVal(4));System.out.println(vars.getFloatVal(6));System.out.println(vars.getDoubleVal(7));System.out.println(vars.getRefVal(9));}
testOperandStack
方法代码如下:
public void testOperandStack(OperandStack ops) {ops.pushInt(100);ops.pushInt(-100);ops.pushLong(2997924580L);ops.pushLong(-2997924580L);ops.pushFloat(3.1415926F);ops.pushDouble(2.71828182845);ops.pushRef(null);System.out.println(ops.popRef());System.out.println(ops.popDouble());System.out.println(ops.popFloat());System.out.println(ops.popLong());System.out.println(ops.popLong());System.out.println(ops.popInt());System.out.println(ops.popInt());}
新增一个测试类,RuntimeDataTest
代码如下:
public class RuntimeDataTest {public static void main(String[] args) {CmdCommand cmdCommand = new CmdCommand();cmdCommand.parseCmd(args);}
}
运行结果如下:
总结
今天主要聊得是JVM
内存结构,也是重点部分。初步实现了Thread
、Stack
、Frame
、OperandStack
和LocalVars
等线程私有的运行时数据区