{"id":20673049,"url":"https://github.com/relph1119/jvm-nodejs","last_synced_at":"2025-04-19T19:12:18.204Z","repository":{"id":106542855,"uuid":"214064861","full_name":"Relph1119/jvm-nodejs","owner":"Relph1119","description":"采用Node.js实现JVM","archived":false,"fork":false,"pushed_at":"2023-06-28T01:41:36.000Z","size":6401,"stargazers_count":16,"open_issues_count":0,"forks_count":6,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-11-16T20:39:49.716Z","etag":null,"topics":["jvm","node-js"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Relph1119.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-10-10T02:07:09.000Z","updated_at":"2024-10-10T02:08:42.000Z","dependencies_parsed_at":null,"dependency_job_id":"efd46cee-19de-42b7-9fc8-0c8acfa2b99c","html_url":"https://github.com/Relph1119/jvm-nodejs","commit_stats":null,"previous_names":["relph1119/jvm-nodejs"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Relph1119%2Fjvm-nodejs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Relph1119%2Fjvm-nodejs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Relph1119%2Fjvm-nodejs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Relph1119%2Fjvm-nodejs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Relph1119","download_url":"https://codeload.github.com/Relph1119/jvm-nodejs/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224970141,"owners_count":17400294,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["jvm","node-js"],"created_at":"2024-11-16T20:39:53.873Z","updated_at":"2024-11-16T20:39:55.205Z","avatar_url":"https://github.com/Relph1119.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# JVM-NodeJS（用NodeJS实现Java虚拟机）\n\n项目完全参考张秀宏的《自己动手写Java虚拟机》代码结构，原书使用Go语言实现，在此向本书作者表示感谢。\n\n原书代码地址：https://github.com/zxh0/jvmgo-book\n\n## 运行环境\n\nNodeJS 版本：18.16.0\nWebStorm 版本：WebStorm 2021.1\nJava版本：1.8\n\n## 项目结构\n\u003cpre\u003e\nimages---------------------------------运行截图\njava-----------------------------------java的代码与class文件\n+----class---------------------------------java编译生成的class文件\n+----code----------------------------------java的测试代码\nsrc------------------------------------jvm代码\n+-----ch01---------------------------------第1章命令行工具的实现代码\n+-----ch02---------------------------------第2章搜索class文件的实现代码\n+-----ch03---------------------------------第3章解析class文件的实现代码\n+-----ch04---------------------------------第4章运行时数据区的实现代码\n+-----ch05---------------------------------第5章指令集和解释器的实现代码\n+-----ch06---------------------------------第6章类和对象的实现代码\n+-----ch07---------------------------------第7章方法调用和返回的实现代码\n+-----ch08---------------------------------第8章数组和字符串的实现代码\n+-----ch09---------------------------------第9章本地方法调用的实现代码\n+-----ch10---------------------------------第10章异常处理的实现代码\n+-----develop_code---------------------持续开发的实现代码\n      +-----classfile----------------------class文件解析的对象类\n      +-----classpath----------------------类路径目录\n      +-----instructions-------------------指令集\n      +-----native-------------------------本地方法目录\n      +-----rtda---------------------------运行时数据区\n      +-----utils--------------------------工具类\n      +-----Cmd.js-------------------------命令行类\n      +-----Interpreter.js-----------------解释器\n      +-----main.js------------------------主函数\ntools----------------------------------解释器工具\n\u003c/pre\u003e\n\n## 代码编写与运行结果\n\n各章项目的测试都是将参数以数组的方式，传入到`main()`方法中，直接运行`main.js`即可。\n\n### 第1章-命令行工具\n\n完成一个简易的命令行工具，使用各种参数执行JVM命令。  \n\n传入参数：\n\u003e --cp foo/bar MyApp arg1 arg2\n\n![命令行工具](images/ch01/command-tool.png)\n\n1. 采用program对命令进行解析。\n\n### 第2章-搜索class文件\n\n完成搜索class文件功能，类路径的查找，按照搜索的先后顺序，类路径可以从以下3个部分查找：启动类路径、扩展类路径、用户类路径。\n\n传入参数：\n\u003e --Xjre \"D:\\JavaTools\\jdk1.8.0_151\\jre\" java.lang.Object\n\n![搜索class文件](images/ch02/read-class-file.png)\n\n1. 由于Node.js的异步机制，在遍历文件夹目录的时候，采用递归的做法，没有用`walk`模块。\n2. Node.js和Java很像，不能按照Python的方式返回多个参数，笔者采用了返回`Map`对象，代替多个返回值的实现。\n3. 使用`adm-zip`包对ZIP文件进行解压缩。\n\n### 第3章-解析class文件\n\n完成解析class文件功能，将class文件加载之后，按照JVM规范：读取字节，存储class的版本号、类属性、方法、接口的对象。  \n\n传入参数：  \n\u003e --Xjre \"D:\\JavaTools\\jdk1.8.0_151\\jre\" java.lang.String\n\n![解析class文件](images/ch03/parse-class-file.png)\n\n1. 数组快速初始化：  \n    ```javascript\n    Array(cp_count).fill(null).map(() =\u003e 0);\n    ```\n2.  将`bytes`数组转成`long`长整型，Node.js中长整型是`BigInt`类型：\n    ```javascript\n    const view = new DataView(bufferUtils.toArrayBuffer(byte_data));\n    this.val = view.getBigInt64(0);\n    ```\n\n### 第4章-运行时数据区\n\n\u0026emsp;\u0026emsp;实现运行时数据区（run-time data area），可分为两类：一类是多线程共享的，另一类是线程私有的。多线程共享的运行时数据区需要在Java虚拟机启动时创建好，在Java虚拟机退出时销毁。线程私有的运行时数据区则在创建线程时才创建，线程退出时销毁。  \n\u0026emsp;\u0026emsp;多现场共享的内存区域主要存放两类数据：类数据和类实例（也就是对象）。对象数据存放在堆中，类数据存放在方法区中。线程私有的运行时数据区用于辅助执行Java字节码。  \n\n![运行时数据区](images/ch04/test-run-time-data-area.png)\n\n1. 不能使用以下代码初始化对象数组，需要用`for`循环遍历初始化对象，因为会陷入死循环（栈溢出）。\n    ```javascript\n    Array(cp_count).fill(null).map(() =\u003e new Object());\n    ```\n2. 为了使用`String.format`，导入`string-format`包，使用方法如下：  \n    ```javascript\n    let format = require('string-format');\n    format.extend(String.prototype);\n    let a = \"size:{0} slots:{1}\".format(this.size, this.slots.toString());\n    console.log(a)\n    ```\n3. 为了将Node.js中的`int`、`long`、`float`、`double`数据类型分开，添加`Numeric.js`文件，用`String`类型保留原始的数据类型，当需要获取时，再进行数据类型转换。\n\n### 第5章-指令集和解释器\n\n\u0026emsp;\u0026emsp;在前两章的基础上编写了一个简单的解释器，并实现大约150条指令，可以执行100个整数求和的程序，能得到5050的正确答案。  \n\n![GaussTest](images/ch05/test-GaussTest.png)\n\n1. 获得类名可以使用以下代码\n    ```javascript\n    obj_instance.constructor.name\n    ```\n2. 读取文件示例代码\n    ```javascript\n    data = fs.readFileSync(file_name, function (err, bytesData) {\n        if (err) {\n            error = err;\n        }\n    });\n    ```\n\n### 第6章-类和对象\n\n\u0026emsp;\u0026emsp;实现线程共享的运行时数据区，包括方法区、运行时常量池、类和对象、一个简单的类加载器，以及`ldc`和部分引用类指令。  \n\n![MyObject](images/ch06/test-MyObject.png)\n\n1. 运算优先级的问题\n    ```\n    0 != 9 \u0026 0x0008\n    ```\n该代码在Python、Go语言下执行为`true`，但是在Node.js中执行为0，因为在Node.js中`!=`的优先级比`\u0026`高。需要加括号，以调整运算优先级。\n\n### 第7章-方法调用和返回\n\n\u0026emsp;\u0026emsp;基本完成了方法调用和返回，并实现了类初始化逻辑，已经可以运行Fibonacci程序（求第30个Fibonacci数）。  \n\n![Fibonacci](images/ch07/test-Fibonacci.png)\n\n1. 该程序会执行很长的时间，但是最后会打印出832040结果。\n2. 该程序可能会报`JavaScript heap out of memory`错误，可以在`Node Parameters`中添加`--max-old-space-size=8000`，表示内存地址8000MB大小。\n\n### 第8章-数组和字符串\n\n实现了数组和字符串的加载，终于可以运行HelloWorld程序了。\n\n1. 解析并执行BubbleSortTest（冒泡排序）算法\n\n![BubbleSortTest](images/ch08/test-BubbleSortTest.png)\n\n2. 解析并执行HelloWorld程序，打印出Hello world!\n\n![HelloWorld](images/ch08/test-HelloWorld.png)\n\n3. 解析并执行PrintArgs程序，打印出传入的参数\n\n![PrintArgs](images/ch08/test-PrintArgs.png)\n\n**本章总结：** \n1. 实现了对象深拷贝方法，在`utils/objectUtil.js`中的`deepcopy`方法。\n2. Node.js的Map遍历的方法如下（其中PrimitiveTypes是Map类型）：\n    ```javascript\n    for(let [class_name, d] of PrimitiveTypes) {\n            if (d === descriptor) {\n                return class_name;\n            }\n        }\n    ```\n\n### 第9章-本地方法调用\n\n实现了本地方法调用的指令，以及Java类库中一些最基本的类和本地方法，有如下本地方法：\n- java.lang.Object.getClass()\n- java.lang.Object.hashCode\n- java.lang.Object.clone\n- java.lang.Class.getPrimitiveClass()\n- java.lang.Class.getName0()\n- java.lang.Class.desiredAssertionStatues0\n- java.lang.String.intern()\n- java.lang.System.arraycopy()\n- java.lang.Float.floatToRawIntBits()\n- java.lang.Float.intBitsToFloat\n- java.lang.Double.doubleToRawLongBits()  \n- java.lang.Double.longBitsToDouble()\n- sun.misc.VM.initialize()\n\n1. 执行GetClassTest程序，得到基本数据类型的类getName()结果。\n\n![GetClassTest](images/ch09/test-GetClassTest.png)\n\n2. 执行StringTest程序，得到字符串判断的结果\n\n![StringTest](images/ch09/test-StringTest.png)\n\n3. 执行ObjectTest程序，得到对象的hashCode值，生成hashCode的代码是直接利用内置函数hash()生成的\n\n![ObjectTest](images/ch09/test-ObjectTest.png)\n\n4. 执行CloneTest程序，可以观察到克隆的对象与原始对象的pi值不一样\n\n![CloneTest](images/ch09/test-CloneTest.png)\n\n5. 执行BoxTest程序，可以看到打印的数组值\n\n![BoxTest](images/ch09/test-BoxTest.png)\n\n**本章总结：** \n1. 由于`invokenative`指令是动态执行本地方法，又因为本地方法在不同的模块里，因此自己实现了动态加载模块，并执行对应的函数方法。  \n    ```javascript\n    for (let load_module of LOAD_MODULES){\n        // 加载该模块，判断里面是否有native_method方法\n        let md = require(load_module);\n        if(Object.getOwnPropertyDescriptors(md).hasOwnProperty(native_method.name)) {\n            let func = Object.getOwnPropertyDescriptors(md)[native_method.name].value;\n            func.call(func, frame);\n            return;\n        }\n    }\n    ```\n2. 在生成hash code时，采用`fnv-plus`包，具体代码如下：\n    ```javascript\n    const fnv = require('fnv-plus');\n    let hash_value = fnv.hash(this_ref.toString(), 32).dec();\n    ```\n3. 在加载java/util/Hashtable类时，由于解析Class文件，初始化ConstantPool的时候，node.js赋值float型的数据不能保留原始数据类型，导致程序运行失败，具体错误如下：\n    ```shell\n    LocalVars: slots:[ num:0 ref:[object Object], num:0 ref:[object Object], num:0 ref:null, num:0 ref:null ]\n    OperandStack: size:1 slots:[ num:0 ref:[object Object], num:1 ref:[object Object] ]\n    RangeError [ERR_OUT_OF_RANGE]: The value of \"value\" is out of range. It must be \u003e= -2147483648 and \u003c= 2147483647. Received 2147483648\n        at checkInt (internal/buffer.js:58:11)\n        at writeU_Int32BE (internal/buffer.js:788:3)\n        at Buffer.writeInt32BE (internal/buffer.js:876:10)\n        at PACK_INT32_BE (D:\\MyJSWork\\JVMByNodeJS\\node_modules\\python-struct\\src\\core.js:90:51)\n        at Function.pack (D:\\MyJSWork\\JVMByNodeJS\\node_modules\\python-struct\\src\\core.js:602:25)\n        at LocalVars.get_float (D:\\MyJSWork\\JVMByNodeJS\\src\\develop_code\\rtda\\LocalVars.js:41:43)\n        at _fload (D:\\MyJSWork\\JVMByNodeJS\\src\\develop_code\\instructions\\loads\\Fload.js:12:32)\n        at FLOAD_1.execute (D:\\MyJSWork\\JVMByNodeJS\\src\\develop_code\\instructions\\loads\\Fload.js:30:9)\n        at Function.loop (D:\\MyJSWork\\JVMByNodeJS\\src\\develop_code\\Interpreter.js:53:18)\n        at Function.interpret (D:\\MyJSWork\\JVMByNodeJS\\src\\develop_code\\Interpreter.js:27:25)\n    ```\n   value的值是2147483648，原本该值应该通过local_vars.get_float()转化为float的类型，值为2147483648.0，由于解析Class文件的时候，在ConstantFloatInfo类调用read_info()函数，初始化val的时候，丢失了原始数据类型。通过在第4章添加了`Numeric.js`文件中的4个数据类型，用`String`类型保留原始的数据类型，问题得以解决，特此记录一下。\n\n### 第10章-异常处理\n\n\u0026emsp;\u0026emsp;实现了异常抛出和处理、异常处理表、athrow指令。在Java语言中，异常可以分为两类：Checked异常和Unchecked异常。Unchecked异常包括java.lang.RuntimeException、java.lang.Error以及它们的子类，其他异常都是Checked异常。所有异常都最终继承自java.lang.Throwable。如果一个方法有可能导致Checked异常抛出，则该方法要么需要捕获该异常并妥善处理，要么必须把该异常列在自己的throws子句中，否则无法通过编译。Unchecked异常没有这个限制。\n\n1. 执行ParseIntTest程序，输出参数123\n\n![ParseIntTest-123](./images/ch10/test-ParseIntTest-123.png)\n\n2. 执行ParseIntTest程序，输出参数abc\n\n![ParseIntTest-abc](./images/ch10/test-ParseIntTest-abc.png)\n\n3. 执行ParseIntTest程序，会抛出异常信息\n\n![ParseIntTest-no-args](./images/ch10/test-ParseIntTest-no-args.png)\n\n**本章总结**\n\n1. 打印异常信息，不用像go语言那样采用反射，由于定义了toString方法，Node.js可以直接用如下代码执行：\n   ```javascript\n      for (let ste of ex.extra) {\n        console.log(\"\\tat \", ste.toString());\n    }\n   ```\n\n## 总结\n\n\u0026emsp;\u0026emsp;第一次代码编写历时8天完成1\\~9章的代码，基本实现了一个JVM的功能，能提供如下命令：\n\u003e -v, --version : 版本号  \n--verbose class : 打印类加载信息    \n--verbose inst : 打印指令  \n--classpath : 用户类路径  \n--Xjre : jre的路径  \n\n\u0026emsp;\u0026emsp;其中遇到的问题都写在前面了，目前完成的功能有基本的命令行、class文件搜索和解析、运行时数据区、指令集和解释器、类和对象、方法调用和返回（支持迭代和递归）、数组和字符串类的加载、调用本地方法。    \n\u0026emsp;\u0026emsp;由于运行第9章的BoxTest程序（打印数组元素）报错，导致不能再继续用Node.js实现JVM，后期可能会自定义Float类型，以区别Number类型中的int类型。\n\n\u0026emsp;\u0026emsp;第二次代码编写历时5天（相隔3年多的时间），重构了第1\\~9章的代码，添加`Numeric.js`文件将Node.js中的`int`、`long`、`float`、`double`数据类型分开，用`String`类型保留原始的数据类型，当需要获取时，再进行数据类型转换，成功解决之前Number类型转换的问题。完成第10章的异常处理代码，由于第11章运用了大量的本地方法，于是放弃编写了。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frelph1119%2Fjvm-nodejs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frelph1119%2Fjvm-nodejs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frelph1119%2Fjvm-nodejs/lists"}