<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Posts on $ echo S</title>
        <link>https://shawzb.com/posts/</link>
        <description>Recent content in Posts on $ echo S</description>
        <generator>Hugo -- gohugo.io</generator>
        <copyright>&lt;a href=&#34;https://creativecommons.org/licenses/by-nc/4.0/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;CC BY-NC 4.0&lt;/a&gt;</copyright>
        <lastBuildDate>Sat, 27 Nov 2021 21:33:37 +0800</lastBuildDate>
        <atom:link href="https://shawzb.com/posts/index.xml" rel="self" type="application/rss+xml" />
        
        <item>
            <title>计算机组成原理面试问题整理</title>
            <link>https://shawzb.com/posts/2021/11/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%84%E6%88%90%E5%8E%9F%E7%90%86%E9%9D%A2%E8%AF%95%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86/</link>
            <pubDate>Sat, 27 Nov 2021 21:33:37 +0800</pubDate>
            
            <guid>https://shawzb.com/posts/2021/11/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%84%E6%88%90%E5%8E%9F%E7%90%86%E9%9D%A2%E8%AF%95%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86/</guid>
            <description>进程和线程 进程和线程的同步方法 线程间的通讯方式 线程间的切换和进程间的切换哪个代价大 内核态和用户态 内核态用户态切换 程序在开始运行的时候，内核态和用户态都发生了什么 CPU 调度算法 死锁 </description>
            <content type="html"><![CDATA[<h2 id="进程和线程">进程和线程</h2>
<h3 id="进程和线程的同步方法">进程和线程的同步方法</h3>
<h3 id="线程间的通讯方式">线程间的通讯方式</h3>
<h3 id="线程间的切换和进程间的切换哪个代价大">线程间的切换和进程间的切换哪个代价大</h3>
<h2 id="内核态和用户态">内核态和用户态</h2>
<h3 id="内核态用户态切换">内核态用户态切换</h3>
<h3 id="程序在开始运行的时候内核态和用户态都发生了什么">程序在开始运行的时候，内核态和用户态都发生了什么</h3>
<h2 id="cpu-调度算法">CPU 调度算法</h2>
<h2 id="死锁">死锁</h2>
]]></content>
        </item>
        
        <item>
            <title>计算机网络面试问题整理</title>
            <link>https://shawzb.com/posts/2021/11/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E9%9D%A2%E8%AF%95%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86/</link>
            <pubDate>Sat, 27 Nov 2021 21:32:37 +0800</pubDate>
            
            <guid>https://shawzb.com/posts/2021/11/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E9%9D%A2%E8%AF%95%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86/</guid>
            <description>网络模型 五层体系结构 在 TCP/IP 网络模型中共分为 5 层，从上到下分别是
应用层 我们使用的软件都是工作在应用层的，专注于为用户提供应用服务，不考虑如何传输。应用层是工作在用户态，而其他层是内核态。
传输层 传输层是给应用层提供数据传输支持的，在传输层有两个协议
网络层 数据链路层 物理层 OSI 网络模型（七层体系结构） 在 TCP/IP 协议模型的基础上，将应用层拆分成了应用层，表示层和会话层
各层都有哪些常见的协议  应用层：HTTP、FTP、TELNET、SMTP、DNS 等 传输层：TCP 和 UDP 网络层：IP、ICMP、ARP  什么是 HTTP 协议 HTTP 协议全称是 Hyper-Text Transfer Protocl 超文本传输协议，所谓超文本就是超越了普通文本的文本，是文本，图片，音视频等的混合体，所以总的来说，HTTP 协议就是用于在计算机中传输文本、图片、音频和视频等超文本数据的规范。
HTTP 协议具有以下特性：
 简单 灵活易用 应用广泛且跨平台 无状态 明文传输 不安全  针对 HTTP 协议明文传输和不安全的缺点，可以使用 HTTPS 来解决，即引入 SSL/TLS 协议，提高安全性。
HTTPS 解决了什么风险  窃听风险 篡改风险 伪装风险  HTTPS 如何解决的  混合加密：通过对信息加密避免数据被窃听的风险  在通信建立前采用非对称加密的方式交换会话密钥，之后的数据传输都采用对称加密。因为对称加密本质上就是位运算，而非对称加密采用了大量的数学运算，我们都知道计算机执行位运算的效率要远高于数学运算，所以对于相同文本对称加密的效率远高于非对称加密。
 摘要算法：提供了数据校验的能力避免被篡改  发送数据时使用摘要算法计算出明文的指纹，并把指纹和明文一起加密成密文发送到对方，对方解密后用相同的摘要算法算出明文的指纹，并和发送时携带的指纹进行比较。</description>
            <content type="html"><![CDATA[<h2 id="网络模型">网络模型</h2>
<h3 id="五层体系结构">五层体系结构</h3>
<p>在 TCP/IP 网络模型中共分为 5 层，从上到下分别是</p>
<h4 id="应用层">应用层</h4>
<p>我们使用的软件都是工作在应用层的，专注于为用户提供应用服务，不考虑如何传输。应用层是工作在用户态，而其他层是内核态。</p>
<h4 id="传输层">传输层</h4>
<p>传输层是给应用层提供数据传输支持的，在传输层有两个协议</p>
<h4 id="网络层">网络层</h4>
<h4 id="数据链路层">数据链路层</h4>
<h4 id="物理层">物理层</h4>
<h3 id="osi-网络模型七层体系结构">OSI 网络模型（七层体系结构）</h3>
<p>在 TCP/IP 协议模型的基础上，将应用层拆分成了应用层，表示层和会话层</p>
<h2 id="各层都有哪些常见的协议">各层都有哪些常见的协议</h2>
<ul>
<li>应用层：HTTP、FTP、TELNET、SMTP、DNS 等</li>
<li>传输层：TCP 和 UDP</li>
<li>网络层：IP、ICMP、ARP</li>
</ul>
<h2 id="什么是-http-协议">什么是 HTTP 协议</h2>
<p>HTTP 协议全称是 Hyper-Text Transfer Protocl 超文本传输协议，所谓超文本就是超越了普通文本的文本，是文本，图片，音视频等的混合体，所以总的来说，HTTP 协议就是用于在计算机中<strong>传输</strong>文本、图片、音频和视频等<strong>超文本</strong>数据的<strong>规范</strong>。</p>
<p>HTTP 协议具有以下特性：</p>
<ul>
<li>简单</li>
<li>灵活易用</li>
<li>应用广泛且跨平台</li>
<li>无状态</li>
<li>明文传输</li>
<li>不安全</li>
</ul>
<p>针对 HTTP 协议明文传输和不安全的缺点，可以使用 HTTPS 来解决，即引入 SSL/TLS 协议，提高安全性。</p>
<h2 id="https-解决了什么风险">HTTPS 解决了什么风险</h2>
<ul>
<li>窃听风险</li>
<li>篡改风险</li>
<li>伪装风险</li>
</ul>
<h2 id="https-如何解决的">HTTPS 如何解决的</h2>
<ul>
<li>混合加密：通过对信息加密避免数据被窃听的风险</li>
</ul>
<p>在通信建立前采用<strong>非对称加密</strong>的方式交换<em>会话密钥</em>，之后的数据传输都采用<strong>对称加密</strong>。因为对称加密本质上就是位运算，而非对称加密采用了大量的数学运算，我们都知道计算机执行位运算的效率要远高于数学运算，所以对于相同文本对称加密的效率远高于非对称加密。</p>
<ul>
<li>摘要算法：提供了数据校验的能力避免被篡改</li>
</ul>
<p>发送数据时使用摘要算法计算出明文的<em>指纹</em>，并把<em>指纹</em>和明文一起加密成密文发送到对方，对方解密后用相同的摘要算法算出明文的<em>指纹</em>，并和发送时携带的<em>指纹</em>进行比较。</p>
<ul>
<li>身份证书：使用身份证书保证不会被伪装</li>
</ul>
<p>服务端将自己的公钥注册到数字证书认证机构（CA），CA 用自己的私钥将服务器的公钥进行数字签名，客户端和服务端建立连接时会验证服务端的证书是否有效。</p>
<h2 id="https-是如何建连接的其间交互了什么">HTTPS 是如何建⽴连接的？其间交互了什么？</h2>
<p>HTTPS 协议是在 HTTP 协议上增加了 TLS 的连接过程，TLS 的连接过程涉及四次通信</p>
<ol>
<li>
<p>Client Hello</p>
<p>由客户端向服务端发送建立连接请求，向服务端发送以下内容</p>
<ul>
<li>客户端支持的 SSL/TLS 协议版本</li>
<li>客户端支持的密码套件</li>
<li>客户端产生的随机数（client-random）</li>
</ul>
</li>
<li>
<p>Server Hello</p>
<p>在服务端收到连接请求后，向客户端发送的内容</p>
<ul>
<li>确认 SSL/TLS 协议版本</li>
<li>确认使用的密码套件</li>
<li>服务端产生的随机数（server-random）</li>
<li>服务端的数字证书</li>
</ul>
</li>
<li>
<p>客户端回应</p>
<p>客户端收到服务端发送来的数字证书，会向 CA 验证证书的有效性，并从证书中取出服务端的公钥，用服务端的公钥加密报文，向服务端发送：</p>
<ul>
<li>用公钥加密的一个随机数（pre-master key）</li>
<li>加密算法改变通知</li>
<li>客户端握手结束通知</li>
</ul>
</li>
<li>
<p>服务端回应</p>
<p>服务端收到客户端发来的内容后，用私钥解密出 pre-master key，此时服务端和客户端各有三个随机数，通过协商的加密算法计算出本次会话的<strong>会话密钥</strong>，然后向客户端发送：</p>
<ul>
<li>加密算法改变通知</li>
<li>服务端握手结束通知</li>
</ul>
</li>
</ol>
<p>到此整个 SSL/TLS 协议握手结束，接下来客户端和服务端使用计算好的会话密钥进行加密通信。</p>
<h3 id="http10http11http2http3-演变">HTTP/1.0、HTTP/1.1、HTTP/2、HTTP/3 演变</h3>
<p>HTTP/1.1 使用 TCP 长连接的方式改善了 HTTP/1.0 短连接频繁握手/挥手造成的开销，同时支持了管道传输，可以减少整体响应时间。但 HTTP/1.1  仍存在以下问题：</p>
<ul>
<li>请求头部冗余</li>
<li>请求在服务端是按顺序执行的，如果某一个请求执行时间过长，客户端会一直收不到相应，就会造成<strong>队头阻塞</strong></li>
<li>没有优先级控制</li>
<li>只能由客户端主动发送请求</li>
</ul>
<p>HTTP/2 相比 HTTP/1.1 做出了一些优化</p>
<ul>
<li>压缩头部：如果多个请求的头部较为相似，那么会自动帮你消除重复的部分，这部分采用 <strong>HPack</strong> 算法实现</li>
<li>二进制报文：在 HTTP/1.0 和 HTTP/1.1 中报文都是可读的纯文本，而在 HTTP/2 中采用了二进制格式，减少了收发报文的两端对纯文本的编码过程，提高效率</li>
<li>数据流：在 HTTP/2 中的数据包不是按序发送的，同一个连接里连续的数据包可能不属于同一个请求；同时客户端可以指定数据流的优先级，高优先级的数据流会先被服务端响应</li>
<li>多路复用：移除了 HTTP/1.1 中的串行请求，不会再出现<strong>队头阻塞</strong>的现象</li>
<li>主动推送：在一定程度上改善了传统的<em><strong>请求-应答</strong></em>工作模式，服务端可以主动推送数据到客户端</li>
</ul>
<p>在 HTTP/3 中放弃使用 TCP 作为传输层协议，采用了基于 UDP 的 <strong>QUIC</strong> 作为传输层协议，同时 TLS 版本更新到 1.3，头部压缩算法改为 <strong>QPack</strong>；最主要的是将因为传输层协议基于 UDP，将以往的 HTTPS 协议的 TCP 三次握手 + TLS 四次握手聚合成了 QUIC 三次握手，大幅提升了请求的连接速度。</p>
<h2 id="tcp-协议">TCP 协议</h2>
<p>TCP 全称 Transmission Control Protocol，大部分应用使用的传输层协议。TCP 是一个面向连接，可靠的，基于字节流的传输层通信协议。</p>
<ul>
<li>面向连接：一定是一对一的，且需建立连接才能通信</li>
<li>可靠的：无论链路中发生任何异常，TCP 都可以保证一个报文顺利的到达对端</li>
<li>字节流：无论消息有多大都可以有序的传输，如果出现重复的报文会丢弃</li>
</ul>
<h2 id="如何唯一确定一个-tcp-连接">如何唯一确定一个 TCP 连接</h2>
<p>通过 TCP 四元祖可以唯一确定一个 TCP 连接</p>
<ul>
<li>源地址</li>
<li>源端口</li>
<li>目标地址</li>
<li>目标端口</li>
</ul>
<p>其中源地址和目标地址在 IP 报文头部，作用是寻找主机；源端口和目标端口在 TCP 报文头部，作用是找到目标进程</p>
<h2 id="tcp-的三次握手和四次挥手">TCP 的三次握手和四次挥手</h2>
<h3 id="三次握手">三次握手</h3>
<p>最开始客户端和服务端都处于 CLOSED 状态，首先服务端监听一个端口，随后进入 LISTEN 状态，等待连接。随后开始三次握手：</p>
<ul>
<li>第一次握手：客户端向服务端发送一个含有随机序号（client_isn）且 SYN 标志位被置为 1 的报文，随后客户端进入 SYN_SENT 阶段。</li>
<li>第二次握手：服务端收到包含 SYN 标志位的报文后，从中获取客户端发送过来的 client_isn 后，将其 +1，并向客户端发送一个含有服务端随机序号（server_isn）且 SYN/ACK 标志位被置为 1 且确认号为 client_isn + 1 的报文给客户端，并进入 SYN_RCVD 阶段。</li>
<li>第三次握手：客户端收到服务端回复的 SYN/ACK 报文后再向服务端发送一个序号为 client_isn + 1 且 ACK 标识位被置为 1 且确认号为 server_isn + 1 的报文，并进入 ESTABLISHED 阶段</li>
</ul>
<p>当服务端收到第三次握手的报文后也进入 ESTABLISHED 阶段</p>
<p><em><strong>注：因为第三次握手时客户端和服务端已经建立连接了，所以第三次握手可以携带数据</strong></em></p>
<h3 id="四次挥手">四次挥手</h3>
<p>参与一条 TCP 连接的两个进程中的任何一个都能终止连接，下面以客户端主动关闭连接为例：</p>
<ul>
<li>第一次挥手：客户端向服务端发送一个 FIN 标识为被置为 1 的报文，并进入 FIN_WAIT_1 状态</li>
<li>第二次挥手：服务端收到 FIN 报文后，向客户端发送一个 ACK 标识被置为 1 的报文，并进入 CLOSED_WAIT 阶段</li>
<li>第三次挥手：服务端收到 ACK 报文后，向客户端发送一个 FIN 标识为被置为 1 的报文，并进入 LAST_ACK 阶段</li>
<li>第四次挥手：当客户端收到第三次挥手的报文后，向客户端发送一个 ACK 标识被置为 1 的报文，并进入 TIME_WAIT 阶段，随后等待 2MSL 的时间进入 CLOSED 状态</li>
</ul>
<p>当服务端收到第四次挥手的报文后进入 CLOSED 状态</p>
<p><em><strong>注：主动关闭连接的一侧才有 TIME_WAIT 阶段</strong></em></p>
<h2 id="为什么是三次握手">为什么是三次握手</h2>
<p>使用三次握手的目的：</p>
<ul>
<li>避免历史连接初始化连接</li>
<li>同步双方初始化序号</li>
<li>避免浪费资源</li>
</ul>
<p>如果是两次握手则⽆法防⽌历史连接的建⽴，会造成双⽅资源的浪费，也⽆法可靠的同步双⽅序列号。</p>
<p>如果是四次握手是因为三次握⼿就已经理论上最少可靠连接建⽴，所以不需要使⽤更多的通信次数。</p>
<h2 id="为什么是四次挥手">为什么是四次挥手</h2>
<ul>
<li>客户端发送 FIN 报文时，仅代表客户端没有数据要向服务端发送</li>
<li>因为 TCP 是双向连接的，可以互相发送数据。所以当服务端收到 FIN 报文时，此时服务端可能还有数据要发送到客户端，所以先返回 ACK 报文，等所有数据发送结束再向客户端发送 FIN 报文</li>
</ul>
<h2 id="为什么关闭时需要-2msl-的延时">为什么关闭时需要 2MSL 的延时</h2>
<p>MSL 是 Maximum Segment Lifetime 最大报文生存时间，它是任何报文在网络中存在的最长时间，超出此时间的报文都会被丢弃。</p>
<p>2MSL 的合理解释是：⽹络中可能存在来⾃发送⽅的数据包，当这些发送⽅的数据包被接收⽅处理后⼜会向对⽅发送响应，所以⼀来⼀回需要等待 <strong>2</strong> 倍的时间，⽐如如果被动关闭⽅没有收到断开连接的最后的 ACK 报⽂，就会触发超时重发 FIN 报⽂，另⼀⽅接收到 FIN 后，会重发 ACK 给被动关闭⽅，⼀来⼀去正好 2MSL。</p>
<h2 id="tcp-和-udp-的区别">TCP 和 UDP 的区别</h2>
<h2 id="tcp-流量控制">TCP 流量控制</h2>
<h2 id="如果-tcp-第三次握手的包没收到会怎样">如果 TCP 第三次握手的包没收到会怎样</h2>
]]></content>
        </item>
        
        <item>
            <title>MySQL 面试问题整理</title>
            <link>https://shawzb.com/posts/2021/11/mysql-%E9%9D%A2%E8%AF%95%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86/</link>
            <pubDate>Tue, 23 Nov 2021 22:24:37 +0800</pubDate>
            
            <guid>https://shawzb.com/posts/2021/11/mysql-%E9%9D%A2%E8%AF%95%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86/</guid>
            <description>数据库的三大范式  第一范式：表中的每一列都不可再拆分 第二范式：在第一范式的基础上，表中的每一列都和主键相关 第三范式：在第二范式的基础上，表中的每一列都和主键直接相关，不能间接相关  MySQL 常见的存储引擎 存储引擎 存储引擎是在数据库中进行创建、读取、修改和删除数据的底层组件。不同的存储引擎提供不同的存储机制，而且还有许多不同的功能，在 MySQL 中我们可以使用show engines;来查看所支持的存储引擎状况。
MyISAM MyISAM 是 MySQL 5.5 之前默认的存储引擎，性能高但不支持事务，也不支持外键。
 高性能读取 保存了表内记录条数，使用count时不会查全表 只支持表锁，虽然开销小、加锁快，但容易发生冲突  InnoDB InnoDB 是一个支持事务的存储引擎，是现在 MySQL 的默认存储引擎。
 支持事务、外键 支持行级锁，粒度小，并发处理能力更强  数据库事务 事务是指具有 ACID 特性的一组操作，使用commit来提交一个事务，用rollback来回滚一个事务。
 原子性（Atomicity）  原子性是指事务被视为不可分割的最小执行单元，简单地说就是一个事务要么全成功，要么全失败，失败时会利用回滚日志（Undo Log）将数据会滚到事务发生之前的状态。
 一致性（Consistency）  一致性是指在事务执行前后，数据库的完整性约束没有被破坏。
 A（100元） 向 B（0元） 转账 100 元，转账前后 A 和 B 两人共 100 元不变
  隔离性（Isolation）  隔离性是指一个事务所修改的内容在提交之前对其他事务是不可见的，InnoDB 依靠 MVCC 多版本并发控制来保证隔离性。
 持久性（Durability）  持久性是指当事务提交后，这些修改应该永久性的保存到数据库（文件系统）中，即使系统崩溃，执行结果也不能丢失。当系统发生崩溃时使用重做日志（Redo Log）来恢复数据。</description>
            <content type="html"><![CDATA[<h2 id="数据库的三大范式">数据库的三大范式</h2>
<ul>
<li>第一范式：表中的每一列都不可再拆分</li>
<li>第二范式：在第一范式的基础上，表中的每一列都和主键相关</li>
<li>第三范式：在第二范式的基础上，表中的每一列都和主键直接相关，不能间接相关</li>
</ul>
<h2 id="mysql-常见的存储引擎">MySQL 常见的存储引擎</h2>
<h3 id="存储引擎">存储引擎</h3>
<p><a href="https://en.wikipedia.org/wiki/Database_engine">存储引擎</a>是在数据库中进行创建、读取、修改和删除数据的底层组件。不同的存储引擎提供不同的存储机制，而且还有许多不同的功能，在 MySQL 中我们可以使用<code>show engines;</code>来查看所支持的存储引擎状况。</p>
<p><img src="https://minio.shaozb.xin/typora/cad8f96f-7eb4-4d1b-9460-203dd91eafd7.png" alt="MySQL 8 中引擎支持情况"></p>
<h3 id="myisam">MyISAM</h3>
<p><a href="https://en.wikipedia.org/wiki/MyISAM">MyISAM</a> 是 MySQL 5.5 之前默认的存储引擎，性能高但不支持事务，也不支持外键。</p>
<ul>
<li>高性能读取</li>
<li>保存了表内记录条数，使用<code>count</code>时不会查全表</li>
<li>只支持表锁，虽然开销小、加锁快，但容易发生冲突</li>
</ul>
<h3 id="innodb">InnoDB</h3>
<p><a href="https://en.wikipedia.org/wiki/InnoDB">InnoDB</a> 是一个支持事务的存储引擎，是现在 MySQL 的默认存储引擎。</p>
<ul>
<li>支持事务、外键</li>
<li>支持行级锁，粒度小，并发处理能力更强</li>
</ul>
<h2 id="数据库事务">数据库事务</h2>
<p>事务是指具有 <strong>ACID</strong> 特性的一组操作，使用<code>commit</code>来提交一个事务，用<code>rollback</code>来回滚一个事务。</p>
<ul>
<li>原子性（Atomicity）</li>
</ul>
<p>原子性是指事务被视为不可分割的最小执行单元，简单地说就是一个事务要么全成功，要么全失败，失败时会利用回滚日志（Undo Log）将数据会滚到事务发生之前的状态。</p>
<ul>
<li>一致性（Consistency）</li>
</ul>
<p>一致性是指在事务执行前后，数据库的完整性约束没有被破坏。</p>
<blockquote>
<p>A（100元） 向 B（0元） 转账 100 元，转账前后 A 和 B 两人共 100 元不变</p>
</blockquote>
<ul>
<li>隔离性（Isolation）</li>
</ul>
<p>隔离性是指一个事务所修改的内容在提交之前对其他事务是不可见的，InnoDB 依靠 MVCC 多版本并发控制来保证隔离性。</p>
<ul>
<li>持久性（Durability）</li>
</ul>
<p>持久性是指当事务提交后，这些修改应该永久性的保存到数据库（文件系统）中，即使系统崩溃，执行结果也不能丢失。当系统发生崩溃时使用重做日志（Redo Log）来恢复数据。</p>
<h2 id="并发情况下会出现哪些问题">并发情况下会出现哪些问题</h2>
<h3 id="丢失修改">丢失修改</h3>
<p>是指两个事务同时修改一条记录，可能会造成其中一个事务的结果被另一个覆盖。</p>
<p><img src="https://minio.shaozb.xin/typora/c5ca15cd-abca-4d75-84fb-de8a118c0417.png" alt="丢失修改"></p>
<h3 id="脏读">脏读</h3>
<p>是指在一个事务中读取到了另外一个事务修改未提交的数据。</p>
<p><img src="https://minio.shaozb.xin/typora/dd1f8ff4-5f22-4245-93ed-3086e76dbc3e.png" alt="脏读"></p>
<h3 id="不可重复读">不可重复读</h3>
<p>是指在一个事务中读取到别的事务已提交的修改，此时会造成事务中前后若干次读取的<strong>数据</strong>不一致。</p>
<p><img src="https://minio.shaozb.xin/typora/093ffb06-c294-4a11-9d02-3226a0cc2294.png" alt="不可重复读"></p>
<h3 id="幻读">幻读</h3>
<p>和不可重复读比较类似，幻读是指在一个事务中读取到另外一个事务插入或者删除的数据，此时当前事务中前后若干次读取的数据记录<strong>条数</strong>不一致。</p>
<p><img src="https://minio.shaozb.xin/typora/9dca1b04-9ccd-4e89-b3a9-5aa9e36d54eb.png" alt="幻读"></p>
<h2 id="事务的隔离级别有哪些分别解决了什么问题">事务的隔离级别有哪些？分别解决了什么问题</h2>
<table>
<thead>
<tr>
<th>隔离级别</th>
<th>描述</th>
<th>脏读</th>
<th>不可重复读</th>
<th>幻读</th>
</tr>
</thead>
<tbody>
<tr>
<td>读未提交（Read Uncommitted）</td>
<td>事务中的修改即使没有提交对其他事务也是可见的</td>
<td>❌</td>
<td>❌</td>
<td>❌</td>
</tr>
<tr>
<td>读已提交（Read Committed）</td>
<td>只能读取到已经提交了的事务修改的数据</td>
<td>✅</td>
<td>❌</td>
<td>❌</td>
</tr>
<tr>
<td>可重复读（Repeatable Read）</td>
<td>保证在事务中多次读取同一数据结果一致</td>
<td>✅</td>
<td>✅</td>
<td>❌</td>
</tr>
<tr>
<td>串行化（Serializable）</td>
<td>强制所有的事务串行化执行，不会有任何并发</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
</tr>
</tbody>
</table>
<h2 id="有哪些类型的锁">有哪些类型的锁</h2>
<p>锁是协调多个进程或线程并发访问某一资源的一种机制，在数据库中是用于保证数据并发访问一致性的一种手段。</p>
<h3 id="按锁的粒度分">按锁的粒度分</h3>
<h4 id="行锁">行锁</h4>
<p>行锁的锁定粒度是最小的，只需对操作的当前行进行锁定，能够支持较大的并发量。在 InnoDB 引擎中，只有通过索引检索数据的时候才能使用行锁。</p>
<p>行锁的获取分为两步：</p>
<ol>
<li>锁住主键索引</li>
<li>锁住非主键索引</li>
</ol>
<p>因此行锁在并发量大的时候容易出现死锁的问题。</p>
<h4 id="表锁">表锁</h4>
<p>表锁是粒度最大的一种锁，会对当前操作的整张表加锁，所以不适合高并发场景。在 InnoDB 引擎中，当无法通过索引检索数据的时候会使用表锁。</p>
<h4 id="页锁">页锁</h4>
<p>页锁是一种锁粒度介于行锁和表锁中间的一种锁，在 MySQL 中仅有 BDB 引擎支持页锁。</p>
<h3 id="按锁的类型分">按锁的类型分</h3>
<h4 id="读写锁">读写锁</h4>
<ul>
<li>互斥锁（X 锁）：又称写锁</li>
<li>共享锁（S 锁）：又称读锁</li>
</ul>
<p>从两个锁的名字就能看出其特点：当一个事务对数据加了写锁，事务本身可以对加了锁的对象读取和修改，但其他事务只能读取不能修改；当一个事务对数据加了读锁，所有事务都可以对其读取，但不能修改。</p>
<h4 id="意向锁">意向锁</h4>
<p>意向锁是一种表级锁，可以更容易地支持多粒度封锁。事务 A 持有表中一条数据的写锁，此时事务 B 想要对表加写锁，此时会发生冲突，所以在事务 B 加写锁之前要对表中记录逐行扫描检测是否有其他的写锁，这样费时又费力；所以引入了意向锁这个概念，一个事务要想获取表中记录的锁，那就必须先获取表的对应意向锁，此时其他事务加锁前只需检测表中是否有其他意向锁即可。</p>
<ul>
<li>意向互斥锁（IX 锁）</li>
</ul>
<p>事务要想获取某些记录的写锁，必需先获取表的意向互斥锁。</p>
<ul>
<li>意向共享锁（IS 锁）</li>
</ul>
<p>事务要想获取某些记录的共享锁，必须先获取表的意向共享锁。</p>
<table>
<thead>
<tr>
<th>各种锁的兼容情况</th>
<th>互斥锁</th>
<th>共享锁</th>
<th>意向互斥锁</th>
<th>意向共享锁</th>
</tr>
</thead>
<tbody>
<tr>
<td>互斥锁</td>
<td>❌</td>
<td>❌</td>
<td>❌（行级别的可以兼容）</td>
<td>✅</td>
</tr>
<tr>
<td>共享锁</td>
<td>❌</td>
<td>✅</td>
<td>❌（行级别的可以兼容）</td>
<td>✅</td>
</tr>
<tr>
<td>意向互斥锁</td>
<td>❌（行级别的可以兼容）</td>
<td>❌</td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td>意向共享锁</td>
<td>❌（行级别的可以兼容）</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
</tr>
</tbody>
</table>
<h2 id="锁有哪些算法实现">锁有哪些算法实现</h2>
<h3 id="record-lock记录锁">Record Lock（记录锁）</h3>
<p>记录锁就是加在索引记录上的锁，通过索引检索数据时使用的都是记录锁。</p>
<h3 id="gap-lock间隙锁">Gap Lock（间隙锁）</h3>
<p>锁定索引之间的间隙，不包含索引本身。例如当一个事务执行以下语句，其它事务就不能在 t.c 中插入 15。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-sql" data-lang="sql"><span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">c</span> <span style="color:#66d9ef">FROM</span> t <span style="color:#66d9ef">WHERE</span> <span style="color:#66d9ef">c</span> <span style="color:#66d9ef">BETWEEN</span> <span style="color:#ae81ff">10</span> <span style="color:#66d9ef">and</span> <span style="color:#ae81ff">20</span> <span style="color:#66d9ef">FOR</span> <span style="color:#66d9ef">UPDATE</span>;
</code></pre></div><h3 id="next-key-lock">Next-Key Lock</h3>
<p>是记录锁和间隙锁的结合，它锁定一个前开后闭的区间，主要用于解决幻读的问题。</p>
<h2 id="mvcc-多版本并发控制">MVCC 多版本并发控制</h2>
<p>多版本并发控制（Muti-Version Concurrency Control）是 InnoDB 引擎实现隔离级别的一种具体方式，主要用于实现读已提交和可重复读两种隔离级别。</p>
<p>对于读未提交要求每次读取最新的数据，所以无需MVCC；对于串行化需要对所有事物加锁，单纯使用 MVCC 无法解决。</p>
<h3 id="基本思想">基本思想</h3>
<p>MVCC 利用多版本的思想，使读操作读取旧版本快照，而写操作更新最新的快照，没有互斥关系。在 MVCC 中事务的修改操作会使得记录增加一个快照，为了解决脏读和不可重复读，MVCC 规定只能读取以提交的快照。</p>
<h3 id="原理">原理</h3>
<p>MVCC 的多版本指的是多个版本的快照，快照存储在 Undo Log 日志中，利用回滚指针把数据所有版本的快照串起来。MVCC 维护了一个 ReadView 的结构，该结构记录了系统中所有未提交的事务 ID，同时记录这些事务 ID 的最小值和最大值用于判断。</p>
<p>当执行 Select 操作时，会根据数据行快照的事务 ID 和 ReadView 中的事务 ID 进行比较：</p>
<ul>
<li>当前快照事务 ID 小于 ReadView 中事务 ID 的最小值，说明该快照是在当前所有未提交的事务之前修改的，可用</li>
<li>当前快照事务 ID 大于 ReadView 中事务 ID 的最大值，说明该快照是在事务启动之后提交的，所以不可用</li>
<li>当前快照事务 ID 介于 ReadView 中事务 ID 最大值和最小值之间则要根据隔离级别来判断快照是否可用
<ul>
<li>读已提交：如果快照事务 ID 在 ReadView 中，说明事务未提交，不可用；反之则可用</li>
<li>可重复读：不可用，因为可用是对所有事务的，此时会造成不可重复读的现象</li>
</ul>
</li>
</ul>
<p>如果当前快照不可用时，会根据 Undo Log 中的回滚指针找到上一版本的快照，重复上述的判断。</p>
<h2 id="索引的类型">索引的类型</h2>
<h3 id="按结构分类">按结构分类</h3>
<ul>
<li>
<p>B+ Tree 索引</p>
</li>
<li>
<p>Hash 索引</p>
</li>
<li>
<p>Full-Text 索引</p>
</li>
</ul>
<p>在MySQL 5.6 以前的版本，只有 MyISAM 存储引擎支持全文索引；5.6 之后的版本 InnoDB 和 MyISAM 都支持全文索引；只有文本类型的字段才可以使用全文索引。</p>
<h3 id="按存储方式分类">按存储方式分类</h3>
<ul>
<li>聚簇索引：索引树的叶节点包含了全部字段</li>
<li>辅助索引：索引树的叶子节点仅记录主键的值而不是地址</li>
</ul>
<h3 id="按字段分类">按字段分类</h3>
<ul>
<li>主键索引</li>
<li>普通索引</li>
<li>前缀索引：截取字段的前 n 个字符建立的索引</li>
<li>覆盖索引：从辅助索引中就可以得到想要的结果</li>
</ul>
<h3 id="按字段个数分类">按字段个数分类</h3>
<ul>
<li>单列索引</li>
<li>组合索引（联合索引）</li>
</ul>
<h2 id="索引有哪些优缺点">索引有哪些优缺点</h2>
<p>优点：</p>
<ul>
<li>加快查询速度，减少查询时间</li>
</ul>
<p>缺点：</p>
<ul>
<li>维护索引比较耗时</li>
<li>索引会占用额外的空间</li>
<li>在增删改的时候要同步修改索引</li>
</ul>
<h2 id="索引使用的策略及优化">索引使用的策略及优化</h2>
<ul>
<li>全列匹配：当按照索引中所有列精确查找时会成功使用索引。原则上语句中索引的顺序会对是否能匹配上索引有影响，但 MySQL 优化器会自动帮你调整为索引列的顺序。</li>
<li>最左匹配原则：在索引<code>&lt;a,b,c&gt;</code> 中使用了 <code>&lt;a,b&gt;</code>，此时可以使用索引</li>
<li>使用了索引列的精确匹配，但缺少中间某个索引列：比如在索引<code>&lt;a,b,c&gt;</code> 中仅使用了 <code>&lt;a,c&gt;</code>，此时可以使用索引，但仅有<code>a</code>列可以匹配，<code>c</code> 列仍需全表扫描</li>
<li>匹配索引列的前缀：使用 <code>like</code> 字段时如果通配符出现在最前面的时候不能使用索引，通配符仅能出现在后面，例如：<code>like 'xxx%' </code> 可以使用索引，但 <code>like '%xxx' </code> 不可以</li>
<li>查询条件中含有函数或者表达式：不能匹配索引</li>
<li>出现了隐式类型转换时不能使用索引，比如查询一个 <code>varchar</code> 索引列的时候没有加两侧的单引号，此时不会命中索引</li>
<li><code>or</code> 左右两侧必须都为索引列，否则不能使用索引</li>
</ul>
<h3 id="索引列的选择">索引列的选择</h3>
<ul>
<li>表记录表较少的时候可以不建立索引</li>
<li>索引的选择性较低时不建立索引</li>
</ul>
<p>索引的选择性是指索引列不重复的字段值数量占中数量的比例</p>
<h2 id="b-tree-和-b-tree-索引的区别">B Tree 和 B+ Tree 索引的区别</h2>
<ul>
<li>B+ Tree 的非叶子结点不存储数据，只存储节点信息。而 B Tree 的非叶子结点也会存储数据。所以 B+ Tree 单个节点数据量更小，相同 IO 次数下可以找到更多节点。</li>
<li>B+ Tree 的叶子结点增加了指针，仿佛一整个单链表，非常适合 MySQL 的范围查找</li>
</ul>
<h2 id="选用-b-tree-的原因">选用 B+ Tree 的原因</h2>
<ul>
<li>不使用 Hash 的原因是虽然 Hash 精确查找效率很高，但范围查找效率很糟糕</li>
<li>二叉平衡树/红黑树在结点数量大的时候树的深度会很大</li>
<li>B Tree/B+ Tree 的每一个节点的大小设置为一个页的大小，这样一个节点只需一次磁盘 IO 即可全部载入</li>
<li>因为 B+ Tree 的非叶子结点不存储数据，这样在相同大小下可以存储更多子节点信息，这样会大幅提高我们查找的效率</li>
<li>同时 B+ Tree 叶子结点之间是有指针的，这样可以大幅增加我们范围查找的效率</li>
</ul>
<h2 id="为什么innodb表必须有主键并且推荐使用整型的自增主键">为什么InnoDB表必须有主键，并且推荐使用<strong>整型</strong>的<strong>自增</strong>主键</h2>
<ul>
<li>因为 InnoDB 引擎的数据文件要按照主键聚集，所以必须要有主键。如果不主动指定主键，引擎会帮你创建一个隐藏的主键列。</li>
<li>整型的主键更容易排序生成 B+ Tree</li>
<li>自增主键可以有效的减少 B+ Tree 的结点分裂，因为直接放到最后即可</li>
</ul>
<h2 id="mysql-的日志类型">MySQL 的日志类型</h2>
<h3 id="redo-log">Redo Log</h3>
<p>用来实现和事务持久化相关的日志，由 Redo Log Buffer 和 Redo Log File 两部分组成。Redo Log 随着事务的创建而产生，但事务提交后不一定写入 Redo Log File，因为 Redo Log 要先写入 Redo Log Buffer 然后根据不同的磁盘写入策略（<code>innodb_flush_log_at_trx_commit</code>）在不同的时机写入文件中。</p>
<p><em>innodb_flush_log_at_trx_commit</em> 字段取值：</p>
<ul>
<li>
<p>0：每次事务提交时写入 Redo Log Buffer，每秒从 Redo Log Buffer 写入到磁盘中</p>
</li>
<li>
<p>1：每次事务提交时写入 OS Buffer，同时写入到磁盘</p>
</li>
<li>
<p>2：每次事务提交时写入 OS Buffer，每秒从 OS Buffer 写入到磁盘中</p>
</li>
</ul>
<h3 id="undo-log">Undo Log</h3>
<p>主要用来保证事务的原子性，同时和 MVCC 有着紧密联系。Undo Log 产生于数据修改之前，但并不会在数据修改后删除，而是将 Undo Log 放到一个链表中，是否删除由 Purge 线程决定</p>
<h3 id="binlog">BinLog</h3>
<p>二进制文件主要用于进行基于时间点的恢复及主从复制环境的建立。BinLog 有<strong>三种</strong>格式：</p>
<ul>
<li>Statement：记录每次执行的 SQL 语句</li>
<li>Row：记录每行数据的修改</li>
<li>Mixed：默认情况下采用 Statement 格式，在某些情况会切换到 Row 格式
<ul>
<li>使用函数或者系统变量时</li>
<li>含有 AUTO_INCREMENT 列时</li>
</ul>
</li>
</ul>
<h3 id="slow-query-log">Slow Query Log</h3>
<p>记录执行时间较长的语句，超时时间需自己配置。通过<code>show variables like '%slow_query_log%'</code>可以查看相关参数值。</p>
<h2 id="mysql-死锁">MySQL 死锁</h2>
<h3 id="在-mysql-中出现死锁的条件">在 MySQL 中出现死锁的条件</h3>
<ol>
<li>两个及两个以上的事务</li>
<li>每个事务都已经持有锁，并且申请新的锁</li>
<li>锁同时只能被一个事务占有</li>
<li>事务间因为持有锁和申请锁导致彼此循环等待</li>
</ol>
<h3 id="如何避免出现死锁">如何避免出现死锁</h3>
<ul>
<li>合理设计索引，尽可能使语句利用索引</li>
<li>避免大事务</li>
<li>以固定的顺序访问表和行</li>
<li>尽量按主键查找记录</li>
<li>在高并发的系统中不要显示加锁，例如 <code>select xxx from for update</code></li>
</ul>
<h2 id="mysql-主从复制">MySQL 主从复制</h2>
<p>主从复制是指数据可以从数据库的主节点复制到一个或多个从节点中，可以给数据库提供一个有效的备份方式，同时可以有效的缓解数据库的压力。</p>
<h3 id="实现原理">实现原理</h3>
<ol>
<li>主库将变更内容写入 binlog</li>
<li>从库启动一个 IO 线程向主库请求 binlog，主库根据从库提交的偏移量发送 binlog</li>
<li>从库获取到 binlog 后写入到 Relay Log（中继日志）</li>
<li>从库启动 SQL 线程根据 Relay Log 中的内容进行重放</li>
</ol>
<h3 id="同步模式">同步模式</h3>
<ul>
<li>异步模式：MySQL 默认的同步模式，主库在执行完客户端的请求后直接响应客户端，不管 binlog 有没有成功同步到从库</li>
<li>半同步模式：主库在执行完客户端的请求等至少收到一个从库返回的确认信息再响应客户端</li>
<li>全同步模式：主库在执行完客户端的请求等收到所有从库返回的确认信息再响应客户端</li>
<li>并行模式：从库启动多个线程分别读取 Relay Log 中不同数据库的日志，并行重放</li>
</ul>
<h3 id="主要用途">主要用途</h3>
<ul>
<li>热备</li>
<li>读写分离</li>
<li>高可用</li>
</ul>
<h2 id="如何解决主从复制的延时">如何解决主从复制的延时</h2>
<ul>
<li>分库，降低主库的压力</li>
<li>并行复制</li>
</ul>
<h2 id="mysql-的内连接左连接右连接">MySQL 的内连接、左连接、右连接</h2>
<ul>
<li>内连接：保留两张表完全匹配的结果集</li>
<li>左连接：返回左边所有的行，无论右边是否有</li>
<li>右连接：返回右边所有的行，无论左边是否有</li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>JVM 知识简记</title>
            <link>https://shawzb.com/posts/2021/11/jvm-%E7%9F%A5%E8%AF%86%E7%AE%80%E8%AE%B0/</link>
            <pubDate>Wed, 10 Nov 2021 15:48:11 +0800</pubDate>
            
            <guid>https://shawzb.com/posts/2021/11/jvm-%E7%9F%A5%E8%AF%86%E7%AE%80%E8%AE%B0/</guid>
            <description>什么是 JVM JVM 是 Java Virtual Machine 的缩写，意为 Java 虚拟机，一种可以运行 Java 编译后生成的字节码的虚拟机。有了 JVM 可以使我们不用修改代码就能运行在各个操作系统上。众所周知在 C/C++ 中是需要开发人员手动管理内存的申请以及释放，稍有不慎就造成了内存泄露或者内存溢出，而 JVM 提供的自动内存管理机制极大的简化了我们的开发过程。
而 JVM 不只有一种，大家都可以按照《Java虚拟机规范》开发自己的虚拟机，常见的虚拟机有：
 HotSpot VM JRockit VM Sun Classic VM J9 VM Exact VM  其中HotSpot 是使用最广泛的虚拟机
注：JVM 可以运行任何符合规范的字节码文件，不仅限于 Java 语言编译后的字节码，其他语言可以参照规范自己实现编译器编译。
Java 内存区域 上面提到的 JVM 内存管理机制本质上就是在程序运行的过程中把内存分成若干不同的区，这就是 Java 内存区域。
先来看这两块不属于 JVM 的内存区域
 本地内存  是随着 JVM 进程一起从操作系统申请的内存空间，也就是运行 JVM 所使用的内存。
 直接内存  直接内存是 NIO（New Input/Out）使用 Native 函数库开辟的内存空间，通过堆中的 DirectByteBuffer 与该内存空间进行操作。
上面两块内存区域不受 Java 堆的大小限制，只与机器的内存相关，但如果配置不当，也会使程序出现 OOM 异常。</description>
            <content type="html"><![CDATA[<h2 id="什么是-jvm">什么是 JVM</h2>
<p>JVM 是 Java Virtual Machine 的缩写，意为 Java 虚拟机，一种可以运行 Java 编译后生成的字节码的虚拟机。有了 JVM 可以使我们不用修改代码就能运行在各个操作系统上。众所周知在 C/C++ 中是需要开发人员手动管理内存的申请以及释放，稍有不慎就造成了内存泄露或者内存溢出，而 JVM 提供的自动内存管理机制极大的简化了我们的开发过程。</p>
<p>而 JVM 不只有一种，大家都可以按照《Java虚拟机规范》开发自己的虚拟机，常见的虚拟机有：</p>
<ul>
<li><a href="https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html">HotSpot VM</a></li>
<li><a href="https://en.wikipedia.org/wiki/JRockit">JRockit VM</a></li>
<li>Sun Classic VM</li>
<li><a href="https://www.ibm.com/docs/en/sdk-java-technology/7?topic=i-j9-virtual-machine-jvm">J9 VM</a></li>
<li>Exact VM</li>
</ul>
<p><em>其中HotSpot 是使用最广泛的虚拟机</em></p>
<p><em>注：JVM 可以运行任何符合规范的字节码文件，不仅限于 Java 语言编译后的字节码，其他语言可以参照规范自己实现编译器编译。</em></p>
<h2 id="java-内存区域">Java 内存区域</h2>
<p>上面提到的 JVM 内存管理机制本质上就是在程序运行的过程中把内存分成若干不同的区，这就是 Java 内存区域。</p>
<p><img src="https://minio.shaozb.xin/typora/908bf3c2-ca20-4a87-bd50-a28e7b9b3fb3.png" alt="Java 内存区域示意图"></p>
<p>先来看这两块不属于 JVM 的内存区域</p>
<ul>
<li>本地内存</li>
</ul>
<p>是随着 JVM 进程一起从操作系统申请的内存空间，也就是运行 JVM 所使用的内存。</p>
<ul>
<li>直接内存</li>
</ul>
<p>直接内存是 NIO（New Input/Out）使用 Native 函数库开辟的内存空间，通过<code>堆</code>中的 <code>DirectByteBuffer</code> 与该内存空间进行操作。</p>
<p>上面两块内存区域不受 Java 堆的大小限制，只与机器的内存相关，但如果配置不当，也会使程序出现 OOM 异常。</p>
<p>以下是由 JVM 所管理的内存区域，也就是运行时数据区（Run-Time Data Areas）</p>
<h3 id="线程共享的内存区域">线程共享的内存区域</h3>
<ul>
<li>堆</li>
</ul>
<p>堆是随 JVM 启动而创建的内存区域，是 JVM 所管理的最大的一块内存区域，几乎所有的对象实例和数组都在该区域分配内存，同时也是会触发垃圾回收机制的内存空间。</p>
<ul>
<li>方法区</li>
</ul>
<p>方法区是一个用于存储被 JVM 加载的类型信息、常量、静态变量、即时编译后的代码缓存等数据。在《Java 虚拟机规范》中提到方法区逻辑上属于<code>堆</code>的一部分，但在具体实现上通常不会在此区域进行垃圾回收或者压缩操作，所以在 HotSpot 虚拟机实现的时候选则用永久代（PermGen）作为方法区的实现。</p>
<p>不过选择使用<code>永久代</code>实现方法区会使得程序更容易发生内存溢出的现象，所以从 JDK 1.7 开始逐步使用<code>元空间（Meta Space）</code>替换永久代作为方法区的实现，并把方法区移到了本地内存中。</p>
<ul>
<li>运行时常量池</li>
</ul>
<p>运行时常量池是分配在方法区中的一块空间。主要用于存储 Class 文件中的常量池表（Constant Pool Table），也就是编译期生成的各种字面量与符号引用，这部分内容在类加载后创建。</p>
<h3 id="线程私有的内存区域">线程私有的内存区域</h3>
<ul>
<li>程序计数器</li>
</ul>
<p>程序计数器可以理解为当前线程所执行的字节码的行号指示器，字节码解释器就是通过修改程序计数器的值来决定下一条要执行的字节码指令，像程序中的分支、跳转、循环和异常恢复等都是依赖程序计数器来完成的。</p>
<p>在多线程中应用中，为了线程切换能够恢复到正确的位置，每个线程都有一个互相独立、互不影响的程序计数器。</p>
<p>当一个线程执行的是 Java 方法，此时程序计数器的值指向的是正在执行的字节码指令地址；而当执行的是 Native 方法时，程序计数器的值为空（Undefined）</p>
<p><em>注：该区域是唯一一个不会出现 OOM 的内存区域</em></p>
<ul>
<li>虚拟机栈</li>
</ul>
<p>每个线程内部都有一个虚拟机栈，随线程的创建而创建。虚拟机栈内部存储着若干<code>栈帧</code>，每执行一个方法，就会对应创建一个栈帧，用于记录局部变量表、操作数栈、动态连接和方法出口等信息。每一个方法调用到结束的过程，就对应一个栈帧在虚拟机栈中入栈到出栈的过程。</p>
<p>如果线程请求栈的深度大于虚拟机所允许的最大上限时，会抛出<code>StackOverflowError</code>异常。</p>
<ul>
<li>本地方法栈</li>
</ul>
<p>本地方法栈和虚拟机栈功能类似，只不过记录的是 Native 方法。</p>
<h2 id="方法区和永久代的区别">方法区和永久代的区别</h2>
<p>方法区是《Java 虚拟机规范》中所提及的一个规范，并没有规定如何实现，而永久代就是依据这一规范实现的。可以理解为方法区是接口，永久代实现了方法区这一接口，是具体的实现类。也就是说永久代只是 HotSpot 虚拟机根据《Java 虚拟机规范》中所描述的方法区规范实现出来的一个概念，在其他的虚拟机中不存在永久代这个概念。</p>
<h2 id="为什么要用元空间替换永久代">为什么要用元空间替换永久代</h2>
<ol>
<li>在 JDK 1.8 合并 HotSpot 虚拟机和 JRockit 虚拟机时，JRockit 中不存在永久代的概念，所以顺势删除了永久代</li>
<li>永久代在方法区内部，受 JVM 整体内存大小受影响，相比之下放在本地内存的元空间不受 JVM 内存大小影响，只受系统内存空间影响，可以加载更多的类</li>
</ol>
<h2 id="对象的创建过程">对象的创建过程</h2>
<h3 id="1-类加载检查">1. 类加载检查</h3>
<p>当虚拟机遇到 new 指令的时候会先去常量池中检查对应类的符号引用是否存在，并检查类是否已经加载过。如果没有，会先去执行对应的类加载过程。</p>
<h3 id="2-分配内存">2. 分配内存</h3>
<p>当通过类加载检查后，会为这个类分配内存空间，一个类所需的内存空间在加载结束时就已经确定了，内存分配的过程就是在剩余的内存空间中找到一块放的下空间的过程。</p>
<p>内存的分配方式有两种：</p>
<ul>
<li>指针碰撞</li>
</ul>
<p>当内存空间“规整”时使用，原理就是有一个指针指在已用过的内存和未使用的内存中间，当需要分配内存的时候，指针向后偏移所需的空间大小即可。</p>
<ul>
<li>空闲列表</li>
</ul>
<p>当内存空间不怎么“规整”时，可能没办法直接找到合适的内存空间用于分配，所以虚拟机内部维护了一份可用空间的列表，内存分配的过程就是在列表上找到足够大的内存空间分配给对象实例。</p>
<p>具体采用哪种方式取决于虚拟机内存空间是否“规整”，而内存空间是否规整又取决于使用的垃圾回收算法是“<strong>标记-整理</strong>”还是“<strong>标记-清除</strong>”。</p>
<p>当虚拟机使用 Seial、ParNew等带有“整理”功能的垃圾回收器时，系统采用指针碰撞的方法分配内存，简单又高效；而当使用 CMS 这种基于“清除”的垃圾回收器时，就只能采用空闲列表的方式分配内存。</p>
<table>
<thead>
<tr>
<th>分配方式</th>
<th>适用场景</th>
<th>原理</th>
<th>垃圾回收器</th>
</tr>
</thead>
<tbody>
<tr>
<td>指针碰撞</td>
<td>内存空间规整</td>
<td>有指针指向已用的空间和未使用的空间中间，更直接的定位到内存开始的地址</td>
<td>Serial、ParNew</td>
</tr>
<tr>
<td>空闲列表</td>
<td>内存空间不规整</td>
<td>利用额外的一个列表记录内存中空闲的内存空间</td>
<td>CMS</td>
</tr>
</tbody>
</table>
<p><strong>内存分配过程中会伴随着并发的问题</strong>，通常虚拟机有两种方式保证线程安全：</p>
<ul>
<li>CAS + 重试：CAS 是一种乐观锁，也就是虚拟机会在分配内存过程中不加锁，如果发生冲突，那就重试到成功为止，虚拟机用这种方式保证内存分配的原子性。</li>
<li>TLAB：本地线程分配缓冲（Thread Local Allocation Buffer），是指预先在每个线程内部分配一小块内存空间，如果哪个线程需要分配内存，就在哪个线程的 本地缓冲区中分配，只有当线程的本地缓冲区用完了需要分配新的缓冲区时才同步锁定。</li>
</ul>
<h3 id="3-初始化零值">3. 初始化“零”值</h3>
<p>在分配到内存空间之后，要将对象中除对象头以外的值赋予默认值，保证了对象的字段可以不赋值就被使用。</p>
<h3 id="4-设置对象头">4. 设置对象头</h3>
<p>当对象初始化“零”值之后，虚拟机会对对象进行必要的设置，比如对象是哪个类的实例、类的元数据信息、类的 GC 分代年龄、以及是否启动偏向锁等，这些信息存储在对象头中。</p>
<h3 id="5-执行-init-指令">5. 执行 <code>init</code> 指令</h3>
<p>到这里，从虚拟机的角度来说，一个对象以经创建完成，但从程序的角度来看，<code>init</code> 指令还没有执行，所以 <code>new</code> 指令后往往是 <code>init</code> 指令，执行完 <code>init</code> 指令，一个对象才算真正的创建完成。</p>
<h2 id="对象的访问">对象的访问</h2>
<p>访问堆中对象主流的方式有两种：</p>
<ul>
<li>句柄：在堆中开辟一块空间用于存储句柄池，reference 中存储的是对象的句柄地址，而句柄对象中包含了对象的对象实例地址和对象类型地址。</li>
<li>直接指针：Java 栈上的 reference 中存储的直接就是对象的地址，这样节省了一次指针定位的开销</li>
</ul>
<h2 id="垃圾收集garbage-collection">垃圾收集（Garbage Collection）</h2>
<p>在 Java 内存区域中，程序计数器、虚拟机栈和本地方法栈随着线程创建而创建，随线程销毁而释放，这几个区域的内存分配具有确定性。</p>
<p>而在堆中，内存的分配具有严重的不确定性，一个接口的不同实现类需要的内存空间可能不一致，一个方法所执行的不同分支所需要的内存空间也可能不一样，只有在运行过程中我们才能知道到底需要创建多少对象，这部分内存的分配是动态的，越多的对象被创建就需要越多的内存。</p>
<p>可内存空间不是无限的，所以我们需要一种机制，帮我们在内存中找到那些可以被回收的对象并释放所占用的内存空间，保证程序的稳定运行。</p>
<h2 id="如何判断对象可以回收">如何判断对象可以回收</h2>
<p>判断对象可以回收通常有两种办法：</p>
<h3 id="引用计数法">引用计数法</h3>
<p>从名字就可以看出引用计数法的原理，给每个对象开辟一个额外的内存空间用于存储引用计数器，每当对象有被引用时，引用计数器 + 1反之则 -1。这种方法简单高效，但也面临着一些问题，所以主流的虚拟机都没有使用该方法管理内存空间，比如引用计数法无法解决对象之间循环引用的问题。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ReferenceCountingGC</span> <span style="color:#f92672">{</span>

    <span style="color:#66d9ef">public</span> Object instance <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span><span style="color:#f92672">;</span>

    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">void</span> <span style="color:#a6e22e">circularReference</span><span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
        ReferenceCountingGC objA <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> ReferenceCountingGC<span style="color:#f92672">();</span>
        ReferenceCountingGC objB <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> ReferenceCountingGC<span style="color:#f92672">();</span>
        <span style="color:#75715e">// 让两个对象互相引用对方
</span><span style="color:#75715e"></span>        objA<span style="color:#f92672">.</span><span style="color:#a6e22e">instance</span> <span style="color:#f92672">=</span> objB<span style="color:#f92672">;</span>
        objB<span style="color:#f92672">.</span><span style="color:#a6e22e">instance</span> <span style="color:#f92672">=</span> objA<span style="color:#f92672">;</span>
        objA <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span><span style="color:#f92672">;</span>
        objB <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span><span style="color:#f92672">;</span>
        <span style="color:#75715e">// 此时两个对象互相引用对方，但两个对象都不能为其他对象访问，此时就产生了循环引用
</span><span style="color:#75715e"></span>    <span style="color:#f92672">}</span>
<span style="color:#f92672">}</span>
</code></pre></div><h3 id="可达性分析">可达性分析</h3>
<p>可达性分析是通过一系列称为“GC Root”的对象出发，根据引用关系向下寻找，寻找过的路径被称为<code>引用链</code>，如果某个对象到 GC Roots 之间没有任何引用链，或者从图论的角度来看，GC Root 到这个对象不可达时，就说明该对象是不可能再被引用的。</p>
<p><img src="https://minio.shaozb.xin/typora/45926ebe-e551-4801-be66-6c27cdf15015.png" alt="可达性分析图例"></p>
<p>如上图所示，Object1-3 就是引用可达的对象，而 Object4-6 虽然互相有引用，但 GC Root 到这些对象之间没有任何的引用链，所以是引用不可达的。</p>
<p>在 Java 中，可以被称为 GC Root 的对象有：</p>
<ul>
<li>在虚拟机栈栈帧中本地变量表的对象，比如方法的参数、局部变量、临时变量等</li>
<li>方法区中类静态属性引用的对象</li>
<li>方法区中常量引用的对象</li>
<li>Native 方法引用的对象</li>
<li>被 Synchronized 关键字持有的对象</li>
</ul>
<h2 id="对象的引用类型">对象的引用类型</h2>
<p>在 Java 中共有四种引用类型：</p>
<ul>
<li>强引用（Strongly）：在代码中普遍存在的引用类型，比如 <code>Object obj = new Object()</code>，只要强引用关系在，垃圾回收器永远不会回收。</li>
<li>软引用（Soft）：描述一些有用但非必需的对象，在内存空间发生溢出前会把这些对象标记为下次回收的对象，使用<code>SoftReference</code>类实现软引用。</li>
<li>弱引用（Weak）：也是描述一些有用但非必需的对象，但这些对象会在下一次回收时被回收掉，使用<code>WeakReference</code>类实现弱引用。</li>
<li>虚引用（Phantom）：最弱的一种引用关系，无法通过虚引用获得任何对象，虚引用唯一的作用就是当对象被回收的时候会收到来自系统的通知，使用<code>PhantomReference</code>类实现虚引用。</li>
</ul>
<p>以上四种引用强度依次减弱</p>
<h2 id="方法区的回收">方法区的回收</h2>
<p>在《Java 虚拟机规范》中指出，方法区可以不实现垃圾回收，但不意味着在方法区没有垃圾回收。只不过与堆相比，方法区的垃圾回收效果甚微，性价比通常是比较低的。</p>
<p>在方法区主要回收两类对象：</p>
<ul>
<li><code>废弃的</code>的常量：比如曾经创建了一个“java”的字符串常量，但现在没有任何一个字符串的值是“java”，此时“java”这个常量就算是<code>废弃的</code>常量，在下次方法区的垃圾回收时回被释放掉。</li>
<li>不再使用的类型</li>
</ul>
<p>判断一个类型是否属于不再使用的类型要<strong>同时</strong>满足以下三个条件：</p>
<ul>
<li>该类型的所有实例都已经被回收</li>
<li>加载该类型的类加载器已经被回收</li>
<li>该类的 Class 对象没有在任何地方被引用，即无法通过反射在访问到该类的方法</li>
</ul>
<p>Java 虚拟机<strong>允许</strong>满足上面三个条件的类型进行回收，但<strong>并不一定会被回收</strong>。</p>
<h2 id="垃圾收集算法">垃圾收集算法</h2>
<h3 id="分代收集理论">分代收集理论</h3>
<p>现在的垃圾收集器大多遵循“分代收集”理论进行设计，它建立在三条假说之上：</p>
<ul>
<li>
<p>弱分代假说：即大多数的对象的生命周期都很短</p>
</li>
<li>
<p>强分代假说：经历过越多次的回收的对象越不容易被释放</p>
</li>
<li>
<p>跨代引用假说：跨代引用的现象只是极少数</p>
</li>
</ul>
<p>正是因为上面的假说才奠定了多数垃圾收集器一致的设计原则：Java 堆应该被划分成多个区域，根据对象的年龄（经历过垃圾收集的次数）放置到不同的区域。</p>
<p>从分代收集的理论看现代的虚拟机，大多都将<code>堆</code>分成两部分：新生代（Young Generation）和老年代（Old Generation）。在新生代区域中，每次发生垃圾收集会有大量对象被释放，只会留下少数对象，而这少数对象会逐步&quot;升级&quot;到老年代中。</p>
<h3 id="标记-清除算法">标记-清除算法</h3>
<p>最早出现的垃圾收集算法，思想就是标记内存中可以被释放的内存空间，然后再统一释放这些内存空间；也可以反过来标记不用被释放的内存，然后释放未被标记的内存空间。</p>
<p>后续出现的垃圾收集算法都是以标记-清除算法为基础，优化其缺点产生的。它的主要缺点有：</p>
<ul>
<li>执行效率不稳定：在面对大量对象时，标记和清除这两个过程执行效率会随着对象数量增长和降低。</li>
<li>内存碎片化：标记、清除过后会产生大量不连续的内存空间，导致接下来分配大对象的时候可能没有足够的空间从而引发一次不必要的 GC。</li>
</ul>
<h3 id="标记-复制算法">标记-复制算法</h3>
<h4 id="半区复制">半区复制</h4>
<p>主要解决了“标记-清除”算法的执行效率不稳定。它将内存空间分成相等的两份，每次分配空间时只在其中一个空间分配，当这一半空间用完时，把存活的对象复制到“另一半”空间中，释放掉可以释放的内存空间。这种算法简单高效，但有个致命的缺点：<strong>可用的内存空间被减少了一半</strong>。</p>
<p>IBM 曾研究过新生代中的对象，发现 98% 的对象在第一次垃圾回收时就被释放掉了，因此现在的商用虚拟机大多都优先采用这种算法回收新生代，只不过并不是按照 1:1 分配的。</p>
<h4 id="appel-式回收">Appel 式回收</h4>
<p>Appel 式回收是将新生代分为一个较大的 Eden 区以及两个较小的 Survivor 区，每次分配内存时仅使用 Eden 区和其中的一个 Survivor 区。发生垃圾回收时，把 Eden 和 Survivor 中存活的对象都丢到另一个 Survivor 区中，并清空 Eden 和已使用的 Survivor 区。</p>
<h3 id="标记-整理算法">标记-整理算法</h3>
<p>“标记-整理”算法和“标记-清除”算法类似，都会先标记内存中可以被释放的空间，但标记-整理算法不会直接释放这些空间，而是让存活的对象都向内存空间的一端移动，然后再清理哪些可以被释放的空间。</p>
<h2 id="垃圾收集器">垃圾收集器</h2>
<h3 id="新生代可用">新生代可用</h3>
<ul>
<li>Serial 收集器：一个单线程的采用标记-复制算法的垃圾收集器，在其工作时会暂停所有的用户线程，但简单而高效（没有多线程交互的开销）。</li>
<li>ParNew 收集器：和 Serial 收集器类似，不过时垃圾收集是多线程工作，也需要暂停所有用户线程，同样采用了标记-复制算法。</li>
<li>Parallel  Scavenge 收集器</li>
</ul>
<h3 id="老年代可用">老年代可用</h3>
<ul>
<li>Serial Old 收集器：Serial 的老年代收集器，不同的是采用标记-整理收集算法。</li>
<li>Parallel Old 收集器</li>
<li>Concurrent Mark Sweep（CMS）收集器：从名字就能看出来是基于标记-清除算法实现的。CMS 收集器是一个追求最短的回收停顿时间的垃圾收集器而且 CMS 的工作线程可以与用户线程并行。</li>
</ul>
<h3 id="全年龄可用">“全年龄”可用</h3>
<ul>
<li>Garbage First 收集器：和其他收集器针对收集特定的新生代或者老年代不同，Garbage First 收集器将整个堆内存划分成若干个大小相等的独立区域（Region），每个 Region 都可以根据需求变成新生代的 Eden 区、Survivor 区或者是老年代，无论是新创建的对象还是存活了很长时间的对象都能有很好的收集效果。</li>
</ul>
<h2 id="jdk-默认的垃圾回收算法">JDK 默认的垃圾回收算法</h2>
<ul>
<li>在 JDK 5 中新生代默认使用的是 Serial（串行化复制收集），老年代默认的是 Serial Old（串行化标记整理）</li>
<li>在 JDK 7/8 中新生代默认使用 Parallel Scavenge（一个注重吞吐量的垃圾回收算法），老年代（8中的元空间）默认的是 Parallel Old</li>
<li>在 JDK 9 中默认使用 G1（全区垃圾收集）</li>
</ul>
<h2 id="jvm-调优">JVM 调优</h2>
<h3 id="什么时候要调优">什么时候要调优</h3>
<ul>
<li>堆内存持续上涨</li>
<li>Full GC 频繁</li>
<li>GC 时间过长</li>
<li>出现 OOM</li>
<li>系统吞吐量下降</li>
</ul>
<h3 id="调优的原则">调优的原则</h3>
<ul>
<li>大多数应用不需要调优</li>
<li>多数导致 GC 的问题都是代码问题</li>
<li>减少对象的创建</li>
<li>减少使用大对象和全局变量</li>
</ul>
<h3 id="调优的目标">调优的目标</h3>
<ul>
<li>GC 低频率， GC 低停顿</li>
<li>低内存占用</li>
<li>高吞吐量</li>
</ul>
<h3 id="调优涉及到的参数">调优涉及到的参数</h3>
<ul>
<li>-Xms：初始化堆内存大小</li>
<li>-Xmx：堆内存最大大小</li>
<li>-Xmn：新生代大小</li>
<li>-XX:SurvivorRatio：Eden 区与每个 Survivor 区域的比值</li>
<li>-XX:+DisableExplicitGC：禁止显示触发 GC</li>
<li>-XX:CMSInitiatingOccupancyFraction：老年代回收阈值</li>
<li>-XX:ConcGCThreads：CMS 回收器线程数</li>
<li>-XX:ParallelGCThreads：新生代并行收集器线程数</li>
<li>-XX:CMSFullGCsBeforeCompaction：经过多少次 Full GC，CMS 才会整理内存空间</li>
</ul>
<h2 id="堆内存设置的参考依据">堆内存设置的参考依据</h2>
<p>默认初始化堆内存大小设置为总内存的 $1/64$，默认堆内存最大大小 $1/4$。</p>
<p>通常设置初始化堆内存大小和堆内存最大大小相等，建议为<strong>最大</strong>可用内存的 80%。</p>
<h2 id="怎么选择合适的垃圾回收算法">怎么选择合适的垃圾回收算法</h2>
<ul>
<li>调整堆内存大小，让 JVM 自动选择</li>
<li>如果内存小于 100，选择串行化</li>
<li>单核且没有停顿时间要求，选择串行化</li>
<li>要求响应时间，选择并发收集器</li>
</ul>
]]></content>
        </item>
        
        <item>
            <title>记录使用 Nginx Proxy Manager 替换 Nginx</title>
            <link>https://shawzb.com/posts/2021/11/%E8%AE%B0%E5%BD%95%E4%BD%BF%E7%94%A8-nginx-proxy-manager-%E6%9B%BF%E6%8D%A2-nginx/</link>
            <pubDate>Mon, 08 Nov 2021 16:04:29 +0800</pubDate>
            
            <guid>https://shawzb.com/posts/2021/11/%E8%AE%B0%E5%BD%95%E4%BD%BF%E7%94%A8-nginx-proxy-manager-%E6%9B%BF%E6%8D%A2-nginx/</guid>
            <description>&lt;p&gt;&lt;a href=&#34;https://nginx.org/en/&#34;&gt;Nginx&lt;/a&gt; 是一款优秀的 HTTP 服务器，同时提供了丰富的功能，其中最常使用的就是&lt;strong&gt;反向代理&lt;/strong&gt;，可以通过配置让我们使用不同的域名在同一个端口访问不同的服务。而 &lt;a href=&#34;https://nginxproxymanager.com/&#34;&gt;Nginx Proxy Manager&lt;/a&gt; 是一个可以通过在 Web UI 中配置内部的 Nginx 实现类似我们所需要的功能的工具。&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><a href="https://nginx.org/en/">Nginx</a> 是一款优秀的 HTTP 服务器，同时提供了丰富的功能，其中最常使用的就是<strong>反向代理</strong>，可以通过配置让我们使用不同的域名在同一个端口访问不同的服务。而 <a href="https://nginxproxymanager.com/">Nginx Proxy Manager</a> 是一个可以通过在 Web UI 中配置内部的 Nginx 实现类似我们所需要的功能的工具。</p>
<p><img src="https://minio.shaozb.xin/typora/b59a09ca-f917-491d-9436-e14c2102ef1b" alt="nginx-proxy-manager"></p>
<h2 id="为什么替换">为什么替换</h2>
<p>我部署的服务虽然不多，但部分服务必须 HTTPS，比如 <a href="https://bitwarden.com/">Bitwarden</a>。所以我就通过 <a href="https://letsencrypt.org/">Let‘s Encrypt</a> 申请了泛域名证书，于是我所有的服务就都支持了 HTTPS 。但是 Let‘s Encrypt 的证书有效期只有 3 个月，想要继续使用就要定期更新。其实在机器上写个定时任务，周期性续期证书就没问题了，但是我发现我每次更新下来的证书都带有个可爱的后缀<code>-1/-2/-3...</code>，这样子也不是不能搞，但是太麻烦了。</p>
<p>最近网上冲浪🏄‍♂️的时候发现了这个工具，它最吸引我的有两点：</p>
<ol>
<li>在 Web UI 中配置就能实现和 Nginx 一样的功能</li>
<li>支持 Let&rsquo;s Encrypt 在线申请</li>
</ol>
<p>本着折腾的原则，我决定在我的机器上部署一个🔨</p>
<h2 id="替换过程记录">替换过程记录</h2>
<ul>
<li>关闭原有的 Nginx</li>
</ul>
<p>因为为了保持和原来的体验一致，那么肯定是要在 <code>:80</code>/<code>:443</code>端口进行反代，所以一定要关闭原有的 Nginx。因为我所有的服务都是 Docker 部署的，所以关闭命令很简单</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">docker stop nginx
</code></pre></div><ul>
<li>部署 Nginx Proxy Manager</li>
</ul>
<p>参考<a href="https://nginxproxymanager.com/setup/#full-setup-instructions">官网教程</a></p>
<ul>
<li>在 Web UI 中配置</li>
</ul>
<p>通过<code>http://localhost:81</code>访问后台配置页面</p>
<p><img src="https://minio.shaozb.xin/typora/8a63458b-40c5-40d9-a572-ed58e3d057b1" alt="后台配置页面"></p>
<p>点击 Proxy Hosts =&gt; Add Proxy Host 即可配置反向代理，规则参照原 Nginx 配置文件配置（这里支持泛域名配置）</p>
<p><img src="https://minio.shaozb.xin/typora/96a860a7-9923-44e3-a930-fa4abcd64713" alt="配置反向代理"></p>
<p>点击 <code>Save</code> 就完成了第一个域名的配置，此时访问我们的配置的域名，请求就会被发送发位于 198.168.0.2:5000 的服务上。</p>
<h2 id="配置-ssl-证书可选">配置 SSL 证书（可选）</h2>
<p>在 SSL Certificates 选项卡中点击 Add Certificate 按钮，可以选择上传自己已有的证书，也可以选择从 Let&rsquo;s Encrypt 重新申请，这里选择重新申请。</p>
<p><img src="https://minio.shaozb.xin/typora/9c0c63a6-eeab-477c-9aaf-18899fdc182c" alt="配置 SSL 证书"></p>
<p>Nginx Proxy Manager 支持无侵入式的 DNS 验证方式申请证书，而且支持多家 DNS 服务商。</p>
<p>如上图，输入域名（支持泛域名）按要求填写自己的 DNS 服务商的 Token/Key 之类的，点击 <code>Save</code> 就完成了第一张证书的申请，接下来就把证书配置到刚刚配置好的 Proxy Host 上吧。</p>
<p><img src="https://minio.shaozb.xin/typora/d5052518-fbf1-4123-a772-75fefa2dab5f" alt="证书管理"></p>
<p>回到 Host 管理，选择要配置的服务，切换到 SSL 选项卡，选择刚申请的域名即可完成配置。</p>
<p><img src="https://minio.shaozb.xin/typora/a7dd19d6-0976-4f68-9153-5734185c9328" alt="配置 SSL 证书"></p>
<hr>
<p>到这里就完成了一个支持 HTTPS 的服务的配置，重复上面的操作（如果申请的泛域名证书后续则不用继续申请证书，只要保证在证书的有效范围即可）就可以完成全站的替换。</p>]]></content>
        </item>
        
        <item>
            <title>Redis面试问题整理</title>
            <link>https://shawzb.com/posts/2021/11/redis%E9%9D%A2%E8%AF%95%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86/</link>
            <pubDate>Mon, 08 Nov 2021 16:00:35 +0800</pubDate>
            
            <guid>https://shawzb.com/posts/2021/11/redis%E9%9D%A2%E8%AF%95%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86/</guid>
            <description>&lt;p&gt;本文简单记录 Redis（缓存）相关的面试知识点&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>本文简单记录 Redis（缓存）相关的面试知识点</p>
<h2 id="为什么要用缓存">为什么要用缓存</h2>
<p>使用缓存的原因要从以下两个场景来分析：</p>
<ul>
<li>高性能</li>
</ul>
<p>如果没有缓存，所有得数据都是从磁盘/硬盘读取（数据库本质上也是从文件读取），此时的读写效率会比较慢。如果将数据存储读写速度超快的内存中就不会有这样的问题，所以要使用缓存</p>
<ul>
<li>高可用/高并发</li>
</ul>
<p>在高并发的场景下，如果没有缓存，大量涌入的请求会导致数据库宕机。如果把部分数据转移到效率更高的内存中，此时部分请求不会请求到数据库，这样减小了数据库系统的压力，同时提高了相应速度</p>
<h2 id="有哪些缓存中间件为什么选用-redis">有哪些缓存中间件？为什么选用 Redis</h2>
<p>常见的缓存中间件有 <a href="https://memcached.org/">Memcached</a> 和 Redis。和 Redis 一样 Memcached 也是基于内存以键值对存储的高性能数据库，但由于二者之间的区别我们通常使用 Redis 而不是 Memcached。</p>
<ul>
<li>Redis 有着更丰富的数据结构和特性，Memcached 仅支持字符串</li>
<li>Redis 支持数据持久化</li>
<li>Memcached 可以利用多核性能更高，而 Redis 只能使用单核</li>
<li>Redis 支持分布式</li>
</ul>
<p>利用了多核的 Memcached 性能更高，如果仅需要一个高性能的缓存时选择 Memcached 准没错，但相比之下 Redis 更可靠，所以通常选用 Redis。</p>
<h2 id="为什么-redis-是单线程的">为什么 Redis 是单线程的</h2>
<p>因为 Redis 是完全基于内存存储，所以 CPU 不会成为瓶颈，如果引入多线程会带来频繁的上下文切换以及锁的操作，这必然会对性能造成损耗，所以 Redis 是单线程的。</p>
<ul>
<li>纯内存操作</li>
<li>避免上下文切换</li>
<li>IO 多路复用</li>
</ul>
<p>也正是因为上面的三点让单线程的 Redis 也能有如此高的性能。</p>
<h2 id="redis-有哪些数据结构">Redis 有哪些数据结构</h2>
<p>Redis 有 5 种基本数据类型</p>
<ul>
<li>String 字符串类型</li>
</ul>
<p>String 类型是 Redis 中最重要且最常用的数据类型。Redis 是基于 C 语言开发的，因为 C 语言中的字符串不是二进制安全的，所以 Redis 使用了一种 <code>Simple Dynamic String</code>（简单动态字符串，简称 SDS）数据结构改良了 C 语言中的字符串（使用单独的字段记录字符串的长度）。</p>
<blockquote>
<p>C 语言中的字符串是以 <code>\0</code> 截断，如果字符串本身含有 <code>\0</code> ，那么字符串就会在不该被截断的位置截断导致出现错误，这种情况下是不能用于存储二进制文本，比如图像、音频和视频等文件，此时被称为是非二进制安全，而 Redis 的 SDS 则避免了这个问题。</p>
</blockquote>
<ul>
<li>Hash 散列表</li>
</ul>
<p>和 Java 中的散列表一样存储的是键值对（K - V结构），散列表有两种底层数据结构：ZipList（压缩列表）和 HashTable （哈希表），当以下满足以下两种情况时时以 ZipList 存储：</p>
<pre tabindex="0"><code>- 散列表中所有的键/值的大小都不超过64字节时
- 散列表中键值对的个数不超过512个的时候
</code></pre><ul>
<li>List</li>
</ul>
<p>一种列表，每个节点都包含一个字符串。其底层数据结构和 Hash 一样，在数据不多或者数据不大的情况下是 ZipList 存储；当无法满足时采用 LinkedList 存储</p>
<ul>
<li>Set</li>
</ul>
<p>和 List 一样可以存储若干字符串，但在 Set 内部通过维护一张只有 Key 的散列表来保证元素不会重复，但元素之间的顺序无法保证。Set 的底层有两种数据结构：HashTable 和 InSet，当以下满足以下两种情况时时以 InSet 存储：</p>
<pre tabindex="0"><code>- 集合中元素的个数不超过512个的时候
- 集合中元素可以用整型表示
</code></pre><ul>
<li>ZSet</li>
</ul>
<p>和 Hash 类似存储键值对，但 ZSet 的键叫做 Member（成员），值叫做 Score （分数，浮点数）；同时又和 Set 一样内部元素不允许重复。ZSet 底层同样有两种数据结构：ZipList 和 [ Dict（字典） + SkipList（跳跃表）]，当以下满足以下两种情况时时以 ZipList 存储：</p>
<pre tabindex="0"><code>- 有序集合中所有的元素的大小都不超过64字节时
- 有序集合中元素的个数不超过128个的时候
</code></pre><p>同时也有 4 种高级数据类型</p>
<ul>
<li>HyperLogLog</li>
</ul>
<p>一种基数估算算法，主要用于统计一组数据中不重复的数据的个数。该算法存在一定误差，但占用空间小，可统计数量高达 $2^{64}$ 个</p>
<ul>
<li>Geo</li>
</ul>
<p>用来存储地理位置信息，实现原理是 <a href="https://en.wikipedia.org/wiki/Geohash">Geo Hash</a></p>
<ul>
<li>Bitmap</li>
</ul>
<p>位图，利用比特位来映射某个元素的状态，是 Redis 基于字符串实现的功能</p>
<ul>
<li>Stream</li>
</ul>
<p>可持久化的消息队列</p>
<h2 id="redis-的使用场景">Redis 的使用场景</h2>
<ul>
<li>
<p>缓存</p>
</li>
<li>
<p>分布式锁（<code>set nx ex</code> / lua 脚本）</p>
</li>
<li>
<p>计数器（利用 <code>incr</code> 命令实现）</p>
</li>
<li>
<p>附近的人（Geo）</p>
</li>
<li>
<p>消息队列（<code>rpush + blpop</code>/<code>lpush + rlpop</code>）</p>
</li>
<li>
<p>延迟队列（ZSet）</p>
</li>
</ul>
<h2 id="什么是缓存雪崩击穿和穿透">什么是缓存雪崩、击穿和穿透</h2>
<ul>
<li>雪崩</li>
</ul>
<p>雪崩是指在一瞬间大量的 Key 过期导致大量请求涌入数据库造成的系统崩溃，可以通过在设置过期时间时增加小随机数的方法来避免雪崩</p>
<ul>
<li>击穿</li>
</ul>
<p>击穿和雪崩类似，不过击穿指的是一个热点 Key 过期导致的崩溃，可以利用互斥锁让只有一个请求到数据库查询并回填缓存</p>
<ul>
<li>穿透</li>
</ul>
<p>穿透是指不断请求缓存中没有的 Key 导致的问题，可以通过对请求参数的校验来避免或者使用布隆过滤器</p>
<h2 id="redis-有哪些内存淘汰策略">Redis 有哪些内存淘汰策略</h2>
<p>因为机器的内存是有限的，所以 Redis 是不可以不停的往里写入的，当 Redis 使用量已经达到最大内存时会触发淘汰机制，会根据配置的不同策略删除不同的 Key</p>
<p><img src="https://minio.shaozb.xin/typora/1145d4fd-3438-42ea-8128-d6af8ac70d90" alt="redis官方文档"></p>
<table>
<thead>
<tr>
<th>淘汰策略</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>noeviction</td>
<td>不删除任何 Key，当继续写入时报错</td>
</tr>
<tr>
<td>allkeys-lru</td>
<td>在所有 Key 中删除最近最少使用的</td>
</tr>
<tr>
<td>allkeys-lfu</td>
<td>在所有 Key 中删除最少访问的</td>
</tr>
<tr>
<td>allkeys-random</td>
<td>在所有 Key 中随机删除</td>
</tr>
<tr>
<td>volatile-ttl</td>
<td>在设置了过期时间的 Key 中删除临近过期的</td>
</tr>
<tr>
<td>volatile-lru</td>
<td>在设置了过期时间的 Key 中删除最近最少使用的</td>
</tr>
<tr>
<td>volatile-lfu</td>
<td>在设置了过期时间的 Key 中删除最少访问的</td>
</tr>
<tr>
<td>volatile-random</td>
<td>在设置了过期时间的 Key 中随机删除</td>
</tr>
</tbody>
</table>
<h2 id="redis-的持久化方法">Redis 的持久化方法</h2>
<p>因为内存中的数据在机器重新启动的时候会全部丢失，所以 Redis 会周期性的进行内存数据的持久化。Redis 的持久化策略有两种：</p>
<ul>
<li>RDB</li>
</ul>
<p>RDB 是对当前内存中所有数据的一个快照，即保存了当前所有数据的状态。 RDB 的工作原理是 <code>fork</code> 一个子进程出来（此时会阻塞主线程），Redis 进程和子进程共享内存，快照操作在子进程中完成，并不会影响 Redis 线程继续处理命令，如果 Redis 内存占用比较高又频繁改动的时候，最多可能会导致 Redis 使用了双倍的内存。</p>
<ul>
<li>AOF</li>
</ul>
<p>与 RDB 全盘备份不同，AOF 是把每个命令在执行成功后写入缓存中，再根据配置的触发策略将缓存中的内容写入文件中</p>
<ul>
<li>混合持久化</li>
</ul>
<p>混合持久化是 Redis 4.0 以后新加入的方式，是将上面两种持久化方式结合起来，先利用 RDB 保存系统的快照，在保存快照的同时产生的新的命令以 AOF 的方式记录，最终将二者结合起来一并写入文件中</p>
<h2 id="redis-的持久化策略该如何选择">Redis 的持久化策略该如何选择</h2>
<ul>
<li>如果缓存中的数据丢失不重要的时候选择不持久化</li>
<li>如果允许丢失较长时间的数据，那么选择 RDB，对性能影响小</li>
<li>如果对数据丢失比较敏感，那么只能选择 AOF</li>
<li>如果在主从分离的环境下，关闭主节点的持久化（提高性能），开启子节点的 RDB</li>
</ul>
<h2 id="有哪些方式保证-redis-的高可用">有哪些方式保证 Redis 的高可用</h2>
<ul>
<li>主从模式</li>
</ul>
<p>避免单节点的 Redis 发生故障，将 Redis 中的数据全量复制到其他节点上。此时 Redis 有两种角色：主节点（Master）和从节点（Slave），在主节点上可以进行读写操作，而在从节点上只能进行读操作。主从模式下主要是为了分担主节点的<code>读</code>的压力，并提供容灾恢复的能力</p>
<ul>
<li>哨兵模式（Sentinel）</li>
</ul>
<p>在主从模式下如果主节点发生故障，此时的 Redis 是无法进行更新操作的，需要手动修改从节点为主节点才能恢复全部功能。针对这个问题，在 Redis 2.8 中引入了哨兵（Sentinel）机制。由 n 个哨兵（n $\geq$ 3, n 为奇数）监控主节点，当发现主节点挂掉后会自动的在可用的从节点中选举出一个新的主节点，保证 Redis 能够正常对外提供服务</p>
<ul>
<li>集群模式（Cluster）</li>
</ul>
<p>以上两种模式，每个 Redis 节点都保存着全量的数据，比较浪费空间。所以在 Redis 3.0 中引入了集群（Cluster）模式。若干个 Redis 节点组成一个集群，每个 Redis 节点都保存着部分数据；每个节点都互相连通，只需连接到任一节点，即可访问全部节点的数据。为了保证高可用，集群中的每个 Redis 都可以有多个子节点，也就是形成了小范围的主从复制，如果主节点挂了，可以选举任一子节点成为主节点，如果主节点连通其子节点一起挂掉，此时集群无法正常对外提供服务</p>
<h2 id="如何保证缓存和数据库的数据一致性">如何保证缓存和数据库的数据一致性</h2>
<p>只要在系统中同时使用了缓存和数据库，那么一定会面临数据的一致性问题。合理的设置缓存的过期时间只能保证最终一致性，但我们追求的是更快的一致性，此时就要在修改数据库的同时对缓存中的数据一并修改，针对缓存和数据库的先后修改顺序，共有 4 中方案：</p>
<ul>
<li>先修改缓存，再修改数据库</li>
<li>先修改数据库，再修改缓存</li>
<li>先删除缓存，再修改数据库</li>
<li>先修改数据库，再删除缓存</li>
</ul>
<h3 id="修改缓存还是删除缓存">修改缓存还是删除缓存</h3>
<p>先说结论，通常情况下都选择删除缓存而不是更新缓存。</p>
<p>因为在对一个写多读少的数据修改的时候，可能短时间内会修改很多次，但只会读取一次，此时就没有必要浪费时间在每次修改数据库后再更新到缓存中，只需等待着唯一的一次读数据时缓存回填即可。</p>
<p>接下来再讨论删除缓存的情况：</p>
<ul>
<li>先删除缓存，再修改数据库</li>
</ul>
<p>在并发量不高的情况下没有什么问题，就算是<strong>第一步删除缓存成功，第二步写数据库失败也不会产生脏数据</strong>，但如果系统并发量较高，先删除了缓存，此时数据库的更新还没有提交，此时又来了一个请求，从数据库中读取到的还是旧的数据，再将旧数据写入缓存就会造成数据不一致的问题</p>
<ul>
<li>先修改数据库，再删除缓存</li>
</ul>
<p>如果<strong>第一步写数据库成功，第二步删除缓存失败，这样会产生脏数据</strong>，其实如果不考虑操作失败的情况下，这种方案是最靠谱的方案，但在极小概率的情况下仍然会产生脏数据：</p>
<ol>
<li>缓存中的 Key 失效</li>
<li>请求 A 从数据库中读取值准备写入缓存</li>
<li>此时请求 B 修改数据库的值，并删除缓存</li>
<li>请求 A 将旧值写入缓存中</li>
</ol>
<p>不过实际情况下，出现的概率极小，因为要满足<strong>缓存失效的瞬间有两个并发的请求，一个读，一个写</strong>这样的条件才有可能出现。</p>
<p>针对上面的情况，可以采用<strong>延时双删</strong>的方案，即在执行完原流程后休眠若干时间（根据自己项目情况配置）再删除缓存，这样最多也就只有<em>若干时间</em>的数据不一致</p>]]></content>
        </item>
        
        <item>
            <title>CentOS 7 下 Redis 的安装与配置</title>
            <link>https://shawzb.com/posts/2021/11/centos-7-%E4%B8%8B-redis-%E7%9A%84%E5%AE%89%E8%A3%85%E4%B8%8E%E9%85%8D%E7%BD%AE/</link>
            <pubDate>Wed, 03 Nov 2021 09:43:37 +0800</pubDate>
            
            <guid>https://shawzb.com/posts/2021/11/centos-7-%E4%B8%8B-redis-%E7%9A%84%E5%AE%89%E8%A3%85%E4%B8%8E%E9%85%8D%E7%BD%AE/</guid>
            <description>本篇文章将介绍在 CentOS 7 系统中安装 Redis 的两种方式：
 利用包管理工具yum安装 从官网下载并安装  一、利用包管理工具安装 1. 修改yum源为阿里源（如已修改可跳过）  备份系统自带源  mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup  下载阿里源并替换  wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo 2. Redis 的安装和配置  配置 Fedora 软件仓库  yum install -y epel-release  安装 Redis  yum install -y redis  Redis 服务相关命令  # 启动 systemctl start redis # 查看状态 systemctl status redis # 配置开机自启 systemctl enable redis # 关闭 systemctl stop redis # 取消开机自启 systemctl disable redis  Redis 服务配置文件位置 /etc/systemd/system/redis.</description>
            <content type="html"><![CDATA[<p>本篇文章将介绍在 CentOS 7 系统中安装 Redis 的两种方式：</p>
<ul>
<li>利用包管理工具<code>yum</code>安装</li>
<li>从官网下载并安装</li>
</ul>
<h2 id="一利用包管理工具安装">一、利用包管理工具安装</h2>
<h3 id="1-修改yum源为阿里源如已修改可跳过">1. 修改yum源为阿里源（如已修改可跳过）</h3>
<ul>
<li>备份系统自带源</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
</code></pre></div><ul>
<li>下载阿里源并替换</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo
</code></pre></div><h3 id="2-redis-的安装和配置">2. Redis 的安装和配置</h3>
<ul>
<li>配置 Fedora 软件仓库</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">yum install -y epel-release
</code></pre></div><ul>
<li>安装 Redis</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">yum install -y redis
</code></pre></div><ul>
<li>Redis 服务相关命令</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 启动</span>
systemctl start redis
<span style="color:#75715e"># 查看状态</span>
systemctl status redis
<span style="color:#75715e"># 配置开机自启</span>
systemctl enable redis
<span style="color:#75715e"># 关闭</span>
systemctl stop redis
<span style="color:#75715e"># 取消开机自启</span>
systemctl disable redis
</code></pre></div><blockquote>
<p>Redis 服务配置文件位置 /etc/systemd/system/redis.service<br>
通过 yum 安装的 Redis 配置文件默认使用的是 /etc/redis.conf 这个配置文件</p>
</blockquote>
<h2 id="二从官网下载并安装">二、从官网下载并安装</h2>
<p>通过 yum 方式安装的 Redis 版本较低，如果对新功能有追求的话可以选择从官网手动安装</p>
<h3 id="1-下载--安装--编译">1. 下载 &amp;&amp; 安装 &amp;&amp; 编译</h3>
<p>确保系统中安装了 gcc 编译器，如果未安装的话变异过程中会出现错误</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">yum install -y gcc
</code></pre></div><p>如果已经安装了 gcc，那么直接开始下面的步骤</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 下载稳定版的 Redis</span>
wget http://download.redis.io/redis-stable.tar.gz
<span style="color:#75715e"># 解压</span>
tar xvzf redis-stable.tar.gz
cd redis-stable
<span style="color:#75715e"># 编译，这里不直接用 make 是将 Redis 命令直接配置到环境变量中方便后续使用</span>
sudo make install
</code></pre></div><p>接下来，耐心等待编译结束。当看到以下画面的时候，就说明已经编译成功了</p>
<p><img src="https://minio.shaozb.xin/outline/uploads/308b2d46-815a-4c50-b5d6-1970845ae54d/5ec16251-adf7-4e5f-9f53-1463774f2e87/1635843426081.jpg" alt="编译成功"></p>
<p>到这里， 我们就可以用<code>redis-server</code>命令启动 Redis 服务啦～</p>
<h3 id="2-配置-redis-服务">2. 配置 Redis 服务</h3>
<ul>
<li>先确定要使用的配置文件（可跳过）</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cp redis.conf /etc
</code></pre></div><ul>
<li>配置服务</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># 创建文件</span>
touch /etc/systemd/system/redis.service
<span style="color:#75715e"># 写入内容</span>
tee /etc/systemd/system/redis.service &lt;&lt;<span style="color:#e6db74">&#34;EOF&#34;</span>
<span style="color:#f92672">[</span>Unit<span style="color:#f92672">]</span>
Description<span style="color:#f92672">=</span>redis-server
After<span style="color:#f92672">=</span>network.target

<span style="color:#f92672">[</span>Service<span style="color:#f92672">]</span>
Type<span style="color:#f92672">=</span>forking
<span style="color:#75715e"># redis-server 后面跟的是指定的配置文件，使用绝对路径</span>
ExecStart<span style="color:#f92672">=</span>/usr/local/bin/redis-server /etc/redis.conf
PrivateTmp<span style="color:#f92672">=</span>true

<span style="color:#f92672">[</span>Install<span style="color:#f92672">]</span>
WantedBy<span style="color:#f92672">=</span>multi-user.target
EOF
</code></pre></div><p>接下来就可以利用上面提到的 <strong>Redis 服务相关命令</strong> 来操作 Redis 服务的启动/停止</p>
]]></content>
        </item>
        
        <item>
            <title>HashMap面试问题整理</title>
            <link>https://shawzb.com/posts/2021/11/hashmap%E9%9D%A2%E8%AF%95%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86/</link>
            <pubDate>Tue, 02 Nov 2021 22:44:37 +0800</pubDate>
            
            <guid>https://shawzb.com/posts/2021/11/hashmap%E9%9D%A2%E8%AF%95%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86/</guid>
            <description>本文记录下 Map 最常用的实现类 HashMap 相关的高频的面试知识点。
HashMap 的底层数据结构 数组 + 链表。结合了数组和链表二者的优点，利用数组可以在计算键的 hash 值以后快速的定位到所属位置，而当发生 hash 冲突（即两个键的 hash 值相等）时，又可以将元素以链表的形式存储。在 JDK 1.8 以后当链表的长度超过 8 且数组长度大于 64 以后会转变为红黑树。
HashMap 的工作原理  根据 HashMap 的底层结构可以知道，要想确定一个键值对（K/V）的位置要对 K 进行 hash 运算，再结合当前数组长度确定一个下标 如果计算出的下标没有元素（未发生 hash 冲突），那就依据当前的键值对构造一个 Node 放入数组中 如果发生 hash 冲突时，先判断二者的 K 是否相等，如果相等说明是同一个元素，此时更新键值对的值即可 如果 K 不相等，那就将当前元素插入到链表当中（JDK 1.7 是头插，1.8 是尾插） 插入元素后判断当前数组的长度是否大于 capacity * loadfactor，如果大于，会对整个数组进行扩容，新的数组长度为 2 * capacity，之后对 HashMap 中的所有 K 重新进行 hash，确定在新数组中的位置  Hash 算法的实现 static final int hash(Object key) { int h; return (key == null) ?</description>
            <content type="html"><![CDATA[<p>本文记录下 Map 最常用的实现类 <code>HashMap</code> 相关的高频的面试知识点。</p>
<h2 id="hashmap-的底层数据结构">HashMap 的底层数据结构</h2>
<p>数组 + 链表。结合了数组和链表二者的优点，利用数组可以在计算<code>键</code>的 hash 值以后快速的定位到所属位置，而当发生 hash 冲突（即两个<code>键</code>的 hash 值相等）时，又可以将元素以链表的形式存储。在 JDK 1.8 以后当链表的长度超过 8 且数组长度大于 64 以后会转变为红黑树。</p>
<h2 id="hashmap-的工作原理">HashMap 的工作原理</h2>
<ul>
<li>根据 HashMap 的底层结构可以知道，要想确定一个<code>键值对（K/V）</code>的位置要对 K 进行 hash 运算，再结合当前数组长度确定一个下标</li>
<li>如果计算出的下标没有元素（未发生 hash 冲突），那就依据当前的键值对构造一个 <code>Node</code> 放入数组中</li>
<li>如果发生 hash 冲突时，先判断二者的 K 是否相等，如果相等说明是同一个元素，此时更新键值对的值即可</li>
<li>如果 K 不相等，那就将当前元素插入到链表当中（JDK 1.7 是头插，1.8 是尾插）</li>
<li>插入元素后判断当前数组的长度是否大于 capacity * loadfactor，如果大于，会对整个数组进行扩容，新的数组长度为 2 * capacity，之后对 HashMap 中的所有 K 重新进行 hash，确定在新数组中的位置</li>
</ul>
<h2 id="hash-算法的实现">Hash 算法的实现</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java"><span style="color:#66d9ef">static</span> <span style="color:#66d9ef">final</span> <span style="color:#66d9ef">int</span> <span style="color:#a6e22e">hash</span><span style="color:#f92672">(</span>Object key<span style="color:#f92672">)</span> <span style="color:#f92672">{</span>
    <span style="color:#66d9ef">int</span> h<span style="color:#f92672">;</span>
    <span style="color:#66d9ef">return</span> <span style="color:#f92672">(</span>key <span style="color:#f92672">==</span> <span style="color:#66d9ef">null</span><span style="color:#f92672">)</span> <span style="color:#f92672">?</span> 0 <span style="color:#f92672">:</span> <span style="color:#f92672">(</span>h <span style="color:#f92672">=</span> key<span style="color:#f92672">.</span><span style="color:#a6e22e">hashCode</span><span style="color:#f92672">())</span> <span style="color:#f92672">^</span> <span style="color:#f92672">(</span>h <span style="color:#f92672">&gt;&gt;&gt;</span> 16<span style="color:#f92672">);</span>
<span style="color:#f92672">}</span>
</code></pre></div><p>计算 K 的 hashCode 的高16位异或低16位的值，让高16位的值参与了 hash 运算，而且减少了 hash 冲突的概率</p>
<h2 id="如何确定-table-的容量loadfactor-又是什么">如何确定 table 的容量？loadFactor 又是什么？</h2>
<ul>
<li>table 数组的长度由 capacity 决定，默认值是16，最大值是 1 &laquo; 30</li>
<li>loadFactor 是负载因子，用于规定 table 数组是否需要扩容。默认的负载因子是 0.75</li>
</ul>
<p><img src="https://minio.shaozb.xin/outline/uploads/308b2d46-815a-4c50-b5d6-1970845ae54d/0e2a64de-aabb-40ae-be0a-b0268c1aa2df/image.png" alt="源码"></p>
<h2 id="为什么-hashmap-的底层数组长度为何总是-2n">为什么 HashMap 的底层数组长度为何总是 $2^n$</h2>
<ul>
<li>如果不考虑这个问题的话，根据 hash 值计算下标通常是 hash % length，此时数组长度为多少都可以。但是数组的长度决定了 HashMap 的存取效率，于是开发人员就发现了当 length 为 $2^n$时 hash &amp; （length - 1）= hash %  length，因为位运算效率要比求模运算效率高，所以数组长度就一直保持在 $2^n$</li>
<li>当使用了上述的计算公式时，如果数组长度不为 $2^n$ 时，length - 1 转换成2进制时最后一位为 0，在进行 &amp; 运算时会造成空间浪费；而当长度为2的n次方时，计算的结果可以均匀的分配到数组的每一个位置</li>
</ul>
<h2 id="jdk17-和-jdk18-中的-hashmap-有什么不同">JDK1.7 和 JDK1.8 中的 HashMap 有什么不同</h2>
<ul>
<li>底层数据结构不同，1.7 中是数组 + 链表，而在 1.8 中当链表的长度大于 8 且数组长度大于 64 时会转变为红黑树（小于 6 时会重新变回链表）</li>
<li>在发生 hash 冲突时，1.7 中是插入到链表的头部，而 1.8 则是插入链表尾部</li>
</ul>
<h2 id="为什么-hashmap-是线程不安全的">为什么 HashMap 是线程不安全的</h2>
<ul>
<li>put/get 操作并不是原子操作，多线程中可能会覆盖其他线程写入的值</li>
<li>在 1.7 中头插法会修改链表元素的位置，导致产生环。当获取有环的元素时会造成死循环，在 1.8 中修改为尾插来避免这个问题</li>
</ul>
]]></content>
        </item>
        
    </channel>
</rss>
