第十二章 用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目录
- 前言
- 初稿完成
- GC
- 可是……少了点什么
- GC,从哪里开始说起?
- 根可达性(Root Reachability)
- 垃圾回收策略
- Safepoint 是怎么回事?
- 代码实现
- 堆
- 线程管理器
- 可达性分析
- GC算法
- 安全点
- 测试
- 总结
前言
上一篇我们已经实现了异常处理,今天开启新的征程,继续往下。聚焦于垃圾回收初稿完成
经过前面的一番努力,其实 JJVM 初稿已经完成了。我们先把之前的hack
删掉,再加上一个方法即可。在JClassLoader
中添加initVM
,代码如下:
public static void initVM(JClassLoader loader) throws ClassNotFoundException {JClass vmClass = loader.loadJClass("sun/misc/VM");vmClass.setState(JClassState.BEING_INITIALIZED);JMethod vm = vmClass.getJMethod("<clinit>", "()V");InstructionContext v = new InstructionContext(vm);v.invoke();vmClass.setState(JClassState.FULLY_INITIALIZED);}
而后修改下CmdCommand#startJVM()
方法即可,代码如下:
private void startJVM(CmdArgs cmdArgs) {ClasspathClassResource cp = new ClasspathClassResource(cmdArgs.getXJreOption(),cmdArgs.getCpOption());ClassParse parse = new ClassParse(cp);try {JClassLoader loader = new AppJClassLoader(parse,cmdArgs.isVerboseClassFlag());JClassLoader.initVM(loader);JClass jClass = loader.loadJClass(cmdArgs.getMainClass());JMethod mainMethod = jClass.getMainMethod();InstructionContext instructionContext = new InstructionContext(mainMethod,cmdArgs);instructionContext.invoke();} catch (Exception e) {e.printStackTrace();}
而后新增一个测试类,就以高斯那道题目为例吧。新增CalcTest
类,代码如下:
public class CalcTest {public static int sum(int size) {int s = 0;for (int i = 1; i < size; i++) {s += i;}return s;}public static void main(String[] args) {int sum = sum(101);System.out.println("初稿完成,这是结果:" + sum);}
}
配置一下idea
,如下:
-Xjre "D:\Oracle\Java\jdk1.8.0_281\jre" -cp "Z:\code\jjvm\ch11\target\test-classes" com.hqd.jjvm.calc.CalcTest
运行,等待一段时间后,输出如下:
GC
可是……少了点什么
走到这一步,其实很多人就可以满意了,毕竟字节码能跑、方法栈能压、局部变量能改,一套虚拟机基本骨架也算搭建完毕。《自己动手写Java虚拟机》这本书到这里就结束了,但总觉得还是少了点什么?
没有 GC 的虚拟机,就像没有心跳的生命体——能动,但不灵。
一个真正运行的虚拟机,不应该总是靠我们手动释放对象。哪怕是个玩具,我们也应该让它尽可能贴近真实世界。
GC,从哪里开始说起?
我们先不急着写代码,先问几个问题:
- 对象都分配在哪?
- 哪些对象还“活着”?
- 哪些对象该“被回收”?
- 我们怎么判断它们该回收?
嗯,没错,我们要讲的就是——垃圾回收(Garbage Collection,简称 GC)
根可达性(Root Reachability)
在 JVM 设计中,对象是否存活的判断,最核心的概念是一个词:
✅ 根可达性(Reachability from GC Roots)
意思是:我们从一些“根对象”出发(比如:局部变量表、操作数栈、静态字段等),沿着引用一路往下找,如果某个对象能被找到了,那它就是“还活着的”;反之就是“垃圾”,该被清理掉了。
用更接地气的方式说:
- 你手里握着一根线(根对象)
- 你顺着这根线找啊找,能摸到的对象都留下
- 摸不到的,就扔了
这套机制,比起传统的“引用计数”,能完美解决循环引用问题,也是现代主流虚拟机普遍采用的方式
垃圾回收策略
现实中,Java 的 GC 算法非常多,比如:
- 复制算法(Copying)
- 标记-清除(Mark-Sweep)
- 标记-整理(Mark-Compact)
- 分代回收(Generational GC)
不过,对我们这种手搓虚拟机来说,太复杂的东西先不碰。我们挑一个最基础、最朴素、最容易实现的:
✅ 标记-清除算法(Mark-Sweep)
流程很简单:
- 标记阶段:从 GC Roots 出发,标记所有活着的对象;
- 清除阶段:遍历堆,把没有被标记的对象“删掉”或回收。
是不是有点像扫地机器人?
- 能看到的就避开(活着的对象)
- 看不到的就清掉(无引用对象)
Safepoint 是怎么回事?
触发 GC(垃圾回收) 之前,我们得把现场“控制住”——不能让线程还在执行字节码、修改引用。否则,你刚回收一块内存,它又来用了,那这不就是出事故了吗?所以,所有线程必须停下来,等 GC 把事情处理完。这就叫:Stop The World(STW):暂停整个 Java 世界,让 GC 安全地运行
但问题来了,我们能随时随地停下线程?显然是不能的,某条指令执行一半就被拦腰砍断,这样会破坏操作数栈、局部变量,留下不一致状态。所以我们得挑选一些“天然安全”的地方来暂停线程,比如:
-
执行一条字节码之前
-
方法调用或返回之后
-
循环回头的位置
这些地方叫做:
✅ Safepoint(安全点):线程能安全停下来的位置,保证暂停时 VM 状态一致。
除了安全点之外,还有安全区域的概念,大同小异,安全区域是对安全点的补充。这里就不在讨论了
代码实现
好,巴拉了这么久,就下来就真正开始实现它
堆
真正的 JVM 堆内存大概率使用byte
数组实现的,我们就不折腾这么麻烦了,用JObject
数组来实现。内存有了,接下来就是分配他了,当创建JObject
时候,分配一个槽位就行了。释放也简单,把原有槽位置空就行了。JHeap
代码如下:
@Data
public class JHeap {private static final JHeap J_HEAP = new JHeap();private static final int DEFAULT_MAX_SIZE = 1000;private static final double GC_TRIGGER_RATIO = 0.8; // 80% 阈值private final JObject[] heap;private final int maxSize;private int heapSize = 0; // 当前已分配对象数public JHeap(int maxSize) {this.maxSize = maxSize;this.heap = new JObject[maxSize];}public static JHeap getInstance() {return J_HEAP;}public int allocate(JObject obj) {if (heapSize >= maxSize * GC_TRIGGER_RATIO) {System.out.println("[JHeap] 内存使用达到 " + (int)(GC_TRIGGER_RATIO * 100) + "%,尝试GC...");GC.getInstance().gc();}if (heapSize >= maxSize) {throw new OutOfMemoryError("JJVM 堆空间已满");}// 找第一个空位置分配对象for (int i = 0; i < maxSize; i++) {if (heap[i] == null) {heap[i] = obj;obj.setAddress(i);heapSize++;return i;}}throw new OutOfMemoryError("JJVM 堆空间已满(无空槽)");}public void freeObject(int address) {if (address >= 0 && address < heap.length && heap[address] != null) {heap[address] = null;heapSize--;}}
}
线程管理器
虽然这次没有实现多线程,但还是稍微提及下吧。像这种涉及到多的,就必然会有个管理者,线程也不例外。管理者负责管理、调度这些线程。这次就不实现这么多了,就一个简单的管理就行了。JThreadManager
代码如下:
/*** 线程管理器* @author hqd*/
public class JThreadManager {private static final JThreadManager instance = new JThreadManager();private final List<JThread> threads = Collections.synchronizedList(new ArrayList<>());public static JThreadManager getInstance() {return instance;}public void addThread(JThread thread) {threads.add(thread);}public JThread createThread(JMethod method, CmdArgs args) {JThread jThread = new JThread(method, args);threads.add(jThread);return jThread;}public void startAndWait(JThread jThread, String name) {JThreadWrapper wrapper = new JThreadWrapper(jThread);Thread t = new Thread(wrapper, name);t.start();try {t.join();} catch (InterruptedException e) {Thread.currentThread().interrupt();}removeThread(jThread);// 检查是否有异常抛出if (wrapper.getThrowable() != null) {System.err.println("[JJVM] 子线程异常退出: " + wrapper.getThrowable());throw new RuntimeException(wrapper.getThrowable()); // 或者更优雅处理}}}
可达性分析
堆有了,接下来就是判断对象是否存活。我们需要获取当前所有正在运行的线程,逐个获取每个线程中的方法栈,遍历所有栈帧。获取对应局部变量表的数据即可。GCRootProvider
代码如下:
/*** 从线程栈帧中收集 GC Roots*/
public class GCRootProvider {public Set<Integer> collectRoots() {ThreadManager tm = ThreadManager.getInstance();List<JThread> allThreads = tm.getAllThreads();Set<Integer> roots = new LinkedHashSet<>();for (JThread t : allThreads) {getRef(t,roots);}return roots;}private void getRef(JThread thread, Set<Integer> roots){for (StackFrame frame : thread.getJvmStack()) {// 从局部变量表中收集引用for (Slot slot : frame.getLocalVars().getSlots()) {if (slot==null){continue;}Object val = slot.getVal();if (val !=null && val instanceof JObject) {Integer address = ((JObject) val).getAddress();roots.add(address);}}// 从操作数栈中收集引用for (Object val : frame.getOperandStack().getStack()) {if (val instanceof RefSlot) {RefSlot rs = (RefSlot) val;if (rs.getVal()!=null&&rs.getVal() instanceof JObject) {int address = rs.getVal().getAddress();roots.add(address);}}}}}
}
GC算法
回收算法我们使用最简单的 标记清理 即可,先获取到对应的 GC ROOT,在依次往下查找数组。记录存活对象的在堆中的下标。反向回收即可。GC
代码如下:
/*** 简单标记清理GC*/
public class GC {private static final GC instance = new GC();private final JHeap heap = JHeap.getInstance();private final GCRootProvider rootProvider = new GCRootProvider();private GC() {}public static GC getInstance() {return instance;}public void gc() {/*** TODO STW*/System.out.println("####################### [GC] 开始垃圾回收 #######################");System.out.println("🚨 GC 开始: 当前堆大小 = " + heap.getHeapSize());// 1. 收集根地址Set<Integer> rootAddrs = collectRoots();// 2. 标记阶段Set<Integer> markedAddrs = new HashSet<>();for (Integer addr : rootAddrs) {mark(addr, markedAddrs);}System.out.println("✅ GC 完成: 存活对象数 = " + markedAddrs.size());// 3. 清理阶段sweep(markedAddrs);System.out.println("✅ GC 完成: 堆对象 = " + heap.getHeapSize());System.out.println("####################### [GC] 结束垃圾回收 #######################");}private Set<Integer> collectRoots() {Set<Integer> roots = new HashSet<>();Set<Integer> providerRoots = rootProvider.collectRoots();if (providerRoots != null) {roots.addAll(providerRoots);}return roots;}private void mark(int addr, Set<Integer> marked) {if (addr == -1 || marked.contains(addr)) {return;}JObject obj = heap.getObject(addr);if (obj == null) {return;}marked.add(addr);// 递归标记对象所有引用字段(假设JObject提供获取引用地址的方法)Slot[] refs = obj.getFields();if (refs.length==0){if (obj instanceof JArray){JArray arr = (JArray) obj;Object data = arr.getData();if (data instanceof JObject[]){for (JObject jo : (JObject[])data) {if (jo!=null){Integer refAddr = jo.getAddress();mark(refAddr, marked);}}}}}else {for (Slot s : refs) {if (s instanceof RefSlot) {Object o = s.getVal();if (o != null) {Integer refAddr = ((JObject) s.getVal()).getAddress();mark(refAddr, marked);}}}}}private void sweep(Set<Integer> marked) {JObject[] objects = heap.getHeap();for (int i = 0; i < objects.length; i++) {if (objects[i] != null && !marked.contains(i)) {heap.freeObject(i);}}}
}
安全点
安全点之前尝试了一版,发现弄起来实现有点麻烦。最终,为了简便性,还是放弃了。但是,整体逻辑还是不变的,即在关系不在发生变化时候进行垃圾回收
测试
好,今天是最终版了,来看看这些天努力的成果。新增一个GCTest
,代码如下:
public class GCTest {public static void main(String[] args) {Object[] array = new Object[1000];for (int i = 0; i < 1000; i++) {array[i] = new Object();if (i % 100 == 0) {System.out.println("已分配对象数量:" + (i + 1));}// 模拟释放引用,让前面的对象成为垃圾if (i >= 200) {array[i - 200] = null;}}System.out.println("测试结束");}
}
这里到达200个的时候,会把下标200以前的数组赋值为空。形成一个垃圾对象。最终到800个的时候会触发GC。那时候存活对象应该只有202个,2个数组:args
、array
加上200个数组元素。来看下最终结果。idea
新增配置:
-Xjre "D:\Oracle\Java\jdk1.8.0_281\jre" -cp "Z:\code\jjvm\ch11\target\test-classes" com.hqd.jjvm.gc.GCTest
测试结果如下:
总结
这个系列也是拖了好久。看到评论有几个小伙伴儿一直在期待这系列,也就是顺势弄完了。不过,写代码和整理博客,期间隔了好段时间了,我也是凭记忆再补充。如果有什么不对的地方,还希望大家多多包涵。那 JJVM 到此就结束了。大家如果有什么想弄的,可以在底下评论,看看能不能一起研究。那就先这样了。下个系列再见。。。