<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>Ju Zi&apos;s blog</title><description>A fall into the pit, a gain in your wit.</description><link>https://juzzi.qzz.io</link><item><title>Skill介绍</title><link>https://juzzi.qzz.io/blog/ai/ai-skills</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/ai/ai-skills</guid><description>skill是目前大多AI工具都支持的功能，可以扩展AI的能力。</description><pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是 Skill&lt;/h2&gt;
&lt;p&gt;Skill（技能）是 Claude Code及常用Agent的扩展能力模块，每个 skill 都是一个独立的功能单元，包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;触发条件&lt;/strong&gt;：定义何时可以使用该 skill&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作流程&lt;/strong&gt;：预定义的处理步骤和检查清单&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具指引&lt;/strong&gt;：skill 相关的操作指导&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Skill 分为以下几种来源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内置 skill&lt;/strong&gt;：Claude Code 自带的功能（如 &lt;code&gt;/help&lt;/code&gt;、&lt;code&gt;/clear&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;插件 skill&lt;/strong&gt;：通过插件安装的第三方扩展&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自定义 skill&lt;/strong&gt;：通过拷贝skill文件夹到指定目录&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;使用 Skill&lt;/h2&gt;
&lt;h3&gt;调用方式&lt;/h3&gt;
&lt;p&gt;在 Claude Code 中，使用斜杠命令调用 skill：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/&amp;#x3C;skill_name&gt; [参数]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/xlsx @野外补兵细案.xlsx 总结下这个文件内容&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/pdf document.pdf 提取第三章内容&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/pptx 创建一个关于AI的演示文稿&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Skill 查找&lt;/h3&gt;
&lt;p&gt;不确定有哪些 skill 可用？输入 &lt;code&gt;/&lt;/code&gt; 可以看到所有可用 skill 的列表。&lt;/p&gt;
&lt;h2&gt;插件 Skill&lt;/h2&gt;
&lt;p&gt;Claude 可以通过安装插件来使用更多 skill。&lt;/p&gt;
&lt;h3&gt;1. 添加 marketplace&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/plugin marketplace add obra/superpowers-marketplace
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 安装插件&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;方式一：命令安装&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/plugin install superpowers@superpowers-marketplace
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方式二：浏览安装&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用 &lt;code&gt;/plugin&lt;/code&gt; 进入插件管理&lt;/li&gt;
&lt;li&gt;选择 &lt;code&gt;Discover&lt;/code&gt; 进行搜索，或选择 &lt;code&gt;Marketplaces&lt;/code&gt; 根据市场筛选&lt;/li&gt;
&lt;li&gt;选择市场后，再选择 &lt;code&gt;Browse plugins&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;按空格勾选插件，再按 &lt;code&gt;i&lt;/code&gt; 键安装&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3. 重载插件&lt;/h3&gt;
&lt;p&gt;安装后使用命令 &lt;code&gt;/reload-plugins&lt;/code&gt; 重载，或重新打开 Claude 即可使用插件。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：要使用插件中的 skill，&lt;strong&gt;必须退出 Claude 并重新进入&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Claude 自带了官方的 marketplace：&lt;code&gt;claude-plugins-official&lt;/code&gt;，无需再添加，可以直接安装其中的插件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;自定义 Skill&lt;/h2&gt;
&lt;p&gt;也可以将 skill 放入指定路径使用，适用于自定义插件。&lt;/p&gt;
&lt;p&gt;注意：&lt;code&gt;&amp;#x3C;name&gt;&lt;/code&gt; 必须和 &lt;code&gt;SKILL.md&lt;/code&gt; 中的 &lt;code&gt;name&lt;/code&gt; &lt;strong&gt;完全一致&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Claude Code&lt;/h3&gt;
&lt;p&gt;Claude Code启动时，会读取&lt;code&gt;~/.claude/skills&lt;/code&gt;和&lt;code&gt;.claude/skills&lt;/code&gt;目录下的所有skill。安装skill时，默认是安装到&lt;code&gt;~/.agents/skills&lt;/code&gt;下，为了claude code也能使用，可以创建符号链接。&lt;/p&gt;
&lt;h4&gt;macOS/Linux&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;ln -s ~/.agents/skills ~/.claude/skills
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Windows&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;MSYS=winsymlinks:nativestrict ln -s /c/Users/&amp;#x3C;name&gt;/.agents/skills /c/Users/&amp;#x3C;name&gt;/.claude/skills
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;find-skills&lt;/h3&gt;
&lt;p&gt;从&lt;a href=&quot;https://skills.sh/&quot;&gt;skills.sh&lt;/a&gt;中查找skill并自动安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx skills add https://github.com/vercel-labs/skills --skill find-skills
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>MCP (Model Context Protocol) 使用指南</title><link>https://juzzi.qzz.io/blog/ai/ai-mcps</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/ai/ai-mcps</guid><description>介绍 MCP 的工作原理、配置方法及使用注意事项</description><pubDate>Sat, 28 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;MCP 介绍&lt;/h3&gt;
&lt;p&gt;MCP（Model Context Protocol）是 Anthropic 推出的开放协议，用于连接 AI 模型与外部数据源和工具。AI 可以通过连接远端 MCP 服务器来调用接口，从而增强 AI 的行为能力，实现 AI 本身无法实现的功能。&lt;/p&gt;
&lt;h4&gt;工作原理&lt;/h4&gt;
&lt;p&gt;MCP 采用客户端/服务器架构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MCP Host&lt;/strong&gt;（如 Claude Code）：发起请求的 AI 应用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP Client&lt;/strong&gt;：运行在 Host 内的客户端，与 Server 保持 1:1 连接&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP Server&lt;/strong&gt;：独立的本地进程，通过 stdio 与 Client 通信，提供资源或工具&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;核心概念&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Resources&lt;/strong&gt;：MCP Server 暴露的数据资源，AI 可以读取（如文件、API 响应）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tools&lt;/strong&gt;：MCP Server 提供的可调用函数，AI 可以执行（如搜索、转换）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompts&lt;/strong&gt;：预定义的提示模板，可快速复用复杂提示&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;与 Function Calling 的区别&lt;/h4&gt;
&lt;p&gt;| | MCP | Function Calling |
|---|---|---|
| 协议 | 开放标准 | 厂商特定 |
| 范围 | 跨平台、跨厂商 | 仅限同一 AI 厂商 |
| 部署 | 本地 MCP Server | 云端函数 |
| 扩展性 | 可连接任何支持 MCP 的数据源 | 受限于厂商提供的函数库 |&lt;/p&gt;
&lt;h3&gt;配置方法&lt;/h3&gt;
&lt;h4&gt;添加MCP&lt;/h4&gt;
&lt;p&gt;使用命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude mcp add -s user &amp;#x3C;mcp_name&gt; &amp;#x3C;command&gt; &amp;#x3C;args&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令说明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Usage: claude mcp add [options] &amp;#x3C;name&gt; &amp;#x3C;commandOrUrl&gt; [args...]

Options:
  --callback-port &amp;#x3C;port&gt;       Fixed port for OAuth callback (for servers requiring pre-registered redirect URIs)
  --client-id &amp;#x3C;clientId&gt;       OAuth client ID for HTTP/SSE servers
  --client-secret              Prompt for OAuth client secret (or set MCP_CLIENT_SECRET env var)
  -e, --env &amp;#x3C;env...&gt;           Set environment variables (e.g. -e KEY=value)
  -H, --header &amp;#x3C;header...&gt;     Set WebSocket headers (e.g. -H &quot;X-Api-Key: abc123&quot; -H &quot;X-Custom: value&quot;)
  -h, --help                   Display help for command
  -s, --scope &amp;#x3C;scope&gt;          Configuration scope (local, user, or project) (default: &quot;local&quot;)
  -t, --transport &amp;#x3C;transport&gt;  Transport type (stdio, sse, http). Defaults to stdio if not specified.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;code&gt;claude mcp add -s user wanyi-watermark uvx wanyi-watermark&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果需要传入环境变量，例如API_KEY等：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude mcp add &quot;-e&amp;#x3C;ENV&gt;=&amp;#x3C;value&gt;&quot; -s user &amp;#x3C;mcp_name&gt; &amp;#x3C;command&gt; &amp;#x3C;args&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;code&gt;claude mcp add &quot;-eDASHSCOPE_API_KEY=sk-xxxx&quot; -s user wanyi-watermark uvx wanyi-watermark&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要将&lt;code&gt;-exxx&lt;/code&gt;的部分用引号包裹起来，否则会提示：&lt;code&gt;error: missing required argument &apos;name&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-e&lt;/code&gt;后面与&lt;code&gt;&amp;#x3C;ENV&gt;&lt;/code&gt;之间不能有空格&lt;/li&gt;
&lt;li&gt;如果不添加&lt;code&gt;-s user&lt;/code&gt;，则会将mcp安装到当前目录。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;移除MCP&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;claude mcp remove &amp;#x3C;mcp_name&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用注意事项&lt;/h3&gt;
&lt;h4&gt;安全&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API Key 管理&lt;/strong&gt;：敏感信息使用环境变量，不要硬编码在配置文件中&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码库安全&lt;/strong&gt;：项目级 &lt;code&gt;.mcp.json&lt;/code&gt; 可能会提交到 Git，建议将其加入 &lt;code&gt;.gitignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权限控制&lt;/strong&gt;：MCP Server 可能拥有数据访问权限，只启用必要的服务&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;调试&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;检查状态&lt;/strong&gt;：运行 &lt;code&gt;claude mcp list&lt;/code&gt; 查看已配置的 MCP 服务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查看日志&lt;/strong&gt;：检查 MCP Server 的输出日志排查连接问题&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逐步排查&lt;/strong&gt;：如果 MCP 不工作，先确认配置文件格式正确，再检查 Server 是否正常运行&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;隔离&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;项目级配置&lt;/strong&gt;：&lt;code&gt;.mcp.json&lt;/code&gt; 仅在所在项目目录及子目录生效&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局配置&lt;/strong&gt;：&lt;code&gt;~/.claude/mcp.json&lt;/code&gt; 对所有项目生效&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优先级&lt;/strong&gt;：项目级配置与全局配置会合并，项目级配置优先&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;兼容性&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版本要求&lt;/strong&gt;：确保 Claude Code 版本支持所用 MCP Server 所需的协议版本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖检查&lt;/strong&gt;：部分 MCP Server 依赖 Node.js、Python 等运行时，需提前安装&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统要求&lt;/strong&gt;：注意 Server 是否仅支持特定操作系统（如 Linux/macOS）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;MCP 推荐&lt;/h3&gt;
&lt;h4&gt;wanyi-watermark&lt;/h4&gt;
&lt;p&gt;媒体资源提取，抖音/小红书提取无水印视频/图片，视频逐字稿（视频声音转文本）。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装 uv: &lt;code&gt;curl -LsSf https://astral.sh/uv/install.sh | sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;从&lt;a href=&quot;https://bailian.console.aliyun.com/?spm=a2c4g.11186623.0.0.3eb13ba2yFW3MW&amp;#x26;tab=model#/api-key&quot;&gt;阿里云百炼&lt;/a&gt;获取 API Key（&lt;em&gt;无需&lt;/em&gt;充值付费）。&lt;/li&gt;
&lt;li&gt;配置 MCP Server：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;wanyi-watermark&quot;: {
      &quot;command&quot;: &quot;uvx&quot;,
      &quot;args&quot;: [&quot;wanyi-watermark&quot;],
      &quot;env&quot;: {
        &quot;DASHSCOPE_API_KEY&quot;: &quot;sk-xxxx&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;把 &lt;code&gt;sk-xxxx&lt;/code&gt; 替换成阿里百炼获取到的 API Key。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Claude Code 使用技巧</title><link>https://juzzi.qzz.io/blog/ai/claude-code-tips</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/ai/claude-code-tips</guid><description>提升 Claude Code 使用效率的实践建议。掌握这些技巧可以让你与 AI 协作更加流畅，减少来回确认，快速完成开发任务。</description><pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;明确任务边界&lt;/h3&gt;
&lt;p&gt;与其说&quot;帮我修复这个bug&quot;，不如具体说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;期望的行为是什么&lt;/li&gt;
&lt;li&gt;当前出了什么问题&lt;/li&gt;
&lt;li&gt;相关的文件或代码位置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;修复 note/devel/ai/claude-code.md 中的图片链接失效问题，第45行开始
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;利用上下文&lt;/h3&gt;
&lt;p&gt;Claude Code 能读取项目文件，直接问&quot;这个模块的作用是什么&quot;比复制粘贴代码更高效。&lt;/p&gt;
&lt;p&gt;常用上下文获取方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/read &amp;#x3C;文件路径&gt;    # 让AI读取文件内容
/refactor          # 在代码审查后重构
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分步骤操作&lt;/h3&gt;
&lt;p&gt;复杂任务不要一次性交给 AI，分步骤可以：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;减少上下文膨胀&lt;/li&gt;
&lt;li&gt;及时发现偏差&lt;/li&gt;
&lt;li&gt;更好地控制风险&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;1. &quot;先帮我分析这个函数的逻辑&quot;
2. &quot;基于以上分析，列出需要修改的地方&quot;
3. &quot;按顺序执行修改&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;巧用 slash commands&lt;/h3&gt;
&lt;p&gt;| 命令 | 用途 |
|------|------|
| &lt;code&gt;/help&lt;/code&gt; | 查看所有可用命令 |
| &lt;code&gt;/compact&lt;/code&gt; | 压缩上下文节省 token |
| &lt;code&gt;/model&lt;/code&gt; | 切换模型（Haiku/Sonnet/Opus） |
| &lt;code&gt;/browse&lt;/code&gt; | 开启网页浏览模式 |
| &lt;code&gt;/skills&lt;/code&gt; | 查看可用技能 |&lt;/p&gt;
&lt;h3&gt;内存功能&lt;/h3&gt;
&lt;p&gt;Claude Code 有持久化内存系统，可以在会话间记住：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户偏好&lt;/li&gt;
&lt;li&gt;项目上下文&lt;/li&gt;
&lt;li&gt;工作流程约定&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 &lt;code&gt;/remember&lt;/code&gt; 或让 AI 自动记忆关键信息。&lt;/p&gt;
&lt;h3&gt;安全操作&lt;/h3&gt;
&lt;p&gt;对于敏感操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;删除文件前先确认路径&lt;/li&gt;
&lt;li&gt;destructive 操作会让 AI 主动确认&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;/allow&lt;/code&gt; 或 &lt;code&gt;/deny&lt;/code&gt; 控制工具权限&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;好的 prompt 实践&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;推荐：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;在 note/ 目录下创建一个新的博客文章模板
要求：
- 包含 title、publishDate、description、tags 四个字段
- 保存到 blog/devel/ 目录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;不推荐：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;帮我创建博客模板
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;越具体的指令，输出越符合预期。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Claude Code 安装与配置</title><link>https://juzzi.qzz.io/blog/ai/claude-code</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/ai/claude-code</guid><description>Claude Code 是 Anthropic 官方推出的终端AI编程助手，直接集成在命令行环境中。比网页版更深入代码本身，能真正帮你完成开发任务，而非仅仅回答问题。适合日常开发中的代码生成、调试、重构等场景。</description><pubDate>Wed, 25 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;安装&lt;/h3&gt;
&lt;p&gt;前置条件：安装 &lt;a href=&quot;https://nodejs.org/en/download&quot;&gt;Node.js&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -g @anthropic-ai/claude-code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：如无国外网络，macOS或Linux则不能使用&lt;code&gt;curl -fsSL https://claude.ai/install.sh | bash&lt;/code&gt;来安装。&lt;/p&gt;
&lt;p&gt;验证安装&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置模型&lt;/h3&gt;
&lt;h4&gt;官方模型&lt;/h4&gt;
&lt;p&gt;必须要梯子，首次运行claude时会提示登录，也可以使用&lt;code&gt;/login&lt;/code&gt;命令来登录。&lt;/p&gt;
&lt;h4&gt;第三方模型&lt;/h4&gt;
&lt;p&gt;创建文件：&lt;code&gt;~/.claude/settings.json&lt;/code&gt;，内容为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;env&quot;: {
    &quot;ANTHROPIC_AUTH_TOKEN&quot;: &quot;&amp;#x3C;api_key&gt;&quot;,
    &quot;ANTHROPIC_BASE_URL&quot;: &quot;&amp;#x3C;url&gt;&quot;,
    &quot;ANTHROPIC_MODEL&quot;: &quot;&amp;#x3C;model&gt;&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;将&lt;code&gt;&amp;#x3C;api_key&gt;&lt;/code&gt;替换为实际的api key，从模型提供商那里获取。&lt;/li&gt;
&lt;li&gt;将&lt;code&gt;&amp;#x3C;url&gt;&lt;/code&gt;替换为实际的api地址，从模型提供商那里获取。&lt;/li&gt;
&lt;li&gt;将&lt;code&gt;&amp;#x3C;model&gt;&lt;/code&gt;替换为实际的模型名字，例如MiniMax M2.7。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建文件：&lt;code&gt;~/.claude.json&lt;/code&gt;，内容为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;hasCompletedOnboarding&quot;: true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：如果不设置该属性，启动claude就会提示你登录，无法使用第三方模型。&lt;/p&gt;
&lt;h3&gt;基本使用&lt;/h3&gt;
&lt;h4&gt;启动对话&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;claude                                # 启动默认对话
claude &amp;#x3C;prompt&gt;                       # 直接执行命令
claude --model &amp;#x3C;model&gt;                # 指定模型名字
claude --r &amp;#x3C;session&gt;                  # 恢复之前的会话，不填会话名会打开菜单供选择
claude --dangerously-skip-permissions # 跳过危险权限确认
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;核心命令&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/help      # 显示帮助信息
/compact   # 压缩当前会话上下文
/clear     # 清空上下文
/rewind    # 恢复代码和对话到上一个检查点
/model     # 切换模型
/exit      # 退出会话
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;模式切换&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/browse    # 启用浏览模式，让AI主动浏览网页
/eval      # 单行评估模式
/improve   # 改进模式，针对当前选中内容
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;管道操作&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;cat file.txt | claude &quot;解释这段代码&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;指定文件&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@文件名 要做的操作
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以直接告诉 AI 要操作哪个文件，更加高效。&lt;/p&gt;
&lt;h3&gt;常见问题&lt;/h3&gt;
&lt;h4&gt;Q: 提示 &quot;Permission denied&quot;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; 检查 &lt;code&gt;~/.claude&lt;/code&gt; 目录权限，确保当前用户有读写权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod 755 ~/.claude
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Q: 第三方模型调用失败&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确认 API Key 有效且未过期&lt;/li&gt;
&lt;li&gt;检查 &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt; 是否正确（有些需要完整的 v1 路径）&lt;/li&gt;
&lt;li&gt;确认模型名称是否与提供商支持的一致&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Q: 上下文窗口满了&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; 使用 &lt;code&gt;/compact&lt;/code&gt; 命令压缩上下文，或开启新会话继续工作。&lt;/p&gt;
&lt;h4&gt;Q: 网络连接问题&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确认环境变量 &lt;code&gt;HTTP_PROXY&lt;/code&gt; / &lt;code&gt;HTTPS_PROXY&lt;/code&gt; 已正确设置&lt;/li&gt;
&lt;li&gt;第三方模型确保 &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt; 可达&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Q: 如何让AI不使用某个工具？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; 使用 &lt;code&gt;/no&amp;#x3C;tool&gt;&lt;/code&gt; 命令，例如 &lt;code&gt;/nobrowse&lt;/code&gt; 禁用浏览模式。&lt;/p&gt;
&lt;h4&gt;Q: 想查看更多调试信息？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; 设置环境变量 &lt;code&gt;CLAUDE_DEBUG=1&lt;/code&gt; 可以看到详细日志。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>OpenCode安装与配置</title><link>https://juzzi.qzz.io/blog/ai/open-code</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/ai/open-code</guid><description>OpenCode 相较于最近比较火的 OpenClaw，更适合编程开发类任务，而相较于 Claude Code，更适合国内的网络环境以及更开放的大模型支持。</description><pubDate>Sat, 21 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;安装&lt;/h2&gt;
&lt;h3&gt;Mac&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;curl -fsSL https://opencode.ai/install | bash&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在终端中运行该命令即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Installing opencode version: 1.2.27
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 100%
Successfully added opencode to $PATH in /Users/juzi/.zshrc

                                 ▄     
█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
█░░█ █░░█ █▀▀▀ █░░█ █░░░ █░░█ █░░█ █▀▀▀
▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀  ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀


OpenCode includes free models, to start:

cd &amp;#x3C;project&gt;  # Open directory
opencode      # Run command

For more information visit https://opencode.ai/docs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后会看到已经将 opencode 加入了环境变量，可直接运行（开新的终端）。&lt;/p&gt;
&lt;h3&gt;Windows&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;安装 &lt;a href=&quot;https://nodejs.org/en/download&quot;&gt;Node.js&lt;/a&gt;，会附带安装 npm。&lt;/li&gt;
&lt;li&gt;使用命令&lt;code&gt;npm install -g opencode-ai&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意：如果是在powershell中运行npm，则需要使用&lt;code&gt;npm.cmd&lt;/code&gt;，因为Powershell默认的执行策略是Restricted，禁止执行脚本文件，可使用如下命令修改。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;运行&lt;/h2&gt;
&lt;p&gt;直接在命令行中输入&lt;code&gt;opencode&lt;/code&gt;即可运行。&lt;/p&gt;
&lt;p&gt;可以直接问他：你是什么模型？&lt;/p&gt;
&lt;p&gt;他会回答&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;我是由 big-pickle 模型驱动的（模型 ID: opencode/big-pickle）。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常用命令&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;切换模型：/models&lt;/li&gt;
&lt;li&gt;新建会话：/new&lt;/li&gt;
&lt;li&gt;初始化项目：/init&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;与 Claude Code 对比&lt;/h2&gt;
&lt;p&gt;| | OpenCode | Claude Code |
|---|---|---|
| 网络 | 国内可直接访问 | 可能需要代理 |
| 模型 | 支持多种模型，内置免费模型 | 以 Claude 系列为主 |
| 定位 | 编程开发任务 | 全能型 AI 助手 |
| 配置 | 配置文件，简洁轻量 | 丰富的 settings.json 选项 |
| MCP | 支持 | 支持 |
| 价格 | 有免费模型可用 | 按用量付费 |&lt;/p&gt;
&lt;h3&gt;OpenCode 独特优势&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;国内友好&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无需代理即可直接访问&lt;/li&gt;
&lt;li&gt;内置免费模型，开箱即用&lt;/li&gt;
&lt;li&gt;对国内大模型的支持更好（如通义千问、DeepSeek 等）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;轻量简洁&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置简单，学习成本低&lt;/li&gt;
&lt;li&gt;命令行交互流畅&lt;/li&gt;
&lt;li&gt;适合快速编程任务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;模型开放&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持多种模型提供商&lt;/li&gt;
&lt;li&gt;可以自由切换不同的模型&lt;/li&gt;
&lt;li&gt;便于对比不同模型的效果&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;适用场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;网络受限环境（国内直连）&lt;/li&gt;
&lt;li&gt;需要对比多种模型效果&lt;/li&gt;
&lt;li&gt;快速简单的编程任务&lt;/li&gt;
&lt;li&gt;不想配置复杂环境的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果需要更强大的功能（如 hooks、MCP 高级用法、全面深入的代码分析），推荐使用 Claude Code。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Golang项目在fork后的注意事项</title><link>https://juzzi.qzz.io/blog/lang/go/golang-repository-fork</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/lang/go/golang-repository-fork</guid><description>如果fork后还被其他项目引用，会产生一系列问题</description><pubDate>Tue, 24 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;问题&lt;/h3&gt;
&lt;p&gt;Golang项目与其他语言的项目有所不同，golang项目直接将github的用户名直接作为了项目的模块名字，定义在了go.mod中，例如:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module github.com/AAA/project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们fork该项目后，变成了BBB/project，如果该项目还被其他项目依赖，并用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go get github.com/BBB/project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拉取之后，会报错，因为项目名字BBB/project与该项目中的go.mod定义不一致。&lt;/p&gt;
&lt;h3&gt;修改依赖项目名&lt;/h3&gt;
&lt;p&gt;可以直接将原项目go.mod中的module改成BBB/project。&lt;/p&gt;
&lt;p&gt;但是项目中可能还引用了本项目的包，因此要把所有import都改成BBB/project。&lt;/p&gt;
&lt;h3&gt;使用replace语句&lt;/h3&gt;
&lt;p&gt;将依赖这个项目的go.mod加入如下语句&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;replace github.com/AAA/project =&gt; github.com/BBB/project main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用&lt;code&gt;go mod tidy&lt;/code&gt;来格式化，go会自动拉取main分支的最新代码，并打成一个版本，自动把上述语句变成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;replace github.com/AAA/project =&gt; github.com/BBB/project v0.0.0-20260223120613-9bece0fc6809
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样我们在代码中的import还是用之前的github.com/AAA/project，go会自动将代码映射到我们fork后的github.com/BBB/project。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>astro pure主题的特殊格式</title><link>https://juzzi.qzz.io/blog/lang/md/astro-theme-pure-special-format</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/lang/md/astro-theme-pure-special-format</guid><description>markdown自身以外的格式</description><pubDate>Thu, 15 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;首先，必须是mdx格式才能使用。&lt;/p&gt;
&lt;h2&gt;旁白&lt;/h2&gt;
&lt;p&gt;首先需要导包&lt;code&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/code&gt;，然后添加如下标签：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;Aside type=&apos;tip&apos; title=&apos;Try this focus exercise&apos;&gt;
  Test aside.....
&amp;#x3C;/Aside&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，type可以是：&quot;note&quot;, &quot;tip&quot;, &quot;caution&quot;, &quot;danger&quot;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>创建https证书</title><link>https://juzzi.qzz.io/blog/os/linux/create-https-cert</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/os/linux/create-https-cert</guid><description>在Linux上创建证书以用作https访问</description><pubDate>Sun, 28 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;解析域名&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ping 域名&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;确保域名解析到了到了正确的ip&lt;/p&gt;
&lt;h2&gt;关闭监听端口&lt;/h2&gt;
&lt;p&gt;Let&apos;s Encrypt 的 Standalone 模式需要占用 80 或 443 端口来验证域名所有权。&lt;/p&gt;
&lt;p&gt;停止web服务，例如xray&lt;/p&gt;
&lt;p&gt;&lt;code&gt;systemctl stop xray&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;申请证书&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;certbot certonly --standalone -d 域名&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;证书位置： 申请成功后，证书通常存放在 /etc/letsencrypt/live/新域名.com/ 目录下。&lt;/p&gt;
&lt;p&gt;fullchain.pem (对应 Xray 的 certificateFile)&lt;/p&gt;
&lt;p&gt;privkey.pem (对应 Xray 的 keyFile)&lt;/p&gt;
&lt;h2&gt;修改证书权限&lt;/h2&gt;
&lt;p&gt;web应用往往不是用root用户来启动的，因此需要修改证书权限，例如xray通常使用nobody用户来启动。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 给 archive 目录递归权限（真正存文件的地方）
chown -R nobody:nogroup /etc/letsencrypt/archive/

# 给 live 目录权限（存放快捷方式的地方）
chown -R nobody:nogroup /etc/letsencrypt/live/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;修改web服务器的证书配置&lt;/h2&gt;
&lt;h3&gt;xray&lt;/h3&gt;
&lt;p&gt;修改xray配置&lt;code&gt;/usr/local/etc/xray/config.json&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;tlsSettings&quot;: {
    &quot;serverName&quot;: &quot;新域名&quot;,
    &quot;certificates&quot;: [
        {
            &quot;certificateFile&quot;: &quot;/etc/letsencrypt/live/新域名/fullchain.pem&quot;,
            &quot;keyFile&quot;: &quot;/etc/letsencrypt/live/新域名/privkey.pem&quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改完成后，启动xray：&lt;code&gt;systemctl start xray&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;自动续期&lt;/h2&gt;
&lt;p&gt;创建脚本到文件：&lt;code&gt;/etc/xray/scripts/renew-hook.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

# 修正证书目录权限，确保 nobody 用户能读取
# 注意：必须同时给 archive 和 live 目录权限，因为 live 下是软链接
chown -R nobody:nogroup /etc/letsencrypt/archive/
chown -R nobody:nogroup /etc/letsencrypt/live/

# 获取当前时间，格式如：2023-10-27 10:30:00
CUR_TIME=$(date &quot;+%Y-%m-%d %H:%M:%S&quot;)
LOG_FILE=&quot;/var/log/xray-renew.log&quot;

echo &quot;[$CUR_TIME] 证书已成功更新并应用到 Xray&quot; &gt;&gt; $LOG_FILE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加运行权限：&lt;code&gt;chmod +x /etc/xray/scripts/renew-hook.sh&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;执行更新命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;certbot renew \
--pre-hook &quot;systemctl stop xray&quot; \
--post-hook &quot;systemctl start xray&quot; \
--deploy-hook &quot;/etc/xray/scripts/renew-hook.sh&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;若机器上有多个证书，可以添加&lt;code&gt;--cert-name 域名&lt;/code&gt;来指定更新域名。&lt;/li&gt;
&lt;li&gt;若证书剩余30天以上，不会执行更新操作，也不会运行pre/post的hook，如果要强制运行，可以添加参数&lt;code&gt;--dry-run&lt;/code&gt;来测试。&lt;/li&gt;
&lt;li&gt;即使加了&lt;code&gt;--dry-run&lt;/code&gt;，也不会运行&lt;code&gt;--deploy-hook&lt;/code&gt;，只有真实更新证书时才会运行，因此建议手动运行一次&lt;code&gt;renew-hook.sh&lt;/code&gt;来测试。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;查看自动运行记录&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;journalctl -u certbot.service -n 10 --no-page&lt;/code&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>MongoDB 常用查询语句</title><link>https://juzzi.qzz.io/blog/devel/mongodb-query-commands</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/devel/mongodb-query-commands</guid><description>系统梳理 MongoDB 的 CRUD 操作和高级查询技巧</description><pubDate>Fri, 21 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;MongoDB 是一种流行的 NoSQL 数据库，以其灵活的文档存储方式和强大的查询功能而著称。本文将系统介绍 MongoDB 的常用查询语句，帮助开发者快速掌握数据库操作。&lt;/p&gt;
&lt;h2&gt;插入数据&lt;/h2&gt;
&lt;h3&gt;插入单条文档&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;db.collection.insertOne({
  name: &quot;张三&quot;,
  age: 28,
  email: &quot;zhangsan@example.com&quot;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;插入多条文档&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;db.collection.insertMany([
  { name: &quot;李四&quot;, age: 32, department: &quot;技术部&quot; },
  { name: &quot;王五&quot;, age: 25, department: &quot;市场部&quot; },
  { name: &quot;赵六&quot;, age: 30, department: &quot;产品部&quot; }
])
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查询数据&lt;/h2&gt;
&lt;h3&gt;基础查询&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 查询所有文档
db.users.find()

// 查询指定字段
db.users.find({}, { name: 1, email: 1 })

// 查询指定文档
db.users.findOne({ _id: ObjectId(&quot;507f1f77bcf86cd799439011&quot;) })

// 等于查询
db.users.find({ age: 28 })

// 不等于查询
db.users.find({ age: { $ne: 30 } })

// 大于、小于
db.users.find({ age: { $gt: 25, $lt: 35 } })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;逻辑运算符&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// AND 条件
db.users.find({
  age: { $gte: 25 },
  department: &quot;技术部&quot;
})

// OR 条件
db.users.find({
  $or: [
    { age: { $gt: 30 } },
    { department: &quot;市场部&quot; }
  ]
})

// NOT 条件
db.users.find({
  age: { $not: { $lt: 18 } }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;元素查询&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 存在字段
db.users.find({ email: { $exists: true } })

// 字段不为空
db.users.find({ age: { $exists: true, $ne: null } })

// 数组包含元素
db.users.find({ hobbies: &quot;编程&quot; })

// 数组长度
db.users.find({ hobbies: { $size: 3 } })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;正则表达式&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 模糊查询
db.users.find({ name: /张/ })

// 忽略大小写
db.users.find({ name: { $regex: /zhang/i } })

// 复杂正则
db.users.find({ email: { $regex: /^user.*@example\.com$/i } })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;更新数据&lt;/h2&gt;
&lt;h3&gt;更新单条文档&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 更新指定字段
db.users.updateOne(
  { name: &quot;张三&quot; },
  { $set: { age: 29 } }
)

// 增加字段
db.users.updateOne(
  { name: &quot;张三&quot; },
  { $set: { lastLogin: new Date() } }
)

// 删除字段
db.users.updateOne(
  { name: &quot;张三&quot; },
  { $unset: { temporary: &quot;&quot; } }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;更新多条文档&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 批量更新
db.users.updateMany(
  { department: &quot;市场部&quot; },
  { $set: { status: &quot;active&quot; } }
)

// 累加字段
db.users.updateMany(
  { name: &quot;张三&quot; },
  { $inc: { visitCount: 1 } }
)

// 数组操作
db.users.updateOne(
  { name: &quot;张三&quot; },
  { $push: { loginHistory: &quot;2025-03-21&quot; } }
)

// 数组移除元素
db.users.updateOne(
  { name: &quot;张三&quot; },
  { $pull: { tags: &quot;deprecated&quot; } }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;替换文档&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;db.users.updateOne(
  { _id: ObjectId(&quot;507f1f77bcf86cd799439011&quot;) },
  { $set: { name: &quot;张三&quot;, age: 29, email: &quot;newemail@example.com&quot; } }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;删除数据&lt;/h2&gt;
&lt;h3&gt;删除单条文档&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 根据条件删除
db.users.deleteOne({ _id: ObjectId(&quot;507f1f77bcf86cd799439011&quot;) })

// 删除第一条匹配文档
db.users.deleteOne({ status: &quot;inactive&quot; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;删除多条文档&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 删除所有匹配文档
db.users.deleteMany({ status: &quot;inactive&quot; })

// 删除整个集合
db.users.deleteMany({})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;排序和分页&lt;/h2&gt;
&lt;h3&gt;排序查询&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 按字段升序
db.users.find({}).sort({ age: 1 })

// 按字段降序
db.users.find({}).sort({ age: -1 })

// 多字段排序
db.users.find({}).sort({ department: 1, age: -1 })

// 随机排序
db.users.find({}).sort({ $natural: -1 })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分页查询&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 限制返回数量
db.users.find({}).limit(10)

// 跳过指定数量
db.users.find({}).skip(20)

// 组合使用
db.users.find({}).limit(10).skip(20)

// 常用分页
const page = 1;
const pageSize = 10;
db.users.find({}).skip((page - 1) * pageSize).limit(pageSize)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;聚合查询&lt;/h2&gt;
&lt;h3&gt;基础聚合&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 统计文档数量
db.users.countDocuments({ age: { $gte: 18 } })

// 去重
db.users.distinct(&quot;department&quot;)

// 字段求和
db.users.aggregate([
  { $match: { age: { $gte: 25 } } },
  { $group: { _id: &quot;$department&quot;, count: { $sum: 1 } } }
])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;复杂聚合&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 分组统计
db.users.aggregate([
  { $group: {
    _id: &quot;$department&quot;,
    avgAge: { $avg: &quot;$age&quot; },
    maxAge: { $max: &quot;$age&quot; },
    minAge: { $min: &quot;$age&quot; },
    total: { $sum: 1 }
  }}
])

// 排序分组结果
db.users.aggregate([
  { $group: {
    _id: &quot;$department&quot;,
    total: { $sum: 1 }
  }},
  { $sort: { total: -1 } }
])

// 条件聚合
db.users.aggregate([
  { $match: { age: { $gte: 25 } } },
  { $group: {
    _id: &quot;$department&quot;,
    activeUsers: { $sum: 1 }
  }},
  { $match: { activeUsers: { $gt: 0 } } }
])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数组操作&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 展开数组
db.orders.aggregate([
  { $unwind: &quot;$items&quot; }
])

// 数组分组
db.users.aggregate([
  { $group: {
    _id: null,
    allHobbies: { $push: &quot;$hobbies&quot; }
  }}
])

// 数组去重
db.users.aggregate([
  { $unwind: &quot;$hobbies&quot; },
  { $group: {
    _id: null,
    uniqueHobbies: { $addToSet: &quot;$hobbies&quot; }
  }}
])
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;优化建议&lt;/h2&gt;
&lt;h3&gt;索引使用&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 创建单字段索引
db.users.createIndex({ name: 1 })

// 创建复合索引
db.users.createIndex({ department: 1, age: -1 })

// 创建唯一索引
db.users.createIndex({ email: 1 }, { unique: true })

// 创建多键索引（数组字段）
db.users.createIndex({ tags: 1 })

// 查看索引
db.users.getIndexes()

// 删除索引
db.users.dropIndex(&quot;name_1&quot;)

// 删除所有索引
db.users.dropIndexes()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查询优化技巧&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 1. 使用投影减少数据传输
db.users.find({}, { name: 1, email: 1 })

// 2. 避免全表扫描
db.users.find({ status: &quot;active&quot; })  // 而不是 db.users.find({})

// 3. 合理使用索引
db.users.createIndex({ name: 1, email: 1 })

// 4. 使用 explain 查看查询计划
db.users.find({ name: &quot;张三&quot; }).explain(&quot;executionStats&quot;)

// 5. 分页避免深度分页
db.users.find({}).limit(10).skip(100000)  // 性能差
db.users.find({}).sort({ _id: 1 }).limit(10).skip(100000)  // 性能好
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;性能问题&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 1. 使用 countDocuments 代替 count()
db.users.countDocuments({})  // 推荐
db.users.count({})  // 已废弃

// 2. 避免使用 $or 查询
db.users.find({ $or: [...] })  // 性能较差

// 3. 使用 explain 分析查询
db.users.find({}).explain()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数据类型问题&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 字符串转 ObjectId
const objectId = ObjectId(&quot;507f1f77bcf86cd799439011&quot;)

// Date 类型处理
db.users.updateOne(
  { _id: objectId },
  { $set: { createdAt: new Date() } }
)

// 数字类型
db.users.find({ age: NumberInt(&quot;28&quot;) })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;批量操作优化&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 使用批量插入
db.users.insertMany([
  // 1000+ 条数据
])

// 使用事务（MongoDB 4.0+）
const session = db.startSession();
try {
  session.startTransaction();
  db.users.insertOne({...}, { session });
  db.users.updateOne({...}, { session });
  session.commitTransaction();
} catch (error) {
  session.abortTransaction();
} finally {
  session.endSession();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考资源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.mongodb.com/&quot;&gt;MongoDB 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mongodb.cn/&quot;&gt;MongoDB 中文社区&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.mongodb.com/manual/core/aggregation-pipeline/&quot;&gt;MongoDB Aggregation Pipeline&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;更新日志&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;2025-03-21: 初始版本，包含基础 CRUD 操作和聚合查询&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Markdown 语法支持</title><link>https://juzzi.qzz.io/blog/lang/md/markdown-zh</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/lang/md/markdown-zh</guid><description>Markdown 是一种轻量级的「标记语言」。</description><pubDate>Wed, 26 Jul 2023 08:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;基本语法&lt;/h2&gt;
&lt;p&gt;Markdown 是一种轻量级且易于使用的语法，用于为您的写作设计风格。&lt;/p&gt;
&lt;h3&gt;标题&lt;/h3&gt;
&lt;p&gt;文章内容较多时，可以用标题分段：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# 标题 1

## 标题 2

## 大标题

### 小标题
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;标题预览会打乱文章的结构，所以在此不展示。&lt;/p&gt;
&lt;h3&gt;粗斜体&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;_斜体文本_

**粗体文本**

**_粗斜体文本_**
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;斜体文本&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;粗体文本&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;粗斜体文本&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;链接&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;文字链接 [链接名称](http://链接网址)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;文字链接 &lt;a href=&quot;http://%E9%93%BE%E6%8E%A5%E7%BD%91%E5%9D%80&quot;&gt;链接名称&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;行内代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;这是一条 `单行代码`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;这是一条 &lt;code&gt;行内代码&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;```js
// calculate fibonacci
function fibonacci(n) {
  if (n &amp;#x3C;= 1) return 1
  return fibonacci(n - 1) + fibonacci(n - 2)
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// calculate fibonacci
function fibonacci(n) {
  if (n &amp;#x3C;= 1) return 1
  return fibonacci(n - 1) + fibonacci(n - 2)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当前使用 shiki 作为代码高亮插件，支持的语言请参考 &lt;a href=&quot;https://shiki.matsu.io/languages.html&quot;&gt;shiki / languages&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;行内公式&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;这是一条行内公式 $e^{i\pi} + 1 = 0$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;这是一条行内公式 $e^{i\pi} + 1 = 0$&lt;/p&gt;
&lt;h3&gt;公式块&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;$$
\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x) e^{-2\pi i x \xi} \, dx
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;$$
\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x) e^{-2\pi i x \xi} , dx
$$&lt;/p&gt;
&lt;p&gt;当前使用 KaTeX 作为数学公式插件，支持的语法请参考 &lt;a href=&quot;https://katex.org/docs/supported.html&quot;&gt;KaTeX Supported Functions&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;图片&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;![CWorld](https://cravatar.cn/avatar/1ffe42aa45a6b1444a786b1f32dfa8aa?s=200)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cravatar.cn/avatar/1ffe42aa45a6b1444a786b1f32dfa8aa?s=200&quot; alt=&quot;CWorld&quot;&gt;&lt;/p&gt;
&lt;h4&gt;删除线&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;~~删除线~~
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;~~删除线~~&lt;/p&gt;
&lt;h3&gt;列表&lt;/h3&gt;
&lt;p&gt;普通无序列表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- 1
- 2
- 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1&lt;/li&gt;
&lt;li&gt;2&lt;/li&gt;
&lt;li&gt;3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;普通有序列表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;1. GPT-4
2. Claude Opus
3. LLaMa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;GPT-4&lt;/li&gt;
&lt;li&gt;Claude Opus&lt;/li&gt;
&lt;li&gt;LLaMa&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;列表里可以继续嵌套语法&lt;/p&gt;
&lt;h3&gt;引用&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&gt; 枪响，雷鸣，剑起。繁花血景。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;枪响，雷鸣，剑起。繁花血景。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;引用里也可以继续嵌套语法。&lt;/p&gt;
&lt;h3&gt;换行&lt;/h3&gt;
&lt;p&gt;markdown 分段落是需要空一行的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;如果不空行
就会在一段

第一段

第二段
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;如果不空行
就会在一段&lt;/p&gt;
&lt;p&gt;第一段&lt;/p&gt;
&lt;p&gt;第二段&lt;/p&gt;
&lt;h3&gt;分隔符&lt;/h3&gt;
&lt;p&gt;如果你有写分割线的习惯，可以新起一行输入三个减号&lt;code&gt;---&lt;/code&gt; 或者星号 &lt;code&gt;***&lt;/code&gt;。当前后都有段落时，请空出一行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;高级技巧&lt;/h2&gt;
&lt;h3&gt;行内 HTML 元素&lt;/h3&gt;
&lt;p&gt;目前只支持部分段内 HTML 元素效果，包括 &lt;code&gt;&amp;#x3C;kdb&gt; &amp;#x3C;b&gt; &amp;#x3C;i&gt; &amp;#x3C;em&gt; &amp;#x3C;sup&gt; &amp;#x3C;sub&gt; &amp;#x3C;br&gt;&lt;/code&gt; ，如&lt;/p&gt;
&lt;h4&gt;键位显示&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;使用 &amp;#x3C;kbd&gt;Ctrl&amp;#x3C;/kbd&gt; + &amp;#x3C;kbd&gt;Alt&amp;#x3C;/kbd&gt; + &amp;#x3C;kbd&gt;Del&amp;#x3C;/kbd&gt; 重启电脑
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;使用 Ctrl + Alt + Del 重启电脑&lt;/p&gt;
&lt;h4&gt;粗斜体&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;#x3C;b&gt; Markdown 在此处同样适用，如 _加粗_ &amp;#x3C;/b&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt; Markdown 在此处同样适用，如 &lt;em&gt;加粗&lt;/em&gt; &lt;/p&gt;
&lt;h3&gt;其他 HTML 写法&lt;/h3&gt;
&lt;h4&gt;折叠块&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;#x3C;details&gt;&amp;#x3C;summary&gt;点击展开&amp;#x3C;/summary&gt;它被隐藏了&amp;#x3C;/details&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;h3&gt;表格&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;| 表头1 | 表头2 |
| ----- | ----- |
| 内容1 | 内容2 |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;| 表头1 | 表头2 |
| ----- | ----- |
| 内容1 | 内容2 |&lt;/p&gt;
&lt;h3&gt;注释&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;在引用的地方使用 [^注释] 来添加注释。

然后在文档的结尾，添加注释的内容（会默认于文章结尾渲染之）。

[^注释]: 这里是注释的内容
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;在引用的地方使用 &lt;a href=&quot;%E8%BF%99%E9%87%8C%E6%98%AF%E6%B3%A8%E9%87%8A%E7%9A%84%E5%86%85%E5%AE%B9&quot;&gt;^注释&lt;/a&gt; 来添加注释。&lt;/p&gt;
&lt;p&gt;然后在文档的结尾，添加注释的内容（会默认于文章结尾渲染之）。&lt;/p&gt;
&lt;h3&gt;To-Do 列表&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- [ ] 未完成的任务
- [x] 已完成的任务
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 未完成的任务&lt;/li&gt;
&lt;li&gt;[x] 已完成的任务&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;符号转义&lt;/h3&gt;
&lt;p&gt;如果你的描述中需要用到 markdown 的符号，比如 _ # * 等，但又不想它被转义，这时候可以在这些符号前加反斜杠，如 &lt;code&gt;\_&lt;/code&gt; &lt;code&gt;\#&lt;/code&gt; &lt;code&gt;\*&lt;/code&gt; 进行避免。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;\_不想这里的文本变斜体\_

\*\*不想这里的文本被加粗\*\*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;p&gt;_不想这里的文本变斜体_&lt;/p&gt;
&lt;p&gt;**不想这里的文本被加粗**&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;内嵌 Astro 组件&lt;/h2&gt;
&lt;p&gt;See &lt;a href=&quot;/docs/integrations/components&quot;&gt;User Components&lt;/a&gt; and &lt;a href=&quot;/docs/integrations/advanced&quot;&gt;Advanced Components&lt;/a&gt; for details.&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.HAXFr_hw.jpg"/><enclosure url="/_astro/thumbnail.HAXFr_hw.jpg"/></item><item><title>Akka节点退出分析</title><link>https://juzzi.qzz.io/blog/devel/akka/akka-node-down</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/devel/akka/akka-node-down</guid><description>内网测试环境发现服务器崩溃，查询日志发现集群节点基本都退出了，针对这种情况展开分析...</description><pubDate>Sat, 13 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;日志查询&lt;/h3&gt;
&lt;h4&gt;退出日志&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;10:47:20,076|DEBUG| - Performing phase [cluster-exiting-done] with [1] tasks.
10:47:20,076|DEBUG| - Performing task [exiting-completed] in CoordinatedShutdown phase [cluster-exiting-done]
10:47:20,076|DEBUG| - Performing phase [cluster-shutdown] with [1] tasks.
10:47:20,076|DEBUG| - Performing task [wait-shutdown] in CoordinatedShutdown phase [cluster-shutdown]
10:47:20,076|DEBUG| - Performing phase [before-actor-system-terminate] with [2] tasks.
10:47:20,076|DEBUG| - Performing task [stop-global-components] in CoordinatedShutdown phase [before-actor-system-terminate]
10:47:20,076|DEBUG| - Performing task [enter-stopped-state] in CoordinatedShutdown phase [before-actor-system-terminate]
10:47:20,076|INFO| - Stopping global components...
10:47:20,076|INFO| - Change state STOPPING -&gt; STOPPED, reason=default
10:47:20,076|INFO| - Global components all stopped.
10:47:20,076|DEBUG| - Performing phase [actor-system-terminate] with [1] tasks.
10:47:20,076|DEBUG| - Performing task [terminate-system] in CoordinatedShutdown phase [actor-system-terminate]
10:47:20,106|INFO| - Actor[akka://mc-Game/user/duidWorkerPool/$a#-1404896902] stopped.
10:47:20,107|INFO| - Actor[akka://mc-Game/user/duidWorkerPool/$b#-741633102] stopped.
10:47:20,118|INFO| - Actor[akka://mc-Game/user/scriptDaemon#1885699308] stopped.
10:47:20,119|INFO| - Shutting down remote daemon.
10:47:20,121|INFO| - Remote daemon shut down; proceeding with flushing remote transports.
10:47:20,136|INFO| - Remoting shut down.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有点类似于关服日志。&lt;/p&gt;
&lt;h4&gt;关闭原因日志&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;10:46:50,930|WARN| - Cluster Node [akka://mc-Game@172.26.101.181:2562] - Scheduled sending of heartbeat was delayed. Previous heartbeat was sent [21309] ms ago, expected interval is [3000] ms. This may cause failure detection to mark members as unreachable. The reason can be thread starvation, CPU overload, or GC.
10:46:52,051|INFO| - Cluster Node [akka://mc-Game@172.26.101.181:2562] - event ReachabilityChanged(akka://mc-Game@172.26.101.181:2552 -&gt; akka://mc-Game@172.26.101.181:2564: Unreachable [Unreachable] (1), akka://mc-Game@172.26.101.181:2553 -&gt; akka://mc-Game@172.26.101.181:2564: Unreachable [Unreachable] (1), akka://mc-Game@172.26.101.181:2556 -&gt; akka://mc-Game@172.26.101.181:2552: Unreachable [Unreachable] (2), akka://mc-Game@172.26.101.181:2556 -&gt; akka://mc-Game@172.26.101.181:2559: Unreachable [Unreachable] (3), akka://mc-Game@172.26.101.181:2556 -&gt; akka://mc-Game@172.26.101.181:2562: Reachable [Reachable] (4))
10:46:52,491|INFO| - Self downed, stopping
10:46:52,491|INFO| - Singleton manager stopping singleton actor [akka://mc-Game/system/sharding/chatCoordinator/singleton]
10:46:52,538|WARN| - Other node [akka://mc-Game@172.26.101.181:2558#5251986850917086146] quarantined this node.
10:46:52,539|WARN| - SBR took decision DownSelfQuarantinedByRemote and is downing [akka://mc-Game@172.26.101.181:2562] including myself,, [11] unreachable of [1] members, all members in DC [Member(akka://mc-Game@172.26.101.181:2551, Down), Member(akka://mc-Game@172.26.101.181:2552, Down), Member(akka://mc-Game@172.26.101.181:2553, Down), Member(akka://mc-Game@172.26.101.181:2554, Down), Member(akka://mc-Game@172.26.101.181:2555, Down), Member(akka://mc-Game@172.26.101.181:2556, Down), Member(akka://mc-Game@172.26.101.181:2557, Down), Member(akka://mc-Game@172.26.101.181:2558, Down), Member(akka://mc-Game@172.26.101.181:2559, Down), Member(akka://mc-Game@172.26.101.181:2560, Down), Member(akka://mc-Game@172.26.101.181:2562, Up), Member(akka://mc-Game@172.26.101.181:2563, Down), Member(akka://mc-Game@172.26.101.181:2564, Down)], full reachability status: [akka://mc-Game@172.26.101.181:2552 -&gt; akka://mc-Game@172.26.101.181:2564: Unreachable [Unreachable] (1), akka://mc-Game@172.26.101.181:2553 -&gt; akka://mc-Game@172.26.101.181:2564: Unreachable [Unreachable] (1), akka://mc-Game@172.26.101.181:2556 -&gt; akka://mc-Game@172.26.101.181:2552: Unreachable [Unreachable] (2), akka://mc-Game@172.26.101.181:2556 -&gt; akka://mc-Game@172.26.101.181:2559: Unreachable [Unreachable] (3)]
10:46:55,012|INFO| - Change state STARTED -&gt; STOPPING, reason=ClusterDowningReason$
10:46:55,014|DEBUG| - Performing phase [service-unbind] with [0] tasks
10:46:55,014|DEBUG| - Performing phase [service-requests-done] with [0] tasks
10:46:55,014|DEBUG| - Performing phase [service-stop] with [0] tasks
10:46:55,014|DEBUG| - Performing phase [before-cluster-shutdown] with [0] tasks
10:46:55,014|DEBUG| - Performing phase [before-cluster-sharding-shutdown-region] with [1] tasks.
10:46:55,014|DEBUG| - Performing task [delay-by-shard-name] in CoordinatedShutdown phase [before-cluster-sharding-shutdown-region]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关闭原因为节点之间连接超时，本节点已经被隔离，SBR决定DownSelfQuarantinedByRemote，然后就把本节点关闭了。&lt;/p&gt;
&lt;h3&gt;核心故障原因：心跳延迟（关键线索）&lt;/h3&gt;
&lt;p&gt;日志中有一条极其关键的 &lt;strong&gt;WARN&lt;/strong&gt; 信息：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Scheduled sending of heartbeat was delayed. Previous heartbeat was sent [7854] ms ago, expected interval is [3000] ms.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分析&lt;/strong&gt;：Akka 期望每 3 秒发一次心跳，结果这次延迟到了 &lt;strong&gt;7.8 秒&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;诊断&lt;/strong&gt;：这是典型的 &lt;strong&gt;JVM 停顿&lt;/strong&gt;。原因通常有三类：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GC 停顿 (Stop-The-World)&lt;/strong&gt;：JVM 正在进行全量垃圾回收。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU 爆满&lt;/strong&gt;：系统负载过高，OS 没给 Akka 线程分配时间片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线程饥饿&lt;/strong&gt;：Akka 的调度器（Dispatcher）被耗时操作（如阻塞 I/O）占满。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;节点状态演变过程&lt;/h3&gt;
&lt;h4&gt;第一阶段：发现异常&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;节点 &lt;code&gt;2553&lt;/code&gt; 发现集群中的 &lt;code&gt;2564&lt;/code&gt; 变得不可达（Unreachable）。&lt;/li&gt;
&lt;li&gt;此时系统还在尝试运行业务逻辑（看到 &lt;code&gt;MoveUnitProcess&lt;/code&gt; 还在处理怪物攻击路径更新），但网络状态已经开始恶化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;第二阶段：收到“判决书”&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Received gossip where this member has been downed, from [...:2552]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解读&lt;/strong&gt;：节点 &lt;code&gt;2552&lt;/code&gt; 作为当时的 Leader，通过 Gossip 协议通知节点 &lt;code&gt;2553&lt;/code&gt;：“你已经被集群踢出了（Downed）。”&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;第三阶段：自我拆解与关闭&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;一旦确认自己被 Down，该节点开始关闭所有的集群服务：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ShardRegion 停止&lt;/strong&gt;：&lt;code&gt;activity&lt;/code&gt;, &lt;code&gt;chat&lt;/code&gt;, &lt;code&gt;world&lt;/code&gt;, &lt;code&gt;alliance&lt;/code&gt; 等分片代理全部关闭。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Singleton 停止&lt;/strong&gt;：&lt;code&gt;worldCoordinator&lt;/code&gt; 单例也随之关闭。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;角色变更&lt;/strong&gt;：所有的 &lt;code&gt;RoleLeaderChanged&lt;/code&gt; 全部变为 &lt;code&gt;None&lt;/code&gt;，意味着该节点不再承担任何集群角色。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;第四阶段：彻底瘫痪&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;节点标记集群中所有其他节点（2551, 2552, 2558...）全部为 &lt;code&gt;Down&lt;/code&gt;。这通常是因为它自己已经脱离了集群联系，处于“弥留之际”。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;业务影响&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;游戏逻辑中断&lt;/strong&gt;：&lt;code&gt;world&lt;/code&gt; 分片（ShardRegion）停止，意味着该节点上承载的游戏地图、玩家数据处理全部中断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单例失效&lt;/strong&gt;：&lt;code&gt;worldCoordinator&lt;/code&gt; 停止，如果它是集群中唯一的协调者，整个集群的资源分配会陷入混乱，直到其他节点选出新的 Leader。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;改进建议&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;资源隔离&lt;/strong&gt;：在同一台机器上跑了至少 13 个 Akka 节点（从 2551 到 2564）。这会造成极严重的 &lt;strong&gt;CPU 争抢&lt;/strong&gt;。减少单机节点数量，或者大幅增加宿主机的 CPU 核心数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排查阻塞代码&lt;/strong&gt;：搜索代码中是否存在 &lt;code&gt;Thread.sleep&lt;/code&gt;、阻塞式数据库查询或文件 IO，且这些操作运行在 Akka 的默认调度器上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调整故障检测器&lt;/strong&gt;：如果网络或 CPU 环境确实较差，可以调大阈值：&lt;code&gt;akka.cluster.failure-detector.threshold = 12.0  # 默认 8.0，调大可提高容灾性但降低灵敏度&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控 GC&lt;/strong&gt;：务必检查该时段的 &lt;code&gt;gc.log&lt;/code&gt;。7.8 秒的延迟极大概率是 Full GC 导致的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;一句话总结：这台机器撑不住这么多节点同时跑，CPU 或内存（GC）在高负载下崩了。&lt;/strong&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Git换行符处理</title><link>https://juzzi.qzz.io/blog/devel/version-control/git-crlf</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/devel/version-control/git-crlf</guid><description>本地文件换行符与Git仓库不一致的处理。</description><pubDate>Thu, 17 Mar 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在Windows系统下，经常遇到打开某个仓库后Git会提示文件有修改，但是一对比发现文件内容完全一样，有的编辑器还会提示文件除换行符外没有差别。&lt;/p&gt;
&lt;p&gt;这是因为Windows的换行符为&lt;code&gt;CRLF&lt;/code&gt;，而Linux/Mac(Unix)的换行符为&lt;code&gt;LF&lt;/code&gt;，因此会导致该差异，怎么解决呢？&lt;/p&gt;
&lt;p&gt;其实Git早已内置了换行符的自动转换功能，即&lt;code&gt;core.autocrlf&lt;/code&gt;属性。&lt;/p&gt;
&lt;h2&gt;配置CRLF属性&lt;/h2&gt;
&lt;h3&gt;Windows&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config --global core.autocrlf true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令的意思是：提交时，把 &lt;code&gt;CRLF&lt;/code&gt; 转成 &lt;code&gt;LF&lt;/code&gt; 存进仓库；检出文件到你的 Windows 工作目录时，再自动把 &lt;code&gt;LF&lt;/code&gt; 转回 &lt;code&gt;CRLF&lt;/code&gt;。这样，仓库里永远是干净的 &lt;code&gt;LF&lt;/code&gt;，而你本地用于编辑的文件则符合 Windows 的 &lt;code&gt;CRLF&lt;/code&gt; 规范。&lt;/p&gt;
&lt;h3&gt;Linux/MacOS&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config --global core.autocrlf input
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令的意思是：提交时，把 &lt;code&gt;CRLF&lt;/code&gt; 转成 &lt;code&gt;LF&lt;/code&gt; 存进仓库；检出到本地时，&lt;strong&gt;不做任何转换&lt;/strong&gt;，保持文件的 &lt;code&gt;LF&lt;/code&gt; 格式。因为 macOS/Linux 系统原生支持 &lt;code&gt;LF&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;生效配置&lt;/h2&gt;
&lt;p&gt;由于刚刚设置的属性只有在提交时才能效，因此要Git重新再处理一遍。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 删除 Git 的索引（暂存区），但保留工作目录的文件
git rm --cached -r .
# 重新添加所有文件，Git 会按新配置自动转换行尾
git add --all
# 提交这次清理（如果仍然有文件变化）
git commit -m &quot;chore: 规范化文件的换行符&quot;
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Akka类型化Actor与传统Actor对比</title><link>https://juzzi.qzz.io/blog/devel/akka/akka-typed-actor</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/devel/akka/akka-typed-actor</guid><description>类型化Actor已经是akka官方默认推荐使用的actor，本文详细对比了Typed Actor与Classic Actor的区别和优缺点。</description><pubDate>Mon, 20 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;从akka 2.6开始，&lt;a href=&quot;https://doc.akka.io/docs/akka/current/typed/actors.html&quot;&gt;Typed Actor&lt;/a&gt;（类型化的Actor）已经成为默认的Actor，原有的Actor被命名为为&lt;a href=&quot;https://doc.akka.io/docs/akka/current/actors.html&quot;&gt;Classic Actor&lt;/a&gt;（经典的Actor）。akka推荐新项目直接使用typed actor，因为其提升了类型安全，并且将Java API和Scala API分离，对开发者更加友好。&lt;/p&gt;
&lt;h2&gt;迁移指南&lt;/h2&gt;
&lt;p&gt;官网上从经典的Actor迁移到类型化的Actor文档&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.akka.io/docs/akka/current/typed/from-classic.html&quot;&gt;https://doc.akka.io/docs/akka/current/typed/from-classic.html&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;核心对比&lt;/h2&gt;
&lt;h3&gt;1. Actor继承与泛型&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Classic Actor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ClassicEchoActor extends AbstractActor {
    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .match(String.class, msg -&gt; getSender().tell(msg, getSelf()))
            .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Typed Actor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class TypedEchoBehavior extends AbstractBehavior&amp;#x3C;String&gt; {
    @Override
    public Receive&amp;#x3C;String&gt; createReceive() {
        return newReceiveBuilder()
            .onMessage(String.class, msg -&gt; {
                getContext().getLog().info(&quot;Received: {}&quot;, msg);
                return Behaviors.same();
            })
            .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对比要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Typed Actor 使用泛型 &lt;code&gt;Behavior&amp;#x3C;String&gt;&lt;/code&gt; 明确指定能处理的消息类型&lt;/li&gt;
&lt;li&gt;类型安全：编译期就能发现消息类型错误&lt;/li&gt;
&lt;li&gt;API 分离：Java 和 Scala 各自有独立的 API&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. Actor 创建与生命周期&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Classic Actor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 从 ActorSystem 创建
ActorRef&amp;#x3C;String&gt; actorRef = system.actorOf(Props.create(ClassicEchoActor.class), &quot;echo&quot;);

// 从 ActorContext 创建
ActorRef&amp;#x3C;String&gt; childRef = context.actorOf(Props.create(ClassicEchoActor.class), &quot;child&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Typed Actor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 从 ActorContext 创建
Behavior&amp;#x3C;String&gt; behavior = TypedEchoBehavior.create();
ActorRef&amp;#x3C;String&gt; actorRef = context.spawn(behavior, &quot;echo&quot;);

// 根 Actor 必须通过守护 Actor 创建
Behavior&amp;#x3C;Void&gt; rootBehavior = Behaviors.setup(context -&gt; {
    ActorRef&amp;#x3C;String&gt; childRef = context.spawn(TypedEchoBehavior.create(), &quot;child&quot;);
    return Behaviors.receiveMessage(msg -&gt; {
        // 处理消息
        return Behaviors.same();
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对比要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Typed Actor 只能从 ActorContext 创建，不能直接从 ActorSystem 创建&lt;/li&gt;
&lt;li&gt;根 Actor 必须通过守护 Actor 创建，保证层级结构&lt;/li&gt;
&lt;li&gt;所有 Actor 必须由另一个 Actor 创建，符合 Actor 模型的本质&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 消息处理机制&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Classic Actor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
public Receive createReceive() {
    return receiveBuilder()
        .match(String.class, msg -&gt; {
            // 处理消息
            getSender().tell(&quot;Echo: &quot; + msg, getSelf());
        })
        .build();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Typed Actor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
public Receive&amp;#x3C;String&gt; createReceive() {
    return newReceiveBuilder()
        .onMessage(String.class, msg -&gt; {
            getContext().getLog().info(&quot;Received: {}&quot;, msg);
            return Behaviors.same(); // 返回相同的行为
        })
        .build();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对比要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Typed Actor 必须返回 Behavior，明确指定下一个状态&lt;/li&gt;
&lt;li&gt;支持 &lt;code&gt;Behaviors.same()&lt;/code&gt; 保持当前状态&lt;/li&gt;
&lt;li&gt;支持 &lt;code&gt;Behaviors.setup()&lt;/code&gt; 初始化逻辑&lt;/li&gt;
&lt;li&gt;支持 &lt;code&gt;Behaviors.unhandled()&lt;/code&gt; 明确标记未处理的消息&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 发送者与父 Actor&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Classic Actor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 直接访问 sender
context.sender().tell(&quot;Hello&quot;, getSelf());

// 直接访问 parent
context.parent()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Typed Actor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 不再支持 sender 和 parent API
// 必须通过消息传递
context.getSpawnSupervisorStrategy() // 获取父 Actor 的策略

// 将 ActorRef 包含在消息中
record WithSender(String message, ActorRef&amp;#x3C;String&gt; sender) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对比要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Typed Actor 移除了 &lt;code&gt;sender()&lt;/code&gt; 和 &lt;code&gt;parent()&lt;/code&gt; 方法&lt;/li&gt;
&lt;li&gt;减少歧义，明确消息传递语义&lt;/li&gt;
&lt;li&gt;需要显式传递 ActorRef&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 消息类型&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Classic Actor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 可以接受任何类型
.receiveBuilder()
    .match(Integer.class, i -&gt; {...})
    .match(String.class, s -&gt; {...})
    .matchAny(msg -&gt; {...}) // 捕获未匹配的消息
    .build()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Typed Actor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 强类型，编译期检查
.onMessage(Integer.class, i -&gt; {...})
.onMessage(String.class, s -&gt; {...})
// 未匹配的消息不会进入处理流程
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对比要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Typed Actor 提供编译期类型安全&lt;/li&gt;
&lt;li&gt;Classic Actor 在运行时才发现类型错误&lt;/li&gt;
&lt;li&gt;Typed Actor 明确区分处理和未处理的消息&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. 状态管理&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Classic Actor 使用 become/unbecome&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private enum State { WORKING, PAUSED }

@Override
public Receive createReceive() {
    return receiveBuilder()
        .matchEquals(State.WORKING, s -&gt; {
            getContext().become(workHandler);
        })
        .matchEquals(State.PAUSED, s -&gt; {
            getContext().unbecome();
        })
        .build();
}

private Receive workHandler() {
    return receiveBuilder()
        .match(String.class, msg -&gt; {
            // 处理工作消息
        })
        .build();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Typed Actor 使用状态模式&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface AppState {
}

public class WorkingState implements AppState {
    @Override
    public Receive&amp;#x3C;String&gt; createReceive() {
        return newReceiveBuilder()
            .onMessage(PauseCommand.class, cmd -&gt; PausedState.create())
            .build();
    }
}

public class PausedState implements AppState {
    @Override
    public Receive&amp;#x3C;String&gt; createReceive() {
        return newReceiveBuilder()
            .onMessage(ResumeCommand.class, cmd -&gt; WorkingState.create())
            .build();
    }
}

// 在 Behavior 中切换状态
public class EchoBehavior extends AbstractBehavior&amp;#x3C;String&gt; {
    private AppState state = WorkingState.create();

    @Override
    public Receive&amp;#x3C;String&gt; createReceive() {
        return newReceiveBuilder()
            .onMessage(String.class, msg -&gt; {
                // 切换状态
                if (shouldPause(msg)) {
                    state = PausedState.create();
                } else {
                    state = WorkingState.create();
                }
                return state.createReceive();
            })
            .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对比要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Typed Actor 鼓励使用显式的状态模式&lt;/li&gt;
&lt;li&gt;更易于测试和维护&lt;/li&gt;
&lt;li&gt;经典 Actor 使用 become/unbecome 可能导致状态不可追踪&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7. 错误处理&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Classic Actor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
public void preRestart(Throwable reason, Optional&amp;#x3C;Object&gt; message) {
    // 自定义错误处理
}

@Override
public void postRestart(Throwable reason) {
    // 重启后逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Typed Actor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class TypedEchoBehavior extends AbstractBehavior&amp;#x3C;String&gt; {
    @Override
    public Behavior&amp;#x3C;String&gt; onMessage(String msg) throws Exception {
        if (shouldFail(msg)) {
            throw new IllegalArgumentException(&quot;Invalid message&quot;);
        }
        // 处理消息
    }

    @Override
    public Behavior&amp;#x3C;String&gt; onFailure(Throwable cause) {
        // 错误处理
        return Behaviors.restart(StartTimeout.of(1, TimeUnit.SECONDS), this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对比要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Typed Actor 在消息处理函数中抛出异常&lt;/li&gt;
&lt;li&gt;支持 &lt;code&gt;onFailure&lt;/code&gt; 回调处理错误&lt;/li&gt;
&lt;li&gt;可以配置重启策略和超时&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8. 测试&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Classic Actor 测试&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ClassicEchoActorTest extends TestKit {
    @Test
    public void testEcho() {
        TestProbe&amp;#x3C;String&gt; probe = new TestProbe&amp;#x3C;String&gt;(system);
        actorOf(Props.create(ClassicEchoActor.class)).tell(&quot;Hello&quot;, probe.ref());
        assertEquals(&quot;Hello&quot;, probe.receiveOne(Duration.ofSeconds(1)));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Typed Actor 测试&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class TypedEchoBehaviorTest extends TestKit {
    @Test
    public void testEcho() {
        TestProbe&amp;#x3C;String&gt; probe = new TestProbe&amp;#x3C;String&gt;(system);
        ActorRef&amp;#x3C;String&gt; actor = spawn(TypedEchoBehavior.create());
        actor.tell(&quot;Hello&quot;, probe.ref());
        assertEquals(&quot;Hello&quot;, probe.receiveOne(Duration.ofSeconds(1)));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对比要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;两种方式测试 API 相似&lt;/li&gt;
&lt;li&gt;Typed Actor 可以更精确地验证行为&lt;/li&gt;
&lt;li&gt;支持 &lt;code&gt;Behaviors.ignore()&lt;/code&gt; 用于测试中忽略消息&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;完整示例对比&lt;/h2&gt;
&lt;h3&gt;Classic Actor 实现&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ClassicCounterActor extends AbstractActor {
    private int count = 0;

    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .match(&quot;increment&quot;, msg -&gt; {
                count++;
                System.out.println(&quot;Count: &quot; + count);
            })
            .match(&quot;decrement&quot;, msg -&gt; {
                if (count &gt; 0) count--;
                System.out.println(&quot;Count: &quot; + count);
            })
            .matchEquals(&quot;reset&quot;, msg -&gt; {
                count = 0;
                System.out.println(&quot;Count reset&quot;);
            })
            .matchAny(msg -&gt; {
                System.out.println(&quot;Unknown message: &quot; + msg);
            })
            .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Typed Actor 实现&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class TypedCounterBehavior extends AbstractBehavior&amp;#x3C;String&gt; {
    private int count = 0;

    private TypedCounterBehavior(ActorContext&amp;#x3C;String&gt; context) {
        super(context);
    }

    public static Behavior&amp;#x3C;String&gt; create() {
        return Behaviors.setup(TypedCounterBehavior::new);
    }

    @Override
    public Receive&amp;#x3C;String&gt; createReceive() {
        return newReceiveBuilder()
            .onMessageEquals(&quot;increment&quot;, msg -&gt; {
                count++;
                getContext().getLog().info(&quot;Count: {}&quot;, count);
                return this;
            })
            .onMessageEquals(&quot;decrement&quot;, msg -&gt; {
                if (count &gt; 0) count--;
                getContext().getLog().info(&quot;Count: {}&quot;, count);
                return this;
            })
            .onMessageEquals(&quot;reset&quot;, msg -&gt; {
                count = 0;
                getContext().getLog().info(&quot;Count reset&quot;);
                return this;
            })
            .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;优缺点对比&lt;/h2&gt;
&lt;h3&gt;Typed Actor 优点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;类型安全&lt;/strong&gt;：编译期就能发现类型错误&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API 清晰&lt;/strong&gt;：Java 和 Scala 各自有独立的 API&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更好的组织&lt;/strong&gt;：鼓励状态模式，代码更易维护&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;错误处理&lt;/strong&gt;：更明确的错误处理机制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测试友好&lt;/strong&gt;：更易于编写测试用例&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;减少歧义&lt;/strong&gt;：移除了容易混淆的 sender/parent API&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Typed Actor 缺点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;学习曲线&lt;/strong&gt;：需要适应新的状态管理模式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更多代码&lt;/strong&gt;：状态管理需要显式定义&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API 改变&lt;/strong&gt;：需要适应新的消息处理方式&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Classic Actor 优点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;熟悉&lt;/strong&gt;：API 简单直观，易于上手&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灵活&lt;/strong&gt;：运行时才能发现类型错误&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兼容性&lt;/strong&gt;：与现有系统集成更方便&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Classic Actor 缺点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;类型不安全&lt;/strong&gt;：运行时才能发现类型错误&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API 混乱&lt;/strong&gt;：容易混淆 sender/parent API&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测试困难&lt;/strong&gt;：状态管理不明确&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;实际项目影响&lt;/h2&gt;
&lt;h3&gt;Gate 节点架构&lt;/h3&gt;
&lt;p&gt;问题：类型化的Actor无法直接由ActorSystem创建，需要通过守护Actor层级创建。在微服务架构中，gate节点创建的channel actor无法被root actor直接访问。&lt;/p&gt;
&lt;p&gt;解决方案：创建Gate Actor作为Gate节点的根Actor，负责创建和管理所有channel actor。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class GateActor(context: ActorContext&amp;#x3C;ChannelHandlerContext&gt;) : AbstractBehavior&amp;#x3C;ChannelHandlerContext&gt;(context) {

    companion object {
        fun create(): Behavior&amp;#x3C;ChannelHandlerContext&gt; = Behaviors.setup { GateActor(it) }
    }

    override fun createReceive(): Receive&amp;#x3C;ChannelHandlerContext&gt; {
        return newReceiveBuilder()
            .onMessage(ChannelHandlerContext::class.java) { createChannelActor(it) }
            .build()
    }

    private fun createChannelActor(ctx: ChannelHandlerContext): Behavior&amp;#x3C;ChannelHandlerContext&gt; {
        val ref = context.spawn(ChannelActor.create(ctx), &quot;channel&quot;)
        ctx.channel().attr(CHANNEL_ACTOR_KEY).set(ref)
        return this
    }
}

// Channel Actor 实现
class ChannelActor(context: ActorContext&amp;#x3C;ChannelMessage&gt;) : AbstractBehavior&amp;#x3C;ChannelMessage&gt;(context) {

    companion object {
        fun create(ctx: ChannelHandlerContext): Behavior&amp;#x3C;ChannelMessage&gt; = Behaviors.setup { ChannelActor(it, ctx) }
    }

    private val channelHandlerContext: ChannelHandlerContext

    private constructor(context: ActorContext&amp;#x3C;ChannelMessage&gt;, ctx: ChannelHandlerContext) : super(context) {
        this.channelHandlerContext = ctx
    }

    override fun createReceive(): Receive&amp;#x3C;ChannelMessage&gt; {
        return newReceiveBuilder()
            .onMessage(ClientMessage::class.java) { msg -&gt; handleClientMessage(msg) }
            .onMessage(ServerMessage::class.java) { msg -&gt; handleServerMessage(msg) }
            .build()
    }

    private fun handleClientMessage(msg: ClientMessage): Behavior&amp;#x3C;ChannelMessage&gt; {
        // 处理客户端消息
        return this
    }

    private fun handleServerMessage(msg: ServerMessage): Behavior&amp;#x3C;ChannelMessage&gt; {
        // 处理服务器消息
        return this
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Root Actor 架构&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;fun main() {
    val system = ActorSystem(GateActor.create(), &quot;GatewaySystem&quot;)

    // 通过守护 Actor 创建根 Actor
    system.become(
        Behaviors.setup { context -&gt;
            val gateActor = context.spawn(GateActor.create(), &quot;gate&quot;)
            Behaviors.receiveMessage&amp;#x3C;Void&gt; { null }
                .then { Behaviors.stopped() }
        }
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;从 Classic Actor 迁移到 Typed Actor 是一次重要的升级，主要收益包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;提升代码质量&lt;/strong&gt;：类型安全带来更好的代码质量&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更好的开发体验&lt;/strong&gt;：清晰的 API 和错误处理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更好的可维护性&lt;/strong&gt;：状态模式使代码更易维护&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;未来兼容性&lt;/strong&gt;：akka 官方推荐使用 Typed Actor&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于新项目，强烈建议直接使用 Typed Actor。对于现有项目，可以根据实际情况逐步迁移，akka 提供了完整的迁移指南。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>AU3语法</title><link>https://juzzi.qzz.io/blog/os/windows/au3</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/os/windows/au3</guid><description>Au3脚本是AutoIt3 Windows自动安装脚本语言，AutoIt 是一种自动控制工具。它可以被用来自动完成任何基于 Windows 或 DOS 的简单任务。</description><pubDate>Mon, 22 Mar 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;相关链接&lt;/h2&gt;
&lt;p&gt;文档：&lt;a href=&quot;https://www.autoitscript.com/autoit3/docs/&quot;&gt;https://www.autoitscript.com/autoit3/docs/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;函数库：&lt;a href=&quot;https://www.autoitscript.com/autoit3/docs/functions/&quot;&gt;https://www.autoitscript.com/autoit3/docs/functions/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;语法&lt;/h2&gt;
&lt;h3&gt;条件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;
If (&amp;#x3C;expression&gt;) Then

    &amp;#x3C;statement&gt;

EndIf

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常用函数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;MsgBox：对话框&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;  &lt;code&gt;MsgBox ( flag, &quot;title&quot;, &quot;text&quot; [, timeout = 0 [, hwnd]] )&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FileExists：检查文件是否存在&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;  &lt;code&gt;FileExists ( &quot;path&quot; )&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FileSelectFolder：选择文件夹&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;  &lt;code&gt;FileSelectFolder ( &quot;dialog text&quot;, &quot;root dir&quot; [, flag = 0 [, &quot;initial dir&quot; [, hwnd]]] )&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;  注：根目录可参考：&lt;a href=&quot;https://www.autoitscript.com/autoit3/docs/appendix/clsid.htm&quot;&gt;https://www.autoitscript.com/autoit3/docs/appendix/clsid.htm&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;_Crypt_HashData：数据串计算哈希&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;
    #include &amp;#x3C;Crypt.au3&gt;

    _Crypt_DeriveKey ( $vPassword, $iAlgID [, $iHashPasswordID = $CALG_MD5] )

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;_Crypt_HashFile：文件计算哈希&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;
    #include &amp;#x3C;Crypt.au3&gt;

    _Crypt_HashFile ( $sFilePath, $iAlgID )

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;StringTrimLeft：修剪字符串左边&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;  &lt;code&gt;StringTrimLeft（“ string”，count） &lt;/code&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Profile分析Python代码性能</title><link>https://juzzi.qzz.io/blog/lang/python/profile-ananyze-python-performance</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/lang/python/profile-ananyze-python-performance</guid><description>几乎每种语言都有分析代码性能的工具，Python也不例外，其自带的profile模块，就提供了该功能。</description><pubDate>Mon, 08 Mar 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;用法&lt;/h2&gt;
&lt;h3&gt;API方式&lt;/h3&gt;
&lt;p&gt;API方式允许我们以代码的方式来运行性能测试，API接口为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import profile
profile.run(&amp;#x3C;function_name&gt;, &amp;#x3C;file_name&gt;, &amp;#x3C;sort&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先需要导入profile模块，然后调用run函数。若未指定文件名，则会将分析结果打印出来。例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import profile

# 计算x1到x2的阶乘
def cal_factorial(x1, x2):
    r = 1
    for i in range(x1, x2 + 1):
        r *= i
    return r

def functionA():
    cal_factorial(1, 10000)

def functionB():
    cal_factorial(1, 100000)

def test_code():
    functionA()
    functionB()

# 打印结果，不排序
profile.run(&quot;test_code()&quot;)
# 打印结果，以累计时间排序
# profile.run(&quot;test_code()&quot;, None, &quot;cumulative&quot;)
# 结果保存到文件：profile
# profile.run(&quot;test_code()&quot;, &quot;profile&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;命令行&lt;/h3&gt;
&lt;p&gt;相比于API方式，使用命令行则会更加方便的分析我们已经写好的代码。语法：&lt;/p&gt;
&lt;p&gt;不保存文件：&lt;code&gt;python -m cProfile &amp;#x3C;code_file_name&gt;.py&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;保存分析结果到文件：&lt;code&gt;python -m cProfile -o &amp;#x3C;save_file_name&gt; &amp;#x3C;code_file_name&gt;.py&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;解析保存的文件&lt;/h2&gt;
&lt;p&gt;在上一步中生成的文件并非文本文件，无法直接打开查看结果，需要先将文件解析出来。&lt;/p&gt;
&lt;p&gt;解析需要用到pstats模块，一般直接在命令行中执行python脚本即可。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;python -c &quot;import pstats; p=pstats.Stats(&apos;&amp;#x3C;file_name&gt;&apos;); p.sort_stats(&apos;&amp;#x3C;sort&gt;&apos;).print_stats()&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;其中，sort_stats支持以下参数排序：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;calls/ncalls：调用次数&lt;/li&gt;
&lt;li&gt;cumtime/cumulative：累计时间&lt;/li&gt;
&lt;li&gt;filename/module：文件名&lt;/li&gt;
&lt;li&gt;line：行号&lt;/li&gt;
&lt;li&gt;name：函数名&lt;/li&gt;
&lt;li&gt;nfl：函数名，文件名，行号（name/file/line）&lt;/li&gt;
&lt;li&gt;pcalls：~~原始调用次数~~&lt;/li&gt;
&lt;li&gt;stdname：~~标准函数名~~&lt;/li&gt;
&lt;li&gt;time/tottime：时间（不包括子函数时间）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考：在pstats.py中找到如下定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The sort_stats() method now processes some additional options (i.e., in
    addition to the old -1, 0, 1, or 2 that are respectively interpreted as
    &apos;stdname&apos;, &apos;calls&apos;, &apos;time&apos;, and &apos;cumulative&apos;).  It takes either an
    arbitrary number of quoted strings or SortKey enum to select the sort
    order.

    For example sort_stats(&apos;time&apos;, &apos;name&apos;) or sort_stats(SortKey.TIME,
    SortKey.NAME) sorts on the major key of &apos;internal function time&apos;, and on
    the minor key of &apos;the name of the function&apos;.  Look at the two tables in
    sort_stats() and get_sort_arg_defs(self) for more examples.

class SortKey(str, Enum):
    CALLS = &apos;calls&apos;, &apos;ncalls&apos;
    CUMULATIVE = &apos;cumulative&apos;, &apos;cumtime&apos;
    FILENAME = &apos;filename&apos;, &apos;module&apos;
    LINE = &apos;line&apos;
    NAME = &apos;name&apos;
    NFL = &apos;nfl&apos;
    PCALLS = &apos;pcalls&apos;
    STDNAME = &apos;stdname&apos;
    TIME = &apos;time&apos;, &apos;tottime&apos;

sort_arg_dict_default = {
            &quot;calls&quot;     : (((1,-1),              ), &quot;call count&quot;),
            &quot;ncalls&quot;    : (((1,-1),              ), &quot;call count&quot;),
            &quot;cumtime&quot;   : (((3,-1),              ), &quot;cumulative time&quot;),
            &quot;cumulative&quot;: (((3,-1),              ), &quot;cumulative time&quot;),
            &quot;filename&quot;  : (((4, 1),              ), &quot;file name&quot;),
            &quot;line&quot;      : (((5, 1),              ), &quot;line number&quot;),
            &quot;module&quot;    : (((4, 1),              ), &quot;file name&quot;),
            &quot;name&quot;      : (((6, 1),              ), &quot;function name&quot;),
            &quot;nfl&quot;       : (((6, 1),(4, 1),(5, 1),), &quot;name/file/line&quot;),
            &quot;pcalls&quot;    : (((0,-1),              ), &quot;primitive call count&quot;),
            &quot;stdname&quot;   : (((7, 1),              ), &quot;standard name&quot;),
            &quot;time&quot;      : (((2,-1),              ), &quot;internal time&quot;),
            &quot;tottime&quot;   : (((2,-1),              ), &quot;internal time&quot;),
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;分析结果&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;         9 function calls in 2.361 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       1    0.000    0.000    2.361    2.361 profile:0(test_code())
       1    0.000    0.000    2.361    2.361 :0(exec)
       1    0.000    0.000    2.361    2.361 &amp;#x3C;string&gt;:1(&amp;#x3C;module&gt;)
       1    0.000    0.000    2.361    2.361 &amp;#x3C;ipython-input-11-a4480b4512b3&gt;:16(test_code)
       2    2.361    1.181    2.361    1.181 &amp;#x3C;ipython-input-11-a4480b4512b3&gt;:4(cal_factorial)
       1    0.000    0.000    2.328    2.328 &amp;#x3C;ipython-input-11-a4480b4512b3&gt;:13(functionB)
       1    0.000    0.000    0.033    0.033 &amp;#x3C;ipython-input-11-a4480b4512b3&gt;:10(functionA)
       1    0.000    0.000    0.000    0.000 :0(setprofile)
       0    0.000             0.000          profile:0(profiler)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;各字段含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ncalls：调用次数&lt;/li&gt;
&lt;li&gt;tottime：总时间（不包括子函数时间）&lt;/li&gt;
&lt;li&gt;cumtime：累计时间（包括子函数时间）&lt;/li&gt;
&lt;li&gt;percall：每次调用时间=总时间/调用次数&lt;/li&gt;
&lt;li&gt;filename：文件名&lt;/li&gt;
&lt;li&gt;lineno：行号&lt;/li&gt;
&lt;li&gt;function：函数名&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从分析结果中可以看出，本次运行test_code一共用了2.361秒，functionA用了0.033秒，functionB用了2.328秒，但他们函数本身都未消耗时间，时间都消耗在调用子函数：cal_factorial上。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Linux系统添加新硬盘</title><link>https://juzzi.qzz.io/blog/os/linux/linux-install-disk</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/os/linux/linux-install-disk</guid><description>大部分Linux系统不会安装图形界面，因此添加硬盘只能使用命令行操作。</description><pubDate>Wed, 18 Nov 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;本文所有操作都需要root来执行&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;检查硬盘&lt;/h2&gt;
&lt;p&gt;插上新硬盘或是在云服务器上购买新硬盘后，首先查看新硬盘是否被识别：&lt;code&gt;fdisk -l&lt;/code&gt;（xxx也可以）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@VM-0-11-centos ljx]# sudo fdisk -l

Disk /dev/vda: 53.7 GB, 53687091200 bytes, 104857600 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 262144 bytes / 262144 bytes
Disk label type: dos
Disk identifier: 0x0009ac89

   Device Boot      Start         End      Blocks   Id  System
/dev/vda1   *        2048   104857566    52427759+  83  Linux

Disk /dev/vdb: 107.4 GB, 107374182400 bytes, 209715200 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 262144 bytes / 262144 bytes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到有块新硬盘/dev/vdb，但是没有分区，因此接下来先给该硬盘分区。&lt;/p&gt;
&lt;h2&gt;硬盘分区&lt;/h2&gt;
&lt;p&gt;分区命令为：&lt;code&gt;fdisk /dev/vdb&lt;/code&gt;，其中/dev/vdb是上一步中看到的硬盘名称。该命令为交互式的脚本，命令步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;n：代表新建分区&lt;/li&gt;
&lt;li&gt;p：表示新建的分区为主分区&lt;/li&gt;
&lt;li&gt;回车：即分区编号使用默认的1&lt;/li&gt;
&lt;li&gt;回车：即第一个扇区使用默认的2048&lt;/li&gt;
&lt;li&gt;回车：即最后一个扇区使用默认的209715199&lt;/li&gt;
&lt;li&gt;w：保存&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;完整命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@VM-0-11-centos ljx]# fdisk /dev/vdb
Welcome to fdisk (util-linux 2.23.2).

Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Device does not contain a recognized partition table
Building a new DOS disklabel with disk identifier 0x77d3e73d.

Command (m for help): n
Partition type:
   p   primary (0 primary, 0 extended, 4 free)
   e   extended
Select (default p): p
Partition number (1-4, default 1):
First sector (2048-209715199, default 2048):
Using default value 2048
Last sector, +sectors or +size{K,M,G} (2048-209715199, default 209715199):
Using default value 209715199
Partition 1 of type Linux and of size 100 GiB is set

Command (m for help): w
The partition table has been altered!

Calling ioctl() to re-read partition table.
Syncing disks.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;格式化&lt;/h2&gt;
&lt;p&gt;分区好之后，再次使用&lt;code&gt;fdisk -l&lt;/code&gt;命令可以看到，新分区为/dev/vdb1。使用分区命令：&lt;code&gt; mkfs.xfs -f /dev/vdb1&lt;/code&gt;将该分区格式化为xfs磁盘格式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;meta-data=/dev/vdb1              isize=512    agcount=16, agsize=1638384 blks
         =                       sectsz=512   attr=2, projid32bit=1
         =                       crc=1        finobt=0, sparse=0
data     =                       bsize=4096   blocks=26214144, imaxpct=25
         =                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0 ftype=1
log      =internal log           bsize=4096   blocks=12799, version=2
         =                       sectsz=512   sunit=0 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;挂载分区&lt;/h2&gt;
&lt;p&gt;挂载分为两种，一种是立即挂载，挂载后马上就可以使用，但是重启后失效。另一种是启动挂载，即在系统启动时自动挂载。&lt;/p&gt;
&lt;h3&gt;立即挂载&lt;/h3&gt;
&lt;p&gt;使用命令：&lt;code&gt;mount -t xfs /dev/vdb1 /data&lt;/code&gt;将该分区挂载到/data目录（目录必须存在），然后使用df -h即可看到已挂载成功。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ljx@VM-0-11-centos download]$ df -h
Filesystem      Size  Used Avail Use% Mounted on
devtmpfs        1.9G     0  1.9G   0% /dev
tmpfs           1.9G   24K  1.9G   1% /dev/shm
tmpfs           1.9G  608K  1.9G   1% /run
tmpfs           1.9G     0  1.9G   0% /sys/fs/cgroup
/dev/vda1        50G  3.7G   44G   8% /
tmpfs           379M     0  379M   0% /run/user/0
tmpfs           379M     0  379M   0% /run/user/1000
/dev/vdb1       100G   33M  100G   1% /data
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启动挂载&lt;/h3&gt;
&lt;p&gt;编辑文件：&lt;code&gt;vi /etc/fstab&lt;/code&gt;，在末尾加入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/dev/vdb1 /data xfs defaults 0 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;操作系统就会在启动时自动将/dev/vdb1挂载到/data，0 0代表挂载时不检查硬盘分区。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>普通用户使用sudo获取部分root权限</title><link>https://juzzi.qzz.io/blog/os/linux/sudo</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/os/linux/sudo</guid><description>使用sudo可以让普通用户像root一样执行部分高权限操作。</description><pubDate>Tue, 15 Sep 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;编辑sudo配置&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;visudo -f /etc/sudoers.d/juzi&lt;/code&gt;
其中，juzi为用户名&lt;/p&gt;
&lt;h2&gt;sudo语法&lt;/h2&gt;
&lt;p&gt;who host=(run_as) TAG:command&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;who：运行者用户名&lt;/li&gt;
&lt;li&gt;host：主机&lt;/li&gt;
&lt;li&gt;run_as：以目标身份运行&lt;/li&gt;
&lt;li&gt;TAG：标签（PASSWD/NOPASSWD）&lt;/li&gt;
&lt;li&gt;command：命令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例：&lt;code&gt;juzi ALL=(root) PASSWD:/usr/sbin/useradd&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;表示用户juzi可以在任何主机用root身份在需要输入密码的情况下使用useradd命令。&lt;/p&gt;
&lt;h2&gt;常用条目&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;系统消息&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;juzi ALL=(root) NOPASSWD:/bin/tail -* /var/log/*
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;软件安装&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;juzi ALL=(root) PASSWD:SOFTWARE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：需要取消/etc/sudoers中的&lt;code&gt;Cmnd_Alias SOFTWARE = ...&lt;/code&gt;的注释。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编辑/etc下的文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;juzi ALL=(root) NOPASSWD:/usr/bin/vi /etc/*
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;make install源码安装&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;juzi ALL=(root) PASSWD:/usr/bin/make install
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Linux Shell——Bash</title><link>https://juzzi.qzz.io/blog/lang/shell/bash</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/lang/shell/bash</guid><description>Bash是众多Linux发行版中最常用的shell，功能丰富。</description><pubDate>Tue, 09 Jun 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;变量&lt;/h2&gt;
&lt;h3&gt;定义变量(变量赋值)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;variable=&quot;xxx&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注：= 左右不能有空格&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;取变量值&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;echo $variable&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;取输入变量&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$@&lt;/code&gt;：所有变量的列表，以空格隔开&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$#&lt;/code&gt;: 变量个数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$1&lt;/code&gt;: 第一个变量，&lt;code&gt;$2, $3&lt;/code&gt;以此类推&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$?&lt;/code&gt;：上一个函数调用的返回值&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;数组&lt;/h2&gt;
&lt;h3&gt;定义数组&lt;/h3&gt;
&lt;p&gt;语法：&lt;code&gt;array=(x1 x2 ...)&lt;/code&gt;，array为变量名，x1、x2为数组元素&lt;/p&gt;
&lt;h3&gt;数组取值&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;取单个值：&lt;code&gt;${array[index]}&lt;/code&gt;，index从0开始。&lt;/li&gt;
&lt;li&gt;取所有值：&lt;code&gt;${array[*]}&lt;/code&gt;或&lt;code&gt;${array[@]}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数组长度&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;${#array[*]}&lt;/code&gt;或&lt;code&gt;${#array[@]}&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;条件&lt;/h2&gt;
&lt;p&gt;语法：&lt;code&gt;x &amp;#x3C;con&gt; y&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;条件判断符与两边对象x, y之间的空格可以省略，即&lt;code&gt;x&amp;#x3C;con&gt;y&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;数字条件判断&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;等于：&lt;code&gt;-eq&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;不等于：&lt;code&gt;-ne&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;大于：&lt;code&gt;-gt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;大于等于：&lt;code&gt;-ge&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;小于：&lt;code&gt;-lt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;小于等于：&lt;code&gt;-le&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;字符串条件判断&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;相等：&lt;code&gt;=&lt;/code&gt;或&lt;code&gt;==&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;不相等：&lt;code&gt;!=&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注：字符串为变量时，需要用引号引起来，例如：&lt;code&gt;&quot;$str&quot; == &quot;success&quot;&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;if语句&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;if [ &amp;#x3C;condition&gt; ]; then
    &amp;#x3C;statement&gt;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;if和[ ]之间必须要空格。&lt;/li&gt;
&lt;li&gt;[ ]和之间必须要空格。&lt;/li&gt;
&lt;li&gt;;和then之间可空格可以省略。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h3&gt;if-else语句&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;if [ &amp;#x3C;condition&gt; ]; then
    &amp;#x3C;statement&gt;
else
    &amp;#x3C;statement&gt;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;if-elseif 语句&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;if [ &amp;#x3C;condition&gt; ]; then
    &amp;#x3C;statement&gt;
elif [ &amp;#x3C;condition&gt; ]; then
    &amp;#x3C;statement&gt;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;循环&lt;/h2&gt;
&lt;h3&gt;for数组循环&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;for param in $array; do
    &amp;#x3C;statement&gt;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;for条件循环&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;for (( i = 0 ; i &amp;#x3C; &amp;#x3C;variable&gt; ; i++ )); do
    &amp;#x3C;statement&gt;
done

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;while循环&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;while :
do
    &amp;#x3C;statement&gt;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;函数&lt;/h2&gt;
&lt;h3&gt;定义函数&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;name(){
    &amp;#x3C;statement&gt;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Git常用命令</title><link>https://juzzi.qzz.io/blog/devel/version-control/git</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/devel/version-control/git</guid><description>git是目前最常用的版本管理工具</description><pubDate>Thu, 28 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;暂存&lt;/h2&gt;
&lt;h3&gt;取消stage&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git restore --staged &amp;#x3C;file_name&gt;&lt;/code&gt;或&lt;code&gt;git reset HEAD &amp;#x3C;file_name&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;删除未stage的已修改（删除）的文件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单个文件：&lt;code&gt;git checkout &amp;#x3C;file_name&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所有文件：&lt;code&gt;git checkout .&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;删除未stage的新增的文件&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git clean -df&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;回退&lt;/h2&gt;
&lt;h3&gt;修改上次提交信息或作者&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git commit --amend --author=&quot;新作者名 &amp;#x3C;新邮箱@example.com&gt;&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;执行后会自动打开编辑器，你可以顺便修改提交信息，保存退出即可。&lt;/p&gt;
&lt;h3&gt;撤回提交&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git reset HEAD~{n}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;其中n替换为撤回的具体提交的个数，撤回后本地仓库变为未提交状态，需重新提交。若已推送到远程仓库，则必须git push -f来覆盖远程仓库（慎用，可能会将别人的提交覆盖）&lt;/p&gt;
&lt;h3&gt;回退到某个提交&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git reset --hard {commit_id}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;其中commit_id为需要回退到的提交id。若已推送到远程仓库，则必须git push -f来覆盖远程仓库（慎用，可能会将别人的提交覆盖）&lt;/p&gt;
&lt;h3&gt;用远程仓库强行覆盖本地仓库&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git reset --hard origin/master&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;合并&lt;/h2&gt;
&lt;h3&gt;Merge（分支）&lt;/h3&gt;
&lt;p&gt;若分支B是在分支A上创建的，想要将分支B上的提交合并到分支A中&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;检出分支A：&lt;code&gt;git checkout A&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;合并：&lt;code&gt;git merge B&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注：若A没有进行过其他提交，则可以直接快速合并（实际上只是修改分支A的指针）。如果A进行过其他提交，则会进行三方合并（A、B、分岔起点），中途可能会产生冲突，解决冲突后再stage (git add xx) ，再提交即可。&lt;/p&gt;
&lt;h3&gt;Merge（仓库）&lt;/h3&gt;
&lt;p&gt;若仓库B是由仓库A fork出来的，想要将A中的提交合并到B中&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;将A添加为远程仓库：&lt;code&gt;git remote add upstream xxx&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将upstream取回本地：&lt;code&gt;git fetch upsteam&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;合并：&lt;code&gt;git merge upsteam/master&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注：同样可能产生冲突，若有冲突，解决即可。&lt;/p&gt;
&lt;h3&gt;Rebase&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;分支变基&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;  若分支B是在分支A上创建的，创建后A与B均进行了多次提交，想要直接将B合并到A会产生提交记录分岔，将B变基到A即可避免分岔的问题，步骤：&lt;/p&gt;
&lt;p&gt;  1. 检出分支A：&lt;code&gt;git checkout A&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;  2. 将B变基到A：&lt;code&gt;git rebase A B&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;  3. 合并B：&lt;code&gt;git merge B&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;解决pull后分岔：&lt;code&gt;git rebase&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pull并解决分岔：&lt;code&gt;git pull --rebase&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;合并多次提交：&lt;code&gt;git rebase -i HEAD~{n}&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Cherry-Pick&lt;/h3&gt;
&lt;p&gt;使用cherry-pick可以将某个分支的某个提交（而不是整个分支）合并到当前分支&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;拷贝想要合并的某个提交id&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;切换到想要合并到的分支&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;合并：&lt;code&gt;git cherry-pick xxx&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;远程仓库&lt;/h2&gt;
&lt;h3&gt;设置upstream分支&lt;/h3&gt;
&lt;p&gt;设置upstream分支，可以在push时省略remote仓库名和分支&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git push --set-upstream xxx xxx&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;删除远程仓库分支&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git push --delete origin &amp;#x3C;branch_name&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;同步远程仓库已删除的分支&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;git remote prune origin&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;忽略修改&lt;/h2&gt;
&lt;p&gt;忽略某文件的修改，就好像其没有被修改过一样&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;忽略：&lt;code&gt;git update-index --assume-unchanged &amp;#x3C;file&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取消忽略：&lt;code&gt;git update-index --no-assume-unchanged &amp;#x3C;file&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>SSH密钥登陆Linux服务器</title><link>https://juzzi.qzz.io/blog/os/linux/ssh-key-login</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/os/linux/ssh-key-login</guid><description>服务器配置方法</description><pubDate>Thu, 28 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;使用前需要先用[[generate-ssh-key]]创建ssh-key&lt;/p&gt;
&lt;h2&gt;连接Linux服务器&lt;/h2&gt;
&lt;h3&gt;步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;进入用户目录：&lt;code&gt;cd&lt;/code&gt;或&lt;code&gt;cd /home/xxx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;创建.ssh文件夹：&lt;code&gt;mkdir .ssh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修改.ssh文件夹权限为700&lt;/strong&gt;：&lt;code&gt;chmod 700 .ssh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;进入.ssh文件夹：&lt;code&gt;cd .ssh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;创建或编辑authorized_keys文件：&lt;code&gt;vi authorized_keys&lt;/code&gt;，将公钥添加到文件末尾。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修改authorized_keys权限为600&lt;/strong&gt;：&lt;code&gt;chmod 600 authorized_keys&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;修改sshd服务配置文件，允许公钥登陆：&lt;code&gt;sudo vi /etc/ssh/sshd_config&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 允许私钥登陆
PubkeyAuthentication yes
# 指定私钥文件
AuthorizedKeysFile .ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;8&quot;&gt;
&lt;li&gt;重启sshd服务：&lt;code&gt;systemctl restart sshd.service&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;若进行了以上操作仍然提示失败，则可使用命令&lt;code&gt;ssh -Tv $server_ip&lt;/code&gt;进行调试。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;如果是CentOS6.x的系统，还需要关闭SeLinux才能正常登录。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;sshd_config配置说明&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.freebsd.org/cgi/man.cgi?sshd_config(5)&quot;&gt;https://www.freebsd.org/cgi/man.cgi?sshd_config(5)&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;服务器host key变化导致连接失败&lt;/h3&gt;
&lt;p&gt;若服务器重装了系统，或者有硬件变化，则可能会导致host key变化，此时客户端通过ssh连接时会出现如下报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@       WARNING: POSSIBLE DNS SPOOFING DETECTED!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
The RSA host key for [xxx.com]:57522 has changed,
and the key for the corresponding IP address [172.26.101.72]:57522
is unknown. This could either mean that
DNS SPOOFING is happening or the IP address for the host
and its host key have changed at the same time.
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
SHA256:du3fKBQRb+udLRzrYt3tVISqL9reYPC2jd4F6klP9Tc.
Please contact your system administrator.
Add correct host key in C:\\Users\\xxx/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in C:\\Users\\xxx/.ssh/known_hosts:2
RSA host key for [xxx.com]:57522 has changed and you have requested strict checking.
Host key verification failed.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决方法：去.ssh/known_hosts下删除服务器对应的host key或者执行如下命令：
&lt;code&gt;ssh-keygen -R [xxx.com]:57522&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;连接GitHub&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;登录&lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;点右上角的头像，选择&lt;code&gt;Settings&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;点左侧&lt;code&gt;Access&lt;/code&gt;栏里的&lt;code&gt;SSH and GPG keys&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;点右侧New SSH key。&lt;/li&gt;
&lt;li&gt;Title填写该密钥的名字（通过该名字来区分密钥，因为添加后是看不到公钥内容的）。&lt;/li&gt;
&lt;li&gt;Key type保持默认的&lt;code&gt;Authentication Key&lt;/code&gt;就好。&lt;/li&gt;
&lt;li&gt;Key填写公钥的内容，即xxx.pub里面的。&lt;/li&gt;
&lt;li&gt;最后点Add SSH key即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://docs.github.com/zh/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account&quot;&gt;https://docs.github.com/zh/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;连接GitLab&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;登录GitLab。&lt;/li&gt;
&lt;li&gt;点右上角的头像，选择&lt;code&gt;编辑个人资料&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;点左侧&lt;code&gt;SSH密钥&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;密钥填写公钥的内容，即xxx.pub里面的。&lt;/li&gt;
&lt;li&gt;标题填写该密钥的名字（通过该名字来区分密钥，因为添加后是看不到公钥内容的）。&lt;/li&gt;
&lt;li&gt;到期时间如果不选，则会一直有效。&lt;/li&gt;
&lt;li&gt;最后点添加密钥即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://docs.gitlab.com/ee/user/ssh.html#add-an-ssh-key-to-your-gitlab-account&quot;&gt;https://docs.gitlab.com/ee/user/ssh.html#add-an-ssh-key-to-your-gitlab-account&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>生成SSH密钥</title><link>https://juzzi.qzz.io/blog/os/linux/generate-ssh-key</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/os/linux/generate-ssh-key</guid><description>包含多个系统的生成方法</description><pubDate>Wed, 20 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;安装ssh-keygen&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Mac或者Linux：一般都自带了SSH Client，所以也自带了ssh-keygen，因此无需额外安装。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Windows：最简单的方法就是下载&lt;a href=&quot;https://git-scm.com/download/win&quot;&gt;Windows版的Git客户端&lt;/a&gt;，因为它自带了&lt;strong&gt;ssh-keygen&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;所以如果你正在用Windows，而又没有安装Git，最快的办法就是找一台Linux服务器去上面生成一下再Download下来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;步骤&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;打开命令行&lt;/li&gt;
&lt;li&gt;输入命令：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh-keygen -t ed25519 -C &quot;your_email@example.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中ed25519可替换rsa，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh-keygen -t rsa -b 4096 -C &quot;your_email@example.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-C&lt;/code&gt;选项代表指定注释内容，一般使用邮件作为注释，该注释会记录在公钥中，方便在服务器上进行识别。若不加&lt;code&gt;-C &quot;your_email@example.com&lt;/code&gt;，也会自动以&lt;code&gt;用户名@主机名&lt;/code&gt;的格式生成注释。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Github从2022年3月15日起停止支持了dsa类型的密钥访问，如果你要用该密钥来访问Github，就不要生成dsa类型的。3. 根据提示，输入生成位置&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;Generating public/private ed25519 key pair.
Enter file in which to save the key (/c/Users/user/.ssh/id_ed25519):
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不做修改（直接按&lt;em&gt;Enter&lt;/em&gt;），默认会生成到用户目录下的**.ssh**文件夹下的id_xxx文件，xxx代表密钥类型。4. 根据提示，输入密码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&gt; Enter passphrase (empty for no passphrase):
&gt; Enter same passphrase again:
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;设置密码后，每次使用密钥时都需要输入密码后才能使用，如果不设置密码，直接按两次&lt;em&gt;Enter&lt;/em&gt;即可。5. 找到生成的密钥文件
文件会出现在第3步中指定的位置，xxx.pub为公钥，存放在服务器上；xxx为私钥，存放在自己电脑上，可以连接有对应公钥的服务器。&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>数据序列化组件PB与FB对比</title><link>https://juzzi.qzz.io/blog/devel/protocol/protobuf-vs-flatbuffer</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/devel/protocol/protobuf-vs-flatbuffer</guid><description>Protocol Buffers与FlatBuffers详细对比。</description><pubDate>Mon, 02 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;  Protocol Buffers，也被称为Protobuf，是由谷歌在2008年开源的与语言无关、与平台无关的可扩展的结构化数据序列化组件，可用于通信协议、数据存储等场景。自开源以来，因为其简单易用、压缩率高、运行效率高、开发效率高等特点，经历了时间的验证，在业界有广泛的应用。&lt;/p&gt;
&lt;p&gt;  那么我们在项目中是不是就可以直接使用了呢？我认为这样是欠妥的，Protobuf的确非常优秀，各方面的测试成绩都名列前茅，但并不是它在所有方面都是第一，如果你的项目对某个性能指标有特别严苛的要求，就需要因地制宜、根据实际需求选择最适合的一款序列化组件。&lt;/p&gt;
&lt;p&gt;  今天要介绍的是另一款数据序列化组件，同样是由谷歌开发并开源，它就是FlatBuffers。&lt;/p&gt;
&lt;h1&gt;FlatBuffers介绍&lt;/h1&gt;
&lt;p&gt;  根据&lt;a href=&quot;https://google.github.io/flatbuffers/&quot;&gt;官网&lt;/a&gt;的介绍，FlatBuffers是一个高效的、跨平台的序列化组件，支持多种编程语言，是专门为游戏开发和其他性能关键的应用而开发的。他与Protobuf相比有什么区别呢？为什么就更适合游戏开发呢？谷歌很直接的回答了这个问题：Protobuf确实与FlatBuffers比较相似，最主要的区别就是，FlatBuffers并不需要一个转换/解包的步骤就可以获取原数据。&lt;/p&gt;
&lt;p&gt;  因为我们知道，在游戏场景下的网络通信中，玩家往往是对延迟非常敏感的（尤其是在FPS，Moba类游戏中），抛去网络本身的网络延迟不谈，如果能够降低数据解析（反序列化）的延迟，就能降低玩家操作的延迟感，提升游戏体验。&lt;/p&gt;
&lt;p&gt;  根据官网的描述，我们对Protobuf与FlatBuffers有如下大致的对比：&lt;/p&gt;
&lt;p&gt;|              | Protobuf                                                                                                                       | Flatbuffers                                                                                       |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| 支持语言     | C/C++, C#, Go, Java, Python, Ruby, Objective-C, Dart                                                                       | C/C++, C#, Go, Java, JavaScript, TypeScript, Lua, PHP, Python, Rust, Lobster                  |
| 版本         | 2.x/3.x，不相互兼容                                                                                                            | 1.x                                                                                               |
| 协议文件     | .proto，需指定协议文件版本                                                                                                     | .fbs                                                                                              |
| 代码生成工具 | 有（生成代码量较多）                                                                                                           | 有（生成代码量较少）                                                                              |
| 协议字段类型 | bool, bytes, int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, float, double, string | bool, int8, uint8, int16, uint16, int32, uint32, int64, uint64, float, double, string, vector |&lt;/p&gt;
&lt;p&gt;  为了对Protobuf与FlatBuffers的性能有更具体的对比，我做了如下的性能测试，具体代码见&lt;a href=&quot;https://git.ppgame.com/lijixue/protocol-benchmark-java&quot;&gt;Git仓库&lt;/a&gt;。&lt;/p&gt;
&lt;h1&gt;测试对比&lt;/h1&gt;
&lt;p&gt;  首先需要选择一组测试数据。为了尽可能真实的模拟业务数据，我选取了3种类型的数据，分别是少量数据、中量数据和大量数据。&lt;/p&gt;
&lt;h2&gt;测试数据&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;少量数据：只有1个int32，这种情况主要模拟简单的客户端请求/服务器返回，实际业务中有部分通信消息都是这种简单的少量数据，原数据大小为4字节。&lt;/li&gt;
&lt;li&gt;中量数据：有常用的数据类型：int32、int64、float和string，每个类型均为10个，每个string大小为10字节，则原数据大小为260字节（10 * (4+8+4+10)）。因为这些都是用的最多的业务数据类型，实际业务中有大量通信消息都是这种数据量一般的中量数据。&lt;/li&gt;
&lt;li&gt;大量数据：同样也有常用的数据类型：int32、int64、float和string，每个类型均为10000个。因为业务中可能会出现少量大消息数据的情况，可以利用该情景来测试一下比较极限的情况，原数据大小为253KB（10000 * (4+8+4+10) / 1024）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;测试环境&lt;/h2&gt;
&lt;p&gt;测试语言：Java 1.8&lt;/p&gt;
&lt;p&gt;操作系统：CentOS Linux release 6.5 (Final)&lt;/p&gt;
&lt;p&gt;JVM：Java HotSpot(TM) 64-Bit Server VM 1.8.0_112&lt;/p&gt;
&lt;p&gt;Protobuf：3.11.4&lt;/p&gt;
&lt;p&gt;FlatBuffers：1.11.0&lt;/p&gt;
&lt;p&gt;  为了对两款组件的使用成本进行对比，我分别列举了它们的开发使用步骤。&lt;/p&gt;
&lt;h2&gt;ProtoBuf使用步骤&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;制定协议文件，并命名为Pb.proto&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-protobuf&quot;&gt;// 协议版本为3.x
syntax = &quot;proto3&quot;;
// 指定生成消息类的Java包
option java_package = &quot;com.digisky.protocol.msg.pb&quot;;


// 消息
message Msg {
    // int32数据
    int32 intData = 1;
    // 数据消息
    repeated DataMsg datas = 2;
}

message DataMsg {
    // int32数据
    int32 intData = 1;
    // int64数据
    int64 longData = 2;
    // float数据
    float floatData = 3;
    // string数据
    string stringData = 4;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  为了方便测试，定义了一个通用消息Msg，其中包含一个int32类型用作“少量”型数据测试，又定义了一个DataMsg类型的数据用作“中量”与“大量”型的数据测试。需要注意的是，如果使用3.x版本的Protobuf，必须加入&lt;code&gt;syntax = &quot;proto3&quot;&lt;/code&gt;的版本标记，并且在3.x版本中移除了2.x版本中的required与optional字段描述，默认所有字段都是optional，这样可以提高消息兼容性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用编译器“protoc”来编译协议文件，生成协议代码。（编译器可以从&lt;a href=&quot;https://github.com/protocolbuffers/protobuf/releases&quot;&gt;Github仓库&lt;/a&gt;下载）&lt;/p&gt;
&lt;p&gt;编译命令：&lt;code&gt;protoc --java_out=../../src/main/java/ *.proto&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;其中&lt;code&gt;*.proto&lt;/code&gt;表示编译当前目录下的所有proto协议文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看生成的Java代码，可以看到已生成了类com.digisky.protocol.msg.pb.Pb。&lt;/p&gt;
&lt;p&gt;  只有2个消息的Protobuf协议文件，生成了约2000行的代码。根据我以往的使用经验，若用一个协议文件记录一个模块的协议消息，仅需要十多个消息就会生成数万行的Java代码，因此Protobuf生成的代码文件算是比较庞大的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加消息序列化代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;protected byte[] serialize(TestData data) {
    Builder builder = Msg.newBuilder();
    // 填充int32
    builder.setIntData(data.getData());
    // 填充data
    for (DataObject dataObject : data.getDataArray()) {
        DataMsg.Builder dataBuilder = DataMsg.newBuilder();
        dataBuilder.setIntData(dataObject.getIntData());
        dataBuilder.setLongData(dataObject.getLongData());
        dataBuilder.setFloatData(dataObject.getFloatData());
        dataBuilder.setStringData(dataObject.getStringData());
        builder.addDatas(dataBuilder);
    }
    Msg msg = builder.build();
    return msg.toByteArray();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  首先是用静态工厂new了一个Msg的builder，然后填充intData，因为datas是复合类型，又是数组（repeated），所以用循环的方式new出DataMsg的builder，填充好DataMsg的builder，然后再将其加入到Msg的builder。再通过build方法构建出Msg对象，最后通过toByteArray方法转化为byte数组，就可以将该byte[]通过网络或其他形式发送出去。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加反序列化代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;protected void deserialize(byte[] serializedData) {
    try {
        Msg.newBuilder().mergeFrom(serializedData);
    } catch (InvalidProtocolBufferException e) {
        e.printStackTrace();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  反序列化就特别简单，（从网络中）拿到收到的byte[]后，一般先通过网络包的消息头中的消息号，确定本消息对应的消息类，假定我们这里对应的是Msg类，直接通过newBuilder方法和mergeFrom方法就可以得到Msg的builder，然后就可以builder对象中的数据了。&lt;/p&gt;
&lt;p&gt;  至此，Protobuf使用完毕。可以看到，如果在框架中已经集成好了Protobuf，添加一个新消息，每个步骤都且简单，也没有任何冗余的步骤。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;FlatBuffers使用步骤&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;制定协议文件，并命名为Fb.fbs&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 指定生成消息类的Java包
namespace com.digisky.protocol.msg.fb;

// 消息
table Msg {
    // int32数据
    intData:int;
    // 数据消息
    datas:[DataMsg];
}

table DataMsg {
    // int32数据
    intData:int;
    // int64数据
    longData:int64;
    // float数据
    floatData:float;
    // string数据
    stringData:string;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  FlatBuffers的消息都以table类型来定义，与Protobuf不同的是，字段是以变量名在前，变量类型在后，并且默认省略了字段顺序（也可以加上）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编译方式与Protobuf非常类似，使用编译器“flatc”来编译协议文件，生成协议代码。（编译器可以从&lt;a href=&quot;https://github.com/google/flatbuffers/releases&quot;&gt;Github仓库&lt;/a&gt;下载）&lt;/p&gt;
&lt;p&gt;编译命令：&lt;code&gt;flatc --java -o ../../src/main/java/ Fb.fbs&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;  需要注意的是flatc不支持通配符来指定协议文件，不能像Protobuf一样使用*.fbs。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同样查看生成的Java代码，可以看到在com.digisky.protocol.msg.fb包下生成了两个类：Msg与DataMsg。&lt;/p&gt;
&lt;p&gt;  所以FlatBuffers的生成规则是为每个table生成单独的类，建议将一个模块的消息单独放到一个包中，每个类经过格式化以后大约只有100行，跟Protobuf相比算是小了许多。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加序列化消息代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;protected byte[] serialize(TestData data) {
    FlatBufferBuilder builder = new FlatBufferBuilder();
    DataObject[] dataObjects = data.getDataArray();
    int[] dataOffsets = new int[dataObjects.length];
    // 构建data偏移
    for (int i = 0; i &amp;#x3C; dataObjects.length; i++) {
        DataObject dataObject = dataObjects[i];
        // 填充string，获得stringDataOffset
        int stringDataOffset = builder.createString(dataObject.getStringData());
        // 填充data的其他字段，获得oneDataOffset
        int oneDataOffset = DataMsg.createDataMsg(builder, dataObject.getIntData(), dataObject.getLongData(),
                dataObject.getFloatData(), stringDataOffset);
        dataOffsets[i] = oneDataOffset;
    }
    int dataOffset = Msg.createDatasVector(builder, dataOffsets);
    Msg.startMsg(builder);
    // 填充int32
    Msg.addIntData(builder, data.getData());
    // 填充data
    Msg.addDatas(builder, dataOffset);
    int end = Msg.endMsg(builder);
    builder.finish(end);
    return builder.sizedByteArray();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  首先是需要new一个FlatBufferBuilder出来，以后填充数据都需要指定到这个builder当中。由于Msg当中引用了DataMsg类型，因此需要先创建DataMsg，获得DataMsg的偏移量，再将偏移量填充到Msg当中。可以看到，FlatBuffers处理自定义类型或是数组等非基本类型的数据时，都是通过先计算偏移量，再填充偏移量来实现的，这个过程比Protobuf略微复杂。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加反序列化代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void deserialize(byte[] serializedData) {
    ByteBuffer buffer = ByteBuffer.wrap(serializedData);
    Msg msg = Msg.getRootAsMsg(buffer);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  反序列化就相对简单了，先利用NIO将byte[]包装成ByteBuffer，然后利用Msg的getRootAsMsg即可获得Msg对象，就可以进行取值操作了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;性能测试&lt;/h2&gt;
&lt;p&gt;  通过上述使用步骤可以看出，Protobuf与FlatBuffers的使用步骤基本一致，最主要的区别就是Protobuf生成的代码量庞大，消息封装简单；而FlatBuffers则相反，生成的代码量少，消息封装稍显复杂。那么他们的性能到底如何呢，在少量、中量、大量数据的情形下表现如何呢，我从数据大小、序列化时间、反序列化时间、内存使用等维度进行了测试对比。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据大小&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;|             | 少量数据（4字节） | 中量数据（260字节） | 大量数据（253千字节） |
| ----------- | ----------------- | ------------------- | --------------------- |
| Protobuf    | 2字节             | 201字节             | 229,735字节           |
| FlatBuffers | 28字节            | 496字节             | 440,056字节           |&lt;/p&gt;
&lt;p&gt;  可以看到，在数据量很少的情况下，Protobuf要比FlatBuffers小很多倍，当数据量逐渐增大后，Protobuf最终会比FlatBuffers小一半左右。所以结论是，Protobuf比FlatBuffers在数据大小上更优（约领先一倍），但因为不同的数据量，不同的字段类型都会影响到该性能指标，这里只得出一个大致的结论，我将在后面源码解析部分做更详细的分析。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;序列化时间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;|             | 少量数据（4字节） | 中量数据（260字节） | 大量数据（253千字节） |
| ----------- | ----------------- | ------------------- | --------------------- |
| Protobuf    | 1466纳秒          | 10757纳秒           | 2,000,497纳秒         |
| FlatBuffers | 1922纳秒          | 9754纳秒            | 2,760,061纳秒         |&lt;/p&gt;
&lt;p&gt;  可以看到，在序列化时间上，Protobuf与Flatbuffers整体上不相上下。因此可以得出结论：Protobuf与FlatBuffers在序列化耗时上性能基本一致。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;反序列化时间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;|             | 少量数据（4字节） | 中量数据（260字节） | 大量数据（253千字节） |
| ----------- | ----------------- | ------------------- | --------------------- |
| Protobuf    | 2040纳秒          | 5393纳秒            | 1,101,464纳秒         |
| FlatBuffers | 847纳秒           | 312纳秒             | 286纳秒               |&lt;/p&gt;
&lt;p&gt;  可以看到，在数据量很少的情况下，FlatBuffers的反序列化时间大约比Protobuf少一半，而在中等数据的情况下优势就已经比较明显，在大量数据的情况下呢？&lt;/p&gt;
&lt;p&gt;  Protobuf直接被秒杀！这也验证了本文刚开始介绍的Flatbuffers的特性：不需要转换/解包的操作就能够获得原数据，因此反序列化时间几乎为0。所以结论是：FlatBuffers可以在反序列化性能上打败任何框架/组件，因为它已经将该性能做到了极致。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内存使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;  因为使用Java语言来测试的缘故，无法准确统计到底使用了多少内存，但可以通过垃圾回收来侧面反映整个测试过程的内存使用情况，我设定了最大堆内存（Xmx）为16MB，首先来看Protobuf的gc情况。&lt;/p&gt;
&lt;p&gt;  可以看到，对于测试数据，Protobuf序列化与反序列化总共消耗了1分06秒，CPU平均占用42%，垃圾回收占用12.2%，GC情况如图。&lt;/p&gt;
&lt;p&gt;  而Flatbuffers只运行了32秒，因为反序列化特性的缘故节省了很多时间，比较符合预期。而CPU利用率只有27%，垃圾回收也只占2.9%，GC次数更是明显比Protobuf少了至少一半，因此Flatbuffers运行过程比较轻量，占用资源较少。&lt;/p&gt;
&lt;h1&gt;源码分析&lt;/h1&gt;
&lt;p&gt;  那么为什么Protobuf可以将数据大小做得这么小呢？一般序列化后的数据会加上一些协议本身的数据，但Protobuf最终的数据大小竟然比数据本身还小，他是如何压缩的呢？还有，Flatbuffers是怎样将反序列化时间做到0这样一个极限的呢？让我们来从源码中一一找到答案。&lt;strong&gt;！！！催眠预警，如果你对源码不感兴趣，也可以直接跳到文末看结论&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Protobuf的数据压缩&lt;/h2&gt;
&lt;p&gt;  Protobuf的数据处理算法主要是在com.google.protobuf.CodedOutputStream类里面，根据不同的字段类型，有许多不同的computeXXXSize，我们以int32为例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
* Compute the number of bytes that would be needed to encode an {@code int32} field, including
* tag.
*/
public static int computeInt32Size(final int fieldNumber, final int value) {
    return computeTagSize(fieldNumber) + computeInt32SizeNoTag(value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  可以看到，int32字段所占的空间大小由tag大小（即字段顺序）与字段值大小的和来决定，首先先来看tag大小。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Compute the number of bytes that would be needed to encode a tag. */
public static int computeTagSize(final int fieldNumber) {
    return computeUInt32SizeNoTag(WireFormat.makeTag(fieldNumber, 0));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  无论是哪种字段类型，tag大小都是由同样的方式计算出的，首先是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Makes a tag value given a field number and wire type. */
static int makeTag(final int fieldNumber, final int wireType) {
    return (fieldNumber &amp;#x3C;&amp;#x3C; TAG_TYPE_BITS) | wireType;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  这里TAG_TYPE_BITS等于3，wireType等于0，因此返回值为8。这里可能会有疑问，为什么要左移3位呢？因为需要让3bit的空间给wireType，就是Protobuf的编码类型，默认为0，详细的编码列表在com.google.protobuf.WireFormat中有定义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  public static final int WIRETYPE_VARINT = 0;
  public static final int WIRETYPE_FIXED64 = 1;
  public static final int WIRETYPE_LENGTH_DELIMITED = 2;
  public static final int WIRETYPE_START_GROUP = 3;
  public static final int WIRETYPE_END_GROUP = 4;
  public static final int WIRETYPE_FIXED32 = 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  因此，int32使用的编码类型就是Varints，在后面会做进一步介绍。我们先继续看computeUInt32SizeNoTag：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Compute the number of bytes that would be needed to encode a {@code uint32} field. */
public static int computeUInt32SizeNoTag(final int value) {
    if ((value &amp;#x26; (~0 &amp;#x3C;&amp;#x3C; 7)) == 0) {// 表示value除后7bit外全为0，例如00000000 00000000 00000000 01111111
      return 1;
    }
    if ((value &amp;#x26; (~0 &amp;#x3C;&amp;#x3C; 14)) == 0) {// 表示value除后14bit外全为0，例如00000000 00000000 00111111 11111111
      return 2;
    }
    if ((value &amp;#x26; (~0 &amp;#x3C;&amp;#x3C; 21)) == 0) {// 表示value除后21bit外全为0，例如00000000 00011111 11111111 11111111
      return 3;
    }
    if ((value &amp;#x26; (~0 &amp;#x3C;&amp;#x3C; 28)) == 0) {// 表示value除后28bit外全为0，例如00001111 11111111 11111111 11111111
      return 4;
    }
    return 5;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  如上注释中所标注，该方法是根据value的值来确定tag的所占空间。当value是0-127时，占1字节，128-16383时占2字节，以此类推，最多占用5字节。而value是由fieldNumber（字段顺序）左移3位得来的，要使value小于128，则fieldNumber需要小于16。因此，&lt;strong&gt;为了使tag所占空间不超过1字节，我们定义消息时字段数量最好不要超过15个。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  tag大小弄明白之后，再来继续看字段值所占的大小computeInt32SizeNoTag：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
* Compute the number of bytes that would be needed to encode an {@code int32} field, including
* tag.
*/
public static int computeInt32SizeNoTag(final int value) {
    if (value &gt;= 0) {
      return computeUInt32SizeNoTag(value);
    } else {
      // Must sign-extend.
      return MAX_VARINT_SIZE;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  首先判断int值是否为非负数，若为非负数，则改用computeUInt32SizeNoTag来计算所占空间，否则固定占用MAX_VARINT_SIZE个字节，即10个。而computeUInt32SizeNoTag我们刚刚已经分析过了，这时可能会产生疑问，uint32为什么是按127，16383来做区间划分的呢？我们再来看writeInt32NoTag：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
public final void writeInt32NoTag(int value) throws IOException {
      if (value &gt;= 0) {
        writeUInt32NoTag(value);
      } else {
        // Must sign-extend.
        writeUInt64NoTag(value);
      }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  当写数据时，首先也是判断value的正负，若是负数则直接扩展成uint64来处理，所以前面直接确定了所占空间为10字节。而如果是非负数，则调用了writeUInt32NoTag来处理：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
public final void writeUInt32NoTag(int value) throws IOException {
      if (HAS_UNSAFE_ARRAY_OPERATIONS
          &amp;#x26;&amp;#x26; !Android.isOnAndroidDevice()
          &amp;#x26;&amp;#x26; spaceLeft() &gt;= MAX_VARINT32_SIZE) {
        if ((value &amp;#x26; ~0x7F) == 0) {
          UnsafeUtil.putByte(buffer, position++, (byte) value);
          return;
        }
        UnsafeUtil.putByte(buffer, position++, (byte) (value | 0x80));
        value &gt;&gt;&gt;= 7;
        if ((value &amp;#x26; ~0x7F) == 0) {
          UnsafeUtil.putByte(buffer, position++, (byte) value);
          return;
        }
        UnsafeUtil.putByte(buffer, position++, (byte) (value | 0x80));
        value &gt;&gt;&gt;= 7;
        if ((value &amp;#x26; ~0x7F) == 0) {
          UnsafeUtil.putByte(buffer, position++, (byte) value);
          return;
        }
        UnsafeUtil.putByte(buffer, position++, (byte) (value | 0x80));
        value &gt;&gt;&gt;= 7;
        if ((value &amp;#x26; ~0x7F) == 0) {
          UnsafeUtil.putByte(buffer, position++, (byte) value);
          return;
        }
        UnsafeUtil.putByte(buffer, position++, (byte) (value | 0x80));
        value &gt;&gt;&gt;= 7;
        UnsafeUtil.putByte(buffer, position++, (byte) value);
      } else {
        try {
          while (true) {
            if ((value &amp;#x26; ~0x7F) == 0) {
              buffer[position++] = (byte) value;
              return;
            } else {
              buffer[position++] = (byte) ((value &amp;#x26; 0x7F) | 0x80);
              value &gt;&gt;&gt;= 7;
            }
          }
        } catch (IndexOutOfBoundsException e) {
          throw new OutOfSpaceException(
              String.format(&quot;Pos: %d, limit: %d, len: %d&quot;, position, limit, 1), e);
        }
      }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  这里就比较复杂了，首先是一个巨大的if-else，如果虚拟机支持Unsafe并且至少有5个字节未分配，就走if，否则，就执行else。其实两种算法的逻辑是一样的，都是在进行byte数组的赋值，只是Unsafe的效率会高一些，如果你有兴趣，可以先自行了解一下Unsafe。&lt;/p&gt;
&lt;p&gt;  那么具体是怎样赋值的呢，这里就需要先介绍Protobuf的&lt;a href=&quot;https://developers.google.com/protocol-buffers/docs/encoding&quot;&gt;Base 128 Varints&lt;/a&gt;的概念。简单来讲，就是将每个字节的8个bit按用途进行划分，最高位的那个bit称为“most significant bit”（MSB）用来存储标志，表示本字节外是否还有额外的字节，其余低7位的bit用来存储数据。因此，每个字节只能存储7个bit的数据（非负值的取值范围为0-127），这也与前面计算占用空间时所说的数据按每7bit分为一段相符合。&lt;/p&gt;
&lt;p&gt;  那么上面的代码含义就是，首先判断数据是否不超过127（最高位是否为0），若为0，则表示本数据只有一个字节，直接强转为byte后写入即可；若不为0，则表示除本字节外，还需要有额外的字节来写数据，于是就先写入本数据的前7bit，并把MSB置为1，作为1个字节数据写入，然后再按照同样的方式（每7bit为一段）来处理后面的数据。&lt;/p&gt;
&lt;p&gt;  看到这里，Protobuf的数据压缩原理基本就清晰了，对于int32类型的数据，会根据数据值将4个字节的空间最多压缩到1个字节，再加上协议本身（tag）所占的空间，最高能压缩到2字节。因此对于int32类型的数据，最高能达到的压缩率是50%（字段为默认值的除外，其压缩率为100%）。&lt;/p&gt;
&lt;p&gt;  还有个细节需要注意，上文中有提到，对于负数值的int32类型，无论是负多少，Protobuf会将其转化为uint64来处理，将会非常的浪费空间（例如int32的数“-1”如果转化uint64，其值将会变为2^32-1=4294967295这么大的数）。那么如果业务中要处理负数怎么办呢？Protobuf提供了一种专门针对负数优化的类型sint型，查看writeSInt32源码可以看到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Write a {@code sint32} field, including tag, to the stream. */
public final void writeSInt32(final int fieldNumber, final int value) throws IOException {
    writeUInt32(fieldNumber, encodeZigZag32(value));
}

/**
* Encode a ZigZag-encoded 32-bit value. ZigZag encodes signed integers into values that can be
* efficiently encoded with varint. (Otherwise, negative values must be sign-extended to 64 bits
* to be varint encoded, thus always taking 10 bytes on the wire.)
*
* @param n A signed 32-bit integer.
* @return An unsigned 32-bit integer, stored in a signed int because Java has no explicit
*     unsigned support.
*/
public static int encodeZigZag32(final int n) {
    // Note:  the right-shift must be arithmetic
    return (n &amp;#x3C;&amp;#x3C; 1) ^ (n &gt;&gt; 31);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  Protobuf采用了zigzag编码来处理有负数的情况，例如1经过编码后变成了2，-1经过编码后变成了1，编码后全为正数，且不会出现绝对值很小的数编码后变成了很大的数，因此可以充分利用Protobuf的Base 128 Varints来进行数据压缩。而其他类型例如enum、float、double、sfixed等等都是转化成了fixed来处理，没有做数据压缩。&lt;/p&gt;
&lt;p&gt;  综上所述，对于整型的数值较小的数据，Protobuf能够很好的对其进行压缩，压缩率最高可达50%（字段为默认值的除外，其压缩率为100%）。&lt;/p&gt;
&lt;h2&gt;FlatBuffers的反序列化&lt;/h2&gt;
&lt;p&gt;  要弄清楚反序列化的过程，首先就得弄明白序列化的过程，因为反序列化是序列化的逆向操作。从Flatbuffers的使用步骤中可以看到，每个消息是用table来表示的，并且table之间是可以嵌套的，给一个table型变量赋值只需要给出该table的偏移量即可。因此，计算偏移量就成为了分析FlatBuffers源码的关键，我们首先来看计算DataMsg偏移量的方法：com.digisky.protocol.msg.fb.DataMsg类（该代码是由flatc编译器生成）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static int createDataMsg(FlatBufferBuilder builder,
                                int intData,
                                long longData,
                                float floatData,
                                int stringDataOffset) {
    builder.startObject(4);
    DataMsg.addLongData(builder, longData);
    DataMsg.addStringData(builder, stringDataOffset);
    DataMsg.addFloatData(builder, floatData);
    DataMsg.addIntData(builder, intData);
    return DataMsg.endDataMsg(builder);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  在该方法中，首先是调用builder的startObject方法来创建对象，参数“4”代表DataMsg包含的变量个数。跟进去看该方法的实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void startObject(int numfields) {
    notNested();
    // new出一个包含变量便宜值的数组
    if (vtable == null || vtable.length &amp;#x3C; numfields) vtable = new int[numfields];
    vtable_in_use = numfields;
    // 初始化数组
    Arrays.fill(vtable, 0, vtable_in_use, 0);
    nested = true;
    // 获得本对象的起始便宜值
    object_start = offset();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  该方法并没有做一些实际的事情，只是做一些初始化的工作。接下来再看给各变量赋值的addXXX方法，我们以addIntData为例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void addIntData(FlatBufferBuilder builder, int intData) {
    builder.addInt(0, intData, 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  可以看到直接调用了builder的addInt方法，第一个参数0表示字段的顺序，0代表第一个字段，第三个参数0表示该字段的默认值，表示默认为0，再继续跟进：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * Add an `int` to a table at `o` into its vtable, with value `x` and default `d`.
 *
 * @param o The index into the vtable.
 * @param x An `int` to put into the buffer, depending on how defaults are handled. If
 *          `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the
 *          default value, it can be skipped.
 * @param d An `int` default value to compare against when `force_defaults` is `false`.
 */
public void addInt(int o, int x, int d) {
    if (force_defaults || x != d) {
        addInt(x);
        slot(o);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  首先是做了一个判断，是否要强制写进默认值，如果不强制，该字段又是默认值，那么就直接返回了，这也是Flatbuffers唯一的数据压缩方案，如果该字段是默认值，就不序列化该字段。如果不是默认值，就调用addInt和slot方法。首先是addInt方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * Add an `int` to the buffer, properly aligned, and grows the buffer (if necessary).
 *
 * @param x An `int` to put into the buffer.
 */
public void addInt(int x) {
    prep(Constants.SIZEOF_INT, 0);
    putInt(x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  AddInt方法中包含了prep与PutInt：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Prepare to write an element of `size` after `additional_bytes`
 * have been written, e.g. if you write a string, you need to align such
 * the int length field is aligned to {@link com.google.flatbuffers.Constants#SIZEOF_INT}, and
 * the string data follows it directly.  If all you need to do is alignment, `additional_bytes`
 * will be 0.
 *
 * @param size This is the of the new element to write.
 * @param additional_bytes The padding size.
 */
public void prep(int size, int additional_bytes) {
    // Track the biggest thing we&apos;ve ever aligned to.
    if (size &gt; minalign) minalign = size;
    // Find the amount of alignment needed such that `size` is properly
    // aligned after `additional_bytes`
    int align_size = ((~(bb.capacity() - space + additional_bytes)) + 1) &amp;#x26; (size - 1);
    // Reallocate the buffer if needed.
    while (space &amp;#x3C; align_size + size + additional_bytes) {
        int old_buf_size = bb.capacity();
        ByteBuffer old = bb;
        bb = growByteBuffer(old, bb_factory);
        if (old != bb) {
            bb_factory.releaseByteBuffer(old);
        }
        space += bb.capacity() - old_buf_size;
    }
    pad(align_size);
}

/**
 * Add an `int` to the buffer, backwards from the current location. Doesn&apos;t align nor
 * check for space.
 *
 * @param x An `int` to put into the buffer.
 */
public void putInt(int x) {
    bb.putInt(space -= Constants.SIZEOF_INT, x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  prep主要是进行一些写入前的准备，确认缓冲区是否足够写入，以及检查数据对齐等等，而putInt就是真正的将数据写入缓冲区。从这里可以看到，Flatbuffers在写入数据时，是将原数据直接写入，并没有做压缩，再来看slot方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;
/**
 * Set the current vtable at `voffset` to the current location in the buffer.
 *
 * @param voffset The index into the vtable to store the offset relative to the end of the
 * buffer.
 */
public void slot(int voffset) {
    vtable[voffset] = offset();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  将数据写入后，根据字段顺序计算出了该字段的偏移量，并保存到了vtable中。至此，各字段数据就写入完毕。再通过计算最终的偏移量，就可以获得整个消息的偏移量，序列化的工作就完成了。&lt;/p&gt;
&lt;p&gt;  接下来就是反序列化了，还记得是如何反序列化FlatBuffers数据的吗：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ByteBuffer buffer = ByteBuffer.wrap(serializedData);
Msg msg = Msg.getRootAsMsg(buffer);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  首先是将byte数组包装成NIO的ByteBuffer，然后调用getRootAsMsg即可获得Msg对象。那Msg对象是如何构建的呢，在构建过程中是否进行了字段解析呢？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static Msg getRootAsMsg(ByteBuffer _bb) {
    return getRootAsMsg(_bb, new Msg());
}

public static Msg getRootAsMsg(ByteBuffer _bb, Msg obj) {
    _bb.order(ByteOrder.LITTLE_ENDIAN);
    return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb));
}

public Msg __assign(int _i, ByteBuffer _bb) {
    __init(_i, _bb);
    return this;
}

public void __init(int _i, ByteBuffer _bb) {
    bb_pos = _i;
    bb = _bb;
    vtable_start = bb_pos - bb.getInt(bb_pos);
    vtable_size = bb.getShort(vtable_start);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  通过上述几个方法深追可以看到，首先将ByteBuffer设置成了小端模式，然后调用__assign方法将ByteBuffer关联到Msg对象，并将其初始化。整个过程中都只是简单的赋值操作，没有进行内存拷贝、数据解码等耗时操作，那么消息字段又怎样解析呢，我们以intData为例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public int intData() {
    int o = __offset(4);
    return o != 0 ? bb.getInt(o + bb_pos) : 0;
}

/**
* Look up a field in the vtable.
*
* @param vtable_offset An `int` offset to the vtable in the Table&apos;s ByteBuffer.
* @return Returns an offset into the object, or `0` if the field is not present.
*/
protected int __offset(int vtable_offset) {
    return vtable_offset &amp;#x3C; vtable_size ? bb.getShort(vtable_start + vtable_offset) : 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  要获取某字段时，先通过字段顺序“4”获取该字段的偏移量，然后再通过该偏移量即可在ByteBuffer中直接获取到字段值，非常的简洁高效。&lt;/p&gt;
&lt;p&gt;  综上所述，FlatBuffers在序列化时计算了各字段在数据体的偏移量，并将偏移量存储在了数据体中。反序列化时，首先读取字段的偏移量，然后根据偏移量读取数据即可。因为反序列化过程没有内存拷贝、数据解码等耗时操作，所以速度非常快。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;FlatBuffers序列化后不需要转换/解包的操作就可以获得原数据，反序列化消耗的时间极短，短到远小于1ms而可以忽略；基本没有对数据进行压缩，因为有偏移量的关系，数据量比原数据有所增加；生成的代码量较少，运行比较轻量，CPU占用较低，内存占用较少。&lt;/li&gt;
&lt;li&gt;Protobuf采用&quot;Base 128 Varints&quot;算法对整型数据进行压缩，压缩比例最高能达到50%（字段为默认值的除外，其压缩率为100%）；序列化与反序列化都比较重度，生成的代码量较大，CPU占用较高，内存占用较多。&lt;/li&gt;
&lt;li&gt;Protobuf 3.x中移除了required和optional字段描述，相当于除repeated以外的所有字段都是optional，提高了Protobuf的消息兼容性。&lt;/li&gt;
&lt;li&gt;Protobuf使用技巧：
&lt;ul&gt;
&lt;li&gt;为了使数据压缩率更高，每个消息的字段数量最好不要超过15个。&lt;/li&gt;
&lt;li&gt;尽量不要使用int32与int64，如果是正数使用uint，如果是负数则使用sint。&lt;/li&gt;
&lt;li&gt;如果业务中能够控制int32或int64型数据的取值范围，尽量控制在0-127。&lt;/li&gt;
&lt;li&gt;通过以上技巧都能够提高Protobuf的数据压缩能力。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Flatbuffers使用技巧：
&lt;ul&gt;
&lt;li&gt;uint类型只是为了扩大int的取值范围（兼容c/c++与c#等有unsigned int类型的语言），而如果是java等没有unsigned int等类型的语言，会在赋值与取值时扩展为long来处理，所以若非有实际需要，尽量不要使用uint。&lt;/li&gt;
&lt;li&gt;如果业务中能够控制bool、int8、int16、int32、int64的取值是0与非0的概率，尽量让取值为0的情况多一些，可以使Flatbuffers具备一定的压缩能力。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;若项目需求对数据处理延时有严苛的要求（例如FPS、Moba、动作RPG等），可以考虑使用Flatbuffers，并配合UDP/KCP等传输层协议，能够比传统的TCP+Protobuf方案有更好的降低延时的效果。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>射击游戏中准心与子弹弹道的探索</title><link>https://juzzi.qzz.io/blog/devel/game-engine/unreal-shooting-bullet</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/devel/game-engine/unreal-shooting-bullet</guid><description>现如今，精品游戏竞争激烈，各大游戏厂商都在争相推出“3A”级品质的游戏。而目前比较热门的科幻、战争、动作等品类大多都有枪械射击内容，因此，“玩枪”便成了很大一部分游戏中常见的玩法。</description><pubDate>Thu, 31 Oct 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;  当前比较热门的使命召唤系列、战地系列、无主之地系列、战争机器系列还有最近流行的吃鸡类游戏（绝地求生、堡垒之夜）等等都是玩枪，核心玩法类型上都是属于射击类游戏。因此做好武器与射击体验，还原逼真的射击情景，便成了这类游戏的核心卖点。&lt;/p&gt;
&lt;h2&gt;射击游戏的分类&lt;/h2&gt;
&lt;p&gt;  目前的射击游戏主要分为两种，一种为“第一人称射击游戏”（FPS）与“第三人称射击游戏”（TPS）。&lt;/p&gt;
&lt;h3&gt;第一人称&lt;/h3&gt;
&lt;p&gt;  第一人称射击游戏是以玩家主视角进行的射击游戏。玩家不再像别的游戏类型一样操纵屏幕中的虚拟人物来进行游戏，而是身临其境的主视角，体验游戏带来的视觉冲击，这就大大增强了游戏的主动性和真实感。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/3.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  如上两张图分别是1999年发售的反恐精英（ Counter-Strike ）与2019年发售的使命召唤：现代战争 （ Call of Duty: Modern Warfare ）。可以看到，时隔20年，场景与枪械变得更加精细与逼真，界面变得精美与合理，但不变的是，屏幕中间都做有一个“准心”。&lt;/p&gt;
&lt;h3&gt;第三人称&lt;/h3&gt;
&lt;p&gt;  第三人称射击游戏与第一人称区别在于，屏幕上显示的主角的视野不同，并且第三人称中玩家控制的游戏人物在游戏屏幕上是可见的，因而第三人称射击游戏加强调更强调动作感。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/4.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/5.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/6.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  如上3张图都是“幽灵行动：断点”的游戏画面，可以看到，在没有瞄准时屏幕中央是没有准心的，这时可以将注意力集中在环境、角色动作、游戏剧情等等。当打开瞄准时，就会出现屏幕中央的准心，还可以通过Alt键切换第一人称与第三人称（第二张图与第三张图）。&lt;/p&gt;
&lt;h2&gt;准心&lt;/h2&gt;
&lt;p&gt;  通过上述的介绍可以看到，无论是第一人称还是第三人称，屏幕中央都会有“准心”以方便玩家瞄准。那为什么要有准心呢？为什么有了准心之后，我们在游戏中就可以瞄准目标呢？&lt;/p&gt;
&lt;h3&gt;现实中的射击&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/6.5.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  我们在物理课程中了解过，如果抛去枪的复杂结构不谈，枪的工作原理简单来讲就是，弹头在枪管中受到推进力后做抛物线运动。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/7.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  如果弹头在运动过程中碰撞到目标，则表示击中；若未碰撞到目标，则表示未击中。那么我们如何瞄准才能击中目标呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/8.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  如图所示，蓝色的水平线则是瞄准线，代表瞄准方向；绿色是枪管轴心线，代表枪管的指向方向；红色则是子弹的飞行轨迹。假定子弹每次从枪口中射出的速度是固定的，空气阻力也是固定的，子弹的重力也是固定的，那么按照上图的瞄准方式，射击命中的“远交点”也是固定的。换句话说，用这把枪和这个瞄准角度的话，只能击中距离枪口x米的目标。那么如果我们射击同一方向上比x米更近或更远的目标怎么办呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/9.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/10.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  如上图所示，分别为步枪和狙击枪的射击距离调整方法。步枪中有瞄准距离刻度线，调节刻度即可；狙击枪在瞄准高倍镜中有刻度线，根据目标的距离，使用对应的瞄准刻度线即可；手枪因为射击距离短（大部分射击距离在50米以内），瞄准误差较小，所以不用调节。&lt;/p&gt;
&lt;h3&gt;游戏中的射击&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/11.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  而在游戏中，未开瞄准镜的情形下，角色的持枪动作往往如上图所示，将枪固定于肘关节处。为什么要这样持枪呢？因为这样持枪姿势可以减小在第一人称人下枪所遮住的视野，提升游戏体验，而这样的持枪动作，在现实中是打不准目标的（因为没有三点一线的瞄准）。在游戏里为了能更加便捷开枪，因此加入了准心的概念。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/12.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  有了准心之后，玩家就可以更加方便地瞄准目标，从而获得更好的射击体验。但这种准心“指哪打哪”的游戏效果是如何实现的呢？&lt;/p&gt;
&lt;h2&gt;UE4中的子弹弹道&lt;/h2&gt;
&lt;p&gt;  我刚开始在UE4中实现子弹弹道时，想法很简答，既然子弹是从枪中射出的，那么子弹的飞行方向当然就是枪口的朝向。为了使子弹能够击中“准心”的位置，我调整了枪在手中的朝向。&lt;/p&gt;
&lt;h3&gt;问题&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/13.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  如上图所示，假定玩家（摄像机）与枪处在同一水平面，从俯视的角度来看，玩家的准心永远指向正前方（用红色线条表示），而枪则在玩家左手或右手，其朝向（用绿色线条表示）与玩家朝向存在夹角Θ。在该夹角下，只有玩家与目标距离y时，弹着点才能刚好在准心上。无论怎么调整夹角Θ，都只有某一个距离下弹着点刚好落在准心上（因为两条不平行的直线永远只有一个交点）。而实际上枪与玩家（摄像头）并不在同一水平面，在三维坐标系下，两条不平行的直线最多有一个交点（可能没有交点）。&lt;/p&gt;
&lt;p&gt;  那么该如何解决该问题呢？既然两条线只有一个交点，那么能不能根据不同的射击距离改变枪的朝向，使交点始终在准心瞄准位置呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/18.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  要想调整枪的朝向，就必须调整枪在玩家动画的骨骼中的相对位置。而且要根据射击距离实时调整（玩家准心瞄到的障碍物的距离），这个运算量会非常大，在现阶段是不可能实现的。而且，角色在“静止状态”时，会随着呼吸而晃动枪的朝向，会对计算结果产生偏差，所以实时调整枪的朝向这条路是走不通的，那该怎么办呢？&lt;/p&gt;
&lt;h3&gt;解决方案&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/14.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  既然枪的方向不方便调整，子弹的方向是可以控制的。可以在开枪时，先根据玩家（摄像机）的朝向，做射线检测，确定目标弹着点，然后再控制子弹的飞行方向为枪口飞向弹着点，这不就可以实现无论玩家瞄向哪里，子弹都可以击中“准心”的位置的需求吗。&lt;/p&gt;
&lt;h3&gt;计算弹着点&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/15.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/16.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  如上蓝图代码所示，用摄像机方向（即准心的朝向）做射线检测，返回击中的弹着点坐标与材质（以便后续播放击中特效），若未击中任何物体（例如朝天上开枪），则返回射线检测的终点，以便客户端展示弹道。&lt;/p&gt;
&lt;h3&gt;广播开枪&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/17.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  如上蓝图代码所示，获得弹着点坐标与材质后进行广播，在所有客户端上播放该玩家的开枪动画（武器特效），并调用C++函数，利用GamePlayTask生成弹道。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;void AWeapon::GenerateBulletTrack(FVector HitLocation, UPhysicalMaterial* HitMaterial)
{
    // 枪口坐标
    FVector MuzzleLocation = Mesh-&gt;GetSocketLocation(&quot;Muzzle&quot;);
    // 向量方向：终点 - 起点
    FVector Direction = HitLocation - MuzzleLocation;
    // 飞行速度
    FVector Speed = Direction.Rotation().Vector() * 10000;
    // 飞行时间：距离 / 速度
    float Time = Direction.Size() / Speed.Size();
    // 击中点特效
    UParticleSystem* HitFX = nullptr;
    // 击中点材质
    if (HitMaterial)
    {
        EPhysicalSurface SurfaceType = HitMaterial-&gt;SurfaceType;
        // 该枪已设置该材质类型的击中特效
        if (VarHitFXs.Contains(SurfaceType))
        {
            HitFX = VarHitFXs[SurfaceType];
        }
    }
    // 运行子弹轨迹的GamePlayTask
    UBulletTrackTask * TrackTask = UBulletTrackTask::InitBulletTrack(BulletTrackComponent, MuzzleLocation, Speed, TargetFX, HitFX, Time, this, HitLocation);
    TrackTask-&gt;ReadyForActivation();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  如上C++代码所示，根据击中点坐标，计算出子弹的飞行方向与距离，然后计算出飞行时间，并用这些参数开启子弹轨迹的GamePlayTask，用于显示客户端的子弹轨迹及击中特效。&lt;/p&gt;
&lt;h3&gt;子弹弹道&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;void UBulletTrackTask::TickTask(float DeltaTime)
{
    // 记录飞行时间
    ActiveTime += DeltaTime;
    UWorld* World = GetWorld();
    check(World);
    const FVector OldLocation = CurrentLocation;
    // 匀速距离公式：距离 = 速度 * 时间
    FVector MoveDistance = CurrentVelocity * DeltaTime;
    // 新位置
    CurrentLocation = OldLocation + MoveDistance;
    // 更新轨迹特效的位置与朝向
    if (TrackComponent)
    {
         TrackComponent-&gt;SetWorldLocationAndRotation(CurrentLocation, UKismetMathLibrary::MakeRotFromX(CurrentVelocity.GetSafeNormal()));
    }
    // 超过飞行时间
    if (ActiveTime &gt;= LifeTime)
    {
        // 有击中特效
        if (HitFX)
        {
            // 显示击中特效
            UGameplayStatics::SpawnEmitterAtLocation(World, HitFX, HitLocation);
        }
        EndTask();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  通过运行该子弹轨迹的GamePlayTask，每帧会更新子弹轨迹特效的位置，显示子弹的飞行过程，如果超过了子弹的飞行时间，则在服务器已计算出的击中点产生击中特效（受伤害特效）等等，实现了子弹总是能击中游戏准心瞄准的目标。&lt;/p&gt;
&lt;p&gt;  上述解决方案是弹道实现的较简化版本，因为没有考虑子弹的飞行时间、子弹重力、枪械后坐力、空气阻力等诸多因素。如果考虑这些因素的话，就不适合直接在服务器用射线检测来计算弹着点，而是同样改用GamePlayTask来实现，以便能够精确计算子弹飞行过程中的受力、飞行碰撞等问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/19.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  如果是单机游戏则不会有影响，因为客户端本地计算，不会有延迟问题，但如果是多人在线游戏，这样做会增加服务器弹着点计算耗时（因为会增加子弹飞行时间），从而加大客户端的表现困难（让玩家感受到开枪有延时），本方案的缺点就显露了出来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/10/shoot-game-bullet-track/20.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  那么，可行的优化思路是怎样的呢？可以先在服务器预计算出（或者客户端根据当前摄像机朝向自行计算）弹着点，根据预计算的弹着点作为子弹飞行方向，然后在客户端与服务器中同步运行子弹飞行任务，根据子弹运行过程中的碰撞点作为实际弹着点，最后服务器再同步碰撞结果。因此，处理好客户端与服务器的弹道同步，就成为了后续工作的关键。&lt;/p&gt;
&lt;p&gt;  以上就是针对游戏开发中子弹弹道实现的探索，若有分析得不妥之处，还请共同探讨。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>UE4独立服务器构建</title><link>https://juzzi.qzz.io/blog/devel/game-engine/unreal-dedicated-server</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/devel/game-engine/unreal-dedicated-server</guid><description>UE4引擎在架构时就已经考虑到了多人游戏的情景，多人游戏基于客户端-服务器模式（CS模式）。</description><pubDate>Sun, 29 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;  根据UE4&lt;a href=&quot;https://docs.unrealengine.com/zh-CN/Gameplay/Networking/Overview/index.html&quot;&gt;官方文档&lt;/a&gt;的介绍，会有一个服务器担当游戏状态的主控者，而连接的客户端将保持近似的副本。服务器是UE4多人游戏的一个重要部分，其作用是：做出所有重要决定，包含所有的主控状态、处理客户端连接、转移到新的地图以及处理比赛开始/结束时的总体游戏流程。&lt;/p&gt;
&lt;p&gt;  服务器的功能开发在UE4中也非常简单，UE4本身是支持客户端/服务器逻辑混合开发的，在蓝图中创建好自定义事件（Custom Event）之后，只需要选择该事件在服务器上运行（Run on Server）即可。而在C++中，也只需要用宏UFunction(Server)来修饰某个函数，然后就可以在客户端调用服务器RPC。本文在此不再具体赘述服务器游戏功能的实现，而是着重介绍如何构建出独立的UE4服务器应用。&lt;/p&gt;
&lt;h2&gt;服务器构建&lt;/h2&gt;
&lt;p&gt;  在UE4编辑器中，我们只需要在播放（Play）选项中勾选运行独立服务器（Run Dedicated Server）即可在编辑器中运行独立服务器，并且客户端会自动连接到该服务器。那么如何构建出能独立运行的服务器呢？官网给出了一篇&lt;a href=&quot;https://wiki.unrealengine.com/index.php?title=Dedicated_Server_Guide_(Windows_%26_Linux)&quot;&gt;指南&lt;/a&gt;，于是我根据这篇指南和我自己的理解开始了UE4服务器的构建。&lt;/p&gt;
&lt;h3&gt;编译虚幻引擎源码&lt;/h3&gt;
&lt;p&gt;  在这第一步我就产生了疑问，我们最初从官网下载UE4启动器后，安装UE4引擎时，不就已经下载了虚幻引擎源码了吗，还有必要重新下载吗？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/09/ue4-dedicated-server-build/1.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  于是我就尝试跳过这一步，直接从“添加构建服务器目标”开始操作，当选择构建目标为&quot;Development Server&quot;后，得到了如下的报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UnrealBuildTool : error : Couldn&apos;t find target rules file for target &apos;UE4Server&apos; in rules assembly &apos;UE4Rules, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null&apos;.
Location: C:\Program Files\Epic Games\UE_4.22\Engine\Intermediate\Build\BuildRules\UE4Rules.dll
Target rules found:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  该问题我在&lt;a href=&quot;https://www.reddit.com/r/unrealengine/comments/ae55r0/problem_building_development_server_couldnt_find/&quot;&gt;Reddit&lt;/a&gt;（国外知名论坛）上找到了详细的阐述，原因就是因为没有按照指南的步骤下载与编译UE4源码。至于为何启动器中下载的UE4引擎源码会出现找不到构建目标“UE4Server”的问题，我推测是因为其源码（构建）不完整，精简了编辑器以外的其他内容（例如独立服务器模块等等），所以，还是老老实实下载源码吧。&lt;/p&gt;
&lt;h3&gt;下载UE4源码&lt;/h3&gt;
&lt;p&gt;  UE4源码存放在Github仓库，并且是私有仓库（private reposity）。如果你想获得访问权限，需要加入Epic Games开发组。在Epic官网登陆帐号后，关联Github帐号，就会收到Epic Games开发组的邀请，接受后即可加入，&lt;a href=&quot;https://www.unrealengine.com/zh-CN/ue4-on-github?lang=zh-CN&quot;&gt;官网&lt;/a&gt;做了详细的操作教程。&lt;/p&gt;
&lt;p&gt;  我下载的版本是4.22.3，大小是333M。&lt;/p&gt;
&lt;h3&gt;安装源码&lt;/h3&gt;
&lt;p&gt;  解压上一步下载的源码压缩包后，双击Setup.bat文件即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/09/ue4-dedicated-server-build/2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  可以看到，该一步是在检查和更新UE4引擎的依赖组件，需要更新9122M的文件（约9G），国内访问速度特别慢，我家100M电信宽带，下载速度只有0.1M/S（100KB/S），连上香港的VPN也是差不多的速度（因为VPN限速1M），按照这个下载速度，下载完大约需要&lt;strong&gt;25个小时&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;  25个小时有点太长了，查看了一下Setup.bat这个文件，它调用了“.\Engine\Binaries\DotNET\GitDependencies.exe”来执行下载，查看该进程连接的远程地址为“13.225.102.98:80”，这个ip地址在美国，所以访问缓慢。我找了一下源码目录，希望可以找到配置类似下载镜像的地方，但最终并没有找到。（绝望~）&lt;/p&gt;
&lt;p&gt;  难道真的要为此默默等待25小时吗？好在生活中处处有惊喜，第二天我试了一下在公司网络进行下载，速度意外地飚到了1.5M/S，只需要等待大约1个半小时，非常开心。下载完毕后，该窗口会自动消失，继续进行下一步即可。&lt;/p&gt;
&lt;h3&gt;生成Visual Studio项目文件&lt;/h3&gt;
&lt;p&gt;  同样在解压后的路径，双击GenerateProjectFiles.bat，会得到如下的报错：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/09/ue4-dedicated-server-build/2.5.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  根据提示来看，是因为需要安装.net框架的SDK。打开微软.NET&lt;a href=&quot;https://dotnet.microsoft.com/download&quot;&gt;官网&lt;/a&gt;，下载.NET Framework Dev Pack（注意不是Runtime）。&lt;/p&gt;
&lt;p&gt;  （Warning！）此处有巨坑。根据上面的提示，是需要安装4.6.2版本的.NET Framework Dev Pack，如果你跟我一样直接下载了目前最新的版本（4.8），同样会有该错误提示，因为这个版本要求已经写在了如下的文件中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Engine\Source\Programs\UnrealBuildTool\UnrealBuildTool.csproj
Engine\Source\Programs\DotNETCommon\DotNETUtilities\DotNETUtilities.csproj
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  “机智”的我立刻找到了文件中的TargetFrameworkVersion字段，将默认的v4.6.2修改成了我安装的版本v4.8，再次运行GenerateProjectFiles.bat后，果然，没有报错，并且成功生成了UE4.sln。其实，如上的步骤并不是指跳过了这个巨坑，因为，这只是噩梦的开始……&lt;/p&gt;
&lt;p&gt;  继续进行下面的编译源码步骤后（耗时约8小时），会得到如下的报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error MSB3075: The command &quot;..\..\Build\BatchFiles\Build.bat exited with code 5.
Please verify that you have sufficient rights to run this command.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  根据报错提示，是&quot;....\Build\BatchFiles\Build.bat&quot;异常退出，因为没有权限。我推测可能是由于文件占用导致的，先是重启了VS再编译，没有效果。然后又重启了电脑，仍然没有效果。然后又尝试了“使用管理员权限”重新编译，还是没有变化。百度和Google上的解决方法也是五花八门，各种情况都有，无法解决。&lt;/p&gt;
&lt;p&gt;  最后正在我绝望的时候，突然想到了.NET版本的问题，卸载.NET Dev Pack 4.8然后重新安装.NET Dev Pack 4.6.2之后。终于，报错消失了，所以，这里只能使用.NET Dev Pack 4.6.2。&lt;/p&gt;
&lt;p&gt;  这个大坑主要是因为.NET Dev Pack版本安装错误后，没有明确的提示，不太容易联想到是.NET Dev Pack版本的问题，而且，编译一次UE4源码真的太太太太太耗时间了。&lt;/p&gt;
&lt;h3&gt;编译UE4源码&lt;/h3&gt;
&lt;p&gt;  双击UE4.sln在Visual Studio中打开UE4源码，在左侧解决方案浏览器（Solution Explorer）中，在“UE4”项目点击右键，然后点击Build。考验电脑性能的时候到了，我在公司的i3处理器的电脑上编译了大概8个小时，并且在编译期间，CPU占用率一直100%，几乎干不了其他事，所以建议在下班的时候进行，第二天早上一上班就可以看到，编译已经完成。&lt;/p&gt;
&lt;h3&gt;为项目添加构建服务器目标&lt;/h3&gt;
&lt;p&gt;  由于官网的指南是针对引擎版本为“4.14, 4.15, 4.16, 4.17, 4.18”编写的，而我使用的UE4引擎版本为4.22.3，因此使用的是指南中针对4.18版本的内容，经过测试，是可以使用的。&lt;/p&gt;
&lt;p&gt;  进入项目的&quot;~/Source&quot;路径，新建一个文件命名为“项目名Server.Target.cs”，将项目名替换为实际项目名，例如我的项目名为“Shooting_Game”，则文件名命名为“Shooting_GameServer.Target.cs”。（如果你的项目没有Source路径，则说明是纯蓝图的UE4项目，则在编辑器中添加任意一个C++类即可。）&lt;/p&gt;
&lt;p&gt;  编辑新建的文件，加入如下内容，注意替换项目名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;
using System.Collections.Generic;

[SupportedPlatforms(UnrealPlatformClass.Server)]
public class Shooting_GameServerTarget : TargetRules   // 修改项目名
{
    public Shooting_GameServerTarget(TargetInfo Target) : base(Target)  // 修改项目名
    {
        Type = TargetType.Server;
        ExtraModuleNames.Add(&quot;Shooting_Game&quot;);    // 修改项目名
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;切换项目的UE4引擎版本&lt;/h3&gt;
&lt;p&gt;  右键项目根目录的“项目名.uproject”文件，选择&quot;Switch Unreal Engine version...&quot;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/09/ue4-dedicated-server-build/3.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  选择刚刚安装的UE4引擎版本，点击OK即可，切换引擎后，会自动生成项目文件（Project files）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/09/ue4-dedicated-server-build/4.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;开始构建&lt;/h3&gt;
&lt;p&gt;  在Visual Studio的解决方案配置（Solution Configuration）中选择&quot;Development Server&quot;，然后在解决方案浏览器（Solution Explorer）中，在&quot;Shooting_Game&quot;（我的项目名称）点击右键，点击Build。这里只有481个构建步骤，比编译UE4源码要少 一些，大约需要2小时。&lt;/p&gt;
&lt;p&gt;  构建完成之后，可以看到输出日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[480/481] Shooting_GameServer.exe
Creating library F:\workspace\ue4\Shooting_Game2\Binaries\Win64\Shooting_GameServer.lib and object F:\workspace\ue4\Shooting_Game2\Binaries\Win64\Shooting_GameServer.exp
[481/481] Shooting_GameServer.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  已经生成了Shooting_GameServer.exe，路径在“~\Binaries\Win64\”，如果我们尝试直接运行（需要添加运行参数-log，否则进程会在后台运行，什么效果也看不到），会看到如下报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LogLinker: Warning: Unable to load package(...). Package contains EditorOnly data which is not supported by the current build.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  意思是包含了仅支持编辑器内容的数据，“~\Binaries\Win64\”是编辑器构建文件夹，所以我们还需要进行客户端打包，然后再将服务器程序拷贝过去。&lt;/p&gt;
&lt;h2&gt;客户端打包&lt;/h2&gt;
&lt;p&gt;  客户端打包需要在编辑器中进行，所以需要在解决方案配置（Solution Configuration）中选择&quot;Development Editor&quot;，启动项目（Startup Projects）中选择“Shooting_Game”，然后点击运行按钮。第一次运行编辑器会编译Shader，所以耗时较久（我大约需要20分钟），并且会长时间卡在45%，等待即可。&lt;/p&gt;
&lt;h3&gt;创建入口关卡&lt;/h3&gt;
&lt;p&gt;  在左下方内容浏览器（Content Browser）中选择Content目录，选择存放关卡的文件夹（我的是Levels），右边窗口右键创建关卡（Level），命名为EntryMap。&lt;/p&gt;
&lt;h3&gt;在入口关卡中连接服务器&lt;/h3&gt;
&lt;p&gt;  双击选择EntryMap，然后在上方工具栏选择蓝图（BluePrints），打开关卡蓝图（Open Level BluePrint）。引出BeginPlay引脚，选择OpenLevel，在LevelName中填写服务器IP地址，保存编译。&lt;/p&gt;
&lt;h3&gt;创建过渡关卡&lt;/h3&gt;
&lt;p&gt;  用同样的方式创建关卡，命名为TransitionMap，无需给该关卡添加内容，保持空白即可。&lt;/p&gt;
&lt;h3&gt;指定游戏地图&lt;/h3&gt;
&lt;p&gt;  菜单栏选择编辑（Edit）- 项目设置（Project Settings）- 项目（Project）- 地图和模式（Maps &amp;#x26; Modes）- 默认地图（Default Maps），依次将编辑器初始地图（Editor Startup Map）设置为GameMap，游戏默认地图（Game Default Map）设置为EntryMap，过渡地图（Transition Map）设置为TransitionMap，服务器默认地图（Server Default Map）设置为GameMap。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/09/ue4-dedicated-server-build/5.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;打包地图设置&lt;/h3&gt;
&lt;p&gt;  菜单栏选择编辑（Edit）- 项目设置（Project Settings）- 项目（Project）- 打包（Packaging）- 打包地图列表（LIst of maps to include in a packaged build），分别添加EntryMap，TransitionMap，GameMap。&lt;/p&gt;
&lt;h3&gt;开始打包&lt;/h3&gt;
&lt;p&gt;  菜单栏选择文件（File）- 打包项目（Package Project）- 窗口（Windows）- 64位窗口（Windows(64 bit)），然后选择打包目录，即可开始打包。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UATHelper: Packaging (Windows (64-bit)): BUILD SUCCESSFUL
UATHelper: Packaging (Windows (64-bit)): AutomationTool exiting with ExitCode=0 (Success)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  根据提示，可以看到，打包成功。&lt;/p&gt;
&lt;h2&gt;运行服务器&lt;/h2&gt;
&lt;p&gt;  之前已经提到，构建出的服务器目标文件需要在客户端资源目录中运行，因此需要拷贝过去。&lt;/p&gt;
&lt;h3&gt;拷贝服务器目标文件&lt;/h3&gt;
&lt;p&gt;  将之前构建出的“~\Binaries\Win64\Shooting_GameServer.exe”拷贝到客户端的打包目录&quot;WindowsNoEditor\Shooting_Game\Binaries\Win64&quot;。&lt;/p&gt;
&lt;h3&gt;在命令行中运行服务器&lt;/h3&gt;
&lt;p&gt;  由于服务器没有图形界面（GUI），因此需要在命令行中运行，否则什么输出都看不到。给拷贝过来的Shooting_GameServer.exe创建快捷方式，并在“目标”后加入“ -log”。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/09/ue4-dedicated-server-build/6.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  双击运行该快捷方式，可以看到一下日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LogInit: WinSock: I am PC-20190329KLKV (192.168.52.160:0)
LogNet: GameNetDriver IpNetDriver_0 IpNetDriver listening on port 7777
LogWorld: Bringing World /Game/Levels/GameMap.GameMap up for play (max tick rate 30) at 2019.10.09-11.20.56
LogWorld: Bringing up level for play took: 0.002008
LogLoad: Took 0.129134 seconds to LoadMap(/Game/Levels/GameMap)
LogLoad: (Engine Initialization) Total time: 1.03 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  可以看到，服务器运行在192.168.52.160，监听端口为7777。打开cmd命令行，输入netstat -an|FIND &quot;7777&quot;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UDP    0.0.0.0:7777           *:*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  由此可见，UE4服务器与客户端连接使用的是UDP协议，默认端口为7777。&lt;/p&gt;
&lt;p&gt;  至此，UE4项目的服务器构建完成，可以使用打出的客户端包来进行连接测试。&lt;/p&gt;
&lt;h2&gt;效果演示&lt;/h2&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>UE4蓝图与C++交互——射击游戏中多武器系统的实现</title><link>https://juzzi.qzz.io/blog/devel/game-engine/unreal-multiple-weapon</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/devel/game-engine/unreal-multiple-weapon</guid><description>学习UE4已有近2周的时间，跟着数天学院“UE4游戏开发”课程的学习，已经完成了UE4蓝图方面比较基础性的学习。</description><pubDate>Fri, 30 Aug 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;回顾&lt;/h2&gt;
&lt;p&gt;  通过UE4蓝图的开发，我实现了类似CS的单人版射击游戏，效果如下视频：&lt;/p&gt;
&lt;p&gt;  不得不说UE4蓝图功能的强大，无需写一句代码，就能实现一个基本的游戏玩法。并且使用门槛极低，只要熟悉蓝图的API，通过“拖拖，连连”就能完成游戏玩法的开发，对游戏策划（设计师）及其友好，与C++相比，生产效率极高。&lt;/p&gt;
&lt;h2&gt;多武器系统&lt;/h2&gt;
&lt;p&gt;  目前的游戏设定是开场后，角色身上就自动装备了一把武器，为了实现类似于CS雪地地图一样的设定：开场没有武器，地图中摆放着多把武器，需要从地上拾取后才能使用，就需要将当前的武器系统进行扩展，创建多个蓝图来实现不同武器的模型、特效、属性等等。&lt;/p&gt;
&lt;h3&gt;武器玩法逻辑的处理&lt;/h3&gt;
&lt;p&gt;  之前只有单个武器时，武器蓝图包含有玩法逻辑例如弹道计算、粒子显示、开枪动画、伤害控制等等，这些玩法（GamePlay）的逻辑是写在武器蓝图里面的。如果按照以前的设计，创建多个武器蓝图时，这些武器中的玩法逻辑也需要拷贝多份。&lt;/p&gt;
&lt;h4&gt;拷贝&lt;/h4&gt;
&lt;p&gt;  然而，“拷贝”这种开发方式在程序编码中是非常不推荐的。所谓拷贝，意味着需要维护多份同样逻辑实现的代码，如果后续需要对该部分玩法进行调整 优化~~（策划又要改需求）~~，那么拷贝了多少次，就需要修改多少次。例如，项目后期有50把武器（参考穿越火线），如果在前期为了省事将武器的玩法逻辑拷贝了49次，那么如果某天有该逻辑的修改需求时，就需要将该修改操作重复50次，这是让人崩溃的一件事。&lt;strong&gt;因此，在软件开发中，不要轻易使用Ctrl-C + Ctrl-V。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;引用&lt;/h4&gt;
&lt;p&gt;  那么该如何解决该问题呢？软件开发中常用的方法是将拷贝转变为引用。所谓引用，意思就是将公共的部分剥离出来，形成函数（方法），在需要该逻辑的地方引用该函数即可。如果后期有修改需求，只需要修改该函数一个地方，就可以实现多处逻辑被同时修改。同样，在UE4的蓝图中也提供了函数（Functions）这样的功能，通过创建函数可以将蓝图中公共的逻辑封装 ，从而实现多处引用。但是，UE4的蓝图是面向对象的，不同的武器（对象）之间是不能共用函数的，因此，将公共逻辑改为引用的方式是行不通的。&lt;/p&gt;
&lt;h4&gt;继承&lt;/h4&gt;
&lt;p&gt;  既然蓝图是面向对象的，那么可以使用面向对象的编程特点：继承。所谓继承，就是将函数、变量封装到父类，从该父类集成出来的子类可以使用父类暴露出来的方法与属性。利用继承的特性，于是我们就可以从蓝图的Actor类中继承出Weapon类，而我们游戏中的各种武器，例如手枪、冲锋枪、狙击枪、火箭炮等等可以使用武器中封装的一些逻辑，比如开枪，换弹匣，等等，继承图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/08/ue4_multi_weapon/1.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  &lt;/p&gt;
&lt;p&gt;  可以看到，各种武器继承了Weapon之后，就拥有的子弹的变量，也拥有了开枪的函数，从而实现了武器逻辑的复用。&lt;/p&gt;
&lt;h3&gt;武器属性的设置&lt;/h3&gt;
&lt;p&gt;  不同的武器除了模型（Mesh）不同以外，还有子弹数量、开枪动画、装弹动画、子弹撞击粒子、子弹伤害等等不同的属性，不同的武器这些属性都不同，而这些属性都需要在父类Weapon中进行处理。那么如何才能为不同的武器配置这些属性呢？这就涉及到C++变量如何暴露给蓝图使用。&lt;/p&gt;
&lt;p&gt;  根据&lt;a href=&quot;https://docs.unrealengine.com/zh-CN/Programming/Introduction/#%E8%99%9A%E5%B9%BB%E5%8F%8D%E5%B0%84%E7%B3%BB%E7%BB%9F&quot;&gt;官方文档：虚幻反射系统&lt;/a&gt;，C++中的变量可以被&lt;code&gt;UPROPERTY()&lt;/code&gt;宏修饰，就可以暴露给蓝图使用，还可以根据需要设定访问权限。&lt;/p&gt;
&lt;p&gt;C++代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;	// 击中目标粒子
	UPROPERTY(EditDefaultsOnly)
	UParticleSystem* TargetFX;

	// 开枪动画
	UPROPERTY(EditDefaultsOnly)
	UAnimationAsset* ShootAnim;

	// 开枪间隔
	UPROPERTY(EditDefaultsOnly)
	float ShootInterval;

	// 换弹匣动画
	UPROPERTY(EditDefaultsOnly)
	UAnimationAsset* ReloadAnim;

	// 换弹匣时间
	UPROPERTY(EditDefaultsOnly)
	float ReloadTime;

	// 每颗子弹伤害值
	UPROPERTY(EditDefaultsOnly)
	float Damage;

	// 最大子弹数
	UPROPERTY(EditDefaultsOnly)
	int8 MaxBullet;

	// 是否正在换弹匣
	UPROPERTY(BlueprintReadOnly)
	bool Reloading = false;

	// 是否正在射击
	UPROPERTY(BlueprintReadOnly)
	bool Shooting = false;

	// 当前子弹数（因为要暴露给蓝图获取，所以类型扩充到int32）
	UPROPERTY(BlueprintReadOnly)
	int32 CurrentBullet;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自动生成的蓝图设置如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/08/ue4_multi_weapon/2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  可以看到，如果变量是粒子指针和动画的指针，蓝图中则直接生成了对应的可视化选择框，太方便了有木有。这不就是策划所需要的配置表吗，还是可视化的，再也不用担心把文件名配错了。&lt;/p&gt;
&lt;h3&gt;武器逻辑&lt;/h3&gt;
&lt;p&gt;  除了变量，C++函数有类似的处理方法，宏&lt;code&gt;UFUNCTION()&lt;/code&gt;可以将函数暴露给蓝图，供蓝图调用。因此，上文中提到的换弹匣的逻辑，就可以移植到C++中，从而给予所有武器具有开枪与换弹匣能力。&lt;/p&gt;
&lt;p&gt;开枪的核心代码如下（已精简）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 获取坐标与朝向
World-&gt;GetFirstPlayerController()-&gt;GetPlayerViewPoint(Location, Rotation);
// 播放开枪动画
Mesh-&gt;PlayAnimation(ShootAnim, false);
// 计算终点坐标 = 起点坐标 + 方向 * 距离
FVector EndLocation = Location + Rotation.Vector() * 10000;
// 发射射线
World-&gt;LineTraceSingleByChannel(Result, Location, EndLocation, ECC_WorldStatic, ccq);
// 已击中
if (Result.Actor.IsValid())
{
    // 播放粒子
    FRotator EmitterRotation = FRotator(0, 0, 0);
    AActor* Actor = Result.Actor.Get();
    UGameplayStatics::SpawnEmitterAtLocation(Actor, TargetFX, Result.Location, EmitterRotation);
    // 中弹的是Character
    if (dynamic_cast&amp;#x3C;ACharacter*&gt;(Actor) != NULL)
    {
        // 受伤害
        ACharacter* Shooter = dynamic_cast&amp;#x3C;ACharacter*&gt;(WeaponOwner);
        UGameplayStatics::ApplyDamage(Actor, Damage, Shooter-&gt;GetController(), this, NULL);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来是换弹匣：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;Reloading = true;
// 播放动画
Mesh-&gt;PlayAnimation(ReloadAnim, false);
// 设置延时回调
GetWorldTimerManager().SetTimer(ReloadTimer, this, &amp;#x26;AWeapon::ReloadFinish, ReloadTime);

void AWeapon::ReloadFinish()
{
	CurrentBullet = MaxBullet;
	Reloading = false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为换弹匣并不是瞬间换好（按了R键需要等一定时间后子弹才会恢复），因此使用了定时器来实现。&lt;/p&gt;
&lt;h1&gt;遇到的问题&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;蓝图中绑定的Mesh无法传递到C++&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;  我按照之前的方法，在C++中定义好骨骼Mesh指针：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 武器mesh
UPROPERTY(EditDefaultsOnly)
USkeletalMeshComponent* MeshComponent;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  编译之后，兴冲冲的跑到蓝图中准备选择Mesh，然而却发现蓝图中却没有出现MeshComponent这个字段，以为是C++代码没有编译到，于是就反复试了几次，结果还是没有。怎么办呢？怀疑是UE4的这个反射系统不支持&lt;code&gt;USkeletalMeshComponent*&lt;/code&gt;这种变量类型，把变量类型改为&lt;code&gt;int32&lt;/code&gt;后，果然，这个字段出现了。&lt;/p&gt;
&lt;p&gt;  SkeletalMeshComponent不行，那么父类MeshComponent呢？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 武器mesh
UPROPERTY(EditDefaultsOnly)
UMeshComponent* MeshComponent;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  然而，编译之后蓝图中还是没有......看来的确是不支持&lt;code&gt;USkeletalMeshComponent*&lt;/code&gt;或者&lt;code&gt;UMeshComponent*&lt;/code&gt;这种类型。怎么办呢？我突然想到，既然不支持在蓝图中直接选择默认值，那么在蓝图中调用set方法来设置该变量吧！&lt;/p&gt;
&lt;p&gt;  于是，我将C++代码修改为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 武器mesh
UPROPERTY(BlueprintReadWrite)
USkeletalMeshComponent* MeshComponent;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  蓝图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/08/ue4_multi_weapon/3.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  这样虽然实现了需求，但是需要在每一把武器蓝图中做一样的调用，如果有50把武器呢？100把武器呢？这样做明显与我的期望不一致，因此这种方法虽然可行，但是不可取。&lt;/p&gt;
&lt;p&gt;  还有其他办法吗？通过查询UE4 C++的API，我找到了&lt;code&gt;AActor::GetComponentByClass&lt;/code&gt;这个函数，官方文档的描述是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Searches components array and returns first encountered component of the specified class&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;  即查找并获取本Actor的指定类型的组件的第一个。这不正是我需要的吗？如果我指定查找类型为USkeletalMeshComponent，而每个武器只有一个USkeletalMeshComponent，那这样不就从蓝图中获取到了绑定的Mesh吗？实现代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AWeapon::BeginPlay()
{
	Super::BeginPlay();
	// 获得Mesh
	MeshComponent = dynamic_cast&amp;#x3C;USkeletalMeshComponent*&gt;(GetComponentByClass(USkeletalMeshComponent::StaticClass()));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  通过这种方式，实现了武器的全部逻辑从蓝图中去除。当新加一件武器时，只需要在武器蓝图属性中配置动画、粒子、子弹容量等属性后，就可以直接在游戏中使用。&lt;/p&gt;
&lt;h2&gt;蓝图与C++选择的思考&lt;/h2&gt;
&lt;p&gt;  既然UE4同时支持蓝图与C++，那么我们在开发时应该如何选择呢？官方文档有如下的解释：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;程序员利用C++即可添加基础Gameplay系统，然后设计师可基于这些系统进行构建或利用这些系统为某个特定关卡或游戏本身创建自定义Gameplay。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;  也就是说，程序员用C++开发一些基础的系统，例如本文当中的武器系统（Weapon），设计师（策划）即可利用该武器系统在蓝图上进行武器的扩充，设计出不同的武器；设计师（策划）也可以利用C++开发的基础系统，将这些系统在蓝图上进行组装，以构建更丰富的玩法系统。除此之外，&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;蓝图的可重构性非常非常非常&lt;strong&gt;低&lt;/strong&gt;，如果某个模块的逻辑比较复杂，就会出现各种线条乱飞，非常的凌乱，过段时间再过来看，可能作者自己都看不明白了。因此我建议，对于比较复杂的逻辑，最好使用C++来实现，如果一定要用蓝图，请将该部分逻辑拆分为几段小逻辑来实现（充分利用蓝图的函数与宏）。&lt;/li&gt;
&lt;li&gt;蓝图本身是作为二进制文件来保存（.uasset），在版本管理工具中（Git）无法进行差异性对比，如果对于某段频繁修改（升级）的逻辑，又想看到每次修改的变化，最好也使用C++来实现，可以对比每个版本的修改内容。&lt;/li&gt;
&lt;li&gt;对于复杂的数学运算或循环次数较多的逻辑，也最好采用C++来实现，以保证运行效率。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>苍龙项目的持续集成、交付与部署</title><link>https://juzzi.qzz.io/blog/devel/ci-cd/canglong-ci-cd</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/devel/ci-cd/canglong-ci-cd</guid><description>如今，持续集成与部署已成为软件开发的标准化流程，也是敏捷开发的重要组成一环。</description><pubDate>Fri, 02 Aug 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;  所谓持续集成，是指软件开发团队成员定期（每天甚至更短）将自己的开发成果合并到产品项目中，通过自动化构建（编译、发布、自动化测试等）验证本次开发成果，尽可能快地发现错误与漏洞（可能是本次开发内容本身的问题，也可能是开发内容与产品现有内容之间的兼容性问题）。&lt;/p&gt;
&lt;p&gt;  而持续交付，则是指定期将产品新版本交付给质量团队或用户评审与测试，以尽早发现产品缺陷，为产品进入生产环境之前进行风险评估与故障排除。在游戏开发流程中，持续交付则表现为，将服务器/客户端最新代码（资源）更新到测试服，以供策划/QA团队测试。&lt;/p&gt;
&lt;p&gt;  最后持续部署，是指上一步交付的代码通过评审之后，能够自动部署到生产环境。他的目标是，代码在任何时刻都是可部署的，可以进入生产阶段。在游戏开发流程中，持续部署则表现为，将服务器/客户端代码（资源）更新到正式服。&lt;/p&gt;
&lt;p&gt;  开发团队通过持续集成、交付、部署，可以大大减少功能集成问题，提高团队的开发效率，使团队能开发出高质量的软件产品。&lt;/p&gt;
&lt;p&gt;  既然持续集成如此重要，那为什么&quot;苍龙&quot;这款产品一直没有做持续集成等相关流程呢？这就要从项目历史说起。&lt;/p&gt;
&lt;h1&gt;项目历史&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/07/canglong_ci_develop/13.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  苍龙项目组立项于2014年初，苍龙在研发初期，把专注力都放在产品本身，为了产品的快速上线，高度重(chóng)用了公司已有项目的开发成果。但在部署运维方面，仍然是用的非常原始的手段：手动编译、手动打包、手动更新...。好消息是，苍龙因为研发周期短，在其他同类游戏出现之前占领了“写实三国卡牌”这个新兴市场，在日本、韩国、东南亚都取得了不错的成绩。但是，因为包括持续集成等在内的流程等方面的不足，也给项目组带来了很多的问题与困扰。&lt;/p&gt;
&lt;h1&gt;问题&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;游戏产品数值变动频繁，但策划没有自主更新测试服配置表的途径。过分依赖服务器程序员帮助其更新配置表，增加了更新配置表的时间消耗，也严重影响的服务器程序员的工作效率。&lt;/li&gt;
&lt;li&gt;手动编译、打包、更新对于服务器程序员来讲，不但费时费力，而且操作过程容易出错。（苍龙历史上曾出现多次因为更新时代码未编译而导致的临时维护事故）&lt;/li&gt;
&lt;li&gt;没有自动化测试流程（单元测试），仅依靠功能验收时的黑盒测试与功能更新时的冒烟测试，并不能完全保证代码质量。&lt;/li&gt;
&lt;li&gt;更新正式服仍依靠手动将更新内容上传到指定位置，然后运维进行更新操作，并不能保证更新内容在正式服与测试服完全一致（后来虽然增加了md5校验，但上传与更新过程仍然费时费力）&lt;/li&gt;
&lt;li&gt;更新正式服需要运维人员参与，开发人员需要在每次维护时与运维人员对接维护内容，会出现因运维人员的疏忽而漏更新某些服务器。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;雏形：配置表发布工具&lt;/h1&gt;
&lt;p&gt;  问题如此之多，需要一步一步来解决。最基础也是最亟待解决的问题就是问题1（策划自主更新测试服配置表），因为这严重影响了策划与服务器程序员的工作效率。时间还是2016年，当时我刚来到苍龙项目组，接到的第一个任务就是在一周之内开发出一套能交付给策划使用更新测试服配置表的工具。在梳理完服务器更新流程后，大致理了一下实现步骤：远程调用服务器脚本关闭服务器 -&gt; 上传策划需要发布的配置表 -&gt; 远程启动服务器。因为时间要求比较紧，所以只做了一个控制台版，效果如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/07/canglong_ci_develop/2.png&quot; alt=&quot;2&quot;&gt;&lt;/p&gt;
&lt;p&gt;  虽然比较简陋，界面也不太友好，但已经能基本解决策划更新配置表的问题。后来也增加了代码打包，发布代码等功能。&lt;/p&gt;
&lt;p&gt;  后记：可能是思维定式的限制，只按照任务要求做了工具解决更新更新问题，并没有想到使用jenkins来引入自动化流程，而是继续着手优化、迭代该发布工具，致使苍龙完整的持续集成、交付、部署工作的延后，这也是比较失误的地方，应当引以为戒。&lt;/p&gt;
&lt;h1&gt;优化：图形界面版本&lt;/h1&gt;
&lt;p&gt;  虽然已经有了测试服发布工具，但是控制台窗口界面不友好，操作不便，连我自己都比较嫌弃它，更何况是其他人。于是便有了制作图形界面版本的想法，在调研了策划、程序等多方需求后，我利用工作空档开始了图形界面版开发。因为桌面软件重在精简，上百m的jre环境让我放弃了继续使用java作为开发语言的想法，而是转向了python。python图形界面开发框架有wxpython，ssh库有paramiko，windows环境打包有pyinstaller，完全符合我的预期，因此图形界面版就此诞生。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/07/canglong_ci_develop/3.png&quot; alt=&quot;3&quot;&gt;&lt;/p&gt;
&lt;p&gt;  该版本通过界面选择jdk路径，实现了代码自动编译、打包、上传、更新，基本实现了持续集成、交付的工作流程，同时解决了问题2（更新测试服过程繁琐、易出错的问题）。&lt;/p&gt;
&lt;h1&gt;持续集成、交付与部署&lt;/h1&gt;
&lt;p&gt;  有了上述持续集成的流程之后，我又在思考持续部署应该怎么做，但是发现好像进入了死胡同。因为使用自定义工具方式的持续集成与交付，代码并没有提交，并不能保证更新的测试服的代码是完整和最新的，所以运维在更新正式服时，也不能直接使用测试服的代码，因此，已有的自定义工具更新无法完成持续部署等后续流程。&lt;/p&gt;
&lt;h2&gt;Jenkins&lt;/h2&gt;
&lt;h3&gt;Jenkins介绍&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/07/canglong_ci_develop/4.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  &lt;a href=&quot;https://jenkins.io/zh/&quot;&gt;Jenkins&lt;/a&gt;是一款开源 CI&amp;#x26;CD （持续构建与部署）软件，用于自动化各种任务，包括构建、测试和部署软件。可以使用Maven来构建Java应用，用npm来构建Node.js与React应用，用PyInstaller来构建python应用等等。安装和使用Jenkins也非常简单，&lt;a href=&quot;https://jenkins.io/zh/doc/book/installing/&quot;&gt;官网&lt;/a&gt;详细介绍了在不同平台的多种安装方法，我选择了最方便的使用war包来运行，因为服务器自带有JDK环境。&lt;/p&gt;
&lt;h3&gt;使用Jenkins&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/07/canglong_ci_develop/11.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  jenkins的使用非常简单，只要按照提示一步步进行即可。这里选择最常用的“构建一个自由风格的软件项目”，如果后续步骤比较复杂，可以考虑使用流水线。&lt;/p&gt;
&lt;h3&gt;现有的构建方式 or Maven改造？&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/07/canglong_ci_develop/12.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  这一步比较关键，Jenkins让我们选择使用何种方式来构建项目。这里我进行了一番斟酌，按照目前苍龙传统的构建方式，是使用javac来编译，然后使用zip来打包应该选择shell方式。但目前这种方式没有持续集成流程中“自动测试”这样一环，而自动测试对于持续集成来讲又非常重要。因此为了加入自动测试以及今后能更方便的构建苍龙服务器代码，最后决定，首先进行Maven的集成。&lt;/p&gt;
&lt;h2&gt;Maven集成&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/07/canglong_ci_develop/5.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Maven介绍&lt;/h3&gt;
&lt;p&gt;  Maven是一款专门对Java应用进行依赖管理的工具，它采用了统一的标准（Pom文件）来构建Java应用，使得Java开发者对于项目中使用的依赖组件能够一目了然，很便捷地处理依赖冲突的问题，也能很高效地完成编译、打包等操作。除此之外，Maven还能在打包之前完成自动化单元测试，非常有利于开发人员的自我测试，在功能开发阶段就找出并解决一些Bug。但非常不幸的是，苍龙服务器代码因为一些历史原因，并没有使用Maven。没有条件就创造条件，下面就开始了Maven集成工作。&lt;/p&gt;
&lt;h3&gt;Maven集成步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;整理各服务器项目代码（游戏服、战斗服、世界服、日志服）所使用的jar包，区分出公有jar包与私有jar包。&lt;/li&gt;
&lt;li&gt;将公有jar包配置为直接从阿里云中央仓库下载，私有jar包上传到私有仓库，再从私有仓库下载。&lt;/li&gt;
&lt;li&gt;整理与解决依赖冲突问题&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;遇到的问题与解决&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;大部分私有jar包来源未知，并且没有源码，也在开源平台中无法找到（应该是祖传jar包），无法通过编译源码方式deploy到私有仓库。解决方法：在Nexus Repository Manager后台登陆后直接上传jar包。&lt;/li&gt;
&lt;li&gt;某些jar包根据名字和内容判断，应该是公有jar包，但是却没有标注版本号（坑啊！），因此无法从阿里云中央仓库中下载。解决方法：作为私有jar包处理。&lt;/li&gt;
&lt;li&gt;原项目中jar包冲突却一直没被发现，例如activemq-all-5.10.2.jar与log4j-slf4j-impl-2.1.jar冲突，启动时会提示slf4j重复绑定。解决方法：将all包拆分为单个组件，并使用exclusion标签排除冲突的jar。&lt;/li&gt;
&lt;li&gt;原项目代码采用JDK1.6编译，但JDK1.6无法兼容最新版的Maven3.6.x。解决方法：使用最后能支持JDK1.6的Maven3.2.5版本。&lt;/li&gt;
&lt;li&gt;**（最棘手）**项目路径问题。普通Java项目源码路径为ROOT/src，但maven项目的源码路径为ROOT/src/main/java。如果要更改源码路径，那么以前在SVN上建立的分支将无法识别新的源码路径（已经过测试验证）。这个问题引起了我的好奇，并且发现在Git上修改源项目码路径不会有该问题，但是在SVN上就会出现。经过一番比较深入的研究后发现，在Git中如果将某个文件A移动了路径B，git会记录该版本进行了rename A-&gt;B，在其他分支上对A的修改仍然可以合并到B。而SVN则不一样，如果文件A移动到了路径B，SVN会记录该版本将A删除，新建了B，在其他分支上对A的修改就无法合并到B。因此，受苍龙使用SVN的限制，就无法修改源码路径，那么如何使Maven能够识别原有的源码路径ROOT/src呢？好在Maven提供了对源码和资源指定的支持：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;sourceDirectory&gt;src&amp;#x3C;/sourceDirectory&gt;
&amp;#x3C;testSourceDirectory&gt;test/java&amp;#x3C;/testSourceDirectory&gt;
&amp;#x3C;resources&gt;
    &amp;#x3C;resource&gt;
        &amp;#x3C;directory&gt;resources&amp;#x3C;/directory&gt;
    &amp;#x3C;/resource&gt;
&amp;#x3C;/resources&gt;
&amp;#x3C;testResources&gt;
    &amp;#x3C;testResource&gt;
        &amp;#x3C;directory&gt;test/resources&amp;#x3C;/directory&gt;
    &amp;#x3C;/testResource&gt;
&amp;#x3C;/testResources&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  至此，Maven集成完毕。&lt;/p&gt;
&lt;h2&gt;测试集成&lt;/h2&gt;
&lt;p&gt;  Maven已经有了，那么可以开始使用Jenkins进行持续集成了吗？不！不要忘了Maven还有一项重要使命：自动完成单元测试。苍龙服务器代码中虽然之前有引入JUnit包，但是需要手动运行，非常不方便（以前也从来没有手动运行过）。测试集成也比较简单，主要分为两类：&lt;/p&gt;
&lt;h3&gt;单元测试&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;静态公共方法(public static ...)：直接在测试类中调用即可。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;私有方法(private ...)：这类方法无法在测试类中直接调用，这时候Java的反射调用就能排上用场了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Method method = xxx.getClass().getDeclaredMethod(&quot;xxx&quot;, xxx.class,xxx.class);
method.setAccessible(true);
method.invoke(xxx, xxx, xxx);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Spring注入对象的成员方法（service等）：从Spring上下文中获取对象再调用即可。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Mock测试&lt;/h3&gt;
&lt;p&gt;  有时候编写测试代码时会出现这些情况，某个类可能过于复杂，可能因为依赖过多，甚至可能构造方法因为业务需要被设置成了私有（private）访问，导致我们无法直接new出这个对象，这时候怎么测试呢？这时候Mock测试就派上用场了。我选用的是目前使用最广泛的mockito库，步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;模拟出该对象：&lt;code&gt;XXX mock = mock(XXX.class);&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;设置XXX.class的xx()方法返回值：&lt;code&gt;when(mock.xx()).thenReturn(xx);&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;  这样就可以在测试代码中，很方便的调用mock.xx()了，非常简单。&lt;/p&gt;
&lt;p&gt;  以后在每次Maven打包时，就会自动查找test路径下所有类中以@Test注解的所有public void xxx测试方法，如果测试结果与预期不符，就会终止打包，我们便可以在打包阶段就解决掉一些Bug，降低产品的风险。&lt;/p&gt;
&lt;h2&gt;流程图&lt;/h2&gt;
&lt;p&gt;  类似于之前采用工具的持续集成方式，jenkins的集成流程也分为了程序版与策划版。&lt;/p&gt;
&lt;h3&gt;程序版流程图&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/07/canglong_ci_develop/6.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;策划版流程图&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/07/canglong_ci_develop/7.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;优化&lt;/h3&gt;
&lt;p&gt;  可以看到，两条流水线的区别就是持续集成不同，程序是检出的代码，并且需要编译、测试、打包；而策划检出的是配置表。&lt;/p&gt;
&lt;p&gt;  两条流水线采用的是独立运行的方式，程序负责代码的持续集成，策划负责配置表的持续集成。使用了一段时间之后，我们发现，这种方式在开发版本（主线分支）没有问题，因为开发版本只有测试服没有正式服。而到了生产版本（线上分支），如果按照这种方式，程序、策划流水线独立运行，会导致在正式服维护期间，需要重启2次正式服。而正式服的重启又比较重度（需要备份线上数据），这就导致服务器更新时间翻倍（10分钟增加到20分钟）。为了解决上述问题，我对上述方案进行了改进，将两条流水线合并。&lt;/p&gt;
&lt;p&gt;流程图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/07/canglong_ci_develop/8.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  在该优化版本中，将之前的两条流水线相同的部分合并，不同的部分保留（SVN检出配置表、代码）。配置表和代码的产物生成之后，将两份产物合并作为新的产物，再生成版本号，用该版本产物来更新测试服与正式服。&lt;/p&gt;
&lt;h1&gt;服务器更新操作流程对比&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;原有更新流程（每一步均为手动操作）：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/07/canglong_ci_develop/9.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;现有更新流程（只有一步操作）：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/07/canglong_ci_develop/10.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h1&gt;细节对比 {#detail-compare}&lt;/h1&gt;
&lt;p&gt;以下分别为有/无持续集成、交付、部署下的对比结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正式服维护操作时间
&lt;ul&gt;
&lt;li&gt;无：开发人员15分钟+运维人员15分钟。&lt;/li&gt;
&lt;li&gt;有：开发人员1秒（无需运维人员参与）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;正式服维护服务器更新时间
&lt;ul&gt;
&lt;li&gt;无：约30分钟。&lt;/li&gt;
&lt;li&gt;有：约10分钟。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;操作复杂度
&lt;ul&gt;
&lt;li&gt;无：约20个步骤，非常复杂.&lt;/li&gt;
&lt;li&gt;有：只需要点一下鼠标，非常简单。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;出错概率
&lt;ul&gt;
&lt;li&gt;无：较高，手动执行的20个步骤，若有一个出错，则会出现更新错误（曾多次出现因代码未编译、未使用策划最新配置表、运维人员漏更新某个功能服等问题）。&lt;/li&gt;
&lt;li&gt;有：几乎为0，使用的是经过测试与验证的更新步骤与脚本。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;持续集成与部署（CI/CD）应在项目初期就应该建立，能大大提高开发效率、提高产品质量，甚至能让产品走得更远。&lt;/li&gt;
&lt;li&gt;自定义工具式的持续集成在进行持续部署方面会较为困难，推荐还是使用Jenkins来实现持续集成、交付与部署。&lt;/li&gt;
&lt;li&gt;实践证明，自定义工具在测试性代码或配置表会比较有用，因为这种方式不用担心会把测试性代码或配置表更新到正式服。&lt;/li&gt;
&lt;li&gt;如今，DevOps的思想已在全球普及，它是重视软件开发（Dev）与运维技术（Ops）之间沟通合作一种文化，透过自动化“软件交付”和“架构变更”的流程，使得构建、测试、发布软件能够更加地快捷、频繁和可靠。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;参考资料：&lt;a href=&quot;https://www.mindtheproduct.com/2016/02/what-the-hell-are-ci-cd-and-devops-a-cheatsheet-for-the-rest-of-us/&quot;&gt;The Product Managers’ Guide to Continuous Delivery and DevOps&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>用LaTeX书写数学公式</title><link>https://juzzi.qzz.io/blog/lang/other/latex-write-math-formula</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/lang/other/latex-write-math-formula</guid><description>为了更方便的书写数学公式，有多种语法支持，主要有TeX/LaTeX, MathML和AsciiMath，目前使用最多的是LaTeX</description><pubDate>Sat, 23 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;公式渲染&lt;/h2&gt;
&lt;p&gt;虽然写好了LaTeX语法的数学公式，但是还需要渲染支持，才能将LaTeX语法渲染为公式，目前主流的渲染引擎为&lt;a href=&quot;https://www.mathjax.org/&quot;&gt;MathJax&lt;/a&gt;。Github默认是不支持渲染LaTeX的（Gitee支持），为了让Github支持渲染，还需要安装chrome浏览器插件&lt;a href=&quot;https://chrome.google.com/webstore/detail/mathjax-3-plugin-for-gith/peoghobgdhejhcmgoppjpjcidngdfkod&quot;&gt;MathJax 3 Plugin for Github&lt;/a&gt;。（FireFox暂时没有找到插件支持）&lt;/p&gt;
&lt;h2&gt;LaTeX语法&lt;/h2&gt;
&lt;h3&gt;界定符&lt;/h3&gt;
&lt;p&gt;公式分为陈列式（displayed mathematics）和行内式（in-line mathematics），有不同的界定符。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;陈列式：&lt;code&gt;$$ ... $$&lt;/code&gt;或&lt;code&gt;\[ ... \]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;例：&lt;/p&gt;
&lt;p&gt;$$ y = ax_1 + bx_2 $$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;行内式：&lt;code&gt;$ ... $&lt;/code&gt;或&lt;code&gt;\( ... \)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;例：&lt;/p&gt;
&lt;p&gt;$ y = ax_1 + bx_2 $&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;MathJax默认不渲染&lt;code&gt;$ ... $&lt;/code&gt;类型，防止干扰文章中正常的&lt;code&gt;$&lt;/code&gt;符号显示，&lt;a href=&quot;https://docs.mathjax.org/en/latest/input/tex/delimiters.html#tex-delimiters&quot;&gt;这里&lt;/a&gt;有详细解释。&lt;/li&gt;
&lt;li&gt;在MarkDown中由于&lt;code&gt;\&lt;/code&gt;需要转义，因此需要写成&lt;code&gt;$ ... $&lt;/code&gt;和&lt;code&gt;\\[ ... \\]&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;常用数学符号&lt;/h3&gt;
&lt;h4&gt;修饰符&lt;/h4&gt;
&lt;p&gt;| 符号名 | 符号                 | 语法               | 备注                    |
| :----- | :------------------- | :----------------- | ----------------------- |
| 上标   | $ x^2 $              | &lt;code&gt;x^2&lt;/code&gt;              |                         |
| 下标   | $ x_1 $              | &lt;code&gt;x_1&lt;/code&gt;              |                         |
| 方括号 | $ \left [ \right ] $ | &lt;code&gt;\left [ \right ]&lt;/code&gt; | 方括号中的内容从&lt;code&gt;[&lt;/code&gt;开始 |
| 分式   | $ \frac{1}{x} $      | &lt;code&gt;\frac{1}{x}&lt;/code&gt;      |                         |
| 平均数 | $ \overline{x} $     | &lt;code&gt;\overline{x}&lt;/code&gt;     |                         |&lt;/p&gt;
&lt;h4&gt;运算符&lt;/h4&gt;
&lt;p&gt;| 符号名  | 符号                       | 语法                    | 备注                       |
| :------ | :------------------------- | :---------------------- | -------------------------- |
| 叉乘    | $ \times $                 | &lt;code&gt;\times&lt;/code&gt;                |                            |
| 点乘    | $ \cdot $                  | &lt;code&gt;\cdot&lt;/code&gt;                 |                            |
| 星乘    | $ \ast $                   | &lt;code&gt;\ast&lt;/code&gt;                  |                            |
| 除号    | $ \div $                   | &lt;code&gt;\div&lt;/code&gt;                  |                            |
| 约等于  | $ \approx $                | &lt;code&gt;\approx&lt;/code&gt;               |                            |
| 平方根  | $ \sqrt{x} $               | &lt;code&gt;\sqrt{x}&lt;/code&gt;              |                            |
| n次方根 | $ \sqrt[n]{x} $            | &lt;code&gt;\sqrt[n]{x}&lt;/code&gt;           | [n]省略时为平方根          |
| 对数    | $ \log(x) $                | &lt;code&gt;\log(x)&lt;/code&gt;               |                            |
| 累加    | $ \sum_{0}^{\infty}{x} $  | &lt;code&gt;\sum_{0}^{\infty}{x}&lt;/code&gt;  | 起始值为下标，终止值为上标 |
| 累乘    | $ \prod_{1}^{\infty}{x} $ | &lt;code&gt;\prod_{1}^{\infty}{x}&lt;/code&gt; | 起始值为下标，终止值为上标 |&lt;/p&gt;
&lt;h4&gt;微积分&lt;/h4&gt;
&lt;p&gt;| 符号名 | 符号                                  | 语法                                | 备注                       |
| :----- | :------------------------------------ | :---------------------------------- | -------------------------- |
| 极限   | $ \lim^{}_{x \to 0}{x} $             | &lt;code&gt;\lim^{}_{x \to 0}{x}&lt;/code&gt;              | 优先用下标                 |
| 积分   | $ \int^{\infty}_{0}{xdx} $           | &lt;code&gt;\int^{\infty}_{0}{xdx}&lt;/code&gt;            | 起始值为下标，终止值为上标 |
| 偏导   | $ \frac{\partial (x+1)}{\partial x} $ | &lt;code&gt;\frac{\partial (x+1)}{\partial x}&lt;/code&gt; |                            |
| 梯度   | $ \nabla $                            | &lt;code&gt;\nabla&lt;/code&gt;                            |                            |&lt;/p&gt;
&lt;h4&gt;集合&lt;/h4&gt;
&lt;p&gt;| 符号名 | 符号       | 语法     | 备注 |
| :----- | :--------- | :------- | ---- |
| 属于   | $ \in $    | &lt;code&gt;\in&lt;/code&gt;    |      |
| 不属于 | $ \notin $ | &lt;code&gt;\notin&lt;/code&gt; |      |&lt;/p&gt;
&lt;p&gt;注：当符号的对象为表达式时，需要用&lt;code&gt;{}&lt;/code&gt;括起来，例如$ x*{t+1} $需要写成&lt;code&gt;x*{t+1}&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;希腊字符&lt;/h3&gt;
&lt;p&gt;| 字符（大） |    语法    | |  | 字符（小） |    语法    |
| :--------: | :--------: | --- | :--------: | :--------: |
|     A      |    &lt;code&gt;A&lt;/code&gt;     | |  |  $\alpha$  |  &lt;code&gt;\alpha&lt;/code&gt;  |
|     B      |    &lt;code&gt;B&lt;/code&gt;     | |  |  $\beta$   |  &lt;code&gt;\beta&lt;/code&gt;   |
|  $\Gamma$  |  &lt;code&gt;\Gamma&lt;/code&gt;  | |  |  $\gamma$  |  &lt;code&gt;\gamma&lt;/code&gt;  |
|  $\Delta$  |  &lt;code&gt;\Delta&lt;/code&gt;  | |  |  $\delta$  |  &lt;code&gt;\delta&lt;/code&gt;  |
|     E      |    &lt;code&gt;E&lt;/code&gt;     | |  | $\epsilon$ | &lt;code&gt;\epsilon&lt;/code&gt; |
|     Z      |    &lt;code&gt;Z&lt;/code&gt;     | |  |  $\zeta$   |  &lt;code&gt;\zeta&lt;/code&gt;   |
|     H      |    &lt;code&gt;H&lt;/code&gt;     | |  |   $\eta$   |   &lt;code&gt;\eta&lt;/code&gt;   |
|  $\Theta$  |  &lt;code&gt;\Theta&lt;/code&gt;  | |  |  $\theta$  |  &lt;code&gt;\theta&lt;/code&gt;  |
|     I      |    &lt;code&gt;I&lt;/code&gt;     | |  |  $\iota$   |  &lt;code&gt;\iota&lt;/code&gt;   |
|     K      |    &lt;code&gt;K&lt;/code&gt;     | |  |  $\kappa$  |  &lt;code&gt;\kappa&lt;/code&gt;  |
| $\Lambda$  | &lt;code&gt;\Lambda&lt;/code&gt;  | |  | $\lambda$  | &lt;code&gt;\lambda&lt;/code&gt;  |
|     N      |    &lt;code&gt;N&lt;/code&gt;     | |  |   $\nu$    |   &lt;code&gt;\nu&lt;/code&gt;    |
|   $\Xi$    |   &lt;code&gt;\Xi&lt;/code&gt;    | |  |   $\xi$    |   &lt;code&gt;\xi&lt;/code&gt;    |
|     O      |    &lt;code&gt;O&lt;/code&gt;     | |  | $\omicron$ | &lt;code&gt;\omicron&lt;/code&gt; |
|   $\Pi$    |   &lt;code&gt;\Pi&lt;/code&gt;    | |  |   $\pi$    |   &lt;code&gt;\pi&lt;/code&gt;    |
|     P      |     P      | |  |   $\rho$   |   &lt;code&gt;\rho&lt;/code&gt;   |
|  $\Sigma$  |  &lt;code&gt;\Sigma&lt;/code&gt;  | |  |  $\sigma$  |  &lt;code&gt;\sigma&lt;/code&gt;  |
|     T      |     T      | |  |   $\tau$   |   &lt;code&gt;\tau&lt;/code&gt;   |
| $\Upsilon$ | &lt;code&gt;\upsilon&lt;/code&gt; | |  | $\upsilon$ | &lt;code&gt;\upsilon&lt;/code&gt; |
|   $\phi$   |   &lt;code&gt;\phi&lt;/code&gt;   | |  |   $\phi$   |   &lt;code&gt;\phi&lt;/code&gt;   |
|     X      |     X      | |  |   $\chi$   |   &lt;code&gt;\chi&lt;/code&gt;   |
|   $\Psi$   |   &lt;code&gt;\Psi&lt;/code&gt;   | |  |   $\psi$   |   &lt;code&gt;\psi&lt;/code&gt;   |
|  $\Omega$  |  &lt;code&gt;\Omega&lt;/code&gt;  | |  |  $\omega$  |  &lt;code&gt;\omega&lt;/code&gt;  |&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>通信协议多维度对比</title><link>https://juzzi.qzz.io/blog/devel/protocol/message-protocol-compare</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/devel/protocol/message-protocol-compare</guid><description>网络程序开发就离不开客户端与服务器，而客户端与服务器交互的核心之一就是通信协议</description><pubDate>Tue, 08 Jan 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;背景&lt;/h1&gt;
&lt;p&gt;通信协议的选择往往关系到通信的稳定性、通信效率（消息序列化/反序列化时间）、数据量（序列化后的字节大小）等等。目前主流开源的通信协议有：Json, Message Pack, Protocol Buffers, Flat Buffers, JDK Serialize等等，我将依次就开发便捷度、序列化效率、数据量等维度从Java开发者的角度作出对比分析。&lt;/p&gt;
&lt;h1&gt;样本数据&lt;/h1&gt;
&lt;p&gt;假定我们有一个装备背包，里面装有玩家拥有的装备，装备的数据结构如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Equip {
    /** 装备唯一id */
    private long id;
    /** 装备等级 */
    private int lv;
    /** 装备品质 */
    private int quality;
    /** 装备星级 */
    private int star;
    /** 装备附加属性 */
    private int[] extralAtts = new int[5];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为模拟真实数据，我们在生成每件装备时，均随机填充装备的各个字段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 构造测试数据
long id = 100000000000L;
Equip[] equips = new Equip[NUM];
for (int i = 0; i &amp;#x3C; NUM; i++) {
    equips[i] = new Equip().random(id++);
}

public Equip random(long id) {
    this.id = id;
    lv = new Random().nextInt(100);
    quality = new Random().nextInt(100);
    star = new Random().nextInt(100);
    for (int i = 0; i &amp;#x3C; extralAtts.length; i++) {
        extralAtts[i] = new Random().nextInt(10000);
    }
    return this;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Json&lt;/h1&gt;
&lt;p&gt;Json在web开发中应用广泛，编程语言基本都支持，序列化后的数据具备可读性，但由于包含数据的字段名，因此数据较为庞大，数据量大时，可考虑使用压缩。Java开发中使用广泛并且效率较高的Json库有Fastjson和Jsoniter，以下是采用Json通信协议的开发步骤。
①. 编写通信消息类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class EquipInventoryInit {
    /** 背包类型 */
    private int type;
    /** 背包里的装备 */
    private EquipMsg[] equips;
    /** 背包的扩展容量 */
    private int extralCapacity;
}

public class EquipMsg {
    /** 装备唯一id */
    private long id;
    /** 装备等级 */
    private int lv;
    /** 装备品质 */
    private int quality;
    /** 装备星级 */
    private int star;
    /** 装备附加属性 */
    private int[] extralAtts = new int[5];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;②. 填充通信消息类数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static EquipInventoryInit createMsg(Equip[] equips) {
    EquipInventoryInit initMsg = new EquipInventoryInit();
    initMsg.setType(1);
    initMsg.setExtralCapacity(NUM);
    initMsg.setEquips(new EquipMsg[equips.length]);
    for (int i = 0; i &amp;#x3C; equips.length; i++) {
        initMsg.getEquips()[i] = new EquipMsg().fillByEquip(equips[i]);
    }
    return initMsg;
}

public EquipMsg fillByEquip(Equip equip) {
    id = equip.getId();
    lv = equip.getLv();
    quality = equip.getQuality();
    star = equip.getStar();
    for (int i = 0; i &amp;#x3C; extralAtts.length; i++) {
        extralAtts[i] = equip.getExtralAtts()[i];
    }
    return this;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Fastjson&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alibaba/fastjson&quot;&gt;https://github.com/alibaba/fastjson&lt;/a&gt;
③. 序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;byte[] fastjsonBytes = JSON.toJSONBytes(initMsg);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;④. 反序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EquipInventoryInit o = JSON.parseObject(fastjsonBytes, EquipInventoryInit.class);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Jsoniter&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://jsoniter.com/&quot;&gt;https://jsoniter.com/&lt;/a&gt;
&lt;a href=&quot;https://github.com/json-iterator&quot;&gt;https://github.com/json-iterator&lt;/a&gt;
③. 序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;byte[] jsoniterBytes = JsonStream.serialize(initMsg).getBytes();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;④. 反序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EquipInventoryInit jsoniterMsg = JsonIterator.deserialize(fastjsonBytes, EquipInventoryInit.class);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开发便捷度：★☆&lt;/p&gt;
&lt;h1&gt;MessagePack&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://msgpack.org/&quot;&gt;https://msgpack.org/&lt;/a&gt;
MessagePack是一种类似于Json、但是速度更快、体积更小，支持非常多的编程语言（50多种）。性能非常好，但是对消息类的字段顺序要求严格，若字段顺序不一致或字段个数不符，都会导致反序列化失败，并且序列化后的数据不具备可读性。开发步骤：
①. 编写通信消息类（与Json的方式一致，这里略过）
②. 填充通信消息类数据（一样，也略过）
③. 序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MessagePack msgpack = new MessagePack();
byte[] msgpackBytes = msgpack.write(initMsg);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;④. 反序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EquipInventoryInit msgpackMsg = msgpack.read(msgpackBytes, EquipInventoryInit.class);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开发便捷度：★☆&lt;/p&gt;
&lt;h1&gt;Protocol Buffers&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://developers.google.com/protocol-buffers/&quot;&gt;https://developers.google.com/protocol-buffers/&lt;/a&gt;
Protobuf支持的编程语言有C++、Java、C#、Python、Go、Dart，可根据协议文件生成编程语言代码，每个字段有单独的编号，可解决向下兼容的问题。序列化后的数据为二进制，不可读，但性能较好，因开发方式友好，效率较高，使用比较广泛。开发步骤如下：
①. 编写通信协议文件（.proto）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

// java包
option java_package = &quot;com.digisky.canglong.msgtest.protobuf&quot;;
// java类名
option java_outer_classname=&quot;TestProto&quot;;

// 装备背包初始化
message PbEquipInventoryInit {
    uint32 type = 1;// 背包类型
    repeated PbEquip equips = 2;// 拥有的装备
    uint32 extralCapacity = 3;// 扩展容量
}

// 装备实体
message PbEquip {
    uint64 id = 1;// 实体id
    uint32 lv = 2;// 等级
    uint32 quality = 3;// 品级
    uint32 star = 4;// 星级
    repeated PbExtralAtts atts = 5;// 额外属性
}

// 附加属性
message PbExtralAtts {
    enum PbAttType {
        ATK = 0;// 攻击
        DEF = 1;// 防御
        MATK = 2;// 魔攻
        MDEF = 3;// 魔御
        LF = 4;// 生命
    }
    PbAttType attType = 1;// 属性类型
    uint32 value = 2;//
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;②. 使用protoc.exe工具生成编程语言代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/Test.proto
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;③. 填充通信消息类数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Builder builder = PbEquipInventoryInit.newBuilder();
builder.setType(1);
builder.setExtralCapacity(NUM);
for (Equip equip : equips) {
    TestProto.PbEquip.Builder equipBuilder = TestProto.PbEquip.newBuilder();
    equipBuilder.setId(equip.getId());
    equipBuilder.setLv(equip.getLv());
    equipBuilder.setQuality(equip.getQuality());
    equipBuilder.setStar(equip.getStar());
    int[] extralAtts = equip.getExtralAtts();
    for (int i = 0; i &amp;#x3C; extralAtts.length; i++) {
        PbExtralAtts.Builder attBuilder = PbExtralAtts.newBuilder();
        attBuilder.setAttTypeValue(i);
        attBuilder.setValue(extralAtts[i]);
        equipBuilder.addAtts(attBuilder);
    }

    builder.addEquips(equipBuilder.build());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;④. 序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;byte[] protobufBytes = builder.build().toByteArray();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⑤. 反序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Builder protobufMsg = PbEquipInventoryInit.newBuilder().mergeFrom(protobufBytes);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开发便捷度：★★★&lt;/p&gt;
&lt;h1&gt;FlatBuffers&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://google.github.io/flatbuffers/&quot;&gt;https://google.github.io/flatbuffers/&lt;/a&gt;
Flatbuffer支持的编程语言有C++、Java、C#、Python、Go、Dart、JS、TS、C、PHP、Lobster、Rust，是Google专门为游戏开发而设计的通信协议，序列化后的数据可以直接读取，无需反序列化，因此可大量节省反序列化的时间。同样可根据协议文件生成编程语言代码。开发步骤如下：
①. 编写通信协议文件（.fbs）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace com.digisky.canglong.msgtest.tester.flatbuffer;

table FbEquipInventoryInit {
    type:int; // 背包类型
    equips:[FbEquip];// 拥有的装备
    extralCapacity:int;// 扩展容量
}

table FbEquip {
    id:int64;// 实体id
    lv:int;// 等级
    quality:int;// 品级
    star:int;// 星级
    atts:[int];// 额外属性
}

root_type FbEquipInventoryInit;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;②. 使用flatc.exe工具生成编程语言代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flatc --java -o ../../java/ Test.fbs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;③. 填充通信消息类数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FlatBufferBuilder builder = new FlatBufferBuilder();
int[] equipsIndex = new int[equips.length];
for (int i = 0; i &amp;#x3C; equips.length; i++) {
    Equip equip = equips[i];
    // atts
    int attOffset = FbEquip.createAttsVector(builder, equip.getExtralAtts());
    int equipOffset = FbEquip.createFbEquip(builder, equip.getId(), equip.getLv(), equip.getQuality(),
            equip.getStar(), attOffset);
    equipsIndex[i] = equipOffset;
}
// 一定要先create里层的FbEquip然后再createEquipsVector，否则会报错
int equipsOffset = FbEquipInventoryInit.createEquipsVector(builder, equipsIndex);
FbEquipInventoryInit.startFbEquipInventoryInit(builder);
// type
FbEquipInventoryInit.addType(builder, 1);
// extralCapacity
FbEquipInventoryInit.addExtralCapacity(builder, Start.NUM);
// equips
FbEquipInventoryInit.addEquips(builder, equipsOffset);
int end = FbEquipInventoryInit.endFbEquipInventoryInit(builder);
builder.finish(end);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;④. 序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;byte[] bytes = builder.sizedByteArray()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⑤. 反序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ByteBuffer bb = ByteBuffer.wrap(bytes);
FbEquipInventoryInit init = FbEquipInventoryInit.getRootAsFbEquipInventoryInit(bb);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开发便捷度：★★★&lt;/p&gt;
&lt;h1&gt;BIO&lt;/h1&gt;
&lt;p&gt;BIO是当前项目正在使用的通信协议，由于该组件是从约2011年左右的老项目继承过来的，所以其开发效率、通信效率都不高，再此一起加入分析与对比。BIO的核心思想是将所有的数据都通过Map的形式封装，因为Map具有通用性与扩展性（可以put进去任何类型的数据），因此可将任意条消息合并成一条消息，但同时也导致单条消息巨大，客户端容易假死。并且Map可随意添加节点的特性项目后期不易维护，Map的格式下客户端与服务器对接消息十分艰难。
①. 构造与填充通信消息Map&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Map msg = new HashMap();
Map r = new HashMap();
msg.put(&quot;r&quot;, r);
r.put(&quot;type&quot;, 1);
r.put(&quot;cap&quot;, NUM);
List equipList = new ArrayList();
r.put(&quot;equips&quot;, equipList);
for (Equip equip : equips) {
    Map equipMsg = new HashMap();
    equipMsg.put(&quot;id&quot;, equip.getId());
    equipMsg.put(&quot;lv&quot;, equip.getLv());
    equipMsg.put(&quot;quality&quot;, equip.getQuality());
    equipMsg.put(&quot;star&quot;, equip.getStar());
    Map extralAttsMsg = new HashMap();
    equipMsg.put(&quot;atts&quot;, extralAttsMsg);
    int[] extralAtts = equip.getExtralAtts();
    for (int i = 0; i &amp;#x3C; extralAtts.length; i++) {
        extralAttsMsg.put(i, extralAtts[i]);
    }
    equipList.add(equipMsg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;③. 序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;byte[] mapBytes = BioHelper.mapToBytes(msg, 1024);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;④. 反序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Map map = BioHelper.mapFromBytes(mapBytes);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开发便捷度：☆&lt;/p&gt;
&lt;h1&gt;测试对比&lt;/h1&gt;
&lt;p&gt;测试环境
&lt;strong&gt;CPU:Intel Core i3-4150 @3.50GHz&lt;/strong&gt;
&lt;strong&gt;JDK:Oracle JDK 1.8.0_111&lt;/strong&gt;
结果如下表：
&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/01/msg_proto_compare.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;h2&gt;JDK&lt;/h2&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;无需依赖其他库，可直接使用&lt;/li&gt;
&lt;li&gt;开发时时可将通信类作为公共模块(Java Dependent Module)，客户端与服务器只用维护一套代码&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;只能与java项目通信，不能跨语言。&lt;/li&gt;
&lt;li&gt;性能偏低。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Json&lt;/h2&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;支持的编程语言很多，使用广泛。&lt;/li&gt;
&lt;li&gt;数据具备可读性。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据量较大，性能较低。&lt;/li&gt;
&lt;li&gt;没有相关的代码生成工具。&lt;/li&gt;
&lt;li&gt;安全性较低，明文数据需要加密。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;MessagePack&lt;/h2&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;支持的编程语言特别多，凡是听说过的编程语言都支持。&lt;/li&gt;
&lt;li&gt;数据量非常小，数据大小方面排名第一。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;序列化与反序列化耗时较长，接近json的水平。&lt;/li&gt;
&lt;li&gt;没有相关的代码生成工具。&lt;/li&gt;
&lt;li&gt;无法向下兼容，增/删字段或调整字段位置会导致旧消息无法解析&lt;/li&gt;
&lt;li&gt;~~网上有测试数据表明反序列化比较占内存，可能是部分编程语言的锅~~&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Protobuf&lt;/h2&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;支持主流的编程语言（C++、Java、C#、Python、Go、Dart）。&lt;/li&gt;
&lt;li&gt;有proto协议文件，与代码生成，开发效率高，对接方便。&lt;/li&gt;
&lt;li&gt;在序列化、反序列化时间、数据大小上表现都较为优秀（领先JDK、Json）。&lt;/li&gt;
&lt;li&gt;每个字段有编号，可以很好的做到向下兼容。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;生成的代码非常庞大，增大了项目的代码体积，打开生成的代码会导致IDE卡顿或假死。&lt;/li&gt;
&lt;li&gt;序列化、反序列化时间上不及FlatBuffers，数据大小上不及Msgpack&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;FlatBuffer&lt;/h2&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;支持主流的编程语言（C++、Java、C#、Python、Go、Dart、JS、TS、C、PHP、Lobster、Rust）。&lt;/li&gt;
&lt;li&gt;有fbs协议文件，与代码生成，开发效率高，对接方便，且生成的代码量小，对项目代码体积影响较小。&lt;/li&gt;
&lt;li&gt;在序列化、反序列化时间上非常优秀，尤其反序列化耗时几乎为0（无需反序列化）。&lt;/li&gt;
&lt;li&gt;比较节省内存，适合移动开发。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;与Protobuf相比数据较大，但也远远领先于Json。&lt;/li&gt;
&lt;li&gt;数据填充必须从里层到外层，否则会出错。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;| 通信协议    | 语言覆盖 | 对接方便度 | 开发难易度 | 序列化耗时 | 反序列化耗时 | 数据量 | 内存消耗 | 总评 |
| ----------- | -------- | ---------- | ---------- | ---------- | ------------ | ------ | -------- | ---- |
| BIO         | ★        | ☆          | ☆          | ★★         | ★★           | ☆      | ☆        | 7    |
| ProtoBuf    | ★☆       | ★★★        | ★★★        | ★★☆        | ★★☆          | ★★☆    | ★★       | 17   |
| Json        | ★★★      | ★☆         | ★★★        | ★☆         | ★☆           | ★      | ★        | 12.5 |
| MessagePack | ★★☆      | ★☆         | ★★☆        | ★☆         | ★            | ★★★    | ★☆       | 13.5 |
| JDK         | ☆        | ★★         | ★★☆        | ★          | ★            | ★☆     | ★★       | 10.5 |
| FlatBufer   | ★★       | ★★★        | ★★         | ★★★        | ★★★          | ★★     | ★★★      | 18   |&lt;/p&gt;
&lt;h1&gt;方案选择&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;传输数据最小（最省流量，适合休闲类游戏、三消类游戏、剧情类游戏）
MessagePack(Gzip) + 自制代码生成工具
根据本次测试数据，MessagePack在数据大小上比ProtoBuf约小40%（gzip后约小20%），序列化时间比Protobuf约多5%-25%，反序列化比Protobuf约多几十-几百ms&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;速度最快（延迟最小，适合MOBA、FPS等类型的游戏）
FlatBuffer + KCP/UDP 
根据本次测试数据，FlatBuffer在数据大小上比ProtoBuf约大25%（gzip后约大18%），序列化时间比Protobuf约少5%-10%，反序列化比Protobuf约少几十-几百ms&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用最广泛、综合成绩不错
Protocol Buffer
如果不追求极致的数据压缩或者消息编码速度，可选择综合性能较好的ProtoBuf，因为使用较广泛，团队成员对其熟悉程度比较高，可降低一定的学习成本；部分开发框架都集成了Protobuf，可拿来直接使用，降低了部分开发成本。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>AI算法进行手写数字识别</title><link>https://juzzi.qzz.io/blog/lang/other/algorithm-recognize-digits</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/lang/other/algorithm-recognize-digits</guid><description>人工智能（Artificial Intelligence，简称AI）一词最初是在1956年Dartmouth学会上提出的，从那以后，研究者们发展了众多理论和原理，人工智能的概念也随之扩展。</description><pubDate>Sun, 02 Dec 2018 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;人工智能&lt;/h2&gt;
&lt;p&gt;  由于人工智能的研究是高度技术性和专业的，各分支领域都是深入且各不相通的，因而涉及范围极广 。 人工智能的核心问题包括建构能够跟人类似甚至超越人类的推理、知识、学习、交流、感知、使用工具和操控机械的能力等，当前人工智能已经有了初步成果，甚至在一些影像识别、语言分析、棋类游戏等等单方面的能力达到了超越人类的水平 。&lt;/p&gt;
&lt;p&gt;  人工智能的分支领域非常多，主要有演绎推理、知识表示、规划、学习、自然语言处理……等十多个分支领域，而以机器学习为代表的“学习”领域，是目前研究最广泛的分支之一。&lt;/p&gt;
&lt;h2&gt;机器学习&lt;/h2&gt;
&lt;p&gt;   机器学习（Machine Learning）是人工智能的一个分支，它是实现人工智能的一个途径，即以机器学习为手段解决人工智能中的问题。机器学习在近30多年已发展为一门多领域交叉性的学科，涉及概率论、统计学、逼近论、凸分析、计算复杂性理论等多门学科。&lt;/p&gt;
&lt;p&gt;   机器学习理论主要是设计和分析一些让计算机可以自动“学习”的算法，该算法是一类从数据中自动分析获得规律，并利用规律对未知数据进行预测的算法。&lt;/p&gt;
&lt;h2&gt;深度学习&lt;/h2&gt;
&lt;p&gt;  深度学习（Deep Learning）是机器学习的分支，是一种以人工神经网络为架构，对数据进行表征学习的算法。表征学习的目标是寻求更好的表示方法并创建更好的模型来从大规模未标记数据中学习这些表示方法。表示方法来自神经科学，并松散地创建在类似神经系统中的信息处理和对通信模式的理解上，如神经编码，试图定义拉动神经元的反应之间的关系以及大脑中的神经元的电活动之间的关系。&lt;/p&gt;
&lt;p&gt;  因此，人工智能、机器学习、深度学习的关系如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/12/2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  至今已有数种深度学习模型，如深度神经网络、卷积神经网络和深度置信网络和递归神经网络已被应用在计算机视觉、语音识别、自然语言处理、音频识别与生物信息学等领域并获取了极好的效果。&lt;/p&gt;
&lt;p&gt;  目前，业内也已经产生了多种优秀的深度学习框架，例如TensorFlow、PyTorch、Caffe、Mxnet等等。但这些都&lt;strong&gt;不是&lt;/strong&gt;本文讨论的重点，本文主要以机器学习初学者的身份，使用最基本的机器学习算法，加以微积分、线性代数、概率统计等基础数学知识，来解决手写数字的识别的问题。&lt;/p&gt;
&lt;h2&gt;问题背景&lt;/h2&gt;
&lt;p&gt;  为什么要去研究数字的识别问题呢？因为最近刚过双11，又看了到许多曝光快递行业野蛮分拣的新闻。据某快递公司负责人回应称，之所以会出现野蛮分拣的问题，主要是双11期间快递数量巨增，为了尽快派发收到的快递，他们不得不请了许多“临时工”，而这些“临时工”缺乏培训，缺乏规范操作，所以出现了暴力分拣快递的问题。&lt;/p&gt;
&lt;h2&gt;问题分析&lt;/h2&gt;
&lt;p&gt;  针对分拣快递这种简单、重复的工作，可以交给机器去做吗？我们来分析一下“临时工”所做的工作。“临时工”拿到一个快递，找到快递上的快递单，然后再找到目的省（城市），如果是华北城市，则将快递扔进“北京”的筐里；如果是华东城市，则扔进“上海”的筐里；如果是西南城市，则扔进“成都”的筐里。那么机器可以完成吗？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/12/4.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  如上图所示，快递通过传送带进入“目的地识别系统”，识别后将该快递分配到对应地点的传送带即可。那么，该问题的关键就是“目的地识别系统”如何将快递单上的目的地识别出来，并作出正确的判断。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/12/3.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  以顺丰速运的快递单为例，快递单上已将目的地翻译为了城市代码，通过识别该代码即可让机器“知道”快递的目的地，然后配置对应传送带接收的具体城市代码即可。&lt;/p&gt;
&lt;p&gt;  识别数字技术在深度学习领域已经非常成熟，常见的解决方案是OpenCV+Keras+TensorFlow，例如Github上有比较完善的&lt;a href=&quot;https://github.com/zeusees/HyperLPR&quot;&gt;车牌识别&lt;/a&gt;项目，但本文并不打算使用这些库，而是采用最底层、最基础的机器学习方法来实现。&lt;/p&gt;
&lt;h2&gt;数学建模&lt;/h2&gt;
&lt;p&gt;  数字照片通过扫描后，以像素点的方式进行存储，因此输入数据即是像素点，通过机器学习算法后，结果则是识别出来的0-9的数字。根据机器学习理论，每个样本都有对应的标签，因此属于“监督式学习”的范畴。而样本的输出值为0-9固定的10种情况，因此可以采用逻辑回归的机器学习模型来解决，分别计算结果为0-9的概率，建模就是找到一个假设函数（Hypothesis Function），函数值是数据通过假设函数后获得对应的输出结果，即概率。&lt;/p&gt;
&lt;p&gt;  逻辑回归的假设函数是由S型函数（Sigmoid Function）演变而来，S型函数的表达式及曲线如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/notes/raw/master/ML/_images/sigmoid_function.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/notes/raw/master/ML/_images/logistic_regression_hypothesis_graph.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  从曲线中可以看到，当变量z趋近于正无穷时，函数值趋近于1，当变量z趋近于负无穷时，函数值趋近于0。这样就能够很好的匹配逻辑回归，因为逻辑回归的输出为0或1，当输出值为0.7时，则表示结果为1的概率是70%，为0的概率是30%，正好可以进行概率的预测。&lt;/p&gt;
&lt;p&gt;  受线性回归所启发，逻辑回归的假设函数公式为（其中θ为模型的参数矩阵，x为输入变量矩阵，变量z变成了θ的转置乘以x）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/notes/raw/master/ML/_images/logistic_regression_hypothesis.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  如果要让机器来识别数字，那么首先就要先用样本去教会机器，即用样本“训练”模型。为了获得“最好”的模型，我们需要计算样本在模型下的代价函数（Cost Function，也有资料称为“损失函数”）。所谓代价函数，就是在该模型下产生的输出与实际结果间产生的偏差，偏差越小，则可以在一定程度上表明模型越好（也不是绝对的，可能会出现模型过度拟合（Overfit）的情况，需要一些手段来避免）。&lt;/p&gt;
&lt;p&gt;  通过概率统计理论中的“最大似然估计”，可以得到如下的逻辑回归的代价函数：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/notes/raw/master/ML/_images/logistic_regression_cost_function.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  该函数看起来很复杂，可以将其拆开来看，log(h(x))是y=1时的代价函数，log(1-h(x))是y=0时的代价函数，最右边的一项为正则化参数，可以减小出现过度拟合的几率。为了找到最好的模型（假设函数），我们需要找到该代价函数的最小值。找到最小值后，自变量θ即为我们要找的逻辑回归的模型参数。&lt;/p&gt;
&lt;p&gt;  根据高等数学中的“拉格朗日中值定理”，可以得知该函数为凹函数，存在最小值。证明过程比较复杂，不在此阐述。&lt;/p&gt;
&lt;h2&gt;模型训练&lt;/h2&gt;
&lt;p&gt;  为了得到代价函数J(θ)的最小值，我们可以采用机器学习中最常用的“梯度下降”算法（Gradient Decent）来求得函数在区间内的极小值。所谓梯度下降算法，就是对于任一函数，首先取任一点（x1或者x2均可），在这一点减去这一点对应的梯度（即该点的导数），那么这一点就会向该函数的某一极小值运动，反复进行梯度下降，则可以得到区间内的极小值x0。如果函数为凹函数，那么该极小值就是函数的最小值。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/12/5.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  因此，执行梯度下降的公式为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/notes/raw/master/ML/_images/gradient_decent1.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  这里需要对J(θ)求“偏导数”，求得后的结果为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/notes/raw/master/ML/_images/gradient_decent2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  至此，理论工作准备完毕，可以进行编码实战。&lt;/p&gt;
&lt;h3&gt;在Matlab/Octave中训练 {#train-in-matlab}&lt;/h3&gt;
&lt;p&gt;  输入样本为手写数字，以20 _ 20像素点的形式存储，将像素点数据摊开作为一行，每行就有400个像素点信息。训练样本中搜集了5000个手写数字的照片，因此样本X为5000 _ 400的矩阵，样本结果y为5000 * 1的列向量。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-matlab&quot;&gt;% 计算代价函数
J = 1 / m * (-y&apos; * log(sigmoid(X * theta)) - (1 - y)&apos; * log(1 - sigmoid(X * theta)));

% 计算梯度
grad = 1 / m * X&apos; * (sigmoid(X * theta) - y);

% 代价函数正则化
J = J + lambda / (2 * m) * (sum(theta(2:end) .^ 2));

% 梯度正则化
theta_temp = theta;
theta_temp(1) = 0;
grad = grad + lambda / m * theta_temp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  通过以上代码，就可以实现一次代价函数的计算，并返回当前点的梯度。根据之前的分析，只要重复进行梯度下降即可。而Matlab提供了一种更加简便的方式“fmincg”函数，它能采用类似梯度下降的方式，来自动优化参数θ。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-matlab&quot;&gt;% 初始化theta
initial_theta = zeros(n + 1, 1);
% 参数
options = optimset(&apos;GradObj&apos;, &apos;on&apos;, &apos;MaxIter&apos;, 50);

% 循环所有数字
for c = 1:num_labels
    % 训练出最优theta
    theta = fmincg(@(t)(lrCostFunction(t, X, (y == c), lambda)), initial_theta, options);
    all_theta(c, :) = theta;
endfor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  可以看到，上述代码进行了1-10总共10个模型的训练，每个模型就是识别0-9这10个数字的概率。通过以上训练后，就可以得到最后的theta，将其带入假设函数hθ(x)，于是就得到了我们训练后的10个模型，可以用该模型来进行手写数字的识别。&lt;/p&gt;
&lt;h2&gt;数字识别&lt;/h2&gt;
&lt;p&gt;  利用已经训练好的10个模型，我们就可以将机器从未见过的手写数字通过10个模型，让每个模型计算出他是对应数字的概率，然后我们取最高的概率，就可以得到机器识别出的数字。我们来举个例子：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/12/6.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  如图红框中的数字，可能有的人会认成“4”，而有的人却会认成“6”。到底是4还是6呢？可能众说纷纭，因为有的人习惯这样写4，而有的人却不习惯这样写。在机器学习中，机器会学习之前样本中的数据，学习到作者写数字的习惯，将该测试样本分别输入到10个模型后，得到如下的概率输出（均保留5位有效数字）：&lt;/p&gt;
&lt;p&gt;| 数字 | 概率          | 数字 | 概率          |
| ---- | ------------- | ---- | ------------- |
| 0    | 0.0000021328% | 5    | 0.0000015184% |
| 1    | 0.0000021719% | 6    | 99.987%       |
| 2    | 2.3224%       | 7    | 0.000033508%  |
| 3    | 0.0000012768% | 8    | 0.0011023%    |
| 4    | 0.013391%     | 9    | 0.032251%     |&lt;/p&gt;
&lt;p&gt;  通过如上数据可以看到，数字6的匹配度高达99.987%占据了绝对领先，第二则是数字2的2.3224%。而数字4只有0.013391的概率，看来在机器学习看来，这个数字基本可以判定为“6”，只是稍微有一丁点像“2”，跟其他数字都特别不像。我们取概率最大值，得出了正确的结果为数字“6”。&lt;/p&gt;
&lt;p&gt;  我们用测试样本的真实值对模型进行校验，最终获得训练的正确率为94.9%。那么取100个测试样本的识别结果如何呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/12/7.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/12/8.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  可以看到，100个测试样本的识别结果有5个数字识别错误，测试识别率为95%。那么有什么方法可以提高识别率呢？&lt;/p&gt;
&lt;h3&gt;提高梯度下降次数&lt;/h3&gt;
&lt;p&gt;  通过之前的理论分析我们知道，梯度下降次数越多，代价函数就越接近最小值，于是我把次数从50提高到100时，测试样本准确率达到了95.98，提升了约1%。然后又提高到200时，达到了96.4%，提升了约0.5%。最后提高到500时，仍为96.4%，没有提升。&lt;/p&gt;
&lt;p&gt;  看来在逻辑回归模型下，手写数字的识别率最高仅可以提升到96.4%，已经达到了最高。还有其他办法可以提升识别率吗？&lt;/p&gt;
&lt;h2&gt;神经网络&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/12/9.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  人工神经网络（Artificial Neural Network），简称神经网络（Neural Network，NN），在机器学习和认知科学领域，是一种模仿生物神经网络（动物的中枢神经系统，特别是大脑）的结构和功能的数学模型或计算模型，用于对函数进行估计或近似。&lt;/p&gt;
&lt;p&gt;  神经网络由大量的人工神经元联结进行计算，大多数情况下人工神经网络能在外界信息的基础上改变内部结构，是一种自适应系统，通俗的讲就是具备学习功能，并且是一种非线性统计性数据建模工具。&lt;/p&gt;
&lt;p&gt;  不得不说，人类是真的聪明，居然可以想到建立类似于生物大脑神经的模型来模拟大脑，从而实现部分人类的能力。神经网络模型如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/notes/raw/master/ML/_images/neural_network.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  可以看到，基本的神经网络模型有输入层、隐藏层、输出层。输入层用于接受输入信号，类似于人类感知视觉信号、声音信号、触觉信号等等。隐藏层可以是多层，可以让数据在不同层之间传递与处理，类似于人类的神经元，可以逐级传递。输出层用于输出处理后的数据。如果有非常多的隐藏层，又可以称为深度神经网络，在这种模型下的机器学习又称作深度学习。&lt;/p&gt;
&lt;p&gt;  由于神经网络是一种非线性模型，属于逻辑结构，因此没有简单的“假设函数”。要计算数据通过输入层、隐藏层后到输出层的数据，可以通过“正向传播算法”（Forward Propagation）。&lt;/p&gt;
&lt;h3&gt;正向传播&lt;/h3&gt;
&lt;p&gt;  神经网络模型看似复杂，如果只看一层的话，就可以用逻辑回归的模型来推导，因为每一层都是逻辑回归问题。若θ1与θ2已知（图中标识），那么就可以用逻辑回归来计算每一层的输出，然后逐渐从左到右，正向传递，所以称为正向传播，最终得出输出值hθ(x)。&lt;/p&gt;
&lt;p&gt;  正向传播的步骤如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/12/10.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  假设函数有了，如果我们能找到模型的θ1与θ2，那么模型就有了，就可以用这个模型来进行数字识别了。那么怎么才能找到合适的θ1与θ2呢？与之前讲的逻辑回归类似，我们也可以先找到该模型的代价函数，然后通过梯度下降找到代价函数的最小值，就可以找到神经网络的参数了。&lt;/p&gt;
&lt;h3&gt;代价函数&lt;/h3&gt;
&lt;p&gt;  前面已经提到，神经网络模型其实就是有很多层的逻辑回归模型，那么代价函数也可以采用逻辑回归的代价函数，然后将每一层网络叠加起来就可以了，所以神经网络的代价函数如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/notes/raw/master/ML/_images/neural_network_cost_function.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  公式看起来比较吓人，实际上只是多了网络层数K，并且参数θ从向量变成了矩阵而已。如果把这个公式转化为矩阵形式，其实非常的简单（不含最右边的正则化）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-matlab&quot;&gt;% m为样本数，y为样本结果矩阵，h为由正向传播计算出的输出矩阵。
J = - 1 / m * (sum(sum(y .* log(h))) + sum(sum((1 - y) .* log(1-h))));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  代价函数有了，梯度该怎么算呢？与逻辑回归不同，因为增加了层数的概念，所以梯度计算也会变得比较复杂，神经网络里称为“反向传播算法”（Backward Propagation）。&lt;/p&gt;
&lt;h3&gt;反向传播&lt;/h3&gt;
&lt;p&gt;  求解梯度，最终还是对代价函数进行求偏导数，但由于模型是非线性的，无法直接求偏导数。所以，反向传播的基本思路就是将最终的计算偏差分摊到每一层，逐渐从右向左，反向传递，所以称为反向传播。&lt;/p&gt;
&lt;p&gt;  反向传播的步骤如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/12/11.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;  通过第4步，我们就得到了J(θ)的偏导数，即梯度。&lt;/p&gt;
&lt;h3&gt;随机初始化&lt;/h3&gt;
&lt;p&gt;  在神经网络模型中，θ的初始化非常重要。我在训练神经网络的实践中，就因为忘了随机初始化θ，而导致模型非常糟糕，无论怎么训练，识别率都只有40%。吃一堑，长一智。神经网络不像逻辑回归中的θ，逻辑回归中的θ初始为0或者其他任何数都可以，神经网络中θ的初始化值直接影响了模型的好坏。&lt;/p&gt;
&lt;p&gt;  θ的随机初始化的要求如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2019/12/12.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;在Matlab/Octave中训练&lt;/h3&gt;
&lt;p&gt;  正向传播：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% 计算h(x)
a1 = [ones(m, 1) X]&apos;;
z2 = Theta1 * a1;
a2 = [ones(1, m); sigmoid(z2)];
z3 = Theta2 * a2;
a3 = sigmoid(z3);
h = a3&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  计算代价函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;J = - 1 / m * (sum(sum(y2 .* log(h))) + sum(sum((1 - y2) .* log(1-h))));
% 计算正则化后的J
% 去掉theta的第一列，即去掉theta0
Theta1_new = Theta1(:, 2:end);
Theta2_new = Theta2(:, 2:end);
J = J + (sum(sum(Theta1_new .^ 2)) + sum(sum(Theta2_new .^ 2))) * lambda / (2 * m);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  反向传播：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% 将y2转化为每一列为一个样本
delta3 = a3 - y2&apos;;
Theta2_grad = delta3 * a2&apos; / m;
% Theta2需要使用去掉了第一列针对偏置的权重
delta2 = Theta2_new&apos; * delta3 .* sigmoidGradient(z2);
Theta1_grad = delta2 * a1&apos; / m;

% 正则化，需要将theta的第一列设置为0
Theta1(:, 1) = 0;
Theta2(:, 1) = 0;
Theta2_grad = Theta2_grad + lambda / m * Theta2;
Theta1_grad = Theta1_grad + lambda / m * Theta1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  梯度下降：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;options = optimset(&apos;MaxIter&apos;, 50);
lambda = 1;
costFunction = @(p) nnCostFunction(p, ...
                                   input_layer_size, ...
                                   hidden_layer_size, ...
                                   num_labels, X, y, lambda);
[nn_params, cost] = fmincg(costFunction, initial_nn_params, options);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  然后用同样的训练样本对神经网络模型训练后，循环50次，测试样本识别率达到了95.82%。提高到100次，达到98.14%。提高到200次，达到了98.94%。最后提高到500次，最终达到了99.36%。可以看到，神经网络模型对于手写数字识别率高于逻辑回归模型。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;对于手写数字，神经网络模型一般比逻辑回归模型的准确率更高。&lt;/li&gt;
&lt;li&gt;逻辑回归模型只能处理二维的输出，如果是高维的输出，需要用多个模型来降维，而神经网络可以直接处理多维输出。&lt;/li&gt;
&lt;li&gt;使用简单的（非深度）神经网络，就可以实现较高的手写数字识别率。&lt;/li&gt;
&lt;li&gt;神经网络模型的初始化参数非常重要。&lt;/li&gt;
&lt;li&gt;在Maltab/Octave中，矩阵的运算效率要远远高于循环的运算效率，因此数据处理尽量采用矩阵形式。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;  （由于水平有限，本文如有分析得不对之处，还请指正。）&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Maven Web项目发布到Tomcat</title><link>https://juzzi.qzz.io/blog/lang/java/maven-web-deploy</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/lang/java/maven-web-deploy</guid><description>Maven是java开发中常用的项目管理工具，可以很好的处理java组件的依赖关系。</description><pubDate>Sat, 22 Sep 2018 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;在java web项目中，maven也提供了“tomcat7-maven-plugin”这样的maven插件来使我们方便地将项目部署到服务器中的tomcat，下面就是我对配置步骤的总结。&lt;/p&gt;
&lt;h1&gt;Maven中的配置&lt;/h1&gt;
&lt;h2&gt;pom文件配置&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;plugins&gt;
	&amp;#x3C;plugin&gt;
        &amp;#x3C;groupId&gt;org.apache.tomcat.maven&amp;#x3C;/groupId&gt;
        &amp;#x3C;artifactId&gt;tomcat7-maven-plugin&amp;#x3C;/artifactId&gt;
        &amp;#x3C;version&gt;2.2&amp;#x3C;/version&gt;
        &amp;#x3C;configuration&gt;
            &amp;#x3C;url&gt;http://192.168.63.56:50031/manager/text&amp;#x3C;/url&gt;
            &amp;#x3C;server&gt;tomcat&amp;#x3C;/server&gt;
        &amp;#x3C;/configuration&gt;
    &amp;#x3C;/plugin&gt;
&amp;#x3C;/plugins&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&quot;server&quot;标签要与C:\Users\{User}\.m2\settings.xml中配置的用户名和密码的id一致，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;server&gt;
    &amp;#x3C;id&gt;tomcat&amp;#x3C;/id&gt;
    &amp;#x3C;username&gt;test&amp;#x3C;/username&gt;
    &amp;#x3C;password&gt;123456&amp;#x3C;/password&gt;
&amp;#x3C;/server&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;maven命令的配置&lt;/h2&gt;
&lt;p&gt;需要使用tomcat7:redeploy，因为如果tomcat中已经发布好了该项目，使用deploy就无法再次发布。&lt;/p&gt;
&lt;h1&gt;Tomcat中的配置&lt;/h1&gt;
&lt;h2&gt;用户配置&lt;/h2&gt;
&lt;p&gt;在conf/tomcat-users.xml中添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;role rolename=&quot;manager-gui&quot;/&gt;
&amp;#x3C;role rolename=&quot;manager-script&quot;/&gt;
&amp;#x3C;user username=&quot;test&quot; password=&quot;123456&quot; roles=&quot;manager-gui, manager-script&quot;/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：user标签中的用户名和密码要与C:\Users\{User}\.m2\settings.xml中配置的用户名和密码一致。&lt;/p&gt;
&lt;h2&gt;权限配置&lt;/h2&gt;
&lt;p&gt;修改webapps/manager/META-INF/context.xml，将Value标签中的allow元素修改为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;Valve className=&quot;org.apache.catalina.valves.RemoteAddrValve&quot;
         allow=&quot;192.168.*.*|::1|0:0:0:0:0:0:0:1&quot; /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者直接注释掉该标签也可以。&lt;/p&gt;
&lt;h2&gt;war包大小限制&lt;/h2&gt;
&lt;p&gt;Tomcat默认的war包限制为50M，若超过大小，则还需要修改webapps/manager/WEB-INF/web.xml中的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;multipart-config&gt;
      &amp;#x3C;!-- 50MB max --&gt;
    &amp;#x3C;max-file-size&gt;52428800&amp;#x3C;/max-file-size&gt;
    &amp;#x3C;max-request-size&gt;52428800&amp;#x3C;/max-request-size&gt;
    &amp;#x3C;file-size-threshold&gt;0&amp;#x3C;/file-size-threshold&gt;
&amp;#x3C;/multipart-config&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过以上的设置，就可以实现Maven中直接将项目部署到Tomcat。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>全球同服游戏服务器设计思路</title><link>https://juzzi.qzz.io/blog/devel/big-world-game-server</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/devel/big-world-game-server</guid><description>最近查阅了一些“全球同服”服务器架构设计的资料，发现全球同服并没有想象中的那么困难。</description><pubDate>Fri, 02 Mar 2018 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;背景&lt;/h1&gt;
&lt;p&gt;对于玩家而言，玩家并感知不到自己属于哪个服务器（登陆时无服务器列表），但实际上，玩家通过既定算法，被自动分配到了某个服务器中进行游戏。&lt;/p&gt;
&lt;h1&gt;分布式设计方案&lt;/h1&gt;
&lt;p&gt;因为服务器单服因承载量的原因，不可能将所有玩家都放在一个服务器（机器硬件），因此，全球同服的服务器架构都使用了分布式设计。&lt;/p&gt;
&lt;p&gt;服务器组一般由登陆服（login）、网关服（gs）、逻辑服（ls）、中心服（cs）、战斗服（fs）、以及交互功能的服务器，例如：好友服（friend）、聊天服（chat）、工会服（union）、排行服（rank）等服务器组构成。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;登陆服：登陆一般分为自有渠道登陆和第三方渠道登陆，登陆流程一般为登陆信息校验、帐号id获取。玩家之间无交互过程，因此可做分布式部署。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;网关服：网关服主要负责与客户端通信，玩家间无交互过程，可做分布式部署。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;逻辑服：逻辑服即游戏内主要玩法的实现，有些架构中 也称为游戏服，只做单机类玩法，无玩家间交互，因此也可做分布式部署。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;中心服：有些架构中也称为世界服、全局服，苍龙称为列表服，中心服作为所有逻辑服的连接枢纽，不同逻辑服间玩家的交互依靠中心服转发，拓扑图呈现星型结构，因此中心服无法做分布式部署。（如果玩家量级太大，中心服无法承担现有玩家通信压力，就只能扩展中心服，但这样就相当于做了玩家分区，例如：《恋与制作人》，现在出现了2服）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;战斗服：战斗服顾名思义就是承担游戏内战斗玩法，实现战斗内的各项功能，最终产出战斗结果。只需同时获取到战斗双方的信息即可开战，因此可做分布式部署。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;好友服、聊天服、工会服、排行服等：因特殊功能性一般不做分布式扩展。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综上所述，在全球同服的设计模式下，除了中心服及各种交互功能服无法做分布式部署，其他服务器节点都可做分布式部署，承担全球同服的服务器压力。&lt;/p&gt;
&lt;h1&gt;目前市面上全球同服的作品&lt;/h1&gt;
&lt;p&gt;《部落冲突》（COC）：游戏不分服，客户端与服务器连接推断是长连接，可以加入部落（公会），有掠夺战，有伪世界聊天，公共频道说话并非每个玩家都能看到。&lt;/p&gt;
&lt;p&gt;《恋与制作人》：目前IOS版增开了2服，推测是因为玩家过多，全局服务器无法承担通信压力。客户端与服务器通信很像是短连接，可长时间处于断网状态，只要断网时游戏内无操作即可正常游戏，无世界聊天。&lt;/p&gt;
&lt;p&gt;《奇迹暖暖》：与《暖暖环游世界》玩法很像，都是少女换装养成类游戏。客户端服务器通信应该是短链接。奇迹暖暖增加了很多社交性玩法，比如竞技场跟玩家PK，搭配赛的参与和点赞，之后又开了联盟。&lt;/p&gt;
&lt;h1&gt;两种分布式方案：数据分散存储与集中存储&lt;/h1&gt;
&lt;p&gt;在分布式服务器架构中，玩家感知不到多个游戏服（Virtual Server）的存在，每次登陆时，通过云中算法为玩家分配游戏服ID。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方案一：数据分散存储&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2018/03/pic1.jpg&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，假设玩家1被分配到了1服，玩家2被分配到了2服。如果数据分散存储，则玩家1数据存储在了1服的数据库中，玩家2存储在了2服的数据库中。那么玩家1以后每次登陆，都只能被分配到1服中。&lt;/p&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;减少了数据存储难度，采用常规数据库存储即可。&lt;/li&gt;
&lt;li&gt;可沿用常规服务器架构设计（非全球同服）中的数据持久化方案，代码改动量少。&lt;/li&gt;
&lt;li&gt;可沿用以往服务器承载量经验，服务器简单、稳定、可靠。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;无法使多个服（Virtual Server）实现负载均衡，可能会出现某个服玩家过多，某个服玩家过少的情况。&lt;/li&gt;
&lt;li&gt;若某个服负载严重时会出现排队登陆的情况（玩家看来就是某些用户需要排队，某些用户不用排队）。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;方案二：数据集中存储&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://gitee.com/juzzi/res/raw/master/pic/2018/03/pic2.jpg&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，采用数据集中存储，则以后无论玩家被分配到哪个服（Virtual Server），都可以通过分布式数据库（Distributed Storage）加载玩家数据。&lt;/p&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可以通过调整云中登陆算法，实现各服的负载均衡&lt;/li&gt;
&lt;li&gt;若所有服（Virtual Server）都已达到负载上限，可通过增加Virtual Server服务器组来增加全服承载量，不会出现排队情况&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;需要高吞吐量的数据库支持，我目前打算采用的有MongoDB Sharding或Redis Cluster&lt;/li&gt;
&lt;li&gt;对搭建数据库的服务器硬件配置要求较高，因为需要较大内存和较高吞吐量的磁盘IO和计算性能较高的CPU&lt;/li&gt;
&lt;li&gt;服务器架构较为复杂，能复用以往项目的代码较少。&lt;/li&gt;
&lt;li&gt;无法处理（或者很难处理？）离线玩家数据，例如要攻击名叫XXX的玩家，因为XXX并不在线，他的数据未加载到任意一个服中，因此无法交互。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;可以看出，采用方案二的数据存储模型更为科学，但同时对代码质量也有较高要求。&lt;/li&gt;
&lt;li&gt;全球同服的架构设计中，技术难点与承载量瓶颈在于中心服的设计，这是影响全服承载量的关键。&lt;/li&gt;
&lt;li&gt;全球同服的玩法设计，应尽量避免与不在线玩家的数据交互（若采用方案二的数据存储模型）。&lt;/li&gt;
&lt;li&gt;因为同时在线玩家数量巨大（可能是十万级甚至百万级），应尽量避免广播全区全服聊天、跑马灯信息，可参照COC的方式做伪世界聊天，并非所有玩家都能看到。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>使用JDK自带工具解决线上Java程序问题</title><link>https://juzzi.qzz.io/blog/lang/java/jdk-tool-analyze</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/lang/java/jdk-tool-analyze</guid><description>JDK自带了许多工具来对java进程进行分析，从基础工具到进阶工具的完整指南</description><pubDate>Mon, 26 Feb 2018 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;问题背景&lt;/h1&gt;
&lt;p&gt;我们有时会在线上生产环境（正式服务器）遇到测试服很难碰到的问题，例如内存泄漏、GC过于频繁、线程死锁、CPU占用过高等问题。因为该类问题需要一定数量的用户基数或特定条件，因此很难在测试服遇到。&lt;/p&gt;
&lt;p&gt;正式服一般没有 debug 日志，也无法打断点，无法像在测试服一样进行常规问题的排查。但 JDK 为我们提供了很多排查该类问题的工具。熟练使用这些工具，能帮助我们快速解决上述问题，同时也是 Java 进阶必备能力。&lt;/p&gt;
&lt;h1&gt;常用JDK自带工具&lt;/h1&gt;
&lt;h2&gt;jinfo&lt;/h2&gt;
&lt;p&gt;显示 JVM 的详细信息，可以查看和修改运行时参数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用法&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;连接到正在运行的进程：&lt;code&gt;jinfo [option] &amp;#x3C;pid&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;连接到一个核心文件：&lt;code&gt;jinfo [option] &amp;#x3C;executable &amp;#x3C;core&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;连接到远程调试服务器：&lt;code&gt;jinfo [option] [server_id@]&amp;#x3C;remote server IP or hostname&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参数介绍&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-flag &amp;#x3C;name&gt; 打印指定变量名的虚拟机参数
-flag [+|-]&amp;#x3C;name&gt; 启用或禁用指定变量名的虚拟机参数
-flag &amp;#x3C;name&gt;=&amp;#x3C;value&gt; 设置虚拟机的特定参数
-flags 打印虚拟机参数
-sysprops 打印Java系统属性
&amp;#x3C;无参数&gt; 打印虚拟机参数和Java系统属性
-h | -help 显示帮助
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常用示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看所有 JVM 参数
jinfo &amp;#x3C;pid&gt;

# 查看特定参数
jinfo -flag MaxHeapSize &amp;#x3C;pid&gt;

# 启用特定参数
jinfo -flag +PrintGCDetails &amp;#x3C;pid&gt;

# 设置特定参数
jinfo -flag:MaxPermSize=512m &amp;#x3C;pid&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;jmap&lt;/h2&gt;
&lt;p&gt;获得运行中的 JVM 的堆的快照，从而可以离线分析堆，以检查内存泄漏，检查一些严重影响性能的大对象的创建，检查系统中什么对象最多，各种对象所占内存的大小等等&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用法&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;连接到正在运行的进程：&lt;code&gt;jmap [option] &amp;#x3C;pid&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;连接到一个核心文件：&lt;code&gt;jmap [option] &amp;#x3C;executable &amp;#x3C;core&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;连接到远程调试服务器：&lt;code&gt;jmap [option] [server_id@]&amp;#x3C;remote server IP or hostname&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参数介绍&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;无参数&gt; 打印与Solaris pmap相同的信息
-heap 打印Java堆的汇总信息
-histo[:live] 打印java堆对象的柱状图；如果有&quot;live&quot;子选项，类加载统计只打印指定数量的活跃对象
-clstats 打印类加载统计
-finalizerinfo 打印等待终止的对象
-dump:&amp;#x3C;dump-options&gt; 生成hprof二进制格式的java堆快照，栗子：jmap -dump:live,format=b,file=heap.bin &amp;#x3C;pid&gt;
	快照选项：
	live 只快照活跃对象，如果该参数没有被指定，堆中所有对象都会被快照
	format=b 二进制格式
-F 强制执行，与-dump或-histo一起使用，来强制执行，当进程未响应的时候。此时&quot;live&quot;子选项无效。
-h | -help 显示帮助
-J&amp;#x3C;flag&gt; 传递参数给运行时系统
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常用示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看堆内存使用情况
jmap -heap &amp;#x3C;pid&gt;

# 查看对象统计（只统计活跃对象）
jmap -histo:live &amp;#x3C;pid&gt;

# 查看所有对象统计
jmap -histo &amp;#x3C;pid&gt;

# 导出堆转储文件
jmap -dump:live,format=b,file=heap.bin &amp;#x3C;pid&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;jps&lt;/h2&gt;
&lt;p&gt;jps (Java Virtual Machine Process Status Tool) 是 JDK 1.5 提供的一个显示当前所有 java 进程 pid 的命令，可以显示主机中运行的 java 进程，与 bash 命令 &lt;code&gt;ps -ef | grep java&lt;/code&gt; 很类似&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用法&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;jps [-q] [-mlvV] [&amp;#x3C;hostid&gt;]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参数介绍&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-q 安静模式，只显示进程id
-m 输出传递给main方法的参数
-l 输出启动类的完整类名
-v 输出传递给JVM的参数
-V 输出通过flag文件传递给JVM的参数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常用示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 显示所有 Java 进程
jps

# 显示完整类名和参数
jps -lv

# 只显示进程ID
jps -q
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;jstack&lt;/h2&gt;
&lt;p&gt;输出指定 Java 进程的线程堆栈信息&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用法&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;连接到正在运行的进程：&lt;code&gt;jstack [-l] &amp;#x3C;pid&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;连接到被挂起的进程：&lt;code&gt;jstack -F [-m] [-l] &amp;#x3C;pid&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;连接到一个核心文件：&lt;code&gt;jstack [-m] [-l] &amp;#x3C;executable&gt; &amp;#x3C;core&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;连接到远程调试服务器：&lt;code&gt;jstack [-m] [-l] [server_id@]&amp;#x3C;remote server IP or hostname&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参数介绍&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-l 输出同步锁信息
-m 检测死锁并输出线程的栈信息
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常用示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看线程堆栈
jstack &amp;#x3C;pid&gt;

# 查看线程堆栈（包含锁信息）
jstack -l &amp;#x3C;pid&gt;

# 查看线程堆栈（包含本地方法）
jstack -m &amp;#x3C;pid&gt;

# 查看线程堆栈（本地方法 + 锁信息）
jstack -m -l &amp;#x3C;pid&gt;

# 强制转储（进程无响应时）
jstack -F &amp;#x3C;pid&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;jstat&lt;/h2&gt;
&lt;p&gt;对 java 进程的资源和性能进行实时的监控，包括了对该进程的 classloader、compiler、gc 情况。也可以监视虚拟机内存内的堆和非堆的大小及其内存使用量，以及加载类的数量&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用法&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;jstat -&amp;#x3C;option&gt; [-t] [-h&amp;#x3C;lines&gt;] &amp;#x3C;vmid&gt; [&amp;#x3C;interval&gt; [&amp;#x3C;count&gt;]]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参数介绍&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;option&gt; 选项，使用jstat --options可以查看可用的选项列表，如下：
	class：统计classloader的行为
	compiler：统计hotspot just-in-time编译器的行为
	gc：统计gc行为
	gccapacity：统计堆中代的容量、空间
	gccause：垃圾收集统计，包括最近引用垃圾收集的事件，基本同gcutil，比gcutil多了两列
	gcnew：统计新生代的行为
	gcnewcapacity：统计新生代的大小和空间
	gcold：统计旧生代的行为
	gcoldcapacity：统计旧生代的大小和空间
	gcpermcapacity：统计永久代的大小和空间
	gcutil：垃圾收集统计
	printcompilation：hotspot编译方法统计
-t 额外显示时间戳
&amp;#x3C;vmid&gt; 虚拟机id
-h&amp;#x3C;lines&gt; 每n次采样，显示标题一次
&amp;#x3C;interval&gt; 采样间隔，以&quot;ms&quot;结尾代表毫秒，以&quot;s&quot;结尾代表秒，默认为毫秒
&amp;#x3C;count&gt; 统计次数次数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常用示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 每 1 秒统计一次 GC 信息
jstat -gc &amp;#x3C;pid&gt; 1000

# 显示时间戳
jstat -t -gc &amp;#x3C;pid&gt; 1000

# 查看原因
jstat -gccause &amp;#x3C;pid&gt; 1000

# 查看新生代统计
jstat -gcnew &amp;#x3C;pid&gt; 1000

# 查看堆容量
jstat -gccapacity &amp;#x3C;pid&gt; 1000
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;进阶 JDK 工具&lt;/h1&gt;
&lt;h2&gt;jcmd&lt;/h2&gt;
&lt;p&gt;jcmd 是 JDK 8 引入的通用诊断命令行工具，集成了 jps、jstat、jmap、jstack 等工具的功能。通过 jcmd 可以执行多种诊断操作，是 Java 8+ 推荐的诊断工具&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用法&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;jcmd [pid|mainclass] [command] [args...]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常用命令&lt;/strong&gt;：&lt;/p&gt;
&lt;h3&gt;1. 查看 JVM 信息&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看所有支持的命令
jcmd &amp;#x3C;pid&gt; help

# 查看 JVM 堆信息
jcmd &amp;#x3C;pid&gt; GC.heap_info

# 查看系统属性
jcmd &amp;#x3C;pid&gt; VM.system_properties

# 查看虚拟机参数
jcmd &amp;#x3C;pid&gt; VM.flags

# 查看编译统计
jcmd &amp;#x3C;pid&gt; Compiler.code_heap_info
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. GC 相关命令&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 打印堆统计信息
jcmd &amp;#x3C;pid&gt; GC.heap_info

# 执行一次 Full GC
jcmd &amp;#x3C;pid&gt; GC.run

# 打印类加载统计
jcmd &amp;#x3C;pid&gt; GC.classloader

# 查看垃圾收集器统计
jcmd &amp;#x3C;pid&gt; GC.gc_info
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 线程相关命令&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 打印线程堆栈
jcmd &amp;#x3C;pid&gt; Thread.print

# 打印线程堆栈（详细）
jcmd &amp;#x3C;pid&gt; Thread.print -l

# 生成线程 dump
jcmd &amp;#x3C;pid&gt; Thread.dump_to_file -format=json -dir /tmp /tmp/thread-dump.json
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. JFR 相关命令&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 列出 JFR 录制
jcmd &amp;#x3C;pid&gt; JFR.request

# 开始录制
jcmd &amp;#x3C;pid&gt; JFR.start name=recording duration=60s filename=/tmp/recording.jfr

# 停止录制
jcmd &amp;#x3C;pid&gt; JFR.stop name=recording

# 查看录制文件
jcmd &amp;#x3C;pid&gt; JFR.print name=recording filename=/tmp/recording.jfr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;jcmd 优势&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;统一的命令行接口&lt;/li&gt;
&lt;li&gt;支持交互式命令&lt;/li&gt;
&lt;li&gt;功能丰富，覆盖多种诊断场景&lt;/li&gt;
&lt;li&gt;比 jstack 等工具更强大和灵活&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;VisualVM&lt;/h2&gt;
&lt;p&gt;VisualVM 是 Oracle 推出的免费性能监控和故障诊断工具，集成了多种 JDK 监控功能。它提供直观的图形界面，支持堆转储分析、线程分析、性能监控等&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;功能特性&lt;/strong&gt;：&lt;/p&gt;
&lt;h3&gt;1. 实时监控&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;内存使用情况（堆内存、非堆内存）&lt;/li&gt;
&lt;li&gt;GC 行为和统计&lt;/li&gt;
&lt;li&gt;线程状态和活动&lt;/li&gt;
&lt;li&gt;类加载情况&lt;/li&gt;
&lt;li&gt;CPU 使用率&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 堆转储分析&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;可视化堆对象分布&lt;/li&gt;
&lt;li&gt;找出占用内存最多的对象&lt;/li&gt;
&lt;li&gt;分析对象引用关系&lt;/li&gt;
&lt;li&gt;识别内存泄漏&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 线程分析&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;查看所有线程&lt;/li&gt;
&lt;li&gt;分析线程阻塞情况&lt;/li&gt;
&lt;li&gt;查看死锁&lt;/li&gt;
&lt;li&gt;线程堆栈查看&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 插件系统&lt;/h3&gt;
&lt;p&gt;VisualVM 支持多种插件扩展功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BTrace&lt;/strong&gt;：运行时字节码修改和监控&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VisualGC&lt;/strong&gt;：详细的 GC 信息可视化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JFR Viewer&lt;/strong&gt;：Java Flight Recorder 文件查看&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;使用流程&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 启动 VisualVM
visualvm

# 或通过命令行连接
visualvm --openpid &amp;#x3C;pid&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常用操作&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;堆转储分析&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &quot;Snapshot&quot; 节点右键 -&gt; &quot;Heap Dump&quot; -&gt; &quot;Dump Heap&quot;&lt;/li&gt;
&lt;li&gt;打开 dump 文件进行分析&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程分析&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &quot;Threads&quot; 节点查看所有线程&lt;/li&gt;
&lt;li&gt;点击 &quot;Thread Dump&quot; 生成线程转储&lt;/li&gt;
&lt;li&gt;查找死锁和阻塞&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;性能监控&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &quot;Overview&quot; 视图查看实时指标&lt;/li&gt;
&lt;li&gt;设置阈值告警&lt;/li&gt;
&lt;li&gt;导出监控数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;jconsole&lt;/h2&gt;
&lt;p&gt;jconsole 是 JDK 自带的图形化监控工具，可以监控本地和远程 JVM 的性能指标&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;功能特性&lt;/strong&gt;：&lt;/p&gt;
&lt;h3&gt;1. 内存监控&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;堆内存使用情况&lt;/li&gt;
&lt;li&gt;非堆内存使用情况&lt;/li&gt;
&lt;li&gt;内存池详细信息&lt;/li&gt;
&lt;li&gt;对象内存统计&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 线程监控&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;当前线程数&lt;/li&gt;
&lt;li&gt;活跃线程数&lt;/li&gt;
&lt;li&gt;线程堆栈&lt;/li&gt;
&lt;li&gt;死锁检测&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 类加载监控&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;已加载类数量&lt;/li&gt;
&lt;li&gt;卸载类数量&lt;/li&gt;
&lt;li&gt;类加载器统计&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. MBeans 监控&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Java 管理扩展（JMX）&lt;/li&gt;
&lt;li&gt;自定义 MBean 监控&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;使用方法&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 启动 jconsole
jconsole

# 连接到远程 JVM
jconsole localhost:5005
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常用场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实时监控 JVM 性能指标&lt;/li&gt;
&lt;li&gt;查看线程状态和堆栈&lt;/li&gt;
&lt;li&gt;监控内存使用情况&lt;/li&gt;
&lt;li&gt;检查类加载情况&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Java Flight Recorder (JFR)&lt;/h2&gt;
&lt;p&gt;JFR 是 JDK 9 引入的低开销性能分析工具，能够在不显著影响应用性能的情况下收集详细的性能数据&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;低开销&lt;/strong&gt;：对应用性能影响小于 1%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事件驱动&lt;/strong&gt;：可自定义事件收集&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长时间运行&lt;/strong&gt;：支持长时间的记录和分析&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据丰富&lt;/strong&gt;：包含 GC、类加载、线程、编译等详细信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;使用方法&lt;/strong&gt;：&lt;/p&gt;
&lt;h3&gt;1. 启动 JVM 时启用 JFR&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java -XX:StartFlightRecording=duration=60s,filename=recording.jfr,settings=profile MyApplication
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 动态开始录制&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 开始录制
jcmd &amp;#x3C;pid&gt; JFR.start name=recording duration=60s filename=/tmp/recording.jfr

# 停止录制
jcmd &amp;#x3C;pid&gt; JFR.stop name=recording

# 列出录制
jcmd &amp;#x3C;pid&gt; JFR.request
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 使用 JDK Mission Control 分析&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 启动 JMC
jmc

# 打开 JFR 文件
# File -&gt; Open File -&gt; 选择 recording.jfr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常用配置&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 基础录制
-XX:StartFlightRecording=duration=10s,filename=recording.jfr

# 详细事件
-XX:StartFlightRecording=duration=60s,filename=recording.jfr,settings=profiling

# 记录到文件系统
-XX:StartFlightRecording=duration=60s,filename=/var/log/recording.jfr

# 自定义事件
-XX:StartFlightRecording=duration=60s,filename=recording.jfr,settings=default,jfr.settings=mysettings

# 排除特定事件
-XX:StartFlightRecording=duration=60s,filename=recording.jfr,settings=profile,jfr.event.exclude=gc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;JFR vs 其他工具对比&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;| 特性 | JFR | VisualVM | jstat |
|------|-----|----------|-------|
| 开销 | 极低 (&amp;#x3C;1%) | 较高 | 低 |
| 分析精度 | 高 | 中 | 低 |
| 长时间运行 | 支持 | 不支持 | 不支持 |
| 实时监控 | 不支持 | 支持 | 支持 |
| 事件追溯 | 支持 | 不支持 | 不支持 |&lt;/p&gt;
&lt;h1&gt;线上常见问题的定位与思路&lt;/h1&gt;
&lt;h2&gt;频繁GC或内存溢出&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;问题特征&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;应用响应变慢&lt;/li&gt;
&lt;li&gt;OOM 错误&lt;/li&gt;
&lt;li&gt;GC 日志频繁出现&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;分析步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;监控 GC 情况&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看详细 GC 日志
jstat -gc &amp;#x3C;pid&gt; 1000

# 查看原因
jstat -gccause &amp;#x3C;pid&gt; 1000

# 监控堆内存增长
jstat -gcutil &amp;#x3C;pid&gt; 1000
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;生成堆转储&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 生成堆转储（只包含活跃对象）
jmap -dump:live,format=b,file=heap.bin &amp;#x3C;pid&gt;

# 导出为文件
jmap -dump:format=b,file=/tmp/heap.hprof &amp;#x3C;pid&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分析堆转储&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 Eclipse Memory Analyzer (MAT)&lt;/li&gt;
&lt;li&gt;使用 VisualVM 的 Heap Dump 分析&lt;/li&gt;
&lt;li&gt;使用 jhat&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;定位问题&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查找大对象&lt;/li&gt;
&lt;li&gt;检查对象引用关系&lt;/li&gt;
&lt;li&gt;找出内存泄漏点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;示例分析流程&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 持续监控
while true; do jstat -gcutil &amp;#x3C;pid&gt; 1000; sleep 1; done

# 2. 发现内存持续增长
# 3. 生成堆转储
jmap -dump:live,format=b,file=/tmp/heap.bin &amp;#x3C;pid&gt;

# 4. 使用 MAT 分析
mat /tmp/heap.bin

# 5. 查看 Dominator Tree
# 找到占用内存最多的对象

# 6. 检查对象引用
# 分析对象的引用关系

# 7. 定位代码问题
# 检查代码中创建大量对象的地方
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;线程死锁问题&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;问题特征&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;应用卡住&lt;/li&gt;
&lt;li&gt;响应缓慢&lt;/li&gt;
&lt;li&gt;线程状态分析显示线程阻塞&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;分析步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;检查线程状态&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看线程堆栈
jstack &amp;#x3C;pid&gt; &gt; thread_dump.txt

# 查看线程堆栈（包含锁信息）
jstack -l &amp;#x3C;pid&gt; &gt; thread_dump_with_locks.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分析线程转储&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查找 &lt;code&gt;BLOCKED&lt;/code&gt; 状态的线程&lt;/li&gt;
&lt;li&gt;查找等待锁的线程&lt;/li&gt;
&lt;li&gt;分析锁的持有和等待关系&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;检测死锁&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 使用 -m 参数自动检测死锁
jstack -m &amp;#x3C;pid&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;死锁示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;Thread-1&quot; #10 prio=5 os_prio=0 tid=0x00007f8c8c0a5100 nid=0x4b03 runnable [0x000070000e2ff000]
   java.lang.Thread.State: RUNNABLE
	at com.example.ResourceA.methodA(ResourceA.java:15)
	- waiting to lock &amp;#x3C;0x0000000768c233a8&gt; (a java.lang.Object)
	- locked &amp;#x3C;0x0000000768c23368&gt; (a java.lang.Object)

&quot;Thread-2&quot; #11 prio=5 os_prio=0 tid=0x00007f8c8c0a5000 nid=0x4b02 waiting on condition [0x000070000e3ff000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on &amp;#x3C;0x0000000768c233a8&gt; (a java.lang.Object)
	at com.example.ResourceB.methodB(ResourceB.java:20)
	- locked &amp;#x3C;0x0000000768c233a8&gt; (a java.lang.Object)

Found one Java-level deadlock:
---------------------
Java stack information for the threads listed above:
---------------------
&quot;Thread-1&quot;:
        waiting to lock &amp;#x3C;0x0000000768c233a8&gt; (a java.lang.Object)
        which is held by &quot;Thread-2&quot;
&quot;Thread-2&quot;:
        waiting to lock &amp;#x3C;0x0000000768c23368&gt; (a java.lang.Object)
        which is held by &quot;Thread-1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优化锁的获取顺序&lt;/li&gt;
&lt;li&gt;使用锁超时机制&lt;/li&gt;
&lt;li&gt;使用可中断锁&lt;/li&gt;
&lt;li&gt;使用更细粒度的锁&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;CPU 占用过高&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;问题特征&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU 使用率异常高&lt;/li&gt;
&lt;li&gt;应用响应缓慢&lt;/li&gt;
&lt;li&gt;某些方法执行时间过长&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;分析步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看 CPU 使用率&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看进程 CPU 使用率
top -p &amp;#x3C;pid&gt;

# 查看线程 CPU 使用率
top -H -p &amp;#x3C;pid&gt;

# 查看特定时间段的 CPU 使用率
sar -p &amp;#x3C;pid&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分析线程状态&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看线程堆栈
jstack &amp;#x3C;pid&gt; &gt; thread_dump.txt

# 查看包含锁信息的堆栈
jstack -l &amp;#x3C;pid&gt; &gt; thread_dump_with_locks.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;识别问题线程&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;找到占用 CPU 的线程&lt;/li&gt;
&lt;li&gt;查看线程堆栈&lt;/li&gt;
&lt;li&gt;定位热点方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;生成 CPU profile&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 使用 jstack 生成线程转储
jstack &amp;#x3C;pid&gt; &gt; thread_dump.txt

# 使用 BTrace 定位热点
# （需要配置 BTrace 插件）
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;分析方法&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 找到 CPU 使用率最高的线程
top -H -p &amp;#x3C;pid&gt;

# 2. 转换线程 ID 为十六进制
printf &quot;%x\n&quot; &amp;#x3C;thread_id&gt;

# 3. 在线程转储中查找对应线程
grep -A 30 &quot;0x&amp;#x3C;hex_thread_id&gt;&quot; thread_dump.txt

# 4. 定位热点方法
# 分析线程堆栈中的方法调用

# 5. 优化代码
# 修改热点方法
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常见原因&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无限循环&lt;/li&gt;
&lt;li&gt;复杂算法&lt;/li&gt;
&lt;li&gt;网络超时等待&lt;/li&gt;
&lt;li&gt;锁竞争&lt;/li&gt;
&lt;li&gt;不必要的计算&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;内存泄漏&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;问题特征&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内存使用持续增长&lt;/li&gt;
&lt;li&gt;OOM 错误&lt;/li&gt;
&lt;li&gt;堆转储分析发现对象未被释放&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;分析步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;监控内存增长&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 持续监控 GC 统计
while true; do jstat -gc &amp;#x3C;pid&gt; 1000; sleep 1; done

# 查看内存池统计
jstat -gcutil &amp;#x3C;pid&gt; 1000

# 查看对象分布
jmap -histo:live &amp;#x3C;pid&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对比堆转储&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 生成初始堆转储
jmap -dump:live,format=b,file=heap_initial.bin &amp;#x3C;pid&gt;

# 运行一段时间后再次生成
sleep 300  # 5分钟
jmap -dump:live,format=b,file=heap_after.bin &amp;#x3C;pid&gt;

# 对比分析
mat heap_initial.bin heap_after.bin
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分析泄漏点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查找不再使用的对象&lt;/li&gt;
&lt;li&gt;检查对象引用关系&lt;/li&gt;
&lt;li&gt;找出生命周期长的对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;常见泄漏原因&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;静态集合类泄漏&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 问题代码
private static final Map&amp;#x3C;String, Object&gt; cache = new HashMap&amp;#x3C;&gt;();
// 集合中存储的对象永远不会被清理
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;监听器和回调未移除&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 问题代码
eventListener.addListener(new MyListener());
// MyListener 永远不会被移除
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ThreadLocal 泄漏&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 问题代码
ThreadLocal&amp;#x3C;Object&gt; local = new ThreadLocal&amp;#x3C;&gt;();
local.set(new Object());
// 如果线程复用，对象不会被回收
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据库连接未关闭&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 问题代码
Connection conn = dataSource.getConnection();
// 如果出现异常，连接可能不会被关闭
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用弱引用和软引用&lt;/li&gt;
&lt;li&gt;及时移除监听器&lt;/li&gt;
&lt;li&gt;正确管理 ThreadLocal&lt;/li&gt;
&lt;li&gt;使用连接池&lt;/li&gt;
&lt;li&gt;定期清理静态集合&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;响应时间过长&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;问题特征&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求响应变慢&lt;/li&gt;
&lt;li&gt;接口超时&lt;/li&gt;
&lt;li&gt;系统吞吐量下降&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;分析步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;监控性能指标&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 监控 GC
jstat -gcutil &amp;#x3C;pid&gt; 1000

# 监控线程
jstack &amp;#x3C;pid&gt; &gt; thread_dump.txt

# 监控系统资源
top -p &amp;#x3C;pid&gt;
iostat
vmstat
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分析线程状态&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看线程堆栈
jstack &amp;#x3C;pid&gt; &gt; thread_dump.txt

# 统计线程状态
grep &quot;java.lang.Thread.State:&quot; thread_dump.txt | sort | uniq -c
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分析网络延迟&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看 TCP 连接
netstat -anp | grep &amp;#x3C;pid&gt;

# 查看 HTTP 请求
# 检查应用日志
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分析数据库查询&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看数据库连接
# 检查慢查询日志
# 使用数据库监控工具
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;分析方法&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 生成线程转储
jstack &amp;#x3C;pid&gt; &gt; thread_dump.txt

# 2. 统计线程状态分布
grep &quot;java.lang.Thread.State:&quot; thread_dump.txt | sort | uniq -c | sort -rn

# 3. 查找长时间运行的线程
grep -A 30 &quot;RUNNABLE&quot; thread_dump.txt | grep -v &quot;^$&quot;

# 4. 查找阻塞的线程
grep &quot;BLOCKED&quot; thread_dump.txt

# 5. 检查线程池使用情况
# 查看是否有线程池满的情况

# 6. 分析热点方法
# 使用方法执行时间分析
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常见原因&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程池配置不当&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程池大小不合理&lt;/li&gt;
&lt;li&gt;队列大小设置过小&lt;/li&gt;
&lt;li&gt;线程被阻塞&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据库问题&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;慢查询&lt;/li&gt;
&lt;li&gt;连接池满&lt;/li&gt;
&lt;li&gt;索引不足&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;网络问题&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;超时设置不合理&lt;/li&gt;
&lt;li&gt;网络延迟&lt;/li&gt;
&lt;li&gt;资源竞争&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GC 问题&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full GC 频繁&lt;/li&gt;
&lt;li&gt;停顿时间长&lt;/li&gt;
&lt;li&gt;内存不足&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优化线程池配置&lt;/li&gt;
&lt;li&gt;优化数据库查询&lt;/li&gt;
&lt;li&gt;添加监控和告警&lt;/li&gt;
&lt;li&gt;使用异步处理&lt;/li&gt;
&lt;li&gt;优化 GC 配置&lt;/li&gt;
&lt;li&gt;使用缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;性能分析最佳实践&lt;/h1&gt;
&lt;h2&gt;问题识别流程&lt;/h2&gt;
&lt;h3&gt;1. 采集数据&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;监控指标&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU 使用率&lt;/li&gt;
&lt;li&gt;内存使用情况&lt;/li&gt;
&lt;li&gt;GC 统计信息&lt;/li&gt;
&lt;li&gt;线程状态&lt;/li&gt;
&lt;li&gt;系统资源&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;采集频率&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实时监控：1秒间隔&lt;/li&gt;
&lt;li&gt;堆转储：问题发生时&lt;/li&gt;
&lt;li&gt;线程转储：定期或触发时&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 数据分析&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;工具选择&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;jstat&lt;/code&gt;：快速查看 GC 情况&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jmap&lt;/code&gt;：生成堆转储分析&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jstack&lt;/code&gt;：查看线程状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jcmd&lt;/code&gt;：执行诊断命令&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VisualVM&lt;/code&gt;：可视化分析&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MAT&lt;/code&gt;：深度堆分析&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JFR&lt;/code&gt;：长时间性能分析&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 定位问题&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;分析方法&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;热点分析：找出 CPU 消耗高的方法&lt;/li&gt;
&lt;li&gt;内存分析：找出占用内存多的对象&lt;/li&gt;
&lt;li&gt;线程分析：找出阻塞或死锁的线程&lt;/li&gt;
&lt;li&gt;系统分析：找出系统资源的瓶颈&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 解决问题&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;优化策略&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码优化：优化算法和逻辑&lt;/li&gt;
&lt;li&gt;配置优化：调整 JVM 参数和系统配置&lt;/li&gt;
&lt;li&gt;架构优化：改进系统架构&lt;/li&gt;
&lt;li&gt;监控优化：增强监控和告警&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;数据采集策略&lt;/h2&gt;
&lt;h3&gt;1. 问题发生前&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;预防性监控&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;启用 GC 日志&lt;/li&gt;
&lt;li&gt;设置内存监控&lt;/li&gt;
&lt;li&gt;记录性能基线&lt;/li&gt;
&lt;li&gt;建立告警规则&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 问题发生时&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;紧急采集&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生成堆转储&lt;/li&gt;
&lt;li&gt;生成线程转储&lt;/li&gt;
&lt;li&gt;记录时间戳&lt;/li&gt;
&lt;li&gt;记录环境信息&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 问题解决后&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;验证和优化&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;验证问题已解决&lt;/li&gt;
&lt;li&gt;重新采集性能数据&lt;/li&gt;
&lt;li&gt;对比优化前后的数据&lt;/li&gt;
&lt;li&gt;更新监控配置&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;工具选择指南&lt;/h2&gt;
&lt;p&gt;| 问题类型 | 推荐工具 | 优先级 |
|---------|---------|--------|
| &lt;strong&gt;内存泄漏&lt;/strong&gt; | jmap + MAT, VisualVM | 高 |
| &lt;strong&gt;频繁 GC&lt;/strong&gt; | jstat, jcmd | 高 |
| &lt;strong&gt;线程死锁&lt;/strong&gt; | jstack, jcmd | 高 |
| &lt;strong&gt;CPU 高&lt;/strong&gt; | jstack, jstack -l | 高 |
| &lt;strong&gt;性能瓶颈&lt;/strong&gt; | VisualVM, JFR | 中 |
| &lt;strong&gt;长时间运行&lt;/strong&gt; | JFR | 高 |&lt;/p&gt;
&lt;h2&gt;自动化分析脚本&lt;/h2&gt;
&lt;h3&gt;1. GC 分析脚本&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
# gc_analysis.sh - GC 问题快速分析脚本

PID=$1
INTERVAL=${2:-1000}

echo &quot;开始监控 PID: $PID, 间隔: ${INTERVAL}ms&quot;

while true; do
    clear
    echo &quot;=== GC 监控 ===&quot;
    echo &quot;时间: $(date)&quot;
    echo &quot;&quot;
    jstat -gcutil $PID $INTERVAL | tail -1
    echo &quot;&quot;
    echo &quot;最近 GC 原因:&quot;
    jstat -gccause $PID $INTERVAL | tail -1
    echo &quot;&quot;
    echo &quot;按 Ctrl+C 停止&quot;
    sleep 1
done
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 堆转储分析脚本&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
# heap_dump.sh - 堆转储生成和分析脚本

PID=$1
OUTPUT_DIR=&quot;/tmp/heap_dumps&quot;
OUTPUT_FILE=&quot;$OUTPUT_DIR/heap_$(date +%Y%m%d_%H%M%S).hprof&quot;

# 创建输出目录
mkdir -p $OUTPUT_DIR

echo &quot;生成堆转储: $OUTPUT_FILE&quot;
jmap -dump:format=b,file=$OUTPUT_FILE $PID

if [ $? -eq 0 ]; then
    echo &quot;堆转储生成成功&quot;
    echo &quot;使用以下命令分析:&quot;
    echo &quot;  jhat $OUTPUT_FILE&quot;
    echo &quot;  或使用 VisualVM: visualvm --open $OUTPUT_FILE&quot;
else
    echo &quot;堆转储生成失败&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 线程分析脚本&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
# thread_analysis.sh - 线程转储生成和分析脚本

PID=$1
OUTPUT_DIR=&quot;/tmp/thread_dumps&quot;
OUTPUT_FILE=&quot;$OUTPUT_DIR/thread_$(date +%Y%m%d_%H%M%S).txt&quot;

# 创建输出目录
mkdir -p $OUTPUT_DIR

echo &quot;生成线程转储: $OUTPUT_FILE&quot;
jstack -l $PID &gt; $OUTPUT_FILE

if [ $? -eq 0 ]; then
    echo &quot;线程转储生成成功&quot;
    echo &quot;统计线程状态:&quot;
    grep &quot;java.lang.Thread.State:&quot; $OUTPUT_FILE | sort | uniq -c | sort -rn
    echo &quot;&quot;
    echo &quot;按 Ctrl+C 停止&quot;
    read -p &quot;按 Enter 键生成下一次转储...&quot;

    # 清屏并继续
    clear
else
    echo &quot;线程转储生成失败&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 系统监控脚本&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
# system_monitor.sh - 系统资源监控脚本

PID=$1
INTERVAL=${2:-1}

echo &quot;监控 PID: $PID&quot;
echo &quot;按 Ctrl+C 停止&quot;

while true; do
    clear
    echo &quot;=== 系统监控 ===&quot;
    echo &quot;时间: $(date)&quot;
    echo &quot;&quot;
    
    # CPU 使用率
    CPU=$(top -p $PID -n 1 -b | grep $PID | awk &apos;{print $9}&apos;)
    echo &quot;CPU 使用率: ${CPU}%&quot;
    
    # 内存使用
    MEM=$(top -p $PID -n 1 -b | grep $PID | awk &apos;{print $6}&apos;)
    echo &quot;内存使用: ${MEM} KB&quot;
    
    # 线程数
    THREADS=$(jstack $PID | grep -c &quot;^&quot;)
    echo &quot;线程数: $THREADS&quot;
    
    # 堆内存
    HEAP=$(jstat -gcutil $PID 1000 | tail -1 | awk &apos;{print $4}&apos;)
    echo &quot;堆内存使用: ${HEAP}%&quot;
    
    echo &quot;&quot;
    echo &quot;按 Ctrl+C 停止&quot;
    sleep $INTERVAL
done
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 一键诊断脚本&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
# diagnose.sh - 一键问题诊断脚本

PID=$1

if [ -z &quot;$PID&quot; ]; then
    echo &quot;用法: $0 &amp;#x3C;pid&gt;&quot;
    echo &quot;示例: $0 12345&quot;
    exit 1
fi

# 检查进程是否存在
if ! ps -p $PID &gt; /dev/null; then
    echo &quot;进程 $PID 不存在&quot;
    exit 1
fi

echo &quot;开始诊断 PID: $PID&quot;
echo &quot;时间: $(date)&quot;
echo &quot;&quot;

# 1. 基本信息检查
echo &quot;=== 基本信息检查 ===&quot;
jps -l $PID
echo &quot;&quot;

# 2. 系统资源监控
echo &quot;=== 系统资源监控 ===&quot;
top -p $PID -n 1 | grep $PID
echo &quot;&quot;

# 3. JVM 参数
echo &quot;=== JVM 参数 ===&quot;
jinfo -flags $PID | grep -E &quot;MaxHeapSize|MaxMetaspaceSize|GC&quot;
echo &quot;&quot;

# 4. GC 统计
echo &quot;=== GC 统计 ===&quot;
jstat -gcutil $PID 1 10
echo &quot;&quot;

# 5. GC 原因
echo &quot;=== GC 原因 ===&quot;
jstat -gccause $PID 1 5
echo &quot;&quot;

# 6. 线程状态统计
echo &quot;=== 线程状态统计 ===&quot;
jstack $PID | grep &quot;java.lang.Thread.State:&quot; | sort | uniq -c | sort -rn | head -10
echo &quot;&quot;

# 7. 对象分布（仅活跃对象）
echo &quot;=== 对象分布（前10） ===&quot;
jmap -histo:live $PID | head -11
echo &quot;&quot;

# 8. 堆内存信息
echo &quot;=== 堆内存信息 ===&quot;
jmap -heap $PID
echo &quot;&quot;

# 9. 类加载统计
echo &quot;=== 类加载统计 ===&quot;
jcmd $PID GC.classloader
echo &quot;&quot;

echo &quot;诊断完成&quot;
echo &quot;建议操作:&quot;
echo &quot;  - 如果 CPU 高，查看 jstack 输出&quot;
echo &quot;  - 如果内存高，生成堆转储: jmap -dump:live,format=b,file=heap.bin $PID&quot;
echo &quot;  - 如果 GC 频繁，调整 JVM 参数&quot;
echo &quot;  - 如果有死锁，查看 jstack -m 输出&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;常见问题速查表&lt;/h1&gt;
&lt;h2&gt;工具速查&lt;/h2&gt;
&lt;p&gt;| 问题 | 命令 | 说明 |
|------|------|------|
| 查看 JVM 参数 | &lt;code&gt;jinfo &amp;#x3C;pid&gt;&lt;/code&gt; | 查看所有 JVM 参数 |
| 查看 GC 情况 | &lt;code&gt;jstat -gcutil &amp;#x3C;pid&gt;&lt;/code&gt; | 查看 GC 统计 |
| 查看 GC 原因 | &lt;code&gt;jstat -gccause &amp;#x3C;pid&gt;&lt;/code&gt; | 查看 GC 原因 |
| 生成堆转储 | &lt;code&gt;jmap -dump:live,format=b,file=heap.bin &amp;#x3C;pid&gt;&lt;/code&gt; | 生成二进制堆转储 |
| 查看对象分布 | &lt;code&gt;jmap -histo:live &amp;#x3C;pid&gt;&lt;/code&gt; | 查看对象统计 |
| 查看线程堆栈 | &lt;code&gt;jstack &amp;#x3C;pid&gt;&lt;/code&gt; | 生成线程转储 |
| 查看线程堆栈（详细） | &lt;code&gt;jstack -l &amp;#x3C;pid&gt;&lt;/code&gt; | 包含锁信息 |
| 查看线程状态 | &lt;code&gt;jcmd &amp;#x3C;pid&gt; Thread.print&lt;/code&gt; | 详细线程信息 |
| 查看 GC 堆信息 | &lt;code&gt;jcmd &amp;#x3C;pid&gt; GC.heap_info&lt;/code&gt; | 堆统计信息 |
| 启动 JFR | &lt;code&gt;jcmd &amp;#x3C;pid&gt; JFR.start name=recording&lt;/code&gt; | 开始性能记录 |&lt;/p&gt;
&lt;h2&gt;性能指标&lt;/h2&gt;
&lt;h3&gt;健康指标&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU&lt;/strong&gt;: &amp;#x3C; 70%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;堆内存&lt;/strong&gt;: &amp;#x3C; 80%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GC 频率&lt;/strong&gt;: Full GC &amp;#x3C; 1次/小时&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;停顿时间&lt;/strong&gt;: &amp;#x3C; 200ms&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;警告指标&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU&lt;/strong&gt;: &gt; 70%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;堆内存&lt;/strong&gt;: &gt; 80%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full GC&lt;/strong&gt;: &gt; 1次/小时&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;停顿时间&lt;/strong&gt;: &gt; 200ms&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;危险指标&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU&lt;/strong&gt;: &gt; 90%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;堆内存&lt;/strong&gt;: &gt; 90%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full GC&lt;/strong&gt;: &gt; 3次/小时&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;停顿时间&lt;/strong&gt;: &gt; 500ms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OOM&lt;/strong&gt;: 发生内存溢出&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考资源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/tech-notes/guides/troubleshoot/&quot;&gt;Oracle JDK 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://visualvm.github.io/&quot;&gt;VisualVM 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/gctuning/jfr-monitoring.html&quot;&gt;Java Flight Recorder 指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html&quot;&gt;Java 性能调优指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/tools/java.html&quot;&gt;JDK 诊断工具文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Markdown介绍及其语法</title><link>https://juzzi.qzz.io/blog/lang/md/markdown-introduce-and-syntax</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/lang/md/markdown-introduce-and-syntax</guid><description>MarkDown是目前最流行的文本标记语言</description><pubDate>Thu, 20 Jul 2017 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;官网：&lt;a href=&quot;https://daringfireball.net/projects/markdown&quot;&gt;https://daringfireball.net/projects/markdown&lt;/a&gt;
Markdown是一种可以使用普通文本编辑器编写的标记语言，通过简单的标记语法，它可以使普通文本内容具有一定的格式。Markdown具有一系列衍生版本，用于扩展Markdown的功能（如表格、脚注、内嵌HTML等等），它们能让Markdown转换成更多的格式，例如LaTeX，Docbook。&lt;/p&gt;
&lt;h2&gt;用途&lt;/h2&gt;
&lt;p&gt;Markdown的语法简洁明了、学习容易，而且功能比纯文本更强，因此有很多人用它写博客。世界上最流行的博客平台WordPress和大型CMS如Joomla、Drupal都能很好的支持Markdown。完全采用Markdown编辑器的博客平台有Ghost和Typecho。&lt;/p&gt;
&lt;h2&gt;编辑器&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Typora&lt;/li&gt;
&lt;li&gt;VSCode&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;语法&lt;/h2&gt;
&lt;p&gt;说明：每种语法后都必须添加&lt;strong&gt;空格&lt;/strong&gt;才能生效&lt;/p&gt;
&lt;h3&gt;标题&lt;/h3&gt;
&lt;p&gt;Markdown 支持两种标题的语法，类 Setext 和类 atx 形式。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;类 Setext 形式是用底线的形式，利用 = （最高阶标题）和 - （第二阶标题），例如&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;This is an H1
=============
This is an H2
-------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;任何数量的 = 和 - 都可以有效果。2. 行首插入1-6个#，每增加一个#表示更深入层次的内容，对应到标题的深度由 1-6 阶，其中Header1为首层，字体最大。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 这是 H1 #
## 这是 H2 ##
### 这是 H3 ###
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;文本样式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;加粗：&lt;code&gt;**Text**&lt;/code&gt; （快捷键：Ctrl+B）
&lt;strong&gt;Text&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;斜体：&lt;code&gt;*Text*&lt;/code&gt; （快捷键：Ctrl+I）
&lt;em&gt;Text&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;删除线：&lt;code&gt;~~Text~~&lt;/code&gt; （快捷键：Ctrl+ALT+U）
~~Text~~&lt;/li&gt;
&lt;li&gt;换行符 : 一行结束时输入两个回车，也可以使用&lt;code&gt;&amp;#x3C;br&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;列表&lt;/h3&gt;
&lt;p&gt;Markdown支持有序列表和无序列表。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有序列表：使用数字接着一个英文句点（并有个空格）：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;1. one is...
2. two is...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果可参考本段内容xD&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无序列表：使用星号、加号或是减号作为列表标记（任意一个即可）：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;* i love you
+ i love you very much
- i love you more
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;i love you&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;i love you very much&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;i love you more&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;链接&lt;/h3&gt;
&lt;p&gt;Markdown 支持两种形式的链接语法：行内式和参考式两种形式。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;行内式&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;要建立一个行内式的链接，只要在方块括号后面紧接着圆括号并插入网址链接即可，如果你还想要加上链接的 title 文字，只要在网址后面，用双引号把 title 文字包起来即可，例如：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;[My Blog1](https://icoding.pw/ &quot;This link is my blog.&quot;)
[My Blog2](https://icoding.pw/)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://icoding.pw/&quot; title=&quot;This link is my blog.&quot;&gt;My Blog1&lt;/a&gt; |
&lt;a href=&quot;https://icoding.pw/&quot;&gt;My Blog2&lt;/a&gt;
鼠标放在链接上，你就会发现不同。xD&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你是要链接到同样主机的资源，你可以使用相对路径：
&lt;code&gt;[这里](/posts/markdown_introduce_and_syntax/)也是这篇文章的地址&lt;/code&gt;
[这里]也是这篇文章的地址&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;参考式
参考式的链接是在链接文字的括号后面再接上另一个方括号，而在第二个方括号里面要填入用以辨识链接的标记：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;这是[My Blog][1]的链接地址
[1]: https://icoding.pw/  &quot;This link is my blog.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是&lt;a href=&quot;https://icoding.pw/&quot; title=&quot;This link is my blog.&quot;&gt;My Blog&lt;/a&gt;的链接地址&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;直接链接
&lt;code&gt;&amp;#x3C;https://icoding.pw&gt;是我的博客地址&lt;/code&gt;
&lt;a href=&quot;https://icoding.pw&quot;&gt;https://icoding.pw&lt;/a&gt;是我的博客地址&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;图片&lt;/h3&gt;
&lt;p&gt;跟行内式链接类似，只需要在最前面添加&lt;code&gt;!&lt;/code&gt;符号即可，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![pic](https://gitee.com/juzzi/res/raw/master/pic/2019/07/test/touxiang.jpg)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分隔线&lt;/h3&gt;
&lt;p&gt;你可以在一行中用三个以上的星号、减号、底线来建立一个分隔线，行内不能有其他东西。你也可以在星号或是减号中间插入空格。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* * *
***
- - -
---------------------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;区块引用&lt;/h3&gt;
&lt;p&gt;Markdown 标记区块引用是使用类似 email 中用 &gt; 的引用方式，Markdown 也允许你偷懒只在整个段落的第一行最前面加上 &gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&gt; No one can call back yesterday, yesterday will not be called again.No one can call back yesterday, yesterday will not be called again.
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;No one can call back yesterday, yesterday will not be called again.No one can call back yesterday, yesterday will not be called again.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;注释&lt;/h3&gt;
&lt;p&gt;注释分为单行注释与多行注释两种&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;单行注释：`单行注释`
&lt;code&gt;单行注释&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;多行注释：
```
多行注释
```&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;多行注释
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;表格&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;|表头1  |表头2  |
|----   | ----  |
|单元格 |单元格 |
|单元格 |单元格 |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表格的对齐方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-: 居右对齐&lt;/li&gt;
&lt;li&gt;:- 居左对齐&lt;/li&gt;
&lt;li&gt;:-: 居中对齐。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Jekyll搭建个人博客</title><link>https://juzzi.qzz.io/blog/lang/other/jekyll-build-personal-blog</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/lang/other/jekyll-build-personal-blog</guid><description>0成本搭建博客</description><pubDate>Wed, 19 Jul 2017 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Ruby环境安装&lt;/h2&gt;
&lt;p&gt;Jekyll是用Ruby语言实现的，要使用Jekyll我们首先就需要先安装Ruby。&lt;/p&gt;
&lt;h3&gt;Linux系统&lt;/h3&gt;
&lt;p&gt;由于jekyll依赖ruby开发环境，并且要求ruby版本&gt;=2.1，因此无法使用centos yum安装。根据&lt;a href=&quot;https://rvm.io/&quot;&gt;RVM官网&lt;/a&gt;的指引，使用RVM安装Ruby环境的步骤如下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装RVM&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;下载签名
&lt;code&gt;gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;下载文件
&lt;code&gt;\curl -sSL https://get.rvm.io | bash -s stable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;执行文件
&lt;code&gt;source /home/ljx/.rvm/scripts/rvm&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;查看可安装的ruby版本
&lt;code&gt;rvm list known|grep ruby&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装指定的ruby版本
&lt;code&gt;rvm install 2.5&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试
&lt;code&gt;ruby -v&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;更新ruby源
&lt;code&gt;gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看当前ruby源
&lt;code&gt;gem sources -l&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Windows系统&lt;/h3&gt;
&lt;p&gt;下载&lt;a href=&quot;https://rubyinstaller.org/&quot;&gt;RubyInstaller&lt;/a&gt;，版本需要选带Devkit的版本，例如：&lt;a href=&quot;https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.5.5-1/rubyinstaller-devkit-2.5.5-1-x64.exe&quot;&gt;Ruby+Devkit 2.5.5&lt;/a&gt;，因为安装完以后，使用bundle安装gem使需要Devkit带的编译环境才行。安装过程直接按照默认的设置，直接点下一步即可，这里不再赘述。&lt;/p&gt;
&lt;h3&gt;Mac环境&lt;/h3&gt;
&lt;p&gt;虽然Mac系统自带了ruby环境，但是不建议使用，详情可以参考&lt;a href=&quot;https://www.moncefbelyamani.com/why-you-shouldn-t-use-the-system-ruby-to-install-gems-on-a-mac/&quot;&gt;这篇文章&lt;/a&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装HomeBrew（已安装可以跳过）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;安装chruby和ruby-install&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;brew install chruby ruby-install xz
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;安装ruby
选择适合你的ruby版本，并使用命令：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ruby-install ruby 3.1.3
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;添加到环境变量&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &quot;source $(brew --prefix)/opt/chruby/share/chruby/chruby.sh&quot; &gt;&gt; ~/.zshrc
echo &quot;source $(brew --prefix)/opt/chruby/share/chruby/auto.sh&quot; &gt;&gt; ~/.zshrc
echo &quot;chruby ruby-3.1.3&quot; &gt;&gt; ~/.zshrc # run &apos;chruby&apos; to see actual version
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://jekyllrb.com/docs/installation/macos/&quot;&gt;https://jekyllrb.com/docs/installation/macos/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;jekyll安装&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;gem install jekyll bundler&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;测试是否安装成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\Administrator&gt;jekyll -v
jekyll 3.8.6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若打印出了jekyll的版本，则代表jekyll已经安装成功了。&lt;/p&gt;
&lt;p&gt;若遇到报错：&lt;code&gt;Could not find public_suffix-4.0.6 in any of the sources...&lt;/code&gt;，则执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gem update
bundler install
bundler update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样我们的jekyll就安装完毕，就可以选择一个模板开始写博客了，最后再上传到github就搞定了。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Linux常用设置</title><link>https://juzzi.qzz.io/blog/os/linux/linux-setting</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/os/linux/linux-setting</guid><description>个人总结的Linux的一些常用设置。</description><pubDate>Sat, 06 May 2017 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;通用&lt;/h2&gt;
&lt;h3&gt;history记录命令时间&lt;/h3&gt;
&lt;p&gt;编辑文件&lt;code&gt;/etc/profile&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;记录日期：&lt;code&gt;export HISTTIMEFORMAT=&apos;%F %T &apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;生效文件：&lt;code&gt;source /etc/profile&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;文件打开数&lt;/h3&gt;
&lt;p&gt;Linux默认的文件打开数为1024，若作为服务器机器，该设置是不够的，需要将其提高。&lt;/p&gt;
&lt;p&gt;编辑文件&lt;code&gt;/etc/security/limits.conf&lt;/code&gt;，添加或修改以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* soft nofile 65535
* hard nofile 65535
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即可将最大文件打开数修改为65535。&lt;/p&gt;
&lt;h2&gt;Ubuntu&lt;/h2&gt;
&lt;h3&gt;修改apt源&lt;/h3&gt;
&lt;p&gt;Ubuntu默认的apt源较慢，可将其更换为阿里云apt源：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;备份旧源：&lt;code&gt;sudo mv /etc/apt/sources.list /etc/apt/sources.list_bak&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;新建文件/etc/apt/sources.list，并添加&lt;a href=&quot;https://developer.aliyun.com/mirror/ubuntu&quot;&gt;阿里云apt&lt;/a&gt;与系统版本匹配的内容&lt;/li&gt;
&lt;li&gt;更新apt缓存：&lt;code&gt;sudo apt update&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;解决vi编辑器中箭头变ABCD&lt;/h3&gt;
&lt;p&gt;更新vi：&lt;code&gt;sudo apt install vim -y&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;CentOS&lt;/h2&gt;
&lt;h3&gt;修改yum源&lt;/h3&gt;
&lt;p&gt;Centos默认的yum源较慢，可将其更换为阿里云yun源：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;备份旧源:&lt;code&gt;mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;下载新的源文件:&lt;code&gt;wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo&lt;/code&gt; （可将7换成6、5等等，根据系统版本而定）&lt;/li&gt;
&lt;li&gt;下载EPEL源:&lt;code&gt;wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;重新生成缓存：&lt;code&gt;yum clean all&lt;/code&gt;和&lt;code&gt;yum makecache&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;屏蔽slice日志&lt;/h3&gt;
&lt;p&gt;CentOS会每分钟记录用户slice日志到/var/log/messages中，会产生大量slice日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Jan 19 14:10:01 VM-0-11-centos systemd: Created slice User Slice of xxx.
Jan 19 14:10:01 VM-0-11-centos systemd: Started Session 12699 of user xxx.
Jan 19 14:10:01 VM-0-11-centos systemd: Started Session 12700 of user root.
Jan 19 14:10:01 VM-0-11-centos systemd: Removed slice User Slice of xxx.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改日志等级
systemd默认日志等级为info，修改为notice：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用命令：&lt;code&gt;systemd-analyze set-log-level notice&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;修改配置文件：&lt;code&gt;/etc/systemd/system.conf&lt;/code&gt;将&lt;code&gt;LogLevel=info&lt;/code&gt;修改为&lt;code&gt;LogLevel=notice&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;将系统日志等级设置为notice，从而屏蔽上述日志。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加过滤规则&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;编辑文件&lt;code&gt;/etc/rsyslog.d/ignore-systemd-session-slice.conf&lt;/code&gt;，添加内容：&lt;code&gt;if $programname == &quot;systemd&quot; and ($msg contains &quot;Starting Session&quot; or $msg contains &quot;Started Session&quot; or $msg contains &quot;Created slice&quot; or $msg contains &quot;Starting user-&quot; or $msg contains &quot;Removed Slice&quot; or $msg contains &quot;Stopping user-&quot;) then stop&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;重启rsyslog：&lt;code&gt;systemctl restart rsyslog&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从而通过该过滤规则过滤掉上述日志。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Windows Shell——Batch(批处理)</title><link>https://juzzi.qzz.io/blog/lang/shell/batch</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/lang/shell/batch</guid><description>batch的基本语法</description><pubDate>Tue, 27 Sep 2016 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;变量&lt;/h2&gt;
&lt;h3&gt;定义变量&lt;/h3&gt;
&lt;p&gt;语法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-batch&quot;&gt;set VARIABLE=value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：“=”左右不能有空格&lt;/p&gt;
&lt;h3&gt;取变量值&lt;/h3&gt;
&lt;p&gt;语法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-batch&quot;&gt;%VARIABLE%
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;取输入变量&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;提取第i个命令选项：&lt;code&gt;%i&lt;/code&gt; 例如%1提取第1个option，i可以取值从1到9&lt;/li&gt;
&lt;li&gt;取文件名（名+扩展名）：&lt;code&gt;%~0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;取全路径：&lt;code&gt;%~f0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;取驱动器名：&lt;code&gt;%~d0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;只取路径（不包括驱动器）：&lt;code&gt;%~p0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;只取文件名：&lt;code&gt;%~n0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;只取文件扩展名：&lt;code&gt;%~x0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;取缩写全路径名：&lt;code&gt;%~s0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;取文件属性：&lt;code&gt;%~a0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;取文件创建时间：&lt;code&gt;%~t0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;取文件大小：&lt;code&gt;%~z0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上选项可以组合起来使用。&lt;/p&gt;
&lt;h2&gt;循环&lt;/h2&gt;
&lt;h3&gt;for循环（集合）&lt;/h3&gt;
&lt;p&gt;语法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for %%i in &amp;#x3C;collection&gt; do (
    &amp;#x3C;statement&gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;collection为要循环的值的集合，例如：&lt;code&gt;(2, 1, 8)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%%i&lt;/code&gt;为定义的循环变量，变量名不能超过1位（例如&lt;code&gt;%%ab&lt;/code&gt;是&lt;strong&gt;错的&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%%i&lt;/code&gt;取值时要用&lt;code&gt;%%i%&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;for循环（范围）&lt;/h3&gt;
&lt;p&gt;/l参数代表以增量形式从开始到结束的一个数字序列，语法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for /l %%i in (&amp;#x3C;start&gt;, &amp;#x3C;step&gt;, &amp;#x3C;end&gt;) do (
    &amp;#x3C;statement&gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;start&lt;/code&gt;为起始值（包含）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;step&lt;/code&gt;为步长&lt;/li&gt;
&lt;li&gt;&lt;code&gt;end&lt;/code&gt;为终止值（若能取到则包含）&lt;/li&gt;
&lt;li&gt;例：&lt;code&gt;(10, 2, 20)&lt;/code&gt;的取值为&lt;code&gt;10, 12, 14, 16, 18, 20&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Python语法</title><link>https://juzzi.qzz.io/blog/lang/python/python-syntax</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/lang/python/python-syntax</guid><description>基础语法规则</description><pubDate>Sat, 02 Jul 2016 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;条件&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;if &amp;#x3C;condition1&gt;:
    &amp;#x3C;statement1&gt;
elif condition2:
    &amp;#x3C;statement2&gt;
else:
    &amp;#x3C;statement3&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;条件语句中可以没有elif，也可以没有else，但必须要有if。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：Python没有switch-case，可用dict结构来实现类似switch-case的功能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;循环&lt;/h2&gt;
&lt;h3&gt;条件循环&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;while &amp;#x3C;condition&gt;:
    &amp;#x3C;statement&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;遍历&lt;/h2&gt;
&lt;h3&gt;集合遍历&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;for &amp;#x3C;variable&gt; in &amp;#x3C;collection&gt;:
    &amp;#x3C;statement&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;步长遍历&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;for i in range(x, y):
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;i包含x，不包含y&lt;/p&gt;
&lt;h2&gt;集合&lt;/h2&gt;
&lt;h3&gt;list&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义List：&lt;code&gt;my_list = []&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;list求和：&lt;code&gt;sum(my_list)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;统计值为value的下标：&lt;code&gt;[i for i, x in enumerate(a) if x == value]&lt;/code&gt;（下标从0开始）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;dict&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义dict：&lt;code&gt;variable = {}&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;遍历dict的key和value&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (k, v) in a.items():
    &amp;#x3C;statement&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;反转key和value：&lt;code&gt;{v: k for k, v in &amp;#x3C;collection&gt;.items()}&lt;/code&gt;（原来重复的value会被过滤掉）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;异常&lt;/h2&gt;
&lt;h3&gt;常见异常&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;文件不存在异常：FileNotFoundError&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;中断信号异常（Ctrl-C）：KeyboardInterrupt（继承于BaseException）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;捕获异常&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;try:
    &amp;#x3C;statement1&gt;
except &amp;#x3C;exception_type&gt; as e:
    &amp;#x3C;statement2&gt;
finally:
    &amp;#x3C;statement3&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;打印堆栈&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;traceback.print_exc()&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;必须要有异常才能用traceback.print_exc()打印出来，否则要用traceback.print_stack()打印。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;文件&lt;/h2&gt;
&lt;h3&gt;读文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;with open(&amp;#x3C;file_path&gt;, &apos;rb&apos;) as f:
    &amp;#x3C;statement&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完成后会自动释放文件。&lt;/p&gt;
&lt;h3&gt;写文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;with open(&amp;#x3C;file_path&gt;, &apos;wb&apos;) as f:
    &amp;#x3C;statement&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完成后会自动释放文件。&lt;/p&gt;
&lt;h3&gt;常用文件方法&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;检查文件是否存在：&lt;code&gt;os.path.exists(&amp;#x3C;file_path&gt;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建文件夹：&lt;code&gt;os.mkdir(&amp;#x3C;dir_path&gt;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建多层文件夹：&lt;code&gt;os.makedirs(&amp;#x3C;dir_path&gt;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除文件：&lt;code&gt;os.remove(&amp;#x3C;file_path&gt;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除文件夹（必须为空）：&lt;code&gt;os.rmdir(&amp;#x3C;dir_path&gt;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;进程&lt;/h2&gt;
&lt;h3&gt;子进程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 创建子进程
p = multiprocessing.Process(target=&amp;#x3C;task_function&gt;, args=(&amp;#x3C;arg1&gt;, &amp;#x3C;arg2&gt;...))
# 启动子进程
p.start()
# 等待子进程执行完毕
p.join()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若不执行p.join，则主进程会立即返回，不会等待子进程执行。&lt;/p&gt;
&lt;h3&gt;进程池&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 创建进程池
pool = multiprocessing.Pool(&amp;#x3C;process_num&gt;)
# 分配任务
result = pool.map(&amp;#x3C;task_function&gt;, &amp;#x3C;task_datas&gt;)
pool.close()
# 等待进程池中的进程全部执行完毕
pool.join()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;若进程池任务有涉及随机数，需要设置不同的随机种子，否则所有进程会使用相同的随机种子（启动时间）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;时间&lt;/h2&gt;
&lt;p&gt;需要先导入时间模块：&lt;code&gt;import time&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;时间戳&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;秒：&lt;code&gt;time.time()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;毫秒：&lt;code&gt;round(time.time() * 1000)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;时间字符串&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;local_time = time.localtime()
time.strftime(&quot;%Y-%m-%d %H:%M:%S&quot;, local_time)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认只能获取到秒，若需要毫秒，需要做以下转换：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def get_time():
    timestamp = time.time()
    timestamp_str = str(timestamp)
    milli_second_index = timestamp_str.index(&quot;.&quot;)
    milli_second = timestamp_str[
        milli_second_index + 1 : milli_second_index + 4  # noqa: E203
    ]
    local_time = time.localtime(timestamp)
    time_str = time.strftime(&quot;%Y-%m-%d %H:%M:%S&quot;, local_time)
    return f&quot;{time_str}.{milli_second}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;随机&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;随机浮点数[0, 1)：&lt;code&gt;random.random()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;随机浮点数[a, b]：&lt;code&gt;random.uniform(a, b)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;随机整数[a, b]：&lt;code&gt;random.randint(a, b)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从序列中随机：&lt;code&gt;random.choice(&amp;#x3C;sequence&gt;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;随机打乱列表顺序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test_list = [&quot;abc&quot;, &quot;bcd&quot;]
random.shuffle(test_list)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Numpy&lt;/h2&gt;
&lt;h3&gt;构造矩阵（numpy.ndarray）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;用list构造：&lt;code&gt;np.array(&amp;#x3C;list&gt;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;起止范围构造：&lt;code&gt;np.arange(&amp;#x3C;start&gt;, &amp;#x3C;stop&gt;[, &amp;#x3C;step&gt;])&lt;/code&gt;，包括start，不包括stop，step默认为1&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Python环境安装</title><link>https://juzzi.qzz.io/blog/lang/python/python-environment-install</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/lang/python/python-environment-install</guid><description>安装方法及注意事项</description><pubDate>Fri, 20 May 2016 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;直接安装Python&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Ubuntu&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装python3：&lt;code&gt;sudo apt install python3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;安装pip3：&lt;code&gt;sudo apt install python3-pip&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Linux源码安装（CentOS）&lt;/p&gt;
&lt;p&gt;由于Python3.7+要求openssl 1.0.2+，而centos 6的openssl的版本是1.0.1，因此最高能安装的Python版本为3.6，否则就只能升级openssl版本。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;下载源码：&lt;a href=&quot;https://www.python.org/downloads/source/&quot;&gt;https://www.python.org/downloads/source/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;解压：&lt;code&gt;tar zxvf xxx.tgz&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;编译：&lt;code&gt;make&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;安装：&lt;code&gt;make install&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;安装完成后，python3会出现在/usr/local/bin/python3&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Windows&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;下载安装包：&lt;a href=&quot;https://www.python.org/downloads/windows/&quot;&gt;https://www.python.org/downloads/windows/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;根据安装向导指示安装&lt;/li&gt;
&lt;li&gt;安装pip：&lt;code&gt;python -m ensurepip&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;在Conda中安装Python&lt;/h2&gt;
&lt;h3&gt;安装Conda&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Linux
&lt;ol&gt;
&lt;li&gt;下载安装脚本：&lt;code&gt;wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;执行脚本：&lt;code&gt;sh Miniconda3-latest-Linux-x86_64.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;当提示&lt;code&gt;Do you wish the installer to initialize Miniconda3 by running conda init?&lt;/code&gt;时输入&lt;code&gt;yes&lt;/code&gt;，该步会将conda初始化脚本加入到&lt;code&gt;~/.bashrc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;登陆时调用bashrc：&lt;code&gt;vi ~/.bash_profile&lt;/code&gt;，在末尾加入以下内容：&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Windows
&lt;ol&gt;
&lt;li&gt;下载安装包：&lt;code&gt;https://repo.anaconda.com/miniconda/Miniconda3-latest-Windows-x86_64.exe&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;根据安装向导指示安装&lt;/li&gt;
&lt;li&gt;将conda目录&lt;code&gt;C:\Users\%User%\miniconda3\condabin&lt;/code&gt;加入到环境变量，将%User%替换为实际用户名。&lt;/li&gt;
&lt;li&gt;如果要在powershell中使用conda，需要执行命令：&lt;code&gt;conda init powershell&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果powshell报错：&lt;code&gt;无法加载文件xxx，因为在此系统上禁止运行脚本&lt;/code&gt;，则需要用管理员身份运行命令：&lt;code&gt;set-executionpolicy remotesigned&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;安装完成后，conda会在控制台开启时自动激活conda环境，如果想要不自动激活，可以执行命令：&lt;code&gt;conda config --set auto_activate_base false&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;创建Python环境&lt;/h3&gt;
&lt;p&gt;语法：&lt;code&gt;conda create -n &amp;#x3C;env_name&gt; python=&amp;#x3C;python_version&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;例：&lt;code&gt;conda create -n py382 python=3.8.2 -y&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Conda常用命令&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;查看env列表：&lt;code&gt;conda env list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;切换env：&lt;code&gt;conda activate py38&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Pip相关命令&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;更新pip：&lt;code&gt;python -m pip install --upgrade pip&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装python模块：&lt;code&gt;pip install &amp;#x3C;module_name&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装模块时指定版本：&lt;code&gt;pip install &amp;#x3C;module_name&gt;==&amp;#x3C;version&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装模块时指定源：&lt;code&gt;pip install &amp;#x3C;module_name&gt; -i &amp;#x3C;source_url&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置默认源：&lt;code&gt;pip config set global.index-url &amp;#x3C;source_url&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;常用pip源&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;阿里：&lt;code&gt;https://mirrors.aliyun.com/pypi/simple/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;腾讯：&lt;code&gt;https://mirrors.cloud.tencent.com/pypi/simple/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;清华：&lt;code&gt;https://pypi.tuna.tsinghua.edu.cn/simple&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;中科大：&lt;code&gt;https://pypi.mirrors.ustc.edu.cn/simple/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Jupyter Notebook&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;启动：&lt;code&gt;jupyter notebook --ip=0.0.0.0 --port=&amp;#x3C;port&gt; --no-browser&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;后台运行：&lt;code&gt;nohup jupyter notebook --ip=0.0.0.0 --port=&amp;#x3C;port&gt; --no-browser &amp;#x26;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看：&lt;code&gt;jupyter notebook list&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭：&lt;code&gt;jupyter notebook stop&lt;/code&gt;（关闭当前用户启动的notebook）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置访问密码：&lt;code&gt;jupyter notebook password&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装扩展&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install jupyter_nbextensions_configurator jupyter_contrib_nbextensions

jupyter contrib nbextension install --user

jupyter nbextensions_configurator enable --user
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;常用扩展&lt;/h3&gt;
&lt;p&gt;Edit-nbextensions config&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Codefolding：代码折叠&lt;/li&gt;
&lt;li&gt;Comment/Uncomment Hotkey：注释快捷键&lt;/li&gt;
&lt;li&gt;ExecuteTime: 执行时间&lt;/li&gt;
&lt;li&gt;Highlight selected word：已选文字高亮&lt;/li&gt;
&lt;li&gt;Hinterland：代码自动补全
注：Hinterland只与ipython7.20+兼容，若不满足会有如下报错
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;[IPKernelApp] ERROR | Exception in message handler:
Traceback (most recent call last):
File &quot;/home/ljx/miniconda3/envs/py382/lib/python3.8/site-packages/ipykernel/kernelbase.py&quot;, line 265, in dispatch_shell
  yield gen.maybe_future(handler(stream, idents, msg))
File &quot;/home/ljx/miniconda3/envs/py382/lib/python3.8/site-packages/tornado/gen.py&quot;, line 762, in run
  value = future.result()
File &quot;/home/ljx/miniconda3/envs/py382/lib/python3.8/site-packages/tornado/gen.py&quot;, line 234, in wrapper
  yielded = ctx_run(next, result)
File &quot;/home/ljx/miniconda3/envs/py382/lib/python3.8/site-packages/ipykernel/kernelbase.py&quot;, line 580, in complete_request
  matches = yield gen.maybe_future(self.do_complete(code, cursor_pos))
File &quot;/home/ljx/miniconda3/envs/py382/lib/python3.8/site-packages/ipykernel/ipkernel.py&quot;, line 356, in do_complete
  return self._experimental_do_complete(code, cursor_pos)
File &quot;/home/ljx/miniconda3/envs/py382/lib/python3.8/site-packages/ipykernel/ipkernel.py&quot;, line 381, in _experimental_do_complete
  completions = list(_rectify_completions(code, raw_completions))
File &quot;/home/ljx/miniconda3/envs/py382/lib/python3.8/site-packages/IPython/core/completer.py&quot;, line 484, in rectify_completions
  completions = list(completions)
File &quot;/home/ljx/miniconda3/envs/py382/lib/python3.8/site-packages/IPython/core/completer.py&quot;, line 1818, in completions
  for c in self._completions(text, offset, _timeout=self.jedi_compute_type_timeout/1000):
File &quot;/home/ljx/miniconda3/envs/py382/lib/python3.8/site-packages/IPython/core/completer.py&quot;, line 1861, in _completions
  matched_text, matches, matches_origin, jedi_matches = self._complete(
File &quot;/home/ljx/miniconda3/envs/py382/lib/python3.8/site-packages/IPython/core/completer.py&quot;, line 2029, in _complete
  completions = self._jedi_matches(
File &quot;/home/ljx/miniconda3/envs/py382/lib/python3.8/site-packages/IPython/core/completer.py&quot;, line 1373, in _jedi_matches
  interpreter = jedi.Interpreter(
File &quot;/home/ljx/miniconda3/envs/py382/lib/python3.8/site-packages/jedi/api/__init__.py&quot;, line 725, in __init__
  super().__init__(code, environment=environment,
TypeError: __init__() got an unexpected keyword argument &apos;column&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;isort formatter：isort格式化imports&lt;/li&gt;
&lt;li&gt;Live Markdown Preview：动态Markdown预览&lt;/li&gt;
&lt;li&gt;Snippets Menu：代码片段菜单&lt;/li&gt;
&lt;li&gt;Toggle all line numbers：切换行号显示&lt;/li&gt;
&lt;li&gt;Variable Inspector：变量监视 （&lt;strong&gt;慎用，可能会影响性能&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Java启动参数</title><link>https://juzzi.qzz.io/blog/lang/java/java-run-option</link><guid isPermaLink="true">https://juzzi.qzz.io/blog/lang/java/java-run-option</guid><description>Java常用的启动参数说明</description><pubDate>Tue, 21 Apr 2015 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;内存配置&lt;/h2&gt;
&lt;h3&gt;堆内存&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;-Xms、-Xmx、-Xmn&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;分别代表堆内存大小（初始、最大、年轻代）&lt;/p&gt;
&lt;p&gt;例：&lt;code&gt;-Xms1M -Xmx2M -Xmn 512K&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;栈内存&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;-Xss&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;虚拟机栈内存大小，每个线程的栈大小&lt;/p&gt;
&lt;p&gt;例：&lt;code&gt;-Xss2M&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;默认值&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HotSpot: 1MB&lt;/li&gt;
&lt;li&gt;OpenJ9: 256KB&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;新生代配置&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Eden和Survivor比例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-XX:SurvivorRatio&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;新生代（Young Generation）被划分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1个 Eden 区&lt;/strong&gt;：新对象分配的主要区域&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2个 Survivor 区&lt;/strong&gt;（From 和 To）：存放存活下来的对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Eden区与Survivor区（单个）的比例&lt;/p&gt;
&lt;p&gt;例：&lt;code&gt;-XX:SurvivorRatio=8&lt;/code&gt;，代表Eden占8/10，每个Survivor占1/10&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Eden区大小&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-XX:NewRatio&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;新生代与老年代的比例&lt;/p&gt;
&lt;p&gt;例：&lt;code&gt;-XX:NewRatio=2&lt;/code&gt;，代表新生代占1/3，老年代占2/3&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最大年轻代大小&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-XX:MaxNewSize&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;限制年轻代最大值&lt;/p&gt;
&lt;p&gt;例：&lt;code&gt;-XX:MaxNewSize=1G&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;元空间配置&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;-XX:MetaspaceSize&lt;/code&gt;：元空间初始大小&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-XX:MaxMetaspaceSize&lt;/code&gt;：元空间最大大小&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;警告&lt;/strong&gt;：元空间默认无上限，建议设置限制&lt;/p&gt;
&lt;h2&gt;垃圾回收&lt;/h2&gt;
&lt;h3&gt;G1GC&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;-XX:+UseG1GC&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;JDK 9以后的默认GC，特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持广泛，从JDK7开始就支持&lt;/li&gt;
&lt;li&gt;停顿时间可控，可通过&lt;code&gt;-XX:MaxGCPauseMillis&lt;/code&gt;设置目标&lt;/li&gt;
&lt;li&gt;堆内存在16GB以下有较好表现&lt;/li&gt;
&lt;li&gt;开销较低，对CPU与内存的影响均不大&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;常用调优参数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 设置最大停顿时间目标（毫秒）
-XX:MaxGCPauseMillis=200

# 并发GC线程数（设置为CPU核心数的3/4）
-XX:ConcGCThreads=4

# GC线程数（设置为CPU核心数）
-XX:ParallelGCThreads=4

# 最大Region大小（1MB-32MB）
-XX:G1HeapRegionSize=16m
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ZGC&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;-XX:+UseZGC&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;从JDK15以后开始支持，特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;极短的停顿时间（10ms以下）&lt;/li&gt;
&lt;li&gt;对堆内存在16GB-16TB支持较好，停顿时间无明显增加&lt;/li&gt;
&lt;li&gt;对CPU和内存的占用较多，约需要额外10%-15%&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;常用调优参数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 启用分代ZGC（JDK 21+）
-XX:+ZGenerational

# 并发标记线程数
-XX:ZMarkingThreads=4

# 并发转储线程数
-XX:ZUncommitThreads=2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;核心机制对比&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;| 对比 维度      | G1GC                                                                                                                                                                                                                        | ZGC                                                                                                                                                                                                                                                                |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| &lt;strong&gt;设计目标&lt;/strong&gt;   | 在提供&lt;strong&gt;可预测的停顿时间&lt;/strong&gt;的同时，兼顾&lt;strong&gt;高吞吐量&lt;/strong&gt;。它是一个&quot;大部分并发&quot;的收集器&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/gctuning/available-collectors.html&quot;&gt;&lt;/a&gt;。                                                          | 致力于将垃圾回收的&lt;strong&gt;停顿时间控制在极低水平&lt;/strong&gt;（官方目标是&amp;#x3C;1ms，实践中通常&amp;#x3C;10ms），且该时间&lt;strong&gt;与堆大小无关&lt;/strong&gt;&lt;a href=&quot;https://developer.unity.cn/projects/68454cbbedbc2aa181664f2c&quot;&gt;&lt;/a&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/gctuning/available-collectors.html&quot;&gt;&lt;/a&gt;。        |
| &lt;strong&gt;核心技术&lt;/strong&gt;   | &lt;strong&gt;Region（分区）&lt;/strong&gt;：将堆内存划分为多个大小相同的Region，每个Region可以动态扮演Eden、Survivor或老年代角色，有效避免了内存碎片&lt;a href=&quot;https://coolshell.cn/articles/1252.html#more-1252&quot;&gt;&lt;/a&gt;&lt;a href=&quot;https://www.ucloud.cn/yun/77310.html&quot;&gt;&lt;/a&gt;。 | &lt;strong&gt;着色指针&lt;/strong&gt;与&lt;strong&gt;读屏障&lt;/strong&gt;：通过在指针中标记状态，并结合读屏障技术，实现了垃圾回收的&lt;strong&gt;几乎所有阶段都能与应用线程并发执行&lt;/strong&gt;，从而大幅减少停顿&lt;a href=&quot;https://developer.aliyun.com/article/1684886&quot;&gt;&lt;/a&gt;&lt;a href=&quot;https://www.ucloud.cn/yun/77310.html&quot;&gt;&lt;/a&gt;。                               |
| &lt;strong&gt;内存整理&lt;/strong&gt;   | 通过&lt;strong&gt;复制算法&lt;/strong&gt;（Evacuation）在暂停时进行内存整理，消除碎片&lt;a href=&quot;https://coolshell.cn/articles/1252.html#more-1252&quot;&gt;&lt;/a&gt;&lt;a href=&quot;https://cloud.tencent.cn/developer/article/2145141&quot;&gt;&lt;/a&gt;。                                                   | 支持&lt;strong&gt;并发重分配&lt;/strong&gt;，能在应用运行的同时整理内存，彻底解决了因内存整理导致的长时间停顿&lt;a href=&quot;https://developer.unity.cn/projects/68454cbbedbc2aa181664f2c&quot;&gt;&lt;/a&gt;。                                                                                                             |
| &lt;strong&gt;堆内存结构&lt;/strong&gt; | 逻辑上分代（年轻代、老年代），但物理上是一系列不连续的Region&lt;a href=&quot;https://coolshell.cn/articles/1252.html#more-1252&quot;&gt;&lt;/a&gt;&lt;a href=&quot;https://www.ucloud.cn/yun/77310.html&quot;&gt;&lt;/a&gt;。                                                                 | 从非分代演进到&lt;strong&gt;分代&lt;/strong&gt;（JDK 21及以后通过 &lt;code&gt;-XX:+ZGenerational&lt;/code&gt; 启用），以降低CPU开销并减少对吞吐量的影响&lt;a href=&quot;https://www.conduktor.io/blog/kafka-jvm-tuning-g1gc-vs-zgc-production&quot;&gt;&lt;/a&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/gctuning/available-collectors.html&quot;&gt;&lt;/a&gt;。 |&lt;/p&gt;
&lt;h3&gt;其他GC选项&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;串行GC&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 老年代使用串行收集器
-XX:+UseSerialGC

# 年轻代使用串行收集器
-XX:+UseSerialGC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;并行GC&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 老年代使用并行收集器（吞吐量优先）
-XX:+UseParallelGC

# 年轻代使用并行收集器
-XX:+UseParallelGC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;CMS（已废弃）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 老年代使用CMS收集器
-XX:+UseConcMarkSweepGC
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;输出和调试&lt;/h2&gt;
&lt;h3&gt;类路径&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;-classpath&lt;/code&gt; 或 &lt;code&gt;-cp&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;指定类文件和资源的位置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 使用通配符
java -cp lib/*:bin com.example.Main

# 指定多个路径
java -cp &quot;lib:/usr/share/java&quot; com.example.Main

# 使用类名
java -cp .:lib/compression.jar com.example.Main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;调试参数&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;调试支持&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 启用调试模式（JDB）
-javaagent

# 调试端口
-jar

# 调试参数
-jar

# 调试参数示例
# -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;详细输出&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 启用详细GC输出
-verbose:gc

# 启用类加载详细信息
-verbose:class

# 启用JIT编译详细信息
-verbose:jit

# 输出类加载器层次结构
-verbose:class
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;符号表&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 生成符号表
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintGCApplicationConcurrentTime
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;性能优化&lt;/h2&gt;
&lt;h3&gt;JIT编译器&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;自适应策略&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 开启自适应字符串去重（JDK 12+）
-XX:+UseStringDeduplication

# 启用编译自适应策略
-XX:+UseAdaptiveSizePolicy

# JIT编译线程数
-XX:CICompilerCount=2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;逃逸分析&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 启用逃逸分析（JDK 11+）
-XX:+DoEscapeAnalysis

# 启用标量替换
-XX:+EliminateAllocations

# 启用循环展开
-XX:+UseLoopUnswitching
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;字符串优化&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 启用字符串驻留（JDK 6u23+）
-XX:+StringInterning

# 字符串去重（JDK 12+）
-XX:+UseStringDeduplication

# 去重阈值（字节数）
-XX:StringDeduplicationAgeThreshold=3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;并行优化&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 启用并行类加载
-XX:+UseParallelClassLoading

# 字节码缓存大小
-XX:ReservedCodeCacheSize=256m
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;内存追踪&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;-XX:NativeMemoryTracking=summary&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;参数可选值&lt;/p&gt;
&lt;p&gt;| 值          | 说明                                     |
| ----------- | ---------------------------------------- |
| &lt;strong&gt;off&lt;/strong&gt;     | 关闭 NMT（默认值）                       |
| &lt;strong&gt;summary&lt;/strong&gt; | 只收集摘要级别的内存信息（不包含调用栈） |
| &lt;strong&gt;detail&lt;/strong&gt;  | 收集详细信息（包含调用栈，性能开销更大） |&lt;/p&gt;
&lt;p&gt;开启后可使用jcmd命令查看内存使用情况&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看当前内存使用
jcmd &amp;#x3C;pid&gt; VM.native_memory
# 查看基线对比（需要先创建基线）
jcmd &amp;#x3C;pid&gt; VM.native_memory baseline
jcmd &amp;#x3C;pid&gt; VM.native_memory summary.diff
# 查看详细信息
jcmd &amp;#x3C;pid&gt; VM.native_memory detail
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;系统属性&lt;/h2&gt;
&lt;h3&gt;标准系统属性&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# JVM版本
java -version

# Java家路径
java.home

# 用户目录
user.dir

# 临时目录
java.io.tmpdir

# 文件编码
file.encoding=UTF-8

# 字符集
file.separator=/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自定义系统属性&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 设置系统属性
-Dproperty.name=value

# 使用Java配置文件
-Djava.security.auth.login.config=/path/to/login.config

# 调整日志级别
-Djava.util.logging.config.file=/path/to/logging.properties
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安全选项&lt;/h2&gt;
&lt;h3&gt;字节码验证&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 启用字节码验证
-verify

# 启用保守的字节码验证
-verifyremote

# 自定义验证器
-Xverify:all
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;内存安全&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 设置最大堆内存
-Xmx2g

# 设置初始堆内存
-Xms1g

# 禁用压缩指针（JDK 32-bit）
-XX:-UseCompressedOops
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常用启动脚本&lt;/h2&gt;
&lt;h3&gt;开发环境&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 开发环境启动脚本
java -Xms512m -Xmx1g \
  -XX:SurvivorRatio=8 \
  -XX:+UseStringDeduplication \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -Xlog:gc*:file=gc.log \
  -cp &quot;lib/*:bin&quot; \
  com.example.Main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;生产环境&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 生产环境启动脚本
java -Xms1g -Xmx2g \
  -XX:SurvivorRatio=8 \
  -XX:MaxMetaspaceSize=512m \
  -XX:+UseStringDeduplication \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:ConcGCThreads=4 \
  -XX:ParallelGCThreads=8 \
  -XX:MaxInlineSize=35 \
  -XX:ReservedCodeCacheSize=256m \
  -Dfile.encoding=UTF-8 \
  -Duser.timezone=Asia/Shanghai \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/var/log/heapdump.hprof \
  -Xlog:gc*:file=/var/log/gc.log:time,tags:filecount=10,filesize=100m \
  -cp &quot;lib/*&quot; \
  com.example.Main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;调试环境&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 调试环境启动脚本
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 \
  -Xms512m -Xmx1g \
  -Xdebug -Xrunjdwp \
  -Xlog:gc*:file=gc_debug.log:time,tags \
  -XX:+PrintGCDetails \
  -XX:+PrintGCTimeStamps \
  -cp &quot;lib/*:bin&quot; \
  com.example.Main
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;内存溢出&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;OOM错误处理&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 启用堆转储
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof

# 生成堆转储
jmap -dump:format=b,file=heapdump.hprof &amp;#x3C;pid&gt;

# 分析堆转储
jhat heapdump.hprof
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;内存泄漏&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;使用MAT分析&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 下载MAT工具
# 运行分析
# 加载堆转储文件
# 查看内存泄漏报告
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;性能分析&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;使用jcmd&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看JVM线程
jcmd &amp;#x3C;pid&gt; Thread.print

# 查看类加载信息
jcmd &amp;#x3C;pid&gt; GC.class_stats

# 查看编译信息
jcmd &amp;#x3C;pid&gt; Compiler.compilethreads
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用VisualVM&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 启动VisualVM
# 监控JVM性能
# 分析内存使用
# 查看线程状态
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最佳实践&lt;/h2&gt;
&lt;h3&gt;内存配置原则&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;堆内存设置&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Xms 和 Xmx 设置相同值，避免动态调整&lt;/li&gt;
&lt;li&gt;根据应用实际需求调整，留出20-30%余量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GC选择&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单机应用：G1GC&lt;/li&gt;
&lt;li&gt;超大内存（&gt;16GB）：ZGC&lt;/li&gt;
&lt;li&gt;吞吐量优先：ParallelGC&lt;/li&gt;
&lt;li&gt;简单应用：SerialGC&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;避免过早优化&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先启用默认配置&lt;/li&gt;
&lt;li&gt;使用性能分析工具识别瓶颈&lt;/li&gt;
&lt;li&gt;针对性优化&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;日志配置&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GC日志&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用-Xlog（JDK 9+）或-verbose:gc&lt;/li&gt;
&lt;li&gt;设置合适的日志级别&lt;/li&gt;
&lt;li&gt;定期分析GC日志&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;应用日志&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置合理的日志级别&lt;/li&gt;
&lt;li&gt;使用结构化日志格式&lt;/li&gt;
&lt;li&gt;避免日志过多影响性能&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参考资源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/gctuning/&quot;&gt;Oracle JVM 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openjdk.org/&quot;&gt;OpenJDK 源代码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html&quot;&gt;Java Performance Tuning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/chewiebug/GCViewer&quot;&gt;GCViewer 工具&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>