[{"content":"此文翻译自Leonie Monigatti的 Building an AI agent from scratch in Python\n如何通过大模型 API 实现一个不依赖框架的单智能体系统(a single AI agent)。\n市面上构建 AI 智能体的框架层出不穷，如 CrewAI、LangGraph 和 OpenAI Agents SDK，面对这些选择，开发者往往会感到无从下手。Anthropic 曾建议：在依赖复杂的框架抽象之前，最好先直接调用大模型(LLM)接口来理解其底层基本功。\n本教程遵循这一思路，我们将直接使用大模型 API，在 Python 中从零开始实现一个 AI 智能体。通过这种方式，你可以深入理解智能体运行的内部机制。我们将首先聚焦于单智能体的实现，这是迈向更复杂的“智能体工作流”(agentic workflows)或“多智能体系统”(multi-agent)的基石。\n手写 AI 智能体的核心组件 #我们将一步步实现一个 Agent() 类，它包含智能体的四大核心组件：\n大模型与指令 (LLM \u0026amp; Instructions)：智能体的“大脑”，负责推理、决策，并遵循特定的行为准则。\n记忆 (Memory)：对话历史（短期记忆 short-term memory），让智能体理解当前的语境。\n工具 (Tools)：智能体可以调用的外部函数或 API。\n循环 (Agent Loop)：将上述组件有机结合的闭环逻辑。\n组件 1：大模型与指令(LLM and Instructions) #智能体的核心是具备“工具调用”能力的大模型（如 Anthropic’s Claude 4 Sonnet、OpenAI’s GPT-4o 或 Google’s Gemini 2.5 Pro）.\n本教程以 Anthropic 的 API 为例，但其逻辑可以轻松迁移到其他模型。\n在使用 Anthropic API 之前，你需要先准备好 ANTHROPIC_API_KEY。只需注册 Anthropic 账号，并在控制面板（Dashboard）的 “API Keys” 栏目中生成即可。获取密钥后，请根据你的开发环境，将其妥善配置在环境变量、.env 配置文件或 Google Colab 的 Secrets（安全密钥）中。\n首先，安装并导入必要的库：\n%%capture %pip install -U anthropic python-dotenv import anthropic import os from dotenv import load_dotenv from google.colab import userdata load_dotenv() print(anthropic.__version__) # 0.69.0 现在，我们实现一个简单的 Agent 类，包含以下部分：\n初始化（Initialization）：设置 LLM 客户端，并为模型配置一个系统提示词（system prompt），该提示词包含了关于 Agent 应当如何行动的指令。（你也可以将其设为一个可以传递给 Agent 的参数，但为了简单起见，我们将使用一个固定的提示词。）\nchat 方法：通过将用户消息发送给 LLM API 并返回响应来处理这些消息。\nclass Agent: \u0026#34;\u0026#34;\u0026#34;一个简单的、能够回答问题的 AI 智能体\u0026#34;\u0026#34;\u0026#34; def __init__(self): self.client = anthropic.Anthropic(api_key=os.getenv(\u0026#34;ANTHROPIC_API_KEY\u0026#34;)) self.model = \u0026#34;claude-sonnet-4-20250514\u0026#34; self.system_message = \u0026#34;You are a helpful assistant that breaks down problems into steps and solves them systematically.\u0026#34; def chat(self, message): \u0026#34;\u0026#34;\u0026#34;Process a user message and return a response\u0026#34;\u0026#34;\u0026#34; response = self.client.messages.create( model=self.model, max_tokens=1024, system=self.system_message, messages=[ {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: message} ], temperature=0.1, ) return response 此时的智能体只能进行单次问答。测试一下：\nagent = Agent() response = agent.chat(\u0026#34;I have 4 apples. How many do you have?\u0026#34;) print(response.content[0].text) don\u0026#39;t have any apples - as an AI, I don\u0026#39;t have a physical form, so I can\u0026#39;t possess physical objects like apples. Only you have apples in # this scenario (4 of them). Is there something you\u0026#39;d like to do with this information, like a math problem involving your apples? 接着问第二个问题：\nresponse = agent.chat(\u0026#34;I ate 1 apple. How many are left?\u0026#34;) print(response.content[0].text) I don\u0026#39;t have enough information to answer how many apples are left. To solve this, I would need to know: **What I need:** - How many apples you started with **The calculation would be:** Starting number of apples - 1 apple eaten = Apples remaining Could you tell me how many apples you had before eating one? 如你所见，Agent 缺失了第一条消息的信息。这就是为什么我们需要给 Agent 提供对话历史。\n组件 2：记忆 (对话上下文) #智能体的记忆（Memory）可以有多种不同的形式，例如短时记忆和长时记忆，而且记忆管理本身就是一个复杂的话题。为了本教程起见，让我们保持简单，从一个基础的短时记忆实现开始。\n短时记忆让智能体能够访问对话历史，从而理解当前的交互。在最简单的形式下，短时记忆仅仅是用户（user）和助手（assistant）之间过去消息的列表。（请注意，随着对话历史变得越来越长，你将会遇到**上下文窗口（context window）**的限制，并需要实现更复杂的解决方案。）\n我们通过添加一个 messages 属性来实现短时记忆，在该属性中我们同时存储以下两部分内容：\n用户输入：使用 {\u0026quot;role\u0026quot;: \u0026quot;user\u0026quot;, \u0026quot;content\u0026quot;: message}\n响应结果：使用 {\u0026quot;role\u0026quot;: \u0026quot;assistant\u0026quot;, \u0026quot;content\u0026quot;: response.content}\nclass Agent: \u0026#34;\u0026#34;\u0026#34;A simple AI agent that can answer questions in a multi-turn conversation\u0026#34;\u0026#34;\u0026#34; def __init__(self): self.client = anthropic.Anthropic(api_key=os.getenv(\u0026#34;ANTHROPIC_API_KEY\u0026#34;)) self.model = \u0026#34;claude-sonnet-4-20250514\u0026#34; self.system_message = \u0026#34;You are a helpful assistant that breaks down problems into steps and solves them systematically.\u0026#34; self.messages = [] def chat(self, message): \u0026#34;\u0026#34;\u0026#34;Process a user message and return a response\u0026#34;\u0026#34;\u0026#34; # Store user input in short-term memory self.messages.append({\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: message}) response = self.client.messages.create( model=self.model, max_tokens=1024, system=self.system_message, messages=self.messages, temperature=0.1, ) # Store assistant\u0026#39;s response in short-term memory self.messages.append({\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: response.content}) return response 现在，让我们用之前的对话示例再次测试该智能体。\nagent = Agent() response = agent.chat(\u0026#34;I have 4 apples. How many do you have?\u0026#34;) print(response.content[0].text) response = agent.chat(\u0026#34;I ate 1 apple. How many are left?\u0026#34;) print(response.content[0].text) I don\u0026#39;t have any apples - as an AI, I don\u0026#39;t have a physical form and can\u0026#39;t possess physical objects like apples. You have 4 apples, and I have 0 apples. Is there something you\u0026#39;d like to do with your 4 apples, like a math problem or recipe suggestion? Let me solve this step by step: **Step 1:** Identify the starting amount - You started with 4 apples **Step 2:** Identify what was consumed - You ate 1 apple **Step 3:** Calculate the remaining amount - Apples left = Starting amount - Apples eaten - Apples left = 4 - 1 = 3 **Answer:** You have 3 apples left. 如你所见，该智能体（Agent）现在已经能够进行对话并引用之前的信息了。\n但是，如果你交给智能体一个稍微复杂一点的数学题，会发生什么呢？\nagent = Agent() response = agent.chat(\u0026#34;What is 157.09 * 493.89?\u0026#34;) print(response.content[0].text) I\u0026#39;ll solve this step by step using the standard multiplication algorithm. 157.09 × 493.89 First, let me multiply 157.09 by each digit of 493.89: **Step 1:** 157.09 × 9 (ones place) 157.09 × 9 = 1,413.81 **Step 2:** 157.09 × 80 (tens place) 157.09 × 8 = 1,256.72 1,256.72 × 10 = 12,567.2 **Step 3:** 157.09 × 300 (hundreds place) 157.09 × 3 = 471.27 471.27 × 100 = 47,127 **Step 4:** 157.09 × 90,000 (ten-thousands place) 157.09 × 9 = 1,413.81 1,413.81 × 10,000 = 14,138,100 **Step 5:** 157.09 × 400,000 (hundred-thousands place) 157.09 × 4 = 628.36 628.36 × 100,000 = 62,836,000 **Step 6:** Add all partial products: 1,413.81 12,567.2 47,127 14,138,100 62,836,000 ----------- 77,035,208.01 Therefore, **157.09 × 493.89 = 77,035.2081** 该智能体的回答听起来完全可信，但如果你去验证一下，你就会发现即使是像 Claude 4 Sonnet 这样强大的大语言模型（LLM），在没有工具辅助的情况下，仍然会犯算术错误。\n157.09 * 493.89 # 77585.1801 组件 3：工具调用 (Tool Use) #为了扩展智能体的能力，你可以为其提供工具，这些工具的范围涵盖从简单的函数到调用外部 API。在本教程中，我们将实现一个简单的 CalculatorTool 类，用于处理数学问题。\n虽然不同供应商对工具使用的具体实现方式有所不同，但在核心层面上，始终需要两个关键组件：\n函数实现（Function implementation）：这是执行工具逻辑的实际代码，例如执行计算或进行 API 调用。\n工具模式（Tool schema）：工具的结构化描述。工具描述非常重要，因为它会告知大语言模型（LLM）该工具的功能、何时应该使用它以及它需要哪些参数。\n本教程遵循 Anthropic 关于工具使用（Tool use）的文档。如果你在学习本教程时使用的是不同的大语言模型 API，我建议你查阅该 LLM 供应商关于工具使用的相关文档。\nclass CalculatorTool(): \u0026#34;\u0026#34;\u0026#34;A tool for performing mathematical calculations\u0026#34;\u0026#34;\u0026#34; def get_schema(self): return { \u0026#34;name\u0026#34;: \u0026#34;calculator\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Performs basic mathematical calculations, use also for simple additions\u0026#34;, \u0026#34;input_schema\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;expression\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Mathematical expression to evaluate (e.g., \u0026#39;2+2\u0026#39;, \u0026#39;10*5\u0026#39;)\u0026#34; } }, \u0026#34;required\u0026#34;: [\u0026#34;expression\u0026#34;] } } def execute(self, expression): \u0026#34;\u0026#34;\u0026#34; Evaluate mathematical expressions. WARNING: This tutorial uses eval() for simplicity but it is not recommended for production use. Args: expression (str): The mathematical expression to evaluate Returns: float: The result of the evaluation \u0026#34;\u0026#34;\u0026#34; try: result = eval(expression) return {\u0026#34;result\u0026#34;: result} except: return {\u0026#34;error\u0026#34;: \u0026#34;Invalid mathematical expression\u0026#34;} 请注意，在本教程中，我们仅实现了一个单一工具。在生产环境的代码中，你通常会使用 抽象基类（abstract base class） 来确保所有工具之间拥有一致的接口。\n让我们测试一下这个计算器函数是否正常工作。\ncalculator_tool = CalculatorTool() calculator_tool.execute(\u0026#34;157.09 * 493.89\u0026#34;) # {\u0026#39;result\u0026#39;: 77585.1801} 既然我们已经有了一个 CalculatorTool，现在让我们分三个步骤为智能体添加工具使用能力：\n添加 tools 和 tool_map 属性，用于存储可用的工具。\n添加私有的 _get_tool_schemas() 方法，用于提取工具的模式（Schemas）。\n在 create 方法中添加工具处理逻辑，用于检测并处理工具调用。\nclass Agent: \u0026#34;\u0026#34;\u0026#34;A simple AI agent that can use tools to answer questions in a multi-turn conversation\u0026#34;\u0026#34;\u0026#34; def __init__(self, tools): self.client = anthropic.Anthropic(api_key=os.getenv(\u0026#34;ANTHROPIC_API_KEY\u0026#34;)) self.model = \u0026#34;claude-sonnet-4-20250514\u0026#34; self.system_message = \u0026#34;You are a helpful assistant that breaks down problems into steps and solves them systematically.\u0026#34; self.messages = [] self.tools = tools self.tool_map = {tool.get_schema()[\u0026#34;name\u0026#34;]: tool for tool in tools} def _get_tool_schemas(self): \u0026#34;\u0026#34;\u0026#34;Get tool schemas for all registered tools\u0026#34;\u0026#34;\u0026#34; return [tool.get_schema() for tool in self.tools] def chat(self, message): \u0026#34;\u0026#34;\u0026#34;Process a user message and return a response\u0026#34;\u0026#34;\u0026#34; # Store user input in short-term memory self.messages.append({\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: message}) response = self.client.messages.create( model=self.model, max_tokens=1024, system=self.system_message, tools=self._get_tool_schemas() if self.tools else None, messages=self.messages, temperature=0.1, ) # Store assistant\u0026#39;s response in short-term memory self.messages.append({\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: response.content}) return response 让我们来试一试。\ncalculator_tool = CalculatorTool() agent = Agent(tools=[calculator_tool]) response = agent.chat(\u0026#34;What is 157.09 * 493.89?\u0026#34;) for block in response: print(block) (\u0026#39;id\u0026#39;, \u0026#39;msg_01BzC2FerKEr8rC1wGfaMiNK\u0026#39;) (\u0026#39;content\u0026#39;, [TextBlock(citations=None, text=\u0026#34;I\u0026#39;ll calculate 157.09 * 493.89 for you.\u0026#34;, type=\u0026#39;text\u0026#39;), ToolUseBlock(id=\u0026#39;toolu_017NhVhd5wYWdEw7fFRPHyXL\u0026#39;, input={\u0026#39;expression\u0026#39;: \u0026#39;157.09 * 493.89\u0026#39;}, name=\u0026#39;calculator\u0026#39;, type=\u0026#39;tool_use\u0026#39;)]) (\u0026#39;model\u0026#39;, \u0026#39;claude-sonnet-4-20250514\u0026#39;) (\u0026#39;role\u0026#39;, \u0026#39;assistant\u0026#39;) (\u0026#39;stop_reason\u0026#39;, \u0026#39;tool_use\u0026#39;) (\u0026#39;stop_sequence\u0026#39;, None) (\u0026#39;type\u0026#39;, \u0026#39;message\u0026#39;) (\u0026#39;usage\u0026#39;, Usage(cache_creation=CacheCreation(ephemeral_1h_input_tokens=0, ephemeral_5m_input_tokens=0), cache_creation_input_tokens=0, cache_read_input_tokens=0, input_tokens=433, output_tokens=77, server_tool_use=None, service_tier=\u0026#39;standard\u0026#39;)) 正如你在响应（response）中看到的，智能体回答道：“I’ll calculate 157.09 * 493.89 for you.”但它并没有自己去计算这个表达式，而是停了下来，其 stop_reason（停止原因）显示为 tool_use。这意味着，智能体正在等待用户去执行该工具，并将工具的运行结果返回给它。\n由于智能体已经做出了响应，表示它需要协助来执行工具并处于等待状态。这时，循环（loop）的最后一个组件就该发挥作用了。\n组件 4：Agent 循环(Agent Loop) #你可能已经听过有人说过这样一句话：“Agent 就是一个在循环中使用工具的模型（Agents are models using tools in a loop）。” 如果没有这个“循环”，Agent 其实只能处理单轮请求，无法进行多轮交互。\n我非常喜欢 Anthropic 的 Barry Zhan 给出的这段伪代码，它很好地说明了：Agent 本质上就是一个在循环中做决策的 LLM —— 观察结果，然后决定下一步该做什么。\nenv = Environment() tools = Tools(env) system_prompt = \u0026#34;Goals, constraints, and how to act\u0026#34; while True: action = llm.run(system_prompt + env.state) env.state = tools.run(action) 对于这个简单的 Agent 实现来说，整体流程如下：\n用户向 Agent 发送消息 Agent 判断自己需要调用某个工具，并返回一个 stop_reason = tool_use 的响应，同时包含一个 tool_use 块，里面给出工具名称和参数。 这一步的含义是：“我先停在这里，请你用这些参数去执行这个工具。” 用户执行该工具，并在下一条消息中把工具的执行结果返回给 Agent Agent 继续执行，并给出最终回复 import json def run_agent(user_input, max_turns=10): calculator_tool = CalculatorTool() agent = Agent(tools=[calculator_tool]) i = 0 while i \u0026lt; max_turns: # It\u0026#39;s safer to use max_turns rather than while True i += 1 print(f\u0026#34;\\nIteration {i}:\u0026#34;) print(f\u0026#34;User input: {user_input}\u0026#34;) response = agent.chat(user_input) print(f\u0026#34;Agent output: {response.content[0].text}\u0026#34;) # Handle tool use if present if response.stop_reason == \u0026#34;tool_use\u0026#34;: # Process all tool uses in the response tool_results = [] for content_block in response.content: if content_block.type == \u0026#34;tool_use\u0026#34;: tool_name = content_block.name tool_input = content_block.input print(f\u0026#34;Using tool {tool_name} with input {tool_input}\u0026#34;) # Execute the tool tool = agent.tool_map[tool_name] tool_result = tool.execute(**tool_input) tool_results.append({ \u0026#34;type\u0026#34;: \u0026#34;tool_result\u0026#34;, \u0026#34;tool_use_id\u0026#34;: content_block.id, \u0026#34;content\u0026#34;: json.dumps(tool_result) }) print(f\u0026#34;Tool result: {tool_result}\u0026#34;) # Add tool results to conversation user_input = tool_results else: return response.content[0].text return 测试已实现的 AI Agent #下面通过几个示例测试用例来验证这个 AI Agent 的行为。\n测试 1：通用问题（不使用工具） #这个测试展示了 Agent 在不需要调用任何外部工具的情况下，回答一个简单通用问题的能力。\nresponse = run_agent(\u0026#34;I have 4 apples. How many do you have?\u0026#34;) Iteration 1: User input: I have 4 apples. How many do you have? Agent output: I don\u0026#39;t have any apples since I\u0026#39;m an AI assistant - I don\u0026#39;t have a physical form or possessions. But I can help you with calculations involving your 4 apples if you need! Is there something specific you\u0026#39;d like to calculate or figure out with your 4 apples? 测试2: 工具使用 #这个测试展示了 Agent 如何理解为了完成一个特定任务，它需要使用工具，并且能够调用 CalculatorTool 来得到正确的计算结果。\nresponse = run_agent(\u0026#34;What is 157.09 * 493.89?\u0026#34;) Iteration 1: User input: What is 157.09 * 493.89? Agent output: I\u0026#39;ll calculate 157.09 * 493.89 for you. Using tool calculator with input {\u0026#39;expression\u0026#39;: \u0026#39;157.09 * 493.89\u0026#39;} Tool result: {\u0026#39;result\u0026#39;: 77585.1801} Iteration 2: User input: [{\u0026#39;type\u0026#39;: \u0026#39;tool_result\u0026#39;, \u0026#39;tool_use_id\u0026#39;: \u0026#39;toolu_01FC9yLWt2Cf6a8zLGhj7ZJz\u0026#39;, \u0026#39;content\u0026#39;: \u0026#39;{\u0026#34;result\u0026#34;: 77585.1801}\u0026#39;}] Agent output: The result of 157.09 * 493.89 is **77,585.1801**. 测试 3：分步骤使用工具 #这个测试展示了 Agent 将一个更复杂的问题拆解为多个更小的步骤的能力，并且能够在同一次对话中多次使用 CalculatorTool，最终得到正确答案。\nresponse = run_agent(\u0026#34;If my brother is 32 years younger than my mother and my mother is 30 years older than me and I am 20, how old is my brother?\u0026#34;) Iteration 1: User input: If my brother is 32 years younger than my mother and my mother is 30 years older than me and I am 20, how old is my brother? Agent output: I\u0026#39;ll solve this step by step using the given information. Given: - You are 20 years old - Your mother is 30 years older than you - Your brother is 32 years younger than your mother Let me calculate your mother\u0026#39;s age first: Using tool calculator with input {\u0026#39;expression\u0026#39;: \u0026#39;20 + 30\u0026#39;} Tool result: {\u0026#39;result\u0026#39;: 50} Iteration 2: User input: [{\u0026#39;type\u0026#39;: \u0026#39;tool_result\u0026#39;, \u0026#39;tool_use_id\u0026#39;: \u0026#39;toolu_01WPMQRzCi4roua9vQ7qXeCR\u0026#39;, \u0026#39;content\u0026#39;: \u0026#39;{\u0026#34;result\u0026#34;: 50}\u0026#39;}] Agent output: So your mother is 50 years old. Now I\u0026#39;ll calculate your brother\u0026#39;s age: Using tool calculator with input {\u0026#39;expression\u0026#39;: \u0026#39;50 - 32\u0026#39;} Tool result: {\u0026#39;result\u0026#39;: 18} Iteration 3: User input: [{\u0026#39;type\u0026#39;: \u0026#39;tool_result\u0026#39;, \u0026#39;tool_use_id\u0026#39;: \u0026#39;toolu_01UL7n7a85XJUn7Tgk8kiHhX\u0026#39;, \u0026#39;content\u0026#39;: \u0026#39;{\u0026#34;result\u0026#34;: 18}\u0026#39;}] Agent output: Your brother is 18 years old. To summarize: - You: 20 years old - Your mother: 50 years old (30 years older than you) - Your brother: 18 years old (32 years younger than your mother) 总结 #本教程向你展示了如何在不依赖任何框架的情况下，仅使用一个 LLM API，从零实现一个最小可用的 AI Agent。\n希望通过这个过程，你已经理解了 AI Agent 在底层是如何工作的，也明白了人们所说的那句话：\n“Agent 是在循环中使用工具的模型（Agents are models using tools in a loop）。”\n参考资源 # Anthropic’s Building Effective Agents Cookbook Build an AI Agent from SCRATCH with Python! (No Frameworks) by Aaron Dunn Building Effective LLM Workflows in Pure Python by Dave Ebbelaar ","date":"2025-10-04","permalink":"https://niweea.github.io/posts/agent/building_ai_agent/","section":"Posts","summary":"\u003cp\u003e此文翻译自Leonie Monigatti的 \u003ca href=\"https://www.leoniemonigatti.com/blog/ai-agent-from-scratch-in-python.html\" target=\"_blank\" rel=\"noreferrer\"\u003eBuilding an AI agent from scratch in Python\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e如何通过大模型 API 实现一个不依赖框架的单智能体系统(a single AI agent)。\u003c/p\u003e","title":"【转译】从零开始：用 Python 构建一个 AI Agent"},{"content":"","date":null,"permalink":"https://niweea.github.io/","section":"Niweea’s Blog","summary":"","title":"Niweea’s Blog"},{"content":"","date":null,"permalink":"https://niweea.github.io/posts/","section":"Posts","summary":"","title":"Posts"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/aot/","section":"Tags","summary":"","title":"AOT"},{"content":"","date":null,"permalink":"https://niweea.github.io/categories/","section":"Categories","summary":"","title":"Categories"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/graalvm/","section":"Tags","summary":"","title":"GraalVM"},{"content":"HotSpot JVM # Java 应用程序的运行生命周期 #上图是 Java 应用程序的运行生命周期示意图，可分为 JVM 初始化、应用初始化、应用预热、稳定运行和关闭 5 个阶段。\n1. 启动 JVM #包括解析启动参数、加载 Agent、创建主线程、初始化 JDK 核心类、初始化系统类加载器、初始化即时编译器等。\ntime java --version # java --version 0.03s user 0.03s system 51% cpu 0.122 total 可用 time 查看仅 JVM 启动的耗时。后续类加载需要从磁盘读取 JAR（解压）和 class 文件，加载类越多，启动时间越长。\n2. 应用初始化 #调用 main 进入应用初始化。此阶段通过解释器（Interpreter）解释执行字节码，同时触发 GC，并对热点代码进行 JIT 编译。\n3. 应用预热 #业务代码运行后进入预热阶段，会有大量类加载（CL）和 JIT 编译行为。\n4. 稳定阶段 #充分预热后进入稳定阶段，此时 JIT 编译完成的方法占比高，整体性能最好。\n5. 应用关闭 #释放资源、关闭 JVM。\nJava 静态编译的成熟方案主要有 Oracle GraalVM 的 Substrate VM（Native Image） 和华为方舟编译器（面向移动端）。\nGraalVM 项目 # GraalVM 是 Oracle 推出的基于 Java 开发的开源高性能多语言运行时平台，可在统一运行时上实现跨语言交互。\n其起源于 Oracle 实验室用 Java 编写新的 JIT 编译器，以替代 OpenJDK HotSpot 中由 C++ 实现的 C2 编译器。\nGraalVM 可以：\n嵌入 OpenJDK，用 Graal 编译器替代 C2 做 JIT 嵌入 Node.js、Oracle 数据库等运行时 作为独立工具链，将 Java 程序 AOT 静态编译 为本地可执行文件（Native Image） 多语言支持 #Truffle 是 GraalVM 的解释器实现框架，开发者可用 Java 快速实现语言解释器，在 JVM 上运行其他语言。常见实现：\n组件 语言 GraalJS JavaScript / Node.js GraalPy Python TruffleRuby Ruby FastR R WebAssembly WASM C/C++ 可通过 LLVM 编译为 bitcode，由 GraalVM 的 Sulong 解释器执行。\nJava on Truffle（Espresso） #从 Java 21 起引入实验性 Espresso：基于 Truffle 的 Java 字节码解释器，符合 Java 8 / 11 规范，可对热点方法启用 Graal JIT。\nObject foreign = Polyglot.eval(\u0026#34;js\u0026#34;, \u0026#34;[2, 0, 2, 1]\u0026#34;); Object local = new int[]{2, 0, 2, 1}; System.out.println(Polyglot.isForeignObject(foreign)); // true System.out.println(Polyglot.isForeignObject(local)); // false 可在与宿主 JVM 不同的 Java 版本上运行 Guest Java，并与 JS 等语言在同一堆内互操作。需单独安装，目前性能仍低于 HotSpot，适合实验和多语言场景。\n静态编译（Native Image） #GraalVM 提供 Java 静态编译的完整工具链：Graal 编译器 同时用于 JIT 和 AOT，Native Image 在构建期将可达代码编译为本地二进制。\n流程概要：\n用户指定编译入口（如 main） 静态分析从入口出发，确定可达类与方法 Graal 编译器将可达代码与 Substrate VM 运行时一起编译为本地可执行文件或动态库 缺点 # 静态分析消耗大量 CPU、内存和时间 对反射、动态代理、JNI 等分析能力有限，需额外配置 峰值吞吐通常低于长期 JIT 预热后的 HotSpot（换取启动速度与内存占用） GraalVM 项目结构 #graal GitHub\nGraal Compiler #用 Java 编写的动态编译器，是 GraalVM 的基石：\n作为 Truffle 语言的 JIT 编译器 作为 Substrate VM / Native Image 的 AOT 编译器 Truffle #解释器实现框架，提供 Language Implementation Framework 接口：\njava -truffle [options] class java -truffle [options] -jar jarfile Substrate VM #Native Image 的静态编译框架，提供静态分析、运行时支持、C 互操作等。编译器使用 Graal Compiler，链接在 Linux 上通常依赖系统 GCC/Clang。\n安装 GraalVM ## 下载 graalvm-jdk-\u0026lt;version\u0026gt;_macos-\u0026lt;architecture\u0026gt;.tar.gz 后 sudo xattr -r -d com.apple.quarantine graalvm-jdk-\u0026lt;version\u0026gt;_macos-\u0026lt;architecture\u0026gt;.tar.gz tar -xzf graalvm-jdk-\u0026lt;version\u0026gt;_macos-\u0026lt;architecture\u0026gt;.tar.gz sudo mv graalvm-jdk-\u0026lt;version\u0026gt;_macos-\u0026lt;architecture\u0026gt; /Library/Java/JavaVirtualMachines export JAVA_HOME=/Library/Java/JavaVirtualMachines/\u0026lt;graalvm\u0026gt;/Contents/Home export PATH=$JAVA_HOME/bin:$PATH java -version native-image --version # 确认 Native Image 可用 xattr 说明\nmacOS 从网络下载的文件可能带有 com.apple.quarantine 扩展属性，运行时会弹出安全提示。xattr -r -d com.apple.quarantine \u0026lt;file\u0026gt; 可移除该属性。查看属性：ls -l@ 或 xattr -l \u0026lt;file\u0026gt;。\nNative Image 将 Java 应用编译为无需 JVM 的本地二进制，启动快、内存占用低、无需预热。主流微服务框架（Spring Boot、Quarkus、Micronaut、Helidon）均提供 Native 构建支持。\nNative Image 构建前配置 #Java 的动态特性（反射、JNI、资源、动态代理等）违反静态分析的封闭性假设。GraalVM 通过 JSON 配置文件在构建期补充这些信息：\n配置文件 用途 reflect-config.json 反射目标 jni-config.json JNI 回调 resource-config.json classpath 资源文件 proxy-config.json 动态代理接口 serialization-config.json 序列化类 predefined-classes-config.json 预定义动态类 框架从编译时 classpath 的 META-INF/native-image/ 读取上述配置。\n使用 Agent 自动生成配置 #Native Image Agent 基于 JVMTI，在应用运行时收集反射、资源等用法，自动生成配置：\njava -cp $CP \\ -agentlib:native-image-agent=config-output-dir=$CONFIG_ROOT/META-INF/native-image \\ com.example.Main 对应用做一次完整功能测试后，Agent 会在 META-INF/native-image/ 下生成 JSON 文件，供后续 native-image 构建使用。\n静态编译 Java 程序 #命令行模式 #最小示例：编译单个 class 或可执行 JAR 的入口类。\n# 1. 先用 javac / mvn 编译出 class 或 jar javac -d out src/com/example/Hello.java # 2. 静态编译 native-image com.example.Hello -o hello # 3. 运行本地二进制（无需 java 命令） ./hello 编译 Spring Boot 可执行 JAR 时，需指定主类并带上完整 classpath：\nnative-image \\ -cp target/myapp.jar \\ --no-fallback \\ -H:+ReportExceptionStackTraces \\ com.example.Application \\ -o myapp-native 常用参数：\n参数 说明 --no-fallback 禁止回退到 JVM 模式（生产环境常用） -H:Name=app 指定输出文件名 -H:ReflectionConfigurationFiles=reflect-config.json 指定反射配置 --initialize-at-build-time=com.example 构建期初始化指定类 首次构建耗时长、内存占用高（建议 -Xmx8g），属正常现象。\n配置文件模式 #在 classpath 的 META-INF/native-image/ 下放置 native-image.properties，构建时自动读取：\nArgs = -H:Class=com.example.demo.NativeDemoApplication \\ --report-unsupported-elements-at-runtime \\ --no-fallback \\ --install-exit-handlers JavaArgs = -Xmx8g ImageName = demo-native 字段 说明 Args 传给 native-image 的参数 JavaArgs 构建过程本身使用的 JVM 参数 ImageName 输出二进制文件名 配合 Agent 生成的 JSON 文件放在同一目录，可实现「配置 + 代码」一体化打包。\nMaven 插件模式 #推荐使用官方 Native Build Tools 插件（org.graalvm.buildtools:native-maven-plugin）。\npom.xml 示例：\n\u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.graalvm.buildtools\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;native-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.10.2\u0026lt;/version\u0026gt; \u0026lt;extensions\u0026gt;true\u0026lt;/extensions\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;build-native\u0026lt;/id\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;compile-no-fork\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;phase\u0026gt;package\u0026lt;/phase\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;imageName\u0026gt;myapp\u0026lt;/imageName\u0026gt; \u0026lt;buildArgs\u0026gt; \u0026lt;buildArg\u0026gt;--no-fallback\u0026lt;/buildArg\u0026gt; \u0026lt;/buildArgs\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; 构建命令：\n# 需要 GraalVM 作为 JAVA_HOME，并安装 native-image ./mvnw -Pnative native:compile # 或 Spring Boot 3.x 常用 ./mvnw -Pnative package 产物通常在 target/myapp（或 ImageName 指定名称），可直接运行。\nSpring Boot 项目还可使用 spring-boot-maven-plugin 配合 native profile，底层同样依赖 GraalVM Native Image。\n参考 # GraalVM 官方文档 Native Image 参考手册 GraalVM GitHub Native Build Tools ","date":"2024-04-11","permalink":"https://niweea.github.io/posts/graalvm/","section":"Posts","summary":"从 HotSpot 生命周期到 GraalVM 架构、Native Image 静态编译与命令行/Maven 构建入门。","title":"GraalVM入门"},{"content":"","date":null,"permalink":"https://niweea.github.io/categories/java/","section":"Categories","summary":"","title":"Java"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/java/","section":"Tags","summary":"","title":"Java"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/native-image/","section":"Tags","summary":"","title":"Native Image"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/","section":"Tags","summary":"","title":"Tags"},{"content":"modules 模块化 #模块化在包之上提供更高层次的聚合。\n列出 JDK 的模块\njava --list-modules 输出结构如下：\njava.base@21 java.compiler@21 java.datatransfer@21 java.desktop@21 java.instrument@21 java.logging@21 java.management@21 java.management.rmi@21 java.naming@21 java.net.http@21 ... @21 表示正在使用的 JDK 的版本。\n查看模型详细信息\njava --describe-module java.base 模块声明 #模块使用module-info.java提供模块信息描述。也就是用于指定模块的依赖项、让其他模块使用的包等的元数据。每个模块声明以关键字 module 开头，紧接着一个专属的模块名称，以及括在括号中的模块主体内容。\nmodule org.example.features { requires java.base; } 模块名称必须是唯一的 module-info.java 不是一个合法的java标识符，因为它包含了破折号。这样做的目的是防止部分IDE将module-info.java或module-info.class 作为普通的Java类加以处理。\nrequires #requires子句用于指定执行的模块还需要哪些模块的支持。\n默认情况下，所有的模块都依赖于名叫java.base的平台模块，它包含了Java主要的包，比如net、io和util。默认情况下，这个模块总是需要的，因此你不需要显式声明。\n隐式可读性 #默认情况下，可读性是不可传递的。例如上图中java.desktop不能通过java.prefs读取java.xml。如果我们希望使用可传递的可读性关系。\nrequires transitive java.xml; exports #exports子句声明了你的模块中哪些包可以被其他模块访问和使用。默认情况下，模块中的所有内容都是被封装的。如果某个类不在导出的包中，就不能通过反射来访问类中的非公共成员。\nimport sun.net.util.IPAddressUtil; String ip = \u0026#34;127.0.0.1\u0026#34;; boolean v4 = IPAddressUtil.isIPv4LiteralAddress(ip); // 错误：软件包 \u0026#39;sun.net.util\u0026#39; 在模块 \u0026#39;java.base\u0026#39; 中声明，但后者没有将它导出到模块 \u0026#39;org.example.features\u0026#39; 解决方案\n--add-exports java.base/sun.net.util=org.example.features exports…to #使用 to 关键字限制可以访问的模块。由多个逗号分隔模块名称。\nexports sun.net.util to java.desktop, java.net.http, jdk.jconsole, jdk.sctp; moditect项目可以自动生成module-info文件\n其他 #uses、provides…with、 open、opens 及 opens…to\n未模块化的代码如何运行在模块化sdk上 #当编译没有模块描述符的代码会被放在未命名模块(unnamed module)中。 未命名模块非常特殊，它可以读取所有其他模块。使用未命名模块，尚未模块化的代码可以继续运行在模块化的JDK上。\n目录结构发生变化 (JEP 220) #JDK 9之后\nbin、conf、 lib、 jmods、include。 jre 目录不再存在，原 65M 大小的java标准库 rt.jar 也不复存在。现在拆分为几十个模块（.jmod扩展名，在jmods目录下），可以按需导入模块。\n其他语言比较 #// javascript export something import { something} from \u0026#39;file\u0026#39; 局部变量类型推断（JEP 286） #局部变量类型推断（JEP 286）（Local-Variable Type Inference ）在其他编程语言早已支持，例如：\nC++ (auto)、C# (var)、Scala/Kotlin (var/val)、Go (声明 := ）、Rust(let)\nJDK 11 提供了一个用来简化局部变量定义的特性。我们可以通过var 关键字启用它。\n// 显式类型： String oldHello = \u0026#34;Hello\u0026#34;; // 类型推断： var newHello = \u0026#34;Hello!\u0026#34;; 通过查看字节码 LocalVariableTable中newHello变量被推断为String类型。\n局部变量类型推断仅限于带有初始化值的局部变量、增强项 for循环中的索引以及传统for循环中声明的局部变量。\nvar nums = Arrays.asList(2, 4, 6, 8); for (var i = 0; i \u0026lt; nums.size(); i++) { System.out.println(nums.get(i)); } for (var i : nums) { System.out.println(i); } 方法签名、构造函数、字段、catch代码块等变量声明都不支持。\n// 编译器推断出类型 var oddNumbers = new ArrayList\u0026lt;Integer\u0026gt;(); // 会出现编译错误 oddNumbers = new LinkedList\u0026lt;Integer\u0026gt;(); 一旦变量被初始化，其类型是固定的，后续的赋值必须与初始化表达式的类型兼容。\n在接口中允许使用私有方法 #在接口中允许使用私有方法（Allow private methods in interfaces ）。 从Java 8开始，Java接口中允许使用默认方法（default methods）和静态方法（static methods）。从Java 9版本开始，Java允许在接口中使用私有方法（private methods）。用于将接口中的重复代码抽象为私有方法，以提高代码的可维护性和可读性。\npublic interface MyInterface { // 接口中的常量,会隐式声明为 public static final int COUNT = 1; /** * 抽象方法 * public abstract void abstractMethod() */ void abstractMethod(); /** * 默认方法 */ default void defaultMethod() { System.out.println(\u0026#34;interface default method\u0026#34;); privateMethod(); // 调用私有方法 } /** * 静态方法 */ static void staticMethod() { System.out.println(\u0026#34;interface static method\u0026#34;); } /** * 私有方法 */ private void privateMethod() { System.out.println(\u0026#34;interface private method\u0026#34;); } /** * 私有静态方法 */ private static void privateStaticMethod() { System.out.println(\u0026#34;interface private static method\u0026#34;); } } 匿名内部类的钻石操作符 #匿名内部类的钻石操作符（Diamond operator for anonymous inner classes ）。钻石操作符（\u0026lt;\u0026gt;）是Java 7中加入的一个非常有用的特性，用来减少冗余类型声明。但是在java 9 之前此特性并不支持匿名内部类（anonymous inner classes）。\n// java 7以后普通类不需要在右侧提及范型类型，使用钻石操作符，编译器可以自行推断。 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); abstract class Calculator\u0026lt;T\u0026gt; { abstract T add(T x, T y); } // 在java 8 中实例化泛型类时不允许省略类型参数，如果省略会提示“无法将 \u0026#39;\u0026lt;\u0026gt;\u0026#39; 用于匿名内部类” Calculator\u0026lt;Integer\u0026gt; calculator = new Calculator\u0026lt;Integer\u0026gt;() { @Override Integer add(Integer x, Integer y) { return x + y; } }; // 在java 9 中匿名内部类允许实例化泛型类时省略类型参数 Calculator\u0026lt;Integer\u0026gt; calculator = new Calculator\u0026lt;\u0026gt;() { @Override Integer add(Integer x, Integer y) { return x + y; } }; JShell REPL(Read–eval–print loop) #JShell（Java Shell），是Java 9引入的交互式编程工具，它允许开发者在命令行界面中即时编写和执行Java代码片段，而不需要创建和编译独立的Java类文件。\n输入表达式可以不包含在方法中 可以省略分号 默认导入常用的包 上/下键可以查看历史记录 TAB键可以提升方法以及javaDoc 将会话保存到文件中(/save) 复杂、多行支持不好 Record Classes 记录类型 （JEPS 395） #java 16中增加了record关键字（Preview in JDK 14 JDK 15），用于表示不可变的数据对象。有助于减少样板代码，提高代码的可读性和可维护性。\n此特性与Kotlin的Data classes、Scala的Case classes相似。\npublic record Point(int x, int y) { public int add() { return x + y; } } 通过查看字节码，发现Point类继承于java.lang.Record抽象类。用final修饰class以及每个字段，编译器还自动为我们创建了构造方法，和字段名同名的方法，以及覆写toString()、equals()和hashCode()方法。\nfinal class Point extends Record { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int add() { return x + y; } public int x() { return this.x; } public int y() { return this.y; } public String toString() { return String.format(\u0026#34;Point[x=%s, y=%s]\u0026#34;, x, y); } public boolean equals(Object o) { ... } public int hashCode() { ... } } 不可变的字段 一个规范的构造器 每个元素都有的访问器方法 equals()、hashCode()、toString() 序列化/反序列化不再需要 serialVersionUID // 显式定义规范构造函数 public record Point(int x, int y) { public Point { if (x \u0026lt; 0) { throw new IllegalArgumentException(\u0026#34;x can\u0026#39;t be negative\u0026#34;); } if (y \u0026lt; 0) { y = 0; } } // 重写 @Override public int x() { return x; } } 其他语言比较 #// kotlin data class Point(val x: Int, val y: Int) // scala case class Point(x: Int, y: Int) Switch Expressions switch表达式 #在JDK14中（Preview in JDK 12 JDK 13）引入switch表达式\nprivate static void showDay(String day) { String result = switch (day) { case \u0026#34;Monday\u0026#34; -\u0026gt; \u0026#34;First day of week\u0026#34;; case \u0026#34;Tuesday\u0026#34;, \u0026#34;Wednesday\u0026#34;, \u0026#34;Thursday\u0026#34; -\u0026gt; \u0026#34;Midweek day\u0026#34;; case \u0026#34;Friday\u0026#34; -\u0026gt; \u0026#34;Last day of week\u0026#34;; default -\u0026gt; { System.out.println(day); yield \u0026#34;Midweek day\u0026#34;; } }; System.out.println(result); } yield关键字与break类似，会终止执行，但是yield会生成一个值。 如果在 switch 中没有涵盖所有 case 的话，它将无法编译通过。\n其他语言比较 #Kotlin if expression 和 when expression\nvar max = if (a \u0026gt; b) a else b val result = when(x) { 1 -\u0026gt; \u0026#34;one\u0026#34; 2 -\u0026gt; \u0026#34;two\u0026#34; else -\u0026gt; \u0026#34;other\u0026#34; } Rust中match表达式\nlet x = 2; let result = match x { 1 =\u0026gt; \u0026#34;one\u0026#34;, 2 =\u0026gt; \u0026#34;two\u0026#34;, _ =\u0026gt; \u0026#34;other\u0026#34;, // `_`相当于Kotlin中的`else`，用来匹配所有其他情况 }; println!(\u0026#34;Result: {}\u0026#34;, result); try-with-resources 语句中允许使用 effectively-final 变量 #try-with-resources 语句中允许使用 effectively-final 变量（Effectively final variables in try-with-resources ）。\n更简洁的try-with-resources 语句, 只要变量被显示的声明为最终变量或者实际上的最终变量都可以使用此新特性。\n// java 8 try (BufferedReader reader = new BufferedReader(new FileReader(\u0026#34;testRead.txt\u0026#34;)); BufferedWriter writer = new BufferedWriter(new FileWriter(\u0026#34;testWrite.txt\u0026#34;))) { // omitted } catch (Exception exception) { } // java 9 BufferedReader reader = new BufferedReader(new FileReader(\u0026#34;testRead.txt\u0026#34;)); BufferedWriter writer = new BufferedWriter(new FileWriter(\u0026#34;testWrite.txt\u0026#34;)); try (reader; writer) { // omitted } catch (Exception exception) { } ⚠️注意事项 # 引用变量被 try-with-resources 释放之后再次使用会触发异常 引用变量无法移除后无法捕获异常，需要在加一层try-catch结构 Text Blocks 文本块（JEPS 378) #java 15（Preview in JDK 13 JDK 14）可以通过使用三重双引号来处理多行文本。此语法与Python/Kotlin一致。\nString json = \u0026#34;{\\n\u0026#34; + \u0026#34; \\\u0026#34;id\\\u0026#34;: \\\u0026#34;6437e92ca47f284a0307c042\\\u0026#34;\\n\u0026#34; + \u0026#34; \\\u0026#34;hash\\\u0026#34;: \\\u0026#34;9222309919699314151\\\u0026#34;,\\n\u0026#34; + \u0026#34; \\\u0026#34;width\\\u0026#34;: 1024,\\n\u0026#34; + \u0026#34; \\\u0026#34;height\\\u0026#34;: 1024\\n\u0026#34; + \u0026#34;}\u0026#34;; json = \u0026#34;\u0026#34;\u0026#34; { \u0026#34;id\u0026#34;: \u0026#34;6437e92ca47f284a0307c042\u0026#34; \u0026#34;hash\u0026#34;: \u0026#34;9222309919699314151\u0026#34;, \u0026#34;width\u0026#34;: %d, \u0026#34;height\u0026#34;: %d } \u0026#34;\u0026#34;\u0026#34;; json = json.formatted(1024, 1024); 为了支持文本块，String 类里添加了一个新的 formatted() 方法\nString singleLine = \u0026#34;\u0026#34;\u0026#34; Hello \\ World \u0026#34;\u0026#34;\u0026#34;; 通过使用\\ 防止换行\n其他语言比较 #// php $str = \u0026lt;\u0026lt;\u0026lt;EOD 这是 一个文本块 EOD; // go text := `这是 一个文本块` // rust let text = r#\u0026#34;这是 一个文本块\u0026#34;#; 有帮助的 NullPointerException 报告机制 # String test = null; test.length(); // java 8 java.lang.NullPointerException // java 21 Exception in thread \u0026#34;main\u0026#34; java.lang.NullPointerException: Cannot invoke \u0026#34;String.length()\u0026#34; because \u0026#34;test\u0026#34; is null at features/org.example.features.nullpointer.Demo.main(Demo.java:6) Sealed Classes 密封类/接口 （JEPS 409） #枚举创建了一个只有固定数量实例的类。JDK 17 （Preview in JDK 15 JDK 16）最终确定引入了密封（sealed）类和密封接口，因此基类或接口可以限制自己能派生出哪些类。提供了更细粒度的继承关系控制。\npublic sealed class Shape permits Triangle, Rectangle, Polygon { } public final class Triangle extends Shape { } public final class Rectangle extends Shape { } public non-sealed class Polygon extends Shape { } // 错误，sealed 层次结构中不允许使用 \u0026#39;Circle\u0026#39; public class Circle extends Shape { } 尝试继承未在permits子句中列出的子类，编译器会产生错误。 除了permits中的子类，不会再有其他子类。\n如果所有的子类都定义在同一个文件中，则不需要 permits 子句。\nsealed 类的子类只能通过下面的某个修饰符来定义。\nfinal： 不允许有进一步的子类 sealed：允许有一组密封子类 non-sealed：一个新关键字，允许未知的子类来继承它 子类继承方式sealed传递了密封性，final确认了密封性，non-sealed显式声明破坏密封性。\n一个 sealed 类必须至少有一个子类 record 也可以用作接口的密封实现\nString Templates 字符串模板 (JEPS 430 Preview) #String customerName = \u0026#34;Java Duke\u0026#34;; String phone = \u0026#34;555-123-4567\u0026#34;; String address = \u0026#34;1 Maple Drive, Anytown\u0026#34;; String json = STR.\u0026#34;\u0026#34;\u0026#34; { \u0026#34;name\u0026#34;: \u0026#34;\\{customerName}\u0026#34;, \u0026#34;phone\u0026#34;: \u0026#34;\\{phone}\u0026#34;, \u0026#34;address\u0026#34;: \u0026#34;\\{address}\u0026#34; } \u0026#34;\u0026#34;\u0026#34;; int index = 0; String data = STR.\u0026#34;\\{index++}, \\{index++}, \\{++index}, \\{index++}, \\{index}\u0026#34;; System.out.println(data); 还提供了FMT模版处理器，其提供了左侧的格式化处理功能。\n通过参数\u0026ndash;enable-preview使用预览特性\n虚拟线程 #TODO:\nHTTP Client 升级 #TODO:\nJVM #TODO:\n其他 # HTTP 2 客户端 \u0026amp; 标准 HTTP Client 升级 改进的 Stream API \u0026amp; 响应式流（Reactive Streams) API 改进的 CompletableFuture API 轻量级的 JSON API 集合工厂方法 socket API重构 参考 # 了解 Java 9 模块 \u0026laquo;Java 9模块化开发\u0026raquo; Java Language Updates A categorized list of all Java and JVM features since JDK 8 to 21 Java 9 到 17 的语言特性更新 ","date":"2023-10-10","permalink":"https://niweea.github.io/posts/java/new-features/","section":"Posts","summary":"\u003ch2 id=\"modules-模块化\" class=\"relative group\"\u003emodules 模块化 \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#modules-%e6%a8%a1%e5%9d%97%e5%8c%96\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h2\u003e\u003cp\u003e模块化在包之上提供更高层次的聚合。\u003c/p\u003e","title":"从Java 8到Java 21的新特性"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/linux/","section":"Tags","summary":"","title":"Linux"},{"content":"简介 #ss（Socket Statistics）是 Linux 系统中用于查看网络连接状态的现代工具，是 netstat 的官方替代品，由 iproute2 工具包提供。\n与 netstat 读取 /proc/net/ 虚拟文件系统不同，ss 直接通过内核的 Netlink 接口获取套接字信息，因此在连接数量大时性能显著更优，输出的信息也更加详细。\n在大多数现代 Linux 发行版中，ss 已预装。如未安装，可通过以下命令获取：\n# RHEL / CentOS / Fedora sudo yum install iproute iproute-doc # Debian / Ubuntu sudo apt install iproute2 参数说明 # 选项 长选项 说明 -h --help 显示帮助信息并退出 -V --version 显示版本信息并退出 -n --numeric 以数字形式显示 IP 地址和端口号，不解析服务名 -r --resolve 解析并显示主机名称 -a --all 显示所有连接，包括监听和非监听状态 -l --listening 仅显示处于监听（LISTEN）状态的套接字 -o --options 显示计时器信息（如 keepalive、retrans） -e --extended 显示详细连接信息（UID、inode 等） -m --memory 显示套接字的内存使用情况 -p --processes 显示占用套接字的进程名称和 PID -i --info 显示 TCP 内部信息（拥塞窗口、RTT 等） -s --summary 显示各协议的套接字统计摘要 -4 --ipv4 仅显示 IPv4 套接字 -6 --ipv6 仅显示 IPv6 套接字 -0 --packet 显示 PACKET 套接字信息 -t --tcp 仅显示 TCP 连接 -u --udp 仅显示 UDP 连接 -d --dccp 显示 DCCP 连接信息 -w --raw 显示 RAW 套接字连接信息 -x --unix 显示 UNIX Domain Socket 连接信息 -f --family=FAMILY 指定地址族，如 inet、inet6、unix -Z --context 显示 SELinux 安全上下文信息 -z --context-addr 显示套接字及地址的 SELinux 上下文 -N --net=NSNAME 在指定的网络命名空间中查找连接信息 常用示例 #查看所有连接 #ss -a 显示所有套接字，包括 TCP、UDP、UNIX socket，以及监听和已建立的连接。\n查看 TCP / UDP 连接 ## 仅查看 TCP 连接 ss -t # 仅查看 UDP 连接 ss -u # 同时查看 TCP 和 UDP（含监听状态） ss -tua 查看监听端口 ## 所有监听端口 ss -l # 仅 TCP 监听端口 ss -tl # 仅 UDP 监听端口 ss -ul 查看监听端口及对应进程 #sudo ss -tlnp 这是日常最常用的组合，输出示例：\nState Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:((\u0026#34;sshd\u0026#34;,pid=1234,fd=3)) LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:((\u0026#34;nginx\u0026#34;,pid=5678,fd=6)) LISTEN 0 128 127.0.0.1:3306 0.0.0.0:* users:((\u0026#34;mysqld\u0026#34;,pid=9012,fd=21)) 各参数含义：-t TCP、-l 监听、-n 不解析域名、-p 显示进程。\n查看统计摘要 #ss -s 输出示例：\nTotal: 432 TCP: 28 (estab 12, closed 3, orphaned 0, timewait 3) Transport Total IP IPv6 RAW 0 0 0 UDP 8 6 2 TCP 25 18 7 INET 33 24 9 FRAG 0 0 0 快速了解当前系统套接字总体状况，适合日常巡检。\n过滤特定端口 #ss 支持强大的过滤表达式，无需借助 grep：\n# 查看本地 80 端口的连接 ss -tnp sport = :80 # 查看目标端口为 443 的连接 ss -tnp dport = :443 # 查看本地端口范围 8000~9000 ss -tnp sport gt :8000 and sport lt :9000 过滤特定 IP ## 查看来自指定 IP 的连接 ss -tn dst 192.168.1.100 # 查看发往指定网段的连接 ss -tn dst 10.0.0.0/24 查看 UNIX Domain Socket #ss -x # 仅查看监听的 UNIX socket ss -xl 常用于排查本机进程间通信（如 Nginx 与 PHP-FPM 的 socket 通信）。\n查看 TCP 内部详情 #ss -tni 输出示例：\nESTAB 0 0 [::ffff:10.67.73.70]:39424 [::ffff:10.60.64.75]:3717 cubic wscale:9,9 rto:207 rtt:6.747/10.607 ato:40 mss:1440 pmtu:1500 rcvmss:820 advmss:1460 cwnd:10 bytes_sent:500 bytes_acked:501 bytes_received:4765026 segs_out:5815 segs_in:5814 data_segs_out:2 data_segs_in:5811 send 17074255bps lastsnd:58158181 lastrcv:9270 lastack:39296 pacing_rate 34147240bps delivery_rate 7097960bps delivered:3 app_limited busy:44ms rcv_rtt:233260 rcv_space:65622 rcv_ssthresh:4195079 minrtt:1.623 snd_wnd:41984 详细说明（按行解读）：\n第一行（连接基本信息） ESTAB：连接状态为已建立（ESTABLISHED）。 前两个 0 0：分别是 Recv-Q 和 Send-Q，表示当前收发队列长度。 [::ffff:10.67.73.70]:39424：本地地址与端口，属于 IPv4-mapped IPv6 表示法（实际 IPv4 地址是 10.67.73.70）。 [::ffff:10.60.64.75]:3717：对端地址与端口（实际 IPv4 地址是 10.60.64.75）。 第二行（拥塞控制与时延基础参数） cubic：当前拥塞控制算法。 wscale:9,9：发送/接收窗口扩大因子。 rto:207：重传超时时间（毫秒），过大通常代表链路质量一般或抖动明显。 rtt:6.747/10.607：RTT 平均值/抖动（毫秒），后者通常可近似理解为 RTT 方差带来的波动。 ato:40：延迟 ACK 相关定时（毫秒）。 mss:1440：当前连接使用的最大报文段大小（Maximum Segment Size）。 pmtu:1500：路径 MTU（Path MTU）。 第三行（接收能力与拥塞窗口） rcvmss:820：接收方向观测到的 MSS。 advmss:1460：通告给对端的 MSS。 cwnd:10：拥塞窗口大小（单位通常可近似看作 MSS 个数）。 bytes_sent:500：已发送字节总量。 bytes_acked:501：被对端确认的字节数。 bytes_received:4765026：已接收字节总量。 第四行（确认与分段计数） segs_out:5815 / segs_in:5814：发送/接收 TCP 段数量。 data_segs_out:2 / data_segs_in:5811：仅统计“携带数据”的 TCP 段，能反映业务方向是否明显偏单向。 第五行（瞬时发送速率与最近活动时间） send 17074255bps：当前估算发送速率。 lastsnd:58158181：距上次发送经过的时间（毫秒）；数值很大说明本端近期几乎不发送数据。 lastrcv:9270：距上次接收经过的时间（毫秒）。 lastack:39296：距上次 ACK 的时间（毫秒）。 pacing_rate 34147240bps：内核 pacing 限速估算值，用于平滑发包。 第六行（有效吞吐与应用限速信号） delivery_rate 7097960bps：有效交付速率（比 send 更贴近“真正送达对端”的吞吐）。 delivered:3：已成功交付的数据段统计。 app_limited：发送受应用供数速度限制，而非网络瓶颈（应用写得慢时常见）。 busy:44ms：发送路径处于忙碌状态的累计时间片段。 第七行（接收方向时延与窗口） rcv_rtt:233260：接收方向估算 RTT（微秒，约等于 233ms）。 rcv_space:65622：当前接收窗口可用空间。 rcv_ssthresh:4195079：接收侧慢启动阈值相关指标。 minrtt:1.623：观测到的最小 RTT（毫秒），可作为链路“理想时延”参考。 snd_wnd:41984：对端通告给本端的发送窗口，过小会限制本端可发送数据量。 快速判读建议：\nbytes_retrans 持续上涨 + rtt 抖动变大：优先怀疑链路丢包/拥塞。 cwnd 长期较小且上不去：可能受丢包或拥塞控制收敛限制。 Send-Q 长期堆积：对端接收慢或网络瓶颈。 出现 app_limited 且吞吐低：先排查应用写入速率，不一定是网络问题。 查看计时器信息 #ss -tno 输出示例：\nESTAB 0 0 10.0.0.1:22 10.0.0.2:51234 timer:(keepalive,1min52sec,0) 可以看到 keepalive、retrans 等计时器的剩余时间，用于判断连接是否即将超时。\n实践案例 #案例一：排查端口被占用 #部署服务时提示 address already in use，快速定位占用进程：\nsudo ss -tlnp | grep :8080 输出：\nLISTEN 0 128 *:8080 *:* users:((\u0026#34;java\u0026#34;,pid=23456,fd=88)) 发现是 PID 为 23456 的 Java 进程占用了 8080 端口，执行 kill 23456 或 systemctl stop \u0026lt;service\u0026gt; 释放端口。\n案例二：统计 TCP 连接状态分布 #服务器响应变慢，快速统计各 TCP 状态的连接数量：\nss -tn | awk \u0026#39;NR\u0026gt;1 {print $1}\u0026#39; | sort | uniq -c | sort -rn 输出示例：\n1024 ESTABLISHED 512 TIME_WAIT 32 CLOSE_WAIT 8 SYN_SENT TIME_WAIT 过多说明短连接频繁，可考虑开启连接复用；CLOSE_WAIT 过多说明服务端代码未及时关闭连接，需排查业务逻辑。\n案例三：监控指定服务的连接数 #实时统计 Nginx（80/443 端口）当前的并发连接数：\n# 统计 80 端口 ESTABLISHED 连接数 ss -tn state established sport = :80 | wc -l # 同时统计 80 和 443 ss -tn state established \u0026#39;( sport = :80 or sport = :443 )\u0026#39; | wc -l 案例四：排查 MySQL 远程连接 #检查 MySQL 是否在监听远程连接，以及当前有哪些 IP 连入：\n# 查看 MySQL 监听情况 sudo ss -tlnp | grep :3306 # 查看当前所有 MySQL 连接及来源 IP sudo ss -tnp dst :3306 若发现 MySQL 监听在 0.0.0.0:3306（而非 127.0.0.1:3306），需检查防火墙规则和 MySQL 的 bind-address 配置，避免安全风险。\n常用组合速查 # 场景 命令 查看监听端口及进程 sudo ss -tlnp 查看所有 TCP 连接 ss -tn 统计各 TCP 状态数量 ss -tn | awk 'NR\u0026gt;1 {print $1}' | sort | uniq -c 过滤指定端口连接 ss -tnp sport = :端口号 查看 TCP 底层指标 ss -tni 查看套接字统计摘要 ss -s 查看 UNIX socket ss -xlp 查看计时器信息 ss -tno 与 netstat 命令对比 # 功能 netstat ss 查看所有连接 netstat -a ss -a 查看监听端口及进程 sudo netstat -tlnp sudo ss -tlnp 过滤特定端口 需配合 grep ss -tnp sport = :80 过滤特定 IP 需配合 grep ss -tn dst 192.168.1.1 查看 TCP 底层指标 不支持 ss -tni 查看内存使用 不支持 ss -m 大量连接时性能 较慢（读取 /proc） 快（Netlink 接口） 系统预装情况 需安装 net-tools 现代发行版默认预装 参考 # man ss iproute2 源码仓库 ","date":"2023-01-29","permalink":"https://niweea.github.io/posts/network/ss/","section":"Posts","summary":"\u003ch2 id=\"简介\" class=\"relative group\"\u003e简介 \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#%e7%ae%80%e4%bb%8b\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h2\u003e\u003cp\u003e\u003ccode\u003ess\u003c/code\u003e（Socket Statistics）是 Linux 系统中用于查看网络连接状态的现代工具，是 \u003ccode\u003enetstat\u003c/code\u003e 的官方替代品，由 \u003ccode\u003eiproute2\u003c/code\u003e 工具包提供。\u003c/p\u003e","title":"Linux ss 命令详解"},{"content":"","date":null,"permalink":"https://niweea.github.io/categories/linux-%E5%B7%A5%E5%85%B7/","section":"Categories","summary":"","title":"Linux 工具"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/","section":"Tags","summary":"","title":"命令行"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/%E7%BD%91%E7%BB%9C/","section":"Tags","summary":"","title":"网络"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/%E8%BF%90%E7%BB%B4/","section":"Tags","summary":"","title":"运维"},{"content":"简介 #netstat（network statistics）是 Linux 系统下一款用于查看网络状态信息的经典工具。它可以显示网络连接、路由表、接口统计、伪装连接、多播成员等信息，是网络排查和运维监控的常用利器。\n注意： 在较新的 Linux 发行版中，netstat 已被 ss 命令逐步替代，但 netstat 在生产环境中仍被广泛使用。可通过安装 net-tools 包来获取 netstat。\n# Debian / Ubuntu sudo apt install net-tools # RHEL / CentOS sudo yum install net-tools 参数说明 # 参数 说明 -a 显示所有连接和侦听端口（包括 TCP、UDP） -c 持续输出，每隔一秒刷新一次连接信息 -e 显示网络数据包的统计信息（入站/出站数量、错误数等） -g 显示多播组（Multicast Group）成员信息 -i 显示网络接口的统计信息 -l 仅显示处于监听（LISTEN）状态的端口 -n 不解析主机名和服务名，直接显示 IP 地址和端口号 -p 显示占用端口的进程名称及 PID -r 显示内核路由表 -s 显示各协议（TCP、UDP、ICMP 等）的统计信息 -t 仅显示 TCP 协议的连接 -u 仅显示 UDP 协议的连接 -v 显示详细输出信息 -w 显示 RAW 套接字的连接信息 常用示例 #查看所有连接 #netstat -a 显示所有处于活动状态和监听状态的连接，包含 TCP 和 UDP。\n查看 TCP / UDP 连接 ## 仅查看 TCP 连接 netstat -t # 仅查看 UDP 连接 netstat -u # 同时查看 TCP 和 UDP netstat -tu 查看监听端口 ## 显示所有监听端口 netstat -l # 仅显示监听中的 TCP 端口 netstat -lt # 仅显示监听中的 UDP 端口 netstat -lu 查看端口对应的进程 ## 需要 root 权限才能看到所有进程信息 sudo netstat -p # 查看监听端口及其进程（常用组合） sudo netstat -tlnp 输出示例：\nProto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1234/sshd tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 5678/nginx 不解析域名（加快输出速度） #netstat -n # 实用组合：查看所有 TCP 连接，不解析域名，显示进程 sudo netstat -tnp 加上 -n 参数后，输出中的主机名和服务名将直接以 IP 和端口号显示，速度更快。\n查看网络接口信息 #netstat -i 输出各网络接口的收发包数量、错误数、丢包数等统计信息，类似 ifconfig 的统计部分。\n查看路由表 #netstat -r # 等价于 route -n，加 -n 不解析主机名 netstat -rn 查看协议统计信息 #netstat -s 分协议（TCP、UDP、ICMP、IP 等）显示数据包收发、错误、重传等详细统计，适合排查网络异常。\n持续监控连接 #netstat -c 每隔 1 秒自动刷新并输出当前连接状态，适合实时观察网络变化。\n查看多播组信息 #netstat -g 显示当前系统加入的多播组列表，用于排查组播相关问题。\n常用组合速查 # 场景 命令 查看所有监听端口及进程 sudo netstat -tlnp 查看所有 TCP 连接状态 netstat -tn 统计各 TCP 状态数量 netstat -tn | awk '{print $6}' | sort | uniq -c 查看路由表 netstat -rn 实时监控连接 netstat -c 查看网络接口统计 netstat -i 查看协议详细统计 netstat -s TCP 连接状态说明 #使用 netstat -t 时，State 列会显示 TCP 连接的当前状态，常见状态如下：\n状态 说明 LISTEN 监听中，等待客户端连接 ESTABLISHED 连接已建立，正在通信 TIME_WAIT 连接已关闭，等待超时后释放 CLOSE_WAIT 等待本端关闭连接 SYN_SENT 已发送 SYN，等待对端响应 SYN_RECV 收到 SYN，等待 ACK 确认 FIN_WAIT1 已发送 FIN，等待对端响应 FIN_WAIT2 收到对端 ACK，等待对端 FIN CLOSING 双方同时发起关闭 CLOSED 连接已完全关闭 与 ss 命令对比 #ss 是 netstat 的现代替代品，速度更快，输出更丰富：\n功能 netstat ss 查看所有连接 netstat -a ss -a 查看监听端口 netstat -tlnp ss -tlnp 查看 TCP 状态 netstat -tn ss -tn 过滤特定端口 需配合 grep ss -tnp sport = :80 在系统资源有限或连接数量较大时，推荐优先使用 ss。\n参考 # man netstat net-tools GitHub Linux ss 命令文档：man ss ","date":"2023-01-15","permalink":"https://niweea.github.io/posts/network/netstat/","section":"Posts","summary":"\u003ch2 id=\"简介\" class=\"relative group\"\u003e简介 \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#%e7%ae%80%e4%bb%8b\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h2\u003e\u003cp\u003e\u003ccode\u003enetstat\u003c/code\u003e（network statistics）是 Linux 系统下一款用于查看网络状态信息的经典工具。它可以显示网络连接、路由表、接口统计、伪装连接、多播成员等信息，是网络排查和运维监控的常用利器。\u003c/p\u003e","title":"Linux netstat 命令详解"},{"content":"简介 #lsof（List Open Files）用于列出当前系统上进程已打开的文件描述符及其关联信息。在 Unix/Linux 中，「一切皆文件」：普通文件、目录、块设备、管道、套接字等都会以文件形式出现在进程的文件描述符表中，因此 lsof 不仅能查磁盘上的文件，还能查 网络连接、管道、设备节点 等。\n典型用途包括：排查端口占用、查找删除后仍被占用的文件、确认某进程打开了哪些库或日志、审计用户或服务的资源使用情况。\n多数发行版已预装。若提示未找到命令，可安装：\n# RHEL / CentOS / Fedora / Rocky sudo yum install lsof # Debian / Ubuntu sudo apt install lsof 参数说明 # 选项 说明 -h 显示帮助摘要 -v 显示版本信息 -a 与（AND）：同时满足其后多个筛选条件时必须加 -a，否则条件之间默认是 或（OR） -c \u0026lt;前缀\u0026gt; 匹配命令名以该字符串开头的进程（如 -c ssh 匹配 sshd） -p \u0026lt;PID\u0026gt; 仅列出指定进程 ID -u \u0026lt;用户\u0026gt; 列出指定用户打开的文件；前缀 ^ 表示排除该用户 -U 仅列出 UNIX Domain Socket -d \u0026lt;描述符\u0026gt; 按文件描述符筛选，如 1、cwd、txt、mem；支持逗号列表与范围 -n 不将 IP 解析为主机名（加快输出、避免 DNS 依赖） -P 不将端口号解析为服务名（显示数字端口） -i 列出网络相关打开项；可配合协议、地址、端口等进一步限定 +d \u0026lt;目录\u0026gt; 列出某目录下被打开的文件（不递归） +D \u0026lt;目录\u0026gt; 递归列出目录下被打开的文件（可能较慢，生产环境慎用） -t 仅输出 PID（适合脚本 kill 等场景） -r [秒] 每隔指定秒数重复执行；r 后无数字则每秒一次，r 可多次叠加间隔 -w 抑制部分警告信息 -e 可配合 -p 等在无法访问内核信息时跳过（具体行为见 man lsof） 网络筛选常用写法（跟在 -i 后）：\n形式 含义 -i4 / -i6 仅 IPv4 / 仅 IPv6 -iTCP / -iUDP 仅 TCP / 仅 UDP -iTCP:80 TCP 且涉及端口 80（监听或连接均可匹配，视内核信息而定） -i @192.168.1.1 与指定主机相关的套接字 -i :3306 与指定端口相关的网络文件 -sTCP:LISTEN TCP 状态为监听（需与 -i 组合使用） 常用示例 #列出所有打开文件（信息量大） #sudo lsof 默认列包括：COMMAND、PID、USER、FD（文件描述符）、TYPE、DEVICE、SIZE/OFF、NODE、NAME 等。普通用户只能看到自己有权限的进程；查看全系统通常需要 sudo。\n按进程查看 ## 指定 PID lsof -p 1234 # 命令名前缀（匹配以 nginx 开头的进程名） sudo lsof -c nginx 按用户查看 #lsof -u www-data # 排除某用户 sudo lsof -u ^root 网络：查看所有网络连接相关项 #sudo lsof -i -n -P -n、-P 避免解析主机名和服务名，输出更稳定、更快。\n查看监听某端口的进程 #sudo lsof -iTCP:8080 -sTCP:LISTEN -n -P 若服务只监听 IPv6，可同时尝试：\nsudo lsof -iTCP:8080 -sTCP:LISTEN -n -P sudo ss -tlnp | grep 8080 （与 ss -tlnp 对照时，lsof 更侧重「进程 ↔ 打开的文件描述符」视角。）\n查看某端口上的所有 TCP 套接字（含已建立连接） #sudo lsof -iTCP:80 -n -P UNIX Domain Socket #lsof -U 常用于排查本机 socket 文件（如 MySQL、PHP-FPM）被谁占用。\n查看某文件或目录被谁占用 ## 单个文件 lsof /var/log/nginx/access.log # 目录内被打开的文件（不递归） sudo lsof +d /var/lib/mysql # 递归（慎用，目录大时很慢） sudo lsof +D /data/app 文件被删除但仍占用磁盘（空间不释放） #当文件已从目录中 unlink 但仍有进程保持打开时，lsof 会显示 (deleted)：\nsudo lsof +L1 # 或针对某路径/进程排查 sudo lsof -p \u0026lt;PID\u0026gt; | grep deleted 处理思路：结束占用进程或让其执行关闭/轮转，空间才会在文件系统层面回收。\n仅输出 PID（便于脚本） #sudo lsof -t -iTCP:8080 -sTCP:LISTEN 可配合 xargs：\nsudo kill $(sudo lsof -t -iTCP:8080 -sTCP:LISTEN) 使用前务必确认 PID 正确，避免误杀。\n组合条件（AND） #默认多个 -u、-i 等是 或 关系。要同时满足，例如「用户 nginx 且网络相关」：\nsudo lsof -a -u nginx -i -n -P 输出列简读 #典型一行含义概要：\nCOMMAND：进程名（可能被截断，短名与完整路径以 man 说明为准）。 PID / USER：进程号与运行用户。 FD：如 cwd 当前工作目录、rtd 根目录、txt 程序文本段、mem 内存映射库、数字为描述符；末尾状态如 u 读写、r 读、w 写等。 TYPE：REG 普通文件、DIR 目录、CHR 字符设备、IPv4/IPv6 网络、unix 本地套接字等。 NAME：路径、对端地址:端口、或 socket 路径等。 实践案例 #案例一：部署报错「Address already in use」 #sudo lsof -iTCP:8080 -sTCP:LISTEN -n -P 确认进程名与 PID 后，再决定停止服务或更换端口。\n案例二：磁盘满但 du 与 df 不一致 #大量空间被已删除但仍被进程持有的文件占用：\nsudo lsof +L1 | grep deleted 对相应进程做日志轮转、重启或优雅关闭，释放 inode 与块。\n案例三：无法卸载挂载点（device is busy） #sudo lsof +D /mnt/data 查看仍有打开文件的进程，结束后再 umount。\n案例四：确认某配置文件是否被进程加载 #sudo lsof /etc/nginx/nginx.conf 可辅助判断重载后是否仍有旧 worker 持有旧配置相关资源（需结合 Nginx 实际行为与部署方式）。\n案例五：查找连接到指定数据库端口的客户端 #sudo lsof -iTCP:3306 -n -P 快速看到哪些本机进程作为客户端连向 3306，或哪些监听与连接并存（视输出而定）。\n常用组合速查 # 场景 命令 谁监听某端口 sudo lsof -iTCP:端口 -sTCP:LISTEN -n -P 某端口所有网络项 sudo lsof -iTCP:端口 -n -P 某进程打开的所有文件 lsof -p PID 某文件被谁打开 lsof /path/to/file 已删除仍占用的文件 sudo lsof +L1 | grep deleted 仅要 PID sudo lsof -t -iTCP:端口 -sTCP:LISTEN 某用户网络相关 sudo lsof -a -u 用户名 -i -n -P UNIX socket lsof -U 与 ss / fuser 的简要对比 # 场景 更趁手的工具 快速看全机监听与 TCP 状态统计 ss（Netlink，适合大量连接） 从进程—文件描述符角度查端口、文件、设备 lsof 对某文件或套接字文件发信号或批量杀进程 fuser（如 fuser -k /path） 三者常配合使用：ss 看连接全景，lsof 追到具体进程与打开对象，fuser 在确认路径后直接作用于占用者。\n参考 # man lsof lsof-org/lsof（源码与说明） ","date":"2023-01-12","permalink":"https://niweea.github.io/posts/network/lsof/","section":"Posts","summary":"\u003ch2 id=\"简介\" class=\"relative group\"\u003e简介 \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#%e7%ae%80%e4%bb%8b\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h2\u003e\u003cp\u003e\u003ccode\u003elsof\u003c/code\u003e（List Open Files）用于列出当前系统上进程已打开的\u003cstrong\u003e文件描述符\u003c/strong\u003e及其关联信息。在 Unix/Linux 中，「一切皆文件」：普通文件、目录、块设备、管道、套接字等都会以文件形式出现在进程的文件描述符表中，因此 \u003ccode\u003elsof\u003c/code\u003e 不仅能查磁盘上的文件，还能查 \u003cstrong\u003e网络连接、管道、设备节点\u003c/strong\u003e 等。\u003c/p\u003e","title":"Linux lsof 命令详解"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/jvm/","section":"Tags","summary":"","title":"JVM"},{"content":"JVM 概览 # 上图为 HotSpot JVM 中 Java 代码的执行流程：.java 经 javac 编译为 .class 字节码，类加载器载入后进入运行时数据区，由解释器或 JIT 编译后的本地代码执行。\n运行时数据区 #JVM 在运行时会划分以下内存区域（逻辑概念，具体实现因 VM 而异）：\n区域 线程共享 说明 程序计数器（PC Register） 否 当前线程执行的字节码行号指示器 Java 虚拟机栈 否 栈帧：局部变量表、操作数栈、动态链接、方法出口 本地方法栈 否 为 Native 方法服务 堆（Heap） 是 对象实例、数组的分配区域，GC 主要工作区 方法区（Method Area） 是 类信息、常量、静态变量；Java 8 起元数据在 Metaspace 运行时常量池 是 方法区的一部分，存放编译期生成的字面量与符号引用 Java 虚拟机解释器（Interpreter）的执行流程如下面的伪代码：\ndo { 自动计算 pc 寄存器以及从 pc 寄存器的位置取出操作码； if (存在操作码) 取出操作数； 执行操作码所定义的操作； } while (处理下一次循环); 热点代码会由 JIT 编译器编译为本地机器码，存入 Code Cache，后续直接执行机器码而非逐条解释字节码。\n字节码指令集简介 #了解 class 文件 #Java 源代码在执行之前要经历一系列变换。首先就是使用 Java 编译器 javac 进行的编译阶段，将 Java 代码转换为包含字节码的 .class 文件。字节码是一种中间表示，没有与特定的机器架构绑定。\npublic class Test { public static void main(String[] args) { System.out.println(\u0026#34;Hello Bytecode!\u0026#34;); } } 使用 JDK 中自带的 javac 命令将 Test.java 源文件编译成 class 文件，就可以生成 Test.class 文件。\njavac Test.java # output: Test.class 使用十六进制查看工具查看 Test.class 文件内容：\n其具体的结构如下面代码所示。\nClassFile { u4 magic; // 魔数 0xCAFEBABE u2 minor_version; // 该类次版本号 u2 major_version; // 该类主版本号 u2 constant_pool_count; // 常量数 cp_info constant_pool[constant_pool_count-1]; // 该类的常量池 u2 access_flags; // 访问标志 u2 this_class; // 该类的类名 u2 super_class; // 超类名 u2 interfaces_count; // 该类的实现的接口数量 u2 interfaces[interfaces_count]; // 该类的实现的接口 u2 fields_count; // 字段数 field_info fields[fields_count]; // 该类的所有字段 u2 methods_count; // 方法数 method_info methods[methods_count]; // 该类的所有方法 u2 attributes_count; // 属性数 attribute_info attributes[attributes_count]; // 该类的所有属性（比如，源文件的名字，等等） } u1、u2、u4 三种数据结构表示 1、2、4 字节无符号整数\n《Optimizing Java》一书作者帮我们编了一句顺口溜：My Very Cute Animal Turns Savage In Full Moon Areas\n魔数 #每个类文件都以魔数（magic number）0xCAFEBABE 开始，前面的 4 个以十六进制表示的字节表示当前文件符合 class 文件格式。\n可以通过此链接 GCK\u0026rsquo;S FILE SIGNATURES TABLE 查看其他文件的魔数值。\n版本号 #随后的 4 个字节表示用于编译该类文件的主版本号和次版本号。Test.class 文件中的主版本号为 52（0x34），表示是使用 Java 8 编译的。\n每次大版本发布，主版本号加 1。Java 8 为 52，Java 21 为 65。\n常量池 #常量池的作用类似于 C 语言中的符号表（Symbol Table），也是 class 文件中第一个出现的变长结构。\n常量池结构由两部分组成：\n常量池大小：u2 两字节\n常量池项：由 1 字节 tag 和具体内容组成\ncp_info { u1 tag; u1 info[]; } 常量池真正有效的索引是 1 ～ n-1，0 属于保留索引。\n下表列出 JVM 规范中常见的常量池 tag 类型（Java 8 使用 tag 1～18，Java 9 起增加 Module / Package）：\n常量池类型\n类型 tag 值 Java SE CONSTANT_Utf8 1 1.0 CONSTANT_Integer 3 1.0 CONSTANT_Float 4 1.0 CONSTANT_Long 5 1.0 CONSTANT_Double 6 1.0 CONSTANT_Class 7 1.0 CONSTANT_String 8 1.0 CONSTANT_Fieldref 9 1.0 CONSTANT_Methodref 10 1.0 CONSTANT_InterfaceMethodref 11 1.0 CONSTANT_NameAndType 12 1.0 CONSTANT_MethodHandle 15 7 CONSTANT_MethodType 16 7 CONSTANT_InvokeDynamic 18 7 CONSTANT_Dynamic 17 11 CONSTANT_Module 19 9 CONSTANT_Package 20 9 CONSTANT_Integer 和 CONSTANT_Float #CONSTANT_Integer/Float { u1 tag; u4 bytes; } 两者都是使用 4 字节来表示具体的数值常量。\nJava 语言中定义的 boolean、byte、short、char 类型的变量在常量池中都会被当作 int 处理。\nCONSTANT_Long 和 CONSTANT_Double #CONSTANT_Long/Double { u1 tag; u4 high_bytes; u4 low_bytes; } 采用 big-endian（high byte first）存储：\n((long) high_bytes \u0026lt;\u0026lt; 32) + low_bytes Long 和 Double 占两个常量池槽位，下一索引不可用。\nCONSTANT_Utf8 #CONSTANT_Utf8 { u1 tag; u2 length; u1 bytes[length]; } length 表示 MUTF-8 编码的字节数组长度，bytes[length] 为具体字节。\nMUTF-8 与 UTF-8 区别：\n采用两个字节表示空字符（\\0） 只使用单字节、双字节、三字节；四字节字符用「代理对」（双字符）表示 CONSTANT_String #用来表示 java.lang.String 类型的常量对象。CONSTANT_Utf8 存储字符串真正的内容，而 CONSTANT_String 并不包含字符串内容，仅包含指向 CONSTANT_Utf8 的索引。\nCONSTANT_String { u1 tag; u2 string_index; } CONSTANT_Class #用来表示类或者接口，结构与 CONSTANT_String 类似。\nCONSTANT_NameAndType #用来表示字段或方法的名称与描述符：\nCONSTANT_NameAndType { u1 tag; u2 name_index; u2 descriptor_index; } tag 值固定为 12 name_index 指向 CONSTANT_Utf8，为字段或方法名 descriptor_index 指向 CONSTANT_Utf8，为字段或方法的类型描述符 CONSTANT_Fieldref、CONSTANT_Methodref、CONSTANT_InterfaceMethodref #CONSTANT_Fieldref/Methodref/InterfaceMethodref { u1 tag; u2 class_index; u2 name_and_type_index; } class_index 指向 CONSTANT_Class，表示字段或方法所在的类 name_and_type_index 指向 CONSTANT_NameAndType CONSTANT_MethodHandle、CONSTANT_MethodType、CONSTANT_InvokeDynamic #从 JDK 7 起为支持动态语言调用新增。CONSTANT_InvokeDynamic 为 invokedynamic 指令提供引导方法（bootstrap method）的引用。\n总结 #可以通过 javap 命令或者 jclasslib 工具查看常量池信息。\nAccess flags #在常量池之后是访问标记，用来表示一个类是否为 final、abstract、interface、annotation、enum 等。两字节，16 个标记位。\nthis_class、super_class、interfaces #这三个字段用来确定继承关系：\nthis_class：当前类索引 super_class：直接父类索引 interfaces：实现的直接父接口列表 均指向常量池索引。\n字段表 #字段表是变长结构，类中定义的字段存储在这个集合中：\nfield_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; } 字段描述符 # 描述符 类型 说明 B byte C char D double F float I int J long S short Z boolean L ClassName; 引用类型 L + 全限定名 + ;，如 Ljava/lang/String; [ 一维数组 [I = int[] [[ 多维数组 [[I = int[][] 方法表 #方法表也是变长结构，类中定义的方法存储在这里：\nmethod_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; } 方法描述符格式：(参数1类型 参数2类型 ...) 返回值类型\n(Ljava/lang/String;)V V 表示 void。\n属性表 #attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; } 常见属性：\nConstantValue：出现在 field_info 中，表示静态变量初始值 Code：出现在 method_info 中，包含方法的字节码 LineNumberTable：Code 的附属属性，存放源码行号与字节码偏移的对应关系 javap 使用 #javap [options] [classes]\n默认显示 public、protected 和默认级别的方法。\n选项 作用 -p 显示 private 方法和字段 -s 输出类型描述符签名 -c 反编译，显示方法内字节码 -v 详细信息：版本号、访问权限、常量池 -l 行号表和局部变量表（需 javac -g 编译） 字节码指令 #Java 虚拟机的指令由 1 字节的操作码（opcode）以及 0 至多个操作数构成。\nopcode [\u0026lt;operand1\u0026gt;, \u0026lt;operand2\u0026gt;]\n0 getstatic #2 \u0026lt;java/lang/System.out : Ljava/io/PrintStream;\u0026gt; 3 ldc #3 \u0026lt;Hello world!\u0026gt; 5 invokevirtual #4 \u0026lt;java/io/PrintStream.println : (Ljava/lang/String;)V\u0026gt; 8 return 虚拟机指令集 #按用途可分为：常量、加载、存储、栈、数学、转换、比较、控制、引用等类别。\n方法调用指令 # 指令 用途 invokestatic 调用静态方法 invokespecial 调用构造器 \u0026lt;init\u0026gt;、私有方法、父类方法 invokevirtual 调用虚方法（大多数实例方法） invokeinterface 调用接口方法 invokedynamic 运行时动态解析引导方法，再执行目标方法 前四条指令的分派逻辑由 JVM 内置；invokedynamic 的分派逻辑由用户提供的 bootstrap method 决定（lambda、Stream 等依赖此机制）。\n非虚方法（Non-Virtual Method）：静态方法、私有方法、构造器、父类方法，以及 final 方法。这类方法在类加载的解析阶段即可确定唯一版本。\n虚方法（Virtual Method）：其余可被覆盖的实例方法，通过 invokevirtual / invokeinterface 在运行时按实际对象类型分派（动态分派）。\n字符串 # 【推荐】循环体内，字符串的连接方式，使用 StringBuilder 的 append 方法进行扩展。\n说明：反编译出的字节码显示，每次循环都会 new 一个 StringBuilder，再 append，最后 toString，造成额外分配。\nString str = \u0026#34;start\u0026#34;; for (int i = 0; i \u0026lt; 100; i++) { str = str + \u0026#34;hello\u0026#34;; } Java 8 编译上述代码的字节码：\n自动装箱/拆箱 #自动装箱、拆箱在编译期被转换为包装类方法调用。以 int / Integer 为例：\n装箱：Integer.valueOf(int) 拆箱：Integer.intValue() try-catch-finally 以及 try-with-resources 的字节码原理 #阿里《Java 开发手册》中关于异常处理的规范，可以从字节码层面理解其原因。\n异常处理 7.【强制】不要在 finally 块中使用 return\n说明：try 块中的 return 执行成功后并不马上返回，而是继续执行 finally；若 finally 中也有 return，会直接返回，丢弃 try 中的返回点。\nclass TryCatchFinallyDemo { public static void main(String[] args) { try { int i = 1 / 0; System.out.println(\u0026#34;try\u0026#34;); } catch (Exception exception) { System.out.println(\u0026#34;catch\u0026#34;); } finally { System.out.println(\u0026#34;finally\u0026#34;); } } } 现代 Java 编译器采用复制 finally 代码块的方式，将其插入 try 和 catch 中所有正常退出与异常退出路径之前，因此 finally 一定会执行。\n异常处理 6.【强制】必须对资源对象、流对象进行关闭，有异常也要做 try-catch。\n说明：JDK 7+ 可使用 try-with-resources。\npublic void testTryResource(FileReader fileReader) throws IOException { try (BufferedReader br = new BufferedReader(fileReader)) { System.out.println(br.readLine()); } } 编译器会展开为带 finally 关闭逻辑的代码（简化示意）：\nBufferedReader br = new BufferedReader(fileReader); Throwable primary = null; try { System.out.println(br.readLine()); } catch (Throwable t) { primary = t; throw t; } finally { if (br != null) { if (primary != null) { try { br.close(); } catch (Throwable suppressed) { primary.addSuppressed(suppressed); } } else { br.close(); } } } Java 7 为 Throwable 增加了 addSuppressed，用于记录关闭资源时被抑制的异常。\n为什么不依赖 finalize 关闭资源？\nfinalize 执行时机不确定，可能在 GC 时才调用，资源可能长期占用 异常被吞掉，难以排查 Java 9 起 Object.finalize() 已废弃，Java 18 起默认禁用 因此应使用 try-with-resources 或显式 close()，而不是指望 GC 回收时自动关闭。\nlambda 表达式的原理 #Java 8 的 lambda 不是匿名内部类的语法糖（早期编译器可能生成内部类，现代 javac 使用 invokedynamic）。\nRunnable r = () -\u0026gt; System.out.println(\u0026#34;hello\u0026#34;); 编译后核心字节码类似：\ninvokedynamic #0:run:()Ljava/lang/Runnable; JVM 在首次执行时调用 bootstrap method（通常是 LambdaMetafactory.metafactory），动态生成实现函数式接口的类实例。好处是：不额外生成 .class 文件、按需生成、性能更好。\n反射的实现原理 #Method.invoke() 在 HotSpot 中的典型路径：\nNative 调用：首次调用走 JNI，开销较大 Inflation（膨胀）：调用次数超过阈值（默认 15 次）后，JVM 生成字节码访问器（Generated Method Accessor），后续走普通方法调用，性能接近直接调用 setAccessible(true)：跳过 Java 访问检查，但仍需处理安全检查与模块限制（Java 9+） 反射打破了编译期类型约束，灵活性高，但首次调用和安全检查有额外成本。\nJava Instrumentation 的原理 #java.lang.instrument 包提供运行时修改字节码的能力，基于 JVMTI 实现。\n两种挂载方式：\n方式 入口 场景 Premain premain(String, Instrumentation) JVM 启动时通过 -javaagent:agent.jar 加载 Agentmain agentmain(String, Instrumentation) 运行时 Attach 到已有进程 常见能力：\naddTransformer：类加载时转换字节码 retransformClasses / redefineClasses：对已加载类重新转换 应用：APM 探针（SkyWalking、Pinpoint）、热部署、单元测试 Mock、Arthas 等。\nJust-In-Time (JIT) 编译器 #《Java 性能权威指南》第四章介绍 JIT 编译器。\n代码格式 11.【推荐】单个方法的总行数不超过 80 行。\n说明：方法过长不利于 JIT 内联与优化，热点方法应保持精简。\nJIT 将热点字节码编译为本地机器码，存入 Code Cache。Code Cache 大小有限，填满后新代码只能解释执行，性能下降。\n常用 JVM 参数（HotSpot，默认值因版本和平台略有差异）：\n参数 含义 默认值（约） InitialCodeCacheSize Code Cache 初始大小 255 KB（Client）/ 160 KB（Server） ReservedCodeCacheSize Code Cache 最大容量 Java 8：240 MB；Java 11+：often 240 MB，可 -XX:ReservedCodeCacheSize=256m 调整 -XX:+UseCodeCacheFlushing 允许在 Cache 满时 flush 冷代码 视版本而定 字节码的应用 #方法重载（静态分派）：编译期根据参数类型选择方法，对应字节码中的 invokestatic / invokespecial 等，在类加载解析阶段即可确定目标。\n方法重写（动态分派）：父类引用指向子类对象时，运行时按实际类型选择方法，对应 invokevirtual / invokeinterface，是 JVM 多态的实现基础。\nclass Parent { void foo() { System.out.println(\u0026#34;parent\u0026#34;); } } class Child extends Parent { void foo() { System.out.println(\u0026#34;child\u0026#34;); } } Parent p = new Child(); p.foo(); // 输出 child，运行时分派 字节码增强与 Java Agent #编译后的 .class 仍是可读写、可变换的中间产物。在类加载前（改磁盘文件）或类加载时（ClassFileTransformer），可以对字节码进行插入、替换、删除指令，从而在不改源码的情况下改变程序行为。\n源码 (.java) ↓ javac 字节码 (.class) ←── ASM / Javassist / Byte Buddy 读写、改写 ↓ ClassLoader + Instrumentation JVM 加载并执行增强后的类 常见框架对比 # 框架 特点 典型使用者 ASM 直接操作字节码，性能高、控制细，API 偏底层 Spring、CGLIB、Many agents Javassist 提供 Java 源码级 API（CtMethod 等），易上手 早期 Hibernate、部分 AOP Byte Buddy 流式 API + 声明式，生成代码质量高，无需编译器 Mockito、SkyWalking、Hibernate Spring AOP 默认对 interface 用 JDK 动态代理，对 class 用 CGLIB（底层 ASM）生成子类；而 Java Agent 场景多在类加载阶段织入逻辑，两者切入点不同。\nJava Agent 如何介入 #Java Agent 通过 Instrumentation 接口注册 ClassFileTransformer：\npublic class MyAgent { public static void premain(String args, Instrumentation inst) { inst.addTransformer(new MyTransformer(), true); } } 类加载时，JVM 把原始 byte[] 交给 Transformer，返回修改后的字节码再加载。-javaagent:my-agent.jar 在进程启动时生效；Attach API 可在运行中对已存活 JVM 注入 Agent（Arthas 即此路径）。\n典型应用场景 # 场景 做法 代表 AOP 方法入口/出口插入拦截逻辑 Spring AOP、AspectJ 编译织入 链路监控 自动埋点，记录耗时、TraceId SkyWalking、Pinpoint、Zipkin Agent 热修复 替换有 bug 的方法体 部分线上 patch 方案（需谨慎） 单元测试 Mock 运行时生成子类/替身 Mockito、PowerMock 诊断排查 动态增强指定类观察行为 Arthas watch / trace 简单示例：方法耗时统计 #用 Javassist 在方法前后插入计时代码（示意）：\nCtMethod method = ctClass.getDeclaredMethod(\u0026#34;doWork\u0026#34;); method.insertBefore(\u0026#34;long _start = System.nanoTime();\u0026#34;); method.insertAfter(\u0026#34;{ System.out.println(\\\u0026#34;cost: \\\u0026#34; + (System.nanoTime() - _start)); }\u0026#34;); 等价的 ASM 需要手动维护操作数栈和局部变量表，代码更长，但性能与体积更优。Byte Buddy 则可以用更声明式的方式达到同样效果：\nnew ByteBuddy() .subclass(Foo.class) .method(named(\u0026#34;doWork\u0026#34;)) .intercept(MethodDelegation.to(TimingInterceptor.class)) .make(); 使用注意 # 类加载顺序：被增强的类一旦加载，需 retransformClasses 才能再次修改 性能开销：频繁插桩会增加方法入口成本，监控类 Agent 通常只增强业务包、排除 JDK 类 兼容性：JDK 模块系统（Java 9+）限制对核心类的改写，Agent 需声明 Can-Redefine-Classes 等权限 安全与合规：热修复、远程 Agent 注入涉及线上变更，需严格管控 更系统的实践可参考美团技术团队文章 字节码增强技术探索。\n参考资料 # 《深入理解 JVM 字节码》 《深入理解 Java 虚拟机》第 6 章（类文件结构）、第 8 章（字节码执行引擎） The Java Virtual Machine Specification 字节码增强技术探索 hexdump Compiler Explorer (godbolt) ","date":"2022-12-04","permalink":"https://niweea.github.io/posts/java/bytecode/jvm_bytecode/","section":"Posts","summary":"从 class 文件结构、常量池到字节码指令，结合 javap 与编译器行为理解 JVM 如何执行 Java 代码。","title":"JVM字节码理解"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/%E5%AD%97%E8%8A%82%E7%A0%81/","section":"Tags","summary":"","title":"字节码"},{"content":"","date":null,"permalink":"https://niweea.github.io/categories/dns/","section":"Categories","summary":"","title":"DNS"},{"content":"DNS（Domain Name System） 域名系统\n基本概念 #域名系统 (DNS) 将人类可读的域名 (例如，www.baidu.com) 转换为机器可读的 IP 地址。\nDNS 属于应用层协议。把 域名/主机名字 转换为IP地址。\n域名 #域名是文本字符串到Ip地址的的映射。\n域名采用层次树状结构方法命名。每一个域名都由标号序列组成，各标号直接用点隔开。例如 news.baidu.com\n级别最低的域名写在最左边，而级别最高的顶级域名则写在最右边。\n每一个标号不超过63个字符（中文需要转换成Punycode），不区分大小写。多标号组成的完整域名总共不超过255个字符。\n顶级域名 TLD （Top Level Domain)\n通用顶级域名 gTLD 国家/地区顶级域名 nTLD 基础结构域名 (infrastructure domain) 这种顶级域名只有一个，即 arpa， 用于反向域名解析 用域名树来表示域名系统较清楚。实际上是一个倒过来的树，在最上面的是根，但没有对应的名字。根下面一级的节点，就是最高一级的顶级域名。顶级域名可往下划分子域，即二级域名。再往下划分就是三级域名、四级域名等等。\n域名由ICANN （Internet Corporation for Assigned Names and Numbers）互联网名称与数字地址分配机构管理。但是具体的管理工作由域名注册管理机构管理（顶级域名）。\n域名注册管理机构将域名注册的商业销售委托给注册商。用户通过注册商购买域名后，注册商必须通知域名注册管理机构并支付费用。\n.COM的顶级域域名注册管理机构是VeriSign, Inc.\n域名服务器 #域名系统是通过域名服务器实现的。\n互联网上的DNS服务器是按照层级结构管理的， 分区管理。\n域名服务器分类 #根域名服务器 （root name server)\n根域名服务器是最高层次的域名服务器。根域名服务器记录着顶级域名服务器的域名和 IP 地址。\n目前根域名服务器由13组构成，为了方便人们记忆，使用从 a.rootservers.net到m.rootservers.net的域名表示。这13组根域名服务器只使用13个不同的IP地址（IPv4/IPv6）。\nRoot Servers IPv4 IPv6 OPERATOR a.root-servers.net 198.41.0.4 2001:503:ba3e::2:30 Verisign, Inc. b.root-servers.net 199.9.14.201 2001:500:200::b University of Southern California, Information Sciences Institute c.root-servers.net 192.33.4.12 2001:500:2::c Cogent Communications d.root-servers.net 199.7.91.13 2001:500:2d::d University of Maryland e.root-servers.net 192.203.230.10 2001:500:a8::e NASA (Ames Research Center) f.root-servers.net 192.5.5.241 2001:500:2f::f nternet Systems Consortium, Inc. g.root-servers.net 192.112.36.4 2001:500:12::d0d US Department of Defense (NIC) h.root-servers.net 198.97.190.53 2001:500:1::53 US Army (Research Lab) i.root-servers.net 192.36.148.17 2001:7fe::53 Netnod j.root-servers.net 192.58.128.30 2001:503:c27::2:30 Verisign, Inc. k.root-servers.net 193.0.14.129 2001:7fd::1 RIPE NCC l.root-servers.net 199.7.83.42 2001:500:9f::42 ICANN m.root-servers.net 202.12.27.33 2001:dc3::35 WIDE Project root name servers\n根域名服务器有很多镜像服务器。在中国大陆北京、上海、广州、杭州等城市都有镜像服务器。\nroot-servers.org\n根域名服务器采用任播(anycast)技术，当 DNS客户向某个根域名服务器的IP地址发出查询报文时，互联网上的路由器就能找到离这个DNS客户最近的一个根域名服务器。这样做不仅加快了DNS的查询过程，也更加合理地利用了互联网的资源。\n顶级域名服务器\n负责管理在该顶级域名服务器注册的所有二级域名。当收到DNS查询请求时，给出相应的应答。\nroot zone\n权威域名服务器(Authoritative DNS server)\n权威域名服务器是实际持有并负责DNS资源记录的服务器。是位于DNS查找链底部的服务器，根据所查询的资源记录进行IP地址响应。\nwhois baidu.com 域名服务器解析原理 #查询方式 #递归查询(Recursive Query)\n如果主机所询问的本地域名服务器不知道被查询域名的 IP 地址，那么本地域名服务器就以 DNS 客户的身份，向其他根域名服务器继续发出查询请求报文（即替该主机继续查询），而不是让该主机自己进行下一步的查询。递归查询返回的查询结果或者是所要查询的IP地址，或者是报错，表示无法查询到所需的 IP 地址。\n迭代查询(Iterative Query)\n向域名服务器查询时，不是替本地域名服务器进行后续的查询，而是告知了后续的域名查询服务器。本地域名服务器向根域名服务器的查询通常是采用迭代查询。\n我们以在浏览器中访问网址为例。（下面内容来源于cloudflare）\nDNS查询步骤\n用户在 Web 浏览器中输入 “example.com”，查询传输到 Internet 中，并被DNS递归解析器接收(一般由用户的互联网服务提供商 (ISP) 进行管理)。 接着，解析器查询 DNS 根域名服务器（.）。 然后，根服务器使用存储其域信息的顶级域（TLD）DNS 服务器（例如 .com）的地址响应该解析器。在搜索 example.com 时，我们的请求指向 .com TLD。 然后，解析器向 .com TLD 发出请求。 TLD 服务器随后使用该域的域名服务器 example.com 的 IP 地址进行响应。 最后，递归解析器将查询发送到域的域名服务器。 example.com 的 IP 地址而后从域名服务器返回解析器。 然后 DNS 解析器使用最初请求的域的 IP 地址响应 Web 浏览器。 DNS 查找的这 8 个步骤返回 example.com 的 IP 地址后，浏览器便能发出对该网页的请求：\n浏览器向该 IP 地址发出 HTTP 请求。 位于该 IP 的服务器返回将在浏览器中呈现的网页（第 10 步）。 DNS 记录 #DNS 记录是位于权威DNS服务器中的指令，提供一个域的相关信息，包括哪些 IP 地址与该域关联，以及如何处理对该域的请求。所有 DNS 记录都有一个 “TTL”，其代表生存时间，指示DNS服务器多久刷新一次该记录。\n常用DNS记录\n类型 介绍 A IPv4 地址 AAAA IPv6 地址 CNAME 将域名指向另一个域名地址，与其保持相同解析 MX 用于邮件服务器 NS 域名服务器 SRV 用于标识某台服务器使用了某个服务 PTR 用于根据IP地址查询域名 实现 #DNS协议 # 前12字节属于Header头。\n标识字段（ID/Transaction ID)\n标识字段（ID/Transaction ID) 2字节，客户端设置并由服务器返回结果。客户端通过它来确定响应与查询是否匹配。\n标志字段（Flags）\n标志字段（Flags） 2字节，如下字段组成：\n字段 长度(bit) 含义 QR 1 0表示查询报文，1表示响应报文 Opcode 4 0（标准查询），其他值为 1（反向查询）和2（服务器状态请求) AA 1 授权(权威)回答 TC 1 可截断的 ( truncated ) RD 1 期望递归 （Recursion Desired） RA 1 可用递归 （Recursion Available ） Z 3 Reserved for future use Rcode 4 The result code of an answer； 0: No error 1: Format error 问题数(QDCOUNT)\n资源记录数(ANCOUNTT)\n授权资源记录数(NSCOUNT)\n额外资源记录数(ARCOUNT)\nDNS报文中最后的三个字段，回答字段、授权字段和附加信息字段，均采用一种称为资源记录RR（Resource Record）的相同格式。\nDNS协议详解\n常用命令 #ping、host、nslookup、dig\nflags\nqr 消息响应 aa 权威 rd 递归 ra 远程服务器支持递归 dig @域名服务器 域名\ndig +[no]recurse 关闭递归查询（递归默认是打开的）\ndig +vc 发送基于tcp的查询 ( +[no]tcp )\ndig -x ip地址 反向查询\ndig +trace 域名\ndig +short 域名\ndig // 显示13个根域服务器 dig @8.8.8.8 baidu.com // 从指定域名服务器上查询 strace -e trace=open -f ping -c1 baidu.com C 语言 #linux glibc库\nstruct hostent *gethostbyname(const char *hostname); int getaddrinfo(const char *restrict node, const char *restrict service, const struct addrinfo *restrict hints, struct addrinfo **restrict res); gethostbyname()函数因只支持IPv4地址已不建议使用，已被getaddrinfo()函数取代。\n具体详细信息可参考getaddrinfo\nJava 语言 #InetAddress.getAllByName(\u0026ldquo;hostname\u0026rdquo;)\nInetAddress[] inetAddresses = InetAddress.getAllByName(\u0026#34;baidu.com\u0026#34;); String addresses = Arrays.stream(inetAddresses).map(String::valueOf).collect(Collectors.joining(\u0026#34;\\r\\n\u0026#34;)); System.out.println(addresses); 其底层代码是通过native InetAddress[] lookupAllHostAddr(String hostname)方法实现。翻看openJDK源码，此方法调用了系统glibc库的getaddrinfo方法 (Inet4AddressImpl.c或者Inet6AddressImpl.c)。\nJava的地址解析默认带缓存功能。在Java 8中是通过使用 Cache 类实例 addressCache（存储解析成功结果）和 negativeCache（存储解析失败结果）对结果进行缓存，每次查询时优先查询缓存。\n缓存时间我们可以通过：\nnetworkaddress.cache.ttl、sun.net.inetaddr.ttl 控制解析成功缓存时间 (两参数效果相同)，在不开启安全管理器的情况下，默认时间为 30秒。\n如果值为 0 时，不使用缓存；值为 -1 时则缓存永久有效。\nnetworkaddress.cache.negative.ttl、 sun.net.inetaddr.negative.ttl 控制解析失败缓存时间 (两参数效果相同)，默认时间为10秒。\nsun.net.inetaddr.ttl 和 sun.net.inetaddr.negative.ttl 不建议使用\n如果开启了 SecurityManager，优先从 ${java.home}/jre/lib/security/java.security 中读取参数，networkaddress.cache.ttl值为 -1，永久缓存。\n可通过 -Djava.security.manager 启用安全管理器，然而java 17 中移除了 Security Manager\nJava默认使用系统的配置策略解析， 我们在程序中可以通过JNDI DNS服务指定自定义DNS服务器\nsun.net.spi.nameservice.provider.\u0026lt;n\u0026gt;=\u0026lt;default|dns,sun|...\u0026gt;\nsun.net.spi.nameservice.nameservers=\u0026lt;server1_ipaddr,server2_ipaddr ...\u0026gt;\n使用Google DNS\n-Dsun.net.spi.nameservice.provider.1=dns,sun -Dsun.net.spi.nameservice.nameservers=8.8.8.8,8.8.4.4 JNDI DNS service 在Java 9 中被移除，使用 jdk.net.hosts.file 替代。\nJava 18 #Java 18 中实现了 Internet-Address Resolution SPI。\n实现 InetAddressResolver 接口 和 InetAddressResolverProvider抽象类并在 resources 文件夹下添加 META-INF/services/java.net.spi.InetAddressResolverProvider 文件， 在文件内配置我们实现的InetAddressResolverProvider 全限定类。\n其他框架 #Okhttp\n提供了Dns接口，方便我们实现自定义DNS解析器\nNetty\nLinux 系统实现 #查看glibc版本\nldd --version // 查看glibc版本 getconf GNU_LIBC_VERSION // 查看glibc版本 libc.so.6 // 查看glibc版本 /etc/nsswitch.conf Name Service Switch configuration file\n用来配置不同的来源的查询顺序。\nhosts: files dns myhostname\n/etc/hosts\n在DNS出现之前，用于维护主机名和IP地址之间的映射关系\n/etc/resolv.conf\n域名解析器配置文件\n参数 描述 示例 nameserver 用于配置DNS服务器 8.8.8.8 search 用户配置主机名搜索列表 example.com company.net sortlist IP/子网掩码对 130.155.160.0/255.255.240.0 130.155.0.0 options 控制参数 nameserver\nnameserver用于DNS服务器的配置，支持IPv4 和 IPv6。最多支持3个(参考/resolv/bits/types/res_state.h 中 MAXNS宏)。按照配置顺序请求DNS服务器，请求超时则使用下一个，直至尝试完所有DNS服务器， 然后再根据配置的重试次数进行重试。\nsearch\n当查询不完全限定域名时会使用到此参数\nsortlist\noptions\ntimeout\n解析器请求超时时间，单位为秒，默认为 5，最大值为30。\nattempts\n重试次数，默认值为2。最大值为3。\nrotate\n轮训名称服务器，而非一直使用第一个。\nsingle-request\n(since glibc 2.10) glibc从2.9版本开始支持并发执行IPv4和IPv6 请求。此选项禁用并发请求，使glibc顺序执行IPv6和IPv4请求。\nsingle-request-reopen (since glibc 2.9)\n对IPv4和IPv6使用相同的套接字会因为某些原因可能会导致第二个请求发生丢包现象。设置此参数如果第二个请求没有被正确处理时，关闭此连接并打开一个新的连接。\nndots: n\n如果域名参数中 \u0026ldquo;.\u0026rdquo; 的数量大于等于设置的数值，那么解析器就会在使用搜索列表之前，先查找这个域名。\n容器化 #Docker #Docker 启动容器时，会从宿主机上复制 /etc/resolv.conf文件，并删掉其中无法连接到的DNS服务器。\n/etc/hosts 只记录容器自身的地址和名称 /etc/hostname 容器的主机名\n启动一个容器，在容器中使用 mount 命令可以看到这三个文件挂载信息\n# mount /dev/sda on /etc/resolv.conf type ext4 /dev/sda on /etc/hostname type ext4 /dev/sda on /etc/hosts type ext4 --dns=IP_ADDRESS 指定DNS服务器 --dns-option list 指定DNS相关的选项 --dns-search=DOMAIN 指定DNS搜索域 K8s #kubectl get service -n kube-system Pod 的 DNS 策略\nDefault ClusterFirst ClusterFirstWithHostNet None 其他DNS #DNS-over-HTTPS (DoH) #DNS-over-TLS (DoT) #DNS-over-HTTP/3 (DoH3) #参考资料 #cloudflare DNS\nVeriSign 域名系统 (DNS) 工作原理\nJava 8 Networking Properties\nJava 18 JEP 418: Internet-Address Resolution SPI\nman resolv.conf\nraft-ietf-dnsind-edns0-01\nDNS解析异常问题排查\n阿里云 服务发现DNS\n","date":"2022-07-19","permalink":"https://niweea.github.io/posts/network/dns/","section":"Posts","summary":"\u003cp\u003e\u003ccode\u003eDNS\u003c/code\u003e（Domain Name System） 域名系统\u003c/p\u003e\n\u003ch1 id=\"基本概念\" class=\"relative group\"\u003e基本概念 \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#%e5%9f%ba%e6%9c%ac%e6%a6%82%e5%bf%b5\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h1\u003e\u003cp\u003e域名系统 (DNS) 将人类可读的域名 (例如，www.baidu.com) 转换为机器可读的 IP 地址。\u003c/p\u003e","title":"你必须知道的DNS那些事"},{"content":"Even Fibonacci numbers #Each new term in the Fibonacci sequence is generated by adding the previous two terms. By starting with 1 and 2, the first 10 terms will be:\n1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ... By considering the terms in the Fibonacci sequence whose values do not exceed four million, find the sum of the even-valued terms.\n偶数斐波那契数 #斐波那契数列中的每一项都是前两项的和。从 1 和 2 开始的前 10 项为：\n1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ... 求斐波那契数列中不超过 400 万的、值为偶数的各项之和。\n知识点 #斐波那契数列递推公式：\n$$ F(n) = F(n-1) + F(n-2), \\quad F(1)=1,\\; F(2)=2 $$暴力解法 #逐项生成斐波那契数，若当前项为偶数则累加，直到超过 400 万：\ntotal = 0 while current \u0026lt; 4_000_000: if current 为偶数: total += current 生成下一项 C ##include \u0026lt;stdio.h\u0026gt; int main(void) { int first = 0; int second = 1; int total = 0; int current = first + second; while (current \u0026lt; 4000000) { if (current % 2 == 0) { total += current; } first = second; second = current; current = first + second; } printf(\u0026#34;%d\\n\u0026#34;, total); return 0; } Java #class Main { public static void main(String[] args) { int firstNumber = 0; int secondNumber = 1; int total = 0; int currentNumber = firstNumber + secondNumber; while (currentNumber \u0026lt; 4000000) { if (currentNumber % 2 == 0) { total += currentNumber; } firstNumber = secondNumber; secondNumber = currentNumber; currentNumber = firstNumber + secondNumber; } System.out.println(total); } } Go #package main import \u0026#34;fmt\u0026#34; func main() { first, second := 0, 1 total := 0 current := first + second for current \u0026lt; 4_000_000 { if current%2 == 0 { total += current } first, second = second, current current = first + second } fmt.Println(total) } Rust #fn main() { let mut first = 0; let mut second = 1; let mut total = 0; let mut current = first + second; while current \u0026lt; 4_000_000 { if current % 2 == 0 { total += current; } first = second; second = current; current = first + second; } println!(\u0026#34;{}\u0026#34;, total); } 优化：每 3 项必有一个偶数 #斐波那契数列的奇偶性按「奇、偶、奇、奇、偶、奇、奇、偶、……」循环，因此每 3 个连续项中恰有 1 个偶数，无需逐项判断奇偶。\n偶数项子序列为 2, 8, 34, 144, ……，本身也满足递推。设连续两个偶数项为 E(n-2) 与 E(n-1)，则：\n$$ E(n) = 4 \\cdot E(n-1) + E(n-2) $$以首两项 E(0)=2、E(1)=8 代入验证：\n$$ 4 \\times 8 + 2 = 34,\\quad 4 \\times 34 + 8 = 144 $$直接遍历偶数项子序列，利用上述递推生成下一项，无需判断奇偶。\nC ##include \u0026lt;stdio.h\u0026gt; int main(void) { int prev = 2; int curr = 8; int total = 2; while (curr \u0026lt; 4000000) { total += curr; int next = 4 * curr + prev; prev = curr; curr = next; } printf(\u0026#34;%d\\n\u0026#34;, total); return 0; } Java #class Main { public static void main(String[] args) { int prev = 2; int curr = 8; int total = 2; while (curr \u0026lt; 4000000) { total += curr; int next = 4 * curr + prev; prev = curr; curr = next; } System.out.println(total); } } Go #package main import \u0026#34;fmt\u0026#34; func main() { prev, curr := 2, 8 total := 2 for curr \u0026lt; 4_000_000 { total += curr prev, curr = curr, 4*curr+prev } fmt.Println(total) } Rust #fn main() { let mut prev = 2; let mut curr = 8; let mut total = 2; while curr \u0026lt; 4_000_000 { total += curr; let next = 4 * curr + prev; prev = curr; curr = next; } println!(\u0026#34;{}\u0026#34;, total); } 两种方法结果均为 4613732。\n","date":"2022-01-18","permalink":"https://niweea.github.io/posts/projecteuler/problem_2/","section":"Posts","summary":"\u003ch1 id=\"even-fibonacci-numbers\" class=\"relative group\"\u003eEven Fibonacci numbers \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#even-fibonacci-numbers\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h1\u003e\u003cp\u003eEach new term in the Fibonacci sequence is generated by adding the previous two terms. By starting with 1 and 2, the first 10 terms will be:\u003c/p\u003e","title":"Even Fibonacci numbers"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/%E6%95%B0%E5%AD%A6/","section":"Tags","summary":"","title":"数学"},{"content":"","date":null,"permalink":"https://niweea.github.io/categories/%E6%AC%A7%E6%8B%89%E8%AE%A1%E5%88%92/","section":"Categories","summary":"","title":"欧拉计划"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/%E6%AC%A7%E6%8B%89%E8%AE%A1%E5%88%92/","section":"Tags","summary":"","title":"欧拉计划"},{"content":"虚拟化的概念 #什么是虚拟化 Virtualization？ #虚拟化是一种技术，可以利用以往局限于硬件的资源来创建有用的 IT 服务。它让您能够将物理计算机的工作能力分配给多个用户或环境，从而充分利用计算机的所有能力。\n虚拟化 就是由位于下层的软件模块，通过向上一层软件模块提供一个与它原先所期待的运行环境完全一致的接口的方法。 抽象出一个虚拟的软件或者硬件接口，使得上层软件可以直接运行在虚拟的环境上。（《系统虚拟化：原理与实现》）\n从计算机抽象层次来划分虚拟化技术\n硬件抽象层上的虚拟化（指令集级虚拟化） 操作系统层上的虚拟化 库函数层上的虚拟化 编程语言层上的虚拟化 系统虚拟化 #什么是VMM? #Virtual Machine Monitor（虚拟机监控器）\n系统虚拟化指将一台物理计算机系统虚拟化为一台或者多台虚拟计算机系统（虚拟机）。每个虚拟机都有拥有自己的虚拟硬件，来提供一个独立的虚拟机执行环境。 通过虚拟化层的模拟，虚拟机中的操作系统任务自己仍然是独占一个系统在运行。这层虚拟化层就被称为VMM。\n虚拟化的三大任务： 处理器虚拟化、内存虚拟化和I/O虚拟化\n按VMM实现结构分类\nHypervisor 模型\n在Hypervisor模型中，VMM首先可以被看作是一个完备的操作系统，专门为虚拟化而设计。所有的物理资源都归VMM所管理。\n缺点：\n实现所有的设备驱动，工作量巨大 宿主模型\n在宿主模式中，物理资源由宿主机操作系统管理。实际的虚拟化功能由作为宿主机操作系统独立的内核模块的VMM来提供。VMM需要调用宿主机操作系统的服务 来获取资源进行虚拟化。\n缺点：\n1. 依赖与宿主机是否考虑支持虚拟化 2. 虚拟机安全依赖于VMM和宿主机 虚拟化产品\nVMware Server、 VMware Workstation、Virtual PC 等\n混合模型\n混合模型是上述两种模式的混合体。VMM依赖位于最底层，拥有所有的物理资源。与Hypervisor模式不同的是，VMM会主动让出大部分I/O设备的控制权，由一个 运行在特权虚拟机中的特权操作系统来控制。\n缺点：\n切换到特权操作系统时会产生上下文切换的开销 虚拟化产品\nWindows Server 2008(Hyper-V)、 Xen\nXen VS KVM #全虚拟化与半虚拟化\n全虚拟化模式 允许虚拟机运行未经修改的操作系统。\n半虚拟模式 通常需要根据虚拟化环境对Guest 操作系统进行修改，以半虚拟模式运行的操作系统比全虚拟化模式下运行的操作系统的性能更佳。\nXen #Xen是半虚拟 (PV) 模式的典型代表，但也支持全虚拟化模式。要使 Guest 操作系统能够在半虚拟模式下运行，通常需要根据虚拟化环境对其进行修改。不过，以半虚拟模式运行的操作系统比全虚拟化模式下运行的操作系统的性能更佳。\nKVM （Kernel-based Virtual Machine） #KVM是全虚拟化模式。全虚拟化模式允许虚拟机运行未经修改的操作系统。\nKVM 从Linux 2.6.20版本开始合并到 Linux 内核中,现在是内建于 Linux 中的开源虚拟化技术。KVM 可将Linux转变为虚拟机监控程序，使主机计算机能够运行多个隔离的虚拟环境。\nKVM 是 Linux 的一部分。Linux 也是 KVM 的一部分。Linux 有的，KVM 全都有。KVM 享有了 Linux 的有点。\n在 KVM 模型中，虚拟机是一种 Linux 进程，由内核进行调度和管理。通过 Linux 调度程序，可对分配给 Linux 进程的资源进行精细的控制，并且保障特定进程的服务质量。\n产品\n阿里、华为ECS(Elastic Compute Service)、腾讯CVM（Cloud Virtual Machine）、VPS 等\n以华为鲲鹏BoostKit虚拟化总体架构为例\nQEMU 全称 Quick Emulator。是纯软件实现的虚拟化模拟器。\nlibvirt 是用于管理虚拟化平台的开源的API，后台程序和管理工具。\nContainer #LXC是一种内核虚拟化技术，可以提供轻量级的虚拟化，以便隔离进程和资源。\nDocker # Linux Namespace 用于隔离系统资源； 支持 Mount namespace、 UTS Namespace、IPC Namepsace、PID Namespace、Network Namespace、User Namespace。\nLinux Cgroups Linux Cgroups(Control Groups)提供了对一组进程及将来子进程的资源（CPU、内存、存储、网络等）限制、控制和统计的能力，通过Cgroups可以方便的限制某个进程的资源占用。\nUnion File System 把其他文件系统联合到一个联合挂载点的文件系统服务。\nKubernetes(k8s) #计算资源配额限定 # resources: requests: memory: \u0026#34;64Mi\u0026#34; cpu: \u0026#34;250m\u0026#34; limits: memory: \u0026#34;128Mi\u0026#34; cpu: \u0026#34;500m\u0026#34; Requests：该资源的最小申请量，系统必须满足要求。\nLimits：该资源最大允许使用的量，不能被突破，当容器试图使用超过这个量的资源时，可能会被Kubernetes“杀掉”并重启。\n资源Limits 允许出现超卖，也就是所有limits申请资源可以超过节点资源总和的100%。但是如果节点资源使用量超过100%，那么就会有容器被Kill。\nCPU 是一种可压缩资源，对其使用量进行限制,而不对容器内运行的进程产生不利影响。\n而内存不同，当进程尝试申请分配比限额更多的内存时会被杀掉。（OOM）\n内存不足时哪个容器会被杀死 #通过Pod的QoS来判断\nBestEffort （优先级最低） Burstable Guaranteed （优先级最高） /sys/fs/cgroup/cpu/cpu.cfs_quota_us\n/sys/fs/cgroup/cpu/cpu.cfs_period_us\nPod生命周期和重启策略 # 状态值 描述 Pending API Server 已经创建该Pod， 但在Pod内还有容器的镜像没有创建，包括正在下载镜像的过程 Running Pod内所有容器均已创建，且至少有一个容器处于运行状态、正在启动状态或者正在重启状态 Succeeded Pod内所有容器均成功执行后退出，切不会再重启 Failed Pod内所有容器已退出，但至少有一个容器退出为失败状态 Unknown 由于某种原因无法获取该Pod的状态，可能由于网络通信不畅导致 PodScheduled：Pod 已经被调度到某节点； ContainersReady：Pod 中所有容器都已就绪； Initialized：所有的 Init 容器 都已成功启动； Ready：Pod 可以为请求提供服务，并且应该被添加到对应服务的负载均衡池中; 容器生命周期 # 状态 描述 Waiting 处于 Waiting 状态的容器仍在运行它完成启动所需要的操作 Running Running 状态表明容器正在执行状态并且没有问题发生。 如果配置了 postStart 回调，那么该回调已经执行且已完成。 Terminated 如果容器配置了 preStop 回调，则该回调会在容器进入 Terminated 状态之前执行。 Health Check #默认健康检查机制 #每个容器启动时都会执行一个进程，此进程由Dockerfile的CMD或ENTRYPOINT指定。如果进程退出时返回码非零，则认为容器发生故障，Kubernetes 就会根据restartPolicy重启容器。\nstartupProbe探针(1.17版本) #startupProbe探针判断容器内的应用程序是否已启动。如果配置了启动探测，在则在启动探针状态为 Succes 之前，其他所有探针都处于无效状态，直到它成功后其他探针才起作用。如果启动探测失败，kubelet 将杀死容器，容器将服从其重启策略。如果容器没有配置启动探测，则默认状态为 Success。 使用startupProbe探针保护慢启动容器。\nReadinessProbe探针 #ReadinessProbe探测则是告诉Kubernetes什么时候可以将容器加入到Service负载均衡池中，对外提供服务。\nReadinessProbe探针用于判断容器是否启动完成，且准备接收请求。 如果ReadinessProbe探针检测到容器启动失败，则Pod的状态将被修改， Endpoint Controller将从Service的Endpoint中删除包含该容器所在Pod的IP地址的Endpoint条目。\nLivenessProbe探针 #LivenessProbe探针让用户可以自定义判断容器是否健康。如果探测到容器不健康，则kubelet将删除该容器，并根据容器的重启策略做相应的处理。\n如果一个容器不包含Livenessprobe探针，那么kubelet认为该容器的 LivenessProbe探针返回的值应该是Success。\n三种实现方式 # ExecAction: 在容器内部执行一个命令，如果该命令的退出状态码为0，则表明容器健康。 TCPSocketAction：通过容器的IP地址和端口号执行TCP检查，如果端口能被访问，则表明容器健康。 HTTPGetAction：通过容器的IP地址和端口号及路径调用 HTTP Get方法，如果响应的状态码大于等于200且小于等于400，则认为容器状态健康。 ExecAction\nlivenessProbe: exec: command: - cat - /tmp/healthy initialDelaySeconds: 15 timeoutSeconds: 1 HTTPGetAction\nlivenessProbe: httpGet: path: /healthy prot: 8080 initialDelaySeconds: 15 timeoutSeconds: 1 initialDelaySeconds: 启动容器后进行首次健康检查的等待时间，单位为s。我们一般会根据应用启动的准备时间来设置，应该大于应用启动时间。\nperiodSeconds: 执行一次Liveness探测周期。\ntimeoutSeconds: 健康检查发送请求后等待响应的超时时间，单位为s。当超时发生时，kubelet会认为容器已经无法提供服务，将会重启该容器。\nsuccessThreshold：探测器在失败后，被视为成功的最小连续成功数。默认值是 1。 存活和启动探测的这个值必须是 1。最小值是 1。\nfailureThreshold：当探测失败时，Kubernetes 的重试次数。 存活探测情况下的放弃就意味着重新启动容器。 就绪探测情况下的放弃 Pod 会被打上未就绪的标签。默认值是 3。最小值是 1。\n监控检查流程 # Kubernetes Probes lifecycle by Andrew Lock\nContainer Lifecycle Hooks # PostStart PreStop lifecycle: postStart: exec: command: [\u0026#34;/bin/sh\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;echo Hello from the postStart handler \u0026gt; /usr/share/message\u0026#34;] preStop: exec: command: [\u0026#34;/bin/sh\u0026#34;,\u0026#34;-c\u0026#34;,\u0026#34;nginx -s quit; while killall -0 nginx; do sleep 1; done\u0026#34;] PostStart #Kubernetes 在容器创建后立即发送 postStart 事件。Kubernetes 的容器管理逻辑会一直阻塞等待 postStart 处理函数执行完毕。 只有 postStart 处理函数执行完毕，容器的状态才会变成 RUNNING。\nPreStop #Kubernetes 在容器结束(Terminated)前立即发送 preStop 事件。除非 Pod 宽限期限超时，Kubernetes 的容器管理逻辑 会一直阻塞等待 preStop 处理函数执行完毕。\n执行停止前钩子（如果配置了的话），然后等待它执行完毕 向容器的主进程发送SIGTERM信号 等待容器优雅地关闭或者等待终止宽限期超时 如果容器主进程没有优雅地关闭，使用SIGKILL信号强制终止进程 SIGINT、SIGTERM和SIGKILL区别\nSIGINT 关联CTRL + C,只能用来结束前台进程，结束信号被被进程树接收到（当前进程以及子进程） SIGTERM 可以被阻塞、处理和忽略，只有当前进程收到结束信号 (kill不带参数发送的信号)，进程可以进行清理工作。 SIGKILL 强行终止进程，不允许别忽略。无法进行清理工作。（kill -9 命令） 僵尸进程、孤儿进程\n孤儿进程：一个父进程退出，而它的一个或多个子进程还在运行，那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养，并由init进程对它们完成状态收集工作。\n僵尸进程：一个进程使用fork创建子进程，如果子进程退出，而父进程并没有调用wait或waitpid回收子进程，那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。\n思考问题 # 容器退出后会出现僵尸进程吗？\n有些程序作为pid = 1 的父进程，无法清理\nDocker and Node.js Best Practices\n使用bash启动服务，容器退出时应用程序没有收到SIGTERM信号？\nsh不进行信号转发\n容器退出时，服务为什么出现Connection Refused？\nk8s常用命令 # 获取命名空间下的所有pod kubectl get pods -n \u0026lt;namespace\u0026gt; 获取pod的消息 kubectl describe pod \u0026lt;pod-name\u0026gt; -n \u0026lt;namespace\u0026gt; 获取pod资源使用情况 kubectl top pod -n \u0026lt;namespace\u0026gt; 获取pod日志信息 kubectl logs --since=1h \u0026lt;pod-name\u0026gt; -n \u0026lt;namespace\u0026gt; // 查看最近1小时的日志 kubectl logs -f \u0026lt;pod-name\u0026gt; -c \u0026lt;container-name\u0026gt; // 查看指定容器日志 进入容器内 kubectl exec -it -n \u0026lt;namespace\u0026gt; \u0026lt;pod-name\u0026gt; bash 参考内容 # SUSE Linux Enterprise Server 15 虚拟化指南\n《Kubernetes in action》\n《Kubernetes权威指南》\n","date":"2021-11-03","permalink":"https://niweea.github.io/posts/virtualization_container/","section":"Posts","summary":"\u003ch2 id=\"虚拟化的概念\" class=\"relative group\"\u003e虚拟化的概念 \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#%e8%99%9a%e6%8b%9f%e5%8c%96%e7%9a%84%e6%a6%82%e5%bf%b5\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h2\u003e\u003ch3 id=\"什么是虚拟化-virtualization\" class=\"relative group\"\u003e什么是虚拟化 Virtualization？ \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#%e4%bb%80%e4%b9%88%e6%98%af%e8%99%9a%e6%8b%9f%e5%8c%96-virtualization\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h3\u003e\u003cp\u003e虚拟化是一种技术，可以利用以往局限于硬件的资源来创建有用的 IT 服务。它让您能够将物理计算机的工作能力分配给多个用户或环境，从而充分利用计算机的所有能力。\u003c/p\u003e","title":"虚拟化与容器化漫谈"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/bitmap/","section":"Tags","summary":"","title":"BitMap"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/bloom-filter/","section":"Tags","summary":"","title":"Bloom Filter"},{"content":"精准去重 #Array、Set、Map、BitSet、Tree等\nArray/普通Set/普通Map #数据量小的时候，大家通常会使用Array、HashSet、TreeSet、Map等数据结构。当数据量大时，此类数据结构占用内存较大。\nBitSet/BitMap #面试题\n在10亿个unsigned int型整数中，如何找出重复出现的数。\n此面试题我们会想到使用BitMap结构来存储unsigned int。\nBitSet本质上是定义了一个很大的 bit 数组，每个元素对应到 bit 数组的其中一位。\nBitSet 将数字值转为bit位的位置。每一个integer值只占用1bit。以集合 ${1, 3, 7}$ 对应的BitMap为 $[10001010]$(伪结构，不区分大小端问题)。\n以Java BitSet为例，其实现采用 long[] 按位处理值。\nJava语言中long 类型 等同于 golang语言中的int64。\n// 存储结构 private long[] words; // 简化后逻辑 int wordIndex = bitIndex \u0026gt;\u0026gt; 6; words[wordIndex] |= (1L \u0026lt;\u0026lt; bitIndex); BitSet 存储所需的内存大小要根据存储元素的最大值来确认，这就造成了稀疏的BitSet和稠密的BitSet占用相同的空间。\n为了节省空间在压缩优化历史中产生了WAH(Word Aligned Hybrid)、EWAH(Enhanced Word Aligned Hybrid)、CONCISE(Compressed N Composble Integer Set)、VALWAH(Variable-Aligned Length WAH) 等压缩算法。\n上述中算法都是基于WAH进行的改进。\nWAH #横向压缩bitmap index, 其实就是对单个bitmap的bit位进行压缩。\n对单个bitmap进行压缩, 最常用的一个算法思想是RLE(Run Length Encoding), 就是指: 如果有连续100个bit位的值都是0, 存储100,0两个数字比存储100个bit更划算.\nWAH就是一种利用RLE思想设计的方案。是一种字对齐的混合结构。\nWAH压缩的内部格式的最小单元是字(一般一个字就是32个bit)。 WAH的的字有两种情况, 原文字(literal word)和压缩字(fill word)。\n两种字通过32个bit中的第一个bit来区分, 第一个bit为0是原文字, 为1则是压缩字。\n如果是原文字, 那么这个字剩余的31个bit, 和原始bitmap中未压缩的31个bit一模一样.\n如果是压缩字, 那么剩余31个bit中:\n第2个bit表示压缩的值是 0 还是 1\n剩余30个bit表示有多少字的重复的值, 称为run number\n比如第一个bit是1, 第2个bit是1, 剩下30个bit是数值5, 则表示有连续的5*31 个1\nWAH前两组: 01000000_00000000_00000011_10000000 10000000_00000000_00000000_00000010\n缺点\n每次操作时都需要解压数据。\nRoaring Bitmap # We partition the range of 32-bit indexes ([0, n)) into chunks of $2^{16}$ integers sharing the same 16 most significant digits. We use specialized containers to store their 16 least significant bits.\n将32-bit的无符号Integer的高16位bits用来计算 Container索引, 低16位bits存放在 Container内。\n那么就有$2^{16}$ = 65536 个 Container。\n计算Container索引\nprotected static char highbits(int x) { return (char) (x \u0026gt;\u0026gt;\u0026gt; 16); } 计算低位\nprotected static char lowbits(int x) { return (char) x; } RoaringBitmap类主要属性包含RoaringArray highLowContainer = null;。\nRoaringArray主要结构\nchar[] keys = null; // short数组，用来存储高16位作为索引 Container[] values = null; // Container数组，用来存储低16位数据 int size = 0; // 用来记录当前RBM包含的key-value有效数量 将一个32-bit的无符号Integer按照高16位分桶处理，其中高16位作为索引存储在char数组中，低16位作为数据存储在某个特定的Container内的数组中。\n存储数据时，先根据高16位找到对应的索引key（二分查找），由于keys和values是一一对应的，即找到了对应的Container，若key存在则将低16位放入对应的Container中，若不存在则创建一个和key对应的Container，并将低16位放入Container中。\nContainer #Container包含ArrayContainer、BitmapContainer、RunContainer\nArrayContainer #当Container内的元素个数少于4096时，默认会采用ArrayContainer。适合存放稀疏的数据。\nArrayContainer 通过char[]实现，当元素个数位4096时，使用内存为 $4096 * 2 * 8 = 65536 bit $ 。当元素个数小于4096时，所使用的内存低于BitSet所使用的内存。\nArraycontainer查询时会使用二分查找。\nJava char类型占2字节(16bit)，等同于golang 中的uint16类型。\nBitmapContainer #当Container内元素个数大于等于4096时，会采用BitmapContainer。BitmapContainer通过long[]实现。适合用来存放稠密数据。\n因为我们只存储低16位数据，所有需要存储的位数最大为 $ 2^{16} = 65536 $。BitmapContainer 使用内存大小一直是 8KB。\nBitmapContainer初始化\n//简化后代码 final long[] bitmap; // 数组最大长度为1024 for (int k = 0; k \u0026lt; arrayContainer.cardinality; ++k) { final char x = arrayContainer.content[k]; bitmap[(x) / 64] |= (1L \u0026lt;\u0026lt; x); } BitmapContainer增加元素\npublic Container add(final char i) { final long previous = bitmap[i \u0026gt;\u0026gt;\u0026gt; 6]; long newval = previous | (1L \u0026lt;\u0026lt; i); bitmap[i \u0026gt;\u0026gt;\u0026gt; 6] = newval; if (USE_BRANCHLESS) { cardinality += (int)((previous ^ newval) \u0026gt;\u0026gt;\u0026gt; i); } else if (previous != newval) { ++cardinality; } return this; } RunContainer #RunContainer适用于存储连续的数据。采用了行程长度压缩算法(Run Length Encoding)。RLE算法：例如 { 1, 10, 20,0, 31,2 } 会被记录为 { 11, 4, 21, 1}。\n最好情况是如果数据是连续分布的，就算是存放 65536 个元素，也只会占用2个short。而最坏的情况就是当数据全部不连续的时候，会占用128 KB 内存。\n通过调用runOptimize来判断是否满足条件转换为RunContainer。\n其他Container超过设定容量后会转为RunContainer\n总结 # 支持64位整数\n支持序列化\n时间空间复杂度\nBitmapContainer只涉及到位运算，时间复杂度为O(1)。ArrayContainer和RunContainer需要用二分查找在有序数组中定位元素，故为O(logN)。\n空间占用（即序列化时写出的字节流长度）方面，BitmapContainer是恒定为8192Byte的。ArrayContainer的空间占用与基数（c）有关，为(2 + 2c)Byte；RunContainer的则与它存储的连续序列数（r）有关，为(2 + 4r)Byte。\n概率性去重 #概率性去重显著特点时存在误判。常使用的方法有基于bit map 的Bloom Filter及变种和基数估计算法如HyperLogLog。\nBloom Filter #布隆过滤器基本思想\n由一个很长的二进制向量和一系列随机映射函数组成。\n当一个元素被加入集合时，通过K个散列函数将这个元素映射成一个位数组中的K个点，把它们置为1。\n检索时，我们只要看看这些点是不是都是1就（大约）知道集合中有没有它了：如果这些点有任何一个0，则被检元素一定不在；如果都是1，则被检元素很可能在。\nFalse Positive: 中文可以理解为“假阳性”。在这里表示，有可能把不属于这个集合的元素误认为属于这个集合；例如上图中的d元素。\nFalse Negative: 中文可以理解为“假阴性”，Bloom Filter是不存在false negatived的， 即不会把属于这个集合的元素误认为不属于这个集合(False Negative)。\nInteractive Demonstration\n公式 #误判率\n$\\sum _{t}\\Pr(q=t)(1-t)^{k}\\approx (1-E[q])^{k}=\\left(1-\\left[1-{\\frac {1}{m}}\\right]^{kn}\\right)^{k}\\approx \\left(1-e^{-kn/m}\\right)^{k}$\nn 是已经添加元素的数量 k 哈希的次数 m 布隆过滤器的总长度 性能优化 #哈佛大学论文 \u0026laquo;Less Hashing, Same Performance: Building a Better Bloom Filter\u0026raquo;提供了优化的解决方法。\ngoogle guava框架实现了哈佛这篇论文。guava采用Murmur3 Hash算法。\n首先将object通过funnel转换为基本类型，计算出64位hash，并且将高位低位分别作为hash。这样一来只调用了一次hash函数，大大节约了时间开销。\n使用示例\nBloomFilter\u0026lt;String\u0026gt; bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 100000, 0.1); bloomFilter.put(\u0026#34;a\u0026#34;); bloomFilter.put(\u0026#34;b\u0026#34;); boolean result = bloomFilter.mightContain(\u0026#34;a\u0026#34;); System.out.println(result); guava实现了MURMUR128_MITZ_32和MURMUR128_MITZ_64两种Hash算法。默认使用MURMUR128_MITZ_64。\n写入\npublic \u0026lt;T\u0026gt; boolean put(T object, Funnel\u0026lt;? super T\u0026gt; funnel, int numHashFunctions, LockFreeBitArray bits) { long bitSize = bits.bitSize(); byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal(); long hash1 = lowerEight(bytes); long hash2 = upperEight(bytes); boolean bitsChanged = false; long combinedHash = hash1; for (int i = 0; i \u0026lt; numHashFunctions; i++) { bitsChanged |= bits.set((combinedHash \u0026amp; Long.MAX_VALUE) % bitSize); combinedHash += hash2; } return bitsChanged; } 根据期望写入数与假阳性率计算总bit位数\n/** * Computes m (total bits of Bloom filter) which is expected to achieve, for the specified * expected insertions, the required false positive probability. * * \u0026lt;p\u0026gt;See http://en.wikipedia.org/wiki/Bloom_filter#Probability_of_false_positives for the * formula. * * @param n expected insertions (must be positive) * @param p false positive rate (must be 0 \u0026lt; p \u0026lt; 1) */ static long optimalNumOfBits(long n, double p) { if (p == 0) { p = Double.MIN_VALUE; } return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2))); } 根据期望插入数量和总位数计算需要hash次数\n/** * Computes the optimal k (number of hashes per element inserted in Bloom filter), given the * expected insertions and total number of bits in the Bloom filter. * * \u0026lt;p\u0026gt;See http://en.wikipedia.org/wiki/File:Bloom_filter_fp_probability.svg for the formula. * * @param n expected insertions (must be positive) * @param m total number of bits in Bloom filter (must be positive) */ static int optimalNumOfHashFunctions(long n, long m) { // (m / n) * log(2), but avoid truncation due to division! return Math.max(1, (int) Math.round((double) m / n * Math.log(2))); } 联想一下算法导论散列表那一节，对于开放地址法，算法导论提出了线性探查、二次探查、双重散列等几种探查方法，以双重散列为冲突最小，这里的思路其实就是双重散列！联想一下上面提到的因为hash冲突而导致误判，那么减少冲突其实是自然而然的。\n缺点\n布隆过滤器不支持删除元素 有误报率 Counting Bloom Filter #A counting Bloom filter is a Bloom filter that uses counters instead of bits.\nCounting Bloom Filter将标准 Bloom Filter 位数组的每一位扩展为一个小的计数器（Counter），在插入元素时给对应的 k （k 为哈希函数个数）个 Counter 的值分别加 1，删除元素时给对应的 k 个 Counter 的值分别减 1。Counting Bloom Filter 通过多占用几倍的存储空间的代价， 给 Bloom Filter 增加了删除操作。\nd-left counting Bloom对CBF的优化。\nCuckoo Filter #布谷鸟过滤器也删除了删除功能。\n实现原理 #初始化内存：初始化一块内存给一维数组Buckets，其中每个Bucket有n个位置可供使用，每个位置存储对应元素的指纹信息，即每个Bucket中可供存储n个元素的指纹信息；\nBucket映射：通过两个Hash函数得到两个对应的位置点（p1和p2）信息，尝试将对应元素的指纹信息存入指定的Bucket中，如果p1对应的Bucket已经填充满了，则尝试填充到p2对应的Bucket中；\n元素指纹挤兑：当两个位置点（p1和p2）对应的Bucket都已经填充满了就会触发填充挤兑，从p1和p2对应的Bucket中随机选择一个进行挤兑操作，将Bucket中的已经存在的指纹信息踢除（被踢除的指纹信息会存储到它可存储的另一个Bucket中，如果另一个Bucket中也没有了位置，则又会触发挤兑操作，直到达到挤兑操作的上限），然后将该指纹信息存储到当前的Bucket中；\n因为布谷鸟过滤器中只存储指纹信息，当这个位置上的指纹被挤兑之后，它需要计算出另一个对偶位置，而计算这个对偶位置是需要元素本身的，但是布谷鸟过滤器巧妙的设计了一个独特的 hash函数，使得可以根据 p1 和 元素指纹 直接计算出 p2，而不需要完整的 x 元素。\nf = fingerprint(x); i1 = hash(x); i2 = i1 ^ hash(f); Cuckoo Hashing Visualization\nInsert\nf = fingerprint(x); i1 = hash(x); i2 = i1 ^ hash(f); if bucket[i1] or bucket[i2] has an empty entry then add f to that bucket; return Done; // must relocate existing items; i = randomly pick i1 or i2; for n = 0; n \u0026lt; MaxNumKicks; n++ do randomly select an entry e from bucket[i]; swap f and the fingerprint stored in entry e; i = i ^ hash(f); if bucket[i] has an empty entry then add f to bucket[i]; return Done; // Hashtable is considered full; return Failure; 缺点 # 由于进行XOR运算，使得Filter个数必须为 2的整数次幂。 Other BF #d-left counting Bloom\nQuotient filter\nXor Filters\n\u0026hellip;\nHyperLogLog #基数估计算法是基于概率统计理论的用于估算给定多重集“势”的算法。现阶段对基数估计算法的研究主要基于Philippe Flaiolet于1985年提出的PCSA算法； 在此基础上，他于2003年引入分桶数的概念，提出了Super-LogLog算法，该算法在提高算法估计精度的同时降低了时空消耗；2007年在SuperLogLog算法的基础上，他用调和平均取代算数平均，并根据 数据规模对估计值进行修正 ，提出了在可处理数据量、估计精度、时空复杂性等方面都有极大提升的Hyper-LogLog算法。\nSketch of the Day: HyperLogLog — Cornerstone of a Big Data Infrastructure\nRedis实现了HyperLogLog\n未完待续\u0026hellip;\u0026hellip;\n参考文章 #Better bitmap performance with Roaring bitmaps\nRoaring Bitmaps\nBitmap Index和在Druid中的应用\nBloom Filter Interactive Demonstration\nBloom Filter Tutorial\nLess Hashing, Same Performance:Building a Better Bloom Filter\n大数据分析常用去重算法分析『Bitmap 篇』\n大数据分析常用去重算法分析『HyperLogLog 篇』\nCuckoo Filter: Practically Better Than Bloom\n布谷鸟过滤器：实际上优于布隆过滤器\nXor Filters: Faster and Smaller Than Bloom and CuckooFilters\n","date":"2021-06-08","permalink":"https://niweea.github.io/posts/exclude_duplicate/","section":"Posts","summary":"\u003ch2 id=\"精准去重\" class=\"relative group\"\u003e精准去重 \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#%e7%b2%be%e5%87%86%e5%8e%bb%e9%87%8d\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h2\u003e\u003cp\u003eArray、Set、Map、BitSet、Tree等\u003c/p\u003e","title":"你必须知道的排重策略"},{"content":"","date":null,"permalink":"https://niweea.github.io/categories/%E6%8E%92%E9%87%8D/","section":"Categories","summary":"","title":"排重"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/%E6%8E%92%E9%87%8D/","section":"Tags","summary":"","title":"排重"},{"content":"","date":null,"permalink":"https://niweea.github.io/categories/%E4%BD%8D%E8%BF%90%E7%AE%97/","section":"Categories","summary":"","title":"位运算"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/%E4%BD%8D%E8%BF%90%E7%AE%97/","section":"Tags","summary":"","title":"位运算"},{"content":"位运算直接操作二进制位，在判断性质、压缩状态和优化性能时非常高效。下面整理几个实用技巧与对应 LeetCode 题目。\n检测两个整数是否符号相反 #^（异或）两值对应位不同时结果为 1。\n正数和负数最高位（符号位）不同（正数为 0，负数为 1），异或后若符号相反，则结果为负数：\nint x = 6; int y = -6; boolean result = (x ^ y) \u0026lt; 0; System.out.println(result ? \u0026#34;opposite\u0026#34; : \u0026#34;same\u0026#34;); // opposite 正数的二进制以原码方式表示，负数以补码方式表示。\n检测一个正整数是否为 2 的幂 #如果一个正整数为 2 的幂，其二进制中只有一位是 1。以 8 为例：\nSystem.out.println(Integer.toBinaryString(8)); // 1000 System.out.println(Integer.toBinaryString(8 - 1)); // 0111 8 二进制: 0000_1000 8-1 二进制: 0000_0111 8 \u0026amp; 7 = 0000_0000 = 0 (v \u0026amp; (v - 1)) 会清除 v 最低位的 1。若 v 是 2 的幂，清除后变为 0，因此：\n(v \u0026gt; 0) \u0026amp;\u0026amp; (v \u0026amp; (v - 1)) == 0 for (int i = 0; i \u0026lt;= 16; i++) { boolean r = i \u0026gt; 0 \u0026amp;\u0026amp; (i \u0026amp; (i - 1)) == 0; // 注意：0 不是 2 的幂 System.out.printf(\u0026#34;%d is a power of 2: %s %n\u0026#34;, i, r ? \u0026#34;yes\u0026#34; : \u0026#34;no\u0026#34;); } 计算与一个数最接近的 2 的 N 次幂 #「最接近」通常有两种需求：向上取（≥ n 的最小 2 的幂）和向下取（≤ n 的最大 2 的幂）。\n向上取：≥ n 的最小 2 的幂 #若 n 本身已是 2 的幂，则返回 n；否则将最高位 1 右侧全部填 1，再加 1：\npublic static int nextPowerOfTwo(int n) { if (n \u0026lt;= 0) return 1; n--; n |= n \u0026gt;\u0026gt;\u0026gt; 1; n |= n \u0026gt;\u0026gt;\u0026gt; 2; n |= n \u0026gt;\u0026gt;\u0026gt; 4; n |= n \u0026gt;\u0026gt;\u0026gt; 8; n |= n \u0026gt;\u0026gt;\u0026gt; 16; return n + 1; } 示例：nextPowerOfTwo(9) = 16，nextPowerOfTwo(8) = 8。\nJava 标准库：Integer.highestOneBit(n - 1) \u0026lt;\u0026lt; 1（需处理 n \u0026lt;= 1 的边界）。\n向下取：≤ n 的最大 2 的幂 #直接取最高位的 1：\nint lower = Integer.highestOneBit(n); // n=9 → 8, n=8 → 8 四舍五入到最近 #比较 lower 与 nextPowerOfTwo(n) 哪个与 n 距离更近即可；相等时通常取较小值。\n计算二进制表示中 1 的个数 #也叫 popcount（population count）。\n方法一：Brian Kernighan 算法 #利用 n \u0026amp; (n - 1) 每次消掉最低位的 1，循环次数等于 1 的个数：\npublic static int bitCount(int n) { int count = 0; while (n != 0) { n \u0026amp;= (n - 1); count++; } return count; } bitCount(0b101101) = 4。\n方法二：逐位统计 #public static int bitCount2(int n) { int count = 0; while (n != 0) { count += n \u0026amp; 1; n \u0026gt;\u0026gt;\u0026gt;= 1; } return count; } 方法三：JDK 内置 #Integer.bitCount(n); // 底层通常由 CPU 指令 POPCNT 或优化实现 SWAR 算法（了解） #并行分治思想，在常数时间内对多个位分组求和，详见 Variable-precision SWAR algorithm。日常开发直接用 Integer.bitCount 即可。\nLeetCode 题 #136. 只出现一次的数字 #136. 只出现一次的数字\n给定一个非空整数数组，除了某个元素只出现一次以外，其余每个元素均出现两次。找出那个只出现了一次的元素。\n异或性质：a ^ a = 0，a ^ 0 = a，且满足交换律、结合律。成对出现的元素异或后为 0，再与单独元素异或即得答案：\npublic int singleNumber(int[] nums) { int single = 0; for (int num : nums) { single ^= num; } return single; } 137. 只出现一次的数字 II #137. 只出现一次的数字 II\n给你一个整数数组 nums，除某个元素仅出现一次外，其余每个元素都恰出现三次。找出并返回那个只出现了一次的元素。\n不能简单异或（三个相同数异或仍为自身）。思路：按位统计每个二进制位上 1 出现的次数，对 3 取模，余数即为结果该位的值。\n状态机解法（推荐） #用 ones 记录「出现 1 次 mod 3」的位，用 twos 记录「出现 2 次 mod 3」的位：\npublic int singleNumber(int[] nums) { int ones = 0, twos = 0; for (int num : nums) { ones = (ones ^ num) \u0026amp; ~twos; twos = (twos ^ num) \u0026amp; ~ones; } return ones; } 状态转移（每个 bit 独立）：\n当前状态 再来一个 1 新状态 00（0 次） 1 01（1 次）→ ones 01（1 次） 1 10（2 次）→ twos 10（2 次） 1 00（3 次，归零） \u0026amp; ~twos / \u0026amp; ~ones 保证同一 bit 不会同时在 ones 和 twos 中为 1。\n逐位统计解法 #public int singleNumber2(int[] nums) { int result = 0; for (int i = 0; i \u0026lt; 32; i++) { int bitSum = 0; for (int num : nums) { bitSum += (num \u0026gt;\u0026gt; i) \u0026amp; 1; } if (bitSum % 3 != 0) { result |= (1 \u0026lt;\u0026lt; i); } } return result; } 时间 O(32n)，直观但慢于状态机 O(n)。\n参考 # Bit Twiddling Hacks Matters Computational: Ideas, Algorithms, Source Code 位运算技巧总结 Variable-precision SWAR algorithm 位运算经典应用 ","date":"2021-06-04","permalink":"https://niweea.github.io/posts/bit_operation/","section":"Posts","summary":"常用位运算技巧：符号判断、2 的幂、统计 1 的个数，以及 LeetCode 136/137 题解。","title":"你必须知道的位运算"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/%E7%AE%97%E6%B3%95/","section":"Tags","summary":"","title":"算法"},{"content":"","date":null,"permalink":"https://niweea.github.io/categories/leetcode/","section":"Categories","summary":"","title":"Leetcode"},{"content":"今天，书店老板有一家店打算试营业 customers.length 分钟。每分钟都有一些顾客（customers[i]）会进入书店，所有这些顾客都会在那一分钟结束后离开。\n在某些时候，书店老板会生气。 如果书店老板在第 i 分钟生气，那么 grumpy[i] = 1，否则 grumpy[i] = 0。 当书店老板生气时，那一分钟的顾客就会不满意，不生气则他们是满意的。\n书店老板知道一个秘密技巧，能抑制自己的情绪，可以让自己连续 X 分钟不生气，但却只能使用一次。\n请你返回这一天营业下来，最多有多少客户能够感到满意的数量。\n示例 #输入：customers = [1,0,1,2,1,1,7,5], grumpy = [0,1,0,1,0,1,0,1], X = 3 输出：16 解释： 书店老板在最后 3 分钟保持冷静。 感到满意的最大客户数量 = 1 + 1 + 1 + 1 + 7 + 5 = 16. 提示 # 1 \u0026lt;= X \u0026lt;= customers.length == grumpy.length \u0026lt;= 20000 0 \u0026lt;= customers[i] \u0026lt;= 1000 0 \u0026lt;= grumpy[i] \u0026lt;= 1 思路 #拆分问题 #老板不生气时，顾客一定满意；生气时顾客一定不满意。秘密技巧只能连续使用 X 分钟，且只能用一次——在这 X 分钟内老板强制不生气。\n因此答案可以拆成两部分：\n最大满意人数 = base + max_increase base：老板本来就不生气的分钟里，所有顾客人数之和 max_increase：选一个长度为 X 的窗口，使用技巧后额外挽回的满意人数 为什么只看「额外」部分 #在不生气的分钟里，顾客已经满意了，技巧用不用都一样。技巧的价值只体现在：把窗口内原本生气的分钟变成不生气。\n第 i 分钟若 grumpy[i] = 1，使用技巧可以多挽回 customers[i] 人；若 grumpy[i] = 0，则无额外收益。定义：\nextra[i] = customers[i] × grumpy[i] 问题转化为：在 extra 数组上找长度为 X 的连续子数组，使其元素之和最大。\n滑动窗口 #暴力枚举每个窗口是 O(n×X)，用滑动窗口优化到 O(n)：\n先算 base 用窗口 [0, X) 初始化 increase（第一个窗口的 extra 之和） 窗口右移：加入 extra[i]，移出 extra[i - X]，维护最大值 返回 base + max_increase 示例推导 #customers = [1, 0, 1, 2, 1, 1, 7, 5] grumpy = [0, 1, 0, 1, 0, 1, 0, 1] extra = [0, 0, 0, 2, 0, 1, 0, 5] // customers[i] * grumpy[i] X = 3 base = 1 + 1 + 1 + 7 = 10（索引 0、2、4、6 不生气） 窗口 [5, 7] 的 extra 之和 = 1 + 0 + 5 = 6（最后 3 分钟使用技巧） 答案 = 10 + 6 = 16 复杂度 # 时间：O(n) 空间：O(1) 实现 #Rust #pub struct Solution {} impl Solution { pub fn max_satisfied(customers: Vec\u0026lt;i32\u0026gt;, grumpy: Vec\u0026lt;i32\u0026gt;, x: i32) -\u0026gt; i32 { let x = x as usize; let mut base: i32 = 0; for (i, b) in grumpy.iter().enumerate() { if b.eq(\u0026amp;0) { base += customers.get(i).unwrap() } } let mut max_increase = 0; let mut increase = 0; for (i, v) in customers.iter().enumerate() { if i \u0026lt; x { increase += v * grumpy.get(i).unwrap(); if i == x - 1 { max_increase = increase; } } else { increase = increase + v * grumpy.get(i).unwrap() - customers.get(i - x).unwrap() * grumpy.get(i - x).unwrap(); max_increase = max(max_increase, increase); } } base + max_increase } } #[cfg(test)] mod tests { use super::*; #[test] fn test() { assert_eq!( 16, Solution::max_satisfied( vec![1, 0, 1, 2, 1, 1, 7, 5], vec![0, 1, 0, 1, 0, 1, 0, 1], 3, ) ); } } Java #public class Solution { public int maxSatisfied(int[] customers, int[] grumpy, int x) { int base = 0; for (int i = 0; i \u0026lt; customers.length; i++) { if (grumpy[i] == 0) { base += customers[i]; } } int increase = 0; for (int i = 0; i \u0026lt; x; i++) { increase += customers[i] * grumpy[i]; } int maxIncrease = increase; for (int i = x; i \u0026lt; customers.length; i++) { increase = increase + customers[i] * grumpy[i] - customers[i - x] * grumpy[i - x]; maxIncrease = Math.max(maxIncrease, increase); } return base + maxIncrease; } } ","date":"2021-02-24","permalink":"https://niweea.github.io/posts/leetcode/grumpy_bookstore_owner/","section":"Posts","summary":"\u003cp\u003e今天，书店老板有一家店打算试营业 customers.length 分钟。每分钟都有一些顾客（customers[i]）会进入书店，所有这些顾客都会在那一分钟结束后离开。\u003c/p\u003e\n\u003cp\u003e在某些时候，书店老板会生气。 如果书店老板在第 i 分钟生气，那么 grumpy[i] = 1，否则 grumpy[i] = 0。 当书店老板生气时，那一分钟的顾客就会不满意，不生气则他们是满意的。\u003c/p\u003e","title":"爱生气的书店老板"},{"content":"","date":null,"permalink":"https://niweea.github.io/categories/rust/","section":"Categories","summary":"","title":"Rust"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/rust/","section":"Tags","summary":"","title":"Rust"},{"content":"Rust 语法奇奇怪怪的写法。\n? 操作符 （Error propagation） #函数调用后带 ? 什么含义\nFile::create(filename)?; 等同于\nlet output = match File::create(filename){ Ok(f) =\u0026gt; { f } Err(e) =\u0026gt; { return Err(e); } }; ? 操作符用于简化 Result 的写法：遇到 Err 就提前返回，遇到 Ok 则解包取值。\nGo 中每次都要写：\nf, err := os.Create(filename) if err != nil { return err } Rust 用 ? 一行等价：\nlet f = File::create(filename)?; || 闭包结构 #|param1, param2| { // do something } 结构\nRust闭包表达式写法,采用了与Smalltalk 和 Ruby 类似的|结构\nlet expensive_closure = |num| { println!(\u0026#34;this is closure expensive...\u0026#34;); num }; 两个竖线(|)包裹的是闭包参数，大括号({})内是闭包执行体, 单行时大括号可省略。\n_ 下划线操作符 # 忽略某些值\n类似于golang 中的用法\nlet numbers = (2, 4, 8, 16, 32); match numbers { (first, _, third, _, fifth) =\u0026gt; { println!(\u0026#34;Some numbers: {}, {}, {}\u0026#34;, first, third, fifth) }, } // output: Some numbers: 2, 8, 32 变量名前添加_ 用来忽略未使用的变量。\n以下划线开始变量名以便去掉未使用变量警告\nfn main() { let _x = 5; // _x不会产生警告 let y = 10; // y 未使用，会产生警告 } _ 不会变量绑定 let s = Some(String::from(\u0026#34;Hello!\u0026#34;)); // s 的值仍然会移动进 _s，并阻止我们再次使用 s // if let Some(_s) = s { // 只使用下`_`本身，并不会绑定值,s 没有被移动进`_` if let Some(_) = s { println!(\u0026#34;found a string\u0026#34;); } println!(\u0026#34;{:?}\u0026#34;, s); r# 操作符 #原始标识符（Raw identifiers）前缀 r#。\n原始标识符允许我们使用任何的单词作为标识符。\nRust不允许使用Rust定义的关键字以及保留字（例：use、as、type、where、match等）作为标识符。\nfn match(needle: \u0026amp;str, haystack: \u0026amp;str) -\u0026gt; bool { haystack.contains(needle) } 会得到如下错误:\nerror: expected identifier, found keyword `match` --\u0026gt; src/main.rs:4:4 | 4 | fn match(needle: \u0026amp;str, haystack: \u0026amp;str) -\u0026gt; bool { | ^^^^^ expected identifier, found keyword 正确写法\nfn r#match(needle: \u0026amp;str, haystack: \u0026amp;str) -\u0026gt; bool { haystack.contains(needle) } fn main() { assert!(r#match(\u0026#34;foo\u0026#34;, \u0026#34;foobar\u0026#34;)); } 原始字符串 r\u0026quot;...\u0026quot; / r#\u0026quot;...\u0026quot;# #前缀 r 表示 raw string（原始字符串），字符串内的 \\、引号等特殊字符不会被转义。\n// 普通字符串：\\n 会被解析为换行 let s1 = \u0026#34;line1\\nline2\u0026#34;; // 原始字符串：\\n 就是字面量 \\ 和 n let s2 = r\u0026#34;line1\\nline2\u0026#34;; // Windows 路径等场景很常用 let path = r\u0026#34;C:\\Users\\name\\file.txt\u0026#34;; 当字符串本身包含 \u0026quot; 时，用 # 定界，避免冲突：\nlet json = r#\u0026#34;{\u0026#34;name\u0026#34;: \u0026#34;Rust\u0026#34;, \u0026#34;version\u0026#34;: 1}\u0026#34;#; println!(\u0026#34;{}\u0026#34;, json); 若字符串里还有 \u0026quot;#，就增加 # 的数量：\nlet s = r##\u0026#34;contains \u0026#34;# and \\\u0026#34; both\u0026#34;##; 与上一节 r# 原始标识符不同：r\u0026quot;...\u0026quot; 用于字符串字面量，r#match 用于标识符。\n其他语言也有类似写法：Python 的 r\u0026quot;...\u0026quot;、C# 的 @\u0026quot;...\u0026quot;、Swift 的 #\u0026quot;...\u0026quot;#。\n! 宏调用 #Rust 里带 ! 的调用（如 println!、vec!）是宏，不是普通函数。\n宏在编译期展开成代码，因此可以：\n接受可变数量的参数 根据参数类型生成不同代码 在编译期做语法层面的代码生成 println!(\u0026#34;x = {}, y = {}\u0026#34;, x, y); let v = vec![1, 2, 3]; let s = format!(\u0026#34;hello {}\u0026#34;, name); 常见标准库宏：\n宏 作用 println! 打印并换行 format! 格式化字符串 vec! 创建 Vec panic! 触发 panic assert! / assert_eq! 断言 dbg! 调试打印表达式 自定义宏用 macro_rules! 声明：\nmacro_rules! say_hello { () =\u0026gt; { println!(\u0026#34;Hello!\u0026#34;); }; } fn main() { say_hello!(); } 宏与函数的区别：函数签名固定，宏在编译期按规则展开，灵活性更高，但错误信息通常不如函数直观。\n所有权系统 #Rust 没有垃圾回收，靠**所有权（Ownership）**在编译期管理内存。这是 Rust 最「怪」也最核心的部分，其他很多语法都建立在它之上。\n三条基本规则 # 每个值有且仅有一个所有者 所有者离开作用域，值被自动释放（drop） 同一时刻，对同一数据只能有一个可变引用（\u0026amp;mut）或多个不可变引用（\u0026amp;），不能并存 移动（Move） #赋值、传参、从函数返回时，所有权会转移，原变量不可再用：\nlet s1 = String::from(\u0026#34;hello\u0026#34;); let s2 = s1; // 所有权 move 给 s2 // println!(\u0026#34;{}\u0026#34;, s1); // 编译错误：s1 已失效 基本类型（如 i32、bool）实现了 Copy trait，赋值时复制而非移动：\nlet x = 5; let y = x; // 复制，x 仍可用 println!(\u0026#34;{}\u0026#34;, x); 借用（Borrow） #不转移所有权，只临时访问，用 \u0026amp;（不可变借用）和 \u0026amp;mut（可变借用）：\nfn main() { let mut s = String::from(\u0026#34;hello\u0026#34;); let r1 = \u0026amp;s; // 不可变借用 let r2 = \u0026amp;s; println!(\u0026#34;{} {}\u0026#34;, r1, r2); let r3 = \u0026amp;mut s; // 可变借用 r3.push_str(\u0026#34; world\u0026#34;); println!(\u0026#34;{}\u0026#34;, r3); } 以下写法会编译失败——同时存在可变和不可变借用：\nlet mut s = String::from(\u0026#34;hello\u0026#34;); let r1 = \u0026amp;s; let r2 = \u0026amp;mut s; // 错误：已有不可变借用 r1 println!(\u0026#34;{} {}\u0026#34;, r1, r2); 与 C/C++ 指针不同：Rust 的借用规则由编译器强制检查，而不是靠程序员自觉。\nref / ref mut 模式 #在 match、if let 等模式匹配中，默认是按值绑定——会移动（move）所有权。\nref 和 ref mut 则在模式里创建引用，避免把值移走：\nlet s = String::from(\u0026#34;hello\u0026#34;); match s { ref r =\u0026gt; println!(\u0026#34;borrowed: {}\u0026#34;, r), } // s 仍可用，因为 match 只借用了它 println!(\u0026#34;{}\u0026#34;, s); 对比：直接绑定会 move 走 s：\nlet s = String::from(\u0026#34;hello\u0026#34;); match s { r =\u0026gt; println!(\u0026#34;owned: {}\u0026#34;, r), } // println!(\u0026#34;{}\u0026#34;, s); // 编译错误：s 已被 move ref mut 用于需要修改的场景：\nlet mut v = vec![1, 2, 3]; match v { ref mut r =\u0026gt; r.push(4), } println!(\u0026#34;{:?}\u0026#34;, v); // [1, 2, 3, 4] 与 _ 的区别（见上文）：_ 忽略值；ref 借用值；直接写变量名则获取所有权。\n其他语言几乎没有在模式匹配里区分「借用」和「移动」，这是 Rust 所有权系统的延伸。\n生命周期 'a #生命周期（Lifetime）标注引用能存活多久，防止悬垂引用（dangling reference）。\n语法上用 'a、'b 等单引号前缀命名：\nfn longest\u0026lt;\u0026#39;a\u0026gt;(x: \u0026amp;\u0026#39;a str, y: \u0026amp;\u0026#39;a str) -\u0026gt; \u0026amp;\u0026#39;a str { if x.len() \u0026gt; y.len() { x } else { y } } 含义：'a 表示「返回的引用，与参数 x、y 中较短的那个生命周期一致」。编译器据此检查：调用方不能在使用返回值时，让被引用的数据已经失效。\n为什么需要 #fn broken() -\u0026gt; \u0026amp;str { let s = String::from(\u0026#34;hello\u0026#34;); \u0026amp;s // 错误：s 在函数结束时被 drop，返回 \u0026amp;s 成为悬垂引用 } 编译器会拒绝这类代码。生命周期标注帮助编译器在编译期验证引用合法性。\n结构体中的生命周期 #结构体若持有引用，必须标注生命周期：\nstruct Excerpt\u0026lt;\u0026#39;a\u0026gt; { text: \u0026amp;\u0026#39;a str, } fn main() { let novel = String::from(\u0026#34;Call me Ishmael.\u0026#34;); let first = novel.split_whitespace().next().unwrap(); let e = Excerpt { text: first }; println!(\u0026#34;{}\u0026#34;, e.text); } Excerpt\u0026lt;'a\u0026gt; 表示：text 字段引用的字符串，不能比 'a 活得更短。\n'static 生命周期 #'static 表示整个程序运行期间都有效，例如字符串字面量：\nlet s: \u0026amp;\u0026#39;static str = \u0026#34;I live forever\u0026#34;; 生命周期是 Rust 独有的显式语法，Go / Java / Python 等语言没有同等机制——它们用 GC 或约定来管理，不在类型系统里标注引用有效期。\n参考资料 #Rust 程序设计语言\n","date":"2021-01-28","permalink":"https://niweea.github.io/posts/rust/grammar/","section":"Posts","summary":"\u003cp\u003eRust 语法奇奇怪怪的写法。\u003c/p\u003e\n\u003ch1 id=\"-操作符-error-propagation\" class=\"relative group\"\u003e\u003ccode\u003e?\u003c/code\u003e 操作符 （Error propagation） \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#-%e6%93%8d%e4%bd%9c%e7%ac%a6-error-propagation\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h1\u003e\u003cp\u003e函数调用后带 \u003ccode\u003e?\u003c/code\u003e 什么含义\u003c/p\u003e","title":"Rust奇奇怪怪的语法"},{"content":"工具类网站 #图片压缩 # tinify.cn\n使用智能有损压缩技术将您的PNG文件的文件大小降低,对视觉的影响几乎不可见。好用，靠谱。\n语言学习 #数学类 # desmos\ndesmos包含图形计算器等，用于绘制数学公式图形，是学习数学的得力助手。\n代码在线运行 # wandbox\n在线代码运行，包含50多种语言及其各版本。界面简洁。\n正则表达式 # regex101\n功能强大、界面清爽的正则表达式测试网站。有正则表达式解释以及大量实例。\n网络工具 # ipip\nip地址查询等。国内高水准ip地址库。\n电子书/PPT # Library Genesis\n请大家多支持正版！\nZ-Lib\n请大家多支持正版！\nmyslide\n国内PPT分享服务的平台,里面包含很多大会、技术等的ppt\n其他 # Vim ","date":"2020-11-18","permalink":"https://niweea.github.io/posts/tools/","section":"Posts","summary":"\u003ch1 id=\"工具类网站\" class=\"relative group\"\u003e工具类网站 \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#%e5%b7%a5%e5%85%b7%e7%b1%bb%e7%bd%91%e7%ab%99\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h1\u003e\u003ch3 id=\"图片压缩\" class=\"relative group\"\u003e图片压缩 \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#%e5%9b%be%e7%89%87%e5%8e%8b%e7%bc%a9\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h3\u003e\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tinify.cn/\" target=\"_blank\" rel=\"noreferrer\"\u003etinify.cn\u003c/a\u003e\u003c/p\u003e","title":"在线工具类网站"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/%E5%B7%A5%E5%85%B7%E7%BD%91%E7%AB%99/","section":"Tags","summary":"","title":"工具网站"},{"content":"","date":null,"permalink":"https://niweea.github.io/categories/%E7%BD%91%E7%AB%99%E5%88%86%E4%BA%AB/","section":"Categories","summary":"","title":"网站分享"},{"content":"Multiples of 3 and 5 #If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below 1000.\n3或5的倍数 #在小于10的自然数中，3或5的倍数有3、5、6和9，这些数之和是23。\n求小于1000的自然数中所有3或5的倍数之和。\n知识点 #等差数列求和公式：\n$$ \\sum_{k=1}^{n} k = \\frac{n(n+1)}{2} $$小于 1000 的 3 或 5 的倍数之和，可分别求 3、5 的倍数之和，再减去被重复计算的 15 的倍数（容斥原理）：\n$$ 3\\sum_{k=1}^{333} k + 5\\sum_{k=1}^{199} k - 15\\sum_{k=1}^{66} k $$项数分别为：\n$$ 333 = \\left\\lfloor \\frac{999}{3} \\right\\rfloor,\\quad 199 = \\left\\lfloor \\frac{999}{5} \\right\\rfloor,\\quad 66 = \\left\\lfloor \\frac{999}{15} \\right\\rfloor $$代入公式：\n$$ = 3 \\cdot \\frac{333 \\cdot 334}{2} + 5 \\cdot \\frac{199 \\cdot 200}{2} - 15 \\cdot \\frac{66 \\cdot 67}{2} = 166833 + 99500 - 33165 = 233168 $$实现 #C ##include \u0026lt;stdio.h\u0026gt; int arithmetic_progression_sum(int i, int limit) { int len = (limit - 1) / i; return i * len * (len + 1) / 2; } int main(void){ int s3 = arithmetic_progression_sum(3, 1000); int s5 = arithmetic_progression_sum(5, 1000); int s15 = arithmetic_progression_sum(15, 1000); int result = s3 + s5 - s15; printf(\u0026#34;%d\u0026#34;,result); return 0; } Java #class Main { public static void main(String[] args) { int s3 = arithmeticProgressionSum(3, 1000); int s5 = arithmeticProgressionSum(5, 1000); int s15 = arithmeticProgressionSum(15, 1000); int result = s3 + s5 - s15; System.out.println(result); } private static int arithmeticProgressionSum(int i, int limit) { int len = (limit - 1) / i; return i * len * (len + 1) / 2; } } Go #package main import ( \u0026#34;fmt\u0026#34; ) func main() { s3 := arithmetic_progression_sum(3, 1000) s5 := arithmetic_progression_sum(5, 1000) s15 := arithmetic_progression_sum(15, 1000) result := s3 + s5 - s15 fmt.Println(result) } func arithmetic_progression_sum(i uint32, limit uint32) uint32 { len := (limit - 1) / i return i * len * (len + 1) / 2 } Rust # fn main() { let s3 = arithmetic_progression_sum(3, 1000); let s5 = arithmetic_progression_sum(5, 1000); let s15 = arithmetic_progression_sum(15, 1000); let result = s3 + s5 - s15; println!(\u0026#34;{}\u0026#34;, result); } fn arithmetic_progression_sum(i: u32, limit: u32) -\u0026gt; u32 { let len = (limit - 1) / i; i * len * (len + 1) / 2 } ","date":"2020-10-28","permalink":"https://niweea.github.io/posts/projecteuler/problem_1/","section":"Posts","summary":"\u003ch1 id=\"multiples-of-3-and-5\" class=\"relative group\"\u003eMultiples of 3 and 5 \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#multiples-of-3-and-5\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h1\u003e\u003cp\u003eIf we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.\nFind the sum of all the multiples of 3 or 5 below 1000.\u003c/p\u003e","title":"Multiples of 3 and 5"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/ab/","section":"Tags","summary":"","title":"Ab"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/wrk/","section":"Tags","summary":"","title":"Wrk"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/%E5%8E%8B%E6%B5%8B/","section":"Tags","summary":"","title":"压测"},{"content":"HTTP 接口压测是评估服务容量和延迟的常用手段。命令行场景下，ab 适合快速验证，wrk 则在高并发和请求定制上更强。本文介绍两者的基本用法，并重点说明 wrk 的 Lua 脚本扩展。\nab #Apache Benchmark（ab）来自 Apache HTTP Server 工具集，安装简单、上手快，适合对单个 URL 做快速冒烟压测。\n安装 ## RHEL / CentOS yum install httpd-tools # Debian / Ubuntu apt install apache2-utils # macOS brew install httpd 基本用法 #ab -n 10000 -c 100 -k http://127.0.0.1:8080/ 常用参数：\n参数 说明 -n 总请求数 -c 并发数（同时发起的请求数） -k 启用 HTTP Keep-Alive -p POST 请求体文件路径 -T Content-Type，配合 -p 使用 -H 自定义请求头，可多次指定 POST 示例：\nab -n 1000 -c 50 -p body.json -T application/json http://127.0.0.1:8080/api 输出解读 #关注以下几项：\nRequests per second：吞吐量（QPS），越高越好 Time per request (mean)：平均响应时间；括号内为每个并发连接的平均时间 Transfer rate：网络吞吐 Failed requests：非 2xx 响应或连接失败，应为 0 局限 # 单 URL、单方法，复杂场景需借助脚本或换工具 性能低于 wrk，高并发压测时容易先打满压测机 POST 定制能力弱，动态 body 需借助外部文件 wrk #wrk 是一款 C 语言开发的 HTTP 压测工具，性能高，并支持通过 LuaJIT 脚本定制请求内容、节奏和结果统计。\n安装 ## macOS brew install wrk # 源码编译 git clone https://github.com/wg/wrk.git \u0026amp;\u0026amp; cd wrk \u0026amp;\u0026amp; make 基本用法 #wrk -t12 -c400 -d30s --latency http://127.0.0.1:8080/index.html 参数说明：\n参数 说明 -t, --threads \u0026lt;N\u0026gt; 压测线程数，通常设为 CPU 核数或其倍数 -c, --connections \u0026lt;N\u0026gt; 保持的 TCP 连接数，即并发连接数 -d, --duration \u0026lt;T\u0026gt; 压测持续时间，如 30s、2m -s, --script \u0026lt;S\u0026gt; Lua 脚本路径，用于定制请求 -H, --header \u0026lt;H\u0026gt; 为每个请求添加 HTTP 头 --latency 压测结束后输出延迟分布（建议始终加上） --timeout \u0026lt;T\u0026gt; Socket / 请求超时时间 \u0026lt;N\u0026gt; 支持国际单位（1k、1M），\u0026lt;T\u0026gt; 支持时间单位（2s、2m、2h）。\n输出解读 #Running 30s test @ http://127.0.0.1:8080/index.html 12 threads and 400 connections Thread Stats Avg Stdev Max +/- Stdev Latency 2.50ms 1.20ms 45.00ms 89.00% Req/Sec 13.50k 1.20k 16.00k 85.00% Latency Distribution 50% 2.00ms 75% 3.00ms 90% 4.50ms 99% 12.00ms 4860000 requests in 30.00s, 580.00MB read Requests/sec: 162000.00 Transfer/sec: 19.33MB Requests/sec：QPS Latency Distribution：延迟分位数，P99 是衡量长尾延迟的关键指标 Thread Stats → Req/Sec：单线程吞吐，可判断线程数是否合适 Lua 脚本 #wrk 在启动、运行、结束三个阶段提供 Lua 钩子，用于定制请求和统计结果。\n生命周期 #setup(thread) ← 每个线程启动前，调用一次 └─ init(args) ← 每个线程开始执行时，调用一次 ├─ delay() ← 每次请求前，返回等待毫秒数（可选） ├─ request() ← 每次请求前，构造请求（可选） └─ response() ← 每次收到响应后（可选） done(summary, ...) ← 全部线程结束后，调用一次 各函数说明 # 函数 调用时机 作用 setup(thread) 每个线程启动前，一次 线程级初始化，如设置 thread 变量 init(args) 每个线程开始执行时，一次 线程内状态初始化，如随机数种子、预生成数据 delay() 每次请求前 返回等待毫秒数，模拟用户思考时间；默认 0 request() 每次发请求前 构造并返回 HTTP 请求；不定义则使用命令行 URL response(status, headers, body) 收到响应后 校验响应、统计错误码 done(summary, latency, requests) 压测结束 输出自定义汇总指标 setup 在 wrk 进程启动时执行，init 在线程真正开始发请求前执行。需要 per-thread 状态时，优先在 init 中初始化。\n使用示例 #指定线程数、连接数、压测时间，并用 Lua 脚本发送 POST JSON 请求：\nwrk -t50 -c500 -d30s --latency -s load.lua http://localhost/filter load.lua：\n-- 全局默认设置（也可在 request() 中覆盖） wrk.method = \u0026#34;POST\u0026#34; wrk.headers[\u0026#34;Content-Type\u0026#34;] = \u0026#34;application/json\u0026#34; local key_length = 16 local charset = \u0026#34;0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\u0026#34; -- 每个线程独立的状态 local keys_pool = {} local function random_key(n) local s = {} for i = 1, n do local idx = math.random(1, #charset) s[i] = charset:sub(idx, idx) end return table.concat(s) end -- 在线程 init 阶段预生成 body 模板，避免 request() 中重复构造 function init(args) math.randomseed(os.time() + wrk.thread:get(\u0026#34;id\u0026#34;)) for i = 1, 10 do local keys = {} for j = 1, 100 do keys[j] = random_key(key_length) end keys_pool[i] = string.format( \u0026#39;{\u0026#34;filter\u0026#34;:\u0026#34;bloom\u0026#34;,\u0026#34;keyspace\u0026#34;:\u0026#34;default\u0026#34;,\u0026#34;action\u0026#34;:2,\u0026#34;keys\u0026#34;:[%s]}\u0026#39;, \u0026#39;\u0026#34;\u0026#39; .. table.concat(keys, \u0026#39;\u0026#34;,\u0026#34;\u0026#39;) .. \u0026#39;\u0026#34;\u0026#39; ) end end local counter = 0 function request() counter = counter + 1 wrk.body = keys_pool[((counter - 1) % #keys_pool) + 1] return wrk.format() end function response(status, headers, body) if status ~= 200 then print(\u0026#34;unexpected status: \u0026#34; .. status) end end function done(summary, latency, requests) print(string.format(\u0026#34;total errors: %d\u0026#34;, summary.errors.status + summary.errors.read + summary.errors.write + summary.errors.timeout)) end 示例说明：\n业务场景：向 /filter 接口批量提交 100 个 key 的 bloom filter 查询请求 init 中预生成 body 模板，避免每次请求都跑随机数和 JSON 序列化，减少对压测结果的干扰 手写 JSON 而非 require \u0026quot;cjson\u0026quot;：wrk 内置 LuaJIT 不包含 cjson，直接使用会报错；若必须用 cjson，需自行编译带该库的 wrk response 校验非 200 响应，done 汇总错误数 压测实践建议 #参数选型 # -t（线程数）：从 CPU 核数开始，观察单线程 Req/Sec，再逐步增加 -c（连接数）：对应目标并发用户数；过高可能先耗尽压测机端口或文件描述符 -d（持续时间）：至少 30s，并预留预热时间；短压测容易受冷启动影响 压测前检查 ## 确认文件描述符上限足够 ulimit -n 65535 被测服务与压测机尽量隔离，避免 localhost 压测掩盖真实网络延迟 压测期间同步观察被测服务的 CPU、内存、GC、连接数等指标 丢弃前几秒的预热数据，或先跑一轮不计结果的 warmup 常见误区 # 问题 原因 建议 QPS 远低于预期 Lua 脚本开销过大 在 init 预生成数据，简化 request() 大量 connection refused 压测机端口耗尽 提高 ulimit -n，或降低 -c P99 极高 服务 GC、锁竞争或队列积压 结合 APM / 监控定位，而非只看 QPS wrk 结果与服务端日志不一致 压测的是错误响应 用 response() 校验状态码 ab 与 wrk 选型 # 维度 ab wrk 上手成本 低 中 峰值性能 较低 较高 请求定制 弱（POST 需文件） 强（Lua 脚本） 适用场景 快速冒烟、简单 GET/POST 高并发、动态 body、自定义统计 其他常用工具：hey（Go，语法简洁）、vegeta（Go，支持速率控制）、k6（JavaScript 脚本，适合复杂场景）。\n参考链接 # wrk GitHub Apache Benchmark 文档 ","date":"2020-10-28","permalink":"https://niweea.github.io/posts/benchmark/","section":"Posts","summary":"\u003cp\u003eHTTP 接口压测是评估服务容量和延迟的常用手段。命令行场景下，\u003ccode\u003eab\u003c/code\u003e 适合快速验证，\u003ccode\u003ewrk\u003c/code\u003e 则在高并发和请求定制上更强。本文介绍两者的基本用法，并重点说明 wrk 的 Lua 脚本扩展。\u003c/p\u003e\n\u003ch1 id=\"ab\" class=\"relative group\"\u003eab \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#ab\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h1\u003e\u003cp\u003eApache Benchmark（\u003ccode\u003eab\u003c/code\u003e）来自 Apache HTTP Server 工具集，安装简单、上手快，适合对单个 URL 做快速冒烟压测。\u003c/p\u003e","title":"压测工具"},{"content":"","date":null,"permalink":"https://niweea.github.io/categories/%E5%B7%A5%E5%85%B7%E4%BD%BF%E7%94%A8/","section":"Categories","summary":"","title":"工具使用"},{"content":"","date":null,"permalink":"https://niweea.github.io/tags/%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/","section":"Tags","summary":"","title":"性能测试"},{"content":"","date":"0001-01-01","permalink":"https://niweea.github.io/about/","section":"Niweea’s Blog","summary":"","title":""}]