首页
关于这个博客
Search
1
Java 实现Google 账号单点登录(OAuth 2.0)全流程解析
2,346 阅读
2
大模型“无脑复读”?3分钟搞懂原因+解决办法
967 阅读
3
Spring AI 无法获取大模型深度思考内容?解决方案来了
782 阅读
4
服务器遭遇 XMRig 挖矿程序入侵排查与清理全记录
419 阅读
5
EasyExcel 实战:导出带图片的 Excel 完整方案
419 阅读
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
AI大模型&智能体
登录
Search
标签搜索
Spring AI
java虚拟机
JVM
Spring AI Alibaba
Java
保姆级教程
SpringBoot
Spring
MCP
WebFlux
龙虾
Agent Skills
大模型
OpenClaw
人工介入
Nginx
Ubuntu
Apache POI
自定义starter
Mybatis
Luca Ju
累计撰写
55
篇文章
累计收到
4
条评论
首页
栏目
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
AI大模型&智能体
页面
关于这个博客
搜索到
22
篇与
的结果
2025-08-11
接入 DeepSeek | Java 调用大模型实战保姆级教程:新手快速上手
接入 DeepSeek | Java 调用大模型实战保姆级教程:新手快速上手最近各大平台接入 DeepSeek 大模型的案例越来越多,作为程序员,你是不是也想亲手实现一次?别担心,这篇教程会用3 种实战方式,带你从零开始用 Java 接入 DeepSeek 大模型,从 SDK 调用到原生 HTTP 请求全覆盖,新手也能轻松上手~前置知识:为什么需要响应式编程?大模型对话为了提升用户体验,通常会采用流式输出(像 ChatGPT 那样逐字显示回复)。而 Java 中实现流式输出需要用到响应式编程(Reactive Programming),核心是通过Flux(Reactor 框架)处理异步数据流。如果你对响应式编程不太熟悉,可以先简单理解:Flux能帮你 “实时接收” 大模型的每一段输出,而不是等全部结果返回后才处理。👉 快速入门参考:Spring 响应式编程官方文档👉 也可参考我的博客:响应式编程学习笔记准备工作:先获取 DeepSeek API Key在开始编码前,需要先申请 DeepSeek 的 API Key:访问 DeepSeek 开放平台:https://platform.deepseek.com/api_keys注册 / 登录账号后,在 “API Keys” 页面点击 “创建 API Key”,保存生成的密钥(后续代码中会用到)。方式一:通过 DeepSeek 官方 SDK 调用(最直接)DeepSeek 官方提供了适配 Spring AI 的 SDK,开箱即用,适合快速集成。1. 添加依赖在pom.xml中引入 Spring AI 对 DeepSeek 的支持(建议使用1.0.0+版本,兼容性更好):<!-- Spring AI 核心依赖 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-deepseek</artifactId> <version>1.0.0</version> <!-- 请使用最新稳定版 --> </dependency>2. 编码实现:阻塞式与流式输出import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.deepseek.DeepSeekChatModel; import org.springframework.ai.deepseek.DeepSeekChatOptions; import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; public class DeepSeekSdkTest { // 初始化DeepSeek模型(核心对象) private final DeepSeekChatModel chatModel = DeepSeekChatModel.builder() .deepSeekApi(DeepSeekApi.builder() .apiKey("你的API Key") // 替换为你的DeepSeek API Key .build()) .defaultOptions(DeepSeekChatOptions.builder() .model("deepseek-chat") // 指定模型(支持deepseek-chat、deepseek-coder等) .temperature(0.7) // 控制输出随机性(0-1,值越高越随机) .build()) .build(); // 测试1:阻塞式输出(适合简单场景,等待完整结果返回) @Test public void testBlockingCall() { // 构建对话消息(SystemMessage设定角色,UserMessage是用户提问) List<Message> messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个出色的聊天助手,擅长通过幽默的风格回答用户问题。")); messages.add(new UserMessage("你好,请问你是谁")); // 调用模型并获取结果 ChatResponse response = chatModel.call(new Prompt(messages)); // 打印完整回复 System.out.println("完整回复:" + response.getResult().getOutput().getText()); } // 测试2:流式输出(适合用户体验场景,逐字显示回复) @Test public void testStreamCall() { // 准备参数 List<Message> messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个出色的聊天助手,擅长通过幽默的风格回答用户问题。")); messages.add(new UserMessage("你好,请问你是谁")); // 调用 Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); // 打印结果 flux.subscribe(resp -> System.out.println(resp.getResult().getOutput().getText())); // 阻塞等待(测试环境用,实际项目无需此操作) try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } }代码说明DeepSeekChatModel:DeepSeek SDK 的核心类,通过建造者模式配置 API Key 和模型参数。call()方法:阻塞式调用,适合不需要实时显示的场景(如后端批量处理),结果如下图。stream()方法:流式调用,返回Flux对象,通过subscribe()实时接收输出(前端可配合 SSE 显示)。模型参数:temperature控制回复随机性(0 = 严谨,1= creative),model可切换为代码模型deepseek-coder。方式二:通过 OpenAI 兼容 SDK 调用(推荐!)大多数主流大模型(包括 DeepSeek)都兼容 OpenAI 的 API 格式,用 OpenAI 的 SDK 调用更通用,后续切换模型(如 GPT、 Claude)几乎不用改代码。1. 添加依赖引入 Spring AI 对 OpenAI 的支持(和方式一的依赖不冲突,可共存):<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> <version>1.0.0</version> <!-- 与DeepSeek依赖版本保持一致 --> </dependency>2. 编码实现:兼容 OpenAI 格式import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.api.OpenAiApi; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; public class OpenAiCompatibleTest { // 初始化OpenAI兼容模型(核心是替换baseUrl为DeepSeek) private final OpenAiChatModel chatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() .baseUrl("https://api.deepseek.com/") // DeepSeek的OpenAI兼容接口地址 .apiKey("你的API Key") // 同样使用DeepSeek的API Key .build()) .defaultOptions(OpenAiChatOptions.builder() .model("deepseek-chat") // 模型名称与DeepSeek一致 .temperature(0.7) .build()) .build(); // 测试阻塞式输出(和方式一用法几乎一致) @Test public void testBlockingCall() { List<Message> messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个出色的聊天助手,擅长通过幽默的风格回答用户问题。")); messages.add(new UserMessage("你好,请问你是谁")); ChatResponse response = chatModel.call(new Prompt(messages)); System.out.println("完整回复:" + response.getResult().getOutput().getText()); } // 测试流式输出 @Test public void testStreamCall() { // 准备参数 List<Message> messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个出色的聊天助手,擅长通过幽默的风格回答用户问题。")); messages.add(new UserMessage("你好,请问你是谁")); // 调用 Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); // 打印结果 flux.subscribe(resp -> System.out.println(resp.getResult().getOutput().getText())); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } }为什么推荐这种方式?兼容性强:后续想切换到 GPT-4、Anthropic Claude 等模型,只需修改baseUrl和model参数。生态成熟:OpenAI 的 SDK 文档和社区支持更丰富,遇到问题更容易找到解决方案。代码复用:团队中如果已有 OpenAI 调用逻辑,无需重复开发,直接适配 DeepSeek。方式三:原生 HTTP 请求调用(无 SDK,更灵活)如果不想依赖第三方 SDK,也可以直接通过 HTTP 请求调用 DeepSeek 的 API,适合需要自定义请求 / 响应处理的场景。1. 核心原理DeepSeek 的聊天接口地址为:https://api.deepseek.com/v1/chat/completions,支持 POST 请求,通过stream: true参数开启流式输出。请求头需携带Authorization: Bearer 你的API Key,请求体为 JSON 格式(包含对话消息、模型参数等)。2. 编码实现:用 WebClient 发送请求import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Flux; public class HttpDirectCallTest { // 你的DeepSeek API Key private static final String API_KEY = "你的API Key"; // DeepSeek聊天接口地址 private static final String API_URL = "https://api.deepseek.com/v1/chat/completions"; @Test public void testStreamHttpCall() { // 构建请求体JSON(流式输出需设置stream: true) String requestBody = """ { "messages": [ { "role": "system", "content": "你是一个出色的聊天助手,擅长通过幽默的风格回答用户问题。" }, { "role": "user", "content": "如何用Java发送HTTP请求?" } ], "model": "deepseek-chat", "stream": true, "temperature": 0.7, "max_tokens": 2048 } """; // 用WebClient发送POST请求(响应式HTTP客户端) WebClient webClient = WebClient.create(API_URL); Flux<String> responseFlux = webClient.post() .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + API_KEY) .bodyValue(requestBody) .retrieve() .bodyToFlux(String.class); // 流式接收响应 // 订阅并处理响应 responseFlux.subscribe( chunk -> { // 每一段流式输出(SSE格式,需解析data字段) if (chunk.contains("\"data\":")) { String content = chunk.split("\"data\":")[1].replace("}", "").replace("\"", "").trim(); if (!content.equals("[DONE]")) { // 过滤结束标识 System.out.print(content); } } }, error -> System.err.println("HTTP请求错误:" + error.getMessage()), () -> System.out.println("\nHTTP流式输出结束~") ); // 阻塞等待 try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } }代码说明请求体参数:messages是对话历史(role支持system/user/assistant),stream: true开启流式输出。响应处理:流式响应为 SSE(Server-Sent Events)格式,每一行是data: {内容},需解析data字段获取实际输出。灵活性:可自定义超时时间、重试机制、请求拦截器等,适合复杂场景(如企业级网关适配)。三种方式对比与选择建议方式优点缺点适合场景DeepSeek 官方 SDK原生支持,参数适配性好仅支持 DeepSeek,切换模型需改代码仅用 DeepSeek,快速集成OpenAI 兼容 SDK跨模型兼容,生态成熟需确认模型兼容性(部分参数可能不同)可能切换模型,追求代码复用原生 HTTP 请求无依赖,高度自定义需手动处理参数、解析响应,开发效率低自定义需求高,避免第三方 SDK 依赖👉 新手推荐方式二,兼顾简单性和灵活性;追求极致自定义用方式三;仅深度集成 DeepSeek 用方式一。避坑指南API Key 安全:不要硬编码 API Key,建议通过配置文件(如application.yml)或环境变量注入,避免泄露。超时设置:大模型响应可能较慢,需在 HTTP 客户端或 SDK 中设置合理超时时间(如 30 秒以上)。流式输出前端适配:如果前端需要显示流式效果,可通过 SSE(Server-Sent Events)或 WebSocket 接收后端的Flux输出。模型参数调试:temperature和top_p控制输出风格,可根据需求调整(技术问答建议temperature=0.3更严谨)。总结通过本文的 3 种方式,介绍了 Java 调用 DeepSeek 大模型的核心方法。无论是快速集成的 SDK 方式,还是灵活的原生 HTTP 请求,都能满足不同场景的需求。接下来可以尝试扩展功能:比如添加对话历史管理、实现多轮对话,或者集成到 Spring Boot 项目中提供 API 接口。如果遇到问题,欢迎在评论区留言, 祝大家开发顺利! 🚀
2025年08月11日
46 阅读
0 评论
0 点赞
2025-07-07
自己动手写 Java 虚拟机笔记 - 第十部分:异常处理机制实现(系列终章)
前言在前一章中,我们实现了本地方法调用与反射机制,让 JVM 具备了与底层交互和动态访问类信息的能力。本章将聚焦 JVM 的 异常处理机制—— 这是保障程序健壮性的核心功能。Java 异常分为 Checked 异常和 Unchecked 异常,通过 throw 关键字抛出,依赖异常处理表和 athrow 指令实现捕获与处理。本章将完整实现异常的抛出、捕获逻辑,以及堆栈跟踪功能,并通过测试验证异常处理的正确性。作为系列笔记的终章,本章结尾还将对整个 JVM 实现之旅进行总结。参考资料《自己动手写 Java 虚拟机》—— 张秀宏开发环境工具 / 环境版本说明操作系统MacOS 15.5基于 Intel/Apple Silicon 均可JDK1.8用于字节码分析和测试Go 语言1.23.10项目开发主语言第十章:异常处理机制实现异常处理是 Java 语言的重要特性,允许程序在运行时捕获并处理错误,而非直接崩溃。JVM 通过异常处理表记录捕获逻辑,通过 athrow 指令抛出异常,并在栈中查找合适的处理程序。本章将实现这一完整流程。一、异常概述:类型与继承关系Java 中的所有异常都继承自 java.lang.Throwable,按是否必须捕获分为两类:异常类型定义示例Checked 异常非 RuntimeException 和 Error 的子类,必须显式捕获或声明抛出IOException、ClassNotFoundExceptionUnchecked 异常包括 RuntimeException 及其子类(运行时异常)和 Error 及其子类(错误),无需显式捕获NullPointerException、OutOfMemoryError继承关系核心:java.lang.Object └── java.lang.Throwable ├── java.lang.Error(错误,如 StackOverflowError) └── java.lang.Exception(异常) ├── Checked 异常(如 IOException) └── java.lang.RuntimeException(运行时异常,Unchecked)二、异常抛出:throw 关键字与 athrow 指令在 Java 代码中,通过 throw 关键字抛出异常,对应字节码中的 athrow 指令,负责将异常对象从操作数栈弹出并触发异常处理流程。1. athrow 指令的核心逻辑athrow 指令的执行流程:从操作数栈弹出异常对象引用(必须是非 null 的 Throwable 实例);遍历当前线程的栈帧,在每个方法的异常处理表中查找匹配的异常处理程序;找到处理程序后,清空当前栈帧的操作数栈,将异常对象推入栈顶,跳转到处理程序执行;若遍历所有栈帧仍未找到处理程序,则终止线程并输出堆栈跟踪。// ATHROW 异常抛出指令 type ATHROW struct { base.NoOperandsInstruction } func (a *ATHROW) Execute(frame *rtda.Frame) { // 1. 从操作数栈弹出异常对象 ex := frame.OperandStack().PopRef() if ex == nil { panic("java.lang.NullPointerException") // 不能抛出 null } thread := frame.Thread() // 2. 查找异常处理程序 if !findAndGotoExceptionHandler(thread, ex) { // 3. 未找到处理程序,输出堆栈并终止线程 handleUncaughtException(thread, ex) } }三、异常处理表:捕获逻辑的存储结构每个方法的 Code 属性中包含异常处理表(exception_table),记录该方法中异常捕获的范围、类型和处理程序位置,是异常捕获的核心依据。1. 异常处理表的结构// ExceptionHandler 异常处理表中的一项 type ExceptionHandler struct { startPc int // 异常监控的起始 PC 地址(包含) endPc int // 异常监控的结束 PC 地址(不包含) handlerPc int // 异常处理程序的 PC 地址(跳转目标) catchType *ClassRef // 捕获的异常类型(null 表示捕获所有异常,对应 catch (Throwable)) } // ExceptionTable 异常处理表(由多个 ExceptionHandler 组成) type ExceptionTable []*ExceptionHandler字段说明:startPc 和 endPc:定义监控的代码范围([startPc, endPc)),该范围内抛出的异常会被当前处理程序监控;handlerPc:当异常被捕获时,程序计数器跳转至此地址执行处理逻辑;catchType:指定捕获的异常类型(通过常量池中的类符号引用),null 表示捕获所有异常(对应 catch (Throwable))。2. 异常处理程序的查找逻辑当异常抛出后,JVM 需要在当前方法的异常处理表中查找最合适的处理程序:// findExceptionHandler 查找匹配的异常处理程序 func (t ExceptionTable) findExceptionHandler(exClass *Class, pc int) *ExceptionHandler { for _, handler := range t { // 1. 检查当前 PC 是否在监控范围内([startPc, endPc)) if pc >= handler.startPc && pc < handler.endPc { // 2. 若捕获所有异常(catchType 为 null),直接返回 if handler.catchType == nil { return handler } // 3. 解析捕获的异常类型,检查是否与抛出的异常兼容 catchClass := handler.catchType.ResolveClass() if catchClass == exClass || exClass.IsSubClassOf(catchClass) { // 异常类型匹配(抛出的异常是捕获类型或其子类) return handler } } } return nil // 未找到匹配的处理程序 }匹配规则:优先匹配范围包含当前 PC 且异常类型兼容的处理程序;若存在多个匹配的处理程序,按在异常处理表中的顺序优先选择第一个。四、异常处理流程:从抛出到捕获异常处理的完整流程涉及栈帧遍历、处理程序查找和流程跳转,确保异常被正确捕获或向上传播。1. 查找并执行异常处理程序// findAndGotoExceptionHandler 在栈中查找异常处理程序并跳转 func findAndGotoExceptionHandler(thread *rtda.Thread, ex *heap.Object) bool { for { // 1. 获取当前栈顶栈帧 frame := thread.CurrentFrame() // 当前指令的 PC(抛出异常的位置) pc := frame.NextPC() - 1 // 2. 在当前方法的异常处理表中查找处理程序 handler := frame.Method().ExceptionTable().findExceptionHandler(ex.Class(), pc) if handler != nil { // 3. 找到处理程序:清空操作数栈,推送异常对象,跳转执行 stack := frame.OperandStack() stack.Clear() stack.PushRef(ex) frame.SetNextPC(handler.handlerPc) return true } // 4. 未找到,弹出当前栈帧,继续在调用栈中查找 thread.PopFrame() // 5. 若栈为空,说明未找到任何处理程序 if thread.IsStackEmpty() { break } } return false }流程说明:从抛出异常的方法开始,逐层遍历调用栈(弹出栈帧),在每个方法的异常处理表中查找匹配的处理程序;找到后,清空当前栈帧的操作数栈,将异常对象推入栈顶,设置程序计数器为 handlerPc 执行处理逻辑;若遍历所有栈帧仍未找到处理程序,则该异常为 “未捕获异常”,触发线程终止。五、堆栈跟踪:fillInStackTrace 本地方法当异常未被捕获时,JVM 需要输出堆栈跟踪信息(包含异常类型、消息和调用栈),帮助定位问题。这一功能通过 Throwable.fillInStackTrace() 本地方法实现。1. 堆栈跟踪元素的结构堆栈跟踪由多个 StackTraceElement 组成,每个元素记录调用栈中的一个方法信息:// StackTraceElement 堆栈跟踪元素 type StackTraceElement struct { fileName string // 文件名(如 "ParseIntTest.java") className string // 类名(如 "ParseIntTest") methodName string // 方法名(如 "bar") lineNumber int // 行号(-1 表示未知) }2. fillInStackTrace 实现该方法填充异常的堆栈信息,记录从异常抛出点到线程启动的完整调用栈:// 注册本地方法:java/lang/Throwable.fillInStackTrace() func init() { native.Register("java/lang/Throwable", "fillInStackTrace", "(I)Ljava/lang/Throwable;", fillInStackTrace) } // fillInStackTrace 填充异常的堆栈跟踪信息 func fillInStackTrace(frame *rtda.Frame) { this := frame.LocalVars().GetThis() // 获取 Throwable 实例 // 从当前线程的栈帧中收集堆栈信息 stacks := collectStackTraceElements(frame.Thread(), this) // 将堆栈信息存储到异常对象中(通过 extra 字段) this.SetExtra(stacks) frame.OperandStack().PushRef(this) // 返回异常对象本身 } // collectStackTraceElements 收集堆栈跟踪元素 func collectStackTraceElements(thread *rtda.Thread, ex *heap.Object) []*StackTraceElement { var elements []*StackTraceElement // 遍历线程的栈帧(跳过 fillInStackTrace 方法本身的栈帧) for frame := thread.CurrentFrame().Lower(); frame != nil; frame = frame.Lower() { method := frame.Method() class := method.Class() // 创建堆栈元素:包含类名、方法名、文件名和行号 element := &StackTraceElement{ className: class.JavaName(), methodName: method.Name(), fileName: class.SourceFile(), // 从类的 SourceFile 属性获取文件名 lineNumber: method.GetLineNumber(frame.NextPC() - 1), // 获取当前 PC 对应的行号 } elements = append(elements, element) } return elements }功能:通过遍历线程的栈帧,收集每个方法的类名、方法名、文件名和行号,最终存储到异常对象中,为后续打印堆栈跟踪提供数据。六、测试:异常处理全流程验证通过 ParseIntTest 测试类验证异常的抛出、捕获和堆栈跟踪功能:1. 测试代码public class ParseIntTest { public static void main(String[] args) { foo(args); // 调用 foo 方法 } private static void foo(String[] args) { try { bar(args); // 调用 bar 方法,可能抛出异常 } catch (NumberFormatException e) { // 捕获数字格式化异常 System.out.println("捕获 NumberFormatException:" + e.getMessage()); } } private static void bar(String[] args) { if (args.length == 0) { // 若没有参数,抛出索引越界异常 throw new IndexOutOfBoundsException("没有输入参数!"); } // 尝试将参数转换为整数(可能抛出 NumberFormatException) int x = Integer.parseInt(args[0]); System.out.println("解析结果:" + x); } }2. 测试场景与结果场景 1:无参数运行(java ParseIntTest)→ bar 方法抛出 IndexOutOfBoundsException,未被 foo 的 NumberFormatException 捕获,向上传播至 main 方法,最终输出堆栈跟踪。场景 2:参数为非数字(java ParseIntTest abc)→ Integer.parseInt 抛出 NumberFormatException,被 foo 的 catch 块捕获并处理。测试结果:两种场景均按预期执行,异常捕获逻辑和堆栈输出正确。系列总结:自己动手写 JVM 的旅程从第一部分的命令行工具到本章的异常处理,我们完成了一个简易 JVM 的核心功能实现。回顾整个系列,我们走过了以下关键旅程:1. 基础搭建(第一、二章)实现命令行参数解析,作为 JVM 的入口;设计类路径查找逻辑,支持从 JAR 包、目录加载 Class 文件。2. 类加载与解析(第三、六章)解析 Class 文件结构,提取魔数、版本号、常量池、字段、方法等信息;实现方法区存储类元信息,通过类加载器完成 “加载→链接→初始化” 流程;解析符号引用为直接引用,建立类、字段、方法的运行时关联。3. 运行时数据区(第四、五章)实现线程、虚拟机栈、栈帧、局部变量表、操作数栈等核心结构;设计指令集和解释器,支持常量加载、算术运算、控制转移等基础指令;实现方法调用与返回机制,支持静态绑定和动态绑定(多态)。4. 复杂数据结构(第七、八章)实现数组的动态创建和操作指令,支持基本类型和引用类型数组;通过字符串池实现字符串常量的共享,支持字符串拼接和 intern 机制。5. 扩展能力(第九、十章)设计本地方法注册与调用框架,实现反射核心功能和类库依赖的本地方法;完整实现异常处理机制,支持异常抛出、捕获和堆栈跟踪。收获与展望通过亲手实现 JVM,我们深入理解了 “Write once, run anywhere” 的底层逻辑:从 Class 文件的二进制结构到指令执行的每一个细节,从内存管理到异常处理,每一部分都是对计算机体系结构和面向对象思想的深度实践。这个简易 JVM 仍有许多可扩展之处(如 JIT 编译、垃圾回收、并发支持等),但已覆盖核心功能,足以执行简单的 Java 程序。希望这份笔记能为同样对 JVM 原理感兴趣的开发者提供参考,让我们在探索技术底层的道路上继续前行。源码地址:https://github.com/Jucunqi/jvmgo.git
2025年07月07日
21 阅读
0 评论
0 点赞
2025-07-04
自己动手写 Java 虚拟机笔记 - 第九部分:本地方法调用与反射机制实现
前言在前一章中,我们实现了数组和字符串的核心机制,完善了 JVM 对复杂数据结构的支持。本章将聚焦 本地方法调用 与反射机制 —— 本地方法(native 方法)是 Java 与底层系统交互的桥梁(如调用操作系统 API、硬件驱动等),而反射机制则依赖本地方法实现类信息的动态访问(如动态获取类结构、调用方法)。本章将通过 Go 语言模拟本地方法的注册、调用逻辑,实现反射的核心功能,并验证关键场景(如字符串拼接、类信息获取),让 JVM 具备与底层交互和动态操作类的能力。参考资料《自己动手写 Java 虚拟机》—— 张秀宏开发环境工具 / 环境版本说明操作系统MacOS 15.5基于 Intel/Apple Silicon 均可JDK1.8用于字节码分析和测试Go 语言1.23.10项目开发主语言第九章:本地方法调用与反射机制本地方法是 Java 语言扩展能力的关键,允许开发者通过其他语言(如 C/C++)实现底层功能;反射则基于本地方法实现类信息的动态访问。本章将从本地方法的注册、调用逻辑入手,逐步实现反射机制,并验证核心场景的正确性。一、本地方法基础:注册与调用机制本地方法(native 方法)没有 Java 字节码实现,需通过外部语言实现并注册到 JVM 中。JVM 需提供注册机制和调用逻辑,确保能正确找到并执行本地方法。1. 本地方法注册:建立方法映射表本地方法通过 “类名 + 方法名 + 方法描述符” 唯一标识,使用 map 存储方法映射关系(key 为标识,value 为 Go 实现的函数)。// NativeMethod 定义本地方法的函数类型(接收栈帧,无返回值) type NativeMethod func(frame *rtda.Frame) // registry 存储本地方法映射:key 为 "类名~方法名~描述符",value 为本地方法实现 var registry = map[string]NativeMethod{} // Register 注册本地方法 func Register(className string, methodName string, methodDescriptor string, method NativeMethod) { key := className + "~" + methodName + "~" + methodDescriptor registry[key] = method }key 设计逻辑:类名、方法名、描述符共同构成唯一标识,避免不同类中同名方法的冲突(如 java/lang/System.arraycopy 与 java/util/Arrays.arraycopy 需区分)。示例:java/lang/System~arraycopy~(Ljava/lang/Object;ILjava/lang/Object;II)V 标识 System.arraycopy 方法。2. 本地方法调用:从字节码到本地实现JVM 通过 invokenative 指令调用本地方法,核心流程为:解析方法标识→查找本地实现→执行本地函数。(1)注入本地方法的 “伪字节码”本地方法无 Code 属性,需为其注入最小化字节码(用于解释器流程兼容):// injectCodeAttribute 为本地方法注入伪 Code 属性 func (m *Method) injectCodeAttribute(returnType string) { m.maxStack = 4 // 操作数栈默认深度 m.maxLocals = m.argSlotCount // 局部变量表大小=参数槽数 // 根据返回类型生成伪字节码(首字节 0xFE 标识本地方法,第二字节为返回指令) switch returnType[0] { case 'V': // void 返回 m.code = []byte{0xfe, 0xb1} // 0xFE=本地方法标识,0xB1=return 指令 case 'D': // double 返回 m.code = []byte{0xfe, 0xaf} // 0xAF=dreturn 指令 case 'F': // float 返回 m.code = []byte{0xfe, 0xae} // 0xAE=freturn 指令 case 'J': // long 返回 m.code = []byte{0xfe, 0xad} // 0xAD=lreturn 指令 case 'L', '[': // 引用类型返回 m.code = []byte{0xfe, 0xb0} // 0xB0=areturn 指令 default: // 基本类型(int/short等)返回 m.code = []byte{0xfe, 0xac} // 0xAC=ireturn 指令 } }设计目的:确保解释器能正常解析方法结构,通过 0xFE 标识触发本地方法调用逻辑。(2)invokenative 指令执行逻辑// INVOKE_NATIVE 调用本地方法的指令 type INVOKE_NATIVE struct { base.NoOperandsInstruction } func (i *INVOKE_NATIVE) Execute(frame *rtda.Frame) { method := frame.Method() className := method.Class().Name() methodName := method.Name() descriptor := method.Descriptor() // 查找本地方法实现 nativeMethod := native.FindNativeMethod(className, methodName, descriptor) if nativeMethod == nil { // 未找到本地方法时抛出异常 panic("java.lang.UnsatisfiedLinkError: " + className + "." + methodName + descriptor) } // 执行本地方法 nativeMethod(frame) } // FindNativeMethod 从注册表查找本地方法 func FindNativeMethod(className, methodName, descriptor string) NativeMethod { key := className + "~" + methodName + "~" + descriptor if method, ok := registry[key]; ok { return method } // 特殊处理:对未实现的 native 方法返回默认实现(如 Object.registerNatives) if methodName == "registerNatives" && descriptor == "()V" { return func(frame *rtda.Frame) {} // 空实现 } return nil }调用流程:从当前栈帧获取方法的类名、方法名、描述符;生成 key 并查找本地方法实现;执行找到的本地函数(传入栈帧,操作局部变量和操作数栈)。二、反射机制实现:基于本地方法的动态类访问反射允许程序在运行时动态获取类信息(如类名、方法、字段)并操作,其核心依赖 java/lang/Class 类(类对象)和相关本地方法。1. 类对象(java/lang/Class 实例)的绑定每个类在 JVM 中对应唯一的 Class 实例(类对象),存储类的元信息,是反射的入口。// Class 结构体新增类对象字段 type Class struct { // ... 原有字段 ... jClass *Object // 对应的 java/lang/Class 实例(类对象) } // 类加载时绑定类对象 func (c *ClassLoader) LoadClass(name string) *Class { // ... 原有加载逻辑 ... // 绑定类对象:当 java/lang/Class 类已加载时 if jlClassClass, ok := c.classMap["java/lang/Class"]; ok { class.jClass = jlClassClass.NewObject() // 创建 Class 实例 class.jClass.extra = class // 关联到当前类(通过 extra 字段存储元信息) } return class }类对象的作用:作为反射的入口(如 obj.getClass() 返回类对象);存储类的元信息(通过 extra 字段关联到 JVM 内部的 Class 结构体)。2. 核心反射本地方法实现反射的关键操作(如获取类名、获取类对象)依赖本地方法实现,以下是核心方法的 Go 实现。(1)Object.getClass():获取对象的类对象// 注册本地方法:java/lang/Object.getClass() func init() { native.Register("java/lang/Object", "getClass", "()Ljava/lang/Class;", getClass) } // getClass 实现:返回对象的类对象 func getClass(frame *rtda.Frame) { this := frame.LocalVars().GetThis() // 获取当前对象(this) class := this.Class().JClass() // 获取类对象(jClass 字段) frame.OperandStack().PushRef(class) // 推送类对象到操作数栈 }(2)Class.getName0():获取类的名称// 注册本地方法:java/lang/Class.getName0() func init() { native.Register("java/lang/Class", "getName0", "()Ljava/lang/String;", getName0) } // getName0 实现:返回类的全限定名 func getName0(frame *rtda.Frame) { this := frame.LocalVars().GetThis() // 获取 Class 实例(类对象) class := this.Extra().(*heap.Class) // 从 extra 字段获取 JVM 内部 Class 结构体 name := class.JavaName() // 转换类名为 Java 格式(如 "[I" → "int[]") jString := heap.JString(class.Loader(), name) // 转换为 Java String 对象 frame.OperandStack().PushRef(jString) // 推送结果到操作数栈 } // JavaName 将 JVM 类名转换为 Java 规范名称 func (c *Class) JavaName() string { if c.IsArray() { return c.name // 数组类名已符合规范(如 "[I") } return strings.ReplaceAll(c.name, "/", ".") // 普通类名:"java/lang/String" → "java.lang.String" }(3)Class.getPrimitiveClass():获取基本类型的类对象// 注册本地方法:java/lang/Class.getPrimitiveClass() func init() { native.Register("java/lang/Class", "getPrimitiveClass", "(Ljava/lang/String;)Ljava/lang/Class;", getPrimitiveClass) } // getPrimitiveClass 实现:返回基本类型的类对象 func getPrimitiveClass(frame *rtda.Frame) { vars := frame.LocalVars() nameObj := vars.GetRef(0) // 获取基本类型名称(如 "int") name := heap.GoString(nameObj) // 转换为 Go 字符串 loader := frame.Method().Class().Loader() var class *heap.Class switch name { case "void": class = loader.LoadClass("void") case "boolean": class = loader.LoadClass("boolean") // ... 其他基本类型 ... default: panic("Invalid primitive type: " + name) } frame.OperandStack().PushRef(class.JClass()) // 推送基本类型的类对象 }三、核心本地方法案例:数组拷贝与字符串操作除反射外,Java 类库中的许多基础功能依赖本地方法,如数组拷贝、字符串拼接等。以下实现关键场景的本地方法。1. System.arraycopy():数组拷贝// 注册本地方法:java/lang/System.arraycopy() func init() { native.Register("java/lang/System", "arraycopy", "(Ljava/lang/Object;ILjava/lang/Object;II)V", arraycopy) } // arraycopy 实现:数组元素拷贝 func arraycopy(frame *rtda.Frame) { vars := frame.LocalVars() src := vars.GetRef(0) // 源数组 srcPos := vars.GetInt(1) // 源数组起始位置 dest := vars.GetRef(2) // 目标数组 destPos := vars.GetInt(3)// 目标数组起始位置 length := vars.GetInt(4) // 拷贝长度 // 校验:源/目标数组非空 if src == nil || dest == nil { panic("java.lang.NullPointerException") } // 校验:数组类型兼容 if !checkArrayCopy(src, dest) { panic("java.lang.ArrayStoreException") } // 校验:索引不越界 if srcPos < 0 || destPos < 0 || length < 0 || srcPos+length > src.ArrayLength() || destPos+length > dest.ArrayLength() { panic("java.lang.IndexOutOfBoundsException") } // 执行拷贝(根据数组类型调用对应拷贝逻辑) heap.ArrayCopy(src, dest, srcPos, destPos, length) } // 校验数组拷贝的类型兼容性 func checkArrayCopy(src, dest *heap.Object) bool { srcClass, destClass := src.Class(), dest.Class() // 必须都是数组 if !srcClass.IsArray() || !destClass.IsArray() { return false } // 基本类型数组必须类型相同;引用类型数组允许子类向父类拷贝 if srcClass.ComponentClass().IsPrimitive() || destClass.ComponentClass().IsPrimitive() { return srcClass == destClass // 基本类型数组必须同类型 } return true // 引用类型数组兼容 }2. 字符串拼接与 String.intern()字符串拼接依赖 StringBuilder.append(),而 append 又依赖 System.arraycopy;String.intern() 则依赖字符串池实现常量共享。(1)String.intern():字符串驻留// 注册本地方法:java/lang/String.intern() func init() { native.Register("java/lang/String", "intern", "()Ljava/lang/String;", intern) } // intern 实现:将字符串驻留到字符串池 func intern(frame *rtda.Frame) { this := frame.LocalVars().GetThis() // 当前 String 对象 interned := heap.InternString(this) // 从字符串池获取驻留的字符串 frame.OperandStack().PushRef(interned) // 推送结果 } // InternString 实现字符串驻留 func InternString(jStr *Object) *Object { goStr := GoString(jStr) // 从 String 对象获取 Go 字符串 // 检查字符串池,存在则返回,否则添加 if interned, ok := internedStrings[goStr]; ok { return interned } internedStrings[goStr] = jStr return jStr }四、功能测试通过测试案例验证本地方法和反射机制的正确性。1. 反射测试:ClassTest 验证类名获取测试目标:通过反射获取基本类型、数组、普通类的类名。public class ClassTest { public static void main(String[] args) { System.out.println(void.class.getName()); // void System.out.println(boolean.class.getName()); // boolean System.out.println(int[].class.getName()); // [I System.out.println(Object.class.getName()); // java.lang.Object System.out.println("abc".getClass().getName()); // java.lang.String } }测试结果:正确输出各类的规范名称,验证 getClass()、getName0() 等本地方法正常工作。2. 字符串测试:StrTest 验证 intern() 机制测试目标:验证字符串池的驻留机制(intern() 后相同内容字符串引用相同)。public class StrTest { public static void main(String[] args) { String s1 = "abc1"; String s2 = "abc1"; System.out.println(s1 == s2); // true(常量池相同引用) int x = 1; String s3 = "abc" + x; // 动态拼接,初始不在常量池 System.out.println(s1 == s3); // false s3 = s3.intern(); // 驻留到字符串池 System.out.println(s1 == s3); // true(引用相同) } }测试结果:输出符合预期,验证 intern() 方法和字符串池机制正确。本章小结本章实现了本地方法调用和反射机制的核心逻辑,重点包括:本地方法框架:通过注册表(map)管理本地方法,注入伪字节码支持解释器流程,实现 invokenative 指令调用逻辑;反射机制:绑定类对象(java/lang/Class 实例)与类元信息,实现 getClass()、getName0() 等核心反射本地方法;关键本地方法:实现 System.arraycopy()(数组拷贝)、String.intern()(字符串驻留)等类库依赖的本地方法;功能验证:通过反射类名测试和字符串驻留测试,验证本地方法和反射机制的正确性。本地方法和反射是 Java 灵活性的重要支撑,下一章将完善异常处理机制,使 JVM 能更健壮地处理运行时错误。源码地址:https://github.com/Jucunqi/jvmgo.git
2025年07月04日
32 阅读
0 评论
1 点赞
2025-07-01
自己动手写 Java 虚拟机笔记 - 第八部分:数组与字符串的实现
前言在前一章中,我们实现了方法调用与返回机制,支撑了函数执行的核心流程。本章将聚焦 JVM 中数组和字符串的实现—— 这两类数据结构在 Java 中使用频繁,但它们的创建、存储和操作逻辑与普通对象存在显著差异。数组类由 JVM 运行时动态生成,而非从 Class 文件加载;字符串则通过常量池和字符串池实现共享。本章将详细实现这些特性,完善 JVM 对复杂数据结构的支持。参考资料《自己动手写 Java 虚拟机》—— 张秀宏开发环境工具 / 环境版本说明操作系统MacOS 15.5基于 Intel/Apple Silicon 均可JDK1.8用于字节码分析和测试Go 语言1.23.10项目开发主语言第八章:数组与字符串的核心实现数组和字符串是 Java 中最基础的数据结构,但其底层实现逻辑与普通对象不同。数组类由 JVM 动态生成,支持多维度和多种数据类型;字符串则通过常量池和字符串池实现高效存储和共享。本章将从数据结构设计、指令实现到功能测试,完整覆盖这两类结构的核心机制。一、数组概述:与普通类的本质区别数组是一种特殊的引用类型,其类信息并非来自 Class 文件,而是由 JVM 在运行时动态创建。理解数组与普通类的区别是实现的基础。特性普通类数组类类信息来源从 Class 文件加载由 JVM 运行时动态生成创建指令new 指令 + 构造器初始化newarray/anewarray/multianewarray 指令类名格式全限定名(如 java/lang/String)特殊格式(如 [I 表示 int 数组,[[Ljava/lang/Object; 表示二维对象数组)继承关系显式继承父类隐式继承 java/lang/Object,实现 Cloneable 和 Serializable 接口核心差异:数组类的结构由 JVM 动态定义,无需预编译的 Class 文件;其创建和操作依赖专门的指令,而非普通对象的 new 指令和构造器。二、数组的核心实现1. 数组对象的数据结构数组对象仍复用 Object 结构体,但通过 interface{} 字段存储数组元素(支持不同类型的数组数据):// Object 统一表示普通对象和数组对象 type Object struct { class *Class // 所属的类(数组类或普通类) data interface{} // 存储数据:普通对象存字段槽位,数组存元素集合 }设计说明:对于普通对象,data 字段存储实例变量的槽位数组(Slots);对于数组对象,data 字段存储 Go 切片(如 []int32 对应 int 数组,[]*Object 对应对象数组),通过 interface{} 兼容不同类型的数组元素。2. 数组类的动态生成数组类由 JVM 动态创建,无需加载 Class 文件。其类信息(如名称、继承关系)由 JVM 按固定规则生成:// NewArray 创建数组对象(根据数组类和长度初始化元素) func (c *Class) NewArray(count uint) *Object { if !c.IsArray() { panic("Not array class: " + c.name) // 校验是否为数组类 } // 根据数组类名创建对应类型的 Go 切片(映射 Java 数组类型) switch c.Name() { case "[Z": // boolean 数组 return &Object{class: c, data: make([]int8, count)} // boolean 用 int8 存储 case "[B": // byte 数组 return &Object{class: c, data: make([]int8, count)} case "[C": // char 数组 return &Object{class: c, data: make([]uint16, count)} // char 用 uint16 存储 case "[S": // short 数组 return &Object{class: c, data: make([]int16, count)} case "[I": // int 数组 return &Object{class: c, data: make([]int32, count)} case "[J": // long 数组 return &Object{class: c, data: make([]int64, count)} case "[F": // float 数组 return &Object{class: c, data: make([]float32, count)} case "[D": // double 数组 return &Object{class: c, data: make([]float64, count)} default: // 对象数组(如 [Ljava/lang/Object;) return &Object{class: c, data: make([]*Object, count)} } }类型映射规则:Java 数组类型与 Go 切片类型的映射需严格对应,确保元素存储和操作的正确性(如 boolean 数组在 JVM 中实际用 byte 存储,故映射为 []int8)。3. 数组类的加载逻辑数组类的加载由类加载器特殊处理,无需读取 Class 文件,直接动态生成类信息:// LoadClass 加载类(支持普通类和数组类) func (c *ClassLoader) LoadClass(name string) *Class { // 1. 检查缓存,已加载则直接返回 if class, ok := c.classMap[name]; ok { return class } // 2. 若为数组类,动态生成类信息 if name[0] == '[' { return c.loadArrayClass(name) } // 3. 加载普通类(从 Class 文件读取) return c.loadNonArrayClass(name) } // loadArrayClass 动态生成数组类信息 func (c *ClassLoader) loadArrayClass(name string) *Class { // 构建数组类的基本信息 class := &Class{ accessFlags: ACC_PUBLIC, // 数组类默认为 public name: name, // 数组类名(如 "[I") loader: c, // 类加载器 initStarted: true, // 数组类无需初始化 superClass: c.LoadClass("java/lang/Object"), // 继承 Object interfaces: []*Class{ // 实现 Cloneable 和 Serializable 接口 c.LoadClass("java/lang/Cloneable"), c.LoadClass("java/io/Serializable"), }, } c.classMap[name] = class // 存入缓存 return class }关键逻辑:数组类的继承和接口实现是固定的(继承 Object,实现 Cloneable 和 Serializable),无需像普通类那样从 Class 文件解析。三、数组操作指令实现JVM 提供专门的指令用于数组的创建、长度获取和元素访问,以下是核心指令的实现。1. newarray:创建基本类型数组用于创建基本类型的一维数组(如 int[]、float[]),操作数包括基本类型标识和数组长度。// 基本类型与 atype 对应关系(JVM 规范定义) const ( AT_BOOLEAN = 4 // boolean 数组 AT_CHAR = 5 // char 数组 AT_FLOAT = 6 // float 数组 AT_DOUBLE = 7 // double 数组 AT_BYTE = 8 // byte 数组 AT_SHORT = 9 // short 数组 AT_INT = 10 // int 数组 AT_LONG = 11 // long 数组 ) // NEW_ARRAY 创建基本类型数组 type NEW_ARRAY struct { atype uint8 // 基本类型标识(对应上述常量) } // 从字节码读取 atype 操作数 func (n *NEW_ARRAY) FetchOperands(reader *base.BytecodeReader) { n.atype = reader.ReadUint8() } // 执行指令:创建数组并推送引用到操作数栈 func (n *NEW_ARRAY) Execute(frame *rtda.Frame) { stack := frame.OperandStack() // 1. 从操作数栈弹出数组长度(必须非负) count := stack.PopInt() if count < 0 { panic("java.lang.NegativeArraySizeException") } // 2. 获取类加载器,解析数组类 classLoader := frame.Method().Class().Loader() arrClass := getPrimitiveArrayClass(classLoader, n.atype) // 3. 创建数组对象并推送引用到栈顶 arr := arrClass.NewArray(uint(count)) stack.PushRef(arr) } // 根据 atype 获取对应的数组类 func getPrimitiveArrayClass(loader *heap.ClassLoader, atype uint8) *heap.Class { switch atype { case AT_BOOLEAN: return loader.LoadClass("[Z") // boolean 数组类名为 "[Z" case AT_BYTE: return loader.LoadClass("[B") // byte 数组类名为 "[B" // 省略其他类型映射... default: panic("Invalid atype!") } }执行流程:从操作数栈获取数组长度并校验非负;根据 atype 确定数组类型(如 AT_INT 对应 [I 类);创建数组对象并将引用推送回操作数栈。2. anewarray:创建引用类型数组用于创建引用类型的一维数组(如 String[]、Object[]),操作数包括类符号引用索引和数组长度。// ANEW_ARRAY 创建引用类型数组 type ANEW_ARRAY struct { base.Index16Instruction // 包含常量池索引(指向类符号引用) } func (a *ANEW_ARRAY) Execute(frame *rtda.Frame) { cp := frame.Method().Class().ConstantPool() // 1. 解析类符号引用,获取元素类型 classRef := cp.GetConstant(a.Index).(*heap.ClassRef) componentClass := classRef.ResolveClass() // 如 "java/lang/String" // 2. 从操作数栈弹出数组长度并校验 stack := frame.OperandStack() count := stack.PopInt() if count < 0 { panic("java.lang.NegativeArraySizeException") } // 3. 获取数组类(元素类型的数组类,如 "[Ljava/lang/String;") arrClass := componentClass.ArrayClass() // 4. 创建数组对象并推送引用 arr := arrClass.NewArray(uint(count)) stack.PushRef(arr) } // ArrayClass 获取元素类型对应的数组类 func (c *Class) ArrayClass() *Class { arrClassName := "[" + c.name // 数组类名规则:元素类名前加 "[" return c.loader.LoadClass(arrClassName) }关键区别:与 newarray 不同,anewarray 需要先解析类符号引用获取元素类型,再动态生成数组类(如元素类型为 String 时,数组类为 [Ljava/lang/String;)。3. arraylength:获取数组长度用于获取数组的长度,无显式操作数,仅需数组引用。// ARRAY_LENGTH 获取数组长度 type ARRAY_LENGTH struct { base.NoOperandsInstruction } func (a *ARRAY_LENGTH) Execute(frame *rtda.Frame) { stack := frame.OperandStack() // 1. 从栈顶弹出数组引用并校验非空 arrRef := stack.PopRef() if arrRef == nil { panic("java.lang.NullPointerException") } // 2. 获取数组长度并推送回栈顶 length := arrRef.ArrayLength() stack.PushInt(length) } // ArrayLength 计算数组长度(根据数组类型返回对应切片长度) func (o *Object) ArrayLength() int32 { switch o.data.(type) { case []int8: return int32(len(o.data.([]int8))) case []uint16: return int32(len(o.data.([]uint16))) case []int32: return int32(len(o.data.([]int32))) // 省略其他类型... case []*Object: return int32(len(o.data.([]*Object))) default: panic("Not array!") } }实现逻辑:数组长度本质是底层 Go 切片的长度,通过类型断言获取不同切片的长度并返回。4. 数组元素访问指令:<t>aload 和 <t>astore<t>aload:从数组指定索引加载元素到操作数栈(如 iaload 加载 int 元素,aaload 加载引用元素);<t>astore:将操作数栈顶元素存入数组指定索引(如 iastore 存储 int 元素,aastore 存储引用元素)。以 aaload(引用元素加载)和 iastore(int 元素存储)为例:// AALOAD 从引用数组加载元素 func (a *AALOAD) Execute(frame *rtda.Frame) { stack := frame.OperandStack() // 1. 弹出索引和数组引用 index := stack.PopInt() arrRef := stack.PopRef() // 2. 校验非空和索引越界 checkNotNil(arrRef) refs := arrRef.Refs() // 获取引用数组([]*Object) checkIndex(len(refs), index) // 3. 推送元素到栈顶 stack.PushRef(refs[index]) } // IASTORE 向 int 数组存储元素 func (i *IASTORE) Execute(frame *rtda.Frame) { stack := frame.OperandStack() // 1. 弹出值、索引和数组引用 val := stack.PopInt() index := stack.PopInt() arrRef := stack.PopRef() // 2. 校验非空和索引越界 checkNotNil(arrRef) ints := arrRef.Ints() // 获取 int 数组([]int32) checkIndex(len(ints), index) // 3. 存储元素 ints[index] = val } // 辅助函数:校验数组非空 func checkNotNil(ref *heap.Object) { if ref == nil { panic("java.lang.NullPointerException") } } // 辅助函数:校验索引不越界 func checkIndex(arrLen int, index int32) { if index < 0 || index >= int32(arrLen) { panic("java.lang.ArrayIndexOutOfBoundsException") } }通用逻辑:所有元素访问指令均需先校验数组非空和索引合法性,再执行加载或存储操作,区别仅在于元素类型的处理。四、字符串的实现Java 字符串通过 java/lang/String 类表示,其核心是字符数组的封装,且通过字符串池实现常量字符串的共享。1. 字符串的本质:字符数组的封装String 类的核心字段是 value(字符数组,存储字符串内容)和 hash(缓存哈希值),JVM 中通过对象字段模拟这一结构:// Java 中的 String 类简化结构 public final class String { private final char value[]; // 存储字符串内容 private int hash; // 缓存哈希值(默认 0) // ... 构造器和方法 ... }在 JVM 实现中,字符串对象的 data 字段存储字符数组的引用,通过字段访问指令操作 value 数组。2. 字符串池:常量字符串的共享机制为节省内存,JVM 对字符串常量采用 “驻留” 机制 —— 相同内容的字符串常量在字符串池中仅存储一份,通过 intern() 方法实现共享。// 字符串池:key 为 Go 字符串(内容),value 为 Java String 对象 var internedStrings = map[string]*Object{} // JString 将 Go 字符串转换为 Java String 对象(并驻留到字符串池) func JString(loader *ClassLoader, goStr string) *Object { // 1. 检查字符串池,若已存在则直接返回 if internedStr, ok := internedStrings[goStr]; ok { return internedStr } // 2. 将 Go 字符串转换为 char 数组([]uint16) chars := stringToUtf16(goStr) // 3. 创建 char 数组对象("[C" 类) jChars := &Object{loader.LoadClass("[C"), chars} // 4. 创建 String 对象("java/lang/String" 类) jStrClass := loader.LoadClass("java/lang/String") jStr := jStrClass.NewObject() // 5. 为 String 对象的 "value" 字段赋值(字符数组) jStr.SetRefVar("value", "[C", jChars) // 6. 存入字符串池 internedStrings[goStr] = jStr return jStr } // stringToUtf16 将 Go 字符串转换为 UTF-16 编码的 char 数组([]uint16) func stringToUtf16(s string) []uint16 { runes := []rune(s) // 转换为 Unicode 码点 chars := make([]uint16, len(runes)) for i, r := range runes { chars[i] = uint16(r) } return chars }核心逻辑:字符串池通过 Go map 实现,键为字符串内容,值为对应的 String 对象;当创建字符串时,先检查池中有否相同内容的字符串,若有则复用,否则创建新对象并加入池。五、功能测试1. 数组测试:冒泡排序验证数组指令通过冒泡排序算法验证数组的创建、元素访问和修改指令的正确性:// 测试类:冒泡排序 public class BubbleSortTest { public static void main(String[] args) { int[] arr = {22, 84, 77, 56, 10, 43, 59}; int[] ints = bubbleSort(arr); for (int anInt : ints) { System.out.println(anInt); // 输出排序结果:10 22 43 56 59 77 84 } } public static int[] bubbleSort(int[] arr) { boolean swapped = true; int j = 0; int tmp; while (swapped) { swapped = false; j++; for (int i = 0; i < arr.length - j; i++) { if (arr[i] > arr[i + 1]) { tmp = arr[i]; arr[i] = arr[i + 1]; arr[i + 1] = tmp; swapped = true; } } } return arr; } }测试结果:排序后的数组元素按从小到大输出,验证 newarray、iaload、iastore、arraylength 等指令正常工作。2. 字符串测试:Hello World 验证字符串池通过经典的 Hello World 程序验证字符串创建和输出功能:// 测试类:输出 Hello World public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World"); // 输出字符串 } }测试结果:成功输出 Hello World,验证字符串池、字符数组封装及 println 方法调用的正确性。本章小结本章实现了 JVM 中数组和字符串的核心机制,重点包括:数组的特殊实现:数组类由 JVM 动态生成,通过 interface{} 存储不同类型的数组元素,支持基本类型和引用类型数组;数组指令集:实现 newarray/anewarray(创建数组)、arraylength(获取长度)、<t>aload/<t>astore(元素访问)等指令,覆盖数组操作全流程;字符串机制:通过 java/lang/String 类封装字符数组,利用字符串池实现常量字符串的共享,减少内存占用;功能验证:通过冒泡排序和 Hello World 程序验证数组指令和字符串功能的正确性。数组和字符串的支持是 JVM 功能完整性的重要标志,下一章将讲述本地方法调用与反射的核心机制。源码地址:https://github.com/Jucunqi/jvmgo.git
2025年07月01日
24 阅读
0 评论
0 点赞
2025-06-30
自己动手写 Java 虚拟机笔记 - 第七部分:方法调用与返回机制
前言在前一章中,我们实现了类与对象的核心机制,包括类信息存储、对象创建和字段访问等。本章将聚焦 JVM 的方法调用与返回机制—— 这是实现函数执行、参数传递和结果返回的核心逻辑,涉及方法符号引用解析、调用指令执行、栈帧管理和类初始化等关键流程,是 JVM 支持面向对象编程的重要基础。参考资料《自己动手写 Java 虚拟机》—— 张秀宏开发环境工具 / 环境版本说明操作系统MacOS 15.5基于 Intel/Apple Silicon 均可JDK1.8用于字节码分析和测试Go 语言1.23.10项目开发主语言第七章:方法调用与返回机制实现方法调用是程序执行的核心动作,JVM 通过特定指令(如 invoke_static、invoke_virtual 等)实现不同类型方法的调用,并通过返回指令(如 ireturn、lreturn 等)完成结果传递。本章将详细实现这些机制,包括符号引用解析、参数传递、栈帧管理和类初始化触发等逻辑。一、方法调用概述JVM 中的方法按调用方式可分为静态方法、实例方法、抽象方法等,不同类型的方法通过不同的指令调用。理解方法调用的核心概念和指令分类是实现的基础。1. 方法类型与调用指令JVM 提供的方法调用指令及其适用场景如下:指令名称适用场景特点invoke_static调用静态方法编译期确定目标方法,直接通过类名调用invoke_special调用构造器、super 方法、private 方法编译期确定目标方法,不涉及动态绑定invoke_virtual调用非静态方法(除 private 和构造器外)运行期通过对象实际类型查找方法(动态绑定,支持多态)invoke_interface调用接口方法运行期查找实现接口的具体方法,比 invoke_virtual 多一步接口适配invoke_dynamic动态调用(如 Lambda 表达式、方法引用)JDK 8 新增,支持动态语言特性(本章暂不实现)核心区别:静态方法和 private 方法通过 “静态绑定” 在编译期确定目标方法;实例方法通过 “动态绑定” 在运行期根据对象实际类型查找方法,这是多态特性的核心实现。2. 方法调用的核心流程无论哪种调用指令,方法调用的基本流程一致,可概括为:解析常量池中的方法符号引用,获取目标方法的直接引用;从操作数栈弹出方法参数(按调用约定顺序);创建目标方法的栈帧并压入虚拟机栈;将弹出的参数放入新栈帧的局部变量表(参数传递);执行目标方法的字节码指令;方法执行完毕后,通过返回指令弹出当前栈帧,将返回值推入调用方栈帧的操作数栈。二、方法符号引用解析方法调用的第一步是将常量池中的 “方法符号引用”(编译期的间接引用)解析为 “直接引用”(运行期的内存地址),这是后续调用的基础。1. 非接口方法的解析流程以 MethodRef(类方法符号引用)为例,解析逻辑如下:// ResolveMethod 将方法符号引用解析为直接引用(Method 实例) func (r *MethodRef) ResolveMethod() *Method { if r.method == nil { r.resolveMethodRef() // 未解析则执行解析逻辑 } return r.method } // resolveMethodRef 实际执行方法解析 func (r *MethodRef) resolveMethodRef() { currentClass := r.cp.class // 引用所在的当前类 methodClass := r.ResolveClass() // 解析方法所属的类(符号引用→直接引用) // 校验:方法所属类不能是接口(接口方法需用 InterfaceMethodref) if methodClass.IsInterface() { panic("java.lang.IncompatibleClassChangeError") } // 查找目标方法(在类、父类、接口中递归匹配) method := lookupMethod(methodClass, r.name, r.descriptor) if method == nil { panic("java.lang.NoSuchMethodError") // 方法未找到 } // 校验访问权限(当前类是否有权限调用目标方法) if !method.isAccessibleTo(currentClass) { panic("java.lang.IllegalAccessError") } r.method = method // 保存解析结果(直接引用) }2. 方法查找逻辑方法查找需在类本身、父类及实现的接口中递归匹配,确保找到正确的方法:// lookupMethod 查找类或接口中的方法 func lookupMethod(class *Class, name string, descriptor string) *Method { // 1. 在当前类中查找 method := LookupMethodInClass(class, name, descriptor) if method != nil { return method } // 2. 在接口中查找(递归检查所有实现的接口) return lookupMethodInInterface(class.interfaces, name, descriptor) } // LookupMethodInClass 在单个类中查找方法(按名称和描述符匹配) func LookupMethodInClass(class *Class, name string, descriptor string) *Method { // 遍历类的所有方法,匹配名称和描述符 for _, method := range class.methods { if method.name == name && method.descriptor == descriptor { return method } } // 类中未找到,递归查找父类 if class.superClass != nil { return LookupMethodInClass(class.superClass, name, descriptor) } return nil } // lookupMethodInInterface 在接口中递归查找方法 func lookupMethodInInterface(ifaces []*Class, name string, descriptor string) *Method { for _, iface := range ifaces { // 遍历接口的所有方法 for _, method := range iface.methods { if method.name == name && method.descriptor == descriptor { return method } } // 接口未找到,递归查找父接口 method := lookupMethodInInterface(iface.interfaces, name, descriptor) if method != nil { return method } } return nil }关键逻辑:方法查找遵循 “类优先于接口”“子类优先于父类” 的原则,确保符合 Java 的方法继承和重写规则。三、方法调用与参数传递解析到目标方法后,需创建栈帧、传递参数并执行方法,这是方法调用的核心执行阶段。1. 方法调用的核心实现// InvokeMethod 执行方法调用的核心逻辑 func InvokeMethod(invokerFrame *rtda.Frame, method *heap.Method) { thread := invokerFrame.Thread() // 获取当前线程 // 1. 创建目标方法的栈帧(根据方法的 maxLocals 和 maxStack 初始化) newFrame := thread.NewFrame(method) // 2. 将新栈帧压入虚拟机栈 thread.PushFrame(newFrame) // 3. 参数传递:从调用方操作数栈弹出参数,放入新栈帧的局部变量表 argSlotCount := int(method.ArgSlotCount()) // 获取参数占用的槽位总数 if argSlotCount > 0 { // 从后往前弹出参数(栈是先进后出,参数顺序需保持一致) for i := argSlotCount - 1; i >= 0; i-- { slot := invokerFrame.OperandStack().PopSlot() // 弹出参数 newFrame.LocalVars().SetSlot(uint(i), slot) // 存入局部变量表 } } // 特殊处理:跳过 Native 方法(本章暂不实现 Native 方法逻辑) if method.IsNative() { if method.Name() == "registerNatives" { // 忽略 Object 类的 registerNatives 方法(无实际逻辑) thread.PopFrame() } else { panic(fmt.Sprintf("未实现 Native 方法:%v.%v%v", method.Class().Name(), method.Name(), method.Descriptor())) } } }参数传递细节:方法的参数数量和类型通过描述符(如 (ILjava/lang/String;)V 表示 2 个参数)确定,ArgSlotCount() 计算参数占用的槽位总数(long 和 double 占 2 个槽位);由于操作数栈是 “先进后出” 结构,参数需从后往前弹出,才能按正确顺序存入局部变量表(索引 0 对应第一个参数)。四、返回指令:结果传递与栈帧管理方法执行完毕后,通过返回指令将结果传递给调用方,并弹出当前栈帧,恢复调用方的执行。1. 返回指令的实现(以 ireturn 为例)ireturn 用于返回 int 类型结果,其他类型(如 long、float)的返回指令逻辑类似:// IRETURN 返回 int 类型结果 type IRETURN struct { base.NoOperandsInstruction } func (i *IRETURN) Execute(frame *rtda.Frame) { thread := frame.Thread() // 1. 弹出当前方法的栈帧 currentFrame := thread.PopFrame() // 2. 从当前栈帧的操作数栈弹出返回值 result := currentFrame.OperandStack().PopInt() // 3. 将返回值推入调用方栈帧的操作数栈 invokerFrame := thread.TopFrame() invokerFrame.OperandStack().PushInt(result) }2. 不同类型返回指令的共性所有返回指令的核心流程一致,差异仅在于返回值的类型处理:lreturn:返回 long 类型,弹出 8 字节值并推入调用方栈;freturn/dreturn:返回 float/double 类型,通过浮点转码处理;areturn:返回引用类型,弹出对象引用并推入调用方栈;return:无返回值(void),仅弹出当前栈帧。五、核心方法调用指令实现不同的方法调用指令对应不同的解析和执行逻辑,以下以 invoke_virtual(动态绑定核心指令)为例说明。1. invoke_virtual 指令:支持多态的实例方法调用// INVOKE_VIRTUAL 调用实例方法(动态绑定) type INVOKE_VIRTUAL struct { base.Index16Instruction // 包含常量池索引(指向方法符号引用) } func (i *INVOKE_VIRTUAL) Execute(frame *rtda.Frame) { currentClass := frame.Method().Class() cp := currentClass.ConstantPool() // 1. 解析方法符号引用 methodRef := cp.GetConstant(i.Index).(*heap.MethodRef) resolvedMethod := methodRef.ResolveMethod() // 校验:不能调用静态方法 if resolvedMethod.IsStatic() { panic("java.lang.IncompatibleClassChangeError") } // 2. 获取操作数栈中的对象引用(this) // 从栈顶弹出参数后,剩余的第一个元素为 this 引用 ref := frame.OperandStack().GetRefFromTop(resolvedMethod.ArgSlotCount() - 1) if ref == nil { // 特殊处理:支持 System.out.println 等常用方法(简化实现) if methodRef.Name() == "println" { _println(frame.OperandStack(), methodRef.Descriptor()) return } panic("java.lang.NullPointerException") // 对象为 null 时调用方法 } // 3. 权限校验(protected 方法的访问控制) if resolvedMethod.IsProtected() && resolvedMethod.Class().IsSuperClassOf(currentClass) && resolvedMethod.Class().GetPackageName() != currentClass.GetPackageName() && ref.Class() != currentClass && !ref.Class().IsSubClassOf(currentClass) { panic("java.lang.IllegalAccessError") } // 4. 动态绑定:根据对象实际类型查找方法(而非编译期类型) methodToBeInvoked := heap.LookupMethodInClass(ref.Class(), methodRef.Name(), methodRef.Descriptor()) if methodToBeInvoked == nil || methodToBeInvoked.IsAbstract() { panic("java.lang.AbstractMethodError") // 方法未实现 } // 5. 执行方法调用 base.InvokeMethod(frame, methodToBeInvoked) }动态绑定核心:invoke_virtual 不直接使用符号引用解析的方法,而是根据对象的实际类型(ref.Class())重新查找方法,这确保了运行时调用的是子类重写的方法,实现多态特性。六、改进解释器:支持日志与多方法执行为便于调试和跟踪方法调用流程,改进解释器以支持指令日志输出,并完善多方法连续执行的逻辑。1. 解释器主循环优化// loop 解释器主循环,支持指令日志输出 func loop(thread *rtda.Thread, logInst bool) { reader := &base.BytecodeReader{} for { frame := thread.CurrentFrame() // 获取当前栈顶栈帧 pc := frame.NextPC() // 获取程序计数器 thread.SetPC(pc) // 解析指令 reader.Reset(frame.Method().Code(), pc) opcode := reader.ReadInt8() // 读取 opcode inst := instructions.NewInstruction(byte(opcode)) // 创建指令实例 inst.FetchOperands(reader) // 读取操作数 // 输出指令日志(若启用) if logInst { logInstruction(frame, inst) } // 执行指令并更新程序计数器 frame.SetNextPC(reader.PC()) inst.Execute(frame) // 线程栈为空时结束循环(所有方法执行完毕) if thread.IsStackEmpty() { break } } }日志功能:通过 verbose:inst 参数启用指令日志,可输出当前执行的指令、PC 地址等信息,便于跟踪方法调用流程和调试问题。七、类初始化:触发与执行 <clinit> 方法类在首次被使用时需执行初始化(执行类构造器 <clinit> 方法),方法调用是触发初始化的重要场景之一。1. 类初始化的触发条件以下情况会触发类初始化(执行 <clinit> 方法):执行 new 指令创建对象时,类未初始化;执行 putstatic/getstatic 指令访问静态字段时,类未初始化;执行 invoke_static 指令调用静态方法时,类未初始化;初始化子类时,父类未初始化;反射操作访问类时,类未初始化。2. 类初始化逻辑实现// InitClass 执行类初始化(触发 <clinit> 方法) func InitClass(thread *rtda.Thread, class *heap.Class) { class.StartInit() // 标记类开始初始化(防止重复初始化) scheduleClinit(thread, class) // 执行类构造器 <clinit> 方法 initSuperClass(thread, class) // 递归初始化父类 } // scheduleClinit 计划执行 <clinit> 方法 func scheduleClinit(thread *rtda.Thread, class *heap.Class) { clinit := class.GetClinitMethod() // 获取 <clinit> 方法 if clinit != nil { // 创建 <clinit> 方法的栈帧并压入栈 frame := thread.NewFrame(clinit) thread.PushFrame(frame) } } // initSuperClass 初始化父类(确保父类先于子类初始化) func initSuperClass(thread *rtda.Thread, class *heap.Class) { if !class.IsInterface() { superClass := class.SuperClass() if superClass != nil && !superClass.InitStarted() { InitClass(thread, superClass) // 递归初始化父类 } } }核心逻辑:类初始化通过执行 <clinit> 方法完成,该方法由编译器自动生成,包含静态变量初始化和静态代码块逻辑。初始化过程中会确保父类先于子类初始化,符合 Java 的类加载规范。本章小结本章实现了 JVM 方法调用与返回的核心机制,重点包括:方法调用指令分类:区分 invoke_static、invoke_virtual 等指令的适用场景,支持静态绑定和动态绑定;符号引用解析:通过递归查找类、父类和接口,将方法符号引用解析为直接引用;参数传递与栈帧管理:创建栈帧并通过操作数栈传递参数,确保方法调用的上下文正确;返回指令实现:通过不同类型的返回指令传递结果,弹出当前栈帧恢复调用方执行;类初始化触发:在方法调用等场景中触发类初始化,执行 <clinit> 方法完成静态变量初始化。这些机制共同支撑了 JVM 对面向对象特性(如多态、继承)的支持,是实现完整 Java 程序执行的关键基础。下一章将进一步完善异常处理和数组支持,增强 JVM 的功能完整性。源码地址:https://github.com/Jucunqi/jvmgo.git
2025年06月30日
27 阅读
0 评论
1 点赞
1
2
3
4
5