第十二章 用Java实现JVM之结束

发布于:2025-07-23 ⋅ 阅读:(15) ⋅ 点赞:(0)

用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之结束



前言

    上一篇我们已经实现了异常处理,今天开启新的征程,继续往下。聚焦于垃圾回收

初稿完成

    经过前面的一番努力,其实 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)

流程很简单:

  1. 标记阶段:从 GC Roots 出发,标记所有活着的对象;
  2. 清除阶段:遍历堆,把没有被标记的对象“删掉”或回收。

是不是有点像扫地机器人?

  • 能看到的就避开(活着的对象)
  • 看不到的就清掉(无引用对象)

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个数组:argsarray 加上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 到此就结束了。大家如果有什么想弄的,可以在底下评论,看看能不能一起研究。那就先这样了。下个系列再见。。。

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到