首页
关于这个博客
Search
1
Java 实现Google 账号单点登录(OAuth 2.0)全流程解析
231 阅读
2
Spring AI 无法获取大模型深度思考内容?解决方案来了
202 阅读
3
微信小程序实现页面返回前确认弹窗:兼容左上角返回与右滑返回
91 阅读
4
服务器遭遇 XMRig 挖矿程序入侵排查与清理全记录
66 阅读
5
解决 Mac 版 PicGo 无法打开问题:“已损坏,无法打开” 报错处理指南
38 阅读
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
登录
Search
标签搜索
java虚拟机
JVM
保姆级教程
Java
Spring AI
SpringBoot
Nginx
WebFlux
Spring
cdn
https
dcdn
网站加速
Tool
图片导出
服务部署
源码解析
单点登录
google
sso
Luca Ju
累计撰写
35
篇文章
累计收到
1
条评论
首页
栏目
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
页面
关于这个博客
搜索到
35
篇与
的结果
2025-08-06
Ubuntu环境Mysql安装的保姆级教程
一、Ubuntu简介Ubuntu 是一款基于 Debian 的开源 Linux 操作系统,由南非企业家马克・沙特尔沃思(Mark Shuttleworth)创立的 Canonical 公司主导开发,首次发布于 2004 年。其名称源自非洲祖鲁语或科萨语,意为 “人性” 或 “共享的关怀”,体现了开源社区协作共享的理念。二、Mysql简介MySQL 是一款开源的关系型数据库管理系统(RDBMS),由瑞典 MySQL AB 公司开发,后被 Sun 公司收购,最终随 Sun 并入 Oracle 公司。它以高性能、稳定性、易用性和开源免费为核心优势,成为全球最流行的数据库之一,广泛应用于 Web 开发、企业级应用、数据存储等场景。三、安装 本文介绍Mysql的在线安装过程1、更新软件包sudo apt update2、下载Mysql# 安装最新版本 sudo apt install -y mysql-server # 安装指定版本 sudo apt install -y mysql-server-8.03、启动Mysql、并设置为开机运行sudo systemctl start mysql sudo systemctl enable mysql4、检查mysql状态sudo systemctl status mysql如果出现active(running),则表示安装启动成功了四、允许远程可视化工具连接1、修改 MySQL 配置文件:找到配置文件,Linux 系统一般位于/etc/mysql/mysql.conf.d/mysqld.cnf,可以使用命令mysql --help | grep "my.cnf"查找。打开配置文件,找到bind-address = 127.0.0.1这一行,将其改为bind-address = 0.0.0.0,表示监听所有 IP 地址上的连接请求。如果是生产环境,也可指定特定 IP,如bind-address = 192.168.1.100。2、创建或授权远程访问用户: 1) 登录 MySQL 服务打开终端,执行以下命令登录 MySQL ,输入 root 密码完成认证(若首次安装无密码,直接回车即可)mysql -u root -p2) 创建并授权新用户(推荐方式)2.1 创建远程访问用户通过 SQL 命令创建新用户,支持自定义用户名、密码,以及控制可访问的 IP 范围:-- 允许任意 IP 访问(% 代表所有 IP) CREATE USER 'remote_user'@'%' IDENTIFIED BY 'your_password'; -- 仅允许指定 IP(如 192.168.1.100)访问,按需替换 -- CREATE USER 'remote_user'@'192.168.1.100' IDENTIFIED BY 'your_password'; 说明: - remote_user:自定义的用户名,可根据需求修改 - your_password:设置的用户密码,建议复杂度足够(字母 + 数字 + 特殊字符) - % 或 192.168.1.100:控制允许登录的 IP 范围,% 适合信任网络,指定 IP 更安全2.2 授予用户远程访问权限创建用户后,需赋予权限并刷新使配置生效:-- 授予所有权限(生产环境可按需缩小权限范围,如指定数据库) GRANT ALL PRIVILEGES ON *.* TO 'remote_user'@'%' WITH GRANT OPTION; -- 刷新权限,让配置立即生效 FLUSH PRIVILEGES; 说明: - ALL PRIVILEGES:授予全部权限(谨慎使用,生产环境建议细化,如 GRANT SELECT,INSERT ON db_name.* TO ... ) - .:表示所有数据库的所有表,可替换为 db_name.* 限制特定数据库3) 修改已有用户权限(备用方案)若不想新建用户,可直接修改现有用户的远程访问权限:-- 允许任意 IP 访问已有用户(替换 existing_user 和 password) GRANT ALL PRIVILEGES ON *.* TO 'existing_user'@'%' IDENTIFIED BY 'password'; -- 刷新权限 FLUSH PRIVILEGES; 说明: - existing_user:替换为实际的已有用户名 - password:替换为该用户对应的密码 - 此操作会同时重置用户密码(若无需改密码,需保持原密码一致)注意如果安装的是msyql8.0及以上版本GRANT 语句的语法有变化,不再支持在 GRANT 语句里直接用 IDENTIFIED BY 创建 / 修改用户密码,得拆分成两步操作。解决方法(分两步执行)1. 先创建 / 修改用户并设置密码(用 CREATE USER 或 ALTER USER):-- 若用户 root@% 不存在,创建并设密码 CREATE USER 'root'@'%' IDENTIFIED BY 'yourpassword'; -- 若用户已存在,修改密码用这个: -- ALTER USER 'root'@'%' IDENTIFIED BY 'yourpassword'; 2. 再给用户授权:GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'; -- 刷新权限让修改生效 FLUSH PRIVILEGES; ## 3、重启 MySQL 服务:使配置生效执行命令sudo systemctl restart mysql4、配置防火墙或安全组,开放3306端口五、远程连接验证结果如图
2025年08月06日
23 阅读
0 评论
0 点赞
2025-08-05
从零起步,Ubuntu环境搭建Typecho个人博客的保姆级教程
一、确认服务器环境是否满足要求根据Typecho官网要求,运行需要基础环境支持,先检查是否安装以下组件:Web 服务器:Nginx 或 Apache(推荐 Nginx,更轻量)。PHP:5.6 及以上版本(推荐 7.2+),并需启用必要扩展(如 pdo_mysql、mbstring、json、gd 等)。数据库:MySQL 或 MariaDB(用于存储博客数据)。 官网链接: https://typecho.org/二、快速安装基础环境默认大家没有任何环境,从零开始# 安装 Nginx、PHP、MySQL sudo apt update sudo apt install nginx php php-fpm php-mysql php-mbstring php-gd php-json mysql-server三、下载并上传至服务器根据官网下载链接,将下载的zip压缩包,放在服务器自定义的目录下,以/usr/local/typecho 为例四、配置 Web 服务器(以 Nginx 为例)Web 服务器需要将访问请求指向 Typecho 的安装目录(/usr/local/typecho),并处理 PHP 解析。1、创建 Nginx 配置文件sudo nano /etc/nginx/sites-available/typecho # 新建配置文件2、写入配置内容根据你的服务器 IP 或域名修改以下内容(假设用 IP 访问,或已解析域名):server { listen 80; # 监听 80 端口(HTTP) server_name your_domain.com; # 替换为你的域名或服务器 IP(如 1.2.3.4) # 网站根目录指向 Typecho 解压目录 root /usr/local/typecho; index index.php index.html; # 默认索引文件 # 关键:Typecho 伪静态规则(必须添加) location / { # 如果请求的文件或目录不存在,将请求转发给 index.php 处理 if (!-e $request_filename) { rewrite ^(.*)$ /index.php?$1 last; } } # PHP 解析配置(保持不变) location ~ \.php$ { fastcgi_pass unix:/run/php/php8.1-fpm.sock; # 替换为你的 PHP 版本 fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } # 禁止访问隐藏文件(保持不变) location ~ /\. { deny all; access_log off; log_not_found off; } # 静态资源缓存(保持不变) location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { expires 30d; add_header Cache-Control "public, max-age=2592000"; } # 日志配置(可选) access_log /var/log/nginx/typecho_access.log; error_log /var/log/nginx/typecho_error.log; }注意:fastcgi_pass 中的 PHP 版本需与你安装的一致(可通过 ls /run/php/ 查看实际 sock 文件名)。3、启用配置并重启 Nginxsudo ln -s /etc/nginx/sites-available/typecho /etc/nginx/sites-enabled/ # 启用站点 sudo nginx -t # 检查配置是否有误 sudo systemctl restart nginx # 重启 Nginx五、设置 Typecho 目录权限Typecho 需要对部分目录有写入权限(如配置文件、缓存、上传目录),否则安装时会提示 “无法写入配置文件”。执行以下命令修改权限:# 递归设置目录所有者为 Web 服务器用户(Nginx 通常用 www-data) sudo chown -R www-data:www-data /usr/local/typecho # 确保关键目录可写(可选,根据实际提示调整) sudo chmod -R 755 /usr/local/typecho/usr # 上传和缓存目录 sudo chmod 755 /usr/local/typecho # 根目录(确保能生成 config.inc.php)六、准备数据库Typecho 需要数据库存储文章、用户等数据,需提前创建数据库和用户。1、登录mysqlsudo mysql -u root -p # 输入 root 密码(首次安装可能无密码,直接回车)2、执行sql命令创建数据库和用户-- 创建数据库(名称自定义,如 typecho_db) CREATE DATABASE typecho_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 创建数据库用户(用户名和密码自定义,如 typecho_user / your_password) CREATE USER 'typecho_user'@'localhost' IDENTIFIED BY 'your_password'; -- 授权用户访问数据库 GRANT ALL PRIVILEGES ON typecho_db.* TO 'typecho_user'@'localhost'; -- 刷新权限 FLUSH PRIVILEGES; -- 退出 MySQL exit;七、通过浏览器访问并完成安装打开本地电脑的浏览器,输入服务器的 IP 地址 或 域名(如 http://1.2.3.4 或 http://your_domain.com)。如果配置正确,会看到 Typecho 的安装向导页面。
2025年08月05日
18 阅读
0 评论
1 点赞
2025-08-05
在 Ubuntu 环境下安装与配置 Nginx 的完整指南
Nginx简介 Nginx是一款高性能的开源 Web 服务器、反向代理服务器、负载均衡器和 HTTP 缓存工具。它由俄罗斯程序员伊戈尔・赛索耶夫(Igor Sysoev)于 2004 年首次公开发布,最初设计的目标是解决高并发场景下的性能瓶颈,如今已成为全球最流行的服务器软件之一,被 Netflix、Airbnb、GitHub、腾讯、阿里等众多大型企业广泛使用。本文将详细介绍如何在 Ubuntu 系统中安装、配置并优化 Nginx,适合初学者入门参考。一、安装 NginxUbuntu 的官方软件仓库中已经包含了 Nginx,我们可以通过 APT 包管理器轻松安装。更新系统包列表首先确保系统包列表是最新的:sudo apt update安装 Nginx执行以下命令安装 Nginx:sudo apt install nginx验证安装是否成功安装完成后,Nginx 会自动启动。可以通过以下命令检查其运行状态:sudo systemctl status nginx如果看到 "active (running)" 字样,说明 Nginx 已经成功启动。配置防火墙 4.1 如果你的 Ubuntu 系统启用了 UFW 防火墙,需要允许 HTTP(80 端口)和 HTTPS(443 端口)流量:sudo ufw allow 'Nginx Full'可以通过以下命令验证防火墙规则:sudo ufw status 4.2 如果你跟我一样,使用的是云服务器,那么只需要在安全组中开放80端口即可二、Nginx的基本操作掌握以下基本命令可以帮助你管理 Nginx 服务:启动 Nginx:sudo systemctl start nginx停止 Nginx:sudo systemctl stop nginx重启 Nginx:sudo systemctl restart nginx重新加载配置(不中断服务):sudo systemctl reload nginx设置开机自启动:sudo systemctl enable nginx禁止开机自启动:sudo systemctl disable nginx三、Nginx 的配置文件结构Nginx 的配置文件位于/etc/nginx目录下,主要文件和目录包括:/etc/nginx/nginx.conf:主配置文件/etc/nginx/sites-available/:存储所有网站的配置文件/etc/nginx/sites-enabled/:存储启用的网站配置(通常是指向 sites-available 目录的软链接)/etc/nginx/conf.d/:可以存放额外的配置片段/etc/nginx/mime.types:定义 MIME 类型这种结构允许我们为每个网站创建独立的配置文件,便于管理。四、配置一个基本的 Web 站点下面我们创建一个简单的 Web 站点配置:创建网站目录首先为网站创建一个目录,并设置适当的权限:sudo mkdir -p /var/www/example.com/html sudo chown -R $USER:$USER /var/www/example.com/html sudo chmod -R 755 /var/www创建测试页面在网站目录下创建一个简单的 HTML 文件:nano /var/www/example.com/html/index.html添加以下内容:预览 <!DOCTYPE html> <html> <head> <title>Welcome to Example.com!</title> </head> <body> <h1>Success! The example.com server block is working!</h1> </body> </html>保存并关闭文件。创建服务器配置文件在sites-available目录下创建一个新的配置文件:sudo nano /etc/nginx/sites-available/example.com添加以下配置:server { listen 80; listen [::]:80; root /var/www/example.com/html; index index.html index.htm index.nginx-debian.html; server_name example.com www.example.com; # 替换为你的域名或服务器 IP(如 1.2.3.4) location / { try_files $uri $uri/ =404; } }这个配置指定了:监听 80 端口(HTTP)网站文件根目录默认索引文件服务器域名基本的请求处理规则启用站点配置通过创建软链接将配置文件链接到sites-enabled目录:sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/检查配置文件语法在应用配置之前,先检查语法是否正确:sudo nginx -t如果输出 "nginx: configuration file /etc/nginx/nginx.conf test is successful",说明配置没有问题。重新加载 Nginx使配置生效:sudo systemctl reload nginx测试网站如果你的域名已经解析到服务器 IP,现在可以通过浏览器访问http://example.com来查看效果。如果没有域名,可以修改本地hosts文件进行测试。五、注意如果访问80端口,显示Apache2默认页面,说明80端口被Apache服务器占用了,可以通过一下命令停止Apache服务器sudo systemctl stop apache2再次访问就可以成功访问到我们部署的页面了
2025年08月05日
11 阅读
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日
5 阅读
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日
7 阅读
0 评论
1 点赞
1
...
3
4
5
...
7