<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
    <channel>
            <title>Suremotoo's Blog</title>
            <link>https://notes.suremotoo.cc</link>
        <generator>Halo 1.6.0</generator>
        <lastBuildDate>Sat, 03 Jan 2026 23:04:14 CST</lastBuildDate>
                <item>
                    <title>
                        <![CDATA[线程安全-尝试找出 Counter 例子中重复计算的数字]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/thread-safety-finderrornumscounter</link>
                    <description>
                            <![CDATA[<h1 id="%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8-%E6%89%BE%E5%87%BA%E4%B8%8D%E5%AE%89%E5%85%A8%E7%9A%84%E6%95%B0%E6%8D%AE" tabindex="-1">线程安全-找出不安全的数据</h1><h2 id="%E4%BB%80%E4%B9%88%E6%89%8D%E6%98%AF%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%EF%BC%9F" tabindex="-1">什么才是线程安全？</h2><p><em>《Java Concurrency in Practice》</em> 有一个比较恰当的定义 ：“当多个线程访问一个对象时，如果不用考虑这些线程在运行时环境下的调度和交替执行，也不需要进行额外的同步或者在调用方法进行任何其他的协调操作，调用这个对象的行为都可以获得正确的结果，那这个对象是线程安全的。”</p><h2 id="%E7%BB%8F%E5%85%B8-counter-%E8%AE%A1%E6%95%B0%E4%BE%8B%E5%AD%90%F0%9F%8C%B0" tabindex="-1">经典 Counter 计数例子🌰</h2><pre><code class="language-java">/** * MultiErrorDemo 多线程环境下的常见的计数错误 * * @author suremotoo * @date 2022/11/07 12:24 */public class MultiErrorDemoCounter implements Runnable {    static int index = 0;    static MultiErrorDemoCounter errorDemo = new MultiErrorDemoCounter();    @Override    public void run() {        for (int i = 0; i &lt; 10000; i++) {            index++;        }    }    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(errorDemo);        Thread t2 = new Thread(errorDemo);        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(index);    }}</code></pre><p>没错，上述代码，我们启动了 <strong>2 个线程：t1、t2 ，分别对 index 进行 10 万次的计数，也就是分别执行 index++</strong>。</p><p>结果，index 最终我们 <strong>预期值为：200000</strong>，为实际运行结果，往往都是 <strong>小于 200000</strong> ；</p><p>这就是典型的线程不安全问题操作，因为 <em>两个线程执行同一个 index 对象的时候，总有可能是针对同一个数值计算，这样重复计算才导致真是数字往往小于预期！</em></p><h5 id="%E5%B0%8F%E8%A7%A3%E5%86%B3" tabindex="-1">小解决</h5><blockquote><p>当然，我们给 <strong>index++</strong> 使用一个 <strong>synchronized</strong> 同步锁即可解决，run 方法调整后代码如下：</p><pre><code class="language-java">public void run() {    for (int i = 0; i &lt; 10000; i++) {      // 需要获得 errorDemo 对象的锁才能进行 index++, 从而保证准确计算        synchronized(errorDemo) {            index++;        }    }}</code></pre><p>当然，更多具体过程可以参考文档 <strong>synchronized</strong> 分析文章（我还没出呢😁）</p></blockquote><h2 id="%E5%B0%9D%E8%AF%95%E6%89%BE%E5%87%BA-counter-%E4%BE%8B%E5%AD%90%E4%B8%AD%E9%87%8D%E5%A4%8D%E8%AE%A1%E7%AE%97%E7%9A%84%E6%95%B0%E5%AD%97%EF%BC%81" tabindex="-1">尝试找出 Counter 例子中重复计算的数字！</h2><p>鉴于上面的问题，大部分想法都是去解决这个问题，既然针对某些值进行了重复计算，那么能不能尝试着能不能找出到底是哪些值呢？本着研究学习的心态，去试一试！💪💪</p><h3 id="%E7%AC%AC%E4%B8%80%E6%AD%A5%EF%BC%9A%E8%AE%B0%E5%BD%95" tabindex="-1">第一步：记录</h3><p>既然是重复计算，那么我们就每次计算的都记录一下，判断一下是不是已经计算过了，计算过了就打印出来。</p><pre><code class="language-java">public class FindErrorNumsCounter implements Runnable {    static FindErrorNumsCounter instance = new FindErrorNumsCounter();    int index = 0;    /**     * 真正运行的次数，该值正好和 index 理论上计算的值是一致     */    static AtomicInteger realCount = new AtomicInteger();    /**     * 错误的次数     */    static AtomicInteger errorCount = new AtomicInteger();    /**     * 记录标记计算的数字，容量比理论计算的数值大一些，以便都能装进去     */    static boolean[] marked = new boolean[1000000];    @Override    public void run() {        for (int i = 0; i &lt; 100000; i++) {            index++;            realCount.incrementAndGet();            // 判断 index 是否已经计算过            if (marked[index]) {                System.out.println(&quot;出错了: &quot; + index);                errorCount.incrementAndGet();            }            marked[index] = true;        }    }    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(instance);        Thread t2 = new Thread(instance);        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(&quot;真正计算的次数: &quot; + realCount.get());        System.out.println(&quot;错误计算的次数: &quot; + errorCount.get());        System.out.println(&quot;实际结果: &quot; + instance.index);        System.out.println(&quot;----------------&quot;);    }}</code></pre><p>为了方便，我们先定义了 <strong>3</strong> 个变量：</p><p><em><strong>boolean[] marked</strong></em>：用于记录标记已经计算过的数值；<br /><em><strong>AtomicInteger realCount</strong></em>：用于记录理论预期的正确值；<br /><em><strong>AtomicInteger errorCount</strong></em>： 记录重复计算的次数；</p><p>重点就分析一下 <code>run()</code> 方法</p><pre><code class="language-java">@Overridepublic void run() {    for (int i = 0; i &lt; 100000; i++) {        index++;        // 计算次数加 1，统计真正计算的次数，也就是理论上 index 的值        realCount.incrementAndGet();        // 判断 index 是否已经计算过，如果已经计算过，说明重复计算，则打印出来该值        if (marked[index]) {            System.out.println(&quot;出错了: &quot; + index);            // 同时重复计算的次数加 1             errorCount.incrementAndGet();        }        // 没有重复计算则添加到数组中，标记已经计算过        marked[index] = true;    }}</code></pre><p>运行一下：</p><pre><code class="language-bash">真正计算的次数: 200000错误计算的次数: 255实际结果: 199604</code></pre><p>会发现数字很离谱，理论上 <strong>实际结果+错误计算的次数=真正计算的次数</strong> 才对！</p><p>这里我们犯了一个错误，就是我们统计重复计算的逻辑，也是线程不安全的！就是这里：</p><pre><code class="language-java">// 判断 index 是否已经计算过，如果已经计算过，说明重复计算，则打印出来该值if (marked[index]) {    System.out.println(&quot;出错了: &quot; + index);    // 同时重复计算的次数加 1     errorCount.incrementAndGet();}// 没有重复计算则添加到数组中，标记已经计算过marked[index] = true;</code></pre><hr /><h4 id="%E9%94%99%E8%AF%AF%E5%88%86%E6%9E%90" tabindex="-1">错误分析</h4><blockquote><p>错误分析</p><p><strong>前提条件：</strong> 假如两个线程发生了重复计算，<code>index 从 0 开始</code>，都执行完 <code>index++</code> 后 <strong>index 的值都为 1；</strong></p><ol><li><p>第 1 个线程判断 <code>if (marked[index]) </code> 不符合，那么要标记该 index 已经计算过 ,也就是要执行  <code>marked[index] = true;</code></p></li><li><p>结果第 1 线程还没执行  <code>marked[index] = true;</code>，偏偏 CPU 调度切换执行第 2 个线程；</p></li><li><p>第 2 个线程判断 <code>if (marked[index]) </code> 也不符合，然后第 2 个线程就执行了   <code>marked[index] = true;</code></p></li><li><p>第 2 个线程执行完成后，CPU 调度又切回第 1 个线程去执行 <code>marked[index] = true;</code></p></li></ol><p>那么最终两个线程对同一个 index 进行了标记！就跟 index++ 重复计算一样，这样就是两个线程冲突，却没有统计到出错的数字。</p></blockquote><h5 id="%E7%A4%BA%E4%BE%8B%E5%9B%BE" tabindex="-1">示例图</h5><blockquote><p>可以配合该动图，理解上面的话</p><p><img src="https://notes.suremotoo.cc/upload/2022/11/find-counter-error-marked-unsafe-1d8b939986d94be99b048f1f3f76c81e.gif" alt="find-counter-error-marked-unsafe" /></p></blockquote><p>没解决问题反而还新造出了新问题🤦🤦</p><h3 id="%E7%AC%AC%E4%BA%8C%E6%AD%A5%EF%BC%9A%E8%AE%B0%E5%BD%95%E8%B0%83%E6%95%B4-%E5%A2%9E%E5%8A%A0-synchronized" tabindex="-1">第二步：记录调整-增加 synchronized</h3><p>上面记录重复次数的代码不安全，那么我们用 <strong>synchronized</strong> 来同步这段代码试试呀！😏😏</p><pre><code class="language-java">@Overridepublic void run() {    for (int i = 0; i &lt; 100000; i++) {        index++;        realCount.incrementAndGet();        // 使用 synchronized 保护        synchronized (instance) {            if (marked[index]) {                System.out.println(&quot;出错了: &quot; + index);                errorCount.incrementAndGet();            }            marked[index] = true;        }    }}</code></pre><p>这样我们再看看～</p><pre><code class="language-bash">... ... 出错了: 183544出错了: 190630真正计算的次数: 200000错误计算的次数: 1781实际结果: 199999</code></pre><p>多运行几次，<strong>实际结果 199999</strong> 都已经逼近 <strong>真正计算的次数 200000</strong> 了，可是这个 <strong>错误计算的次数</strong> 竟然高的离谱！应该是 <strong>1</strong> 的呀！😦😦</p><hr /><h4 id="%E9%94%99%E8%AF%AF%E5%88%86%E6%9E%90-1" tabindex="-1">错误分析</h4><blockquote><p>错误分析<br /><strong>前提条件：</strong> 假如两个线程没有发生冲突，正常计算，<code>index 从 0 开始</code>，第 1 个线程执行完 <code>index++</code> 后 <strong>index 的值为 1</strong>；</p><p><strong>提示 2 ：</strong> 这个又要提到一个概念，就是 <strong>synchronized</strong> 拥有一个特性：<strong><u>线程可见</u></strong>。</p><ol><li><p>第 1 个线程正常执行 <strong>index++</strong> ,index 值变为 1，紧接着进入 <strong>synchronized</strong> 中，第 2 个线程是无法进入 <strong>synchronized</strong>  代码块的。</p></li><li><p>第 1 个线程此时即将要执行  <code>if (marked[index]) </code>  代码，却又没执行的时候， CPU 调度又回去让第 2 个线程继续执行；</p></li><li><p>这时候第 2 个线程又执行 <strong>index++</strong>，<strong>index</strong> 变为 2，执行完后 CPU 调度又回去让第 1 个线程执行，这时候第 1 个线程要执行：  <code>if (marked[index]) </code>  代码</p></li></ol><p><img src="https://notes.suremotoo.cc/upload/2022/11/counter-error-img-1-4830dabea2f3495096a4d8dcc47c96df.png" alt="counter-error-img-1" /></p><ol start="5"><li><p>由于 <strong>synchronized</strong> <strong>的线程可见性， 1 个线程可以看到之前的线程干了什么事情</strong>，这样第 1 个线程本来要 <code>if (marked[index])</code> 判断的是 <strong>marked[1]</strong>，由于第 2 个线程的结果导致变成了 <strong>marked[2]</strong></p></li><li><p>如此的话，第 1 个线程将 <strong>index 2</strong>  就被标记为 true，然后退出 <strong>synchronzied</strong> 代码块。</p></li><li><p>轮到第 2 个线程继续执行的时候，第 2 个线程执行 <code>if (marked[index])</code>  判断的也是 <strong>marked[2]</strong>，因为第 1 个线程已经标记过了，所以会满足 <code>if (marked[index])</code> 的条件，  从而打印出 <code>出错了</code>。</p></li></ol><p>可实际上两个线程并没有冲突，1 个线程将 0+1=1，另 1 个线程将 1+1=2。</p><p>这样下来，本来正确的计算，却打印出了 1 次 <code>出错了</code>，就会导致上述 <strong>错误计算的次数</strong> <em>统计过多</em>的问题了。</p></blockquote><h5 id="%E7%A4%BA%E4%BE%8B%E5%9B%BE-1" tabindex="-1">示例图</h5><blockquote><p>可以配合该动图，理解上面的话</p><p><img src="https://notes.suremotoo.cc/upload/2022/11/find-counter-error-marked-synchronized-7e63c981ad5244a1813737389531f342.gif" alt="find-counter-error-marked-synchronized" /></p></blockquote><h3 id="%E7%AC%AC%E4%B8%89%E6%AD%A5%EF%BC%9A%E8%AE%B0%E5%BD%95%E8%B0%83%E6%95%B4-%E4%BF%9D%E6%8C%81%E6%AF%8F%E4%B8%A4%E4%B8%AA%E7%BA%BF%E7%A8%8B%E4%B8%80%E7%BB%84" tabindex="-1">第三步：记录调整-保持每两个线程一组</h3><p>上面分析增加 <strong>synchronized</strong> 还不行，因为 CPU 调度，可能会让前面的线程一、线程二中的某一个线程步骤加快，进入下一次 <strong>index++</strong> 的计算，那么我们就控制一下，每次 <strong>index++</strong> 前确保是 2 个线程一起来！</p><p>这时候我们引入一个新的工具类：<strong>CyclicBarrier</strong>，先不用理解它到底是什么，只需要知道它到底有什么作用即可。</p><p><strong>CyclicBarrier</strong> 其实就个栅栏，假如你有个牧场，养了一窝的阿拉斯加，它们也总是兴致勃勃，为了不让他们乱跑，你为了个栅栏把他们圈起来~，哪天你想放出来遛遛它们，打开栅栏，那叫一个：<strong>斯如涌泉</strong> 🤪，一下子全冲出来了～</p><p><strong>CyclicBarrier</strong> 其实就跟这个差不多，我们可以设置一个条件，比如有 2 个线程都等待就绪后，然后才允许放行！我们看代码：</p><pre><code class="language-java">/** * FindErrorNumsCounter * * @author suremotoo * @date 2022/11/07 19:57 */public class FindErrorNumsCounter implements Runnable {    static FindErrorNumsCounter instance = new FindErrorNumsCounter();    int index = 0;    /**     * 真正运行的次数，该值正好和 index 理论上计算的值是一致     */    static AtomicInteger realCount = new AtomicInteger();    /**     * 错误的次数     */    static AtomicInteger errorCount = new AtomicInteger();    /**     * 记录标记计算的数字，容量比理论计算的数值大一些     */    static boolean[] marked = new boolean[1000000];    static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);    @Override    public void run() {        for (int i = 0; i &lt; 100000; i++) {            try {                cyclicBarrier1.await();            } catch (InterruptedException e) {                throw new RuntimeException(e);            } catch (BrokenBarrierException e) {                throw new RuntimeException(e);            }            index++;            realCount.incrementAndGet();            synchronized (instance) {                if (marked[index]) {                    System.out.println(&quot;出错了: &quot; + index);                    errorCount.incrementAndGet();                }                marked[index] = true;            }        }    }    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(instance);        Thread t2 = new Thread(instance);        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(&quot;真正计算的次数: &quot; + realCount.get());        System.out.println(&quot;错误计算的次数: &quot; + errorCount.get());        System.out.println(&quot;实际结果: &quot; + instance.index);        System.out.println(&quot;----------------&quot;);    }}</code></pre><p>重点看这里：</p><p><img src="https://notes.suremotoo.cc/upload/2022/11/counter-error-img-cyclicBarrier1-b6b26b759f4744908fc6837d23b68aab.png" alt="counter-error-img-cyclicBarrier1" /></p><p>我们定义了一个变量 <strong>cyclicBarrier1</strong> 并且设置了栅栏开启的线程数量是 2 个，这里 <code>cyclicBarrier1.await();</code> 就是 2 个线程就绪了才会执行下一行！</p><p>这下我们再运行，看看结果：</p><pre><code class="language-bash">真正计算的次数: 200000错误计算的次数: 4实际结果: 200000</code></pre><p>🎵眼睛瞪得像铜铃🔔 ~~~~</p><p>我… …  怎么还是有问题❓❓❓❓❓</p><h4 id="%E9%94%99%E8%AF%AF%E5%88%86%E6%9E%90-2" tabindex="-1">错误分析</h4><blockquote><p>错误分析经过  <code>cyclicBarrier1.await();</code> 代码后，我们有 2 个线程过来。</p><p><strong>前提条件（很重要）：</strong> 我们 <strong>假设 index 为 0，</strong> 第 2 个线程进来就一直没有执行，就卡在 <strong>index++;</strong> ，<em>注意</em>：是没执行 <strong>index++;</strong></p><ol><li><p>而第 1 个线程执行 <strong>index++</strong>, <strong>i 变成 1</strong>，并且进入 <strong>synchronized</strong> 代码块，<code>if (marked[index])</code> <strong>marks[1]</strong> 也不满足条件（不记录错误），当即将执行 <code>marked[index]=true</code> 的时候却被 CPU 调度走了，让第 2 个线程继续执行了～</p></li><li><p>第 2 个线程执行 <strong>index++;</strong>  <strong>index 是从 1 开始计算的</strong>，因为第 1 个线程已经 index++ 了！</p></li><li><p>这时候 第 2 个线程 index++  完 <strong>index 就变成了 2</strong> ，刚执行完又被 CPU 调度回去，继续让第 1 个线程执行，而这时候第 1 个线程继续执行：<code>marked[index]=true</code>，可此时 index 已经变成 2 了，本来是要执行 <strong>marked[1]=true</strong>，结果变成了 <strong>marked[2]=true</strong>！</p></li><li><p>最后第 1 个线程执行完后<em>退出</em> <strong>synchronized</strong>，第 2 个线程回来继续执行 <em>进入</em> <strong>synchronized</strong>。</p></li><li><p>这样第 2 个线程本来要 <code>if (marked[index])</code> 判断的是 <strong>marked[1]</strong>，结果也变成了 <strong>marked[2]</strong>！</p></li></ol><p>如此的话，第 2 个线程 <code>if (marked[index])</code>  判断的也是 <strong>marked[2]</strong>，这样 2 就已经重复了，就会打印出来 <strong>“出错了：”</strong> ，可实际并没有重复呢！和之前分析导致 <strong>错误计算的次数</strong> 统计过多的问题一样。</p></blockquote><p>其实你会发现，这和 <strong>第二步</strong> 的场景是完全一样的嘛！😁</p><h3 id="%E7%AC%AC%E5%9B%9B%E6%AD%A5%EF%BC%9A%E8%AE%B0%E5%BD%95%E8%B0%83%E6%95%B4-%E4%BF%9D%E6%8C%81%E6%AF%8F%E4%B8%A4%E4%B8%AA%E7%BA%BF%E7%A8%8B%E4%B8%80%E7%BB%84-%E5%8D%87%E7%BA%A7%EF%BC%81" tabindex="-1">第四步：记录调整-保持每两个线程一组-升级！</h3><p>第三步，我们添加了 1 个 <strong>CyclicBarrier</strong> 栅栏来解决 第二步两轮计算 <strong>index++</strong> 的问题，发现还是不行，依然存在一个在 <strong>synchronized</strong> 代码块里，因为线程切换执行 <strong>index++</strong> 导致 index 值变的问题！</p><p>既然有可能线程半路才 index++，这样，那么我再加 1 个 <strong>CyclicBarrier</strong> 栅栏，放在 <strong>index++</strong> 后面 <strong>synchronized</strong> 前面，这样就避免了其中 1 个线程在 <strong>synchronized</strong> 里执行代码的时候突然让别的线程 <strong>index++</strong>。</p><p>没问题，上代码：</p><pre><code class="language-java">/** * FindErrorNumsCounter * * @author suremotoo * @date 2022/11/07 19:57 */public class FindErrorNumsCounter implements Runnable {    static FindErrorNumsCounter instance = new FindErrorNumsCounter();    int index = 0;    /**     * 真正运行的次数，该值正好和 index 理论上计算的值是一致     */    static AtomicInteger realCount = new AtomicInteger();    /**     * 错误的次数     */    static AtomicInteger errorCount = new AtomicInteger();    /**     * 记录标记计算的数字，容量比理论计算的数值大一些     */    static boolean[] marked = new boolean[1000000];    static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);    static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);    @Override    public void run() {        for (int i = 0; i &lt; 100000; i++) {            try {                cyclicBarrier2.reset();                cyclicBarrier1.await();            } catch (InterruptedException e) {                throw new RuntimeException(e);            } catch (BrokenBarrierException e) {                throw new RuntimeException(e);            }            index++;            try {                cyclicBarrier1.reset();                cyclicBarrier2.await();            } catch (InterruptedException e) {                throw new RuntimeException(e);            } catch (BrokenBarrierException e) {                throw new RuntimeException(e);            }            realCount.incrementAndGet();            synchronized (instance) {                if (marked[index]) {                    System.out.println(&quot;出错了: &quot; + index);                    errorCount.incrementAndGet();                }                marked[index] = true;            }        }    }    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(instance);        Thread t2 = new Thread(instance);        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(&quot;真正计算的次数: &quot; + realCount.get());        System.out.println(&quot;错误计算的次数: &quot; + errorCount.get());        System.out.println(&quot;实际结果: &quot; + instance.index);        System.out.println(&quot;----------------&quot;);    }}</code></pre><p>我们在 <strong>index++</strong> 前后都添加了 <strong>CyclicBarrier</strong> 栅栏！这样可以确保让两个线程都会执行 <strong>index++</strong> 。</p><p>ok，我们运行一下代码。</p><pre><code class="language-bash">出错了: 199920出错了: 199922出错了: 199924出错了: 199926出错了: 199928出错了: 199930出错了: 199932出错了: 199934出错了: 199936出错了: 199938出错了: 199940出错了: 199942出错了: 199944出错了: 199946出错了: 199948出错了: 199950出错了: 199952出错了: 199954出错了: 199956出错了: 199958出错了: 199960出错了: 199962出错了: 199964出错了: 199966出错了: 199968出错了: 199970出错了: 199972出错了: 199974出错了: 199976出错了: 199978出错了: 199980出错了: 199982出错了: 199984出错了: 199986出错了: 199988出错了: 199990出错了: 199992出错了: 199994出错了: 199996出错了: 199998真正计算的次数: 200000错误计算的次数: 100000实际结果: 199998</code></pre><p>俏丽马！怎么还越来越离谱了❓❓❓❓❓❓而且出错的还都是偶数。</p><p><img src="https://notes.suremotoo.cc/upload/2022/11/counter-error-img-cyclicBarrier2-e44d5c712b4540b6bdaf26b333cb754e.png" alt="counter-error-img-cyclicBarrier2" /></p><h5 id="%E6%B3%A8%E6%84%8F%EF%BC%9A" tabindex="-1">注意：</h5><blockquote><p><strong>注意：</strong> 由于在 index++ 前后都加入了栅栏，所以 2 个线程都会执行 index++ 的。</p></blockquote><h4 id="%E9%94%99%E8%AF%AF%E5%88%86%E6%9E%90-3" tabindex="-1">错误分析</h4><blockquote><p><strong>前提条件：index 为 0</strong>：</p><ol><li><p>假如现在 2 个线程是正常按照逻辑执行， 第 1 个线程 <code>index++</code> index 变为 <strong>1</strong>；</p></li><li><p>因为第 1 个线程的结果，第 2 个线程 <code>index++</code> index 就变为 <strong>2</strong>；</p></li><li><p>然后放开栅栏！两个都去执行 <strong>synchronized</strong> 代码，要进行锁竞争，由于 <strong>synchronized</strong> <strong>的线程可见特性， 1 个线程可以看到之前的线程干了什么事情</strong>， <strong>所以无论哪个线程抢到锁去执行，它们用的 index 值都是 2</strong>。</p></li><li><p>第 1 个线程 <code>if (marked[index])</code> 判断 <strong>marked[2]</strong>  不满足条件，则执行 <strong>marked[2]=true;</strong></p></li><li><p>第 2 个线程 <code>if (marked[index])</code> 判断 <strong>marked[2]</strong>  满足条件，则打印 <code>出错了:</code></p></li></ol><p>这样的情况对吗？肯定不对啊，我们的目标、宗旨是要找出重复计算的，现在的 index 有重复计算吗？并没有！ 1 个线程将 index 从 0 变为 1，另 1 个线程从 1 变为 2，没问题呀，但我们现在这种代码就会多打印出来 <code>出错了:</code>。</p></blockquote><p>🤨🤨你会发现，这还是和之前 <strong>第三步、第四步</strong> 类似呀，都是正常的逻辑情况下，因为其中 1 个线程将 index 变更后，导致另 1 个线程使用变更后的 index ，从而导致重复打印的问题！</p><h3 id="%E7%AC%AC%E4%BA%94%E6%AD%A5%EF%BC%9A%E8%AE%B0%E5%BD%95%E8%B0%83%E6%95%B4-%E8%B0%83%E6%95%B4%E9%87%8D%E5%A4%8D%E8%AE%A1%E7%AE%97%E7%9A%84%E5%88%A4%E6%96%AD%E5%A4%84%E7%90%86%EF%BC%81" tabindex="-1">第五步：记录调整-调整重复计算的判断处理！</h3><p>基于上面 第四步，我们分析了正常情况多统计了，现在想办法要去掉，那么，同时我们也要会想一下错误的情况，应该是什么样的。</p><h5 id="%E6%8F%90%E7%A4%BA" tabindex="-1">提示</h5><blockquote><p><strong>提示：</strong></p><p>因为 index 从 0 开始，index++ 对于 0 是不会漏算的，我们就把设置 <code>marked[0]=true;</code></p></blockquote><p><strong>正常的情况，0→1→2：</strong></p><table><thead><tr><th>线程</th><th>index 值</th><th>marked 标记结果</th><th>备注</th></tr></thead><tbody><tr><td><em>线程 1</em></td><td>1</td><td>false</td><td>并没有执行 <code>marked[1] = true;</code> 标记，<br>因为 <em>线程 2</em> 的将 index 变更为 2 了，所以 <em>线程 1</em> 实际执行的是 <code>marked[2] = true;</code> <br>所以 index 为 1 在 marked 里默认的值为 false</td></tr><tr><td><em>线程2</em></td><td>2</td><td>true</td><td>实际执行的是 <code>marked[2] = true;</code></td></tr></tbody></table><p>或者</p><table><thead><tr><th>线程</th><th>index 值</th><th>marked 标记结果</th><th>备注</th></tr></thead><tbody><tr><td><em>线程 2</em></td><td>1</td><td>false</td><td>并没有执行 <code>marked[1] = true;</code> 标记，<br>因为 <em>线程 1</em> 的将 index 变更为 2 了，所以 <em>线程 2</em> 实际执行的是 <code>marked[2] = true;</code> <br>所以 index 为 1 在 marked 里默认的值为 false</td></tr><tr><td><em>线程1</em></td><td>2</td><td>true</td><td>实际执行的是 <code>marked[2] = true;</code></td></tr></tbody></table><p><strong>⚠️错误的情况，0→1→1：</strong></p><table><thead><tr><th>线程</th><th>index 值</th><th>marked 标记结果</th><th>备注</th></tr></thead><tbody><tr><td>第 1 个线程</td><td>1</td><td>true</td><td>实际执行的是 <code>marked[1] = true;</code></td></tr><tr><td>第 2 个线程</td><td>1</td><td>true</td><td>实际执行的是 <code>marked[1] = true;</code></td></tr></tbody></table><p>从上面的表格，我们可以看到，因为 2 个线程去执行 <strong>index++</strong>,  所以 <code>正常的情况</code>，总会像 <strong>0→1→2</strong> 这样的规律，而且 0→<strong>1</strong>→2  中间的那个 1 是会略过 <strong>sychronized</strong> 的代码处理的，<strong>2</strong> 是正确的，但我们却多打印出来来了，不应该打印它，所以 <strong>marked[0] 总是 true，marked[1] 是 false，marked[2] 是 true</strong>；<strong>以此类推，后面都是 true、false 交替的结果。</strong></p><p><code>错误的情况</code>总会像 <strong>0→1→1</strong> 这样的规律，所以 <strong>marked[0] 总是 true，marked[1] 也是 true</strong>，这样呢，我就可以得出一个规律，只要出现了  <strong>marked[index] 和 marked[index-1] 都为 true 的情况，index 才是真正的重复计算，这种情况下才是需要将信息打印出来！</strong></p><p>这样我们就知道调整哪里的代码了，就是 <strong>synchronized</strong> 代码块中 <code>if (marked[index]) </code> 这句代码，我们调整为 <code>if (marked[index] &amp;&amp; marked[index - 1])</code></p><p><strong>run()</strong> 完整代码如下：</p><pre><code class="language-java">@Overridepublic void run() {   // 0 的时候永远不会重复计算，手动标记为 true    marked[0]=true;    for (int i = 0; i &lt; 100000; i++) {        try {            cyclicBarrier2.reset();            cyclicBarrier1.await();        } catch (InterruptedException e) {            throw new RuntimeException(e);        } catch (BrokenBarrierException e) {            throw new RuntimeException(e);        }        index++;        try {            cyclicBarrier1.reset();            cyclicBarrier2.await();        } catch (InterruptedException e) {            throw new RuntimeException(e);        } catch (BrokenBarrierException e) {            throw new RuntimeException(e);        }        realCount.incrementAndGet();        synchronized (instance) {          // 判断条件，如果上一个和当前这个都为 true，则说明 index 漏算            if (marked[index] &amp;&amp; marked[index - 1]) {                System.out.println(&quot;出错了: &quot; + index);                errorCount.incrementAndGet();            }            marked[index] = true;        }    }}</code></pre><p>这样我们再次运行</p><pre><code class="language-bash">真正计算的次数: 200000错误计算的次数: 0实际结果: 200000</code></pre><p>ok，没问题了！emm，运行好多次，只是一直没有失败的，这是因为我们调整多次代码后，线程碰撞的概率变小了，没关系！我们微调一下再测试就可以！</p><h3 id="%E7%AC%AC%E5%85%AD%E6%AD%A5%EF%BC%9A%E8%B0%83%E6%95%B4-main-%E6%96%B9%E6%B3%95%EF%BC%8C%E8%BF%90%E8%A1%8C%E4%BB%A3%E7%A0%81%E6%89%93%E5%8D%B0%EF%BC%81" tabindex="-1">第六步：调整 main 方法，运行代码打印！</h3><p>只要出现错误，<code>errorCount</code> 肯定能统计到值，我们就来个循环，直到遇到漏算的错误情况。</p><p>完整代码：</p><pre><code class="language-java">/** * FindErrorNumsCounter 找出并发情况下哪些计数被漏算 * * @author suremotoo * @date 2022/11/07 15:57 */public class FindErrorNumsCounter implements Runnable {    static FindErrorNumsCounter instance = new FindErrorNumsCounter();    int index = 0;    /**     * 真正运行的次数，该值正好和 index 理论上计算的值是一致     */    static AtomicInteger realCount = new AtomicInteger();    /**     * 错误的次数     */    static AtomicInteger errorCount = new AtomicInteger();    /**     * 记录标记计算的数字，容量比理论计算的数值大一些     */    static boolean[] marked = new boolean[1000000];    static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);    static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);    @Override    public void run() {        marked[0] = true;        for (int i = 0; i &lt; 100000; i++) {            try {                cyclicBarrier2.reset();                cyclicBarrier1.await();            } catch (InterruptedException e) {                throw new RuntimeException(e);            } catch (BrokenBarrierException e) {                throw new RuntimeException(e);            }            index++;            try {                cyclicBarrier1.reset();                cyclicBarrier2.await();            } catch (InterruptedException e) {                throw new RuntimeException(e);            } catch (BrokenBarrierException e) {                throw new RuntimeException(e);            }            realCount.incrementAndGet();            synchronized (instance) {                if (marked[index] &amp;&amp; marked[index - 1]) {                    System.out.println(&quot;出错了: &quot; + index);                    errorCount.incrementAndGet();                }                marked[index] = true;            }        }    }    public static void main(String[] args) throws InterruptedException {        while (errorCount.get() == 0) {            realCount.set(0);            errorCount.set(0);            instance.index = 0;            marked = new boolean[1000000];            Thread t1 = new Thread(instance);            Thread t2 = new Thread(instance);            t1.start();            t2.start();            t1.join();            t2.join();            System.out.println(&quot;真正计算的次数: &quot; + realCount.get());            System.out.println(&quot;错误计算的次数: &quot; + errorCount.get());            System.out.println(&quot;实际结果: &quot; + instance.index);            System.out.println(&quot;----------------&quot;);        }    }}</code></pre><p>结果：</p><pre><code class="language-bash">真正计算的次数: 200000错误计算的次数: 0实际结果: 200000----------------真正计算的次数: 200000错误计算的次数: 0实际结果: 200000----------------出错了: 77953真正计算的次数: 200000错误计算的次数: 1实际结果: 199999----------------</code></pre><p>77953 重复计算了，错误 1 次，实际结果 199999，真正的计算为：200000，这次统计对咯~</p><p>可以多次运行验证，发现没问题~🎉🎉</p>]]>
                    </description>
                    <pubDate>Wed, 09 Nov 2022 12:10:18 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Java 锁与线程的那些事]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/java-lock-thread-something</link>
                    <description>
                            <![CDATA[<h1 id="java-锁与线程的那些事">Java 锁与线程的那些事</h1><h2 id="一引言">一、引言</h2><p><strong>引言</strong>：“操作系统的线程状态和 java 的线程状态有什么关系？” 这是校招时被问到的一个问题。当时只顾着看博文、面经等零散的资料，没有形成系统的知识体系，一时语塞，答的不是很对。在网上也没找到足够细致的讲解博文，于是整理出了这篇内容。</p><p><img src="https://tech.youzan.com/content/images/2021/06/image-20210627214603558-1.png" alt="" /></p><p>Java 的线程状态牵扯到了同步语义，要探讨 Java 的线程状态的，必不可免要回顾其锁机制。因此本文的主要分为两大块：一是 Synchronized 源码粗析，分析了各类锁的进入、释放、升级过程，并大致说明了 monitor 原理；二是介绍了线程的实现方式和 Java 线程状态转换的部分细节。</p><p><strong>P.S.</strong> 本文内容较啰嗦，时间不充裕的同学可以直接看 <strong>2.6 小结</strong>及 <strong>3.3 小结</strong>。</p><h2 id="二synchronized-锁">二、Synchronized 锁</h2><p>Java 采用 synchronized 关键字、以互斥同步的方式的解决线程安全问题，那么什么是线程安全呢？这里引用《Java 并发编程实战》作者 Brian Goetz 给出的定义：</p><blockquote><p>当多个线程同时访问一个对象时，如果不用考虑这些线程在运行时环境下的调度和交替执行，也不需要进行额外的同步，或者在调用方进行任何其他的协调操作，调用这个对象的行为都可以获得正确的结果，那就称这个对象是线程安全的。 —— Brian Goetz</p></blockquote><h3 id="21-synchronized-的使用">2.1 Synchronized 的使用</h3><p>先写过个 demo，大致过一下 <code>synchronized</code> 的使用，包含同步代码块、实例方法和静态方法。</p><pre><code>  public synchronized void test1(){  }  public void test2(){    synchronized(new Test()){    }  }  public static synchronized void test3(){  }</code></pre><p>反编译可查看字节码：</p><pre><code>  public synchronized void test1();    descriptor: ()V    flags: ACC_PUBLIC, ACC_SYNCHRONIZED    // here  public void test2();    descriptor: ()    flags: ACC_PUBLIC    Code:      stack=2, locals=3, args_size=1         0: new           #2                  // class com/easy/helloworld/Test         3: dup         4: invokespecial #3                  // Method &quot;&lt;init&gt;&quot;:()V         7: dup         8: astore_1         9: monitorenter                   // here        10: aload_1        11: monitorexit                    // here        12: goto          20        15: astore_2        16: aload_1        17: monitorexit                    // here        18: aload_2        19: athrow        20: return  public static synchronized void test3();    descriptor: ()V    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED   // here</code></pre><p>可以观察到：</p><ul><li>同步代码：通过 moniterenter、moniterexit 关联到到一个 monitor 对象，进入时设置 Owner 为当前线程，计数 + 1、退出 - 1。除了正常出口的 monitorexit，还在异常处理代码里插入了 monitorexit。</li><li>实例方法：隐式调用 moniterenter、moniterexit</li><li>静态方法：隐式调用 moniterenter、moniterexit</li></ul><h3 id="22-moniterentermoniterexit">2.2 Moniterenter、Moniterexit</h3><p>monitorenter 和 monitorexit 这两个 jvm 指令，主要是基于 <code>Mark Word</code> 和 <code>Object monitor</code> 来实现的。</p><p>在 JVM 中，对象在内存中分为三块区域：</p><ul><li><p>对象头：由 <code>Mark Word</code> 和 <code>Klass Point</code> 构成。</p><ul><li><p><strong>Mark Word</strong>（标记字段）：用于存储对象自身的运行时数据，例如存储对象的 HashCode，分代年龄、锁标志位等信息，是 synchronized 实现轻量级锁和偏向锁的关键。 64 位 JVM 的 Mark Word 组成如下：</p><p><img src="https://tech.youzan.com/content/images/2021/06/image-20210627210825952.png" alt="" /></p></li><li><p><strong>Klass Point</strong>（类型指针）：对象指向它的类元数据的指针，虚拟机通过这个指针来确定这个对象是哪个类的实例。</p></li></ul></li><li><p>实例数据：这部分主要是存放类的数据信息，父类的信息。</p></li><li><p>字节对齐：为了内存的 IO 性能，JVM 要求对象起始地址必须是 8 字节的整数倍。对于不对齐的对象，需要填充数据进行对齐。</p></li></ul><p>在 JDK 1.6 之前，<code>synchronized</code> 只有传统的锁机制，直接关联到 <code>monitor</code> 对象，存在性能上的瓶颈。在 JDK 1.6 后，为了提高锁的获取与释放效率，JVM 引入了两种锁机制：偏向锁和轻量级锁。它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。这几种锁的实现和转换正是依靠对象头中的 <code>Mark Word</code>。</p><h3 id="23-偏向锁">2.3 偏向锁</h3><p>引入偏向锁的目的：在没有多线程竞争的情况下，尽量减少不必要的轻量级锁的执行。轻量级锁的获取及释放依赖多次 CAS 原子指令，而偏向锁只依赖一次 CAS 原子指令。但在多线程竞争时，需要进行偏向锁撤销步骤，因此其撤销的开销必须小于节省下来的 CAS 开销，否则偏向锁并不能带来收益。JDK 1.6 中默认开启偏向锁，可以通过 - XX:-UseBiasedLocking 来禁用偏向锁。</p><h3 id="231-进入偏向锁">2.3.1 进入偏向锁</h3><p>关于 HotSpot 虚拟机中获取锁的入口，网上主要有两种看法：一为 <a href="http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/interpreter/interpreterRuntime.cpp#l608">interpreterRuntime.cpp#monitorenter#1608</a>；二为 <a href="http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/interpreter/bytecodeInterpreter.cpp#l1816">bytecodeInterpreter.cpp#1816</a>。在 HotSpot 的中，有两处地方对 <code>monitorenter</code> 指令进行解析：一个是 <a href="http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/interpreter/bytecodeInterpreter.cpp#l1816">bytecodeInterpreter.cpp#1816</a> ，另一个在 <a href="http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/cpu/x86/vm/templateTable_x86_64.cpp#l3667">templateTable_x86_64.cpp#3667</a>。其中，<code>bytecodeInterpreter</code> 是 JVM 中的字节码解释器， <code>templateInterpreter</code> 为模板解释器。HotSpot 对运行效率有着极其执着的追求，显然会倾向于用模板解释器来实现。R 大的<a href="https://book.douban.com/annotation/31407691/">读书笔记</a>中有说明，HotSpot 中只用到了模板解释器，并没有用到字节码解释器。因此，本文认为 <code>montorenter</code> 的解析入口为 <a href="http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/cpu/x86/vm/templateTable_x86_64.cpp#l3667">templateTable_x86_64.cpp#3667</a>。</p><p>但模板解释器 <code>templateInterpreter</code> 都是汇编代码，不易读，且实现逻辑与字节码解释器 <code>bytecodeInterpreter</code> 大体一致。因此本文的源码都以 <code>bytecodeInterpreter</code> 来说明，借此窥探 <code>synchronized</code> 的实现原理。在看代码之前，先介绍几个在偏向锁中会被大量应用的概念，以便后续理解：</p><p><code>prototype_header</code>：JVM 中的每个类有一个类似 <code>mark word</code> 的 <code>prototype_header</code>，用来标记该 class 的 <code>epoch</code> 和偏向开关等信息。</p><p><code>匿名偏向状态</code>：锁对象 mark word 标志位为 101，且存储的 <code>Thread ID</code> 为空时的状态 (即锁对象为偏向锁，且没有线程偏向于这个锁对象)。</p><p><code>Atomic::cmpxchg_ptr</code>：CAS 函数。这个方法有三个参数，依次为 <code>exchange_value</code>、<code>dest</code>、<code>compare_value</code>。如果 dest 的值为 <code>compare_value</code> 则更新为 <code>exchange_value</code>，并返回 <code>compare_value</code>。否则，不更新并返回<code>实际原值</code>。</p><p>接下来开始源码实现分析，HotSpot 中偏向锁的具体实现可参考 <a href="http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/interpreter/bytecodeInterpreter.cpp#l1816">bytecodeInterpreter.cpp#1816</a>，代码如下：</p><pre><code>CASE(_monitorenter): {    //锁对象  oop lockee = STACK_OBJECT(-1);  // derefing's lockee ought to provoke implicit null check  CHECK_NULL(lockee);  // 步骤1  // 在栈中找到第一个空闲的Lock Record  // 会找到栈中最高的  BasicObjectLock* limit = istate-&gt;monitor_base();  BasicObjectLock* most_recent = (BasicObjectLock*) istate-&gt;stack_base();  BasicObjectLock* entry = NULL;  while (most_recent != limit ) {    if (most_recent-&gt;obj() == NULL) entry = most_recent;    else if (most_recent-&gt;obj() == lockee) break;    most_recent++;  }  // entry不为null，代表还有空闲的Lock Record  if (entry != NULL) {    // 将Lock Record的obj指针指向锁对象    entry-&gt;set_obj(lockee);    int success = false;    uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;    // markoop即对象头的mark word    markOop mark = lockee-&gt;mark();    intptr_t hash = (intptr_t) markOopDesc::no_hash;    // 步骤2    // implies UseBiasedLocking    // 如果为偏向模式，即判断标识位是否为101    if (mark-&gt;has_bias_pattern()) {      ...      // 一顿操作      anticipated_bias_locking_value =        (((uintptr_t)lockee-&gt;klass()-&gt;prototype_header() | thread_ident) ^ (uintptr_t)mark) &amp;        ~((uintptr_t) markOopDesc::age_mask_in_place);      // 步骤3      if  (anticipated_bias_locking_value == 0) {        // already biased towards this thread, nothing to do        // 偏向的是自己，啥都不做        if (PrintBiasedLockingStatistics) {          (* BiasedLocking::biased_lock_entry_count_addr())++;        }        success = true;      }      // class的prototype_header不是偏向模式      else if ((anticipated_bias_locking_value &amp; markOopDesc::biased_lock_mask_in_place) != 0) {        // 尝试撤销偏向                ...      }      // epoch过期，重新偏向      else if ((anticipated_bias_locking_value &amp; epoch_mask_in_place) !=0) {        // try rebias                ...        success = true;          }      else {        // try to bias towards thread in case object is anonymously biased        // 尝试偏向该线程，只有匿名偏向能成功        // 构建了匿名偏向的mark word        markOop header = (markOop) ((uintptr_t) mark &amp; ((uintptr_t)markOopDesc::biased_lock_mask_in_place |(uintptr_t)markOopDesc::age_mask_in_place |epoch_mask_in_place));        if (hash != markOopDesc::no_hash) {          header = header-&gt;copy_set_hash(hash);        }        // 用「或」操作设置thread ID        markOop new_header = (markOop) ((uintptr_t) header | thread_ident);、        // 只有匿名偏向才能成功        if (Atomic::cmpxchg_ptr((void*)new_header, lockee-&gt;mark_addr(), header) == header) {          // cas修改成功              if (PrintBiasedLockingStatistics)            (* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;        }        else {          // 失败说明存在竞争，进入monitorenter          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);        }        success = true;      }    }    // 步骤4    // traditional lightweight locking    // false走轻量级锁逻辑    if (!success) {      // 构造一个无锁状态的Displaced Mark Word，并将lock record指向它      markOop displaced = lockee-&gt;mark()-&gt;set_unlocked();      entry-&gt;lock()-&gt;set_displaced_header(displaced);      bool call_vm = UseHeavyMonitors;      if (call_vm || Atomic::cmpxchg_ptr(entry, lockee-&gt;mark_addr(), displaced) != displaced) {        // 如果CAS替换不成功，代表锁对象不是无锁状态，这时候判断下是不是锁重入        // Is it simple recursive case?        if (!call_vm &amp;&amp; THREAD-&gt;is_lock_owned((address) displaced-&gt;clear_lock_bits())) {          // 如果是锁重入，则直接将Displaced Mark Word设置为null          // 轻量级锁重入是使用lock record的数量来计入的          entry-&gt;lock()-&gt;set_displaced_header(NULL);        } else {          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);        }      }    }    UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);  } else {    // 没拿到lock record，重新执行    istate-&gt;set_msg(more_monitors);    UPDATE_PC_AND_RETURN(0); // Re-execute  }}</code></pre><p>偏向锁流程：</p><p><code>步骤 1</code>、从当前线程的栈中找到一个空闲的 <code>Lock Record</code>，并指向当前锁对象。</p><p><code>步骤 2</code>、获取对象的 markOop 数据 mark，即对象头的 Mark Word；</p><p><code>步骤 3</code>、判断锁对象的 <code>mark word</code> 是否是偏向模式，即低 3 位是否为 101。若不是，进入步骤 4。若是，计算 <code>anticipated_bias_locking_value</code>，判断偏向状态：</p><p><code>步骤 3.1</code>、<code>anticipated_bias_locking_value</code> 若为 0，代表<strong>偏向的线程是当前线程</strong>且 <code>mark word</code> 的 epoch 等于 class 的 epoch，这种情况下直接执行同步代码块，什么都不用做。</p><p><code>步骤 3.2</code>、判断 class 的 <code>prototype_header</code> 是否为非偏向模式。若为非偏向模式，CAS 尝试将对象恢复为无锁状态。无论 cas 是否成功都会进入轻量级锁逻辑。</p><p><code>步骤 3.3</code>、如果 epoch 偏向<strong>时间戳已过期</strong>，则需要重偏向。利用 CAS 指令将锁对象的 <code>mark word</code> 替换为一个偏向当前线程且 epoch 为类的 epoch 的新的 <code>mark word</code>。</p><p><code>步骤 3.4</code>、CAS 将偏向线程改为当前线程，如果当前是<strong>匿名偏向</strong>（即对象头中的 bit field 存储的 Thread ID 为空）且<strong>无并发冲突</strong>，则能<code>修改成功</code>获取偏向锁，否则进入<code>锁升级</code>的逻辑。</p><p><code>步骤 4</code>、走到一步会进行轻量级锁逻辑。构造一个无锁状态的 <code>mark word</code>，然后存储到 <code>Lock Record</code>。设置为无锁状态的原因是：轻量级锁解锁时是将对象头的 <code>mark word</code>cas 替换为 <code>Lock Record</code> 中的 <code>Displaced Mark Word</code>，所以设置为无锁状态。如果是锁重入，则将 <code>Lock Record</code> 的 <code>Displaced Mark Word</code> 设置为 null，放到栈帧中，起到计数作用。</p><p>以上是偏向锁加锁的大致流程，如果当前锁<strong>已偏向其他线程</strong> || <strong>epoch 值过期</strong> || <strong>class 偏向模式关闭</strong> || <strong>获取偏向锁的过程中存在并发冲突</strong>，都会进入到 <code>InterpreterRuntime::monitorenter</code> 方法， 在该方法中会进行偏向锁撤销和升级。流程如下图所示：</p><p><img src="https://tech.youzan.com/content/images/2021/07/---.svg" alt="偏向锁" /></p><p><strong>Issue</strong>：有的同学可能会问了，对象一开始不是无锁状态吗，为什么上述偏向锁逻辑没有判断<strong>无锁状态的锁对象</strong>（001）？</p><p><strong>只有匿名偏向的对象才能进入偏向锁模式</strong>。JVM 启动时会延时初始化偏向锁，默认是 4000ms。初始化后会将所有加载的 Klass 的 prototype header 修改为匿名偏向样式。当创建一个对象时，会通过 Klass 的 prototype_header 来初始化该对象的对象头。简单的说，偏向锁初始化结束后，后续所有对象的对象头都为<strong>匿名偏向</strong>样式，在此之前创建的对象则为<strong>无锁状态</strong>。而对于无锁状态的锁对象，如果有竞争，会直接进入到轻量级锁。这也是为什么 JVM 启动前 4 秒对象会直接进入到轻量级锁的原因。</p><p>为什么需要延迟初始化？</p><p>JVM 启动时必不可免会有大量 sync 的操作，而偏向锁并不是都有利。如果开启了偏向锁，会发生大量锁撤销和锁升级操作，大大降低 JVM 启动效率。</p><p>因此，我们可以明确地说，只有锁对象处于<strong>匿名偏向</strong>状态，线程才能拿到到我们通常意义上的偏向锁。而处于无锁状态的锁对象，只能进入到轻量级锁状态。</p><h3 id="232-偏向锁的撤销">2.3.2 偏向锁的撤销</h3><p>偏向锁的 <code>撤销</code>（revoke）是一个很特殊的操作，为了执行撤销操作，需要等待<code>全局安全点</code>，此时所有的工作线程都停止了执行。偏向锁的撤销操作并不是将对象恢复到无锁可偏向的状态（<strong>注意区分偏向锁撤销和释放这两个概念，撤销的触发见上图</strong>），而是在偏向锁的获取过程中，发现竞争时，直接将一个被偏向的对象<code>升级到</code>被加了轻量级锁的状态。这个操作的具体完成方式如下：</p><pre><code>IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))    ...  Handle h_obj(thread, elem-&gt;obj());  assert(Universe::heap()-&gt;is_in_reserved_or_null(h_obj()),         &quot;must be NULL or an object&quot;);    // 开启了偏向锁  if (UseBiasedLocking) {    // Retry fast entry if bias is revoked to avoid unnecessary inflation    ObjectSynchronizer::fast_enter(h_obj, elem-&gt;lock(), true, CHECK);  } else {    ObjectSynchronizer::slow_enter(h_obj, elem-&gt;lock(), CHECK);  }  ...</code></pre><p>如果开启了 JVM 偏向锁，则会进入到 <code>ObjectSynchronizer::fast_enter</code> 方法中。</p><pre><code>void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {   //再次校验 if (UseBiasedLocking) {    if (!SafepointSynchronize::is_at_safepoint()) {      //不在安全点的执行      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);      if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {        return;      }    } else {      assert(!attempt_rebias, &quot;can not rebias toward VM thread&quot;);          //批量撤销,底层调用bulk_revoke_or_rebias_at_safepoint      BiasedLocking::revoke_at_safepoint(obj);    }    assert(!obj-&gt;mark()-&gt;has_bias_pattern(), &quot;biases should be revoked by now&quot;); } slow_enter (obj, lock, THREAD) ;}</code></pre><p>主要看 <code>BiasedLocking::revoke_and_rebias</code> 方法。这个方法的主要作用像它的方法名：撤销或者重偏向。第一个参数封装了锁对象和当前线程，第二个参数代表是否允许重偏向，这里是 true。</p><pre><code>BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {    assert(!SafepointSynchronize::is_at_safepoint(), &quot;must not be called while at safepoint&quot;);  markOop mark = obj-&gt;mark(); //获取锁对象的对象头  if (mark-&gt;is_biased_anonymously() &amp;&amp; !attempt_rebias) {    // 如果锁对象为匿名偏向状态且不允许重偏向下，进入该分支。在一个非全局安全点进行偏向锁撤销    markOop biased_value       = mark;    // 创建一个匿名偏向的markword    markOop unbiased_prototype = markOopDesc::prototype()-&gt;set_age(mark-&gt;age());    // 通过cas重新设置偏向锁状态    markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj-&gt;mark_addr(), mark);    if (res_mark == biased_value) {// 如果CAS成功，返回偏向锁撤销状态      return BIAS_REVOKED;    }  } else if (mark-&gt;has_bias_pattern()) {    // 锁为偏向模式（101）会走到这里     Klass* k = obj-&gt;klass();     markOop prototype_header = k-&gt;prototype_header();    // 如果对应class关闭了偏向模式    if (!prototype_header-&gt;has_bias_pattern()) {      markOop biased_value       = mark;      // CAS更新对象头markword为非偏向锁      markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header, obj-&gt;mark_addr(), mark);      assert(!(*(obj-&gt;mark_addr()))-&gt;has_bias_pattern(), &quot;even if we raced, should still be revoked&quot;);      return BIAS_REVOKED; // 返回偏向锁撤销状态    } else if (prototype_header-&gt;bias_epoch() != mark-&gt;bias_epoch()) {      // 如果epoch过期，则进入当前分支      if (attempt_rebias) {        // 如果允许重偏        assert(THREAD-&gt;is_Java_thread(), &quot;&quot;);        markOop biased_value       = mark;        markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark-&gt;age(), prototype_header-&gt;bias_epoch());        // 通过CAS操作， 将本线程的 ThreadID 、时间戳、分代年龄尝试写入对象头中        markOop res_mark = (markOop) Atomic::cmpxchg_ptr(rebiased_prototype, obj-&gt;mark_addr(), mark);        if (res_mark == biased_value) { //CAS成功，则返回撤销和重新偏向状态          return BIAS_REVOKED_AND_REBIASED;        }      } else {        // 如果不允许尝试获取偏向锁，进入该分支取消偏向        // 通过CAS操作更新分代年龄        markOop biased_value       = mark;        markOop unbiased_prototype = markOopDesc::prototype()-&gt;set_age(mark-&gt;age());        markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj-&gt;mark_addr(), mark);        if (res_mark == biased_value) { //如果CAS操作成功，返回偏向锁撤销状态          return BIAS_REVOKED;        }      }    }  }  //执行到这里有以下两种情况：  //1.对象不是偏向模式  //2.上面的cas操作失败  HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias);  if (heuristics == HR_NOT_BIASED) {    // 非偏向从这出去    // 轻量级锁、重量级锁    return NOT_BIASED;  } else if (heuristics == HR_SINGLE_REVOKE) {    // 撤销单个线程    // Mark，最常见的执行分支    // Mark，最常见的执行分支    // Mark，最常见的执行分支    Klass *k = obj-&gt;klass();    markOop prototype_header = k-&gt;prototype_header();    if (mark-&gt;biased_locker() == THREAD &amp;&amp;        prototype_header-&gt;bias_epoch() == mark-&gt;bias_epoch()) {      // 偏向当前线程且不过期      // 这里撤销的是偏向当前线程的锁，调用Object#hashcode方法时也会走到这一步      // 因为只要遍历当前线程的栈就能拿到lock record了，所以不需要等到safe point再撤销。      ResourceMark rm;      if (TraceBiasedLocking) {        tty-&gt;print_cr(&quot;Revoking bias by walking my own stack:&quot;);      }      BiasedLocking::Condition cond = revoke_bias(obj(), false, false, (JavaThread*) THREAD);      ((JavaThread*) THREAD)-&gt;set_cached_monitor_info(NULL);      assert(cond == BIAS_REVOKED, &quot;why not?&quot;);      return cond;    } else {      // 下面代码最终会在safepoint调用revoke_bias方法撤销偏向      VM_RevokeBias revoke(&amp;obj, (JavaThread*) THREAD);      VMThread::execute(&amp;revoke);      return revoke.status_code();    }  }  assert((heuristics == HR_BULK_REVOKE) ||         (heuristics == HR_BULK_REBIAS), &quot;?&quot;);   //批量撤销、批量重偏向的逻辑  VM_BulkRevokeBias bulk_revoke(&amp;obj, (JavaThread*) THREAD,                                (heuristics == HR_BULK_REBIAS),                                attempt_rebias);  VMThread::execute(&amp;bulk_revoke);  return bulk_revoke.status_code();}</code></pre><p>这块代码注释写的算是比较清楚，只简单介绍下最常见的情况：锁已经偏向线程 A，此时线程 B 尝试获取锁。这种情况下会走到 Mark 标记的分支。如果需要撤销的是当前线程，只要遍历当前线程的栈就能拿到 lock record，可以直接调用 <code>revoke_bias</code>，不需要等到 safe point 再撤销。在调用 Object#hashcode 时，也会走到该分支将为偏向锁的锁对象直接恢复为无锁状态。若不是当前线程，会被 push 到 VM Thread 中等到 <code>safepoint</code> 的时候再执行。</p><p>VMThread 内部维护了一个 VMOperationQueue 类型的队列，用于保存内部提交的 VM 线程操作 VM_operation。GC、偏向锁的撤销等操作都是在这里被执行。</p><p>撤销调用的 <code>revoke_bias</code> 方法的代码就不贴了。大致逻辑是：</p><p><code>步骤 1</code>、查看偏向的线程是否存活，如果已经死亡，则直接撤销偏向锁。JVM 维护了一个集合存放所有存活的线程，通过遍历该集合判断某个线程是否存活。</p><p><code>步骤 2</code>、偏向的线程是否还在同步块中，如果不在，则撤销偏向锁。如果在同步块中，执行步骤 3。这里<strong>是否在同步块的判断</strong>基于上文提到的偏向锁的重入计数方式：在偏向锁的获取中，每次进入同步块的时候都会在栈中找到第一个可用（即栈中最高的）的 <code>Lock Record</code>，将其 obj 字段指向锁对象。每次解锁的时候都会把最低的 <code>Lock Record</code> 移除掉，所以可以通过遍历线程栈中的 <code>Lock Record</code> 来判断是否还在同步块中。轻量级锁的重入也是基于 <code>Lock Record</code> 的计数来判断。</p><p><code>步骤 3</code>、升级为轻量级锁。将偏向线程所有相关 <code>Lock Record</code> 的 <code>Displaced Mark Word</code> 设置为 null，再将最高位的 <code>Lock Record</code> 的 <code>Displaced Mark Word</code> 设置为无锁状态，然后将对象头指向最高位的 <code>Lock Record</code>。这里没有用到 CAS 指令，因为是在 <code>safepoint</code>，可以直接升级成轻量级锁。</p><h3 id="233-偏向锁的释放">2.3.3 偏向锁的释放</h3><p>偏向锁的释放可参考 <a href="http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/interpreter/bytecodeInterpreter.cpp#l1923">bytecodeInterpreter.cpp#1923</a>，这里也不贴了。偏向锁的释放只要将对应 <code>Lock Record</code> 释放就好了，但这里的释放并不会将 mark word 里面的 thread ID 去掉，这样做是为了下一次更方便的加锁。而轻量级锁则需要将 <code>Displaced Mark Word</code> 替换到对象头的 mark word 中。如果 CAS 失败或者是重量级锁则进入到 <code>InterpreterRuntime::monitorexit</code> 方法中。</p><h3 id="234-批量重偏向与撤销">2.3.4 批量重偏向与撤销</h3><p>从上节偏向锁的加锁解锁过程中可以看出，当只有一个线程反复进入同步块时，偏向锁带来的性能开销基本可以忽略，但是当有其他线程尝试获得锁时，就需要等到 <code>safe point</code> 时将偏向锁撤销为无锁状态或升级为轻量级 / 重量级锁。因此，JVM 中增加了一种批量重偏向 / 撤销的机制以减少锁撤销的开销，而 mark word 中的 epoch 也是在这里被大量应用，这里不展开说明。但无论怎么优化，偏向锁的撤销仍有一定不可避免的成本。如果业务场景存在大量多线程竞争，那偏向锁的存在不仅不能提高性能，而且会导致性能下降（<strong>偏向锁并不都有利，jdk15 默认不开启</strong>）。</p><h3 id="24-轻量级锁">2.4 轻量级锁</h3><p>引入轻量级锁的目的：在多线程交替执行同步块的情况下，尽量避免重量级锁使用的操作系统互斥量带来的开销，但是如果多个线程在同一时刻进入临界区，会导致轻量级锁膨胀升级重量级锁，所以轻量级锁的出现并非是要替代重量级锁。</p><h3 id="241-进入轻量级锁">2.4.1 进入轻量级锁</h3><p>轻量级锁在上文或多或少已经涉及到，其获取流程入口为 <a href="http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/interpreter/bytecodeInterpreter.cpp#l1816">bytecodeInterpreter.cpp#1816</a>。前大半部分都是偏向锁逻辑，还有一部分为轻量级锁逻辑。在偏向锁逻辑中，cas 失败会执行到 <code>InterpreterRuntime::monitorenter</code>。在轻量级锁逻辑中，如果当前线程不是轻量级锁的重入，也会执行到 <code>InterpreterRuntime::monitorenter</code>。我们再看看 <code>InterpreterRuntime::monitorenter</code> 方法：</p><pre><code>IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))    ...  Handle h_obj(thread, elem-&gt;obj());  assert(Universe::heap()-&gt;is_in_reserved_or_null(h_obj()),         &quot;must be NULL or an object&quot;);  if (UseBiasedLocking) {    // Retry fast entry if bias is revoked to avoid unnecessary inflation    ObjectSynchronizer::fast_enter(h_obj, elem-&gt;lock(), true, CHECK);  } else {    ObjectSynchronizer::slow_enter(h_obj, elem-&gt;lock(), CHECK);  }  ...IRT_END  </code></pre><p><code>fast_enter</code> 的流程在偏向锁的撤销小节中已经分析过，主要逻辑为 <code>revoke_and_rebias</code>：如果当前是偏向模式且偏向的线程还在使用锁，会将锁的 <code>mark word</code> 改为轻量级锁的状态，并将偏向的线程栈中的 <code>Lock Record</code> 修改为轻量级锁对应的形式（此时 Lock Record 是无锁状态），且返回值不是 <code>BIAS_REVOKED_AND_REBIASED</code>，会继续执行 <code>slow_enter</code>。</p><p>我们直接看 <code>slow_enter</code> 的流程:</p><pre><code>void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {    // 步骤1  markOop mark = obj-&gt;mark();  assert(!mark-&gt;has_bias_pattern(), &quot;should not see bias pattern here&quot;);  // 步骤2  // 如果为无锁状态  if (mark-&gt;is_neutral()) {    // 步骤3    // 设置mark word到栈     lock-&gt;set_displaced_header(mark);    // CAS更新指向栈中Lock Record的指针    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()-&gt;mark_addr(), mark)) {      TEVENT (slow_enter: release stacklock) ;      return ;    }    // Fall through to inflate() ... cas失败走下面锁膨胀方法  } else if (mark-&gt;has_locker() &amp;&amp; THREAD-&gt;is_lock_owned((address)mark-&gt;locker())) {    // 步骤4    // 为轻量级锁且owner为当前线程    assert(lock != mark-&gt;locker(), &quot;must not re-lock the same lock&quot;);    assert(lock != (BasicLock*)obj-&gt;mark(), &quot;don't relock with same BasicLock&quot;);    // 设置Displaced Mark Word为null，重入计数用    lock-&gt;set_displaced_header(NULL);    return;  }  // 步骤5  // 走到这一步说明已经是存在多个线程竞争锁了，需要膨胀或已经是重量级锁  lock-&gt;set_displaced_header(markOopDesc::unused_mark());  // 进入、膨胀到重量级锁的入口  // 膨胀后再调用monitor的enter方法竞争锁  ObjectSynchronizer::inflate(THREAD, obj())-&gt;enter(THREAD);}</code></pre><p><code>步骤 1</code>、<code>markOop mark = obj-&gt;mark()</code> 方法获取对象的 markOop 数据 mark；</p><p><code>步骤 2</code>、<code>mark-&gt;is_neutral()</code> 方法判断 mark 是否为无锁状态，标识位 <strong>001</strong>；</p><p><code>步骤 3</code>、如果 mark 处于无锁状态，把 mark 保存到 BasicLock 对象 (Lock Record 的属性) 的 displaced_header 字段；</p><p><code>步骤 3.1</code>、通过 CAS 尝试将 Mark Word 更新为指向 BasicLock 对象的指针，如果更新成功，表示竞争到锁，则执行同步代码，否则执行步骤 4；</p><p><code>步骤 4</code>、如果是重入，则设置 Displaced Mark Word 为 null。</p><p><code>步骤 5</code>、到这说明有多个线程竞争轻量级锁，轻量级锁需要膨胀升级为重量级锁；</p><p>结合上文偏向锁的流程，可以整理得到如下的流程图：</p><p><img src="https://tech.youzan.com/content/images/2021/06/-----2.svg" alt="轻量级锁" /></p><h3 id="242-轻量级锁的释放">2.4.2 轻量级锁的释放</h3><p>轻量级锁释放的入口在 <a href="http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/interpreter/bytecodeInterpreter.cpp#l1923">bytecodeInterpreter.cpp#1923</a>。</p><p>轻量级锁释放时需要将 <code>Displaced Mark Word</code> 替换回对象头的 <code>mark word</code> 中。如果 CAS 失败或者是重量级锁则进入到 <code>InterpreterRuntime::monitorexit</code> 方法中。<code>monitorexit</code> 直接调用 <code>slow_exit</code> 方法释放 <code>Lock Record</code>。直接看 <code>slow_exit</code>：</p><pre><code>IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem))    Handle h_obj(thread, elem-&gt;obj());  assert(Universe::heap()-&gt;is_in_reserved_or_null(h_obj()),         &quot;must be NULL or an object&quot;);  if (elem == NULL || h_obj()-&gt;is_unlocked()) {    THROW(vmSymbols::java_lang_IllegalMonitorStateException());  }  // 直接调用slow_exit  ObjectSynchronizer::slow_exit(h_obj(), elem-&gt;lock(), thread);  // Free entry. This must be done here, since a pending exception might be installed on  // exit. If it is not cleared, the exception handling code will try to unlock the monitor again.  elem-&gt;set_obj(NULL);IRT_ENDvoid ObjectSynchronizer::slow_exit(oop object, BasicLock* lock, TRAPS) {    fast_exit (object, lock, THREAD) ;}void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {    ...  // displaced header就是对象mark word的拷贝  markOop dhw = lock-&gt;displaced_header();  markOop mark ;  if (dhw == NULL) {     // 什么也不做     // Recursive stack-lock. 递归堆栈锁     // Diagnostics -- Could be: stack-locked, inflating, inflated.      ...     return ;  }  mark = object-&gt;mark() ;  // 此处为轻量级锁的释放过程，使用CAS方式解锁。  // 如果对象被当前线程堆栈锁定，尝试将displaced header和锁对象中的MarkWord替换回来。  // If the object is stack-locked by the current thread, try to  // swing the displaced header from the box back to the mark.  if (mark == (markOop) lock) {     assert (dhw-&gt;is_neutral(), &quot;invariant&quot;) ;     if ((markOop) Atomic::cmpxchg_ptr (dhw, object-&gt;mark_addr(), mark) == mark) {        TEVENT (fast_exit: release stacklock) ;        return;     }  }  //走到这里说明已经是重量级锁或者解锁时发生了竞争，膨胀后再调用monitor的exit方法释放  ObjectSynchronizer::inflate(THREAD, object)-&gt;exit (true, THREAD) ;}</code></pre><p>最后执行的是如果是 fast_exit 方法。如果是轻量级锁，尝试 cas 替换 <code>mark word</code>。若解锁时有竞争，会调用 <code>inflate</code> 方法进行重量级锁膨胀，升级到到重量级锁后再执行 <code>exit</code> 方法。</p><h3 id="25-重量级锁">2.5 重量级锁</h3><h3 id="251-重量级锁的进入">2.5.1 重量级锁的进入</h3><p>重量级锁通过对象内部的监视器（monitor）实现，其依赖于底层操作系统的 <code>Mutex Lock</code> 实现，需要额外的用户态到内核态切换的开销。由上文分析，<code>slow_enter</code> 获取轻量级锁未成功时，会在 <code>inflate</code> 中完成锁膨胀：</p><pre><code>ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {    ...  for (;;) {      const markOop mark = object-&gt;mark() ;      assert (!mark-&gt;has_bias_pattern(), &quot;invariant&quot;) ;        // mark是以下状态中的一种：      // *  Inflated（重量级锁状态）     - 直接返回      // *  Stack-locked（轻量级锁状态） - 膨胀      // *  INFLATING（膨胀中）    - 忙等待直到膨胀完成      // *  Neutral（无锁状态）      - 膨胀      // *  BIASED（偏向锁）       - 非法状态，在这里不会出现      // CASE: inflated      if (mark-&gt;has_monitor()) {          // 已经是重量级锁状态了，直接返回          ObjectMonitor * inf = mark-&gt;monitor() ;          ...          return inf ;      }      // CASE: inflation in progress      if (mark == markOopDesc::INFLATING()) {         // 正在膨胀中，说明另一个线程正在进行锁膨胀，continue重试         TEVENT (Inflate: spin while INFLATING) ;         // 在该方法中会进行spin/yield/park等操作完成自旋动作          ReadStableMark(object) ;         continue ;      }      // 当前是轻量级锁，后面分析      // CASE: stack-locked          if (mark-&gt;has_locker()) {        ...      }      // 无锁状态      // CASE: neutral      // 分配以及初始化ObjectMonitor对象      ObjectMonitor * m = omAlloc (Self) ;      // prepare m for installation - set monitor to initial state      m-&gt;Recycle();      m-&gt;set_header(mark);      // owner为NULL      m-&gt;set_owner(NULL);      m-&gt;set_object(object);      m-&gt;OwnerIsThread = 1 ;      m-&gt;_recursions   = 0 ;      m-&gt;_Responsible  = NULL ;      m-&gt;_SpinDuration = ObjectMonitor::Knob_SpinLimit ;       // consider: keep metastats by type/class        // 用CAS替换对象头的mark word为重量级锁状态      if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object-&gt;mark_addr(), mark) != mark) {          // 不成功说明有另外一个线程在执行inflate，释放monitor对象          m-&gt;set_object (NULL) ;          m-&gt;set_owner  (NULL) ;          m-&gt;OwnerIsThread = 0 ;          m-&gt;Recycle() ;          omRelease (Self, m, true) ;          m = NULL ;          continue ;          // interference - the markword changed - just retry.          // The state-transitions are one-way, so there's no chance of          // live-lock -- &quot;Inflated&quot; is an absorbing state.      }      ...      return m ;}</code></pre><p><code>inflate</code> 其中是一个 for 循环，主要是为了处理多线程同时调用 inflate 的情况。然后会根据锁对象的状态进行不同的处理：</p><p>1. 已经是重量级状态，说明膨胀已经完成，返回并继续执行 ObjectMonitor::enter 方法。<br />2. 如果是轻量级锁则需要进行膨胀操作。<br />3. 如果是膨胀中状态，则进行忙等待。<br />4. 如果是无锁状态则需要进行膨胀操作。</p><p>轻量级锁膨胀流程如下：</p><pre><code>if (mark-&gt;has_locker()) {    // 步骤1  // 当前轻量级锁状态，先分配一个ObjectMonitor对象，并初始化值  ObjectMonitor * m = omAlloc (Self) ;            m-&gt;Recycle();  m-&gt;_Responsible  = NULL ;  m-&gt;OwnerIsThread = 0 ;  m-&gt;_recursions   = 0 ;  m-&gt;_SpinDuration = ObjectMonitor::Knob_SpinLimit ;   // Consider: maintain by type/class  // 步骤2  // 将锁对象的mark word设置为INFLATING (0)状态   markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object-&gt;mark_addr(), mark) ;  if (cmp != mark) {    omRelease (Self, m, true) ;    continue ;       // Interference -- just retry  }  // 步骤3  // 栈中的displaced mark word  markOop dmw = mark-&gt;displaced_mark_helper() ;  assert (dmw-&gt;is_neutral(), &quot;invariant&quot;) ;  // 设置monitor的字段  m-&gt;set_header(dmw) ;  // owner为Lock Record  m-&gt;set_owner(mark-&gt;locker());  m-&gt;set_object(object);  ...  // 步骤4  // 将锁对象头设置为重量级锁状态  object-&gt;release_set_mark(markOopDesc::encode(m));  ...  return m ;}</code></pre><p><code>步骤 1</code>、调用 <code>omAlloc</code> 获取一个可用的 <code>ObjectMonitor</code> 对象。在 <code>omAlloc</code> 方法中会先从<strong>线程私有</strong>的 <code>monitor</code> 集合 <code>omFreeList</code> 中分配对象，如果 <code>omFreeList</code> 中已经没有 <code>monitor</code> 对象，则从 <strong>JVM 全局</strong>的 <code>gFreeList</code> 中分配一批 <code>monitor</code> 到 <code>omFreeList</code> 中；</p><p><code>步骤 2</code>、通过 CAS 尝试将 Mark Word 设置为 markOopDesc:INFLATING，标识当前锁正在膨胀中。如果 CAS 失败，说明同一时刻其它线程已经将 Mark Word 设置为 markOopDesc:INFLATING，当前线程进行自旋等待膨胀完成。</p><p><code>步骤 3</code>、如果 CAS 成功，设置 monitor 的各个字段：设置 <code>monitor</code> 的 header 字段为 <code>displaced mark word</code>，owner 字段为 <code>Lock Record</code>，obj 字段为锁对象等；</p><p><code>步骤 4</code>、设置锁对象头的 <code>mark word</code> 为重量级锁状态，指向第一步分配的 <code>monitor</code> 对象；</p><h3 id="252-monitor-竞争">2.5.2 monitor 竞争</h3><p>当锁膨胀 <code>inflate</code> 执行完并返回对应的 <code>ObjectMonitor</code> 时，并不表示该线程竞争到了锁，真正的锁竞争发生在 <code>ObjectMonitor::enter</code> 方法中。</p><pre><code>void ATTR ObjectMonitor::enter(TRAPS) {    Thread * const Self = THREAD ;  void * cur ;  // 步骤1  // owner为null，如果能CAS设置成功，则当前线程直接获得锁  cur = Atomic::cmpxchg_ptr (Self, &amp;_owner, NULL) ;  if (cur == NULL) {     ...     return ;  }  // 如果是重入的情况  if (cur == Self) {     // TODO-FIXME: check for integer overflow!  BUGID 6557169.     _recursions ++ ;     return ;  }  // 步骤2  // 如果当前线程是之前持有轻量级锁的线程  // 上节轻量级锁膨胀将owner指向之前Lock Record的指针  // 这里利用owner判断是否第一次进入。  if (Self-&gt;is_lock_owned ((address)cur)) {    assert (_recursions == 0, &quot;internal state error&quot;);    // 重入计数重置为1    _recursions = 1 ;    // 设置owner字段为当前线程    _owner = Self ;    OwnerIsThread = 1 ;    return ;  }  ...  // 步骤3  // 在调用系统的同步操作之前，先尝试自旋获得锁  if (Knob_SpinEarly &amp;&amp; TrySpin (Self) &gt; 0) {         ...     //自旋的过程中获得了锁，则直接返回     Self-&gt;_Stalled = 0 ;     return ;  }  ...  {     ...    // 步骤4    for (;;) {      jt-&gt;set_suspend_equivalent();      // 在该方法中调用系统同步操作      EnterI (THREAD) ;      ...    }    Self-&gt;set_current_pending_monitor(NULL);   }  ...}</code></pre><p><code>步骤 1</code>、当前是无锁、锁重入，简单操作后返回。</p><p><code>步骤 2</code>、当前线程是之前持有轻量级锁的线程，则为首次进入，设置 recursions 为 1，owner 为当前线程，该线程成功获得锁并返回。</p><p><code>步骤 3</code>、先<strong>自旋尝试</strong>获得锁，尽可能减少同步操作带来的开销。</p><p><code>步骤 4</code>、调用 EnterI 方法。</p><p>这里注意，轻量级锁膨胀成功时，会把 owner 字段设置为 <code>Lock Record</code> 的指针，并在竞争时判断。这么做的原因是，假设当前线程 A 持有锁对象的锁，线程 B 进入同步代码块，并把锁对象升级为重量级锁。但此时，线程 A 可能还在执行，并无法感知其持有锁对象的变化。因此，需要线程 B 在执行 <code>ObjectMonitor::enter</code> 时，将自己放入到阻塞等列等待。并需要线程 A 第二次进入、或者退出的时候对 monitor 进行一些操作，以此保证代码块的同步。</p><p>这里有个<strong>自旋</strong>操作，直接看 <code>TrySpin</code> 对应的方法：</p><pre><code>// TrySpin对应的方法int ObjectMonitor::TrySpin_VaryDuration (Thread * Self) {      // Dumb, brutal spin.  Good for comparative measurements against adaptive spinning.    int ctr = Knob_FixedSpin ;  // 固定自旋次数    if (ctr != 0) {        while (--ctr &gt;= 0) {            if (TryLock (Self) &gt; 0) return 1 ;            SpinPause () ;        }        return 0 ;    }    // 上一次自旋次数    for (ctr = Knob_PreSpin + 1; --ctr &gt;= 0 ; ) {      if (TryLock(Self) &gt; 0) {  // 尝试获取锁        // Increase _SpinDuration ...        // Note that we don't clamp SpinDuration precisely at SpinLimit.        // Raising _SpurDuration to the poverty line is key.        int x = _SpinDuration ;        if (x &lt; Knob_SpinLimit) {           if (x &lt; Knob_Poverty) x = Knob_Poverty ;           _SpinDuration = x + Knob_BonusB ;        }        return 1 ;      }      ...      ...</code></pre><p>从方法名和注释可以看出，这就是自适应自旋，<strong>和网上说的轻量级锁 cas 失败会自旋的说法并不一致</strong>。实际上，无论是轻量级锁 cas 自旋还是重量级锁 cas 自旋，都是在用户态尽可能减少同步操作带来的开销，并没有太多本质上的区别。 到此为止，我们可以再结合上述的内容，整理出如下的状态转换图：</p><p><img src="https://tech.youzan.com/content/images/2021/06/-----3.svg" alt="重量级锁" /></p><h3 id="253-monitor-等待">2.5.3 monitor 等待</h3><p><code>ObjectMonitor</code> 竞争失败的线程，通过自旋执行 <code>ObjectMonitor::EnterI</code> 方法等待锁的释放，EnterI 方法的部分逻辑实现如下：</p><pre><code>void ATTR ObjectMonitor::EnterI (TRAPS) {          // 尝试自旋    if (TrySpin (Self) &gt; 0) {        ...        return ;    }    ...    // 将线程封装成node节点中    ObjectWaiter node(Self) ;    Self-&gt;_ParkEvent-&gt;reset() ;    node._prev   = (ObjectWaiter *) 0xBAD ;    node.TState  = ObjectWaiter::TS_CXQ ;    // 将node节点插入到_cxq队列的头部，cxq是一个单向链表    ObjectWaiter * nxt ;    for (;;) {        node._next = nxt = _cxq ;        if (Atomic::cmpxchg_ptr (&amp;node, &amp;_cxq, nxt) == nxt) break ;        // CAS失败的话 再尝试获得锁，这样可以降低插入到_cxq队列的频率        if (TryLock (Self) &gt; 0) {            ...            return ;        }    }        ...}</code></pre><p>EnterI 大致原理：一个 <code>ObjectMonitor</code> 对象包括两个同步队列（<code>_cxq</code> 和<code>_EntryList</code>） ，以及一个等待队列<code>_WaitSet</code>。cxq、EntryList 、WaitSet 都是由 ObjectWaiter 构成的链表结构。其中，<code>_cxq</code> 为单向链表，<code>_EntryList</code> 为双向链表。</p><p><img src="https://tech.youzan.com/content/images/2021/06/---------.svg" alt="同步队列、阻塞队列" /></p><p>当一个线程尝试获得重量级锁且没有竞争到时，该线程会被封装成一个 <code>ObjectWaiter</code> 对象插入到 cxq 的队列的队首，然后调用 <code>park</code> 函数挂起当前线程，进入 BLOCKED 状态。当线程释放锁时，会根据唤醒策略，从 cxq 或 EntryList 中挑选一个线程 <code>unpark</code> 唤醒。如果线程获得锁后调用 <code>Object#wait</code> 方法，则会将线程加入到 WaitSet 中，进入 WAITING 或 TIMED_WAITING 状态。当被 <code>Object#notify</code> 唤醒后，会将线程从 WaitSet 移动到 cxq 或 EntryList 中去，进入 BLOCKED 状态。需要注意的是，当调用一个锁对象的 <code>wait</code> 或 <code>notify</code> 方法时，若当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。</p><h3 id="254-monitor-释放">2.5.4 monitor 释放</h3><p>当某个持有锁的线程执行完同步代码块时，会进行锁的释放。在 HotSpot 中，通过退出 monitor 的方式实现锁的释放，并通知被阻塞的线程，具体实现位于 <code>ObjectMonitor::exit</code> 方法中。</p><pre><code>void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {     Thread * Self = THREAD ;   // 如果_owner不是当前线程   if (THREAD != _owner) {     // 轻量级锁膨胀上来，还没调用过enter方法，_owner还指向之前轻量级锁Lock Record的指针。     if (THREAD-&gt;is_lock_owned((address) _owner)) {       assert (_recursions == 0, &quot;invariant&quot;) ;       _owner = THREAD ;       _recursions = 0 ;       OwnerIsThread = 1 ;     } else {       // 异常情况:当前不是持有锁的线程       TEVENT (Exit - Throw IMSX) ;       assert(false, &quot;Non-balanced monitor enter/exit!&quot;);       if (false) {          THROW(vmSymbols::java_lang_IllegalMonitorStateException());       }       return;     }   }   // 重入计数器还不为0，则计数器-1后返回   if (_recursions != 0) {     _recursions--;        // this is simple recursive enter     TEVENT (Inflated exit - recursive) ;     return ;   }   ...   //这块开始是唤醒操作   for (;;) {     ...     ...     ObjectWaiter * w = NULL ;     // 根据QMode的不同会有不同的唤醒策略，默认为0     int QMode = Knob_QMode ;     if (QMode == 2 &amp;&amp; _cxq != NULL) {          ...          ...</code></pre><p><code>步骤 1</code>、处理 owner 不是当前线程的状况。这里特指之前持有轻量级锁的线程，由于没有调用过 enter，owner 指向仍为 Lock Record 的指针，以及其他异常情况。</p><p><code>步骤 2</code>、重入计数器还不为 0，则计数器 - 1 后返回。</p><p><code>步骤 3</code>、唤醒操作。根据不同的策略（由 QMode 指定），从 cxq 或 EntryList 中获取头节点，通过 <code>ObjectMonitor::ExitEpilog</code> 方法唤醒该节点封装的线程，唤醒操作最终由 unpark 完成。</p><p>根据 QMode 的不同 (默认为 0)，有不同的处理方式：</p><p>QMode = 0：暂时什么都不做；<br />QMode = 2 且 cxq 非空：取 cxq 队列队首的 ObjectWaiter 对象，调用 ExitEpilog 方法，该方法会唤醒 ObjectWaiter 对象的线程，然后立即返回，后面的代码不会执行了；<br />QMode = 3 且 cxq 非空：把 cxq 队列插入到 EntryList 的尾部；<br />QMode = 4 且 cxq 非空：把 cxq 队列插入到 EntryList 的头部；</p><p>只有 QMode=2 的时候会提前返回，等于 0、3、4 的时继续执行：</p><p>1. 如果 EntryList 的首元素非空，就取出来调用 ExitEpilog 方法，该方法会唤醒 ObjectWaiter 对象的线程，然后立即返回；<br />2. 如果 EntryList 的首元素为空，就将 cxq 的所有元素放入到 EntryList 中，然后再从 EntryList 中取出来队首元素执行 ExitEpilog 方法，然后立即返回；<br />3. 被唤醒的线程，继续竞争 monitor；</p><h3 id="26-本章小节">2.6 本章小节</h3><p>本章介绍了 Synchronized 的底层实现和锁升级过程。对于锁升级，再看看本文整理的图，一图胜千言：</p><h6></h6><p><img src="https://tech.youzan.com/content/images/2021/06/-----3.svg" alt="重量级锁" /></p><p>这里有几个点可以注意一下:</p><p>1.HotSpot 中，只用到了<strong>模板解释器</strong>，并没有用到字节码解释器，<code>monitorenter</code> 的实际入口位于 <a href="http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/cpu/x86/vm/templateTable_x86_64.cpp#l3667">templateTable_x86_64.cpp#3667</a>。本文的分析是基于字节码解释器的，因此部分结论不能作为实际执行情况。本章的内容只能作为 Synchronized 锁升级原理、各类锁的适用场景的一种 <strong>窥探</strong>。<br />2. 再次强调，无锁状态只能升级为轻量级锁，<strong>匿名偏向状态</strong>才能进入到偏向锁。<br />3. 偏向锁<strong>并不都有利，<strong>其适用于</strong>单个线程重入</strong>的场景，原因为：偏向锁的撤销需要进入 <code>safepoint</code>，开销较大。需要进入 <code>safepoint</code> 是由于，偏向锁的撤销需要对锁对象的 <code>lock record</code> 进行操作，而 <code>lock record</code> 要到其他线程的栈帧中遍历寻找。在非 safepoint，栈帧是动态的，会引入更多的问题。目前看来，偏向锁存在的价值是为历史遗留的 Collection 类如 Vector 和 HashTable 等做优化，迟早药丸。Java 15 中默认不开启。<br />4. 执行 Object 类的 <code>hashcode</code> 方法，偏向锁撤销并且锁会膨胀为轻量级锁或者重量锁。执行 Object 类的 <code>wait/notify/notifyall</code> 方法，偏向锁撤销并膨胀成重量级锁。<br />5. 轻量级锁适用于<strong>两个线程的交替执行</strong>场景：线程 A 进入轻量级锁，退出同步代码块并释放锁，会将锁对象恢复为无锁状态；线程 B 再进入锁，发现为无锁状态，会 cas 尝试获取该锁对象的轻量级锁。如果有竞争，则直接膨胀为重量级锁，没有自旋操作，详情看 10。<br />6. 唤醒策略依赖于 <strong>QMode</strong>。重量级锁获取失败后，线程会加入 cxq 队列。当线程释放锁时，会从 cxq 或 EntryList 中挑选一个线程唤醒。线程获得锁后调用 <code>Object#wait</code> 方法，则会将线程加入到 WaitSet 中。当被 <code>Object#notify</code> 唤醒后，会将线程从 WaitSet 移动到 cxq 或 EntryList 中去。<br />7. 重量级锁，会将线程放进等待队列，等待操作系统调度。而偏向锁和轻量级锁，未交由操作系统调度，依然处于用户态，只是采用 CAS 无锁竞争的方式获取锁。CAS 通过 Unsafe 类中 compareAndSwap 方法，jni 调用 C++ 方法，通过汇编指令锁住 cpu 中的北桥信号。<br />8. 许多文章声称一个对象关联到一个 monitor，这个说法不够准确。如果对象已经是重量级锁了，对象头的确指向了一个 <code>monitor</code>。但对于正在膨胀的锁，会先从<strong>线程私有</strong>的 <code>monitor</code> 集合 <code>omFreeList</code> 中分配对象。如果 <code>omFreeList</code> 中已经没有 <code>monitor</code> 对象，再从 <strong>JVM 全局</strong>的 <code>gFreeList</code> 中分配一批 <code>monitor</code> 到 <code>omFreeList</code> 中。<br />9. 在编译期间还有<strong>锁消除</strong>和<strong>锁粗化</strong>这两步锁优化操作，本章没做介绍。<br />10. <strong>字节码实现中没有体现轻量级锁自旋逻辑</strong>。这可能是模板解释器中的实现，或者是 jvm 在不同平台、不同 jvm 版本的不同实现。但本文分析的字节码链路中没有发现该逻辑，倒是发现了<strong>重量级锁会自适应自旋竞争锁</strong>。因此个人对轻量级锁自适应自旋的说法存疑，至少 hotspot jdk8u 字节码实现中没有这个逻辑。但两者都是在用户态进行自适应自旋，以尽可能减少同步操作带来的开销，没有太多本质上的区别，并不需要特别关心。</p><h2 id="三线程的实现与状态转换">三、线程的实现与状态转换</h2><h3 id="31-线程的实现">3.1 线程的实现</h3><p>（1）内核线程实现</p><p><strong>内核线程</strong>（Kernel-Level Thread，KLT）：由<strong>内核</strong>来完成线程切换，内核通过<strong>调度器</strong>对线程进行调度，并负责将线程的任务<strong>映射</strong>到各个处理器上。 程序一般不会直接去使用内核线程，而是使用内核线程的一种高级接口 —— <strong>轻量级进程</strong>（Light Weight Process，LWP），也就是通常意义上的线程。</p><p><strong>优点</strong>：每个 LWP 都是独立的调度单元。一个 LWP 被阻塞，不影响其他 LWP。</p><p><strong>缺点</strong>：基于 KLT，耗资源。线程的创建、析构、同步都需要进行系统调用，频繁的用户态、内核态切换。</p><p><img src="https://tech.youzan.com/content/images/2021/06/-----4.svg" alt="内核线程" /></p><p>(2) 用户线程实现（User Thread，UT）</p><p><strong>广义：非内核线程</strong>，都可认为是用户线程。（包括 LWT，虽然 LWT 的大多操作都要映射到 KLT）</p><p><strong>狭义</strong>：完全建立在<strong>用户空间</strong>的线程库上，系统内核不能感知线程存在的实现。UT 也只感知到掌管这些 UT 的进程 P。</p><p><strong>优点</strong>：用户线程的创建、同步、销毁和调度完全在用户态中完成，不需要内核的帮助。</p><p><strong>缺点</strong>：线程的创建、销毁、切换和调度都是用户必须考虑到问题。“阻塞如何处理”、“多处理器系统中如何将线程映射到其他处理器上” 这类问题解决起来将会异常困难。</p><p><img src="https://tech.youzan.com/content/images/2021/07/----2.svg" alt="内核线程2" /></p><p>（3) 混合实现 混合模式下，<strong>即存在用户线程，也存在轻量级进程</strong>。用户线程的创建、切换、析构等操作依然廉价，可以支持大规模的用户线程并发，且可以使用内核线程提供的线程调度功能及处理器映射。</p><p><img src="https://tech.youzan.com/content/images/2021/06/----3-1.svg" alt="内核线程3" /></p><p>线程的实现依赖操作系统支持的线程模型。在主流的操作系统上，hotspot、classic、art 等虚拟机默认是 1:1 的线程模型。在 Solaris 平台上，hotspot 支持 1:1、N：M 两种线程模型。</p><h3 id="32-线程的转换">3.2 线程的转换</h3><p>首先明确一点，当我们讨论一个线程的状态，指的是 Thread 类中 threadStatus 的值。</p><pre><code>private volatile int threadStatus = 0;  </code></pre><p>该值映射后对应的枚举为：</p><pre><code>public enum State {      NEW,    RUNNABLE,    BLOCKED,    WAITING,    TIMED_WAITING,    TERMINATED;}</code></pre><p>也就是说，线程的具体状态，看 threadStatus 就行了。</p><p><strong>NEW</strong></p><p>先要创建 Thread 类的对象，才能谈其状态。</p><pre><code>Thread t = new Thread();  </code></pre><p>这个时候，线程 t 就处于新建状态。但他还不是 “线程”。</p><p><strong>RUNNABLE</strong></p><p>然后调用 start () 方法。</p><pre><code>t.start();  </code></pre><p>调用 start () 后，会执行一个 native 方法创建内核线程，以 linux 为例：</p><pre><code>private native void start0();// 最后走到这hotspot/src/os/linux/vm/os_linux.cpp  pthread_create(...);  </code></pre><p>这时候才有一个真正的线程创建出来，并即刻开始运行。这个内核线程与线程 t 进行 1：1 的映射。这时候 t 具备运行能力，进入 RUNNABLE 状态。 RUNNABLE 可以细分为 READY 和 RUNNING，两者的区别只是是否等待到了资源并开始运行。<br />处于 RUNNABLE 且未运行的线程，会进入一个就绪队列中，等待操作系统的调度。处于就绪队列的线程都在等待资源，这个资源可以是 cpu 的时间片、也可以是系统的 IO。 JVM 并不关系 READY 和 RUNNING 这两种状态，毕竟上述的枚举类都不对 RUNNABLE 进行细分。</p><p><strong>TERMINATED</strong></p><p>当一个线程执行完毕（或者调用已经不建议的 stop 方法），线程的状态就变为 TERMINATED。进入 TERMINATED 后，线程的状态不可逆，无法再复活。</p><p><strong>关于 BLOCKED、WAITING、TIMED_WAITING</strong></p><p>BLOCKED、WAITING、TIMED_WAITING 都是带有同步语义的状态，我们先看一下 <code>wait</code> 和 <code>notify</code> 方法的底层实现。<br />wait 方法的底层实现:</p><pre><code>void ObjectSynchronizer::wait(Handle obj, jlong millis, TRAPS) {      ...    ...  //获得Object的monitor对象  ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj());  DTRACE_MONITOR_WAIT_PROBE(monitor, obj(), THREAD, millis);  //调用monitor的wait方法  monitor-&gt;wait(millis, true, THREAD);    ...}  inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) {    ...  if (_WaitSet == NULL) {    //_WaitSet为null，就初始化_waitSet    _WaitSet = node;    node-&gt;_prev = node;    node-&gt;_next = node;  } else {    //否则就尾插    ObjectWaiter* head = _WaitSet ;    ObjectWaiter* tail = head-&gt;_prev;    assert(tail-&gt;_next == head, &quot;invariant check&quot;);    tail-&gt;_next = node;    head-&gt;_prev = node;    node-&gt;_next = head;    node-&gt;_prev = tail;  }}</code></pre><p>主要流程：通过 object 获得 objectMonitor，将 Thread 封装成 OjectWaiter 对象，然后 <code>addWaiter</code> 将它插入 <code>waitSet</code> 中，进入 waiting 或 timed_waiting 状态。最后释放锁，并通过底层的 <code>park</code> 方法挂起线程；</p><p>notify 方法的底层实现：</p><pre><code>void ObjectSynchronizer::notify(Handle obj, TRAPS) {      ...    ...    ObjectSynchronizer::inflate(THREAD, obj())-&gt;notify(THREAD);}    //通过inflate方法得到ObjectMonitor对象    ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {    ...     if (mark-&gt;has_monitor()) {          ObjectMonitor * inf = mark-&gt;monitor() ;          assert (inf-&gt;header()-&gt;is_neutral(), &quot;invariant&quot;);          assert (inf-&gt;object() == object, &quot;invariant&quot;) ;          assert (ObjectSynchronizer::verify_objmon_isinpool(inf), &quot;monitor is inva;lid&quot;);          return inf       }    ...      }    //调用ObjectMonitor的notify方法    void ObjectMonitor::notify(TRAPS) {    ...    //调用DequeueWaiter方法移出_waiterSet第一个结点    ObjectWaiter * iterator = DequeueWaiter() ;    //将上面DequeueWaiter尾插入_EntrySet或cxq等操作    ...    ...  }</code></pre><p>通过 object 获得 objectMonitor，调用 objectMonitor 的 <code>notify</code> 方法。这个 notify 最后会走到 <code>ObjectMonitor::DequeueWaiter</code> 方法，获取 waitSet 列表中的第一个 ObjectWaiter 节点。并根据不同的策略，将取出来的 ObjectWaiter 节点，加入到 <code>EntryList</code> 或 <code>cxq</code> 中。 <code>notifyAll</code> 的实现类似于 <code>notify</code>，主要差别在多了个 for 循环。</p><p>由这里以及上一章 2.5.4 monitor 释放小节中可以了解到，<code>notify</code> 和 <code>notifyAll</code> 并不会立即释放所占有的 ObjectMonitor 对象，其真正释放 ObjectMonitor 的时间点是在执行 <code>monitorexit</code> 指令。</p><p>一旦释放 <code>ObjectMonitor</code> 对象了，<code>entryList</code> 和 <code>cxq</code> 中的 ObjectWaiter 节点会依据 <code>QMode</code> 所配置的策略，通过 ExitEpilog 方法唤醒取出来的 ObjectWaiter 节点。被唤醒的线程，继续参与 monitor 的竞争。若竞争失败，重新进入 BLOCKED 状态，再回顾一下 monitor 的核心结构。</p><p><img src="https://tech.youzan.com/content/images/2021/06/---------.svg" alt="同步队列、阻塞队列" /></p><p>既然聊到了 <code>wait</code> 和 <code>notify</code>，那顺便也看下 <code>join</code>、<code>sleep</code> 和 <code>park</code>。</p><p>打开 Thread.join () 的源码：</p><pre><code>public final synchronized void join(long millis) throws InterruptedException {      ...  if (millis == 0) {    while (isAlive()) {      wait(0);    }  } else {    while (isAlive()) {      long delay = millis - now;      if (delay &lt;= 0) {        break;      }      wait(delay);      now = System.currentTimeMillis() - base;    }  }}</code></pre><p><code>join</code> 的本质仍然是 <code>wait()</code> 方法。在使用 <code>join</code> 时，JVM 会帮我们隐式调用 <code>notify</code>，因此我们不需要主动 notify 唤醒主线程。 而 <code>sleep()</code> 方法最终是调用 <code>SleepEvent</code> 对象的 park 方法：</p><pre><code>int os::sleep(Thread* thread, jlong millis, bool interruptible) {    //获取thread中的_SleepEvent对象  ParkEvent * const slp = thread-&gt;_SleepEvent ;  ...  //如果是允许被打断  if (interruptible) {    //记录下当前时间戳，这是时间比较的基准    jlong prevtime = javaTimeNanos();    for (;;) {      //检查打断标记，如果打断标记为true，则直接返回      if (os::is_interrupted(thread, true)) {        return OS_INTRPT;      }      //线程被唤醒后的当前时间戳      jlong newtime = javaTimeNanos();      //睡眠毫秒数减去当前已经经过的毫秒数      millis -= (newtime - prevtime) / NANOSECS_PER_MILLISEC;      //如果小于0，那么说明已经睡眠了足够多的时间，直接返回      if (millis &lt;= 0) {        return OS_OK;      }      //更新基准时间      prevtime = newtime;      //调用_SleepEvent对象的park方法，阻塞线程      slp-&gt;park(millis);    }  } else {    //如果不能打断，除了不再返回OS_INTRPT以外，逻辑是完全相同的    for (;;) {      ...      slp-&gt;park(millis);      ...    }    return OS_OK ;  }}</code></pre><p><code>Thread.sleep</code> 在 jvm 层面上是调用 thread 中 <code>SleepEvent</code> 对象的 <code>park()</code> 方法实现阻塞线程，在此过程中会通过判断时间戳来决定线程的睡眠时间是否达到了指定的毫秒。看到这里，对于 <code>sleep</code> 和 <code>wait</code> 的区别应该会有更深入的理解。</p><p><code>park</code>、<code>unpark</code> 方法也与同步语义无关。每个线程都与一个许可 (permit) 关联。<code>unpark</code> 函数为线程提供 permit，线程调用 <code>park</code> 函数则等待并消耗 permit。park 和 unpark 方法具体实现比较复杂，这里不展开。 到此为止，我们可以整理出如下的线程状态转换图。</p><p><img src="https://tech.youzan.com/content/images/2021/06/-----5.svg" alt="内核线程3" /></p><h3 id="33-本章小节">3.3 本章小节</h3><p>Java 将 OS 经典五种状态中的 ready 和 running，统一为 RUNNABLE。将 WAITING（即不可能得到 CPU 运行机会的状态）细分为了 BLOCKED、WAITING、TIMED_WAITING。本章的内容较为简短，因为部分的内容已囊括在第一章中。</p><p>这里提个会使人困惑的问题： 使用 socket 时，调用 accept ()，read () 等阻塞方法时，线程处于什么状态？</p><p>答案是 java 线程处于 RUNNABLE 状态，OS 线程处于 WAITING 状态。因为在 jvm 层面，等待 cpu 时间片和等待 io 资源是等价的。</p><p>这里有几个点可以注意一下：</p><p>1.JVM 线程状态不代表内核线程状态。<br />2.BLOCKED 的线程一定处于 entryList 或 cxq 中，而处于 WAITING 和 TIMED WAITING 的线程，可能是由于执行了 sleep 或 park 进入该状态，不一定在 waitSet 中。也就是说，处于 BLOCKED 状态的线程一定是与同步相关。由这可延伸出，调用 jdk 的 lock 并获取不到锁的线程，进入的是 WAITING 或 TIMED_WAITING 状态，而不是 BLOCKED 状态。</p><h2 id="四相关资料">四、相关资料</h2><p>本文主要参考资料：</p><p><a href="https://github.com/farmerjohngit/myblog/issues/12">死磕 Synchronized 底层实现 -- 概论</a></p><p><a href="https://github.com/farmerjohngit/myblog/issues/13">死磕 Synchronized 底层实现 -- 偏向锁</a></p><p><a href="https://github.com/farmerjohngit/myblog/issues/14">死磕 Synchronized 底层实现 -- 轻量级锁</a></p><p><a href="https://github.com/farmerjohngit/myblog/issues/15">死磕 Synchronized 底层实现 -- 重量级锁</a></p><p>本文转自<a href="https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/">有赞技术</a></p>]]>
                    </description>
                    <pubDate>Thu, 03 Nov 2022 16:16:38 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[大出血！剁手！]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/macbookpro-m1pro</link>
                    <description>
                            <![CDATA[<p><img src="https://notes.suremotoo.cc/upload/2022/07/Macbook%20Pro%20Mockup%204k%20freebie-ab7e66a923bf4b1381563304d27293c8.png" alt="Macbook Pro Mockup 4k freebie" /></p><h2 id="%E8%BE%97%E8%BD%AC%E5%8F%8D%E4%BE%A7%E4%B9%8B%E8%B7%AF" tabindex="-1">辗转反侧之路</h2><p>没错，我换了新的笔记本😓</p><p>我之前一直使用得  <strong>2015 年中期那款 15 寸 i7 16G 内存，256G 的硬盘</strong>！别看它有点年限了，但是它依然劲头十足，老当益壮！</p><p>最让我难受的就是硬盘空间不够，其实说实话，如果不是为了工作方便点，这个 256G 还是够用的。</p><p>系统➕一堆软件➕ maven 仓库➕微信聊天记录➕公司资料文件，就占得满满当当了～</p><p>像 IDEA、WPS、Microsoft Office  等等这种大型软件下来，十几 G 就没了；微信记录是由于工作需要，有时候经常会翻历史记录，私人的记录删了就删了，工作的记录那叫留痕，懂得都懂～</p><p>maven 本地仓库，大大小小也好几个 G，有时候自己折腾点其他项目，还有前端的一些小东西，一个项目 <strong>node_modules</strong>  几百 M，多来几个，也上 G～</p><p>尤其是公司的资料，虽说用了 SVN 这种版本管理工具，但是真大，每次项目上线，都需要备份，它就有 <strong>40 多个 G</strong> ❗️❗️</p><p>之所以会把这些文件都 download 本地，也是为了有时候用的方便，也偶尔会在家里排查个问题，看看代码，看看 SVN 里的资料、文档等，不至于到时候想找当时的东西发现却没有。</p><p>还有就是经常逛一些博客、网站，特效稍微多了点，明显就能感受到那个卡、掉帧，不过这到不影响工作 🥲🥲</p><p>种种因素吧，想过换硬盘，我这款是最后一款支持可以更换的，需要转接头一堆，怕换了性能差劲，不如不换，又怕换了，出了故障，我就这一个吃饭的家伙，想想还是别乱折腾了。</p><p>然后换电脑的计划就出来了，我是计划着到时候要换，肯定换当时的高配，也只是计划着，具体日子也没定。</p><p>恰巧，之前老妹就一直给我说，她那个旧电脑已经不行了！面试都经常黑屏死机，面试官都觉得她作弊😄😄</p><p>老妹需要换，我也需要换；我的不够用，给她绰绰有余，那我买个新的，旧的给她。</p><p>老妹还说发了奖学金补助我呢🥹🥹</p><p>本来打算今年年底，等她发了奖金，我就换~ 可是开学季教育优惠来了呀，还送耳机 AirPods ！</p><p>想了许久，在开学季 7.14 刚开始，我就下单了~</p><p><strong>16 寸 M1 Pro 32G 内存 1TB 硬盘，算是 M1 Pro 的顶配了</strong>，硬盘没必要再大了~ 64G 只能给 M1 Max，奈何用不到 Max， M1 Pro 16 其实都够用，为了能提高效率以及多用几年，上 32G ~</p><p><img src="https://notes.suremotoo.cc/upload/2022/07/M1%20Pro%20Introducetion-0ccee9dc8f03433a81a6573bda548605.png" alt="M1 Pro Introducetion" /></p><h2 id="%E5%BC%80%E7%AE%B1" tabindex="-1">开箱</h2><p>emmm，没了！</p><p>T*D，用手机录了开箱视频，手机空间不够了，我就赶紧隔空投送到电脑，紧接着就把手机里的视频删了，彻底删除了那种。</p><p>最后我在电脑上打开一看，我去年买了个表，怎么是个图片！传错了。。。。。。</p><p>So，没开箱了视频了。</p><p>对了，有一个取快递的照片，那个快递柜刚刚好，感觉就是定制的一般</p><p><img src="https://notes.suremotoo.cc/upload/2022/07/M1%20Pro%2016%20%E5%BF%AB%E9%80%92-032611cc1d734d09bbd74f8855efc238.jpeg" alt="M1 Pro 16 快递" /></p><h2 id="%E7%9C%9F%E5%AE%9E%E4%BD%93%E9%AA%8C" tabindex="-1">真实体验</h2><p>爽！太爽了！</p><p>流畅，真流畅！</p><p>120Hz 刷新率，看了直接回不去！</p><h2 id="%E6%95%88%E6%9E%9C%E5%9B%BE" tabindex="-1">效果图</h2><p><img src="https://notes.suremotoo.cc/upload/2022/07/M1%20Pro%2016%20%E4%B8%8E%2015%E5%AF%B9%E6%AF%94-2-c93afdd3917f4f4a92a4e693e2b9e664.jpeg" alt="M1 Pro 16 与 15对比-2" /></p><p><img src="https://notes.suremotoo.cc/upload/2022/07/M1%20Pro%2016%20%E4%B8%8E%2015%E5%AF%B9%E6%AF%94-3-5a788b947918488ba1f865179dc3d702.jpeg" alt="M1 Pro 16 与 15对比-3" /></p>]]>
                    </description>
                    <pubDate>Thu, 28 Jul 2022 23:50:19 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Cron 表达式介绍]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/cron-expression</link>
                    <description>
                            <![CDATA[<h1 id="cron-表达式介绍">Cron 表达式介绍</h1><p>Cron 表达式是一个具有时间含义的字符串，字符串以 5~6 个空格隔开，分为 6~7 个域，格式为<code>X X X X X X X</code>。 其中<code>X</code>是一个域的占位符。最后一个代表年份的域非必须，可省略。单个域有多个取值时，使用半角逗号<code>,</code>隔开取值。每个域可以是确定的取值，也可以是具有逻辑意义的特殊字符。每个域最多支持一个前导零。</p><blockquote><p><strong>说明</strong> 如指定 2022 年每天上午 8:15 执行任务，Cron 表达式可指定为<code>0 15 8 ? * * 2022</code> 或 <code>0 15 08 ? * * 2022</code> ，而不能指定为 <code>0 15 008 ? * * 2022</code>。</p></blockquote><h2 id="域取值">域取值</h2><p>下表为 Cron 表达式中六个域能够取的值以及支持的特殊字符。</p><table><thead><tr><th>域</th><th>是否必须</th><th align="left">取值范围</th><th align="left">特殊字符</th></tr></thead><tbody><tr><td>秒</td><td>是</td><td align="left">[0, 59]</td><td align="left">* , - /</td></tr><tr><td>分钟</td><td>是</td><td align="left">[0, 59]</td><td align="left">* , - /</td></tr><tr><td>小时</td><td>是</td><td align="left">[0, 23]</td><td align="left">* , - /</td></tr><tr><td>日期</td><td>是</td><td align="left">[1, 31]</td><td align="left">* , - / ? L W</td></tr><tr><td>月份</td><td>是</td><td align="left">[1, 12] 或 [JAN, DEC]</td><td align="left">* , - /</td></tr><tr><td>星期</td><td>是</td><td align="left">[1, 7] 或 [MON, SUN]。<br/>若您使用 [1, 7] 表达方式，1代表星期一，7代表星期日。</td><td align="left">* , - / ? L #</td></tr><tr><td>年</td><td>否</td><td align="left">[当前年份，2099]</td><td align="left">* , - /</td></tr></tbody></table><h2 id="特殊字符">特殊字符</h2><p>Cron 表达式中的每个域都支持一定数量的特殊字符，每个特殊字符有其特殊含义。</p><table><thead><tr><th>特殊字符</th><th align="left">含义</th><th>示例</th></tr></thead><tbody><tr><td>*</td><td align="left">所有可能的值。</td><td>在月域中，*表示每个月；在星期域中，*表示星期的每一天。</td></tr><tr><td>,</td><td align="left">列出枚举值。</td><td>在分钟域中，<strong>5,20</strong> 表示分别在 5 分钟和 20 分钟触发一次。</td></tr><tr><td>-</td><td align="left">范围。</td><td>在分钟域中，<strong>5-20</strong> 表示从  5 分钟到 20 分钟之间每隔一分钟触发一次。</td></tr><tr><td>/</td><td align="left">指定数值的增量。</td><td>在分钟域中，<strong>0/15</strong> 表示从第  0 分钟开始，每 15 分钟。在分钟域中 <strong>3/20</strong> 表示从第 3 分钟开始，每 20 分钟。</td></tr><tr><td>?</td><td align="left">不指定值，仅日期和星期域支持该字符。</td><td>当日期或星期域其中之一被指定了值以后，为了避免冲突，需要将另一个域的值设为 <strong>?</strong> 。</td></tr><tr><td>L</td><td align="left">单词 <strong>Last</strong> 的首字母，表示最后一天，仅日期和星期域支持该字符。<br>说明 指定 <strong>L</strong> 字符时，避免指定列表或者范围，否则，会导致逻辑问题。</td><td>在日期域中，<strong>L</strong> 表示某个月的最后一天。在星期域中，<strong>L</strong>表示一个星期的最后一天，也就是星期日（SUN）。<br>如果在 <strong>L</strong> 前有具体的内容，例如，在星期域中的 <strong>6L</strong> 表示这个月的最后一个星期六。</td></tr><tr><td>W</td><td align="left">除周末以外的有效工作日，在离指定日期的最近的有效工作日触发事件。<strong>W</strong> 字符寻找最近有效工作日时不会跨过当前月份，连用字符 <strong>LW</strong> 时表示为指定月份的最后一个工作日。</td><td>在日期域中 <strong>5W</strong>，如果 5 日是星期六，则将在最近的工作日星期五，即 4 日触发。如果 5 日是星期天，则将在最近的工作日星期一，即 6 日触发；如果 5 日在星期一到星期五中的一天，则就在 5 日触发。</td></tr><tr><td>#</td><td align="left">确定每个月第几个星期几，仅星期域支持该字符。</td><td>在星期域中，<strong>4#2</strong> 表示某月的第二个星期四。</td></tr></tbody></table><h2 id="取值示例">取值示例</h2><p>以下为 Cron 表达式的取值示例。</p><table><thead><tr><th align="left">示例</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">0 15 10 ? * *</td><td align="left">每天上午 10:15 执行任务</td></tr><tr><td align="left">0 15 10 * * ?</td><td align="left">每天上午 10:15 执行任务</td></tr><tr><td align="left">0 0 12 * * ?</td><td align="left">每天中午 12:00 执行任务</td></tr><tr><td align="left">0 0 10,14,16 * * ?</td><td align="left">每天上午 10:00 点、下午 14:00 以及下午 16:00 执行任务</td></tr><tr><td align="left">0 0/30 9-17 * * ?</td><td align="left">每天上午 09:00 到下午 17:00 时间段内每隔半小时执行任务</td></tr><tr><td align="left">0 * 14 * * ?</td><td align="left">每天下午 14:00 到下午 14:59 时间段内每隔 1 分钟执行任务</td></tr><tr><td align="left">0 0-5 14 * * ?</td><td align="left">每天下午 14:00 到下午 14:05 时间段内每隔 1 分钟执行任务</td></tr><tr><td align="left">0 0/5 14 * * ?</td><td align="left">每天下午 14:00 到下午 14:55 时间段内每隔 5 分钟执行任务</td></tr><tr><td align="left">0 0/5 14,18 * * ?</td><td align="left">每天下午 14:00 到下午 14:55、下午 18:00 到下午 18:55  时间段内每隔 5 分钟执行任务</td></tr><tr><td align="left">0 0 12 ? * WED</td><td align="left">每个星期三中午 12:00 执行任务</td></tr><tr><td align="left">0 15 10 15 * ?</td><td align="left">每月 15 日上午 10:15 执行任务</td></tr><tr><td align="left">0 15 10 L * ?</td><td align="left">每月最后一日上午 10:15 执行任务</td></tr><tr><td align="left">0 15 10 ? * 6L</td><td align="left">每月最后一个星期六上午 10:15 执行任务</td></tr><tr><td align="left">0 15 10 ? * 6#3</td><td align="left">每月第三个星期六上午 10:15 执行任务</td></tr><tr><td align="left">0 10,44 14 ? 3 WED</td><td align="left">每年 3 月的每个星期三下午 14:10 和 14:44 执行任务</td></tr><tr><td align="left">0 15 10 ? * * 2022</td><td align="left">2022 年每天上午 10:15 执行任务</td></tr><tr><td align="left">0 15 10 ? * * *</td><td align="left">每年每天上午 10:15 执行任务</td></tr><tr><td align="left">0 0/5 14,18 * * ? 2022</td><td align="left">2022 年每天下午 14:00 到下午 14:55、下午 18:00 到下午 18:55 时间段内每隔 5 分钟执行任务</td></tr><tr><td align="left">0 15 10 ? * 6#3 2022,2023</td><td align="left">2022 年至 2023 年每月第三个星期六上午 10:15 执行任务</td></tr><tr><td align="left">0 0/30 9-17 * * ? 2022-2025</td><td align="left">2022 年至 2025 年每天上午 09:00 到下午 17:30 时间段内每隔半小时执行任务</td></tr><tr><td align="left">0 10,44 14 ? 3 WED 2022/2</td><td align="left">从 2022 年开始，每隔两年 3 月的每个星期三下午 14:10 和 14:44 执行任务</td></tr></tbody></table><p>本文摘自<a href="https://help.aliyun.com/document_detail/64769.html?spm=a2c4g.11174399.0.0.b97427e1nBFHwf">阿里云</a></p>]]>
                    </description>
                    <pubDate>Thu, 23 Jun 2022 17:11:28 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Spring Cloud Gateway 限流适配多规则的解决方案]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/springcloudgateway-diy-ratelimiter</link>
                    <description>
                            <![CDATA[<h1 id="spring-cloud-gateway-%E9%99%90%E6%B5%81%E9%80%82%E9%85%8D%E5%A4%9A%E8%A7%84%E5%88%99%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88" tabindex="-1">Spring Cloud Gateway 限流适配多规则的解决方案</h1><blockquote><h3 id="%E9%A6%96%E5%85%88%E8%A6%81%E8%AF%B4%E6%98%8E%EF%BC%8C%E6%9C%AC%E6%96%87%E6%98%AF%E4%BD%BF%E7%94%A8%E7%9A%84-spring-cloud-gateway-%E8%87%AA%E5%B8%A6%E7%9A%84%E6%88%96%E8%80%85%E7%A7%B0%E5%8E%9F%E7%94%9F%E7%9A%84-redis-%E9%99%90%E6%B5%81%EF%BC%81" tabindex="-1"><u>首先要说明</u>，本文是使用的 <strong>Spring Cloud Gateway 自带的或者称原生的 Redis 限流！</strong></h3></blockquote><h2 id="%E8%83%8C%E6%99%AF" tabindex="-1">背景</h2><p>限流作用就不说了，往往都是防止一些恶意请求，无限制请求接口导致服务处理时间过长，继而导致响应延迟，服务阻塞等等，所以会对高频率的一些接口添加限流这样的功能。</p><p>通常，我们往往是针对 1 个路由或者说是对 1 个接口进行限流，限流的规则通常是：<strong>XXX 路由 XXX 在 XXX 时间内最多允许访问 XXX 次</strong>。</p><p>比如：查询用户信息接口 <em>[路由]</em> 每个用户 <em>[条件]</em> 每秒 <em>[频率时间]</em> 最多支持访问 10 次 <em>[频率最大限制]</em>。</p><p>举个明白点的例子，我 1 秒内连续请求 11 次 <strong>[查询用户信息接口]</strong>，那么第 11 次就应该被拦截，提示请求频繁，相信大家在一些双 11 这样的节日里会遇到过类似情况~</p><p>我们换个规则，再举个例子：<strong>[查询用户信息接口]</strong> 每秒 最多支持访问 100 次~，也就是说不管谁请求，反正 <strong>[查询用户信息接口]</strong> 1 秒内最大支持访问 100 次请求，超过 100 的都会被拦截~</p><p>以上两个例子，都是单独使用，是对 1 个路由指定了 1 个限流的规则，实际业务需求中，可能还需要 2 个或者多个规则同时使用。</p><p>比如：</p><p><strong>[查询用户信息接口]</strong> 每个用户 每秒 最多支持访问 10 次 ，这是规则 1，<em>用来限制单个用户的次数</em>；</p><p>同时，<strong>[查询用户信息接口]</strong> 每秒 最多支持访问 100 次~，这是规则 2，<em>用来限制接口的次数</em>。</p><p>这个我再举个明白点的例子，假如有 10 个人在同 1 秒来请求 <strong>[查询用户信息接口]</strong></p><p>前 8 个人都在 1 秒内请求 10 次，（8 个人每个人都不违反规则 1，接口请求总数也不超过 100 次，接口还可以请求 20 次，不违反规则 2）</p><p>第 9 个人请求 11 次，（达到规则 1 限流条件，这个人第 11 次请求肯定被拦截，接口请求总数不超过 100 次，接口还可以请求 9 次）</p><p>第 10 个人请求 10 次，（不违反规则 1，接口请求为 101 次，总数超过 100 次，达到规则 2 限流条件，所以这个人第 10 次请求肯定被拦截）</p><p>当然请求顺序这都是理想状态，实际场景中顺序会有差别~</p><p>既然了解后，那么现在的问题就是：Spring Cloud Gateway 自带的限流默认 1 个路由（或者说是 1 个接口）只能配置 1 个限流规则！本文就是来解决这种问题，让 1 个路由适配多个规则！🤯</p><blockquote><p>！！！Spring Cloud Gateway 提供了一套限流方案的接口，并且也基于 Redis 实现了一套限流方案，这个也就是本文的要着重分析的点！</p></blockquote><h2 id="spring-cloud-gateway-%E5%A4%A7%E8%87%B4%E6%B5%81%E7%A8%8B%E7%86%9F%E6%82%89" tabindex="-1">Spring Cloud Gateway 大致流程熟悉</h2><h3 id="%E5%A4%A7%E8%87%B4%E6%B5%81%E7%A8%8B%E5%9B%BE" tabindex="-1">大致流程图</h3><p><img src="https://notes.suremotoo.cc/upload/2021/06/SpringCloudGateway-01-632ff879414146b9832075fe54dbdc66.png" alt="SpringCloudGateway-01" /></p><p>具体流程这里就不说了，我直接说本文的涉及的要点。</p><p>当请求进入到网关，网关会根据请求路由来组装对应的过滤器，而我们的限流也是其中的过滤器，Spring Cloud Gateway 自己的实现就是：<strong>RequestRateLimiterGatewayFilterFactory</strong>，所以我们要分析其源码，了解它大致干了什么事，我们才好知道有没有办法调整！</p><h2 id="%E5%B9%B3%E5%B8%B8%E9%85%8D%E7%BD%AE%E4%BD%BF%E7%94%A8%E5%9B%9E%E9%A1%BE" tabindex="-1">平常配置使用回顾</h2><p>分析前，我们先回顾下平常我们配置限流是怎么配置的。</p><p>附： RateLimiterConfig，首先我们定义好限流规则 KeyResolver</p><pre><code class="language-java">/** * Author: Suremotoo */@Configurationpublic class RateLimiterConfig {    @Primary    @Bean(value = &quot;remoteAddrKeyResolver&quot;)    public KeyResolver remoteAddrKeyResolver() {        return exchange -&gt; {            String hostAddress = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();            // log(&quot;remoteAddrKeyResolver 限流规则 ip {}&quot;, hostAddress);            return Mono.just(hostAddress);        };    }    /**     * 按照 Path 限流     *     * @return key     */    @Bean(value = &quot;pathKeyResolver&quot;)    public KeyResolver pathKeyResolver() {        return exchange -&gt; {            Route route = (Route)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);            // log(&quot;pathKeyResolver 限流规则 ip {}&quot;, route.getId());            return Mono.just(exchange.getRequest().getPath().toString());        };    }}</code></pre><p>然后在 application.yml 配置路由的限流， 示例:</p><pre><code class="language-yaml">spring:  cloud:    gateway:      routes:        - id: query_user_info_route          uri: lb://user-center          filters:            - name: RequestRateLimiter              args:                # 令牌桶每秒填充平均速率                redis-rate-limiter.replenishRate: 1                # 令牌桶的上限                redis-rate-limiter.burstCapacity: 10                # 使用 SpEL 表达式从 Spring 容器中获取 Bean 对象,pathKeyResolver 是根据地址来限流                key-resolver: &quot;#{@remoteAddrKeyResolver}&quot; # 详情见 RateLimiterConfig</code></pre><p>可以看到，过滤器 （filters）,我们配置的是 <strong>RequestRateLimiter</strong>，这里的 <strong>RequestRateLimiter</strong> 其实指的就是 <strong>RequestRateLimiterGatewayFilterFactory</strong> ，只是省略了后面的 <strong>GatewayFilterFactory</strong>~</p><p>该过滤器的参数有 <code>redis-rate-limiter、key-resolver</code>,说明这两个其实是很重要的属性！</p><p>1 个限流规则我们配置 1 个过滤器及属性，那么我想再加一个规则，正常情况我们会这样做，示例：</p><pre><code class="language-yaml">spring:  cloud:    gateway:      routes:        - id: query_user_info_route          uri: lb://user-center          filters:          # 第一个限流过滤器和规则            - name: RequestRateLimiter              args:                # 令牌桶每秒填充平均速率                redis-rate-limiter.replenishRate: 1                # 令牌桶的上限                redis-rate-limiter.burstCapacity: 100                # 使用 SpEL 表达式从 Spring 容器中获取 Bean 对象,pathKeyResolver 是根据地址来限流                key-resolver: &quot;#{@pathKeyResolver}&quot; # 详情见 RateLimiterConfig          # 第二个限流过滤器和规则            - name: RequestRateLimiter              args:                # 令牌桶每秒填充平均速率                redis-rate-limiter.replenishRate: 1                # 令牌桶的上限                redis-rate-limiter.burstCapacity: 10                # 使用 SpEL 表达式从 Spring 容器中获取 Bean 对象,remoteAddrKeyResolver 是根据请求 ip 来限流                key-resolver: &quot;#{@remoteAddrKeyResolver}&quot; # 详情见 RateLimiterConfig</code></pre><p>写的时候还洋洋洒洒~</p><p><img src="https://notes.suremotoo.cc/upload/2021/06/skrjlg-44ce612e136247d790eb132a7862b013.jpeg" alt="skrjlg" /></p><p>写完后看上没问题，程序也能跑起来，<strong>但你会发现实际就只有 1 个生效，下面属性的把上面的覆盖了！</strong>，欧了买了噶，~</p><p><img src="https://notes.suremotoo.cc/upload/2021/06/omg-6ed523eefa1f4a2c98270db008d31aa9.jpeg" alt="omg" /></p><p>是配置了两个一样的过滤器，实际运行的时候，也确实都跑了两次这个过滤器，只是每次取的速率什么的，是相同的<sub>，相当于同一个限流规则，校验了两遍</sub></p><p><img src="https://notes.suremotoo.cc/upload/2021/06/%E5%A5%BD%E5%BE%97%E5%BE%88-2b41e00c94734b7bbdf3586b5625b2c0.jpeg" alt="好得很" /></p><p>具体跑起来效果我就不展示了，接下来我们来正儿八经分析下源码，看看什么情况！</p><h2 id="requestratelimitergatewayfilterfactory-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90" tabindex="-1">RequestRateLimiterGatewayFilterFactory 源码分析</h2><blockquote><p>这里仅列出核心代码分析😬</p></blockquote><pre><code class="language-java">public class RequestRateLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory&lt;RequestRateLimiterGatewayFilterFactory.Config&gt; {    public static final String KEY_RESOLVER_KEY = &quot;keyResolver&quot;;    private static final String EMPTY_KEY = &quot;____EMPTY_KEY__&quot;;       /**     * 限流算法及实现     */    private final RateLimiter defaultRateLimiter;       /**     * 限流关键字 key     */    private final KeyResolver defaultKeyResolver;    private boolean denyEmptyKey = true;    private String emptyKeyStatusCode;    public RequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver) {        super(RequestRateLimiterGatewayFilterFactory.Config.class);        this.emptyKeyStatusCode = HttpStatus.FORBIDDEN.name();        this.defaultRateLimiter = defaultRateLimiter;        this.defaultKeyResolver = defaultKeyResolver;    }    public KeyResolver getDefaultKeyResolver() {        return this.defaultKeyResolver;    }    public RateLimiter getDefaultRateLimiter() {        return this.defaultRateLimiter;    }    public boolean isDenyEmptyKey() {        return this.denyEmptyKey;    }    public void setDenyEmptyKey(boolean denyEmptyKey) {        this.denyEmptyKey = denyEmptyKey;    }    public String getEmptyKeyStatusCode() {        return this.emptyKeyStatusCode;    }    public void setEmptyKeyStatusCode(String emptyKeyStatusCode) {        this.emptyKeyStatusCode = emptyKeyStatusCode;    }    public GatewayFilter apply(RequestRateLimiterGatewayFilterFactory.Config config) {        KeyResolver resolver = (KeyResolver)this.getOrDefault(config.keyResolver, this.defaultKeyResolver);        RateLimiter&lt;Object&gt; limiter = (RateLimiter)this.getOrDefault(config.rateLimiter, this.defaultRateLimiter);        boolean denyEmpty = (Boolean)this.getOrDefault(config.denyEmptyKey, this.denyEmptyKey);        HttpStatusHolder emptyKeyStatus = HttpStatusHolder.parse((String)this.getOrDefault(config.emptyKeyStatus, this.emptyKeyStatusCode));        return (exchange, chain) -&gt; {            Route route = (Route)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);            return resolver.resolve(exchange).defaultIfEmpty(&quot;____EMPTY_KEY__&quot;).flatMap((key) -&gt; {                if (&quot;____EMPTY_KEY__&quot;.equals(key)) {                    if (denyEmpty) {                        ServerWebExchangeUtils.setResponseStatus(exchange, emptyKeyStatus);                        return exchange.getResponse().setComplete();                    } else {                        return chain.filter(exchange);                    }                } else {                    // isAllowed 方法，根据 [路由 id] 和 [限流关键字 key] 来判断是否要限流                    return limiter.isAllowed(route.getId(), key).flatMap((response) -&gt; {                        Iterator var4 = response.getHeaders().entrySet().iterator();                        while(var4.hasNext()) {                            Entry&lt;String, String&gt; header = (Entry)var4.next();                            exchange.getResponse().getHeaders().add((String)header.getKey(), (String)header.getValue());                        }                        if (response.isAllowed()) {                            return chain.filter(exchange);                        } else {                            ServerWebExchangeUtils.setResponseStatus(exchange, config.getStatusCode());                            return exchange.getResponse().setComplete();                        }                    });                }            });        };    }    private &lt;T&gt; T getOrDefault(T configValue, T defaultValue) {        return configValue != null ? configValue : defaultValue;    }</code></pre><p>上述代码中，结合我们从 application.yml 配置中查看，该源码中其实最重要的就是：</p><p><strong>RateLimiter</strong> ：限流算法及实现（实际实现是令牌桶算法，这里先不做深入探究）</p><p><strong>KeyResolver</strong> ：限流关键字 key（这里 key 其实就是我们说的对用户限流、对接口限流，当我们要对 ip 限流时，这个 key 就是请求的 ip）</p><p>还有就是  <code>limiter.isAllowed</code> 这个函数，是校验是否达到限流条件的重要方法！</p><p><strong>KeyResolver</strong> 看上去就不是影响多规则限流的重要因素~，那么我们就直接来看看 <strong>RateLimiter</strong>~</p><h2 id="ratelimiter-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90" tabindex="-1">RateLimiter 源码分析</h2><p>打开源码一看，哦是 <strong>interface</strong>，我们看看实现类（ idea 中点击下图标记📌处即可查看）</p><p><img src="https://notes.suremotoo.cc/upload/2021/06/RateLimiter-6deca20f4d594320ab7b8766284c9a3d.png" alt="RateLimiter" /></p><p>发现有两个实现类，一个是抽象类 <code>AbstractRateLimiter</code>,一个是基于 Redis 实现的 <code>RedisRateLimiter</code>,(o゜▽゜)o☆[BINGO!]，肯定是 <code>RedisRateLimiter</code>，我们直接打开它~</p><p><img src="https://notes.suremotoo.cc/upload/2021/06/RateLimiter-Impl-c3501845395148f5baa85d0ac9abeaed.png" alt="RateLimiter-Impl" /></p><p><code>RedisRateLimiter</code> 源码(别着急看代码，先往下翻)</p><pre><code class="language-java">@ConfigurationProperties(&quot;spring.cloud.gateway.redis-rate-limiter&quot;)public class RedisRateLimiter extends AbstractRateLimiter&lt;RedisRateLimiter.Config&gt; implements ApplicationContextAware {    /** @deprecated */    @Deprecated    public static final String REPLENISH_RATE_KEY = &quot;replenishRate&quot;;    /** @deprecated */    @Deprecated    public static final String BURST_CAPACITY_KEY = &quot;burstCapacity&quot;;    public static final String CONFIGURATION_PROPERTY_NAME = &quot;redis-rate-limiter&quot;;    public static final String REDIS_SCRIPT_NAME = &quot;redisRequestRateLimiterScript&quot;;    public static final String REMAINING_HEADER = &quot;X-RateLimit-Remaining&quot;;    public static final String REPLENISH_RATE_HEADER = &quot;X-RateLimit-Replenish-Rate&quot;;    public static final String BURST_CAPACITY_HEADER = &quot;X-RateLimit-Burst-Capacity&quot;;    private Log log = LogFactory.getLog(this.getClass());    private ReactiveRedisTemplate&lt;String, String&gt; redisTemplate;    private RedisScript&lt;List&lt;Long&gt;&gt; script;    private AtomicBoolean initialized = new AtomicBoolean(false);    private RedisRateLimiter.Config defaultConfig;    private boolean includeHeaders = true;    private String remainingHeader = &quot;X-RateLimit-Remaining&quot;;    private String replenishRateHeader = &quot;X-RateLimit-Replenish-Rate&quot;;    private String burstCapacityHeader = &quot;X-RateLimit-Burst-Capacity&quot;;    public RedisRateLimiter(ReactiveRedisTemplate&lt;String, String&gt; redisTemplate, RedisScript&lt;List&lt;Long&gt;&gt; script, Validator validator) {        super(RedisRateLimiter.Config.class, &quot;redis-rate-limiter&quot;, validator);        this.redisTemplate = redisTemplate;        this.script = script;        this.initialized.compareAndSet(false, true);    }    public RedisRateLimiter(int defaultReplenishRate, int defaultBurstCapacity) {        super(RedisRateLimiter.Config.class, &quot;redis-rate-limiter&quot;, (Validator)null);        this.defaultConfig = (new RedisRateLimiter.Config()).setReplenishRate(defaultReplenishRate).setBurstCapacity(defaultBurstCapacity);    }    static List&lt;String&gt; getKeys(String id) {        String prefix = &quot;request_rate_limiter.{&quot; + id;        String tokenKey = prefix + &quot;}.tokens&quot;;        String timestampKey = prefix + &quot;}.timestamp&quot;;        return Arrays.asList(tokenKey, timestampKey);    }    public boolean isIncludeHeaders() {        return this.includeHeaders;    }    public void setIncludeHeaders(boolean includeHeaders) {        this.includeHeaders = includeHeaders;    }    public String getRemainingHeader() {        return this.remainingHeader;    }    public void setRemainingHeader(String remainingHeader) {        this.remainingHeader = remainingHeader;    }    public String getReplenishRateHeader() {        return this.replenishRateHeader;    }    public void setReplenishRateHeader(String replenishRateHeader) {        this.replenishRateHeader = replenishRateHeader;    }    public String getBurstCapacityHeader() {        return this.burstCapacityHeader;    }    public void setBurstCapacityHeader(String burstCapacityHeader) {        this.burstCapacityHeader = burstCapacityHeader;    }    public void setApplicationContext(ApplicationContext context) throws BeansException {        if (this.initialized.compareAndSet(false, true)) {            this.redisTemplate = (ReactiveRedisTemplate)context.getBean(&quot;stringReactiveRedisTemplate&quot;, ReactiveRedisTemplate.class);            this.script = (RedisScript)context.getBean(&quot;redisRequestRateLimiterScript&quot;, RedisScript.class);            if (context.getBeanNamesForType(Validator.class).length &gt; 0) {                this.setValidator((Validator)context.getBean(Validator.class));            }        }    }    RedisRateLimiter.Config getDefaultConfig() {        return this.defaultConfig;    }    public Mono&lt;Response&gt; isAllowed(String routeId, String id) {        if (!this.initialized.get()) {            throw new IllegalStateException(&quot;RedisRateLimiter is not initialized&quot;);        } else {            RedisRateLimiter.Config routeConfig = this.loadConfiguration(routeId);            int replenishRate = routeConfig.getReplenishRate();            int burstCapacity = routeConfig.getBurstCapacity();            try {                List&lt;String&gt; keys = getKeys(id);                List&lt;String&gt; scriptArgs = Arrays.asList(replenishRate + &quot;&quot;, burstCapacity + &quot;&quot;, Instant.now().getEpochSecond() + &quot;&quot;, &quot;1&quot;);                Flux&lt;List&lt;Long&gt;&gt; flux = this.redisTemplate.execute(this.script, keys, scriptArgs);                return flux.onErrorResume((throwable) -&gt; {                    return Flux.just(Arrays.asList(1L, -1L));                }).reduce(new ArrayList(), (longs, l) -&gt; {                    longs.addAll(l);                    return longs;                }).map((results) -&gt; {                    boolean allowed = (Long)results.get(0) == 1L;                    Long tokensLeft = (Long)results.get(1);                    Response response = new Response(allowed, this.getHeaders(routeConfig, tokensLeft));                    if (this.log.isDebugEnabled()) {                        this.log.debug(&quot;response: &quot; + response);                    }                    return response;                });            } catch (Exception var9) {                this.log.error(&quot;Error determining if user allowed from redis&quot;, var9);                return Mono.just(new Response(true, this.getHeaders(routeConfig, -1L)));            }        }    }    RedisRateLimiter.Config loadConfiguration(String routeId) {        RedisRateLimiter.Config routeConfig = (RedisRateLimiter.Config)this.getConfig().getOrDefault(routeId, this.defaultConfig);        if (routeConfig == null) {            routeConfig = (RedisRateLimiter.Config)this.getConfig().get(&quot;defaultFilters&quot;);        }        if (routeConfig == null) {            throw new IllegalArgumentException(&quot;No Configuration found for route &quot; + routeId + &quot; or defaultFilters&quot;);        } else {            return routeConfig;        }    }    @NotNull    public Map&lt;String, String&gt; getHeaders(RedisRateLimiter.Config config, Long tokensLeft) {        Map&lt;String, String&gt; headers = new HashMap();        if (this.isIncludeHeaders()) {            headers.put(this.remainingHeader, tokensLeft.toString());            headers.put(this.replenishRateHeader, String.valueOf(config.getReplenishRate()));            headers.put(this.burstCapacityHeader, String.valueOf(config.getBurstCapacity()));        }        return headers;    }    @Validated    public static class Config {        @Min(1L)        private int replenishRate;        @Min(1L)        private int burstCapacity = 1;        public Config() {        }        public int getReplenishRate() {            return this.replenishRate;        }        public RedisRateLimiter.Config setReplenishRate(int replenishRate) {            this.replenishRate = replenishRate;            return this;        }        public int getBurstCapacity() {            return this.burstCapacity;        }        public RedisRateLimiter.Config setBurstCapacity(int burstCapacity) {            this.burstCapacity = burstCapacity;            return this;        }        public String toString() {            return &quot;Config{replenishRate=&quot; + this.replenishRate + &quot;, burstCapacity=&quot; + this.burstCapacity + &#39;}&#39;;        }    }}</code></pre><p>别看代码多，不要慌！实际上就是基于 Redis 限流是怎么个算法实现的，但是和限流为什么只能有一个规则，好像啥都没有<sub>🤣，说明不在这里</sub></p><p><img src="https://notes.suremotoo.cc/upload/2021/06/%E5%90%93%E5%82%BB%E4%BA%86-9b968ca19b2e44d1b71df81fd83704d0.jpeg" alt="吓傻了" /></p><p>📢注意了，但是它 <code>extends AbstractRateLimiter</code> 了，继承了 <code>AbstractRateLimiter</code> 类，我们还是看看这个类吧~</p><h2 id="abstractratelimiter-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90" tabindex="-1">AbstractRateLimiter 源码分析</h2><p><code>AbstractRateLimiter</code> 源码</p><pre><code class="language-java">public abstract class AbstractRateLimiter&lt;C&gt; extends AbstractStatefulConfigurable&lt;C&gt; implements RateLimiter&lt;C&gt;, ApplicationListener&lt;FilterArgsEvent&gt; {    private String configurationPropertyName;    private Validator validator;    protected AbstractRateLimiter(Class&lt;C&gt; configClass, String configurationPropertyName, Validator validator) {        super(configClass);        this.configurationPropertyName = configurationPropertyName;        this.validator = validator;    }    protected String getConfigurationPropertyName() {        return this.configurationPropertyName;    }    protected Validator getValidator() {        return this.validator;    }    public void setValidator(Validator validator) {        this.validator = validator;    }    public void onApplicationEvent(FilterArgsEvent event) {        Map&lt;String, Object&gt; args = event.getArgs();        if (!args.isEmpty() &amp;&amp; this.hasRelevantKey(args)) {            String routeId = event.getRouteId();            C routeConfig = this.newConfig();            ConfigurationUtils.bind(routeConfig, args, this.configurationPropertyName, this.configurationPropertyName, this.validator);          // 重点            this.getConfig().put(routeId, routeConfig);        }    }    private boolean hasRelevantKey(Map&lt;String, Object&gt; args) {        return args.keySet().stream().anyMatch((key) -&gt; {            return key.startsWith(this.configurationPropertyName + &quot;.&quot;);        });    }    public String toString() {        return (new ToStringCreator(this)).append(&quot;configurationPropertyName&quot;, this.configurationPropertyName).append(&quot;config&quot;, this.getConfig()).append(&quot;configClass&quot;, this.getConfigClass()).toString();    }}</code></pre><p>代码不多，就一个核心方法 <code>onApplicationEvent</code>，参数是个 <code>FilterArgsEvent</code>，看上去是把过滤器的参数 args 都获取出来，再做处理</p><blockquote><p>小提示，看看人家的命名，一看就让人知道大概什么意思，以后大家也注意下命名！</p></blockquote><p>贴一下限流的核心配置示例：</p><pre><code class="language-yaml">        - name: RequestRateLimiter          args:            # 令牌桶每秒填充平均速率            redis-rate-limiter.replenishRate: 1            # 令牌桶的上限            redis-rate-limiter.burstCapacity: 10            # 使用 SpEL 表达式从 Spring 容器中获取 Bean 对象,pathKeyResolver 是根据地址来限流            key-resolver: &quot;#{@pathKeyResolver}&quot;</code></pre><p>捋一捋，这个过滤器的参数 args 就是限流参数，而</p><pre><code class="language-java">RedisRateLimiter extends AbstractRateLimiter&lt;RedisRateLimiter.Config&gt;</code></pre><p>那么 <code>onApplicationEvent</code>,应该是把参数对应的 routeConfig 对象初始化出来~，也就是 <code>RedisRateLimiter.Config</code> 的这个 Config 对象，</p><p><code>Config</code> 里就两个属性，也就是限流的重要参数，果然没错~</p><p>简单再贴一下 <code>RedisRateLimiter.Config</code> 代码</p><pre><code class="language-java">@Validatedpublic static class Config {    @Min(1L)    private int replenishRate;    @Min(1L)    private int burstCapacity = 1;    public Config() {    }    // 省略...}</code></pre><p>最后最后有个 <code>this.getConfig().put(routeId, routeConfig);</code></p><p>这不就是把路由和其对应的限流规则存到一个 Map 里嘛~，盲猜都知道这个 <code>this.getConfig()</code> 是个 Map,可以去 <code>AbstractStatefulConfigurable</code> 代码里看，这里就不展示了~</p><pre><code class="language-java">AbstractRateLimiter&lt;C&gt; extends AbstractStatefulConfigurable&lt;C&gt;</code></pre><p>其实看到 <code>this.getConfig().put(routeId, routeConfig); </code> 这里我大概已经知道是什么问题了：我们针对一个 1 路由配置多个限流规则，最终名为 Config 的 Map 里存储的只有 1 个！！因为存储到 Map 里的 key 就是 routeId~</p><p>你的路由 id 是固定的，所以后面的规则把前面的覆盖了~~~🎉🎉🎉🎉</p><p><img src="https://notes.suremotoo.cc/upload/2021/06/%E5%8F%89%E4%BC%9A%E8%85%B0-a1cad806c223483d93c27d2455304d25.jpeg" alt="叉会腰" /></p><h2 id="%E6%94%B9%E9%80%A0%E6%96%B9%E6%A1%88" tabindex="-1">改造方案</h2><p>既然找到问题了，我们就想办法改造它！经过前面的分析，我们应该要改造的就是 <code>onApplicationEvent</code> 方法里的 <code>this.getConfig().put(routeId, routeConfig);</code></p><p>我们应该改造成，放入 Config 里的 Key  <strong>不用 RouteId</strong>!</p><h5 id="%E7%94%A8%E4%BB%80%E4%B9%88%E5%91%A2%EF%BC%8C%E6%88%91%E8%BF%99%E9%87%8C-%E4%BD%BF%E7%94%A8-routeid-%E5%92%8C-keyresolver-%E7%9A%84-hashcode-%E7%BB%84%E5%90%88" tabindex="-1">用什么呢，我这里 <strong>使用 routeId 和 KeyResolver 的 hashcode 组合</strong></h5><p>改造前再捋清楚，Spring Cloud Gateway 自带的 Redis 限流实现类是 <code>RedisRateLimiter</code>,它继承的抽象类 <code>AbstractRateLimiter</code>,而我们要改造的方法在 <code>AbstractRateLimiter</code>，</p><p>所以我们重写一个 <code>RedisRateLimiter</code>，重写 <code>onApplicationEvent</code> 方法 ！</p><p>ok！Just Do It~</p><h3 id="%E8%87%AA%E5%AE%9A%E4%B9%89-diyredisratelimiter" tabindex="-1">自定义 DiyRedisRateLimiter</h3><p>首先，我们新建 1 个类，叫 <code>DiyRedisRateLimiter</code>，剩下的代码就从 <code>RedisRateLimiter</code> 全部拷贝过来！</p><p>然后重写 <code>onApplicationEvent</code> 方法！</p><p><code>DiyRedisRateLimiter</code> 代码:</p><pre><code class="language-java">/** * Author: Suremotoo */public class DiyRedisRateLimiter extends AbstractRateLimiter&lt;DiyRedisRateLimiter.Config&gt;    implements ApplicationContextAware {    public static final String REPLENISH_RATE_KEY = &quot;replenishRate&quot;;    public static final String BURST_CAPACITY_KEY = &quot;burstCapacity&quot;;    public static final String CONFIGURATION_PROPERTY_NAME = &quot;redis-rate-limiter&quot;;    public static final String REDIS_SCRIPT_NAME = &quot;redisRequestRateLimiterScript&quot;;    public static final String REMAINING_HEADER = &quot;X-RateLimit-Remaining&quot;;    public static final String REPLENISH_RATE_HEADER = &quot;X-RateLimit-Replenish-Rate&quot;;    public static final String BURST_CAPACITY_HEADER = &quot;X-RateLimit-Burst-Capacity&quot;;    private Log log = LogFactory.getLog(this.getClass());    private ReactiveRedisTemplate&lt;String, String&gt; redisTemplate;    private RedisScript&lt;List&lt;Long&gt;&gt; script;    private AtomicBoolean initialized = new AtomicBoolean(false);    private DiyRedisRateLimiter.Config defaultConfig;    private boolean includeHeaders = true;    private String remainingHeader = &quot;X-RateLimit-Remaining&quot;;    private String replenishRateHeader = &quot;X-RateLimit-Replenish-Rate&quot;;    private String burstCapacityHeader = &quot;X-RateLimit-Burst-Capacity&quot;;    public DiyRedisRateLimiter(ReactiveRedisTemplate&lt;String, String&gt; redisTemplate, RedisScript&lt;List&lt;Long&gt;&gt; script,        Validator validator) {        super(DiyRedisRateLimiter.Config.class, &quot;redis-rate-limiter&quot;, validator);        this.redisTemplate = redisTemplate;        this.script = script;        this.initialized.compareAndSet(false, true);    }    public DiyRedisRateLimiter(int defaultReplenishRate, int defaultBurstCapacity) {        super(DiyRedisRateLimiter.Config.class, &quot;redis-rate-limiter&quot;, (Validator)null);        this.defaultConfig = (new DiyRedisRateLimiter.Config()).setReplenishRate(defaultReplenishRate)            .setBurstCapacity(defaultBurstCapacity);    }    static List&lt;String&gt; getKeys(String id) {        String prefix = &quot;request_rate_limiter.{&quot; + id;        String tokenKey = prefix + &quot;}.tokens&quot;;        String timestampKey = prefix + &quot;}.timestamp&quot;;        return Arrays.asList(tokenKey, timestampKey);    }    public boolean isIncludeHeaders() {        return this.includeHeaders;    }    public void setIncludeHeaders(boolean includeHeaders) {        this.includeHeaders = includeHeaders;    }    public String getRemainingHeader() {        return this.remainingHeader;    }    public void setRemainingHeader(String remainingHeader) {        this.remainingHeader = remainingHeader;    }    public String getReplenishRateHeader() {        return this.replenishRateHeader;    }    public void setReplenishRateHeader(String replenishRateHeader) {        this.replenishRateHeader = replenishRateHeader;    }    public String getBurstCapacityHeader() {        return this.burstCapacityHeader;    }    public void setBurstCapacityHeader(String burstCapacityHeader) {        this.burstCapacityHeader = burstCapacityHeader;    }    @Override    public void setApplicationContext(ApplicationContext context) throws BeansException {        if (this.initialized.compareAndSet(false, true)) {            this.redisTemplate =                (ReactiveRedisTemplate)context.getBean(&quot;stringReactiveRedisTemplate&quot;, ReactiveRedisTemplate.class);            this.script = (RedisScript)context.getBean(&quot;redisRequestRateLimiterScript&quot;, RedisScript.class);            if (context.getBeanNamesForType(Validator.class).length &gt; 0) {                this.setValidator((Validator)context.getBean(Validator.class));            }        }    }    DiyRedisRateLimiter.Config getDefaultConfig() {        return this.defaultConfig;    }                                                                                                           //                                    &#96;&#96;.-/++oosyyyyyyyyyssso++/-.&#96;                                    //                                ./oydmmmhyso+/:------://++oshdmmmhs/-&#96;                               //                            &#96;:odmdhs+:.&#96;&#96;                   &#96;&#96;&#96;-/ohmNmyo:&#96;                           //                         ./ymmy+-&#96;&#96;                               &#96;&#96;-+ymNd+.                         //                      &#96;:ymmy/.&#96;            小哥哥                      &#96;.-sdmy:                       //                    &#96;/hmh/.                    小姐姐                      ./hmy:&#96;                    //                   :hNy:./o-                                                &#96;:dNh/&#96;                  //                 &#96;oNd:&#96;:dNs.             帅哥                                  &#96;+dNy.                 //                &#96;sNd- /Nd:&#96;                   美女                               .yNm/&#96;               //               &#96;yMh. +Nd-                                                        &#96;+NNo&#96;              //              &#96;oNd&#96; /Nm-                                           &#96;ss.            :mNo&#96;             //              /Nm- .dN/       &#96;..-:/+osssssooooooooooooo+//:-.&#96;&#96;   &#96;dMo             +NN/             //             -mN:  /Nh&#96;&#96;..:/oyhddddhysssoossssssssssyyhhhddmmmdhys+:hMh&#96;            &#96;hMd&#96;            //            &#96;hNo&#96; &#96;yMyohmmdhso/-..&#96;                      &#96;&#96;.-:/oyhdmNMd&#96;             :NM/            //            +Mh&#96;  &#96;yNmhmMd.                                       &#96;.yMm.             &#96;hMh            //           .mN:    &#96;-. sMd&#96;                                         oMm.              +MN-           //  ./+:.    oMy&#96;        sMm.                                         sMm.              :NN:           // .dMNNms. .dN:--&#96;      sMm.                                         yMh&#96;              .mM/           // :NN+:yNmo/NmyNN-      sMm.        我是 Suremotoo                  &#96;dMy               &#96;mN/           // .mMo  :dMNMMMNN:    &#96;+NNs&#96;                                        .NN+               -Nm&#96;           //  yMd.  &#96;oNMMdyMs:///hMm:         看这里                            oMm.               oMy            //  .NMo    -hMMNNmdhmMMh.                                          -mMs           /yo&#96;:mN-            //   +MN:    &#96;/dMm/&#96;&#96;hMh&#96;              看这里                         oMMho/-.&#96;&#96;    :NMMymN:             //    +NN+&#96;    &#96;/mMdsMN:                                            &#96;:oydmNMNmdhhhmMNMMm:              //     /mMy&#96;     .yNMMd&#96;                  看这里                       &#96;&#96;.-yMNhhhyo.-:.               //      -dMd-      :mMh&#96;                                                  .sNN+&#96;                       //       &#96;yMm/      /NNs.                     看这里                      .+mNh:                         //        &#96;oNNo&#96;    &#96;/mMmo.&#96;                                         &#96;-smNd/&#96;                          //          :dMh-    :dNmNNdo:.&#96;                                &#96;&#96;-/sdNMMy.                            //           .yMm+&#96; /mm/.:sdmNmdyo++/:--..&#96;&#96;&#96;&#96;&#96;&#96;&#96;&#96;&#96;.---::::/+osydmNmdy+:yNh-                           //            &#96;oNNy/mm:    &#96;.-+oydNMNmmmNmmmmmmmmmmmNNNNNNNNNMNhyo:.&#96;   &#96;/mm+&#96;                         //              :dMNMh&#96;      .yy..dN+.-yMh///++++++//::sMd:-:Nm-          -yNy.                        //               .sNMh&#96;      +Mm.-Nm.  sMs   下   |    -Nd. &#96;dNy/          &#96;oNh.                       //                &#96;oNN+.    &#96;hMh&#96;+Mh&#96;  yM+   面   |   &#96;dN/  sMMN+&#96;         &#96;oNd.                      //                  -sdmy+-&#96;.mMo sMs  &#96;hN/   就   |     sMs  :NmdN+&#96;         &#96;hMs                      //                    ./sddddMN-&#96;dM/  &#96;dN:   是   |    /Nd&#96; .mN/dN+&#96;    &#96;.:ohNM+                      //                       &#96;-+NMh&#96;-Nm.  .NN.   改   |     .mm. &#96;hM/-dNo-:+shddhosNh.                     //                         -NMo +Mh   /Nd&#96;   造   |   &#96;mM+ &#96;yMs .ymNMNy+:.&#96; &#96;sNy.                    //                         /NN: oMh   oMd&#96;   的   |     &#96;dMs &#96;yMd&#96; &#96;.+Nd.      &#96;yNs&#96;                   //                         -ss&#96; /s+   /yo&#96;   方   |   &#96;sdo  +dh&#96;    +ms&#96;      .hd-    //        这                                      |   //  这↓   // 👇👇👇👇👇👇👇👇👇👇👇👇👇👇    @Override    public void onApplicationEvent(FilterArgsEvent event) {        Map&lt;String, Object&gt; args = event.getArgs();        if (!args.isEmpty() &amp;&amp; this.hasRelevantKey(args)) {            String routeId = event.getRouteId();            Config routeConfig = this.newConfig();            ConfigurationUtils.bind(routeConfig, args, super.getConfigurationPropertyName(),                this.getConfigurationPropertyName(), this.getValidator());            /**             * 这里重写 id，防止过冲规则重复被覆盖 by Suremotoo             */            // 使用 routeId + KeyResolver 的 hashcode 组合作为配置 id，防止重复            routeId = routeId + event.getArgs().get(&quot;key-resolver&quot;).hashCode();            // System.out.println(&quot;routeId put = &quot; + routeId);            this.getConfig().put(routeId, routeConfig);        }    }      /*****************************************************************/    private boolean hasRelevantKey(Map&lt;String, Object&gt; args) {        return args.keySet().stream().anyMatch((key) -&gt; {            return key.startsWith(this.getConfigurationPropertyName() + &quot;.&quot;);        });    }    @Override    public Mono&lt;Response&gt; isAllowed(String routeId, String id) {        if (!this.initialized.get()) {            throw new IllegalStateException(&quot;DiyRedisRateLimiter is not initialized&quot;);        } else {            DiyRedisRateLimiter.Config routeConfig = this.loadConfiguration(routeId);            int replenishRate = routeConfig.getReplenishRate();            int burstCapacity = routeConfig.getBurstCapacity();            try {                List&lt;String&gt; keys = getKeys(id);                List&lt;String&gt; scriptArgs =                    Arrays.asList(replenishRate + &quot;&quot;, burstCapacity + &quot;&quot;, Instant.now().getEpochSecond() + &quot;&quot;, &quot;1&quot;);                Flux&lt;List&lt;Long&gt;&gt; flux = this.redisTemplate.execute(this.script, keys, scriptArgs);                return flux.onErrorResume((throwable) -&gt; {                    return Flux.just(Arrays.asList(1L, -1L));                }).reduce(new ArrayList(), (longs, l) -&gt; {                    longs.addAll(l);                    return longs;                }).map((results) -&gt; {                    boolean allowed = (Long)results.get(0) == 1L;                    Long tokensLeft = (Long)results.get(1);                    Response response = new Response(allowed, this.getHeaders(routeConfig, tokensLeft));                    if (this.log.isDebugEnabled()) {                        this.log.debug(&quot;response: &quot; + response);                    }                    return response;                });            } catch (Exception var9) {                this.log.error(&quot;Error determining if user allowed from redis&quot;, var9);                return Mono.just(new Response(true, this.getHeaders(routeConfig, -1L)));            }        }    }    DiyRedisRateLimiter.Config loadConfiguration(String routeId) {        DiyRedisRateLimiter.Config routeConfig =            (DiyRedisRateLimiter.Config)this.getConfig().getOrDefault(routeId, this.defaultConfig);        if (routeConfig == null) {            routeConfig = (DiyRedisRateLimiter.Config)this.getConfig().get(&quot;defaultFilters&quot;);        }        if (routeConfig == null) {            throw new IllegalArgumentException(&quot;No Configuration found for route &quot; + routeId + &quot; or defaultFilters&quot;);        } else {            return routeConfig;        }    }    @NotNull    public Map&lt;String, String&gt; getHeaders(DiyRedisRateLimiter.Config config, Long tokensLeft) {        Map&lt;String, String&gt; headers = new HashMap();        if (this.isIncludeHeaders()) {            headers.put(this.remainingHeader, tokensLeft.toString());            headers.put(this.replenishRateHeader, String.valueOf(config.getReplenishRate()));            headers.put(this.burstCapacityHeader, String.valueOf(config.getBurstCapacity()));        }        return headers;    }    @Validated    public static class Config {        @Min(1L)        private int replenishRate;        @Min(1L)        private int burstCapacity = 1;        public Config() {}        public int getReplenishRate() {            return this.replenishRate;        }        public DiyRedisRateLimiter.Config setReplenishRate(int replenishRate) {            this.replenishRate = replenishRate;            return this;        }        public int getBurstCapacity() {            return this.burstCapacity;        }        public DiyRedisRateLimiter.Config setBurstCapacity(int burstCapacity) {            this.burstCapacity = burstCapacity;            return this;        }        @Override        public String toString() {            return &quot;Config{replenishRate=&quot; + this.replenishRate + &quot;, burstCapacity=&quot; + this.burstCapacity + &#39;}&#39;;        }    }}</code></pre><p>创建完后，我们再在将这个类初始化为 Spring 里</p><p></p><pre><code class="language-java">/** * Author: Suremotoo */@Configurationpublic class RateLimiterConfig {    /**     * 使用自定义的限流类     *      * @param redisTemplate     * @param redisScript     * @param validator     * @return     */    @Bean    @Primary    public DiyRedisRateLimiter diyRedisRateLimiter(ReactiveRedisTemplate&lt;String, String&gt; redisTemplate,        @Qualifier(DiyRedisRateLimiter.REDIS_SCRIPT_NAME) RedisScript&lt;List&lt;Long&gt;&gt; redisScript, Validator validator) {        return new DiyRedisRateLimiter(redisTemplate, redisScript, validator);    }    // .... 其他 KeyResolver 省略，详情见上文描述中的 RateLimiterConfig  }</code></pre><p>这就弄好了，但是注意，我们还没有改造完！</p><p>这仅仅是放入 Map 中已经不是 1 个了！但是用的时候呢？还记得前面提到的 <strong>isAllow</strong> 方法吗？这个方法是在 <code>RequestRateLimiterGatewayFilterFactory</code> 里的 <code>apply</code> 方法中 ，所以我们还要重写 这里！</p><p><img src="https://notes.suremotoo.cc/upload/2021/06/%E5%A4%8D%E5%88%B6%E7%B2%98%E8%B4%B4-fb585625e6f6498da1993b55516800e1.jpeg" alt="复制粘贴" /></p><h3 id="%E8%87%AA%E5%AE%9A%E4%B9%89-diyrequestratelimitergatewayfilterfactory" tabindex="-1">自定义 DiyRequestRateLimiterGatewayFilterFactory</h3><p>首先，我们新建 1 个类，叫 <code>DiyRequestRateLimiterGatewayFilterFactory</code>，继承 <code>RequestRateLimiterGatewayFilterFactory</code></p><p>然后重写 <code>apply</code> 方法！</p><p><code>DiyRequestRateLimiterGatewayFilterFactory</code> 代码示例：</p><pre><code class="language-java">/** * Author: Suremotoo */@Componentpublic class DiyRequestRateLimiterGatewayFilterFactory extends RequestRateLimiterGatewayFilterFactory {    private final RateLimiter defaultRateLimiter;    private final KeyResolver defaultKeyResolver;    private boolean denyEmptyKey = true;    public DiyRequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver) {        super(defaultRateLimiter, defaultKeyResolver);        this.defaultKeyResolver = defaultKeyResolver;        this.defaultRateLimiter = defaultRateLimiter;    }    @Override    public GatewayFilter apply(RequestRateLimiterGatewayFilterFactory.Config config) {        KeyResolver resolver = (KeyResolver)this.getOrDefault(config.getKeyResolver(), this.defaultKeyResolver);        RateLimiter&lt;Object&gt; limiter = (RateLimiter)this.getOrDefault(config.getRateLimiter(), this.defaultRateLimiter);        boolean denyEmpty = (Boolean)this.getOrDefault(config.getDenyEmptyKey(), this.denyEmptyKey);        HttpStatusHolder emptyKeyStatus =            HttpStatusHolder.parse((String)this.getOrDefault(config.getEmptyKeyStatus(), this.getEmptyKeyStatusCode()));        return (exchange, chain) -&gt; {            Route route = (Route)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);            return resolver.resolve(exchange).defaultIfEmpty(&quot;____EMPTY_KEY__&quot;).flatMap((key) -&gt; {                                if (&quot;____EMPTY_KEY__&quot;.equals(key)) {                    if (denyEmpty) {                        ServerWebExchangeUtils.setResponseStatus(exchange, emptyKeyStatus);                        return exchange.getResponse().setComplete();                    } else {                        return chain.filter(exchange);                    }                } else {                                         //             &#96;-/++//////+++-&#96;                                //          .+o+:.          &#96;:os+&#96;                             //        .so:      在这里      .+o.                           //       :y:y.                    &#96;yo                          //      -h.h&#96;  .-://::///:-.&#96; y&#96;    os                         //     &#96;h&#96;+hyo+/-.&#96;&#96;.....-:/ooN-    &#96;d.                        // &#96;:. o/ &#96;&#96;d.                d-     oo                        // +yssmd- &#96;m.    在这里     &#96;m&#96;     +o                        // .d&#96;.hmssd-                /h    /-h.                        //  :h. :hm:    在这里       .+oyyshy-                         //   .h: &#96;h+                  &#96;oy.                             //    &#96;ss&#96;+yys+:-.&#96;&#96;&#96;&#96;&#96;..-:/osyh&#96;                              //      /dh  &#96;soyod+ooo+doh+&#96;  -y-                             //       .y+-/ys::s     o//N/   &#96;d.                            //         &#96;:m:d&#96;++     -y.hsooooy/                            //           y.s +:     .y&#96;h .y&#96;  s.                          //                                //   👇👇👇👇👇🏻                                      /**                     * 改造校验时传入的 routeId                     */                    // 获取 routeId                    String routeId = route.getId();                    // 使用 routeId+KeyResolver 的 hashcode 获取 config                    routeId = routeId + resolver.hashCode();                                        return limiter.isAllowed(routeId, key).flatMap((response) -&gt; {                        Iterator var4 = response.getHeaders().entrySet().iterator();                        while (var4.hasNext()) {                            Map.Entry&lt;String, String&gt; header = (Map.Entry)var4.next();                            exchange.getResponse().getHeaders().add((String)header.getKey(), (String)header.getValue());                        }                        if (response.isAllowed()) {                            return chain.filter(exchange);                        } else {                            // 原返回信息代码                            // ServerWebExchangeUtils.setResponseStatus(exchange, config.getStatusCode());                            // return exchange.getResponse().setComplete();                                                             // 自定义返回信息 | start                            ServerHttpResponse httpResponse = exchange.getResponse();                            httpResponse.setStatusCode(config.getStatusCode());                            Map&lt;String, Object&gt; dataMap = new HashMap&lt;&gt;(4);                            dataMap.put(&quot;errorCode&quot;, &quot;1000&quot;);                            dataMap.put(&quot;errorMsg&quot;, &quot;操作频繁，歇会再来~&quot;);                            DataBuffer buffer = httpResponse.bufferFactory()                                .wrap(JSONObject.wrap(dataMap).toString().getBytes(StandardCharsets.UTF_8));                            return httpResponse.writeWith(Mono.just(buffer));                            // 自定义返回信息 | end                        }                    });                }            });        };    }    private &lt;T&gt; T getOrDefault(T configValue, T defaultValue) {        return configValue != null ? configValue : defaultValue;    }}</code></pre><p>然后在 <strong>application.yml</strong> 中使用的时候用自己定义的 <code>DiyRequestRateLimiterGatewayFilterFactory</code></p><p>示例：</p><pre><code class="language-yaml">spring:  cloud:    gateway:      routes:        - id: query_user_info_route          uri: lb://user-center          filters:          # 第一个限流过滤器和规则            - name: DiyRequestRateLimiter              args:                # 令牌桶每秒填充平均速率                redis-rate-limiter.replenishRate: 1                # 令牌桶的上限                redis-rate-limiter.burstCapacity: 100                # 使用 SpEL 表达式从 Spring 容器中获取 Bean 对象,pathKeyResolver 是根据地址来限流                key-resolver: &quot;#{@pathKeyResolver}&quot; # 详情见 RateLimiterConfig          # 第二个限流过滤器和规则            - name: DiyRequestRateLimiter              args:                # 令牌桶每秒填充平均速率                redis-rate-limiter.replenishRate: 1                # 令牌桶的上限                redis-rate-limiter.burstCapacity: 10                # 使用 SpEL 表达式从 Spring 容器中获取 Bean 对象,remoteAddrKeyResolver 是根据请求 ip 来限流                key-resolver: &quot;#{@remoteAddrKeyResolver}&quot; # 详情见 RateLimiterConfig</code></pre><p>终于大功告成~</p><p><img src="https://notes.suremotoo.cc/upload/2021/06/a-ab4779bd673e4791be62c7fbd88072c7.jpg" alt="a" /></p>]]>
                    </description>
                    <pubDate>Sun, 27 Jun 2021 02:20:57 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[MySQL MHA 系统构建]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/mysql-mha</link>
                    <description>
                            <![CDATA[<h1 id="mysql-mha-%E7%B3%BB%E7%BB%9F%E6%9E%84%E5%BB%BA" tabindex="-1">MySQL MHA 系统构建</h1><p>使用 Docker 来构建 1 主 2 从的半同步复制的 MySQL 集群，并使用 MHA 来对 MySQL 集群进行监控，实现 MySQL 集群的故障转移。</p><h2 id="01.-%E7%B3%BB%E7%BB%9F%E8%BD%AF%E4%BB%B6%E7%89%88%E6%9C%AC" tabindex="-1">01. 系统软件版本</h2><p>Linux 构建：<strong>Docker version 19.03.13</strong></p><p>Linux 版本：<strong>CentOS 7.9.2009</strong></p><p>MySQL 版本：<strong>MySQL 5.7.31</strong></p><p>MHA 版本：<strong>0.58-0.el7.centos</strong></p><p>辅助软件： <strong>Docker Desktop</strong></p><h2 id="02.-%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84" tabindex="-1">02. 系统架构</h2><p><img src="https://notes.suremotoo.cc/upload/2020/11/MHA-ecce948b5fdb4275ad827d8f1fd57ee7.png" alt="MHA" /></p><h3 id="%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%B8%85%E5%8D%95-container-list" tabindex="-1">服务器清单 <strong>Container List</strong></h3><table><thead><tr><th>Container Name</th><th>Container IP</th><th>PORTS Mapping</th><th>Description</th></tr></thead><tbody><tr><td>mysql-master</td><td>172.17.0.2</td><td>33061:3306；<br>2201:22</td><td>MySQL 主</td></tr><tr><td>mysql-slave-1</td><td>172.17.0.3</td><td>33062:3306；<br/>2202:22</td><td>MySQL 从，MySql 主备用机</td></tr><tr><td>mysql-slave-2</td><td>172.17.0.4</td><td>33063:3306；<br/>2203:22</td><td>MySQL 从</td></tr><tr><td>mysql-mha</td><td>172.17.0.5</td><td>2204:22</td><td>MySQL MHA 管理机</td></tr></tbody></table><blockquote><p><strong>注意</strong>：本次使用 Docker 来构建，默认 Docker 启动时，端口都是 172.17.0.* 依次启动分配的，为保证和以上服务器的 ip 对应，请按照以下顺序依次启动：</p><ol><li>mysql-master</li><li>msyql-slave-1</li><li>mysql-slave-2</li><li>Mysql-mha</li></ol></blockquote><h2 id="03.-%E8%BD%AF%E4%BB%B6%E5%AE%89%E8%A3%85" tabindex="-1">03. 软件安装</h2><h3 id="3.1-%E5%88%9B%E5%BB%BA-centos7_mysql%3A5.7.31-%E9%95%9C%E5%83%8F" tabindex="-1">3.1 创建 centos7_mysql:5.7.31 镜像</h3><p>使用 Docker 拉取 CentOS 7.8 镜像到本地，启动该镜像为 centos 容器，注意 Docker 镜像源地址。</p><pre><code class="language-shell"># 拉取 CentOS 7.9.2009 镜像docker pull centos:centos7.9.2009# 创建容器 centos docker run -dit --privileged --name centos-mysql -p 22:22 centos:centos7.9.2009 /usr/sbin/init# 进入容器docker exec -it centos-mysql /bin/bash</code></pre><blockquote><p><strong>注意</strong>，如果提示 22 端口被监听占用，可任意改用其他未使用过的端口，比如： 222:22</p><p>完整示例：docker run -dit --privileged --name centos-mysql -p <u><em>222:22</em></u> centos:centos7.9.2009 /usr/sbin/init</p></blockquote><p>创建完在 <strong>Docker Desktop</strong> 中可以查看到如下信息, 第一个 centos_mysql 就是刚刚建的</p><p><img src="https://notes.suremotoo.cc/upload/2020/11/docker-containers-0fc8a9952c694e8a8d3b6a94b39280d3.png" alt="docker-containers" /></p><p>安装 SSH，MySQL 等软件。</p><pre><code class="language-shell">## 换源 aliyunyum -y install wgetwget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repoyum clean all &amp;&amp; yum makecache## 安装 SSH yum -y install openssh-server# 修改 root 密码为 rootecho &#39;root:root&#39;|chpasswd# 修改系统语言编码echo &quot;export LC_ALL=en_US.UTF-8&quot;  &gt;&gt;  /etc/profilesource /etc/profile# 修改系统时间ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime# 重启 sshd systemctl enable sshdsystemctl restart sshd## 安装 MySQL# 下载 mysql 源wget https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm# 安装 mysql 源rpm -ivh mysql80-community-release-el7-3.noarch.rpm # 更新源yum clean all &amp;&amp; yum makecache# 关闭 8.0 版本，开启 5.7 版本yum-config-manager --disable mysql80-community yum-config-manager --enable mysql57-community # 安装 mysql-community-serveryum install -y mysql-community-server# 查看 mysql 状态，先不要启动 mysql！！！systemctl status mysqld.service</code></pre><p>安装完 MySQL 之后，导出 centos 容器。</p><pre><code class="language-shell"># 导出 centos， centos-mysqldocker export  centos-mysql  &gt; centos7_mysql57.tar# 导入 centos7_mysql57.tar 为本地镜像docker import centos7_mysql57.tar# 镜像重命名 “镜像 id” 就填写对应的镜像 ID 号docker tag 镜像id centos7_mysql:5.7.31</code></pre><p><img src="https://notes.suremotoo.cc/upload/2020/11/docker-desktop-images-9d01ee849b91496dac55db0e28652bfe.png" alt="docker-desktop-images" /></p><h3 id="3.2-%E5%88%9B%E5%BB%BA-mysql-1-%E4%B8%BB-2-%E4%BB%8E%E5%AE%B9%E5%99%A8" tabindex="-1">3.2 创建 MySQL 1 主 2 从容器</h3><p>导出完毕后，关闭 centos 容器，然后使用 centos7_mysql:5.7.31 镜像创建 3 个容器。</p><pre><code class="language-shell"># 创建容器 mysql-masterdocker run -dit  --privileged  --name mysql-master -p 33061:3306 -p 2201:22 centos7_mysql:5.7.31 /usr/sbin/init# 创建容器 mysql-slave-1docker run -dit  --privileged  --name  mysql-slave-1 -p 33062:3306 -p 2202:22 centos7_mysql:5.7.31 /usr/sbin/init# 创建容器 mysql-slave-2docker run -dit  --privileged  --name mysql-slave-2 -p 33063:3306 -p 2203:22 centos7_mysql:5.7.31 /usr/sbin/init</code></pre><p>创建好这 3 个 mysql 容器后，就可以使用 ssh 工具进入容器了，注意区分端口号，用户名密码用安装 SSH 时设置的。</p><blockquote><p><strong>注意</strong>：使用 SSH 连接时，ip 地址为本机：127.0.0.1,<strong>并非</strong>虚拟机的虚拟 ip(172.17.0.*),端口为启动容器时候的代理端口</p><p>如:<code>mysql-master -p 33061:3306 -p 2201:22</code> 33061 代理的是 3306 端口，2201 代理的是 22 端口</p></blockquote><p>mysql-master ssh 连接示意图</p><p><img src="https://notes.suremotoo.cc/upload/2020/11/ssh-tools-6144821d1bb6468d879ffb576abf2ce3.png" alt="ssh-tools" /></p><p>Navicat 连接 mysql-master 服务器的 MySQL 数据库示意图</p><p><img src="https://notes.suremotoo.cc/upload/2020/11/docker-mysql-master-navicat-19456c837d1f4791a248f52c98b0d90b.png" alt="docker-mysql-master-navicat" /></p><h3 id="3.3-%E5%88%9B%E5%BB%BA-mysql-mha-%E5%AE%B9%E5%99%A8" tabindex="-1">3.3 创建 mysql-mha 容器</h3><pre><code class="language-shell"># 创建 mysql-mha 容器docker run -dit --privileged --name mysql-mha -p 2204:22 centos:7.9.2009 /usr/sbin/init# 进入容器docker exec -it mysql-mha /bin/bash## 安装 SSH yum -y install openssh-server# 修改 root 密码echo &#39;root:root&#39;|chpasswd# 修改系统语言编码echo &quot;export LC_ALL=en_US.UTF-8&quot;  &gt;&gt;  /etc/profilesource /etc/profile# 修改系统时间ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime# 重启 sshd systemctl enable sshdsystemctl restart sshd</code></pre><p>安装完 SSH 就可以用 ssh 工具进入容器了，端口号为 2204。</p><p>使用 SSH 连接上 4 个容器后，安装 MHA。</p><blockquote><p>如果不知道 Docker 各个容器的 ip，可以使用以下命令查看</p><pre><code class="language-shell">docker inspect --format=&#39;{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}&#39; $(docker ps -aq)</code></pre><p>效果图：</p><p><img src="https://notes.suremotoo.cc/upload/2020/11/display-docker-container-ip-791c15b515224339a636662c142b347e.png" alt="display-docker-container-ip" /></p></blockquote><h3 id="3.4-%E5%AE%89%E8%A3%85-mha" tabindex="-1">3.4 安装 MHA</h3><p>安装前，我们再看一下 MHA 的结构图，如下：</p><p><img src="https://notes.suremotoo.cc/upload/2020/11/MHA-ecce948b5fdb4275ad827d8f1fd57ee7.png" alt="MHA" /></p><p>把 <code>mha4mysql-node-0.58-0.el7.centos.noarch.rpm</code> 文件传入这 <strong>4</strong> 个容器中，把 <code>mha4mysql-manager-0.58-0.el7.centos.noarch.rpm</code> 传入 <strong>mysql-mha</strong> 容器中</p><blockquote><p>这两个文件下载地址：</p><p>链接: <a href="https://pan.baidu.com/s/1LE7lyanlk5ZV4VK_fSo9hg" target="_blank">https://pan.baidu.com/s/1LE7lyanlk5ZV4VK_fSo9hg</a><br />密码: ge4o</p></blockquote><p>传输工具可以使用： <strong>FileZilla</strong> 等这类工具，连接方式同 SSH</p><p>FileZilla 连接示意图</p><p><img src="https://notes.suremotoo.cc/upload/2020/11/fileZilla-ade0c5aaed064f4a9ab3d6017c6f1ca7.png" alt="fileZilla" /></p><blockquote><p><strong>注意</strong>:  <code>mha4mysql-node-0.58-0.el7.centos.noarch.rpm</code>、<code>mha4mysql-manager-0.58-0.el7.centos.noarch.rpm</code> 这两个包的版本的文件只支持 CentOS7</p></blockquote><p>首先在 <strong>MySQL 主从</strong> 这 3 个容器上安装 mha4mysql-node 包以及需要的依赖</p><pre><code class="language-shell">## 安装 epel 源yum install -y wgetwget -O /etc/yum.repos.d/epel-7.repo http://mirrors.aliyun.com/repo/epel-7.repoyum clean all &amp;&amp; yum makecache# 安装 perl 依赖yum install -y perl-DBD-MySQL## 安装 node 注意文件位置rpm -ivh mha4mysql-node-0.58-0.el7.centos.noarch.rpm</code></pre><p>在 <strong>mysql-mha</strong> 上安装 mha4mysql-node 和 mha4mysql-manager 包</p><pre><code class="language-shell">## 安装 epel 源，适用于 CentOS7yum install -y wgetwget -O /etc/yum.repos.d/epel-7.repo http://mirrors.aliyun.com/repo/epel-7.repoyum clean all &amp;&amp; yum makecache# 安装 perl 依赖yum install -y perl-DBD-MySQLyum install -y perl-Config-Tinyyum install -y perl-Log-Dispatchyum install -y perl-Parallel-ForkManager## 安装 noderpm -ivh mha4mysql-node-0.58-0.el7.centos.noarch.rpm## 安装 managerrpm -ivh mha4mysql-manager-0.58-0.el7.centos.noarch.rpm</code></pre><h2 id="04.-mysql-%E9%9B%86%E7%BE%A4%E5%8D%8A%E5%90%8C%E6%AD%A5%E5%A4%8D%E5%88%B6" tabindex="-1">04. MySQL 集群半同步复制</h2><h3 id="4.1-%E4%BF%AE%E6%94%B9%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6" tabindex="-1">4.1 修改配置文件</h3><p>修改 mysql-master 的 mysqld.cnf：</p><pre><code class="language-shell"># 修改 master mysqld.cnfvi /etc/my.cnf# master idserver_id=1# 开启二进制日志log-bin=mysql-bin# 忽略系统库的数据同步binlog-ignore-db=information_schema binlog-ignore-db=mysql binlog-ignore-db=performance_schema binlog-ignore-db=sys</code></pre><p>修改 mysql-slave-1 的 mysqld.cnf：</p><pre><code class="language-shell"># 修改 mysql-slave-1 mysqld.cnfvi /etc/my.cnf# slave idserver-id=2# 中继日志relay_log=mysql-relay-bin# 只读关闭read_only=0# master 备用机开启log-bin=mysql-bin# 忽略的库，要和主库保持一致！！！！！！binlog-ignore-db=information_schema binlog-ignore-db=mysql binlog-ignore-db=performance_schema binlog-ignore-db=sys</code></pre><p>修改 mysql-slave-2 的 mysqld.cnf：</p><pre><code class="language-shell"># 修改 mysql-slave-2 mysqld.cnfvi /etc/my.cnf# slave idserver-id=3# 中继日志relay_log=mysql-relay-bin# 只读read_only=1# 忽略的库，要和主库保持一致！！！！！！binlog-ignore-db=information_schema binlog-ignore-db=mysql binlog-ignore-db=performance_schema binlog-ignore-db=sys</code></pre><p>配置完毕，启动 3 台 MySQL：</p><pre><code class="language-shell"># 启动 mysqld，卡住使用 ctrl+c 返回systemctl start mysqld</code></pre><blockquote><p>执行 <code>systemctl start mysqld</code> 如果一直卡住， <strong>Ctrl+C</strong>退出就行，只要 mysql 命令行工具可以正常连接基本上没什么影响</p></blockquote><h3 id="4.2-%E5%BC%80%E5%90%AF%E5%8D%8A%E5%90%8C%E6%AD%A5%E5%A4%8D%E5%88%B6" tabindex="-1">4.2 开启半同步复制</h3><ul><li>mysql-master 开启半同步复制：</li></ul><pre><code class="language-shell"># 查看默认密码cat /var/log/mysqld.log | grep password# 初始密码登录mysql -uroot -p# 修改root密码，请确认密码规则ALTER USER &#39;root&#39;@&#39;localhost&#39; IDENTIFIED BY &#39;root&#39;;# master开启授权grant replication slave on *.* to &#39;root&#39;@&#39;%&#39; identified by &#39;root&#39;;grant all privileges on *.* to &#39;root&#39;@&#39;%&#39; identified by &#39;root&#39;;# 安装 半同步复制 master 插件install plugin rpl_semi_sync_master soname &#39;semisync_master.so&#39;;# 开启 半同步复制 插件set global rpl_semi_sync_master_enabled=1;# 修改同步时间 1000 msset global rpl_semi_sync_master_timeout=1000;# 刷新flush privileges;# 查看 master 状态，若为空请重启 MySQLshow master status;</code></pre><p><img src="https://notes.suremotoo.cc/upload/2020/11/show-master-status-840a522e43b441ab8e9138817388c82a.png" alt="show-master-status" /></p><ul><li>2 个 mysql-slave 开启半同步复制：</li></ul><pre><code class="language-shell"># 查看默认密码cat /var/log/mysqld.log | grep password# 初始密码登录mysql -uroot -p# 修改root密码，请确认密码规则ALTER USER &#39;root&#39;@&#39;localhost&#39; IDENTIFIED BY &#39;root&#39;;# 开启远程登录grant all privileges on *.* to &#39;root&#39;@&#39;%&#39; identified by &#39;root&#39;;# slave 设置 masterchange master to master_host=&#39;172.17.0.2&#39;,master_port=3306,master_user=&#39;root&#39;,master_password=&#39;root&#39;,master_log_file=&#39;mysql-bin.000001&#39;,master_log_pos=869;# 安装 半同步复制 slave 插件install plugin rpl_semi_sync_slave soname &#39;semisync_slave.so&#39;;# 开启 半同步复制 插件set global rpl_semi_sync_slave_enabled=1;# 刷新flush privileges;# 开启 slavestart slave;# 查看 slave 状态show slave status \G</code></pre><h2 id="05.-%E5%BC%80%E5%90%AF-mha" tabindex="-1">05. 开启 MHA</h2><h3 id="5.1-%E9%85%8D%E7%BD%AE-ssh-%E7%99%BB%E5%BD%95%E6%97%A0%E5%AF%86%E7%A0%81%E9%AA%8C%E8%AF%81" tabindex="-1">5.1 配置 SSH 登录无密码验证</h3><p>在 <strong>4 个容器</strong> 间配置 SSH 登录无密码验证</p><pre><code class="language-shell">## 配置 SSH 登录无密码验证yum -y install openssh-clients# 配置 ssh key，默认即可，会在 /root/.ssh/ 生成 id_rsa 和 id_rsa.pub 文件ssh-keygen -t rsa</code></pre><p>在容器 <strong>mysql-master（172.17.0.2）</strong> 中执行：</p><pre><code class="language-shell"># 配置 SSH 登录无密码验证ssh-copy-id -i /root/.ssh/id_rsa.pub root@172.17.0.3ssh-copy-id -i /root/.ssh/id_rsa.pub root@172.17.0.4</code></pre><blockquote><p>执行 <code>ssh-copy-id -i /root/.ssh/id_rsa.pub root@172.17.0.3</code> 时的示意图如下，其余的几个方式一样</p><p><img src="https://notes.suremotoo.cc/upload/2020/11/ssh-no-password-edc08d0283904b3ca2a928ab845df8b3.png" alt="ssh-no-password" /></p></blockquote><p>在容器 <strong>mysql-slave-1（172.17.0.3）</strong> 中执行：</p><pre><code class="language-shell">ssh-copy-id -i /root/.ssh/id_rsa.pub root@172.17.0.2ssh-copy-id -i /root/.ssh/id_rsa.pub root@172.17.0.4</code></pre><p>在容器 <strong>mysql-slave-2（172.17.0.4）</strong> 中执行：</p><pre><code class="language-shell">ssh-copy-id -i /root/.ssh/id_rsa.pub root@172.17.0.2ssh-copy-id -i /root/.ssh/id_rsa.pub root@172.17.0.3</code></pre><p>在容器 <strong>mysql-mha（172.17.0.5）</strong> 中执行：</p><pre><code class="language-shell">ssh-copy-id -i /root/.ssh/id_rsa.pub root@172.17.0.2ssh-copy-id -i /root/.ssh/id_rsa.pub root@172.17.0.3ssh-copy-id -i /root/.ssh/id_rsa.pub root@172.17.0.4</code></pre><h3 id="5.2-%E9%85%8D%E7%BD%AE-manager" tabindex="-1">5.2 配置 manager</h3><p>在 <strong>mysql-mha(172.17.0.5)</strong> 中创建 mha 配置文件，先提前把该文件及目录创建出来</p><pre><code class="language-shell">cd /etcmkdir masterhacd masterhatouch mha.cnf</code></pre><p>然后再编辑 mha.cnf 的内容</p><pre><code class="language-shell"># 创建 mha 配置文件vi /etc/masterha/mha.cnf</code></pre><p><strong>mha.cnf</strong> 文件内容：</p><pre><code class="language-shell">[server default]# mysql user and passworduser=rootpassword=rootssh_user=root# working directory on the managermanager_workdir=/var/log/masterha/mha# working directory on MySQL serversremote_workdir=/var/log/masterha/mha# log filemanager_log=/var/log/masterha/MHA.log# mysql-master[server1]hostname=172.17.0.2# mysql-slave-1[server2]hostname=172.17.0.3# 作为备用主节点candidate_master=1# mysql-slave-2[server3]hostname=172.17.0.4</code></pre><h3 id="5.3-%E6%A3%80%E6%9F%A5-manager" tabindex="-1">5.3 检查 manager</h3><p>配置完成后，首先来检查容器间的 SSH：</p><pre><code class="language-shell">masterha_check_ssh --conf=/etc/masterha/mha.cnf</code></pre><p><img src="https://notes.suremotoo.cc/upload/2020/11/masterha_check_ssh-7355f1e785f3462ab1de0a6b526ec91f.png" alt="masterha_check_ssh" /></p><p>检查没有问题后，再来检查 MySQL 的主从复制：</p><pre><code class="language-shell">masterha_check_repl --conf=/etc/masterha/mha.cnf</code></pre><p><img src="https://notes.suremotoo.cc/upload/2020/11/masterha_check_repl-f7e6c35d01414c19a706075e883140dd.png" alt="masterha_check_repl" /></p><h3 id="5.4-%E5%90%AF%E5%8A%A8-manager" tabindex="-1">5.4 启动 manager</h3><p>检查完成没有问题后，启动 manage 查看，没有错误 <strong>Ctrl + C</strong> 退出即可，再使用后台启动的方式</p><pre><code class="language-shell">masterha_manager --conf=/etc/masterha/mha.cnf</code></pre><p>后台启动：</p><pre><code class="language-shell"># 后台启动nohup masterha_manager --conf=/etc/masterha/mha.cnf &lt; /dev/null &gt; /var/log/masterha/mha/mha.log 2&gt;&amp;1 &amp;</code></pre><p>后台启动可查看 manager 状态</p><pre><code class="language-shell">masterha_check_status --conf=/etc/masterha/mha.cnf# 输出mha (pid:17594) is running(0:PING_OK), master:172.17.0.2</code></pre><p>示意图：</p><p><img src="https://notes.suremotoo.cc/upload/2020/11/MHA-status-85be29661129418d9adff4e2351266ed.png" alt="MHA-status" /></p><hr><p><strong>注意：当完成一次正常的故障转移后，manager 进程将会终止。</strong></p><hr><p>手动停止 manager-<strong>暂时不需要手动</strong></p><pre><code class="language-shell">masterha_stop --conf=/etc/masterha/mha.cnf</code></pre><h3 id="5.5-%E6%B5%8B%E8%AF%95" tabindex="-1">5.5 测试</h3><p>去 **mysql-master（172.17.0.2）**上手动停止 mysql-master 服务</p><pre><code class="language-shell">systemctl stop mysqld</code></pre><p>然后去 <strong>mysql-mha（172.17.0.5）</strong> 查看 mysql-mha 输出：</p><pre><code class="language-shell">tail -400f MHA.log</code></pre><p>切换成功示意图</p><p><img src="https://notes.suremotoo.cc/upload/2020/11/MHA-change-master-1f6d744508fe43efa32b3ab776345fb0.png" alt="MHA-change-master" /></p>]]>
                    </description>
                    <pubDate>Thu, 26 Nov 2020 15:13:02 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[MySQL 主从搭建及配置]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/mysql-master-slave-config</link>
                    <description>
                            <![CDATA[<h2 id="%E5%87%86%E5%A4%87%E7%8E%AF%E5%A2%83" tabindex="-1">准备环境</h2><h3 id="%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%88%97%E8%A1%A8%EF%BC%88%E8%99%9A%E6%8B%9F%E6%9C%BA%E3%80%81docker-%E5%9D%87%E5%8F%AF%EF%BC%8Cip-%E4%BB%A5%E5%AE%9E%E9%99%85%E4%B8%BA%E5%87%86%EF%BC%89" tabindex="-1">服务器列表（虚拟机、Docker 均可，ip 以实际为准）</h3><table><thead><tr><th style="text-align:left">角色</th><th style="text-align:left">IP</th><th style="text-align:left">主机名</th><th style="text-align:left">server_id</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td style="text-align:left">Master</td><td style="text-align:left">172.17.0.2</td><td style="text-align:left">mysql-master</td><td style="text-align:left">1</td><td style="text-align:left">主库-写请求</td></tr><tr><td style="text-align:left">Slave1</td><td style="text-align:left">172.17.0.3</td><td style="text-align:left">mysql-slave-1</td><td style="text-align:left">2</td><td style="text-align:left">从库</td></tr><tr><td style="text-align:left">Slave2</td><td style="text-align:left">172.17.0.4</td><td style="text-align:left">mysql-slave-2</td><td style="text-align:left">3</td><td style="text-align:left">从库</td></tr></tbody></table><p>给 Master、Slave1、Slave2 三台 DB 数据库服务器安装 MySQL，可以先将 MySQL 安装包上传至服务器（这里就不演示了）</p><pre><code class="language-shell">[root@localhost ~]# lsanaconda-ks.cfg  mysql-5.7.28-1.el7.x86_64.rpm-bundle.tar[root@localhost ~]# tar -xvf mysql-5.7.28-1.el7.x86_64.rpm-bundle.tar mysql-community-embedded-5.7.28-1.el7.x86_64.rpmmysql-community-libs-compat-5.7.28-1.el7.x86_64.rpmmysql-community-devel-5.7.28-1.el7.x86_64.rpmmysql-community-embedded-compat-5.7.28-1.el7.x86_64.rpmmysql-community-libs-5.7.28-1.el7.x86_64.rpmmysql-community-test-5.7.28-1.el7.x86_64.rpmmysql-community-common-5.7.28-1.el7.x86_64.rpmmysql-community-embedded-devel-5.7.28-1.el7.x86_64.rpmmysql-community-client-5.7.28-1.el7.x86_64.rpmmysql-community-server-5.7.28-1.el7.x86_64.rpm</code></pre><p>查看服务器中是否有自带的 mariadb，如果有，要移除，否则会冲突</p><pre><code class="language-shell">[root@localhost ~]# rpm -qa|grep mariadbmariadb-libs-5.5.41-2.el7_0.x86_64[root@localhost ~]# rpm -e mariadb-libs-5.5.41-2.el7_0.x86_64 --nodeps## 再次查看[root@localhost ~]# rpm -qa|grep mariadb</code></pre><p>正式安装 MySQL，注意安装顺序</p><ol><li>先来 mysql-community-common</li></ol><pre><code class="language-shell">[root@localhost ~]# rpm -ivh mysql-community-common-5.7.28-1.el7.x86_64.rpm warning: mysql-community-common-5.7.28-1.el7.x86_64.rpm: Header V3 DSA/SHA1 Signature, key ID 5072e1f5: NOKEYPreparing...                          ################################# [100%]Updating / installing...   1:mysql-community-common-5.7.28-1.e################################# [100%]</code></pre><ol start="2"><li>再来 mysql-community-libs</li></ol><pre><code class="language-shell">[root@localhost ~]# rpm -ivh mysql-community-libs-5.7.28-1.el7.x86_64.rpm warning: mysql-community-libs-5.7.28-1.el7.x86_64.rpm: Header V3 DSA/SHA1 Signature, key ID 5072e1f5: NOKEYPreparing...                          ################################# [100%]Updating / installing...   1:mysql-community-libs-5.7.28-1.el7################################# [100%]</code></pre><ol start="3"><li>再是 mysql-community-libs-compat</li></ol><pre><code class="language-shell">[root@localhost ~]# rpm -ivh mysql-community-libs-compat-5.7.28-1.el7.x86_64.rpm warning: mysql-community-libs-compat-5.7.28-1.el7.x86_64.rpm: Header V3 DSA/SHA1 Signature, key ID 5072e1f5: NOKEYPreparing...                          ################################# [100%]Updating / installing...   1:mysql-community-libs-compat-5.7.2################################# [100%]</code></pre><ol start="4"><li>再接着 mysql-community-client</li></ol><pre><code class="language-shell">[root@localhost ~]# rpm -ivh mysql-community-client-5.7.28-1.el7.x86_64.rpm warning: mysql-community-client-5.7.28-1.el7.x86_64.rpm: Header V3 DSA/SHA1 Signature, key ID 5072e1f5: NOKEYPreparing...                          ################################# [100%]Updating / installing...   1:mysql-community-client-5.7.28-1.e################################# [100%]</code></pre><ol start="5"><li>再接着 mysql-community-server</li></ol><pre><code class="language-shell">[root@localhost ~]# rpm -ivh mysql-community-server-5.7.28-1.el7.x86_64.rpm warning: mysql-community-server-5.7.28-1.el7.x86_64.rpm: Header V3 DSA/SHA1 Signature, key ID 5072e1f5: NOKEYPreparing...                          ################################# [100%]Updating / installing...   1:mysql-community-server-5.7.28-1.e################################# [100%]</code></pre><ol start="6"><li>再接着 mysql-community-devel</li></ol><pre><code class="language-shell">[root@localhost ~]# rpm -ivh mysql-community-devel-5.7.28-1.el7.x86_64.rpm warning: mysql-community-devel-5.7.28-1.el7.x86_64.rpm: Header V3 DSA/SHA1 Signature, key ID 5072e1f5: NOKEYPreparing...                          ################################# [100%]Updating / installing...   1:mysql-community-devel-5.7.28-1.el################################# [100%]</code></pre><p>初始化数据库</p><pre><code class="language-shell">[root@localhost ~]# mysqld --initialize --user=mysql</code></pre><p>查看初始化后 MySQL 的账户名和密码</p><pre><code class="language-shell">[root@localhost ~]# cat /var/log/mysqld.log 2020-11-22T16:56:05.518869Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).2020-11-22T16:56:06.260995Z 0 [Warning] InnoDB: New log files created, LSN=457902020-11-22T16:56:06.368797Z 0 [Warning] InnoDB: Creating foreign key constraint system tables.2020-11-22T16:56:06.431422Z 0 [Warning] No existing UUID has been found, so we assume that this is the first time that this server has been started. Generating a new UUID: 9cc56a78-2ce3-11eb-9cb5-000c29150471.2020-11-22T16:56:06.496706Z 0 [Warning] Gtid table is not ready to be used. Table &#39;mysql.gtid_executed&#39; cannot be opened.2020-11-22T16:56:07.122745Z 0 [Warning] CA certificate ca.pem is self signed.2020-11-22T16:56:07.302638Z 1 [Note] A temporary password is generated for root@localhost: ceZbIi5+OGbd</code></pre><p>最后的 root@localhost: ceZbIi5+OGbd，<strong>eZbIi5+OGbd</strong> 这个就是用户名和密码</p><p>启动 MySQL</p><pre><code class="language-shell">[root@localhost ~]# systemctl start mysqld.service</code></pre><p>查看启动状态</p><pre><code class="language-shell">[root@localhost ~]# systemctl status mysqld.servicemysqld.service - MySQL Server   Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled)   Active: active (running) since Sun 2020-11-22 08:59:05 PST; 21s ago     Docs: man:mysqld(8)           http://dev.mysql.com/doc/refman/en/using-systemd.html  Process: 12003 ExecStart=/usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid $MYSQLD_OPTS (code=exited, status=0/SUCCESS)  Process: 11981 ExecStartPre=/usr/bin/mysqld_pre_systemd (code=exited, status=0/SUCCESS) Main PID: 12006 (mysqld)   CGroup: /system.slice/mysqld.service           └─12006 /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid</code></pre><p>登录 MySQL 命令行客户端，并修改密码</p><pre><code class="language-shell">[root@localhost ~]# mysql -uroot -pEnter password: Welcome to the MySQL monitor.  Commands end with ; or \g.Your MySQL connection id is 2Server version: 5.7.28Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.Oracle is a registered trademark of Oracle Corporation and/or itsaffiliates. Other names may be trademarks of their respectiveowners.Type &#39;help;&#39; or &#39;\h&#39; for help. Type &#39;\c&#39; to clear the current input statement.mysql&gt; set password=password(&#39;root&#39;);Query OK, 0 rows affected, 1 warning (0.00 sec)</code></pre><p><strong>exit</strong> 命令退出，重新登录</p><p>最好也关闭一下防火墙，免得有其他杂七杂八的问题</p><pre><code class="language-shell">[root@localhost ~]# systemctl stop iptables[root@localhost ~]# systemctl stop firewalld[root@localhost ~]# systemctl disable firewalld.service</code></pre><h2 id="%E4%B8%BB%E4%BB%8E%E9%85%8D%E7%BD%AE" tabindex="-1">主从配置</h2><h3 id="%E4%B8%BB%E5%BA%93%E7%9A%84%E9%85%8D%E7%BD%AE" tabindex="-1">主库的配置</h3><h4 id="%E7%BC%96%E8%BE%91-my.cnf-%E6%96%87%E4%BB%B6" tabindex="-1">编辑 my.cnf 文件</h4><pre><code class="language-shell">vi /etc/my.cnf</code></pre><p><strong>按字母 i</strong> 进入编辑模式，在文件中添加一下配置：</p><pre><code class="language-"># 开启 binlog，后面配置从库的时候要保持一致log_bin=mysql-bin# 设置服务 id，每台服务器的 server 保持唯一，不可重复server-id=1# 每次 binlog 操作都进行磁盘同步sync-binlog=1# 忽略的库,后面配置从库的时候要保持一致binlog-ignore-db=performance_schemabinlog-ignore-db=information_schemabinlog-ignore-db=sysbinlog-ignore-db=mysql</code></pre><p><strong>server-id</strong>这里主设置为 1，从依次往后排，2、3…</p><p>编辑完成后，按 Esc 退出编辑模式，再输入 <code>:wq</code> 进行保存退出</p><p>紧接着在终端中重启 mysql 服务</p><pre><code class="language-shell">systemctl restart mysqld</code></pre><p>连接主客户端，进行操作授权</p><pre><code class="language-shell">mysql -uroot -pEnter password: 这里输入密码，我的是：rootmysql&gt; grant replication slave on *.* to &#39;root&#39;@&#39;%&#39; identified by &#39;root&#39;;mysql&gt; grant all privileges on *.* to &#39;root&#39;@&#39;%&#39; identified by &#39;root&#39;;mysql&gt; flush privileges;</code></pre><p>查看主库状态：</p><pre><code class="language-shell">mysql&gt; show master status;</code></pre><p>注意下图中标红的地方，配置从库会用到</p><p><img src="https://notes.suremotoo.cc/upload/2020/11/show-master-status-840a522e43b441ab8e9138817388c82a.png" alt="show-master-status" /></p><h3 id="%E4%BB%8E%E5%BA%93-1-%E7%9A%84%E9%85%8D%E7%BD%AE" tabindex="-1">从库 1 的配置</h3><p>同理，编辑 my.cnf 文件，并重启</p><pre><code class="language-"># 设置服务 id，每台服务器的 server 保持唯一，不可重复server-id=2relay_log=mysql-relay-bin# 只读read_only=1# 忽略的库,后面配置从库的时候要保持一致binlog-ignore-db=performance_schemabinlog-ignore-db=information_schemabinlog-ignore-db=sysbinlog-ignore-db=mysql</code></pre><p>再登录查询从库状态</p><pre><code class="language-shell">mysql&gt; show slave status;</code></pre><p>再配置从哪个主库同步</p><pre><code class="language-shell">mysql&gt; change master to master_host=&#39;172.17.0.2&#39;,master_port=3306,master_user=&#39;root&#39;,master_password=&#39;root&#39;,master_log_file=&#39;mysql-bin.000001&#39;,master_log_pos=869;</code></pre><p>这里 <strong>master_log_file</strong> 和 <strong>master_log_pos</strong> 对应下面查询主库状态图中的红线</p><p><img src="https://notes.suremotoo.cc/upload/2020/11/show-master-status-840a522e43b441ab8e9138817388c82a.png" alt="show-master-status" /></p><p>然后开启从库配置</p><pre><code class="language-mysql">mysql&gt; start slave;</code></pre><p>然后就可以查询从库的状态</p><pre><code class="language-shell">mysql&gt; show slave staus;</code></pre><h3 id="%E4%BB%8E%E5%BA%93-2-%E7%9A%84%E9%85%8D%E7%BD%AE" tabindex="-1">从库 2 的配置</h3><p>同理从库 2 同从库 1 配置一样，注意 <strong>server-id 改成 3</strong></p><h4 id="%E5%88%87%E5%9B%9E%E4%B8%BB%E5%BA%93%E5%88%9B%E5%BB%BA%E8%A1%A8%E8%AF%AD%E5%8F%A5" tabindex="-1">切回主库创建表语句</h4><h4 id="%E5%9C%A8%E4%B8%BB%E5%BA%93%E6%AF%8F%E6%89%A7%E8%A1%8C%E4%B8%80%E6%9D%A1%E8%AF%AD%E5%8F%A5%EF%BC%8C%E5%B0%B1%E5%8E%BB%E4%B8%A4%E4%B8%AA%E4%BB%8E%E5%BA%93%E6%9F%A5%E8%AF%A2%E4%B8%80%E4%B8%8B%EF%BC%8C%E6%98%AF%E5%90%A6%E5%90%8C%E6%AD%A5%E5%88%B0%E4%BB%8E%E5%BA%93" tabindex="-1">在主库每执行一条语句，就去两个从库查询一下，是否同步到从库</h4><pre><code class="language-sql"># 创建库create database suremotoo_master;# 创建完数据库，可以去从库查询一下，从库也会有该数据库# 商品表CREATE TABLE products(    pid INT PRIMARY KEY,    pname VARCHAR(50),    price DOUBLE,    flag VARCHAR(2)   #是否上架标记为：1表示上架、0表示下架) engine=innodb charset=utf8;# 创建完表，可以去从库查询一下，从库也会有该表# 再尝试插入一条数据INSERT INTO products(pid,pname,price,flag) VALUES(&#39;1&#39;,&#39;联想&#39;,5000.00,&#39;1&#39;);</code></pre><p>这里就不一一展示了，看个效果图</p><p><img src="https://notes.suremotoo.cc/upload/2020/11/sync-database-and-table-and-data-64c91192ab414bf0bd39ba4a7b8e029a.png" alt="sync-database-and-table-and-data" /></p><h2 id="%E5%8D%8A%E5%90%8C%E6%AD%A5%E5%A4%8D%E5%88%B6" tabindex="-1">半同步复制</h2><p>先登录主库的命令行客户端</p><pre><code class="language-shell">mysql -uroot -pEnter password: root## 查看是否支持动态加载插件mysql&gt; select @@have_dynamic_loading;</code></pre><p><img src="https://notes.suremotoo.cc/upload/2020/11/have-dynamic-loading-25e0bf84f6d84b17b821240e7b86ee02.png" alt="have-dynamic-loading" /></p><p><strong>YES</strong> 说明支持</p><p>接下来安装半同步工具插件</p><pre><code class="language-shell">mysql&gt; install plugin rpl_semi_sync_master soname &#39;semisync_master.so&#39;;</code></pre><p>安装完成后查看插件信息</p><pre><code class="language-shell">mysql&gt; show variables like &#39;%semi%&#39;;</code></pre><p><img src="https://notes.suremotoo.cc/upload/2020/11/semi-sync-enabled-df16101b99ac4ac3b8e6bd13c73e95d8.png" alt="semi-sync-enabled" /></p><p><strong>rpl_semi_sync_master_enabled</strong> 的值为 <strong>OFF</strong>， 说明现在处于关闭状态</p><p><strong>rpl_semi_sync_master_timeout</strong>是延迟时间（单位毫秒），当前是 10 秒</p><p>我们调整一下这两个参数</p><pre><code class="language-shell">mysql&gt; set global rpl_semi_sync_master_enabled=1;mysql&gt; set global rpl_semi_sync_master_timeout=1000;</code></pre><p>最后还可以再进行查看：</p><pre><code class="language-shell">mysql&gt; show variables like &#39;%semi%&#39;;</code></pre><p><img src="https://notes.suremotoo.cc/upload/2020/11/semi-sync-enabled-on-055c5991bb7748fe90a555058c834537.png" alt="semi-sync-enabled-on" /></p><p>然后登陆从库，进行安装插件配置！</p><p>同样登陆命令行客户端（此处登陆就略过了…）</p><pre><code class="language-shell">mysql&gt; install plugin rpl_semi_sync_slave soname &#39;semisync_slave.so&#39;;</code></pre><p>也可以查看下同步配置信息：</p><pre><code class="language-shell">mysql&gt; show variables like &#39;%semi%&#39;;</code></pre><p><img src="https://notes.suremotoo.cc/upload/2020/11/semi-sync-slave-config-b9bd122cc943462db82662fcdc90f176.png" alt="semi-sync-slave-config" /></p><p>从库的配置信息比较少，我们就调整将其打开就行</p><pre><code class="language-shell">mysql&gt; set global rpl_semi_sync_slave_enabled=1;# 打开后再查看mysql&gt; show variables like &#39;%semi%&#39;;</code></pre><p>最后，我们将从库重新进行加载</p><pre><code class="language-shell">mysql&gt; stop slave;mysql&gt; start slave;</code></pre><p>回到主库，再进行插入数据测试，看看从库是否同步~ 😁😁，这里就不演示了</p>]]>
                    </description>
                    <pubDate>Thu, 26 Nov 2020 14:54:53 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[手写迷你版 Tomcat - Minicat]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/handwriting-minicat</link>
                    <description>
                            <![CDATA[<h1 id="%E6%89%8B%E5%86%99%E8%BF%B7%E4%BD%A0%E7%89%88-tomcat---minicat" tabindex="-1">手写迷你版 Tomcat - Minicat</h1><h2 id="minicat-%E7%9A%84%E7%9B%AE%E6%A0%87" tabindex="-1">Minicat 的目标</h2><p>我们可以通过浏览器客户端发送 http 请求， Minicat 可以接收到请求进⾏处理，处理之后的结果可以返回浏览器客户端。</p><h4 id="%E5%9F%BA%E6%9C%AC%E6%96%B9%E5%90%91" tabindex="-1">基本方向</h4><ul><li><p>提供服务，接收请求（Socket 通信）</p></li><li><p>请求信息封装成 Request 对象，同样响应信息封装成 Response 对象</p></li><li><p>客户端请求资源，资源分为静态资源（HTML）和动态资源（Servlet）</p></li><li><p>资源返回给客户端浏览器</p></li></ul><h4 id="%E8%BF%AD%E4%BB%A3%E5%AE%9E%E7%8E%B0" tabindex="-1">迭代实现</h4><p>我们实现时候呢，一步一步来，可以制定的小版本计划</p><ul><li>V1.0 需求：浏览器请求 <a href="http://localhost:8080" target="_blank">http://localhost:8080</a>, 返回⼀个固定的字符串到⻚⾯&quot;Hello Minicat.&quot;</li><li>V2.0 需求：封装 Request 和 Response 对象，返回 HTML 静态资源⽂件</li><li>V3.0 需求：可以请求动态资源（Servlet）</li><li>V4.0 需求：可以多线程访问</li><li>V5.0 需求：在已有 Minicat 基础上进⼀步扩展，模拟出 webapps 部署效果,磁盘上放置⼀个 webapps ⽬录，webapps 中可以有多个项⽬，⽐如 demo1,demo2,demo3… 具体的项⽬⽐如 demo1 中有 serlvet（也即为：servlet 是属于具体某⼀个项⽬的 servlet），这样的话在 Minicat 初始化配置加载，以及根据请求 url 查找对应 serlvet 时都需要进⼀步处理。</li></ul><h2 id="v1.0-%E7%89%88%E6%9C%AC" tabindex="-1">V1.0 版本</h2><h3 id="%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA" tabindex="-1">环境搭建</h3><p>确定好方向，就进行项目搭建开发</p><p>新建一个 Maven 项目，并调整在 <code>pom.xml</code></p><pre><code class="language-xml">  &lt;groupId&gt;site.suremotoo&lt;/groupId&gt;  &lt;artifactId&gt;Minicat&lt;/artifactId&gt;  &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;  &lt;build&gt;      &lt;plugins&gt;          &lt;plugin&gt;              &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;              &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;              &lt;version&gt;3.1&lt;/version&gt;              &lt;configuration&gt;                  &lt;source&gt;11&lt;/source&gt;                  &lt;target&gt;11&lt;/target&gt;                  &lt;encoding&gt;utf-8&lt;/encoding&gt;              &lt;/configuration&gt;          &lt;/plugin&gt;      &lt;/plugins&gt;  &lt;/build&gt;</code></pre><p>编写启动类 <code>Bootstrap</code></p><pre><code class="language-java">public class Bootstrap {  /**   * 设定启动和监听端口   */  private int port = 8080;  /**   * 启动函数   *   * @throws IOException   */  public void start() throws IOException {      System.out.println(&quot;Minicat starting...&quot;);      String responseData = &quot;Hello Minicat.&quot;;      ServerSocket socket = new ServerSocket(port);      while (true) {          Socket accept = socket.accept();          OutputStream outputStream = accept.getOutputStream();          String responseText = HttpProtocolUtil.getHttpHeader200(responseData.length()) + responseData;          outputStream.write(responseText.getBytes());          accept.close();      }  }  /**   * 启动入口   *   * @param args   */  public static void main(String[] args) throws IOException {      Bootstrap bootstrap = new Bootstrap();      bootstrap.start();  }}</code></pre><p>HTTP 协议辅助类 <code>HttpProtocolUtil</code></p><pre><code class="language-java">public class HttpProtocolUtil {    /**     * 200 状态码，头信息     *     * @param contentLength 响应信息长度     * @return 200 header info     */    public static String getHttpHeader200(long contentLength) {        return &quot;HTTP/1.1 200 OK \n&quot; + &quot;Content-Type: text/html \n&quot;                + &quot;Content-Length: &quot; + contentLength + &quot; \n&quot; + &quot;\r\n&quot;;    }    /**     * 为响应码 404 提供请求头信息(此处也包含了数据内容)     *     * @return 404 header info     */    public static String getHttpHeader404() {        String str404 = &quot;&lt;h1&gt;404 not found&lt;/h1&gt;&quot;;        return &quot;HTTP/1.1 404 NOT Found \n&quot; + &quot;Content-Type: text/html \n&quot;                + &quot;Content-Length: &quot; + str404.getBytes().length + &quot; \n&quot; + &quot;\r\n&quot; + str404;    }}</code></pre><p>然后我们访问浏览器：<a href="http://localhost:8080" target="_blank">http://localhost:8080</a>，页面显示 <strong>Hello Minicat.</strong>,就说明成功啦。</p><p>这就完成 V1.0 版本了 🎉</p><hr /><h2 id="v2.0-%E7%89%88%E6%9C%AC" tabindex="-1">V2.0 版本</h2><p>紧接着我们实现请求信息、响应信息的封装，即：<code>Request</code>、<code>Response</code> 的实现</p><pre><code class="language-java">public class Request {  /**   * 请求方式, eg: GET、POST   */  private String method;  /**   * 请求路径,eg: /index.html   */  private String url;  /**   * 请求信息输入流 &lt;br&gt;   * 示例   * &lt;pre&gt;   *  GET / HTTP/1.1   *  Host: www.baidu.com   *  Connection: keep-alive   *  Pragma: no-cache   *  Cache-Control: no-cache   *  Upgrade-Insecure-Requests: 1   *  User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36   * &lt;/pre&gt;   */  private InputStream inputStream;  public Request() {  }  public Request(InputStream inputStream) throws IOException {      this.inputStream = inputStream;      int count = 0;      while (count == 0) {          count = inputStream.available();      }      byte[] bytes = new byte[count];      inputStream.read(bytes);      // requestString 参考：this.inputStream 示例      String requestString = new String(bytes);      // 按换行分隔      String[] requestStringArray = requestString.split(&quot;\\n&quot;);      // 读取第一行数据,即： GET / HTTP/1.1      String firstLine = requestStringArray[0];      // 把厉第一行数据按空格分隔      String[] firstLineArray = firstLine.split(&quot; &quot;);      this.method = firstLineArray[0];      this.url = firstLineArray[1];  }}</code></pre><p>Response</p><pre><code class="language-java">public class Response {    /**     * 响应信息     */    private OutputStream outputStream;    public Response() {    }    public Response(OutputStream outputStream) {        this.outputStream = outputStream;    }    public void output(String content) throws IOException {        outputStream.write(content.getBytes());    }    /**     * 根据 url 拼接绝对路径，根据绝对路径再读取文件资源，再响应     *     * @param url 请求 url     */    public void outputHtml(String url) throws IOException {        // 获取 url 资源的全路径        String absoluteResourcePath = StaticResourceUtil.getAbsolutePath(url);        File file = new File(absoluteResourcePath);        if (file.exists() &amp;&amp; file.isFile()) {            // 输出静态资源            StaticResourceUtil.outputStaticResource(new FileInputStream(file), outputStream);        } else {            output(HttpProtocolUtil.getHttpHeader404());        }    }}</code></pre><p>我们也提取了一些共用类和函数，<code>StaticResourceUtil</code></p><pre><code class="language-java">public class StaticResourceUtil {    /**     * 根据请求 url 获取完整绝对路径     *     * @param url 请求 url     * @return 完整绝对路径     */    public static String getAbsolutePath(String url) {        String path = StaticResourceUtil.class.getResource(&quot;/&quot;).getPath();        return path.replaceAll(&quot;\\\\&quot;, &quot;/&quot;) + url;    }    /**     * 输出静态资源信息     *     * @param inputStream  静态资源文件的读取流     * @param outputStream 响应的输出流     * @throws IOException     */    public static void outputStaticResource(InputStream inputStream, OutputStream outputStream) throws IOException {        int count = 0;        while (count == 0) {            count = inputStream.available();        }        // 输出 http 请求头,然后再输出具体内容        int resourceSize = count;        // 读取内容输出        outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());        // 已经读取的内容⻓度        long written = 0;        // 计划每次缓冲的⻓度        int byteSize = 1024;        byte[] bytes = new byte[byteSize];        while (written &lt; resourceSize) {            // 说明剩余未读取⼤⼩不⾜⼀个 1024 ⻓度，那就按真实⻓度处理            if (written + byteSize &gt; resourceSize) {                // 计算实际剩余内容                byteSize = (int) (resourceSize - written);                bytes = new byte[byteSize];            }            inputStream.read(bytes);            outputStream.write(bytes);            outputStream.flush();            // 计算已经读取的长度            written += byteSize;        }    }}</code></pre><p>最后调整一下<code>Bootstrap</code>中的<code>start()</code>函数</p><pre><code class="language-java">public void start() throws IOException {    System.out.println(&quot;Minicat starting...&quot;);    ServerSocket socket = new ServerSocket(port);    while (true) {        Socket accept = socket.accept();        OutputStream outputStream = accept.getOutputStream();        // 分别封装 Request 和 Response        Request request = new Request(accept.getInputStream());        Response response = new Response(outputStream);        // 根据 request 中的 url，输出        response.outputHtml(request.getUrl());        accept.close();    }}</code></pre><p>哦对了，还缺少 1 个具体的 HTML 静态资源⽂件，我们创建一个名为 <code>index.html</code> 的吧</p><pre><code class="language-html">&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;Hello Minicat&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    &lt;h1&gt;Hello Minicat,  index.html&lt;/h1&gt;&lt;/body&gt;&lt;/html&gt;</code></pre><p>然后我们访问浏览器：<a href="http://localhost:8080/index.html%EF%BC%8C%E9%A1%B5%E9%9D%A2%E6%98%BE%E7%A4%BA" target="_blank">http://localhost:8080/index.html，页面显示</a> <strong>Hello Minicat,  index.html</strong>,就说明成功啦。</p><p>这就完成 V2.0 版本了 🎉🎉</p><hr /><h2 id="v3.0-%E7%89%88%E6%9C%AC" tabindex="-1">V3.0 版本</h2><p>接下来就来实现请求动态资源</p><p>我们先定义 <code>Servlet</code> 的接口，指定规范</p><pre><code class="language-java">public interface Servlet {    void init() throws Exception;    void destroy() throws Exception;    void service(Request request, Response response) throws Exception;}</code></pre><p>紧接着再完成 1 个公共的抽象父类 <code>HttpServlet</code></p><pre><code class="language-java">public abstract class HttpServlet implements Servlet {    public abstract void doGet(Request request, Response response) throws Exception;    public abstract void doPost(Request request, Response response) throws Exception;    @Override    public void service(Request request, Response response) throws Exception {        String method = request.getMethod();        if (&quot;GET&quot;.equalsIgnoreCase(method)) {            doGet(request, response);        } else {            doPost(request, response);        }    }}</code></pre><p>实际的 <code>doGet</code>、<code>doPost</code> 由实际配置的业务 <code>Servlet</code> 来实现，那么我们就创建 1 个实际业务： <code>ShowServlet</code></p><pre><code class="language-java">public class ShowServlet extends HttpServlet {    @Override    public void doGet(Request request, Response response) throws Exception {        String repText = &quot;&lt;h1&gt;ShowServlet by GET&lt;/h1&gt;&quot;;        response.output(HttpProtocolUtil.getHttpHeader200(repText.length()) + repText);    }    @Override    public void doPost(Request request, Response response) throws Exception {        String repText = &quot;&lt;h1&gt;ShowServlet by POST&lt;/h1&gt;&quot;;        response.output(HttpProtocolUtil.getHttpHeader200(repText.length()) + repText);    }    @Override    public void init() throws Exception {}    @Override    public void destroy() throws Exception {}}</code></pre><p>我们再在项目 <code>resources</code> 文件夹中创建 <code>web.xml</code>，并配置我们的业务 Servlet: <code>ShowServlet</code> 的名称和访问路径</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;web-app&gt;    &lt;servlet&gt;        &lt;servlet-name&gt;show&lt;/servlet-name&gt;        &lt;servlet-class&gt;server.ShowServlet&lt;/servlet-class&gt;    &lt;/servlet&gt;    &lt;servlet-mapping&gt;        &lt;servlet-name&gt;show&lt;/servlet-name&gt;        &lt;url-pattern&gt;/show&lt;/url-pattern&gt;    &lt;/servlet-mapping&gt;&lt;/web-app&gt;</code></pre><p>既然配置了 <code>web.xml</code>，那我们还需要再对其进行解析和处理</p><p>解析 XML，我们在 <code>pom.xml</code> 里引入相关坐标依赖</p><pre><code class="language-xml">&lt;dependencies&gt;    &lt;dependency&gt;        &lt;groupId&gt;dom4j&lt;/groupId&gt;        &lt;artifactId&gt;dom4j&lt;/artifactId&gt;        &lt;version&gt;1.6.1&lt;/version&gt;    &lt;/dependency&gt;    &lt;dependency&gt;        &lt;groupId&gt;jaxen&lt;/groupId&gt;        &lt;artifactId&gt;jaxen&lt;/artifactId&gt;        &lt;version&gt;1.1.6&lt;/version&gt;    &lt;/dependency&gt;&lt;/dependencies&gt;</code></pre><p>我们把处理逻辑写在 <code>Bootstrap</code> 启动类中</p><pre><code class="language-java">/** * 存放 Servlet信息，url: Servlet 实例 */private Map&lt;String, HttpServlet&gt; servletMap = new HashMap&lt;&gt;();/** * 加载配置的 Servlet * * @throws Exception */public void loadServlet() throws Exception {    InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(&quot;web.xml&quot;);    SAXReader saxReader = new SAXReader();    Document document = saxReader.read(resourceAsStream);    Element rootElement = document.getRootElement();    List&lt;Element&gt; list = rootElement.selectNodes(&quot;//servlet&quot;);    for (Element element : list) {        // &lt;servlet-name&gt;show&lt;/servlet-name&gt;        Element servletnameElement = (Element) element.selectSingleNode(&quot;servlet-name&quot;);        String servletName = servletnameElement.getStringValue();        // &lt;servlet-class&gt;server.ShowServlet&lt;/servlet-class&gt;        Element servletclassElement = (Element) element.selectSingleNode(&quot;servlet-class&quot;);        String servletClass = servletclassElement.getStringValue();        // 根据 servlet-name 的值找到 url-pattern        Element servletMapping = (Element) rootElement.selectSingleNode(&quot;/web-app/servlet-mapping[servlet-name=&#39;&quot; + servletName + &quot;&#39;]&quot;);        // /show        String urlPattern = servletMapping.selectSingleNode(&quot;url-pattern&quot;).getStringValue();        servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).getDeclaredConstructor().newInstance());    }}</code></pre><p>再调整 <code>start</code> 方法，在方法里执行 <code>loadServlet</code> 函数来初始化 Servlet</p><pre><code class="language-java">public void start() throws Exception {      System.out.println(&quot;Minicat starting...&quot;);// 此处加载并初始化 Servlet  loadServlet();      ServerSocket socket = new ServerSocket(port);      while (true) {          Socket accept = socket.accept();          OutputStream outputStream = accept.getOutputStream();          // 分别封装 Request 和 Response          Request request = new Request(accept.getInputStream());          Response response = new Response(outputStream);        // 根据 url 来获取 Servlet          HttpServlet httpServlet = servletMap.get(request.getUrl());        // 如果 Servlet 为空，说明是静态资源，不为空即为动态资源，需要执行 Servlet 里的方法          if (httpServlet == null) {              response.outputHtml(request.getUrl());          } else {              httpServlet.service(request, response);          }          accept.close();      }  }</code></pre><p>然后我们访问浏览器: <a href="http://localhost:8080/index.html%EF%BC%8C%E9%A1%B5%E9%9D%A2%E6%98%BE%E7%A4%BA" target="_blank">http://localhost:8080/index.html，页面显示</a> <strong>Hello Minicat,  index.html</strong>,就说明<strong>静态资源</strong>访问成功啦。</p><p>我们再访问: <a href="http://localhost:8080/show" target="_blank">http://localhost:8080/show</a>, 页面显示 <strong>ShowServlet by GET</strong>,就说明<strong>动态资源</strong>也访问成功啦。</p><p>这就完成 V3.0 版本了 🎉🎉🎉</p><hr /><h2 id="v4.0-%E7%89%88%E6%9C%AC" tabindex="-1">V4.0 版本</h2><p>完成 V3.0 之后，3.0 还是有点问题的，比如在多线程访问情况下</p><p>我们假设业务 <code>ShowServlet</code> 里，动态资源处理延迟，这里使用 <code>Thread.sleep</code> 模拟延迟的情况</p><pre><code class="language-java">@Overridepublic void doGet(Request request, Response response) throws Exception {    // 模拟延迟    Thread.sleep(100000);    String repText = &quot;&lt;h1&gt;ShowServlet by GET&lt;/h1&gt;&quot;;    response.output(HttpProtocolUtil.getHttpHeader200(repText.length()) + repText);}</code></pre><p>然后再重新启动，先访问:<a href="http://localhost:8080/show%EF%BC%8C%E4%BC%9A%E5%8F%91%E7%8E%B0%E4%B8%80%E7%9B%B4%E5%9C%A8%E5%8A%A0%E8%BD%BD%E5%A4%84%E7%90%86%EF%BC%8C%E6%AD%A4%E6%97%B6%E5%86%8D%E5%8E%BB%E8%AE%BF%E9%97%AE" target="_blank">http://localhost:8080/show，会发现一直在加载处理，此时再去访问</a> <a href="http://localhost:8080/index.html%EF%BC%8C%E4%B9%9F%E4%BC%9A%E4%B8%80%E7%9B%B4%E5%9C%A8%E5%8A%A0%E8%BD%BD%E5%A4%84%E7%90%86%E3%80%82" target="_blank">http://localhost:8080/index.html，也会一直在加载处理。</a></p><p>意思就是我们访问动态资源时候的一直等待，此时，再去访问静态资源的时候，也会一直等待，这样就不行了。</p><p>这里就可以使用线程来解决，每个请求过来的 <code>Socket</code> 都分配 1 个线程，各自处理各自的请求</p><p>所以再在 <code>Bootstrap</code> 中的 <code>start</code> 方法做个改造</p><pre><code class="language-java">public void start() throws Exception {    System.out.println(&quot;Minicat starting...&quot;);    loadServlet();    ServerSocket socket = new ServerSocket(port);    while (true) {        Socket accept = socket.accept();        // 多线程改造，每个 socket 是个单独的线程        RequestProcessor requestProcessor = new RequestProcessor(accept, servletMap);        requestProcessor.start();    }}</code></pre><p><code>RequestProcessor</code> 就集成 <code>Thread</code> ，重写 <code>run</code> 函数，实现具体的输入输出的处理</p><pre><code class="language-java">public class RequestProcessor extends Thread {    private Socket socket;    private Map&lt;String, HttpServlet&gt; servletMap;    public RequestProcessor() {    }    public RequestProcessor(Socket socket, Map&lt;String, HttpServlet&gt; servletMap) {        this.socket = socket;        this.servletMap = servletMap;    }    @Override    public void run() {        try {            OutputStream outputStream = socket.getOutputStream();            // 分别封装 Request 和 Response            Request request = new Request(socket.getInputStream());            Response response = new Response(outputStream);            HttpServlet httpServlet = servletMap.get(request.getUrl());            if (httpServlet == null) {                response.outputHtml(request.getUrl());            } else {                httpServlet.service(request, response);            }            socket.close();        } catch (Exception e) {            e.printStackTrace();        }    }}</code></pre><p>每个都分配个线程，有点消耗资源，所以我们再升级下，使用<strong>线程池</strong>，所以再对 <code>start</code> 函数进行一次简单的调整</p><pre><code class="language-java">public void start() throws Exception {    System.out.println(&quot;Minicat starting...&quot;);    loadServlet();    // 定义一个线程池    int corePoolSize = 10;    int maximumPoolSize =50;    long keepAliveTime = 100L;    TimeUnit unit = TimeUnit.SECONDS;    BlockingQueue&lt;Runnable&gt; workQueue = new ArrayBlockingQueue&lt;&gt;(50);    ThreadFactory threadFactory = Executors.defaultThreadFactory();    RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(            corePoolSize,            maximumPoolSize,            keepAliveTime,            unit,            workQueue,            threadFactory,            handler    );    ServerSocket socket = new ServerSocket(port);    while (true) {        Socket accept = socket.accept();        // 多线程改造，每个 socket 是个单独的线程        RequestProcessor requestProcessor = new RequestProcessor(accept, servletMap);         // 线程池执行        threadPoolExecutor.execute(requestProcessor);    }}</code></pre><p>这就完成 V4.0 版本了 🎉🎉🎉🎉</p><hr /><h2 id="v5.0-%E7%89%88%E6%9C%AC" tabindex="-1">V5.0 版本</h2><p>开始之前，我们先捋一下之前实现 <code>Minicat</code> 的步骤，来看个图</p><p><img src="https://notes.suremotoo.cc/upload/2020/09/handwriting-minicat-process-1116d96e74d44b6c92edff5d86b171aa.png" alt="handwriting-minicat-process" /></p><p>5.0 版本来了，我们要模拟出 webapps 部署效果，这个功能说简单点了，就是<strong>把一个指定的文件夹 webapps 下面的所有的 <code>Servlet</code> 给读取出来并加载，然后在请求的时候去解析对应的 servlet</strong> 就行了。</p><p>那么我们就 <code>resources</code> 里写个配置文件：<code>server.xml</code> 来配置指定的文件夹路径信息，为了模仿实际的 Tomcat，我们再加几个标签</p><pre><code class="language-xml">&lt;Server&gt;    &lt;Service name=&quot;Mojave&quot;&gt;        &lt;Connector port=&quot;8080&quot;/&gt;        &lt;Engine defaultHost=&quot;localhost&quot;&gt;          &lt;!-- appBase 就是指定的文件夹 --&gt;            &lt;Host name=&quot;localhost&quot; appBase=&quot;/Users/suremotoo/Documents/webapps&quot;/&gt;        &lt;/Engine&gt;    &lt;/Service&gt;&lt;/Server&gt;</code></pre><p>而业务 Servlet 呢，肯定要在单独的项目中写了，Minicat 是要部署加载它们的，后面再说。</p><p>根据面向对象的思想，我们在编写一些 POJO 类</p><p><strong><code>Context</code></strong> 就是在指定的 webapps 目录中的，每个项目，<strong>每个项目里有自己的 Servlet 集合</strong></p><pre><code class="language-java">public class Context {    public Context() {    }    public Context(Map&lt;String, HttpServlet&gt; servletMap) {        this.servletMap = servletMap;    }  // Context 中的 Servlet    private Map&lt;String, HttpServlet&gt; servletMap;    public Map&lt;String, HttpServlet&gt; getServletMap() {        return servletMap;    }    public void setServletMap(Map&lt;String, HttpServlet&gt; servletMap) {        this.servletMap = servletMap;    }}</code></pre><p><strong><code>Host</code></strong> 作为配置中的主机信息，<strong>每个主机下面有自己的 Context 项目集合</strong></p><pre><code class="language-java">public class Host {    public Host() {    }    public Host(Map&lt;String, Context&gt; contextMap) {        this.contextMap = contextMap;    }  // hHst 中的 Context    private Map&lt;String, Context&gt; contextMap;    public Map&lt;String, Context&gt; getContextMap() {        return contextMap;    }    public void setContextMap(Map&lt;String, Context&gt; contextMap) {        this.contextMap = contextMap;    }}</code></pre><p><strong><code>Mapper</code></strong> 其实指的是 Service,<strong>每个 Service 下面有自己的 Host 主机集合</strong></p><pre><code class="language-java">public class Mapper {  // Service 中的 Host    private Map&lt;String, Host&gt; hostMap;    public Mapper(Map&lt;String, Host&gt; hostMap) {        this.hostMap = hostMap;    }    public Map&lt;String, Host&gt; getHostMap() {        return hostMap;    }    public void setHostMap(Map&lt;String, Host&gt; hostMap) {        this.hostMap = hostMap;    }}</code></pre><p><strong><code>Server</code></strong> 指 Minicat 服务实例, 1 个 Minicat 就 1 个 Server,<strong>每个 Server 下面有自己的 Mapper 集合</strong></p><pre><code class="language-java">public class Server {  // Server 里的 Service    private Map&lt;String, Mapper&gt; serviceMap;    public Server() {    }    public Server(Map&lt;String, Mapper&gt; serviceMap) {        this.serviceMap = serviceMap;    }    public Map&lt;String, Mapper&gt; getServiceMap() {        return serviceMap;    }    public void setServiceMap(Map&lt;String, Mapper&gt; serviceMap) {        this.serviceMap = serviceMap;    }}</code></pre><p>POJO 类完成后，就开始改造程序入口 <code>Bootstrap</code> 了，要加载指定目录下的文件，所以我们要改造 <code>loadServlet</code></p><pre><code class="language-java">/** * 加载实际项目里配置的 Servlet * * @throws Exception */@SuppressWarnings({&quot;unchecked&quot;})public Context loadContextServlet(String path) throws Exception {    String webPath = path + &quot;/web.xml&quot;;    if (!(new File(webPath).exists())) {        System.out.println(&quot;not found &quot; + webPath);        return null;    }    InputStream resourceAsStream = new FileInputStream(webPath);    SAXReader saxReader = new SAXReader();    Document document = saxReader.read(resourceAsStream);    Element rootElement = document.getRootElement();    List&lt;Element&gt; list = rootElement.selectNodes(&quot;//servlet&quot;);    Map&lt;String, HttpServlet&gt; servletMap = new HashMap&lt;&gt;(16);    for (Element element : list) {        // &lt;servlet-name&gt;show&lt;/servlet-name&gt;        Element servletnameElement = (Element) element.selectSingleNode(&quot;servlet-name&quot;);        String servletName = servletnameElement.getStringValue();        // &lt;servlet-class&gt;server.ShowServlet&lt;/servlet-class&gt;        Element servletclassElement = (Element) element.selectSingleNode(&quot;servlet-class&quot;);        String servletClass = servletclassElement.getStringValue();        // 根据 servlet-name 的值找到 url-pattern        Element servletMapping = (Element) rootElement.selectSingleNode(&quot;/web-app/servlet-mapping[servlet-name=&#39;&quot; + servletName + &quot;&#39;]&quot;);        // /show        String urlPattern = servletMapping.selectSingleNode(&quot;url-pattern&quot;).getStringValue();        // 自定义类加载器，来加载 webapps 目录下的 class        WebClassLoader webClassLoader = new WebClassLoader();        Class&lt;?&gt; aClass = webClassLoader.findClass(path, servletClass);        servletMap.put(urlPattern, (HttpServlet) aClass.getDeclaredConstructor().newInstance());    }    return new Context(servletMap);}/** * 加载 server.xml，解析并初始化 webapps 下面的各个项目的 servlet */@SuppressWarnings({&quot;unchecked&quot;})public void loadServlet() {    InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(&quot;server.xml&quot;);    SAXReader saxReader = new SAXReader();    try {        Document document = saxReader.read(resourceAsStream);        Element rootElement = document.getRootElement();        // 解析 server 标签        Element serverElement = (Element) rootElement.selectSingleNode(&quot;//Server&quot;);        // 解析 server 下的 Service 标签        List&lt;Element&gt; serviceNodes = serverElement.selectNodes(&quot;//Service&quot;);        // 存储各个 Host        Map&lt;String, Host&gt; hostMap = new HashMap&lt;&gt;(8);        //遍历 service        for (Element service : serviceNodes) {            String serviceName = service.attributeValue(&quot;name&quot;);            Element engineNode = (Element) service.selectSingleNode(&quot;//Engine&quot;);            List&lt;Element&gt; hostNodes = engineNode.selectNodes(&quot;//Host&quot;);            // 存储有多少个项目            Map&lt;String, Context&gt; contextMap = new HashMap&lt;&gt;(8);            for (Element hostNo : hostNodes) {                String hostName = hostNo.attributeValue(&quot;name&quot;);                String appBase = hostNo.attributeValue(&quot;appBase&quot;);                File file = new File(appBase);                if (!file.exists() || file.list() == null) {                    break;                }                String[] list = file.list();                //遍历子文件夹，即：实际的项目列表                for (String path : list) {                    //将项目封装成 context，并保存入map                    contextMap.put(path, loadContextServlet(appBase + &quot;/&quot; + path));                }                // hsot:port                // eg: localhost:8080                hostMap.put(hostName + &quot;:&quot; + port, new Host(contextMap));            }            serviceMap.put(serviceName, new Mapper(hostMap));        }    } catch (Exception e) {        e.printStackTrace();    }}</code></pre><p>代码有点长，稍微说明下理解起来还是比较简单的：</p><p>先解析 <code>server.xml</code> ，然后根据配置的指定文件夹目录去解析，目录下面的每个文件夹就是一个 <code>Context</code>；</p><p>而 Context 里的 servlet 信息，都会配置在 Context 自己的 <code>web.xml</code> 中；</p><p>所以再解析每个 Context 里自己的 web.xml，封装所有的 Servlet</p><p>当然了，<code>Context</code>、<code>Host</code>、<code>Mapper</code>、<code>Server</code> 也都会一并封装</p><p>封装所有的 Servlet 的时候，需要自己去写个类加载器去实例化，所以加了个自定义的类加载器</p><pre><code class="language-java">public class WebClassLoader extends ClassLoader {    @Override    protected Class&lt;?&gt; findClass(String basePath, String className) {        byte[] classBytes = getClassBytes(basePath, className);        return defineClass(className, classBytes, 0, classBytes.length);    }    /**     * 读取类的字节码     *     * @param basePath  根路径     * @param className 类的全限定名     * @return servlet 的字节码信息     * @throws IOException     */    private byte[] getClassBytes(String basePath, String className) {        InputStream in = null;        ByteArrayOutputStream out = null;        String path = basePath + File.separatorChar +                className.replace(&#39;.&#39;, File.separatorChar) + &quot;.class&quot;;        try {            in = new FileInputStream(path);            out = new ByteArrayOutputStream();            byte[] buffer = new byte[2048];            int len = 0;            while ((len = in.read(buffer)) != -1) {                out.write(buffer, 0, len);            }            return out.toByteArray();        } catch (Exception e) {            e.printStackTrace();        } finally {            try {                in.close();                out.close();            } catch (IOException e) {                e.printStackTrace();            }        }        return null;    }}</code></pre><p>基础的工作都做好了，就差解析了，我们要改造 <code>RequestProcessor</code></p><pre><code class="language-java">public class RequestProcessor extends Thread {    private Socket socket;    private Server server;    public RequestProcessor() {    }    public RequestProcessor(Socket socket, Server server) {        this.socket = socket;        this.server = server;    }    @Override    public void run() {        try {            OutputStream outputStream = socket.getOutputStream();            // 分别封装 Request 和 Response            Request request = new Request(socket.getInputStream());            Response response = new Response(outputStream);            HttpServlet httpServlet = findHttpServlet(request);            if (httpServlet == null) {                response.outputHtml(request.getUrl());            } else {                httpServlet.service(request, response);            }            socket.close();        } catch (Exception e) {            e.printStackTrace();        }    }    /**     * 根据请求信息找到对应业务 Servlet     * &lt;pre&gt;     *  GET web-greet/greet HTTP/1.1     *  Host: suremotoo.com     * &lt;/pre&gt;     *     * @param request     * @return 具体要执行的 servlet     */    private HttpServlet findHttpServlet(Request request) {        HttpServlet businessServlet = null;        Map&lt;String, Mapper&gt; serviceMap = server.getServiceMap();        for (String key : serviceMap.keySet()) {            String hostName = request.getHost();            Map&lt;String, Host&gt; hostMap = serviceMap.get(key).getHostMap();            Host host = hostMap.get(hostName);            if (host != null) {                Map&lt;String, Context&gt; contextMap = host.getContextMap();                // 处理 url                // eg: web-greet/greet                String url = request.getUrl();                String[] urlPattern = url.split(&quot;/&quot;);                String contextName = urlPattern[1];                String servletStr = &quot;/&quot;;                if (urlPattern.length &gt; 2) {                    servletStr += urlPattern[2];                }                // 获取上下文                Context context = contextMap.get(contextName);                if (context != null) {                    Map&lt;String, HttpServlet&gt; servletMap = context.getServletMap();                    businessServlet = servletMap.get(servletStr);                }            }        }        return businessServlet;    }}</code></pre><p>核心就是 <code>findHttpServlet</code> 方法，从 <code>Server、Mapper、Host、Context</code> 依次取出，直到最后的 Servlet 配置集合，</p><p>根据 url 找到对应的 Servlet 执行</p><p>至于我们的测试项目，用于部署到 webapps 目录嘛，我这里就简单说一下就行</p><p><strong>先把 Minicat 打个 jar 包，给测试项目用！</strong></p><p>参考 Minicat 的目录，可以建立 <code>maven</code> 项目，编写自定义的业务 Servlet ，然后在 <code>resources</code> 文件夹中，建立 <code>web.xml</code> 文件，用于配置自定义的 Servlet</p><p>📣📣 注意啦：<strong>而自定义的 Servlet 一定要 <code>extends</code> Minicat 中的 <code>HttpServet</code>！！</strong></p><p>至此就 V5.0 就大功告成 🎉🎉🎉🎉🎉</p><h2 id="%E5%90%84%E7%89%88%E6%9C%AC%E5%AE%9E%E7%8E%B0%E5%9B%9E%E9%A1%BE" tabindex="-1">各版本实现回顾</h2><p><img src="https://notes.suremotoo.cc/upload/2020/09/handwriting-minicat-timelines-2d2752b9ef534556b3d7e5ed3cb83f93.png" alt="handwriting-minicat-timelines" /></p><h2 id="%E9%99%84-%E7%B2%BE%E7%BE%8E-pdf-%E7%89%88%E6%9C%AC" tabindex="-1">附-精美 PDF 版本</h2><p><a href="https://notes.suremotoo.cc/upload/2020/09/%E6%89%8B%E5%86%99%E8%BF%B7%E4%BD%A0%E7%89%88-Tomcat-Minicat-57c69b34b2b94b668f62a4fee139c1ae.pdf" target="_blank">😘😘 精美 PDF 版本 👈</a></p>]]>
                    </description>
                    <pubDate>Tue, 08 Sep 2020 15:55:15 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Spring IoC 容器源码分析]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/spring-ioc</link>
                    <description>
                            <![CDATA[<p>Spring 最重要的概念是 IoC 和 AOP，本篇文章其实就是要带领大家来分析下 Spring 的 IoC 容器。既然大家平时都要用到 Spring，怎么可以不好好了解 Spring 呢？阅读本文并不能让你成为 Spring 专家，不过一定有助于大家理解 Spring 的很多概念，帮助大家排查应用中和 Spring 相关的一些问题。</p><p>本文采用的源码版本是 4.3.11.RELEASE，算是 5.0.x 前比较新的版本了。为了降低难度，本文所说的所有的内容都是基于 xml 的配置的方式，实际使用已经很少人这么做了，至少不是纯 xml 配置，不过从理解源码的角度来看用这种方式来说无疑是最合适的。</p><p>阅读建议：读者至少需要知道怎么配置 Spring，了解 Spring 中的各种概念，少部分内容我还假设读者使用过 SpringMVC。本文要说的 IoC 总体来说有两处地方最重要，一个是创建 Bean 容器，一个是初始化 Bean，如果读者觉得一次性看完本文压力有点大，那么可以按这个思路分两次消化。读者不一定对 Spring 容器的源码感兴趣，也许附录部分介绍的知识对读者有些许作用。</p><p>希望通过本文可以让读者不惧怕阅读 Spring 源码，也希望大家能反馈表述错误或不合理的地方。</p><h2 id="引言">引言</h2><p>先看下最基本的启动 Spring 容器的例子：</p><pre><code class="language-java">public static void main(String[] args) {    ApplicationContext context = new ClassPathXmlApplicationContext(&quot;classpath:applicationfile.xml&quot;);}</code></pre><p>以上代码就可以利用配置文件来启动一个 Spring 容器了，请使用 maven 的小伙伴直接在 dependencies 中加上以下依赖即可，个人比较反对那些不知道要添加什么依赖，然后把 Spring 的所有相关的东西都加进来的方式。</p><pre><code class="language-java">&lt;dependency&gt;  &lt;groupId&gt;org.springframework&lt;/groupId&gt;  &lt;artifactId&gt;spring-context&lt;/artifactId&gt;  &lt;version&gt;4.3.11.RELEASE&lt;/version&gt;&lt;/dependency&gt;</code></pre><blockquote><p>spring-context 会自动将 spring-core、spring-beans、spring-aop、spring-expression 这几个基础 jar 包带进来。</p></blockquote><p>多说一句，很多开发者入门就直接接触的 SpringMVC，对 Spring 其实不是很了解，Spring 是渐进式的工具，并不具有很强的侵入性，它的模块也划分得很合理，即使你的应用不是 web 应用，或者之前完全没有使用到 Spring，而你就想用 Spring 的依赖注入这个功能，其实完全是可以的，它的引入不会对其他的组件产生冲突。</p><p>废话说完，我们继续。<code>ApplicationContext context = new ClassPathXmlApplicationContext(...)</code> 其实很好理解，从名字上就可以猜出一二，就是在 ClassPath 中寻找 xml 配置文件，根据 xml 文件内容来构建 ApplicationContext。当然，除了 ClassPathXmlApplicationContext 以外，我们也还有其他构建 ApplicationContext 的方案可供选择，我们先来看看大体的继承结构是怎么样的：</p><p><img src="https://www.javadoop.com/blogimages/spring-context/1.png" alt="1" /></p><blockquote><p>读者可以大致看一下类名，源码分析的时候不至于找不着看哪个类，因为 Spring 为了适应各种使用场景，提供的各个接口都可能有很多的实现类。对于我们来说，就是揪着一个完整的分支看完。</p><p>当然，读本文的时候读者也不必太担心，每个代码块分析的时候，我都会告诉读者我们在说哪个类第几行。</p></blockquote><p>我们可以看到，ClassPathXmlApplicationContext 兜兜转转了好久才到 ApplicationContext 接口，同样的，我们也可以使用绿颜色的 <strong>FileSystemXmlApplicationContext</strong> 和 <strong>AnnotationConfigApplicationContext</strong> 这两个类。</p><p><strong>1、FileSystemXmlApplicationContext</strong> 的构造函数需要一个 xml 配置文件在系统中的路径，其他和 ClassPathXmlApplicationContext 基本上一样。</p><p><strong>2、AnnotationConfigApplicationContext</strong> 是基于注解来使用的，它不需要配置文件，采用 java 配置类和各种注解来配置，是比较简单的方式，也是大势所趋吧。</p><p>不过本文旨在帮助大家理解整个构建流程，所以决定使用 ClassPathXmlApplicationContext 进行分析。</p><p>我们先来一个简单的例子来看看怎么实例化 ApplicationContext。</p><p>首先，定义一个接口：</p><pre><code class="language-java">public interface MessageService {    String getMessage();}</code></pre><p>定义接口实现类：</p><pre><code class="language-java">public class MessageServiceImpl implements MessageService {    public String getMessage() {        return &quot;hello world&quot;;    }}</code></pre><p>接下来，我们在 <strong>resources</strong> 目录新建一个配置文件，文件名随意，通常叫 application.xml 或 application-xxx.xml 就可以了：</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;beans xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd&quot; default-autowire=&quot;byName&quot;&gt;    &lt;bean id=&quot;messageService&quot; class=&quot;com.javadoop.example.MessageServiceImpl&quot;/&gt;&lt;/beans&gt;</code></pre><p>这样，我们就可以跑起来了：</p><pre><code class="language-java">public class App {    public static void main(String[] args) {        // 用我们的配置文件来启动一个 ApplicationContext        ApplicationContext context = new ClassPathXmlApplicationContext(&quot;classpath:application.xml&quot;);              System.out.println(&quot;context 启动成功&quot;);              // 从 context 中取出我们的 Bean，而不是用 new MessageServiceImpl() 这种方式        MessageService messageService = context.getBean(MessageService.class);        // 这句将输出: hello world        System.out.println(messageService.getMessage());    }}</code></pre><p>以上例子很简单，不过也够引出本文的主题了，就是怎么样通过配置文件来启动 Spring 的 ApplicationContext ？也就是我们今天要分析的 IOC 的核心了。ApplicationContext 启动过程中，会负责创建实例 Bean，往各个 Bean 中注入依赖等。</p><h2 id="beanfactory-简介">BeanFactory 简介</h2><p>BeanFactory，从名字上也很好理解，生产 bean 的工厂，它负责生产和管理各个 bean 实例。</p><p>初学者可别以为我之前说那么多和 BeanFactory 无关，前面说的 ApplicationContext 其实就是一个 BeanFactory。我们来看下和 BeanFactory 接口相关的主要的继承结构：</p><p><img src="https://www.javadoop.com/blogimages/spring-context/2.png" alt="2" /></p><p>我想，大家看完这个图以后，可能就不是很开心了。ApplicationContext 往下的继承结构前面一张图说过了，这里就不重复了。这张图呢，背下来肯定是不需要的，有几个重点和大家说明下就好。</p><ol><li>ApplicationContext 继承了 ListableBeanFactory，这个 Listable 的意思就是，通过这个接口，我们可以获取多个 Bean，大家看源码会发现，最顶层 BeanFactory 接口的方法都是获取单个 Bean 的。</li><li>ApplicationContext 继承了 HierarchicalBeanFactory，Hierarchical 单词本身已经能说明问题了，也就是说我们可以在应用中起多个 BeanFactory，然后可以将各个 BeanFactory 设置为父子关系。</li><li>AutowireCapableBeanFactory 这个名字中的 Autowire 大家都非常熟悉，它就是用来自动装配 Bean 用的，但是仔细看上图，ApplicationContext 并没有继承它，不过不用担心，不使用继承，不代表不可以使用组合，如果你看到 ApplicationContext 接口定义中的最后一个方法 getAutowireCapableBeanFactory() 就知道了。</li><li>ConfigurableListableBeanFactory 也是一个特殊的接口，看图，特殊之处在于它继承了第二层所有的三个接口，而 ApplicationContext 没有。这点之后会用到。</li><li>请先不用花时间在其他的接口和类上，先理解我说的这几点就可以了。</li></ol><p>然后，请读者打开编辑器，翻一下 BeanFactory、ListableBeanFactory、HierarchicalBeanFactory、AutowireCapableBeanFactory、ApplicationContext 这几个接口的代码，大概看一下各个接口中的方法，大家心里要有底，限于篇幅，我就不贴代码介绍了。</p><h2 id="启动过程分析">启动过程分析</h2><p>下面将会是冗长的代码分析，记住，一定要自己打开源码来看，不然纯看是很累的。</p><p>第一步，我们肯定要从 ClassPathXmlApplicationContext 的构造方法说起。</p><pre><code class="language-java">public class ClassPathXmlApplicationContext extends AbstractXmlApplicationContext {  private Resource[] configResources;    // 如果已经有 ApplicationContext 并需要配置成父子关系，那么调用这个构造方法  public ClassPathXmlApplicationContext(ApplicationContext parent) {    super(parent);  }  ...  public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)      throws BeansException {    super(parent);    // 根据提供的路径，处理成配置文件数组(以分号、逗号、空格、tab、换行符分割)    setConfigLocations(configLocations);    if (refresh) {      refresh(); // 核心方法    }  }    ...}</code></pre><p>接下来，就是 <code>refresh()</code>，这里简单说下为什么是 refresh()，而不是 init() 这种名字的方法。因为 ApplicationContext 建立起来以后，其实我们是可以通过调用 refresh() 这个方法重建的，refresh() 会将原来的 ApplicationContext 销毁，然后再重新执行一次初始化操作。</p><p>往下看，refresh() 方法里面调用了那么多方法，就知道肯定不简单了，请读者先看个大概，细节之后会详细说。</p><pre><code class="language-java">@Overridepublic void refresh() throws BeansException, IllegalStateException {   // 来个锁，不然 refresh() 还没结束，你又来个启动或销毁容器的操作，那不就乱套了嘛   synchronized (this.startupShutdownMonitor) {      // 准备工作，记录下容器的启动时间、标记“已启动”状态、处理配置文件中的占位符      prepareRefresh();           // 这步比较关键，这步完成后，配置文件就会解析成一个个 Bean 定义，注册到 BeanFactory 中，      // 当然，这里说的 Bean 还没有初始化，只是配置信息都提取出来了，      // 注册也只是将这些信息都保存到了注册中心(说到底核心是一个 beanName-&gt; beanDefinition 的 map)      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();      // 设置 BeanFactory 的类加载器，添加几个 BeanPostProcessor，手动注册几个特殊的 bean      // 这块待会会展开说      prepareBeanFactory(beanFactory);      try {         // 【这里需要知道 BeanFactoryPostProcessor 这个知识点，Bean 如果实现了此接口，         // 那么在容器初始化以后，Spring 会负责调用里面的 postProcessBeanFactory 方法。】                 // 这里是提供给子类的扩展点，到这里的时候，所有的 Bean 都加载、注册完成了，但是都还没有初始化         // 具体的子类可以在这步的时候添加一些特殊的 BeanFactoryPostProcessor 的实现类或做点什么事         postProcessBeanFactory(beanFactory);         // 调用 BeanFactoryPostProcessor 各个实现类的 postProcessBeanFactory(factory) 方法         invokeBeanFactoryPostProcessors(beanFactory);         // 注册 BeanPostProcessor 的实现类，注意看和 BeanFactoryPostProcessor 的区别         // 此接口两个方法: postProcessBeforeInitialization 和 postProcessAfterInitialization         // 两个方法分别在 Bean 初始化之前和初始化之后得到执行。注意，到这里 Bean 还没初始化         registerBeanPostProcessors(beanFactory);         // 初始化当前 ApplicationContext 的 MessageSource，国际化这里就不展开说了，不然没完没了了         initMessageSource();         // 初始化当前 ApplicationContext 的事件广播器，这里也不展开了         initApplicationEventMulticaster();         // 从方法名就可以知道，典型的模板方法(钩子方法)，         // 具体的子类可以在这里初始化一些特殊的 Bean（在初始化 singleton beans 之前）         onRefresh();         // 注册事件监听器，监听器需要实现 ApplicationListener 接口。这也不是我们的重点，过         registerListeners();         // 重点，重点，重点         // 初始化所有的 singleton beans         //（lazy-init 的除外）         finishBeanFactoryInitialization(beanFactory);         // 最后，广播事件，ApplicationContext 初始化完成         finishRefresh();      }      catch (BeansException ex) {         if (logger.isWarnEnabled()) {            logger.warn(&quot;Exception encountered during context initialization - &quot; +                  &quot;cancelling refresh attempt: &quot; + ex);         }         // Destroy already created singletons to avoid dangling resources.         // 销毁已经初始化的 singleton 的 Beans，以免有些 bean 会一直占用资源         destroyBeans();         // Reset 'active' flag.         cancelRefresh(ex);         // 把异常往外抛         throw ex;      }      finally {         // Reset common introspection caches in Spring's core, since we         // might not ever need metadata for singleton beans anymore...         resetCommonCaches();      }   }}</code></pre><p>下面，我们开始一步步来肢解这个 refresh() 方法。</p><h3 id="创建-bean-容器前的准备工作">创建 Bean 容器前的准备工作</h3><p>这个比较简单，直接看代码中的几个注释即可。</p><pre><code class="language-java">protected void prepareRefresh() {   // 记录启动时间，   // 将 active 属性设置为 true，closed 属性设置为 false，它们都是 AtomicBoolean 类型   this.startupDate = System.currentTimeMillis();   this.closed.set(false);   this.active.set(true);   if (logger.isInfoEnabled()) {      logger.info(&quot;Refreshing &quot; + this);   }   // Initialize any placeholder property sources in the context environment   initPropertySources();   // 校验 xml 配置文件   getEnvironment().validateRequiredProperties();   this.earlyApplicationEvents = new LinkedHashSet&lt;ApplicationEvent&gt;();}</code></pre><h3 id="创建-bean-容器加载并注册-bean">创建 Bean 容器，加载并注册 Bean</h3><p>我们回到 refresh() 方法中的下一行 obtainFreshBeanFactory()。</p><p>注意，<strong>这个方法是全文最重要的部分之一</strong>，这里将会初始化 BeanFactory、加载 Bean、注册 Bean 等等。</p><p>当然，这步结束后，Bean 并没有完成初始化。这里指的是 Bean 实例并未在这一步生成。</p><p>// AbstractApplicationContext.java</p><pre><code class="language-java">protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {   // 关闭旧的 BeanFactory (如果有)，创建新的 BeanFactory，加载 Bean 定义、注册 Bean 等等   refreshBeanFactory();     // 返回刚刚创建的 BeanFactory   ConfigurableListableBeanFactory beanFactory = getBeanFactory();   if (logger.isDebugEnabled()) {      logger.debug(&quot;Bean factory for &quot; + getDisplayName() + &quot;: &quot; + beanFactory);   }   return beanFactory;}</code></pre><p>// AbstractRefreshableApplicationContext.java 120</p><pre><code class="language-java">@Overrideprotected final void refreshBeanFactory() throws BeansException {   // 如果 ApplicationContext 中已经加载过 BeanFactory 了，销毁所有 Bean，关闭 BeanFactory   // 注意，应用中 BeanFactory 本来就是可以多个的，这里可不是说应用全局是否有 BeanFactory，而是当前   // ApplicationContext 是否有 BeanFactory   if (hasBeanFactory()) {      destroyBeans();      closeBeanFactory();   }   try {      // 初始化一个 DefaultListableBeanFactory，为什么用这个，我们马上说。      DefaultListableBeanFactory beanFactory = createBeanFactory();      // 用于 BeanFactory 的序列化，我想不部分人应该都用不到      beanFactory.setSerializationId(getId());           // 下面这两个方法很重要，别跟丢了，具体细节之后说      // 设置 BeanFactory 的两个配置属性：是否允许 Bean 覆盖、是否允许循环引用      customizeBeanFactory(beanFactory);           // 加载 Bean 到 BeanFactory 中      loadBeanDefinitions(beanFactory);      synchronized (this.beanFactoryMonitor) {         this.beanFactory = beanFactory;      }   }   catch (IOException ex) {      throw new ApplicationContextException(&quot;I/O error parsing bean definition source for &quot; + getDisplayName(), ex);   }}</code></pre><blockquote><p>看到这里的时候，我觉得读者就应该站在高处看 ApplicationContext 了，ApplicationContext 继承自 BeanFactory，但是它不应该被理解为 BeanFactory 的实现类，而是说其内部持有一个实例化的 BeanFactory（DefaultListableBeanFactory）。以后所有的 BeanFactory 相关的操作其实是委托给这个实例来处理的。</p></blockquote><p>我们说说为什么选择实例化 <strong>DefaultListableBeanFactory</strong> ？前面我们说了有个很重要的接口 ConfigurableListableBeanFactory，它实现了 BeanFactory 下面一层的所有三个接口，我把之前的继承图再拿过来大家再仔细看一下：</p><p><img src="https://www.javadoop.com/blogimages/spring-context/3.png" alt="3" /></p><p>我们可以看到 ConfigurableListableBeanFactory 只有一个实现类 DefaultListableBeanFactory，而且实现类 DefaultListableBeanFactory 还通过实现右边的 AbstractAutowireCapableBeanFactory 通吃了右路。所以结论就是，最底下这个家伙 DefaultListableBeanFactory 基本上是最牛的 BeanFactory 了，这也是为什么这边会使用这个类来实例化的原因。</p><blockquote><p>如果你想要在程序运行的时候动态往 Spring IOC 容器注册新的 bean，就会使用到这个类。那我们怎么在运行时获得这个实例呢？</p><p>之前我们说过 ApplicationContext 接口能获取到 AutowireCapableBeanFactory，就是最右上角那个，然后它向下转型就能得到 DefaultListableBeanFactory 了。</p><p>那怎么拿到 ApplicationContext 实例呢？如果你不会，说明你没用过 Spring。</p></blockquote><p>在继续往下之前，我们需要先了解 BeanDefinition。<strong>我们说 BeanFactory 是 Bean 容器，那么 Bean 又是什么呢？</strong></p><p>这里的 BeanDefinition 就是我们所说的 Spring 的 Bean，我们自己定义的各个 Bean 其实会转换成一个个 BeanDefinition 存在于 Spring 的 BeanFactory 中。</p><p>所以，如果有人问你 Bean 是什么的时候，你要知道 Bean 在代码层面上可以简单认为是 BeanDefinition 的实例。</p><blockquote><p>BeanDefinition 中保存了我们的 Bean 信息，比如这个 Bean 指向的是哪个类、是否是单例的、是否懒加载、这个 Bean 依赖了哪些 Bean 等等。</p></blockquote><h4 id="beandefinition-接口定义">BeanDefinition 接口定义</h4><p>我们来看下 BeanDefinition 的接口定义：</p><pre><code class="language-java">public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {   // 我们可以看到，默认只提供 sington 和 prototype 两种，   // 很多读者可能知道还有 request, session, globalSession, application, websocket 这几种，   // 不过，它们属于基于 web 的扩展。   String SCOPE_SINGLETON = ConfigurableBeanFactory.SCOPE_SINGLETON;   String SCOPE_PROTOTYPE = ConfigurableBeanFactory.SCOPE_PROTOTYPE;   // 比较不重要，直接跳过吧   int ROLE_APPLICATION = 0;   int ROLE_SUPPORT = 1;   int ROLE_INFRASTRUCTURE = 2;   // 设置父 Bean，这里涉及到 bean 继承，不是 java 继承。请参见附录的详细介绍   // 一句话就是：继承父 Bean 的配置信息而已   void setParentName(String parentName);     // 获取父 Bean   String getParentName();     // 设置 Bean 的类名称，将来是要通过反射来生成实例的   void setBeanClassName(String beanClassName);      // 获取 Bean 的类名称   String getBeanClassName();    // 设置 bean 的 scope   void setScope(String scope);   String getScope();   // 设置是否懒加载   void setLazyInit(boolean lazyInit);      boolean isLazyInit();   // 设置该 Bean 依赖的所有的 Bean，注意，这里的依赖不是指属性依赖(如 @Autowire 标记的)，   // 是 depends-on=&quot;&quot; 属性设置的值。   void setDependsOn(String... dependsOn);   // 返回该 Bean 的所有依赖   String[] getDependsOn();   // 设置该 Bean 是否可以注入到其他 Bean 中，只对根据类型注入有效，   // 如果根据名称注入，即使这边设置了 false，也是可以的   void setAutowireCandidate(boolean autowireCandidate);   // 该 Bean 是否可以注入到其他 Bean 中   boolean isAutowireCandidate();   // 主要的。同一接口的多个实现，如果不指定名字的话，Spring 会优先选择设置 primary 为 true 的 bean   void setPrimary(boolean primary);   // 是否是 primary 的   boolean isPrimary();   // 如果该 Bean 采用工厂方法生成，指定工厂名称。对工厂不熟悉的读者，请参加附录   // 一句话就是：有些实例不是用反射生成的，而是用工厂模式生成的   void setFactoryBeanName(String factoryBeanName);   // 获取工厂名称   String getFactoryBeanName();   // 指定工厂类中的 工厂方法名称   void setFactoryMethodName(String factoryMethodName);   // 获取工厂类中的 工厂方法名称   String getFactoryMethodName();   // 构造器参数   ConstructorArgumentValues getConstructorArgumentValues();   // Bean 中的属性值，后面给 bean 注入属性值的时候会说到   MutablePropertyValues getPropertyValues();   // 是否 singleton   boolean isSingleton();   // 是否 prototype   boolean isPrototype();   // 如果这个 Bean 是被设置为 abstract，那么不能实例化，   // 常用于作为 父bean 用于继承，其实也很少用......   boolean isAbstract();   int getRole();   String getDescription();   String getResourceDescription();   BeanDefinition getOriginatingBeanDefinition();}</code></pre><blockquote><p>这个 BeanDefinition 其实已经包含很多的信息了，暂时不清楚所有的方法对应什么东西没关系，希望看完本文后读者可以彻底搞清楚里面的所有东西。</p><p>这里接口虽然那么多，但是没有类似 getInstance() 这种方法来获取我们定义的类的实例，真正的我们定义的类生成的实例到哪里去了呢？别着急，这个要很后面才能讲到。</p></blockquote><p>有了 BeanDefinition 的概念以后，我们再往下看 refreshBeanFactory() 方法中的剩余部分：</p><pre><code class="language-java">customizeBeanFactory(beanFactory);loadBeanDefinitions(beanFactory);</code></pre><p>虽然只有两个方法，但路还很长啊。。。</p><h4 id="customizebeanfactory">customizeBeanFactory</h4><p>customizeBeanFactory(beanFactory) 比较简单，就是配置是否允许 BeanDefinition 覆盖、是否允许循环引用。</p><pre><code class="language-java">protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {   if (this.allowBeanDefinitionOverriding != null) {      // 是否允许 Bean 定义覆盖      beanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);   }   if (this.allowCircularReferences != null) {      // 是否允许 Bean 间的循环依赖      beanFactory.setAllowCircularReferences(this.allowCircularReferences);   }}</code></pre><p>BeanDefinition 的覆盖问题可能会有开发者碰到这个坑，就是在配置文件中定义 bean 时使用了相同的 id 或 name，默认情况下，allowBeanDefinitionOverriding 属性为 null，如果在同一配置文件中重复了，会抛错，但是如果不是同一配置文件中，会发生覆盖。</p><p>循环引用也很好理解：A 依赖 B，而 B 依赖 A。或 A 依赖 B，B 依赖 C，而 C 依赖 A。</p><p>默认情况下，Spring 允许循环依赖，当然如果你在 A 的构造方法中依赖 B，在 B 的构造方法中依赖 A 是不行的。</p><p>至于这两个属性怎么配置？我在附录中进行了介绍，尤其对于覆盖问题，很多人都希望禁止出现 Bean 覆盖，可是 Spring 默认是不同文件的时候可以覆盖的。</p><p>之后的源码中还会出现这两个属性，读者有个印象就可以了，它们不是非常重要。</p><h4 id="加载-bean-loadbeandefinitions">加载 Bean: loadBeanDefinitions</h4><p>接下来是最重要的 loadBeanDefinitions(beanFactory) 方法了，这个方法将根据配置，加载各个 Bean，然后放到 BeanFactory 中。</p><p>读取配置的操作在 XmlBeanDefinitionReader 中，其负责加载配置、解析。</p><p>// AbstractXmlApplicationContext.java 80</p><pre><code class="language-java">/** 我们可以看到，此方法将通过一个 XmlBeanDefinitionReader 实例来加载各个 Bean。*/@Overrideprotected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {   // 给这个 BeanFactory 实例化一个 XmlBeanDefinitionReader   XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);   // Configure the bean definition reader with this context's   // resource loading environment.   beanDefinitionReader.setEnvironment(this.getEnvironment());   beanDefinitionReader.setResourceLoader(this);   beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));   // 初始化 BeanDefinitionReader，其实这个是提供给子类覆写的，   // 我看了一下，没有类覆写这个方法，我们姑且当做不重要吧   initBeanDefinitionReader(beanDefinitionReader);   // 重点来了，继续往下   loadBeanDefinitions(beanDefinitionReader);}</code></pre><p>现在还在这个类中，接下来用刚刚初始化的 Reader 开始来加载 xml 配置，这块代码读者可以选择性跳过，不是很重要。也就是说，下面这个代码块，读者可以很轻松地略过。</p><p>// AbstractXmlApplicationContext.java 120</p><pre><code class="language-java">protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {   Resource[] configResources = getConfigResources();   if (configResources != null) {      // 往下看      reader.loadBeanDefinitions(configResources);   }   String[] configLocations = getConfigLocations();   if (configLocations != null) {      // 2      reader.loadBeanDefinitions(configLocations);   }}// 上面虽然有两个分支，不过第二个分支很快通过解析路径转换为 Resource 以后也会进到这里@Overridepublic int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException {   Assert.notNull(resources, &quot;Resource array must not be null&quot;);   int counter = 0;   // 注意这里是个 for 循环，也就是每个文件是一个 resource   for (Resource resource : resources) {      // 继续往下看      counter += loadBeanDefinitions(resource);   }   // 最后返回 counter，表示总共加载了多少的 BeanDefinition   return counter;}// XmlBeanDefinitionReader 303@Overridepublic int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {   return loadBeanDefinitions(new EncodedResource(resource));}// XmlBeanDefinitionReader 314public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {   Assert.notNull(encodedResource, &quot;EncodedResource must not be null&quot;);   if (logger.isInfoEnabled()) {      logger.info(&quot;Loading XML bean definitions from &quot; + encodedResource.getResource());   }   // 用一个 ThreadLocal 来存放配置文件资源   Set&lt;EncodedResource&gt; currentResources = this.resourcesCurrentlyBeingLoaded.get();   if (currentResources == null) {      currentResources = new HashSet&lt;EncodedResource&gt;(4);      this.resourcesCurrentlyBeingLoaded.set(currentResources);   }   if (!currentResources.add(encodedResource)) {      throw new BeanDefinitionStoreException(            &quot;Detected cyclic loading of &quot; + encodedResource + &quot; - check your import definitions!&quot;);   }   try {      InputStream inputStream = encodedResource.getResource().getInputStream();      try {         InputSource inputSource = new InputSource(inputStream);         if (encodedResource.getEncoding() != null) {            inputSource.setEncoding(encodedResource.getEncoding());         }         // 核心部分是这里，往下面看         return doLoadBeanDefinitions(inputSource, encodedResource.getResource());      }      finally {         inputStream.close();      }   }   catch (IOException ex) {      throw new BeanDefinitionStoreException(            &quot;IOException parsing XML document from &quot; + encodedResource.getResource(), ex);   }   finally {      currentResources.remove(encodedResource);      if (currentResources.isEmpty()) {         this.resourcesCurrentlyBeingLoaded.remove();      }   }}// 还在这个文件中，第 388 行protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)      throws BeanDefinitionStoreException {   try {      // 这里就不看了，将 xml 文件转换为 Document 对象      Document doc = doLoadDocument(inputSource, resource);      // 继续      return registerBeanDefinitions(doc, resource);   }   catch (...}// 还在这个文件中，第 505 行// 返回值：返回从当前配置文件加载了多少数量的 Beanpublic int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {   BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();   int countBefore = getRegistry().getBeanDefinitionCount();   // 这里   documentReader.registerBeanDefinitions(doc, createReaderContext(resource));   return getRegistry().getBeanDefinitionCount() - countBefore;}// DefaultBeanDefinitionDocumentReader 90@Overridepublic void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {   this.readerContext = readerContext;   logger.debug(&quot;Loading bean definitions&quot;);   Element root = doc.getDocumentElement();   // 从 xml 根节点开始解析文件   doRegisterBeanDefinitions(root);}         </code></pre><p>经过漫长的链路，一个配置文件终于转换为一颗 DOM 树了，注意，这里指的是其中一个配置文件，不是所有的，读者可以看到上面有个 for 循环的。下面开始从根节点开始解析：</p><h5 id="doregisterbeandefinitions">doRegisterBeanDefinitions：</h5><pre><code class="language-java">// DefaultBeanDefinitionDocumentReader 116protected void doRegisterBeanDefinitions(Element root) {   // 我们看名字就知道，BeanDefinitionParserDelegate 必定是一个重要的类，它负责解析 Bean 定义，   // 这里为什么要定义一个 parent? 看到后面就知道了，是递归问题，   // 因为 &lt;beans /&gt; 内部是可以定义 &lt;beans /&gt; 的，所以这个方法的 root 其实不一定就是 xml 的根节点，也可以是嵌套在里面的 &lt;beans /&gt; 节点，从源码分析的角度，我们当做根节点就好了   BeanDefinitionParserDelegate parent = this.delegate;   this.delegate = createDelegate(getReaderContext(), root, parent);   if (this.delegate.isDefaultNamespace(root)) {      // 这块说的是根节点 &lt;beans ... profile=&quot;dev&quot; /&gt; 中的 profile 是否是当前环境需要的，      // 如果当前环境配置的 profile 不包含此 profile，那就直接 return 了，不对此 &lt;beans /&gt; 解析      // 不熟悉 profile 为何物，不熟悉怎么配置 profile 读者的请移步附录区      String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);      if (StringUtils.hasText(profileSpec)) {         String[] specifiedProfiles = StringUtils.tokenizeToStringArray(               profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);         if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {            if (logger.isInfoEnabled()) {               logger.info(&quot;Skipped XML bean definition file due to specified profiles [&quot; + profileSpec +                     &quot;] not matching: &quot; + getReaderContext().getResource());            }            return;         }      }   }   preProcessXml(root); // 钩子   // 往下看   parseBeanDefinitions(root, this.delegate);   postProcessXml(root); // 钩子   this.delegate = parent;}</code></pre><p>preProcessXml(root) 和 postProcessXml(root) 是给子类用的钩子方法，鉴于没有被使用到，也不是我们的重点，我们直接跳过。</p><p>这里涉及到了 profile 的问题，对于不了解的读者，我在附录中对 profile 做了简单的解释，读者可以参考一下。</p><p>接下来，看核心解析方法 parseBeanDefinitions(root, this.delegate) :</p><pre><code class="language-java">// default namespace 涉及到的就四个标签 &lt;import /&gt;、&lt;alias /&gt;、&lt;bean /&gt; 和 &lt;beans /&gt;，// 其他的属于 custom 的protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {   if (delegate.isDefaultNamespace(root)) {      NodeList nl = root.getChildNodes();      for (int i = 0; i &lt; nl.getLength(); i++) {         Node node = nl.item(i);         if (node instanceof Element) {            Element ele = (Element) node;            if (delegate.isDefaultNamespace(ele)) {               // 解析 default namespace 下面的几个元素               parseDefaultElement(ele, delegate);            }            else {               // 解析其他 namespace 的元素               delegate.parseCustomElement(ele);            }         }      }   }   else {      delegate.parseCustomElement(root);   }}</code></pre><p>从上面的代码，我们可以看到，对于每个配置来说，分别进入到 parseDefaultElement(ele, delegate); 和 delegate.parseCustomElement(ele); 这两个分支了。</p><p>parseDefaultElement(ele, delegate) 代表解析的节点是 <code>&lt;import /&gt;</code>、<code>&lt;alias /&gt;</code>、<code>&lt;bean /&gt;</code>、<code>&lt;beans /&gt;</code> 这几个。</p><blockquote><p>这里的四个标签之所以是 <strong>default</strong> 的，是因为它们是处于这个 namespace 下定义的：</p><pre><code>http://www.springframework.org/schema/beans</code></pre><p>又到初学者科普时间，不熟悉 namespace 的读者请看下面贴出来的 xml，这里的第二行 <strong>xmlns</strong> 就是咯。</p><pre><code class="language-xml">&lt;beans xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xsi:schemaLocation=&quot;            http://www.springframework.org/schema/beans          http://www.springframework.org/schema/beans/spring-beans.xsd&quot;       default-autowire=&quot;byName&quot;&gt;</code></pre><p>而对于其他的标签，将进入到 delegate.parseCustomElement(element) 这个分支。如我们经常会使用到的 <code>&lt;mvc /&gt;</code>、<code>&lt;task /&gt;</code>、<code>&lt;context /&gt;</code>、<code>&lt;aop /&gt;</code>等。</p><p>这些属于扩展，如果需要使用上面这些 ”非 default“ 标签，那么上面的 xml 头部的地方也要引入相应的 namespace 和 .xsd 文件的路径，如下所示。同时代码中需要提供相应的 parser 来解析，如 MvcNamespaceHandler、TaskNamespaceHandler、ContextNamespaceHandler、AopNamespaceHandler 等。</p><p>假如读者想分析 <code>&lt;context:property-placeholder location=&quot;classpath:xx.properties&quot; /&gt;</code> 的实现原理，就应该到 ContextNamespaceHandler 中找答案。</p><pre><code class="language-xml">&lt;beans xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;      xmlns=&quot;http://www.springframework.org/schema/beans&quot;      xmlns:context=&quot;http://www.springframework.org/schema/context&quot;      xmlns:mvc=&quot;http://www.springframework.org/schema/mvc&quot;      xsi:schemaLocation=&quot;           http://www.springframework.org/schema/beans            http://www.springframework.org/schema/beans/spring-beans.xsd           http://www.springframework.org/schema/context           http://www.springframework.org/schema/context/spring-context.xsd           http://www.springframework.org/schema/mvc              http://www.springframework.org/schema/mvc/spring-mvc.xsd         &quot;      default-autowire=&quot;byName&quot;&gt;</code></pre><p>同理，以后你要是碰到 <code>&lt;dubbo /&gt;</code> 这种标签，那么就应该搜一搜是不是有 DubboNamespaceHandler 这个处理类。</p></blockquote><p>回过神来，看看处理 default 标签的方法：</p><pre><code class="language-java">private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {   if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {      // 处理 &lt;import /&gt; 标签      importBeanDefinitionResource(ele);   }   else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {      // 处理 &lt;alias /&gt; 标签定义      // &lt;alias name=&quot;fromName&quot; alias=&quot;toName&quot;/&gt;      processAliasRegistration(ele);   }   else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {      // 处理 &lt;bean /&gt; 标签定义，这也算是我们的重点吧      processBeanDefinition(ele, delegate);   }   else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {      // 如果碰到的是嵌套的 &lt;beans /&gt; 标签，需要递归      doRegisterBeanDefinitions(ele);   }}</code></pre><p>如果每个标签都说，那我不吐血，你们都要吐血了。我们挑我们的重点 <code>&lt;bean /&gt;</code> 标签出来说。</p><h5 id="processbeandefinition-解析-bean-标签">processBeanDefinition 解析 bean 标签</h5><p>下面是 processBeanDefinition 解析 <code>&lt;bean /&gt;</code> 标签：</p><p>// DefaultBeanDefinitionDocumentReader 298</p><pre><code class="language-java">protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {   // 将 &lt;bean /&gt; 节点中的信息提取出来，然后封装到一个 BeanDefinitionHolder 中，细节往下看   BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);     // 下面的几行先不要看，跳过先，跳过先，跳过先，后面会继续说的     if (bdHolder != null) {      bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);      try {         // Register the final decorated instance.         BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());      }      catch (BeanDefinitionStoreException ex) {         getReaderContext().error(&quot;Failed to register bean definition with name '&quot; +               bdHolder.getBeanName() + &quot;'&quot;, ele, ex);      }      // Send registration event.      getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));   }}</code></pre><p>继续往下看怎么解析之前，我们先看下 <strong><code>&lt;bean /&gt;</code></strong> 标签中可以定义哪些属性：</p><table><thead><tr><th>Property</th><th> </th></tr></thead><tbody><tr><td>class</td><td>类的全限定名</td></tr><tr><td>name</td><td>可指定 id、name(用逗号、分号、空格分隔)</td></tr><tr><td>scope</td><td>作用域</td></tr><tr><td>constructor arguments</td><td>指定构造参数</td></tr><tr><td>properties</td><td>设置属性的值</td></tr><tr><td>autowiring mode</td><td>no(默认值)、byName、byType、 constructor</td></tr><tr><td>lazy-initialization mode</td><td>是否懒加载(如果被非懒加载的bean依赖了那么其实也就不能懒加载了)</td></tr><tr><td>initialization method</td><td>bean 属性设置完成后，会调用这个方法</td></tr><tr><td>destruction method</td><td>bean 销毁后的回调方法</td></tr></tbody></table><p>上面表格中的内容我想大家都非常熟悉吧，如果不熟悉，那就是你不够了解 Spring 的配置了。</p><p>简单地说就是像下面这样子：</p><pre><code class="language-xml">&lt;bean id=&quot;exampleBean&quot; name=&quot;name1, name2, name3&quot; class=&quot;com.javadoop.ExampleBean&quot;      scope=&quot;singleton&quot; lazy-init=&quot;true&quot; init-method=&quot;init&quot; destroy-method=&quot;cleanup&quot;&gt;      &lt;!-- 可以用下面三种形式指定构造参数 --&gt;    &lt;constructor-arg type=&quot;int&quot; value=&quot;7500000&quot;/&gt;    &lt;constructor-arg name=&quot;years&quot; value=&quot;7500000&quot;/&gt;    &lt;constructor-arg index=&quot;0&quot; value=&quot;7500000&quot;/&gt;      &lt;!-- property 的几种情况 --&gt;    &lt;property name=&quot;beanOne&quot;&gt;        &lt;ref bean=&quot;anotherExampleBean&quot;/&gt;    &lt;/property&gt;    &lt;property name=&quot;beanTwo&quot; ref=&quot;yetAnotherBean&quot;/&gt;    &lt;property name=&quot;integerProperty&quot; value=&quot;1&quot;/&gt;&lt;/bean&gt;</code></pre><p>当然，除了上面举例出来的这些，还有 factory-bean、factory-method、<code>&lt;lockup-method /&gt;</code>、<code>&lt;replaced-method /&gt;</code>、<code>&lt;meta /&gt;</code>、<code>&lt;qualifier /&gt;</code> 这几个，大家是不是熟悉呢？自己检验一下自己对 Spring 中 bean 的了解程度。</p><p>有了以上这些知识以后，我们再继续往里看怎么解析 bean 元素，是怎么转换到 BeanDefinitionHolder 的。</p><p>// BeanDefinitionParserDelegate 428</p><pre><code class="language-java">public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) {    return parseBeanDefinitionElement(ele, null);}public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, BeanDefinition containingBean) {   String id = ele.getAttribute(ID_ATTRIBUTE);   String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);   List&lt;String&gt; aliases = new ArrayList&lt;String&gt;();         // 将 name 属性的定义按照 “逗号、分号、空格” 切分，形成一个 别名列表数组，   // 当然，如果你不定义 name 属性的话，就是空的了   // 我在附录中简单介绍了一下 id 和 name 的配置，大家可以看一眼，有个20秒就可以了   if (StringUtils.hasLength(nameAttr)) {      String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);      aliases.addAll(Arrays.asList(nameArr));   }   String beanName = id;   // 如果没有指定id, 那么用别名列表的第一个名字作为beanName   if (!StringUtils.hasText(beanName) &amp;&amp; !aliases.isEmpty()) {      beanName = aliases.remove(0);      if (logger.isDebugEnabled()) {         logger.debug(&quot;No XML 'id' specified - using '&quot; + beanName +               &quot;' as bean name and &quot; + aliases + &quot; as aliases&quot;);      }   }   if (containingBean == null) {      checkNameUniqueness(beanName, aliases, ele);   }     // 根据 &lt;bean ...&gt;...&lt;/bean&gt; 中的配置创建 BeanDefinition，然后把配置中的信息都设置到实例中,   // 细节后面细说，先知道下面这行结束后，一个 BeanDefinition 实例就出来了。   AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);      // 到这里，整个 &lt;bean /&gt; 标签就算解析结束了，一个 BeanDefinition 就形成了。   if (beanDefinition != null) {      // 如果都没有设置 id 和 name，那么此时的 beanName 就会为 null，进入下面这块代码产生      // 如果读者不感兴趣的话，我觉得不需要关心这块代码，对本文源码分析来说，这些东西不重要      if (!StringUtils.hasText(beanName)) {         try {            if (containingBean != null) {// 按照我们的思路，这里 containingBean 是 null 的               beanName = BeanDefinitionReaderUtils.generateBeanName(                     beanDefinition, this.readerContext.getRegistry(), true);            }            else {               // 如果我们不定义 id 和 name，那么我们引言里的那个例子：               //   1. beanName 为：com.javadoop.example.MessageServiceImpl#0               //   2. beanClassName 为：com.javadoop.example.MessageServiceImpl                             beanName = this.readerContext.generateBeanName(beanDefinition);                              String beanClassName = beanDefinition.getBeanClassName();               if (beanClassName != null &amp;&amp;                     beanName.startsWith(beanClassName) &amp;&amp; beanName.length() &gt; beanClassName.length() &amp;&amp;                     !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {                  // 把 beanClassName 设置为 Bean 的别名                  aliases.add(beanClassName);               }            }            if (logger.isDebugEnabled()) {               logger.debug(&quot;Neither XML 'id' nor 'name' specified - &quot; +                     &quot;using generated bean name [&quot; + beanName + &quot;]&quot;);            }         }         catch (Exception ex) {            error(ex.getMessage(), ele);            return null;         }      }      String[] aliasesArray = StringUtils.toStringArray(aliases);      // 返回 BeanDefinitionHolder      return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);   }   return null;}</code></pre><p>然后，我们再看看怎么根据配置创建 BeanDefinition 实例的：</p><pre><code class="language-java">public AbstractBeanDefinition parseBeanDefinitionElement(      Element ele, String beanName, BeanDefinition containingBean) {   this.parseState.push(new BeanEntry(beanName));   String className = null;   if (ele.hasAttribute(CLASS_ATTRIBUTE)) {      className = ele.getAttribute(CLASS_ATTRIBUTE).trim();   }   try {      String parent = null;      if (ele.hasAttribute(PARENT_ATTRIBUTE)) {         parent = ele.getAttribute(PARENT_ATTRIBUTE);      }      // 创建 BeanDefinition，然后设置类信息而已，很简单，就不贴代码了      AbstractBeanDefinition bd = createBeanDefinition(className, parent);      // 设置 BeanDefinition 的一堆属性，这些属性定义在 AbstractBeanDefinition 中      parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);      bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));          /**       * 下面的一堆是解析 &lt;bean&gt;......&lt;/bean&gt; 内部的子元素，       * 解析出来以后的信息都放到 bd 的属性中       */           // 解析 &lt;meta /&gt;      parseMetaElements(ele, bd);      // 解析 &lt;lookup-method /&gt;      parseLookupOverrideSubElements(ele, bd.getMethodOverrides());      // 解析 &lt;replaced-method /&gt;      parseReplacedMethodSubElements(ele, bd.getMethodOverrides());    // 解析 &lt;constructor-arg /&gt;      parseConstructorArgElements(ele, bd);      // 解析 &lt;property /&gt;      parsePropertyElements(ele, bd);      // 解析 &lt;qualifier /&gt;      parseQualifierElements(ele, bd);      bd.setResource(this.readerContext.getResource());      bd.setSource(extractSource(ele));      return bd;   }   catch (ClassNotFoundException ex) {      error(&quot;Bean class [&quot; + className + &quot;] not found&quot;, ele, ex);   }   catch (NoClassDefFoundError err) {      error(&quot;Class that bean class [&quot; + className + &quot;] depends on not found&quot;, ele, err);   }   catch (Throwable ex) {      error(&quot;Unexpected failure during bean definition parsing&quot;, ele, ex);   }   finally {      this.parseState.pop();   }   return null;}</code></pre><p>到这里，我们已经完成了根据 <code>&lt;bean /&gt;</code> 配置创建了一个 BeanDefinitionHolder 实例。注意，是一个。</p><p>我们回到解析 <code>&lt;bean /&gt;</code> 的入口方法:</p><pre><code class="language-java">protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {   // 将 &lt;bean /&gt; 节点转换为 BeanDefinitionHolder，就是上面说的一堆   BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);   if (bdHolder != null) {      // 如果有自定义属性的话，进行相应的解析，先忽略      bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);      try {         // 我们把这步叫做 注册Bean 吧         BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());      }      catch (BeanDefinitionStoreException ex) {         getReaderContext().error(&quot;Failed to register bean definition with name '&quot; +               bdHolder.getBeanName() + &quot;'&quot;, ele, ex);      }      // 注册完成后，发送事件，本文不展开说这个      getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));   }}</code></pre><p>大家再仔细看一下这块吧，我们后面就不回来说这个了。这里已经根据一个 <code>&lt;bean /&gt;</code> 标签产生了一个 BeanDefinitionHolder 的实例，这个实例里面也就是一个 BeanDefinition 的实例和它的 beanName、aliases 这三个信息，注意，我们的关注点始终在 BeanDefinition 上：</p><pre><code class="language-java">public class BeanDefinitionHolder implements BeanMetadataElement {  private final BeanDefinition beanDefinition;  private final String beanName;  private final String[] aliases;...</code></pre><p>然后我们准备注册这个 BeanDefinition，最后，把这个注册事件发送出去。</p><p>下面，我们开始说说注册 Bean 吧。</p><h5 id="注册-bean">注册 Bean</h5><p>// BeanDefinitionReaderUtils 143</p><pre><code class="language-java">public static void registerBeanDefinition(      BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)      throws BeanDefinitionStoreException {   String beanName = definitionHolder.getBeanName();   // 注册这个 Bean   registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());   // 如果还有别名的话，也要根据别名全部注册一遍，不然根据别名就会找不到 Bean 了   String[] aliases = definitionHolder.getAliases();   if (aliases != null) {      for (String alias : aliases) {         // alias -&gt; beanName 保存它们的别名信息，这个很简单，用一个 map 保存一下就可以了，         // 获取的时候，会先将 alias 转换为 beanName，然后再查找         registry.registerAlias(beanName, alias);      }   }}</code></pre><p>别名注册的放一边，毕竟它很简单，我们看看怎么注册 Bean。</p><p>// DefaultListableBeanFactory 793</p><pre><code class="language-java">@Overridepublic void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)      throws BeanDefinitionStoreException {   Assert.hasText(beanName, &quot;Bean name must not be empty&quot;);   Assert.notNull(beanDefinition, &quot;BeanDefinition must not be null&quot;);   if (beanDefinition instanceof AbstractBeanDefinition) {      try {         ((AbstractBeanDefinition) beanDefinition).validate();      }      catch (BeanDefinitionValidationException ex) {         throw new BeanDefinitionStoreException(...);      }   }   // old? 还记得 “允许 bean 覆盖” 这个配置吗？allowBeanDefinitionOverriding   BeanDefinition oldBeanDefinition;     // 之后会看到，所有的 Bean 注册后会放入这个 beanDefinitionMap 中   oldBeanDefinition = this.beanDefinitionMap.get(beanName);     // 处理重复名称的 Bean 定义的情况   if (oldBeanDefinition != null) {      if (!isAllowBeanDefinitionOverriding()) {         // 如果不允许覆盖的话，抛异常         throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription()...      }      else if (oldBeanDefinition.getRole() &lt; beanDefinition.getRole()) {         // log...用框架定义的 Bean 覆盖用户自定义的 Bean       }      else if (!beanDefinition.equals(oldBeanDefinition)) {         // log...用新的 Bean 覆盖旧的 Bean      }      else {         // log...用同等的 Bean 覆盖旧的 Bean，这里指的是 equals 方法返回 true 的 Bean      }      // 覆盖      this.beanDefinitionMap.put(beanName, beanDefinition);   }   else {      // 判断是否已经有其他的 Bean 开始初始化了.      // 注意，&quot;注册Bean&quot; 这个动作结束，Bean 依然还没有初始化，我们后面会有大篇幅说初始化过程，      // 在 Spring 容器启动的最后，会 预初始化 所有的 singleton beans      if (hasBeanCreationStarted()) {         // Cannot modify startup-time collection elements anymore (for stable iteration)         synchronized (this.beanDefinitionMap) {            this.beanDefinitionMap.put(beanName, beanDefinition);            List&lt;String&gt; updatedDefinitions = new ArrayList&lt;String&gt;(this.beanDefinitionNames.size() + 1);            updatedDefinitions.addAll(this.beanDefinitionNames);            updatedDefinitions.add(beanName);            this.beanDefinitionNames = updatedDefinitions;            if (this.manualSingletonNames.contains(beanName)) {               Set&lt;String&gt; updatedSingletons = new LinkedHashSet&lt;String&gt;(this.manualSingletonNames);               updatedSingletons.remove(beanName);               this.manualSingletonNames = updatedSingletons;            }         }      }      else {         // 最正常的应该是进到这个分支。                 // 将 BeanDefinition 放到这个 map 中，这个 map 保存了所有的 BeanDefinition         this.beanDefinitionMap.put(beanName, beanDefinition);         // 这是个 ArrayList，所以会按照 bean 配置的顺序保存每一个注册的 Bean 的名字         this.beanDefinitionNames.add(beanName);         // 这是个 LinkedHashSet，代表的是手动注册的 singleton bean，         // 注意这里是 remove 方法，到这里的 Bean 当然不是手动注册的         // 手动指的是通过调用以下方法注册的 bean ：         //     registerSingleton(String beanName, Object singletonObject)         // 这不是重点，解释只是为了不让大家疑惑。Spring 会在后面&quot;手动&quot;注册一些 Bean，         // 如 &quot;environment&quot;、&quot;systemProperties&quot; 等 bean，我们自己也可以在运行时注册 Bean 到容器中的         this.manualSingletonNames.remove(beanName);      }      // 这个不重要，在预初始化的时候会用到，不必管它。      this.frozenBeanDefinitionNames = null;   }   if (oldBeanDefinition != null || containsSingleton(beanName)) {      resetBeanDefinition(beanName);   }}</code></pre><p>总结一下，到这里已经初始化了 Bean 容器，<code>&lt;bean /&gt;</code> 配置也相应的转换为了一个个 BeanDefinition，然后注册了各个 BeanDefinition 到注册中心，并且发送了注册事件。</p><p>--------- 分割线 ---------</p><p>到这里是一个分水岭，前面的内容都还算比较简单，不过应该也比较繁琐，大家要清楚地知道前面都做了哪些事情。</p><h3 id="bean-容器实例化完成后">Bean 容器实例化完成后</h3><p>说到这里，我们回到 refresh() 方法，我重新贴了一遍代码，看看我们说到哪了。是的，我们才说完 obtainFreshBeanFactory() 方法。</p><p>考虑到篇幅，这里开始大幅缩减掉没必要详细介绍的部分，大家直接看下面的代码中的注释就好了。</p><pre><code class="language-java">@Overridepublic void refresh() throws BeansException, IllegalStateException {   // 来个锁，不然 refresh() 还没结束，你又来个启动或销毁容器的操作，那不就乱套了嘛   synchronized (this.startupShutdownMonitor) {      // 准备工作，记录下容器的启动时间、标记“已启动”状态、处理配置文件中的占位符      prepareRefresh();           // 这步比较关键，这步完成后，配置文件就会解析成一个个 Bean 定义，注册到 BeanFactory 中，      // 当然，这里说的 Bean 还没有初始化，只是配置信息都提取出来了，      // 注册也只是将这些信息都保存到了注册中心(说到底核心是一个 beanName-&gt; beanDefinition 的 map)      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();      // 设置 BeanFactory 的类加载器，添加几个 BeanPostProcessor，手动注册几个特殊的 bean      // 这块待会会展开说      prepareBeanFactory(beanFactory);      try {         // 【这里需要知道 BeanFactoryPostProcessor 这个知识点，Bean 如果实现了此接口，         // 那么在容器初始化以后，Spring 会负责调用里面的 postProcessBeanFactory 方法。】                 // 这里是提供给子类的扩展点，到这里的时候，所有的 Bean 都加载、注册完成了，但是都还没有初始化         // 具体的子类可以在这步的时候添加一些特殊的 BeanFactoryPostProcessor 的实现类或做点什么事         postProcessBeanFactory(beanFactory);         // 调用 BeanFactoryPostProcessor 各个实现类的 postProcessBeanFactory(factory) 回调方法         invokeBeanFactoryPostProcessors(beanFactory);                                      // 注册 BeanPostProcessor 的实现类，注意看和 BeanFactoryPostProcessor 的区别         // 此接口两个方法: postProcessBeforeInitialization 和 postProcessAfterInitialization         // 两个方法分别在 Bean 初始化之前和初始化之后得到执行。这里仅仅是注册，之后会看到回调这两方法的时机         registerBeanPostProcessors(beanFactory);         // 初始化当前 ApplicationContext 的 MessageSource，国际化这里就不展开说了，不然没完没了了         initMessageSource();         // 初始化当前 ApplicationContext 的事件广播器，这里也不展开了         initApplicationEventMulticaster();         // 从方法名就可以知道，典型的模板方法(钩子方法)，不展开说         // 具体的子类可以在这里初始化一些特殊的 Bean（在初始化 singleton beans 之前）         onRefresh();         // 注册事件监听器，监听器需要实现 ApplicationListener 接口。这也不是我们的重点，过         registerListeners();         // 重点，重点，重点         // 初始化所有的 singleton beans         //（lazy-init 的除外）         finishBeanFactoryInitialization(beanFactory);         // 最后，广播事件，ApplicationContext 初始化完成，不展开         finishRefresh();      }      catch (BeansException ex) {         if (logger.isWarnEnabled()) {            logger.warn(&quot;Exception encountered during context initialization - &quot; +                  &quot;cancelling refresh attempt: &quot; + ex);         }         // Destroy already created singletons to avoid dangling resources.         // 销毁已经初始化的 singleton 的 Beans，以免有些 bean 会一直占用资源         destroyBeans();         // Reset 'active' flag.         cancelRefresh(ex);         // 把异常往外抛         throw ex;      }      finally {         // Reset common introspection caches in Spring's core, since we         // might not ever need metadata for singleton beans anymore...         resetCommonCaches();      }   }}</code></pre><h3 id="准备-bean-容器-preparebeanfactory">准备 Bean 容器: prepareBeanFactory</h3><p>之前我们说过，Spring 把我们在 xml 配置的 bean 都注册以后，会&quot;手动&quot;注册一些特殊的 bean。</p><p>这里简单介绍下 prepareBeanFactory(factory) 方法：</p><pre><code class="language-java">/** * Configure the factory's standard context characteristics, * such as the context's ClassLoader and post-processors. * @param beanFactory the BeanFactory to configure */protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {   // 设置 BeanFactory 的类加载器，我们知道 BeanFactory 需要加载类，也就需要类加载器，   // 这里设置为加载当前 ApplicationContext 类的类加载器   beanFactory.setBeanClassLoader(getClassLoader());       // 设置 BeanExpressionResolver   beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));   //    beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));   // 添加一个 BeanPostProcessor，这个 processor 比较简单：   // 实现了 Aware 接口的 beans 在初始化的时候，这个 processor 负责回调，   // 这个我们很常用，如我们会为了获取 ApplicationContext 而 implement ApplicationContextAware   // 注意：它不仅仅回调 ApplicationContextAware，   //   还会负责回调 EnvironmentAware、ResourceLoaderAware 等，看下源码就清楚了   beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));     // 下面几行的意思就是，如果某个 bean 依赖于以下几个接口的实现类，在自动装配的时候忽略它们，   // Spring 会通过其他方式来处理这些依赖。   beanFactory.ignoreDependencyInterface(EnvironmentAware.class);   beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);   beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);   beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);   beanFactory.ignoreDependencyInterface(MessageSourceAware.class);   beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);   /**    * 下面几行就是为特殊的几个 bean 赋值，如果有 bean 依赖了以下几个，会注入这边相应的值，    * 之前我们说过，&quot;当前 ApplicationContext 持有一个 BeanFactory&quot;，这里解释了第一行。    * ApplicationContext 还继承了 ResourceLoader、ApplicationEventPublisher、MessageSource    * 所以对于这几个依赖，可以赋值为 this，注意 this 是一个 ApplicationContext    * 那这里怎么没看到为 MessageSource 赋值呢？那是因为 MessageSource 被注册成为了一个普通的 bean    */   beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);   beanFactory.registerResolvableDependency(ResourceLoader.class, this);   beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);   beanFactory.registerResolvableDependency(ApplicationContext.class, this);   // 这个 BeanPostProcessor 也很简单，在 bean 实例化后，如果是 ApplicationListener 的子类，   // 那么将其添加到 listener 列表中，可以理解成：注册 事件监听器   beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));   // 这里涉及到特殊的 bean，名为：loadTimeWeaver，这不是我们的重点，忽略它   // tips: ltw 是 AspectJ 的概念，指的是在运行期进行织入，这个和 Spring AOP 不一样，   //    感兴趣的读者请参考我写的关于 AspectJ 的另一篇文章 https://www.javadoop.com/post/aspectj   if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {      beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));      // Set a temporary ClassLoader for type matching.      beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));   }   /**    * 从下面几行代码我们可以知道，Spring 往往很 &quot;智能&quot; 就是因为它会帮我们默认注册一些有用的 bean，    * 我们也可以选择覆盖    */     // 如果没有定义 &quot;environment&quot; 这个 bean，那么 Spring 会 &quot;手动&quot; 注册一个   if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {      beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());   }   // 如果没有定义 &quot;systemProperties&quot; 这个 bean，那么 Spring 会 &quot;手动&quot; 注册一个   if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {      beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());   }   // 如果没有定义 &quot;systemEnvironment&quot; 这个 bean，那么 Spring 会 &quot;手动&quot; 注册一个   if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {      beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());   }}</code></pre><p>在上面这块代码中，Spring 对一些特殊的 bean 进行了处理，读者如果暂时还不能消化它们也没有关系，慢慢往下看。</p><h3 id="初始化所有的-singleton-beans">初始化所有的 singleton beans</h3><p>我们的重点当然是 <code>finishBeanFactoryInitialization(beanFactory);</code> 这个巨头了，这里会负责初始化所有的 singleton beans。</p><p>注意，后面的描述中，我都会使用<strong>初始化</strong>或<strong>预初始化</strong>来代表这个阶段，Spring 会在这个阶段完成所有的 singleton beans 的实例化。</p><p>我们来总结一下，到目前为止，应该说 BeanFactory 已经创建完成，并且所有的实现了 BeanFactoryPostProcessor 接口的 Bean 都已经初始化并且其中的 postProcessBeanFactory(factory) 方法已经得到回调执行了。而且 Spring 已经“手动”注册了一些特殊的 Bean，如 <code>environment</code>、<code>systemProperties</code> 等。</p><p>剩下的就是初始化 singleton beans 了，我们知道它们是单例的，如果没有设置懒加载，那么 Spring 会在接下来初始化所有的 singleton beans。</p><p>// AbstractApplicationContext.java 834</p><pre><code class="language-java">// 初始化剩余的 singleton beansprotected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {   // 首先，初始化名字为 conversionService 的 Bean。本着送佛送到西的精神，我在附录中简单介绍了一下 ConversionService，因为这实在太实用了   // 什么，看代码这里没有初始化 Bean 啊！   // 注意了，初始化的动作包装在 beanFactory.getBean(...) 中，这里先不说细节，先往下看吧   if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &amp;&amp;         beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {      beanFactory.setConversionService(            beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));   }   // Register a default embedded value resolver if no bean post-processor   // (such as a PropertyPlaceholderConfigurer bean) registered any before:   // at this point, primarily for resolution in annotation attribute values.   if (!beanFactory.hasEmbeddedValueResolver()) {      beanFactory.addEmbeddedValueResolver(new StringValueResolver() {         @Override         public String resolveStringValue(String strVal) {            return getEnvironment().resolvePlaceholders(strVal);         }      });   }   // 先初始化 LoadTimeWeaverAware 类型的 Bean   // 之前也说过，这是 AspectJ 相关的内容，放心跳过吧   String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);   for (String weaverAwareName : weaverAwareNames) {      getBean(weaverAwareName);   }   // Stop using the temporary ClassLoader for type matching.   beanFactory.setTempClassLoader(null);   // 没什么别的目的，因为到这一步的时候，Spring 已经开始预初始化 singleton beans 了，   // 肯定不希望这个时候还出现 bean 定义解析、加载、注册。   beanFactory.freezeConfiguration();   // 开始初始化   beanFactory.preInstantiateSingletons();}</code></pre><p>从上面最后一行往里看，我们就又回到 DefaultListableBeanFactory 这个类了，这个类大家应该都不陌生了吧。</p><h4 id="preinstantiatesingletons">preInstantiateSingletons</h4><p>// DefaultListableBeanFactory 728</p><pre><code class="language-java">@Overridepublic void preInstantiateSingletons() throws BeansException {   if (this.logger.isDebugEnabled()) {      this.logger.debug(&quot;Pre-instantiating singletons in &quot; + this);   }   // this.beanDefinitionNames 保存了所有的 beanNames   List&lt;String&gt; beanNames = new ArrayList&lt;String&gt;(this.beanDefinitionNames);   // 下面这个循环，触发所有的非懒加载的 singleton beans 的初始化操作   for (String beanName : beanNames) {           // 合并父 Bean 中的配置，注意 &lt;bean id=&quot;&quot; class=&quot;&quot; parent=&quot;&quot; /&gt; 中的 parent，用的不多吧，      // 考虑到这可能会影响大家的理解，我在附录中解释了一下 &quot;Bean 继承&quot;，不了解的请到附录中看一下      RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);           // 非抽象、非懒加载的 singletons。如果配置了 'abstract = true'，那是不需要初始化的      if (!bd.isAbstract() &amp;&amp; bd.isSingleton() &amp;&amp; !bd.isLazyInit()) {         // 处理 FactoryBean(读者如果不熟悉 FactoryBean，请移步附录区了解)         if (isFactoryBean(beanName)) {            // FactoryBean 的话，在 beanName 前面加上 ‘&amp;’ 符号。再调用 getBean，getBean 方法别急            final FactoryBean&lt;?&gt; factory = (FactoryBean&lt;?&gt;) getBean(FACTORY_BEAN_PREFIX + beanName);            // 判断当前 FactoryBean 是否是 SmartFactoryBean 的实现，此处忽略，直接跳过            boolean isEagerInit;            if (System.getSecurityManager() != null &amp;&amp; factory instanceof SmartFactoryBean) {               isEagerInit = AccessController.doPrivileged(new PrivilegedAction&lt;Boolean&gt;() {                  @Override                  public Boolean run() {                     return ((SmartFactoryBean&lt;?&gt;) factory).isEagerInit();                  }               }, getAccessControlContext());            }            else {               isEagerInit = (factory instanceof SmartFactoryBean &amp;&amp;                     ((SmartFactoryBean&lt;?&gt;) factory).isEagerInit());            }            if (isEagerInit) {                              getBean(beanName);            }         }         else {            // 对于普通的 Bean，只要调用 getBean(beanName) 这个方法就可以进行初始化了            getBean(beanName);         }      }   }   // 到这里说明所有的非懒加载的 singleton beans 已经完成了初始化   // 如果我们定义的 bean 是实现了 SmartInitializingSingleton 接口的，那么在这里得到回调，忽略   for (String beanName : beanNames) {      Object singletonInstance = getSingleton(beanName);      if (singletonInstance instanceof SmartInitializingSingleton) {         final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;         if (System.getSecurityManager() != null) {            AccessController.doPrivileged(new PrivilegedAction&lt;Object&gt;() {               @Override               public Object run() {                  smartSingleton.afterSingletonsInstantiated();                  return null;               }            }, getAccessControlContext());         }         else {            smartSingleton.afterSingletonsInstantiated();         }      }   }}</code></pre><p>接下来，我们就进入到 getBean(beanName) 方法了，这个方法我们经常用来从 BeanFactory 中获取一个 Bean，而初始化的过程也封装到了这个方法里。</p><h4 id="getbean">getBean</h4><p>在继续前进之前，读者应该具备 FactoryBean 的知识，如果读者还不熟悉，请移步附录部分了解 FactoryBean。</p><p>// AbstractBeanFactory 196</p><pre><code class="language-java">@Overridepublic Object getBean(String name) throws BeansException {   return doGetBean(name, null, null, false);}// 我们在剖析初始化 Bean 的过程，但是 getBean 方法我们经常是用来从容器中获取 Bean 用的，注意切换思路，// 已经初始化过了就从容器中直接返回，否则就先初始化再返回@SuppressWarnings(&quot;unchecked&quot;)protected &lt;T&gt; T doGetBean(      final String name, final Class&lt;T&gt; requiredType, final Object[] args, boolean typeCheckOnly)      throws BeansException {   // 获取一个 “正统的” beanName，处理两种情况，一个是前面说的 FactoryBean(前面带 ‘&amp;’)，   // 一个是别名问题，因为这个方法是 getBean，获取 Bean 用的，你要是传一个别名进来，是完全可以的   final String beanName = transformedBeanName(name);     // 注意跟着这个，这个是返回值   Object bean;    // 检查下是不是已经创建过了   Object sharedInstance = getSingleton(beanName);     // 这里说下 args 呗，虽然看上去一点不重要。前面我们一路进来的时候都是 getBean(beanName)，   // 所以 args 传参其实是 null 的，但是如果 args 不为空的时候，那么意味着调用方不是希望获取 Bean，而是创建 Bean   if (sharedInstance != null &amp;&amp; args == null) {      if (logger.isDebugEnabled()) {         if (isSingletonCurrentlyInCreation(beanName)) {            logger.debug(&quot;...&quot;);         }         else {            logger.debug(&quot;Returning cached instance of singleton bean '&quot; + beanName + &quot;'&quot;);         }      }      // 下面这个方法：如果是普通 Bean 的话，直接返回 sharedInstance，      // 如果是 FactoryBean 的话，返回它创建的那个实例对象      // (FactoryBean 知识，读者若不清楚请移步附录)      bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);   }   else {      if (isPrototypeCurrentlyInCreation(beanName)) {         // 创建过了此 beanName 的 prototype 类型的 bean，那么抛异常，         // 往往是因为陷入了循环引用         throw new BeanCurrentlyInCreationException(beanName);      }      // 检查一下这个 BeanDefinition 在容器中是否存在      BeanFactory parentBeanFactory = getParentBeanFactory();      if (parentBeanFactory != null &amp;&amp; !containsBeanDefinition(beanName)) {         // 如果当前容器不存在这个 BeanDefinition，试试父容器中有没有         String nameToLookup = originalBeanName(name);         if (args != null) {            // 返回父容器的查询结果            return (T) parentBeanFactory.getBean(nameToLookup, args);         }         else {            // No args -&gt; delegate to standard getBean method.            return parentBeanFactory.getBean(nameToLookup, requiredType);         }      }      if (!typeCheckOnly) {         // typeCheckOnly 为 false，将当前 beanName 放入一个 alreadyCreated 的 Set 集合中。         markBeanAsCreated(beanName);      }      /*       * 稍稍总结一下：       * 到这里的话，要准备创建 Bean 了，对于 singleton 的 Bean 来说，容器中还没创建过此 Bean；       * 对于 prototype 的 Bean 来说，本来就是要创建一个新的 Bean。       */      try {         final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);         checkMergedBeanDefinition(mbd, beanName, args);         // 先初始化依赖的所有 Bean，这个很好理解。         // 注意，这里的依赖指的是 depends-on 中定义的依赖         String[] dependsOn = mbd.getDependsOn();         if (dependsOn != null) {            for (String dep : dependsOn) {               // 检查是不是有循环依赖，这里的循环依赖和我们前面说的循环依赖又不一样，这里肯定是不允许出现的，不然要乱套了，读者想一下就知道了               if (isDependent(beanName, dep)) {                  throw new BeanCreationException(mbd.getResourceDescription(), beanName,                        &quot;Circular depends-on relationship between '&quot; + beanName + &quot;' and '&quot; + dep + &quot;'&quot;);               }               // 注册一下依赖关系               registerDependentBean(dep, beanName);               // 先初始化被依赖项               getBean(dep);            }         }         // 如果是 singleton scope 的，创建 singleton 的实例         if (mbd.isSingleton()) {            sharedInstance = getSingleton(beanName, new ObjectFactory&lt;Object&gt;() {               @Override               public Object getObject() throws BeansException {                  try {                     // 执行创建 Bean，详情后面再说                     return createBean(beanName, mbd, args);                  }                  catch (BeansException ex) {                     destroySingleton(beanName);                     throw ex;                  }               }            });            bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);         }         // 如果是 prototype scope 的，创建 prototype 的实例         else if (mbd.isPrototype()) {            // It's a prototype -&gt; create a new instance.            Object prototypeInstance = null;            try {               beforePrototypeCreation(beanName);               // 执行创建 Bean               prototypeInstance = createBean(beanName, mbd, args);            }            finally {               afterPrototypeCreation(beanName);            }            bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);         }         // 如果不是 singleton 和 prototype 的话，需要委托给相应的实现类来处理         else {            String scopeName = mbd.getScope();            final Scope scope = this.scopes.get(scopeName);            if (scope == null) {               throw new IllegalStateException(&quot;No Scope registered for scope name '&quot; + scopeName + &quot;'&quot;);            }            try {               Object scopedInstance = scope.get(beanName, new ObjectFactory&lt;Object&gt;() {                  @Override                  public Object getObject() throws BeansException {                     beforePrototypeCreation(beanName);                     try {                        // 执行创建 Bean                        return createBean(beanName, mbd, args);                     }                     finally {                        afterPrototypeCreation(beanName);                     }                  }               });               bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);            }            catch (IllegalStateException ex) {               throw new BeanCreationException(beanName,                     &quot;Scope '&quot; + scopeName + &quot;' is not active for the current thread; consider &quot; +                     &quot;defining a scoped proxy for this bean if you intend to refer to it from a singleton&quot;,                     ex);            }         }      }      catch (BeansException ex) {         cleanupAfterBeanCreationFailure(beanName);         throw ex;      }   }   // 最后，检查一下类型对不对，不对的话就抛异常，对的话就返回了   if (requiredType != null &amp;&amp; bean != null &amp;&amp; !requiredType.isInstance(bean)) {      try {         return getTypeConverter().convertIfNecessary(bean, requiredType);      }      catch (TypeMismatchException ex) {         if (logger.isDebugEnabled()) {            logger.debug(&quot;Failed to convert bean '&quot; + name + &quot;' to required type '&quot; +                  ClassUtils.getQualifiedName(requiredType) + &quot;'&quot;, ex);         }         throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());      }   }   return (T) bean;}</code></pre><p>大家应该也猜到了，接下来当然是分析 createBean 方法：</p><pre><code class="language-java">protected abstract Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException;</code></pre><p>第三个参数 args 数组代表创建实例需要的参数，不就是给构造方法用的参数，或者是工厂 Bean 的参数嘛，不过要注意，在我们的初始化阶段，args 是 null。</p><p>这回我们要到一个新的类了 AbstractAutowireCapableBeanFactory，看类名，AutowireCapable？类名是不是也说明了点问题了。</p><p>主要是为了以下场景，采用 @Autowired 注解注入属性值：</p><pre><code class="language-java">public class MessageServiceImpl implements MessageService {    @Autowired    private UserService userService;      public String getMessage() {        return userService.getMessage();    }}</code></pre><pre><code class="language-xml">&lt;bean id=&quot;messageService&quot; class=&quot;com.javadoop.example.MessageServiceImpl&quot; /&gt;</code></pre><p>以上这种属于混用了 xml 和 注解 两种方式的配置方式，Spring 会处理这种情况。</p><p>好了，读者要知道这么回事就可以了，继续向前。</p><p>// AbstractAutowireCapableBeanFactory 447</p><pre><code class="language-java">/** * Central method of this class: creates a bean instance, * populates the bean instance, applies post-processors, etc. * @see #doCreateBean */@Overrideprotected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {   if (logger.isDebugEnabled()) {      logger.debug(&quot;Creating instance of bean '&quot; + beanName + &quot;'&quot;);   }   RootBeanDefinition mbdToUse = mbd;   // 确保 BeanDefinition 中的 Class 被加载   Class&lt;?&gt; resolvedClass = resolveBeanClass(mbd, beanName);   if (resolvedClass != null &amp;&amp; !mbd.hasBeanClass() &amp;&amp; mbd.getBeanClassName() != null) {      mbdToUse = new RootBeanDefinition(mbd);      mbdToUse.setBeanClass(resolvedClass);   }   // 准备方法覆写，这里又涉及到一个概念：MethodOverrides，它来自于 bean 定义中的 &lt;lookup-method /&gt;    // 和 &lt;replaced-method /&gt;，如果读者感兴趣，回到 bean 解析的地方看看对这两个标签的解析。   // 我在附录中也对这两个标签的相关知识点进行了介绍，读者可以移步去看看   try {      mbdToUse.prepareMethodOverrides();   }   catch (BeanDefinitionValidationException ex) {      throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(),            beanName, &quot;Validation of method overrides failed&quot;, ex);   }   try {      // 让 InstantiationAwareBeanPostProcessor 在这一步有机会返回代理，      // 在 《Spring AOP 源码分析》那篇文章中有解释，这里先跳过      Object bean = resolveBeforeInstantiation(beanName, mbdToUse);      if (bean != null) {         return bean;       }   }   catch (Throwable ex) {      throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName,            &quot;BeanPostProcessor before instantiation of bean failed&quot;, ex);   }   // 重头戏，创建 bean   Object beanInstance = doCreateBean(beanName, mbdToUse, args);   if (logger.isDebugEnabled()) {      logger.debug(&quot;Finished creating instance of bean '&quot; + beanName + &quot;'&quot;);   }   return beanInstance;}</code></pre><h4 id="创建-bean">创建 Bean</h4><p>我们继续往里看 doCreateBean 这个方法：</p><pre><code class="language-java">/** * Actually create the specified bean. Pre-creation processing has already happened * at this point, e.g. checking {@code postProcessBeforeInstantiation} callbacks. * &lt;p&gt;Differentiates between default bean instantiation, use of a * factory method, and autowiring a constructor. * @param beanName the name of the bean * @param mbd the merged bean definition for the bean * @param args explicit arguments to use for constructor or factory method invocation * @return a new instance of the bean * @throws BeanCreationException if the bean could not be created * @see #instantiateBean * @see #instantiateUsingFactoryMethod * @see #autowireConstructor */protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)      throws BeanCreationException {   // Instantiate the bean.   BeanWrapper instanceWrapper = null;   if (mbd.isSingleton()) {      instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);   }   if (instanceWrapper == null) {      // 说明不是 FactoryBean，这里实例化 Bean，这里非常关键，细节之后再说      instanceWrapper = createBeanInstance(beanName, mbd, args);   }   // 这个就是 Bean 里面的 我们定义的类 的实例，很多地方我直接描述成 &quot;bean 实例&quot;   final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);   // 类型   Class&lt;?&gt; beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);   mbd.resolvedTargetType = beanType;   // 建议跳过吧，涉及接口：MergedBeanDefinitionPostProcessor   synchronized (mbd.postProcessingLock) {      if (!mbd.postProcessed) {         try {            // MergedBeanDefinitionPostProcessor，这个我真不展开说了，直接跳过吧，很少用的            applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);         }         catch (Throwable ex) {            throw new BeanCreationException(mbd.getResourceDescription(), beanName,                  &quot;Post-processing of merged bean definition failed&quot;, ex);         }         mbd.postProcessed = true;      }   }   // Eagerly cache singletons to be able to resolve circular references   // even when triggered by lifecycle interfaces like BeanFactoryAware.   // 下面这块代码是为了解决循环依赖的问题，以后有时间，我再对循环依赖这个问题进行解析吧   boolean earlySingletonExposure = (mbd.isSingleton() &amp;&amp; this.allowCircularReferences &amp;&amp;         isSingletonCurrentlyInCreation(beanName));   if (earlySingletonExposure) {      if (logger.isDebugEnabled()) {         logger.debug(&quot;Eagerly caching bean '&quot; + beanName +               &quot;' to allow for resolving potential circular references&quot;);      }      addSingletonFactory(beanName, new ObjectFactory&lt;Object&gt;() {         @Override         public Object getObject() throws BeansException {            return getEarlyBeanReference(beanName, mbd, bean);         }      });   }   // Initialize the bean instance.   Object exposedObject = bean;   try {      // 这一步也是非常关键的，这一步负责属性装配，因为前面的实例只是实例化了，并没有设值，这里就是设值      populateBean(beanName, mbd, instanceWrapper);      if (exposedObject != null) {         // 还记得 init-method 吗？还有 InitializingBean 接口？还有 BeanPostProcessor 接口？         // 这里就是处理 bean 初始化完成后的各种回调         exposedObject = initializeBean(beanName, exposedObject, mbd);      }   }   catch (Throwable ex) {      if (ex instanceof BeanCreationException &amp;&amp; beanName.equals(((BeanCreationException) ex).getBeanName())) {         throw (BeanCreationException) ex;      }      else {         throw new BeanCreationException(               mbd.getResourceDescription(), beanName, &quot;Initialization of bean failed&quot;, ex);      }   }   if (earlySingletonExposure) {      //       Object earlySingletonReference = getSingleton(beanName, false);      if (earlySingletonReference != null) {         if (exposedObject == bean) {            exposedObject = earlySingletonReference;         }         else if (!this.allowRawInjectionDespiteWrapping &amp;&amp; hasDependentBean(beanName)) {            String[] dependentBeans = getDependentBeans(beanName);            Set&lt;String&gt; actualDependentBeans = new LinkedHashSet&lt;String&gt;(dependentBeans.length);            for (String dependentBean : dependentBeans) {               if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {                  actualDependentBeans.add(dependentBean);               }            }            if (!actualDependentBeans.isEmpty()) {               throw new BeanCurrentlyInCreationException(beanName,                     &quot;Bean with name '&quot; + beanName + &quot;' has been injected into other beans [&quot; +                     StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +                     &quot;] in its raw version as part of a circular reference, but has eventually been &quot; +                     &quot;wrapped. This means that said other beans do not use the final version of the &quot; +                     &quot;bean. This is often the result of over-eager type matching - consider using &quot; +                     &quot;'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.&quot;);            }         }      }   }   // Register bean as disposable.   try {      registerDisposableBeanIfNecessary(beanName, bean, mbd);   }   catch (BeanDefinitionValidationException ex) {      throw new BeanCreationException(            mbd.getResourceDescription(), beanName, &quot;Invalid destruction signature&quot;, ex);   }   return exposedObject;}</code></pre><p>到这里，我们已经分析完了 doCreateBean 方法，总的来说，我们已经说完了整个初始化流程。</p><p>接下来我们挑 doCreateBean 中的三个细节出来说说。一个是创建 Bean 实例的 createBeanInstance 方法，一个是依赖注入的 populateBean 方法，还有就是回调方法 initializeBean。</p><p>注意了，接下来的这三个方法要认真说那也是极其复杂的，很多地方我就点到为止了，感兴趣的读者可以自己往里看，最好就是碰到不懂的，自己写代码去调试它。</p><h5 id="创建-bean-实例">创建 Bean 实例</h5><p>我们先看看 createBeanInstance 方法。需要说明的是，这个方法如果每个分支都分析下去，必然也是极其复杂冗长的，我们挑重点说。此方法的目的就是实例化我们指定的类。</p><pre><code class="language-java">protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) {   // 确保已经加载了此 class   Class&lt;?&gt; beanClass = resolveBeanClass(mbd, beanName);   // 校验一下这个类的访问权限   if (beanClass != null &amp;&amp; !Modifier.isPublic(beanClass.getModifiers()) &amp;&amp; !mbd.isNonPublicAccessAllowed()) {      throw new BeanCreationException(mbd.getResourceDescription(), beanName,            &quot;Bean class isn't public, and non-public access not allowed: &quot; + beanClass.getName());   }   if (mbd.getFactoryMethodName() != null)  {      // 采用工厂方法实例化，不熟悉这个概念的读者请看附录，注意，不是 FactoryBean      return instantiateUsingFactoryMethod(beanName, mbd, args);   }   // 如果不是第一次创建，比如第二次创建 prototype bean。   // 这种情况下，我们可以从第一次创建知道，采用无参构造函数，还是构造函数依赖注入 来完成实例化   boolean resolved = false;   boolean autowireNecessary = false;   if (args == null) {      synchronized (mbd.constructorArgumentLock) {         if (mbd.resolvedConstructorOrFactoryMethod != null) {            resolved = true;            autowireNecessary = mbd.constructorArgumentsResolved;         }      }   }   if (resolved) {      if (autowireNecessary) {         // 构造函数依赖注入         return autowireConstructor(beanName, mbd, null, null);      }      else {         // 无参构造函数         return instantiateBean(beanName, mbd);      }   }   // 判断是否采用有参构造函数   Constructor&lt;?&gt;[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);   if (ctors != null ||         mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR ||         mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args))  {      // 构造函数依赖注入      return autowireConstructor(beanName, mbd, ctors, args);   }   // 调用无参构造函数   return instantiateBean(beanName, mbd);}</code></pre><p>挑个简单的<strong>无参构造函数</strong>构造实例来看看：</p><pre><code class="language-java">protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) {   try {      Object beanInstance;      final BeanFactory parent = this;      if (System.getSecurityManager() != null) {         beanInstance = AccessController.doPrivileged(new PrivilegedAction&lt;Object&gt;() {            @Override            public Object run() {                              return getInstantiationStrategy().instantiate(mbd, beanName, parent);            }         }, getAccessControlContext());      }      else {         // 实例化         beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent);      }      // 包装一下，返回      BeanWrapper bw = new BeanWrapperImpl(beanInstance);      initBeanWrapper(bw);      return bw;   }   catch (Throwable ex) {      throw new BeanCreationException(            mbd.getResourceDescription(), beanName, &quot;Instantiation of bean failed&quot;, ex);   }}</code></pre><p>我们可以看到，关键的地方在于：</p><pre><code class="language-java">beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent);</code></pre><p>这里会进行实际的实例化过程，我们进去看看:</p><p>// SimpleInstantiationStrategy 59</p><pre><code class="language-java">@Overridepublic Object instantiate(RootBeanDefinition bd, String beanName, BeanFactory owner) {   // 如果不存在方法覆写，那就使用 java 反射进行实例化，否则使用 CGLIB,   // 方法覆写 请参见附录&quot;方法注入&quot;中对 lookup-method 和 replaced-method 的介绍   if (bd.getMethodOverrides().isEmpty()) {      Constructor&lt;?&gt; constructorToUse;      synchronized (bd.constructorArgumentLock) {         constructorToUse = (Constructor&lt;?&gt;) bd.resolvedConstructorOrFactoryMethod;         if (constructorToUse == null) {            final Class&lt;?&gt; clazz = bd.getBeanClass();            if (clazz.isInterface()) {               throw new BeanInstantiationException(clazz, &quot;Specified class is an interface&quot;);            }            try {               if (System.getSecurityManager() != null) {                  constructorToUse = AccessController.doPrivileged(new PrivilegedExceptionAction&lt;Constructor&lt;?&gt;&gt;() {                     @Override                     public Constructor&lt;?&gt; run() throws Exception {                        return clazz.getDeclaredConstructor((Class[]) null);                     }                  });               }               else {                  constructorToUse = clazz.getDeclaredConstructor((Class[]) null);               }               bd.resolvedConstructorOrFactoryMethod = constructorToUse;            }            catch (Throwable ex) {               throw new BeanInstantiationException(clazz, &quot;No default constructor found&quot;, ex);            }         }      }      // 利用构造方法进行实例化      return BeanUtils.instantiateClass(constructorToUse);   }   else {      // 存在方法覆写，利用 CGLIB 来完成实例化，需要依赖于 CGLIB 生成子类，这里就不展开了。      // tips: 因为如果不使用 CGLIB 的话，存在 override 的情况 JDK 并没有提供相应的实例化支持      return instantiateWithMethodInjection(bd, beanName, owner);   }}</code></pre><p>到这里，我们就算实例化完成了。我们开始说怎么进行属性注入。</p><h5 id="bean-属性注入">bean 属性注入</h5><p>看完了 createBeanInstance(...) 方法，我们来看看 populateBean(...) 方法，该方法负责进行属性设值，处理依赖。</p><p>// AbstractAutowireCapableBeanFactory 1203</p><pre><code class="language-java">protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {   // bean 实例的所有属性都在这里了   PropertyValues pvs = mbd.getPropertyValues();   if (bw == null) {      if (!pvs.isEmpty()) {         throw new BeanCreationException(               mbd.getResourceDescription(), beanName, &quot;Cannot apply property values to null instance&quot;);      }      else {         // Skip property population phase for null instance.         return;      }   }   // 到这步的时候，bean 实例化完成（通过工厂方法或构造方法），但是还没开始属性设值，   // InstantiationAwareBeanPostProcessor 的实现类可以在这里对 bean 进行状态修改，   // 我也没找到有实际的使用，所以我们暂且忽略这块吧   boolean continueWithPropertyPopulation = true;   if (!mbd.isSynthetic() &amp;&amp; hasInstantiationAwareBeanPostProcessors()) {      for (BeanPostProcessor bp : getBeanPostProcessors()) {         if (bp instanceof InstantiationAwareBeanPostProcessor) {            InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;            // 如果返回 false，代表不需要进行后续的属性设值，也不需要再经过其他的 BeanPostProcessor 的处理            if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {               continueWithPropertyPopulation = false;               break;            }         }      }   }   if (!continueWithPropertyPopulation) {      return;   }   if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME ||         mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {      MutablePropertyValues newPvs = new MutablePropertyValues(pvs);      // 通过名字找到所有属性值，如果是 bean 依赖，先初始化依赖的 bean。记录依赖关系      if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) {         autowireByName(beanName, mbd, bw, newPvs);      }      // 通过类型装配。复杂一些      if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {         autowireByType(beanName, mbd, bw, newPvs);      }      pvs = newPvs;   }   boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();   boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE);   if (hasInstAwareBpps || needsDepCheck) {      PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);      if (hasInstAwareBpps) {         for (BeanPostProcessor bp : getBeanPostProcessors()) {            if (bp instanceof InstantiationAwareBeanPostProcessor) {               InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;               // 这里有个非常有用的 BeanPostProcessor 进到这里: AutowiredAnnotationBeanPostProcessor               // 对采用 @Autowired、@Value 注解的依赖进行设值，这里的内容也是非常丰富的，不过本文不会展开说了，感兴趣的读者请自行研究               pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);               if (pvs == null) {                  return;               }            }         }      }      if (needsDepCheck) {         checkDependencies(beanName, mbd, filteredPds, pvs);      }   }   // 设置 bean 实例的属性值   applyPropertyValues(beanName, mbd, bw, pvs);}</code></pre><h5 id="initializebean">initializeBean</h5><p>属性注入完成后，这一步其实就是处理各种回调了，这块代码比较简单。</p><pre><code class="language-java">protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {   if (System.getSecurityManager() != null) {      AccessController.doPrivileged(new PrivilegedAction&lt;Object&gt;() {         @Override         public Object run() {            invokeAwareMethods(beanName, bean);            return null;         }      }, getAccessControlContext());   }   else {      // 如果 bean 实现了 BeanNameAware、BeanClassLoaderAware 或 BeanFactoryAware 接口，回调      invokeAwareMethods(beanName, bean);   }   Object wrappedBean = bean;   if (mbd == null || !mbd.isSynthetic()) {      // BeanPostProcessor 的 postProcessBeforeInitialization 回调      wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);   }   try {      // 处理 bean 中定义的 init-method，      // 或者如果 bean 实现了 InitializingBean 接口，调用 afterPropertiesSet() 方法      invokeInitMethods(beanName, wrappedBean, mbd);   }   catch (Throwable ex) {      throw new BeanCreationException(            (mbd != null ? mbd.getResourceDescription() : null),            beanName, &quot;Invocation of init method failed&quot;, ex);   }   if (mbd == null || !mbd.isSynthetic()) {      // BeanPostProcessor 的 postProcessAfterInitialization 回调      wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);   }   return wrappedBean;}</code></pre><p>大家发现没有，BeanPostProcessor 的两个回调都发生在这边，只不过中间处理了 init-method，是不是和读者原来的认知有点不一样了？</p><h2 id="附录">附录</h2><h3 id="id-和-name">id 和 name</h3><p>每个 Bean 在 Spring 容器中都有一个唯一的名字（beanName）和 0 个或多个别名（aliases）。</p><p>我们从 Spring 容器中获取 Bean 的时候，可以根据 beanName，也可以通过别名。</p><pre><code class="language-java">beanFactory.getBean(&quot;beanName or alias&quot;);</code></pre><p>在配置 <code>&lt;bean /&gt;</code> 的过程中，我们可以配置 id 和 name，看几个例子就知道是怎么回事了。</p><pre><code class="language-xml">&lt;bean id=&quot;messageService&quot; name=&quot;m1, m2, m3&quot; class=&quot;com.javadoop.example.MessageServiceImpl&quot;&gt;</code></pre><p>以上配置的结果就是：beanName 为 messageService，别名有 3 个，分别为 m1、m2、m3。</p><pre><code class="language-xml">&lt;bean name=&quot;m1, m2, m3&quot; class=&quot;com.javadoop.example.MessageServiceImpl&quot; /&gt;</code></pre><p>以上配置的结果就是：beanName 为 m1，别名有 2 个，分别为 m2、m3。</p><pre><code class="language-xml">&lt;bean class=&quot;com.javadoop.example.MessageServiceImpl&quot;&gt;</code></pre><p>beanName 为：com.javadoop.example.MessageServiceImpl#0，</p><p>别名 1 个，为： com.javadoop.example.MessageServiceImpl</p><pre><code class="language-xml">&lt;bean id=&quot;messageService&quot; class=&quot;com.javadoop.example.MessageServiceImpl&quot;&gt;</code></pre><p>以上配置的结果就是：beanName 为 messageService，没有别名。</p><h3 id="配置是否允许-bean-覆盖是否允许循环依赖">配置是否允许 Bean 覆盖、是否允许循环依赖</h3><p>我们说过，默认情况下，allowBeanDefinitionOverriding 属性为 null。如果在同一配置文件中 Bean id 或 name 重复了，会抛错，但是如果不是同一配置文件中，会发生覆盖。</p><p>可是有些时候我们希望在系统启动的过程中就严格杜绝发生 Bean 覆盖，因为万一出现这种情况，会增加我们排查问题的成本。</p><p>循环依赖说的是 A 依赖 B，而 B 又依赖 A。或者是 A 依赖 B，B 依赖 C，而 C 却依赖 A。默认 allowCircularReferences 也是 null。</p><p>它们两个属性是一起出现的，必然可以在同一个地方一起进行配置。</p><p>添加这两个属性的作者 Juergen Hoeller 在这个 <a href="https://jira.spring.io/browse/SPR-4374">jira</a> 的讨论中说明了怎么配置这两个属性。</p><pre><code class="language-java">public class NoBeanOverridingContextLoader extends ContextLoader {   @Override  protected void customizeContext(ServletContext servletContext, ConfigurableWebApplicationContext applicationContext) {    super.customizeContext(servletContext, applicationContext);    AbstractRefreshableApplicationContext arac = (AbstractRefreshableApplicationContext) applicationContext;    arac.setAllowBeanDefinitionOverriding(false);  }}</code></pre><pre><code class="language-java">public class MyContextLoaderListener extends org.springframework.web.context.ContextLoaderListener {   @Override  protected ContextLoader createContextLoader() {    return new NoBeanOverridingContextLoader();  }  }</code></pre><pre><code class="language-xml">&lt;listener&gt;    &lt;listener-class&gt;com.javadoop.MyContextLoaderListener&lt;/listener-class&gt;  &lt;/listener&gt;</code></pre><p>如果以上方式不能满足你的需求，请参考这个链接：<a href="http://blog.csdn.net/zgmzyr/article/details/39380477">解决spring中不同配置文件中存在name或者id相同的bean可能引起的问题</a></p><h3 id="profile">profile</h3><p>我们可以把不同环境的配置分别配置到单独的文件中，举个例子：</p><pre><code class="language-xml">&lt;beans profile=&quot;development&quot;    xmlns=&quot;http://www.springframework.org/schema/beans&quot;    xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;    xmlns:jdbc=&quot;http://www.springframework.org/schema/jdbc&quot;    xsi:schemaLocation=&quot;...&quot;&gt;    &lt;jdbc:embedded-database id=&quot;dataSource&quot;&gt;        &lt;jdbc:script location=&quot;classpath:com/bank/config/sql/schema.sql&quot;/&gt;        &lt;jdbc:script location=&quot;classpath:com/bank/config/sql/test-data.sql&quot;/&gt;    &lt;/jdbc:embedded-database&gt;&lt;/beans&gt;</code></pre><pre><code class="language-xml">&lt;beans profile=&quot;production&quot;    xmlns=&quot;http://www.springframework.org/schema/beans&quot;    xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;    xmlns:jee=&quot;http://www.springframework.org/schema/jee&quot;    xsi:schemaLocation=&quot;...&quot;&gt;    &lt;jee:jndi-lookup id=&quot;dataSource&quot; jndi-name=&quot;java:comp/env/jdbc/datasource&quot;/&gt;&lt;/beans&gt;</code></pre><p>应该不必做过多解释了吧，看每个文件第一行的 profile=&quot;&quot;。</p><p>当然，我们也可以在一个配置文件中使用：</p><pre><code class="language-xml">&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;    xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;    xmlns:jdbc=&quot;http://www.springframework.org/schema/jdbc&quot;    xmlns:jee=&quot;http://www.springframework.org/schema/jee&quot;    xsi:schemaLocation=&quot;...&quot;&gt;    &lt;beans profile=&quot;development&quot;&gt;        &lt;jdbc:embedded-database id=&quot;dataSource&quot;&gt;            &lt;jdbc:script location=&quot;classpath:com/bank/config/sql/schema.sql&quot;/&gt;            &lt;jdbc:script location=&quot;classpath:com/bank/config/sql/test-data.sql&quot;/&gt;        &lt;/jdbc:embedded-database&gt;    &lt;/beans&gt;    &lt;beans profile=&quot;production&quot;&gt;        &lt;jee:jndi-lookup id=&quot;dataSource&quot; jndi-name=&quot;java:comp/env/jdbc/datasource&quot;/&gt;    &lt;/beans&gt;&lt;/beans&gt;</code></pre><p>理解起来也很简单吧。</p><p>接下来的问题是，怎么使用特定的 profile 呢？Spring 在启动的过程中，会去寻找 “spring.profiles.active” 的属性值，根据这个属性值来的。那怎么配置这个值呢？</p><p>Spring 会在这几个地方寻找 spring.profiles.active 的属性值：操作系统环境变量、JVM 系统变量、web.xml 中定义的参数、JNDI。</p><p>最简单的方式莫过于在程序启动的时候指定：</p><pre><code class="language-shell">-Dspring.profiles.active=&quot;profile1,profile2&quot;</code></pre><blockquote><p>profile 可以激活多个</p></blockquote><p>当然，我们也可以通过代码的形式从 Environment 中设置 profile：</p><pre><code class="language-java">AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();ctx.getEnvironment().setActiveProfiles(&quot;development&quot;);ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);ctx.refresh(); // 重启</code></pre><p><span profile="">如果是 Spring Boot 的话更简单，我们一般会创建 application.properties、application-dev.properties、application-prod.properties 等文件，其中 application.properties 配置各个环境通用的配置，application-</span>.properties 中配置特定环境的配置，然后在启动的时候指定 profile：</p><pre><code class="language-shell">java -Dspring.profiles.active=prod -jar JavaDoop.jar</code></pre><p>如果是单元测试中使用的话，在测试类中使用 @ActiveProfiles 指定，这里就不展开了。</p><h3 id="工厂模式生成-bean">工厂模式生成 Bean</h3><p>请读者注意 factory-bean 和 FactoryBean 的区别。这节说的是前者，是说静态工厂或实例工厂，而后者是 Spring 中的特殊接口，代表一类特殊的 Bean，附录的下面一节会介绍 FactoryBean。</p><p>设计模式里，工厂方法模式分静态工厂和实例工厂，我们分别看看 Spring 中怎么配置这两个，来个代码示例就什么都清楚了。</p><p>静态工厂：</p><pre><code class="language-xml">&lt;bean id=&quot;clientService&quot;    class=&quot;examples.ClientService&quot;    factory-method=&quot;createInstance&quot;/&gt;</code></pre><pre><code class="language-java">public class ClientService {    private static ClientService clientService = new ClientService();    private ClientService() {}    // 静态方法    public static ClientService createInstance() {        return clientService;    }}</code></pre><p>实例工厂：</p><pre><code class="language-xml">&lt;bean id=&quot;serviceLocator&quot; class=&quot;examples.DefaultServiceLocator&quot;&gt;    &lt;!-- inject any dependencies required by this locator bean --&gt;&lt;/bean&gt;&lt;bean id=&quot;clientService&quot;    factory-bean=&quot;serviceLocator&quot;    factory-method=&quot;createClientServiceInstance&quot;/&gt;&lt;bean id=&quot;accountService&quot;    factory-bean=&quot;serviceLocator&quot;    factory-method=&quot;createAccountServiceInstance&quot;/&gt;</code></pre><pre><code class="language-java">public class DefaultServiceLocator {    private static ClientService clientService = new ClientServiceImpl();    private static AccountService accountService = new AccountServiceImpl();    public ClientService createClientServiceInstance() {        return clientService;    }    public AccountService createAccountServiceInstance() {        return accountService;    }}</code></pre><h3 id="factorybean">FactoryBean</h3><p>FactoryBean 适用于 Bean 的创建过程比较复杂的场景，比如数据库连接池的创建。</p><pre><code class="language-java">public interface FactoryBean&lt;T&gt; {    T getObject() throws Exception;    Class&lt;T&gt; getObjectType();    boolean isSingleton();}</code></pre><pre><code class="language-java">public class Person {     private Car car ;    private void setCar(Car car){ this.car = car;  }  }</code></pre><p>我们假设现在需要创建一个 Person 的 Bean，首先我们需要一个 Car 的实例，我们这里假设 Car 的实例创建很麻烦，那么我们可以把创建 Car 的复杂过程包装起来：</p><pre><code class="language-java">public class MyCarFactoryBean implements FactoryBean&lt;Car&gt;{    private String make;     private int year ;        public void setMake(String m){ this.make =m ; }        public void setYear(int y){ this.year = y; }        public Car getObject(){       // 这里我们假设 Car 的实例化过程非常复杂，反正就不是几行代码可以写完的那种      CarBuilder cb = CarBuilder.car();            if(year!=0) cb.setYear(this.year);      if(StringUtils.hasText(this.make)) cb.setMake( this.make );       return cb.factory();     }        public Class&lt;Car&gt; getObjectType() { return Car.class ; }         public boolean isSingleton() { return false; }}</code></pre><p>我们看看装配的时候是怎么配置的：</p><pre><code class="language-xml">&lt;bean class = &quot;com.javadoop.MyCarFactoryBean&quot; id = &quot;car&quot;&gt;  &lt;property name = &quot;make&quot; value =&quot;Honda&quot;/&gt;  &lt;property name = &quot;year&quot; value =&quot;1984&quot;/&gt;&lt;/bean&gt;&lt;bean class = &quot;com.javadoop.Person&quot; id = &quot;josh&quot;&gt;  &lt;property name = &quot;car&quot; ref = &quot;car&quot;/&gt;&lt;/bean&gt;</code></pre><p>看到不一样了吗？id 为 “car” 的 bean 其实指定的是一个 FactoryBean，不过配置的时候，我们直接让配置 Person 的 Bean 直接依赖于这个 FactoryBean 就可以了。中间的过程 Spring 已经封装好了。</p><p>说到这里，我们再来点干货。我们知道，现在还用 xml 配置 Bean 依赖的越来越少了，更多时候，我们可能会采用 java  config 的方式来配置，这里有什么不一样呢？</p><pre><code class="language-java">@Configuration public class CarConfiguration {     @Bean     public MyCarFactoryBean carFactoryBean(){       MyCarFactoryBean cfb = new MyCarFactoryBean();      cfb.setMake(&quot;Honda&quot;);      cfb.setYear(1984);      return cfb;    }    @Bean    public Person aPerson(){     Person person = new Person();      // 注意这里的不同    person.setCar(carFactoryBean().getObject());    return person;     } }</code></pre><p>这个时候，其实我们的思路也很简单，把 MyCarFactoryBean 看成是一个简单的 Bean 就可以了，不必理会什么 FactoryBean，它是不是 FactoryBean 和我们没关系。</p><h3 id="初始化-bean-的回调">初始化 Bean 的回调</h3><p>有以下四种方案：</p><pre><code class="language-xml">&lt;bean id=&quot;exampleInitBean&quot; class=&quot;examples.ExampleBean&quot; init-method=&quot;init&quot;/&gt;</code></pre><pre><code class="language-java">public class AnotherExampleBean implements InitializingBean {    public void afterPropertiesSet() {        // do some initialization work    }}</code></pre><pre><code class="language-java">@Bean(initMethod = &quot;init&quot;)public Foo foo() {    return new Foo();}</code></pre><pre><code class="language-java">@PostConstructpublic void init() {    }</code></pre><h3 id="销毁-bean-的回调">销毁 Bean 的回调</h3><pre><code class="language-xml">&lt;bean id=&quot;exampleInitBean&quot; class=&quot;examples.ExampleBean&quot; destroy-method=&quot;cleanup&quot;/&gt;</code></pre><pre><code class="language-java">public class AnotherExampleBean implements DisposableBean {    public void destroy() {        // do some destruction work (like releasing pooled connections)    }}</code></pre><pre><code class="language-java">@Bean(destroyMethod = &quot;cleanup&quot;)public Bar bar() {    return new Bar();}</code></pre><pre><code class="language-java">@PreDestroypublic void cleanup() {    }</code></pre><h3 id="conversionservice">ConversionService</h3><p>既然文中说到了这个，顺便提一下好了。</p><p>最有用的场景就是，它用来将前端传过来的参数和后端的 controller 方法上的参数进行绑定的时候用。</p><p>像前端传过来的字符串、整数要转换为后端的 String、Integer 很容易，但是如果 controller 方法需要的是一个枚举值，或者是 Date 这些非基础类型（含基础类型包装类）值的时候，我们就可以考虑采用 ConversionService 来进行转换。</p><pre><code class="language-xml">&lt;bean id=&quot;conversionService&quot;  class=&quot;org.springframework.context.support.ConversionServiceFactoryBean&quot;&gt;  &lt;property name=&quot;converters&quot;&gt;    &lt;list&gt;      &lt;bean class=&quot;com.javadoop.learning.utils.StringToEnumConverterFactory&quot;/&gt;    &lt;/list&gt;  &lt;/property&gt;&lt;/bean&gt;</code></pre><p>ConversionService 接口很简单，所以要自定义一个 convert 的话也很简单。</p><p>下面再说一个实现这种转换很简单的方式，那就是实现 Converter 接口。</p><p>来看一个很简单的例子，这样比什么都管用。</p><pre><code class="language-java">public class StringToDateConverter implements Converter&lt;String, Date&gt; {    @Override    public Date convert(String source) {        try {            return DateUtils.parseDate(source, &quot;yyyy-MM-dd&quot;, &quot;yyyy-MM-dd HH:mm:ss&quot;, &quot;yyyy-MM-dd HH:mm&quot;, &quot;HH:mm:ss&quot;, &quot;HH:mm&quot;);        } catch (ParseException e) {            return null;        }    }}</code></pre><p>只要注册这个 Bean 就可以了。这样，前端往后端传的时间描述字符串就很容易绑定成 Date 类型了，不需要其他任何操作。</p><h3 id="bean-继承">Bean 继承</h3><p>在初始化 Bean 的地方，我们说过了这个：</p><pre><code class="language-java">RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);</code></pre><p>这里涉及到的就是 <code>&lt;bean parent=&quot;&quot; /&gt;</code> 中的 parent 属性，我们来看看 Spring 中是用这个来干什么的。</p><p>首先，我们要明白，这里的继承和 java 语法中的继承没有任何关系，不过思路是相通的。child bean 会继承 parent bean 的所有配置，也可以覆盖一些配置，当然也可以新增额外的配置。</p><p>Spring 中提供了继承自 AbstractBeanDefinition 的 <code>ChildBeanDefinition</code> 来表示 child bean。</p><p>看如下一个例子:</p><pre><code class="language-java">&lt;bean id=&quot;inheritedTestBean&quot; abstract=&quot;true&quot; class=&quot;org.springframework.beans.TestBean&quot;&gt;    &lt;property name=&quot;name&quot; value=&quot;parent&quot;/&gt;    &lt;property name=&quot;age&quot; value=&quot;1&quot;/&gt;&lt;/bean&gt;&lt;bean id=&quot;inheritsWithDifferentClass&quot; class=&quot;org.springframework.beans.DerivedTestBean&quot;        parent=&quot;inheritedTestBean&quot; init-method=&quot;initialize&quot;&gt;            &lt;property name=&quot;name&quot; value=&quot;override&quot;/&gt;&lt;/bean&gt;</code></pre><p>parent bean 设置了 <code>abstract=&quot;true&quot;</code> 所以它不会被实例化，child bean 继承了 parent bean 的两个属性，但是对 name 属性进行了覆写。</p><p>child bean 会继承 scope、构造器参数值、属性值、init-method、destroy-method 等等。</p><p>当然，我不是说 parent bean 中的 abstract = true 在这里是必须的，只是说如果加上了以后 Spring 在实例化 singleton beans 的时候会忽略这个 bean。</p><p>比如下面这个极端 parent bean，它没有指定 class，所以毫无疑问，这个 bean 的作用就是用来充当模板用的 parent bean，此处就必须加上 abstract = true。</p><pre><code class="language-java">&lt;bean id=&quot;inheritedTestBeanWithoutClass&quot; abstract=&quot;true&quot;&gt;    &lt;property name=&quot;name&quot; value=&quot;parent&quot;/&gt;    &lt;property name=&quot;age&quot; value=&quot;1&quot;/&gt;&lt;/bean&gt;</code></pre><h3 id="方法注入">方法注入</h3><p>一般来说，我们的应用中大多数的 Bean 都是 singleton 的。singleton 依赖 singleton，或者 prototype 依赖 prototype 都很好解决，直接设置属性依赖就可以了。</p><p>但是，如果是 singleton 依赖 prototype 呢？这个时候不能用属性依赖，因为如果用属性依赖的话，我们每次其实拿到的还是第一次初始化时候的 bean。</p><p>一种解决方案就是不要用属性依赖，每次获取依赖的 bean 的时候从 BeanFactory 中取。这个也是大家最常用的方式了吧。怎么取，我就不介绍了，大部分 Spring 项目大家都会定义那么个工具类的。</p><p>另一种解决方案就是这里要介绍的通过使用 Lookup method。</p><h4 id="lookup-method">lookup-method</h4><p>我们来看一下 Spring Reference 中提供的一个例子：</p><pre><code class="language-java">package fiona.apple;// no more Spring imports!public abstract class CommandManager {    public Object process(Object commandState) {        // grab a new instance of the appropriate Command interface        Command command = createCommand();        // set the state on the (hopefully brand new) Command instance        command.setState(commandState);        return command.execute();    }    // okay... but where is the implementation of this method?    protected abstract Command createCommand();}</code></pre><p>xml 配置 <code>&lt;lookup-method /&gt;</code>：</p><pre><code class="language-xml">&lt;!-- a stateful bean deployed as a prototype (non-singleton) --&gt;&lt;bean id=&quot;myCommand&quot; class=&quot;fiona.apple.AsyncCommand&quot; scope=&quot;prototype&quot;&gt;    &lt;!-- inject dependencies here as required --&gt;&lt;/bean&gt;&lt;!-- commandProcessor uses statefulCommandHelper --&gt;&lt;bean id=&quot;commandManager&quot; class=&quot;fiona.apple.CommandManager&quot;&gt;    &lt;lookup-method name=&quot;createCommand&quot; bean=&quot;myCommand&quot;/&gt;&lt;/bean&gt;</code></pre><p>Spring 采用 <strong>CGLIB 生成字节码</strong>的方式来生成一个子类。我们定义的类不能定义为 final class，抽象方法上也不能加 final。</p><p>lookup-method 上的配置也可以采用注解来完成，这样就可以不用配置 <code>&lt;lookup-method /&gt;</code> 了，其他不变：</p><pre><code class="language-java">public abstract class CommandManager {    public Object process(Object commandState) {        MyCommand command = createCommand();        command.setState(commandState);        return command.execute();    }    @Lookup(&quot;myCommand&quot;)    protected abstract Command createCommand();}</code></pre><blockquote><p>注意，既然用了注解，要配置注解扫描：<code>&lt;context:component-scan base-package=&quot;com.javadoop&quot; /&gt;</code></p></blockquote><p>甚至，我们可以像下面这样：</p><pre><code class="language-java">public abstract class CommandManager {    public Object process(Object commandState) {        MyCommand command = createCommand();        command.setState(commandState);        return command.execute();    }    @Lookup    protected abstract MyCommand createCommand();}</code></pre><blockquote><p>上面的返回值用了 MyCommand，当然，如果 Command 只有一个实现类，那返回值也可以写 Command。</p></blockquote><h4 id="replaced-method">replaced-method</h4><p>记住它的功能，就是替换掉 bean 中的一些方法。</p><pre><code class="language-java">public class MyValueCalculator {    public String computeValue(String input) {        // some real code...    }    // some other methods...}</code></pre><p>方法覆写，注意要实现 MethodReplacer 接口：</p><pre><code class="language-java">public class ReplacementComputeValue implements org.springframework.beans.factory.support.MethodReplacer {    public Object reimplement(Object o, Method m, Object[] args) throws Throwable {        // get the input value, work with it, and return a computed result        String input = (String) args[0];        ...        return ...;    }}</code></pre><p>配置也很简单：</p><pre><code class="language-xml">&lt;bean id=&quot;myValueCalculator&quot; class=&quot;x.y.z.MyValueCalculator&quot;&gt;    &lt;!-- 定义 computeValue 这个方法要被替换掉 --&gt;    &lt;replaced-method name=&quot;computeValue&quot; replacer=&quot;replacementComputeValue&quot;&gt;        &lt;arg-type&gt;String&lt;/arg-type&gt;    &lt;/replaced-method&gt;&lt;/bean&gt;&lt;bean id=&quot;replacementComputeValue&quot; class=&quot;a.b.c.ReplacementComputeValue&quot;/&gt;</code></pre><blockquote><p>arg-type 明显不是必须的，除非存在方法重载，这样必须通过参数类型列表来判断这里要覆盖哪个方法。</p></blockquote><h3 id="beanpostprocessor">BeanPostProcessor</h3><p>应该说 BeanPostProcessor 概念在 Spring 中也是比较重要的。我们看下接口定义：</p><pre><code class="language-java">public interface BeanPostProcessor {   Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;   Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;}</code></pre><p>看这个接口中的两个方法名字我们大体上可以猜测 bean 在初始化之前会执行 postProcessBeforeInitialization 这个方法，初始化完成之后会执行 postProcessAfterInitialization 这个方法。但是，这么理解是非常片面的。</p><p>首先，我们要明白，除了我们自己定义的 BeanPostProcessor 实现外，Spring 容器在启动时自动给我们也加了几个。如在获取 BeanFactory 的 obtainFactory() 方法结束后的 prepareBeanFactory(factory)，大家仔细看会发现，Spring 往容器中添加了这两个 BeanPostProcessor：ApplicationContextAwareProcessor、ApplicationListenerDetector。</p><p>我们回到这个接口本身，读者请看第一个方法，这个方法接受的第一个参数是 bean 实例，第二个参数是 bean 的名字，重点在返回值将会作为新的 bean 实例，所以，没事的话这里不能随便返回个 null。</p><p>那意味着什么呢？我们很容易想到的就是，我们这里可以对一些我们想要修饰的 bean 实例做一些事情。但是对于 Spring 框架来说，它会决定是不是要在这个方法中返回 bean 实例的代理，这样就有更大的想象空间了。</p><p>最后，我们说说如果我们自己定义一个 bean 实现 BeanPostProcessor 的话，它的执行时机是什么时候？</p><p>如果仔细看了代码分析的话，其实很容易知道了，在 bean 实例化完成、属性注入完成之后，会执行回调方法，具体请参见类 AbstractAutowireCapableBeanFactory#initBean 方法。</p><p>首先会回调几个实现了 Aware 接口的 bean，然后就开始回调 BeanPostProcessor 的 postProcessBeforeInitialization 方法，之后是回调 init-method，然后再回调 BeanPostProcessor 的 postProcessAfterInitialization 方法。</p><h2 id="总结">总结</h2><p>按理说，总结应该写在附录前面，我就不讲究了。</p><p>在花了那么多时间后，这篇文章终于算是基本写完了，大家在惊叹 Spring 给我们做了那么多的事的时候，应该透过现象看本质，去理解 Spring 写得好的地方，去理解它的设计思想。</p><p>本文的缺陷在于对 Spring 预初始化 singleton beans 的过程分析不够，主要是代码量真的比较大，分支旁路众多。同时，虽然附录条目不少，但是庞大的 Spring 真的引出了很多的概念，希望日后有精力可以慢慢补充一些。</p><p>（全文完）</p><p>From: <a href="https://www.javadoop.com/post/spring-ioc">https://www.javadoop.com/post/spring-ioc</a></p>]]>
                    </description>
                    <pubDate>Mon, 10 Aug 2020 22:50:21 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[转载-一文搞懂 JVM 架构和运行时数据区]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/jvm-architecture</link>
                    <description>
                            <![CDATA[<p>From: <a href="https://blog.csdn.net/weixin_43207056/article/details/104076177" target="_blank">https://blog.csdn.net/weixin_43207056/article/details/104076177</a></p><h3 id="%E6%96%87%E7%AB%A0%E7%9B%AE%E5%BD%95" tabindex="-1">文章目录</h3><ul><li>前言</li><li>一、Java 虚拟机架构 (JVM Architecture)<ul><li>1.1 Class 文件 (字节码文件)</li><li>1.2 类加载器子系统 (ClassLoader Subsystem)</li><li>1.3 Java 虚拟机运行时数据区 (JVM Runtime Data Area)</li><li>1.4 执行引擎 (Execution Engine)</li><li>1.5 本地库接口 (JNI，Java Native Interface)</li><li>1.6 本地方法库 (Native Method Library)</li></ul></li><li>二、Java 虚拟机运行时数据区<ul><li>2.1 程序计数器 (Program Counter Register)</li><li>2.2 Java 虚拟机栈 (JVM Stacks)<ul><li>2.2.1 局部变量表</li><li>2.2.2 操作数栈</li><li>2.2.3 动态连接</li><li>2.2.4 方法出口</li></ul></li><li>2.3 本地方法栈 (Native Method Stacks)</li><li>2.4 Java 堆 (Heap)</li><li>2.5 方法区 (Method Area)</li><li>2.6 运行时常量池 (Runtime Constant Pool)</li></ul></li><li>总结</li></ul><h2 id="%E5%89%8D%E8%A8%80" tabindex="-1">前言</h2><p>之前写博客一直比较随性，主题也很随意，就是想到什么写什么，对什么感兴趣就写什么。虽然写起来无拘无束，自在随意，但也带来了一些问题，每次写完一篇后就要去纠结下一篇到底写什么，看来选择太多也不是好事儿，更重要的是不成体系的内容对读者也不够友好。所以以后的博客尽量按系列来写，不过偶尔也会穿插其他的内容。接下来一段时间我会把写博客的重点放在 JVM (Java Virtual Machine) 和 JUC (java util concurrent ) 上，对 Java 虚拟机和 Java 并发编程进行一系列的介绍，欢迎关注。</p><p>了解 JVM 是对 Java 开发人员的基本要求，JVM 的相关内容自然也成了现在 Java 程序员面试的重要考点。不过估计很多小伙伴和我一样，长时间醉心于 CRUD，却忘了去了解一下更底层、更基础的东西，殊不知这些才是决定你能在这条路上走多远的关键因素，那接下来我们就一起来深入学习一下看似神秘的 JVM 吧。JVM 总体来看内容还是很多的，我会把最重要的内容介绍给大家，不过如果你有时间和精力的话，还是推荐你去看一下《深入理解Java虚拟机》这本书，确实是有口皆碑。本系列文章也会引用很多此书的内容并加上我自己的理解，如果你坚持看下去的话，相信会有很大的收获。</p><p>首先对 JVM 做个简单的介绍，JVM 是 JDK 的一部分，《Java 虚拟机规范》(The Java Virtual Machine Specification) 是平行于《Java 语言规范》(The Java Language Specification)的一套独立的规范，不同的公司对其有不同的实现 (类似于一个接口被不同的类实现)，比较著名的 Java 虚拟机实现版本有 HotSpot、JRockit 和 J9 等。</p><p>本文分为两大部分，将分别为大家介绍 JVM 的整体架构和运行时数据区，这两部分的依据均是《Java 虚拟机规范》，而不针对任何特定的 JVM 具体实现版本。</p><h2 id="%E4%B8%80%E3%80%81java-%E8%99%9A%E6%8B%9F%E6%9C%BA%E6%9E%B6%E6%9E%84-(jvm-architecture)" tabindex="-1">一、Java 虚拟机架构 (JVM Architecture)</h2><p>在我看来，不管学习什么样的知识或技术，首先要做的就是从全局上去认识它，这样才能避免盲人摸象，事倍功半的情况发生。既然要学习 JVM，就要先了解它的整体架构，于是我画了个 JVM 架构图来帮助大家认识它。</p><p><img src="https://notes.suremotoo.cc/upload/2025/03/JVM_by_hyn.png" alt="JVM_by_hyn" /></p><blockquote><p>Java 虚拟机架构图</p></blockquote><p>对 JVM 还不太了解的同学第一次看到这张花里胡哨的图肯定会一脸懵逼，不用怕，其实我们只需要重点理解并掌握其中一部分 (同时也是面试重点) 就好了，比如运行时数据区、垃圾收集器、内存分配策略和类加载机制等，类文件结构也可以学习一下，其他的稍作了解即可。既然本篇文章是要带领大家认识 JVM 架构的，那就先把图中各个部分都介绍一下吧 (注：本文只做介绍，让各位先对 JVM 有个整体的认识，本系列后续文章会做深入探讨)。</p><h3 id="1.1-class-%E6%96%87%E4%BB%B6-(%E5%AD%97%E8%8A%82%E7%A0%81%E6%96%87%E4%BB%B6)" tabindex="-1">1.1 Class 文件 (字节码文件)</h3><p>Java 之所以号称“一次编写，处处运行”，就是得益于虚拟机和 Class 文件 (注：CLass 文件、字节码文件和类文件是一个意思) 的组合机制。程序员并不需要自己去适配不同的操作系统，大家都知道我们平时编写的 java 代码在编译成 Class 文件后才能执行，而 Class 文件可以在任何操作系统上的 JVM 上执行，这样就做到了“平台无关性”。下面是一个最简单的 HelloWorld 程序及其对应的 Class 文件。</p><p><img src="https://notes.suremotoo.cc/upload/2026/01/hello_world_class_by_hyn.png" alt="hello_world_class_by_hyn" /></p><blockquote><p>HelloWorld 程序及其编译后的 Class 文件</p></blockquote><p>得益于 Class 文件，JVM 还可以做到“语言无关性”，也就是说不只有 Java 程序可以运行于 JVM 之上，很多其他语言例如最近在安卓开发者中大火的 Kotlin 语言，还有 Scala、Groovy 等语言也都是基于 JVM 平台的，这些语言的代码都可以编译成 Class 文件，然后在 JVM 上运行。</p><p><img src="https://notes.suremotoo.cc/upload/2026/01/jvm_platform_by_hyn.png" alt="JVM提供的平台无关性和语言无关性" /></p><blockquote><p>JVM提供的平台无关性和语言无关性</p></blockquote><h3 id="1.2-%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8%E5%AD%90%E7%B3%BB%E7%BB%9F-(classloader-subsystem)" tabindex="-1">1.2 类加载器子系统 (ClassLoader Subsystem)</h3><p>要执行 Class 文件就需要先将其加载进内存，这一工作正是由类加载器 (ClassLoader) 完成的，系统为我们提供了三种类加载器，分别是启动类加载器 (Bootstrap ClassLoader)、扩展类加载器 (Extension ClassLoader) 和应用程序类加载器 (Application ClassLoader)，如果有必要，我们也可以加入自定义的类加载器。类加载过程如下：</p><p><img src="https://notes.suremotoo.cc/upload/2026/01/classLoader_by_hyn.png" alt="类加载过程" /></p><blockquote><p>类加载过程</p></blockquote><p>类加载过程分为加载、连接和初始化三个阶段，其中的连接阶段又分为验证、准备和解析三个阶段 (详细的类加载机制在后续文章中进行介绍)。</p><h3 id="1.3-java-%E8%99%9A%E6%8B%9F%E6%9C%BA%E8%BF%90%E8%A1%8C%E6%97%B6%E6%95%B0%E6%8D%AE%E5%8C%BA-(jvm-runtime-data-area)" tabindex="-1">1.3 Java 虚拟机运行时数据区 (JVM Runtime Data Area)</h3><p>这部分内容较多，放在本文第二部分单独进行介绍。</p><h3 id="1.4-%E6%89%A7%E8%A1%8C%E5%BC%95%E6%93%8E-(execution-engine)" tabindex="-1">1.4 执行引擎 (Execution Engine)</h3><p>字节码被加载进运行时数据区后，执行引擎会进行读取并执行，执行引擎主要包含以下模块：</p><ul><li>解释器 (Interpreter)：相信大家很久以前就听过“计算机只认识0和1”这句话，时至今日，计算机依然只认识0和1，所以任何编程语言的代码最终都要转化成机器码 (二进制代码)才能执行，Java 也不例外，而解释器的工作正是将编译得到的字节码再转化成机器码，然后才能执行。正因为如此，Java 才被称为解释型语言，也正是因为边解释边执行的特点，Java 程序在执行时才会慢于 C++ 之类的编译型语言。</li><li>即时编译器 (JIT Compiler，just-in-time compiler)：<a href="%5Bhttps://baike.baidu.com/item/%E5%8D%B3%E6%97%B6%E7%BC%96%E8%AF%91%E5%99%A8/18428531%5D(https://baike.baidu.com/item/%E5%8D%B3%E6%97%B6%E7%BC%96%E8%AF%91%E5%99%A8/18428531)" target="_blank">即时编译器百度百科</a>，为了弥补解释执行带来的速度劣势，JVM 引入了即时编译器，它的作用就是把热点代码，比如重复调用的方法和循环代码等，编译成机器码并存放在 code cache 中，这样之后再用到这些代码就不用重新解释执行了，可以提高程序运行效率。</li><li>垃圾收集器 (Garbage Collector)：Java 程序员可以不用手动释放内存，全是垃圾收集器的功劳，这也是 JVM 中尤其重要的内容，后续会有多篇文章对其进行介绍。</li></ul><h3 id="1.5-%E6%9C%AC%E5%9C%B0%E5%BA%93%E6%8E%A5%E5%8F%A3-(jni%EF%BC%8Cjava-native-interface)" tabindex="-1">1.5 本地库接口 (JNI，Java Native Interface)</h3><p>如果你经常看 JDK 源码的话，一定会注意到 native 这个关键词，被它修饰的方法是没有方法体的，是因为它调用了计算机本地的方法库 (通常是 C 或 C++ 代码)。JDK 源码中有很多类的方法，特别是一些需要操作计算机硬件的方法，都调用了本地方法库，毕竟与硬件打交道还是用 C 和 C++ 更方便，比如下面这些方法：</p><pre><code>public static native Thread currentThread();</code></pre><p>​<br />private native void open0(String name) throws FileNotFoundException;</p><h3 id="1.6-%E6%9C%AC%E5%9C%B0%E6%96%B9%E6%B3%95%E5%BA%93-(native-method-library)" tabindex="-1">1.6 本地方法库 (Native Method Library)</h3><p>本地库接口所调用的对象正是位于这个库中，一般是位于计算机本地的 C 或 C++ 语言代码。</p><h2 id="%E4%BA%8C%E3%80%81java-%E8%99%9A%E6%8B%9F%E6%9C%BA%E8%BF%90%E8%A1%8C%E6%97%B6%E6%95%B0%E6%8D%AE%E5%8C%BA" tabindex="-1">二、Java 虚拟机运行时数据区</h2><p>Java 虚拟机运行时数据区是我们需要重点了解并熟悉的部分，因为这与我们写的程序息息相关，平时常见的 StackOverflowError 和 OutOfMemoryError 也几乎都是来自这个区域。说“几乎”是因为当本机直接内存不够用时也会抛出 OutOfMemoryError。如下图所示，程序计数器、Java 虚拟机栈和本地方法栈是线程私有的，堆和方法区是线程共享的，其中方法区又包含了运行时常量池。下面就对这个部分做个详细的介绍吧 (注：本部分引用内容来自《深入理解Java虚拟机》)。</p><p><img src="https://notes.suremotoo.cc/upload/2026/01/jvm_runtime_data_area_by_hyn.png" alt="Java 虚拟机运行时数据区" /></p><blockquote><p>Java 虚拟机运行时数据区</p></blockquote><h3 id="2.1-%E7%A8%8B%E5%BA%8F%E8%AE%A1%E6%95%B0%E5%99%A8-(program-counter-register)" tabindex="-1">2.1 程序计数器 (Program Counter Register)</h3><p>怕有些小伙伴不清楚，提示一下：下面这样的段落格式就是 Markdown 里的引用格式，，一般用于引用他人的文章或别处的内容。</p><blockquote><p>程序计数器(Program Counter Register)是一块较小的内存空间，它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念里，字节码解释器工作时就是通过改变这个计数器 的值来选取下一条需要执行的字节码指令，它是程序控制流的指示器，分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。</p><p>由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的，在任何一个确定的时刻，一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此，为了线程切换后能恢复到正确的执行位置，每条线程都需要有一个独立的程序计数器，各条线程之间计数器互不影响，独立存储，我们称这类内存区域为“线程私有”的内存。</p><p>如果线程正在执行的是一个Java方法，这个计数器记录的是正在执行的虚拟机字节码指令的地址；如果正在执行的是本地 (Native) 方法，这个计数器值则应为空 (Undefined)。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域。</p></blockquote><p>这里引用了《深入理解Java虚拟机》书中的内容，其实不难理解，程序计数器的作用就是保存线程的执行状态，引用部分的第三段中说“<em>如果线程正在执行的是一个Java方法，这个计数器记录的是正在执行的虚拟机字节码指令的地址</em>”，这个地址就是字节码执行到的位置。我们平时说的 Java 多线程上下文切换就需要程序计数器的辅助，当 CPU 从一个线程切换到另一个线程时，要从程序计数器中读取线程执行状态从而恢复现场。后面又说“<em>如果执行的是本地 (Native)方法，这个计数器值为空(Undefined)</em>”，这是为何呢？是因为本地方法执行的是 C / C++ 代码，在原生平台直接运行，也就不存在 Java 虚拟机的概念，自然也无法保存字节码指令地址，此时要想记录代码运行状态的话，只能使用原生 CPU 的 PC 寄存器。</p><h3 id="2.2-java-%E8%99%9A%E6%8B%9F%E6%9C%BA%E6%A0%88-(jvm-stacks)" tabindex="-1">2.2 Java 虚拟机栈 (JVM Stacks)</h3><blockquote><p>与程序计数器一样，Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的，它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候，Java 虚拟机都 会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程，就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。</p><p>局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、 float、long、double)、对象引用 (reference 类型，它并不等同于对象本身，可能是一个指向对象起始地址的引用指针，也可能是指向一个代表对象的句柄或者其他与此对象相关的位置) 和 returnAddress 类型(指向了一条字节码指令的地址)。</p><p>这些数据类型在局部变量表中的存储空间以局部变量槽 (Slot) 来表示，其中64位长度的 long 和 double 类型的数据会占用两个变量槽，其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配，当进入一个方法时，这个方法需要在栈帧中分配多大的局部变量空间是完全确定的，在方法运行期间不会改变局部变量表的大小。请读者注意，这里说的“大小”是指变量槽的数量，虚拟机真正使用多大的内存空间 (譬如按照1个变量槽占用32个比特、64个比特，或者更多)来实现一个变量槽，这是完全由具体的虚拟机实现自行决定的事情。</p><p>在《Java虚拟机规范》中，对这个内存区域规定了两类异常状况：如果线程请求的栈深度大于虚拟机所允许的深度，将抛出 StackOverflowError 异常；如果 Java 虚拟机栈容量可以动态扩展，当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。</p></blockquote><p>Java 虚拟机栈的内部结构如下图所示：</p><p><img src="https://notes.suremotoo.cc/upload/2026/01/java_virtual_stack_by_hyn.png" alt="Java 虚拟机栈" /></p><blockquote><p>Java 虚拟机栈</p></blockquote><h4 id="2.2.1-%E5%B1%80%E9%83%A8%E5%8F%98%E9%87%8F%E8%A1%A8" tabindex="-1">2.2.1 局部变量表</h4><p>局部变量表是存放方法参数和局部变量的区域。 局部变量没有准备阶段， 必须显式初始化。如果是非静态方法，则在 index[0] 位置上存储的是方法所属对象的实例引用，一个引用变量占 4 个字节，随后存储的是参数和局部变量。</p><h4 id="2.2.2-%E6%93%8D%E4%BD%9C%E6%95%B0%E6%A0%88" tabindex="-1">2.2.2 操作数栈</h4><p>操作数栈是个初始状态为空的桶式结构栈。在方法执行过程中， 会有各种指令往栈中写入和提取信息。JVM 的执行引擎是基于栈的执行引擎，其中的栈指的就是操作数栈。字节码指令集的定义都是基于栈类型的，栈的深度在方法元信息的 stack 属性中。下面使用 i++ 和 ++i 的区别来帮助理解操作数栈：</p><p><strong>i++ 和 ++i 的区别：</strong></p><ol><li>i++：从局部变量表取出 i 并压入操作栈，然后对局部变量表中的 i 自增 1，将操作栈栈顶值取出使用，最后，使用栈顶值更新局部变量表，如此线程从操作栈读到的是自增之前的值。</li><li>++i：先对局部变量表的 i 自增 1，然后取出并压入操作栈，再将操作栈栈顶值取出使用，最后，使用栈顶值更新局部变量表，线程从操作栈读到的是自增之后的值。</li></ol><p>之所以说 i++ 不是原子操作，即使使用 volatile 修饰也不是线程安全，就是因为，可能 i 被从局部变量表（内存）取出，压入操作栈（寄存器），操作栈中自增，使用栈顶值更新局部变量表（寄存器更新写入内存），其中分为 3 步，volatile 保证可见性，保证每次从局部变量表读取的都是最新的值，但可能这 3 步可能被另一个线程的 3 步打断，产生数据互相覆盖问题，从而导致 i 的值比预期的小。</p><h4 id="2.2.3-%E5%8A%A8%E6%80%81%E8%BF%9E%E6%8E%A5" tabindex="-1">2.2.3 动态连接</h4><p>每个栈帧中包含一个在常量池中对当前方法的引用， 目的是支持方法调用过程的动态连接。</p><h4 id="2.2.4-%E6%96%B9%E6%B3%95%E5%87%BA%E5%8F%A3" tabindex="-1">2.2.4 方法出口</h4><p>方法执行时有两种退出情况：</p><ol><li>正常退出，即正常执行到任何方法的返回字节码指令，如 RETURN、IRETURN、ARETURN 等；</li><li>异常退出。</li></ol><p>无论何种退出情况，都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧，退出可能有三种方式：</p><ol><li>返回值压入上层调用栈帧。</li><li>异常信息抛给能够处理的栈帧。</li><li>程序计数器指向方法调用后的下一条指令。</li></ol><h3 id="2.3-%E6%9C%AC%E5%9C%B0%E6%96%B9%E6%B3%95%E6%A0%88-(native-method-stacks)" tabindex="-1">2.3 本地方法栈 (Native Method Stacks)</h3><blockquote><p>本地方法栈与虚拟机栈所发挥的作用是非常相似的，其区别只是虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务，而本地方法栈则是为虚拟机使用到的本地 (Native) 方法服务。</p><p>《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定，因此具体的虚拟机可以根据需要自由实现它，甚至有的Java虚拟机 (譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样，本地方法栈也会在栈深度溢出或者栈扩展失 败时分别抛出 StackOverflowError 和OutOfMemoryError 异常。</p></blockquote><p>这部分比较好理解，就不做解析了。</p><h3 id="2.4-java-%E5%A0%86-(heap)" tabindex="-1">2.4 Java 堆 (Heap)</h3><blockquote><p>对于Java应用程序来说，Java 堆 (Java Heap)是虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域，在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例，Java 世界里“几乎”所有的对象实例都在这里分配内存。Java 堆是垃圾收集器管理的内存区域，因此也常被称为“GC 堆”。</p><p>根据《Java虚拟机规范》的规定，Java堆可以处于物理上不连续的内存空间中，但在逻辑上它应该被视为连续的，这点就像我们用磁盘空间去存储文件一样，并不要求每个文件都连续存放。但对于大 对象(典型的如数组对象)，多数虚拟机实现出于实现简单、存储高效的考虑，很可能会要求连续的内存空间。</p><p>Java 堆既可以被实现成固定大小的，也可以是可扩展的，不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在 Java 堆中没有内存完成实例分配，并且堆也无法再扩展时，Java 虚拟机将会抛出 OutOfMemoryError 异常。</p></blockquote><p>Java 堆的唯一作用就是存放对象实例，这也是垃圾收集器最关注的内存区域，因为大多数对象实例的存活时间都很短，比如在方法内部创建的实例在方法执行完之后就没有存在价值了，所以这个区域的垃圾回收性价比最高。关于垃圾回收的详细内容，见后续文章。</p><h3 id="2.5-%E6%96%B9%E6%B3%95%E5%8C%BA-(method-area)" tabindex="-1">2.5 方法区 (Method Area)</h3><blockquote><p>方法区 (Method Area)与 Java 堆一样，是各个线程共享的内存区域，它用于存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分，但是它却有一个别名叫作“非堆”(Non-Heap)，目的是与 Java 堆区分开来。</p><p>说到方法区，不得不提一下“永久代”这个概念，尤其是在JDK 8以前，许多 Java 程序员都习惯在 HotSpot 虚拟机上开发、部署程序，很多人都更愿意把方法区称呼为“永久代”(Permanent Generation)，或将两者混为一谈。本质上这两者并不是等价的，因为仅仅是当时的 HotSpot 虚拟机设计团队选择把收集器的分代设计扩展至方法区，或者说使用永久代来实现方法区而已，这样使得 HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存，省去专门为方法区编写内存管理代码的工作。但是对于其他虚拟机实现，譬如 BEA JRockit、IBM J9 等来说，是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节，不受《Java虚拟机规范》管束，并不要求统一。但现在回头来看，当年使用永久代来实现方法区的决定并不是一个好主意，这种设计导致了 Java 应用更容易遇到 内存溢出的问题(永久代有-XX:M axPermSize 的上限，即使不设置也有默认大小，而 J9 和 JRockit 只要没有触碰到进程可用内存的上限，例如32位系统中的4GB限制，就不会出问题 )，而且有极少数方法 (例如 String :: intern() ) 会因永久代的原因而导致不同虚拟机下有不同的表现。当 Oracle 收购 BEA 获得了 JRockit 的所有权后，准备把 JRockit 中的优秀功能，譬如 Java Mission Control 管理工具，移植到 HotSpot 虚拟机时，但因为两者对方法区实现的差异而面临诸多困难。考虑到 HotSpot 未来的发展，在 JDK 6 的 时候 HotSpot 开发团队就有放弃永久代，逐步改为采用本地内存 (Native Memory) 来实现方法区的计划了，到了JDK 7 的 HotSpot，已经把原本放在永久代的字符串常量池、静态变量等移出，而到了 JDK 8，终于完全废弃了永久代的概念，改用与 JRockit、J9 一样在本地内存中实现的元空间(Metaspace)来代替，把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。</p><p>《Java虚拟机规范》对方法区的约束是非常宽松的，除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外，甚至还可以选择不实现垃圾收集。相对而言，垃圾收集行为在这个区域的确是比较少出现的，但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载，一般来说这个区域的回收效果比较难令人满意，尤其是类型的卸载，条件相当苛刻，但是这部分区域的回收有时又确实是必要的。</p><p>根据《Java虚拟机规范》的规定，如果方法区无法满足新的内存分配需求时，将抛出 OutOfMemoryError 异常。</p></blockquote><p>这部分引用内容对方法区的介绍十分全面，切记不要将方法区和永久代混为一谈，从JDK 8 以后已经没有永久代的概念了。</p><h3 id="2.6-%E8%BF%90%E8%A1%8C%E6%97%B6%E5%B8%B8%E9%87%8F%E6%B1%A0-(runtime-constant-pool)" tabindex="-1">2.6 运行时常量池 (Runtime Constant Pool)</h3><blockquote><p>运行时常量池 (Runtime Constant Pool) 是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外，还有一项信息是常量池表 (Constant Pool Table)，用于存放编译期生成的各种字面量与符号引用，这部分内容将在类加载后存放到方法区的运行时常量池中。</p><p>既然运行时常量池是方法区的一部分，自然受到方法区内存的限制，当常量池无法再申请到内存 时会抛出OutOfMemoryError异常。</p></blockquote><p>常量池是为了避免频繁的创建和销毁对象而影响系统性能，其实现了对象的共享。</p><h2 id="%E6%80%BB%E7%BB%93" tabindex="-1">总结</h2><p>本文作为 Java 虚拟机系列的第一篇文章，为大家介绍了 Java 虚拟机的整体架构和运行时数据区，相信大家对 JVM 已经有了整体的认识。但这还远远不够，JVM 还有更多而内容和细节等着我们去探索，后续文章敬请期待。</p><p><em><strong>最后是参考文章和文献：</strong></em></p><ul><li>周志明《深入理解Java虚拟机：JVM高级特性与最佳实践》(强烈推荐)</li><li><a href="https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html" target="_blank">Java虚拟机规范</a></li><li><a href="https://www.cnblogs.com/czwbig/p/11127124.html" target="_blank">Java内存区域（运行时数据区域）和内存模型（JMM</a></li><li><a href="http://www.topperskills.com/tutorials/java/java-jvm-internal-architecture-structure.html" target="_blank">Architecture of JVM Java Virtual Machine</a></li><li><a href="https://www.jianshu.com/p/d21010003bb7" target="_blank">编译型与解释型的区别</a></li><li><a href="https://www.cnblogs.com/java-zzl/archive/2018/10/31/9862329.html" target="_blank">JVM总括三-字节码、字节码指令、JIT编译执行</a></li><li><a href="https://cloud.tencent.com/developer/article/1450501" target="_blank">彻底弄懂java中的常量池</a></li></ul>]]>
                    </description>
                    <pubDate>Mon, 15 Jun 2020 19:35:30 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[转载-垃圾收集机制详解，动图帮你理解]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/gc-animation</link>
                    <description>
                            <![CDATA[<p>From: <a href="https://blog.csdn.net/weixin_43207056/article/details/104133272">https://blog.csdn.net/weixin_43207056/article/details/104133272</a></p><h3 id="文章目录">文章目录</h3><ul><li>前言</li><li>一、需要进行垃圾收集的内存区域</li><li>二、判断对象是否可回收的方法<ul><li>2.1 引用计数法</li><li>2.2 可达性分析法</li></ul></li><li>三、垃圾收集算法介绍<ul><li>3.1 标记-清除算法</li><li>3.2 标记-复制算法</li><li>3.3 标记-整理算法</li><li>3.4 分代收集算法</li></ul></li><li>四、JVM 的内存分配和垃圾收集机制<ul><li>4.1 JVM 堆内存的划分</li><li>4.2 分代收集原理<ul><li>4.2.1 新生代中对象的分配与回收</li><li>4.2.2 对象晋升老年代</li></ul></li></ul></li><li>总结</li></ul><h2 id="前言">前言</h2><p>上篇文章已经给大家介绍了 JVM 的架构和运行时数据区 (内存区域)，本篇文章将给大家介绍 JVM 的重点内容——垃圾收集。众所周知，相比 C / C++ 等语言，Java 可以省去手动管理内存的繁琐操作，很大程度上解放了 Java 程序员的生产力，而这正是得益于 JVM 的垃圾收集机制和内存分配策略。我们平时写程序时并感知不到这一点，但是如果是在生产环境中，JVM 的不同配置对于服务器性能的影响是非常大的，所以掌握 JVM 调优是高级 Java 工程师的必备技能。正所谓“基础不牢，地动山摇”，在这之前我们先来了解一下底层的 JVM 垃圾收集机制。</p><p>既然要介绍垃圾收集机制，就要搞清楚以下几个问题：</p><ol><li>哪些内存区域需要进行垃圾收集？</li><li>如何判断对象是否可回收？</li><li>新的对象是如何进行内存分配的？</li><li>如何进行垃圾收集？</li></ol><p>本文将按以下行文结构展开，对上述问题一一解答。</p><ol><li>需要进行垃圾收集的内存区域；</li><li>判断对象是否可回收的方法；</li><li>主流的垃圾收集算法介绍；</li><li>JVM 的内存分配与垃圾收集机制。</li></ol><p>下面开始正文，还是图文并茂的老配方，走起。</p><h2 id="一需要进行垃圾收集的内存区域">一、需要进行垃圾收集的内存区域</h2><p>先来回顾一下 JVM 的运行时数据区：</p><p><img src="https://img-blog.csdnimg.cn/20200201141210918.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="JVM 运行时数据区" /></p><blockquote><p>JVM 运行时数据区</p></blockquote><p>其中程序计数器、Java 虚拟机栈和本地方法栈都是线程私有的，与其对应的线程是共生关系，随线程而生，随线程而灭，栈中的栈帧也随着方法的进入和退出井然有序地进行入栈和出栈操作。所以这几个区域的内存分配和回收都是有很大确定性的，在方法结束或线程结束时，内存也会随之释放，因此也就不需要考虑这几个区域的内存回收问题了。</p><p>而堆和方法区就不一样了，Java 的对象几乎都是在堆上创建出来的，方法区则存储了被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据，方法区中的运行时常量池则存放了各种字面量与符号引用，上述的这些数据大部分都是在运行时才能确定的，所以需要进行动态的内存管理。</p><p>还要说明一点，JVM 中的垃圾收集器的最主要的关注对象是 Java 堆，因为这里进行垃圾收集的“性价比”是最高的，尤其是在新生代 (后文对分代算法进行介绍) 中的垃圾收集，一次就可以回收 70% - 99% 的内存。而方法区由于垃圾收集判定条件，尤其是类型卸载的判定条件相当苛刻，其回收性价比是非常低的，因此有些垃圾收集器就干脆不支持或不完全支持方法区的垃圾收集，比如 JDK 11 中的 ZGC 收集器就不支持类型卸载。</p><h2 id="二判断对象是否可回收的方法">二、判断对象是否可回收的方法</h2><h3 id="21-引用计数法">2.1 引用计数法</h3><p>引用计数法的实现很简单，在对象中添加一个引用计数器，每当有一个地方引用它时，计数器值就加一；当引用失效时，计数器值就减一；任何时刻计数器为零的对象就是不可能再被使用的。大部分情况下这个方法是可以发挥作用的，但是在存在循环引用的情况下，引用计数法就无能为力了。比如下面这种情况：</p><pre><code>public class Student {            public Student friend = null;      public static void test() {        Student a = new Student();        Student b = new Student();        a.friend = b;        b.friend = a;        a = null;        b = null;        System.gc();    }}</code></pre><p>上述代码创建了 a 和 b 两个 Student 实例，并把它们各自的 friend 字段赋值为对方，除此之外，这两个对象再无任何引用，然后将它们都赋值为 null，在这种情况下，这两个对象已经不可能再被访问，但是它们因为互相引用着对方，导致它们的引用计数都不为零，引用计数算法也就无法回收它们。如下图所示：</p><p><img src="https://img-blog.csdnimg.cn/20200201141238736.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="循环引用" /></p><blockquote><p>循环引用</p></blockquote><p>但是在 Java 程序中，a 和 b 是可以被回收的，因为 JVM 并没有使用引用计数法判定对象是否可回收，而是采用了可达性分析法。</p><h3 id="22-可达性分析法">2.2 可达性分析法</h3><p>这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集 (GC Root Set)，从这些节点开始，根据引用关系向下搜索，搜索过程所走过的路径称为“引用链” (Reference Chain)，如果某个对象到GC Roots间没有任何引用链相连，则说明此对象不再被使用，也就可以被回收了。要进行可达性分析就需要先枚举根节点 (GC Roots)，在枚举根节点过程中，为防止对象的引用关系发生变化，需要暂停所有用户线程 (垃圾收集之外的线程)，这种暂停全部用户线程的行为被称为 (Stop The World)。可达性分析法如下图所示：</p><p><img src="https://img-blog.csdnimg.cn/20200201141725953.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="可达性分析法" /></p><blockquote><p>可达性分析法</p></blockquote><p>图中绿色的都是位于 GC Root Set 中的 GC Roots，所有与其有关联的对象都是可达的，被标记为蓝色，而所有与其没有任何关联的对象都是不可达的，被标记为灰色。即使是不可达对象，也并非一定会被回收，如果该对象同时满足以下几个条件，那么它仍有“逃生”的可能：</p><ol><li>该对象有重写的 <code>finalize()</code>方法 (Object 类中的方法)；</li><li><code>finalize()</code>方法中将其自身链接到了引用链上；</li><li>JVM 此前没有调用过该对象的<code>finalize()</code>方法 (因为 JVM 在收集可回收对象时会调用且仅调用一次该对象的<code>finalize()</code>方法)。</li></ol><p>不过由于<code>finalize()</code>方法的运行代价高昂，不确定性大，且无法保证各个对象的调用顺序，所以并不推荐使用。那么 GC Roots 又是何方神圣呢？在 Java 语言中，固定可作为GC Roots的对象包括以下几种：</p><ol><li>在虚拟机栈 (栈帧中的本地变量表) 中引用的对象，比如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。</li><li>在方法区中类静态属性引用的对象，比如Java类的引用类型静态变量。</li><li>在方法区中常量引用的对象，比如字符串常量池(String Table)里的引用。</li><li>在本地方法栈中JNI (即通常所说的Native方法) 引用的对象。</li><li>Java虚拟机内部的引用，如基本数据类型对应的Class对象，一些常驻的异常对象 (比如<br />NullPointExcepiton、OutOfMemoryError) 等，还有系统类加载器。</li><li>所有被同步锁 (synchronized关键字) 持有的对象。</li><li>反映Java虚拟机内部情况的 JM XBean、JVM TI 中注册的回调、本地代码缓存等。</li></ol><h2 id="三垃圾收集算法介绍">三、垃圾收集算法介绍</h2><h3 id="31-标记-清除算法">3.1 标记-清除算法</h3><p>标记-清除算法的思想很简单，顾名思义，该算法的过程分为标记和清除两个阶段：首先标记出所有需要回收的对象，其中标记过程就是使用可达性分析法判断对象是否属于垃圾的过程。在标记完成后，统一回收掉所有被标记的对象，也可以反过来，标记存活的对象，统一回收所有未被标记的对象。示意图如下：</p><p><img src="https://img-blog.csdnimg.cn/2020020114132598.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="标记清除算法" /></p><blockquote><p>标记清除算法</p></blockquote><p>这个算法虽然很简单，但是有两个明显的缺点：</p><ol><li>执行效率不稳定。如果 Java 堆中包含大量对象，而且其中大部分是需要被回收的，这时必须进行大量标记和清除的动作，导致标记和清除两个过程的执行效率都随对象数量增长而降低;</li><li>导致内存空间碎片化。标记、清除之后会产生大量不连续的内存碎片，空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作，非常影响程序运行效率。</li></ol><h3 id="32-标记-复制算法">3.2 标记-复制算法</h3><p>标记-复制算法常简称复制算法，这一算法正好解决了标记-清除算法在面对大量可回收对象时执行效率低下的问题。其实现方法也很易懂：在可用内存中划分出两块大小相同的区域，每次只使用其中一块，另一块保持空闲状态，第一块用完的时候，就把存活的对象全部复制到第二块区域，然后把第一块全部清空。如下图所示：</p><p><img src="https://img-blog.csdnimg.cn/20200201141348920.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="标记-复制算法" /></p><blockquote><p>标记-复制算法</p></blockquote><p>这个算法很适合用于对象存活率低的情况，因为它只关注存活对象而无需理会可回收对象，所以 JVM 中新生代的垃圾收集正是采用的这一算法。但是其缺点也很明显，每次都要浪费一半的内存，未免太过奢侈，不过新生代有更精细的内存划分，比较好地解决了这个问题，见下文。</p><h3 id="33-标记-整理算法">3.3 标记-整理算法</h3><p>这个算法完美解决了标记-清除算法的空间碎片化问题，其标记过程与“标记-清除”算法一样，但后续步骤不是直接对可回收对象进行清理，而是让所有存活的对象都向内存空间一端移动，然后直接清理掉边界以外的内存。</p><p><img src="https://img-blog.csdnimg.cn/20200201141405564.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="标记整理算法" /></p><blockquote><p>标记整理算法</p></blockquote><p>这个算法虽然可以很好地解决空间碎片化问题，但是每次垃圾回收都要移动存活的对象，还要对引用这些对象的地方进行更新，对象移动的操作也需要全程暂停用户线程 (Stop The World)。</p><h3 id="34-分代收集算法">3.4 分代收集算法</h3><p>与其说是算法，不如说是理论。如今大多数虚拟机的实现版本都遵循了“分代收集”的理论进行设计，这个理论可以看作是经验之谈，因为开发人员在开发过程中发现了 JVM 中存活对象的数量和它们的年龄之间有着某种规律，如下图：</p><p><img src="https://img-blog.csdnimg.cn/202002011414385.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="JVM 中存活对象数量与年龄之间的关系" /></p><blockquote><p>JVM 中存活对象数量与年龄之间的关系</p></blockquote><p>在此基础上，人们得出了以下假说：</p><ol><li>绝大多数对象都是朝生夕灭的。</li><li>熬过越多次垃圾收集过程的对象就越难以消亡。</li></ol><p>根据这两个假说，可以把 JVM 的堆内存大致分为新生代和老年代，新生代对象大多存活时间短，每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象，就能以较低代价回收到大量的空间，所以这一区域一般采用标记-复制算法进行垃圾收集，频率比较高。而老年代则是一些难以消亡的对象，可以采用标记-清除和标记整理算法进行垃圾收集，频率可以低一些。</p><p>按照 Hotspot 虚拟机的实现，针对新生代和老年代的垃圾收集又分为不同的类型，也有不同的名词，如下：</p><ol><li>部分收集 (Partial GC)：指目标不是完整收集整个Java堆的垃圾收集，其中又分为:</li></ol><ul><li><p>新生代收集 (Minor GC / Young GC)：指目标只是新生代的垃圾收集。</p></li><li><p>老年代收集 (Major GC / Old GC)：指目标只是老年代的垃圾收集，目前只有CMS收集器的并发收集阶段是单独收集老年代的行为。</p></li><li><p>混合收集 (Mixed GC)：指目标是收集整个新生代以及部分老年代的垃圾收集，目前只有G1收集器会有这种行为。</p></li></ul><ol start="2"><li>整堆收集 (Full GC)：收集整个Java堆和方法区的垃圾收集。</li></ol><p>人们经常会混淆 Major GC 和 Full GC，不过这也有情可原，因为这两种 GC 行为都包含了老年代的垃圾收集，而单独的老年代收集 (Major GC) 又比较少见，大多数情况下只要包含老年代收集，就会是整堆收集 (Full GC)，不过还是分得清楚一点比较好哈。</p><h2 id="四jvm-的内存分配和垃圾收集机制">四、JVM 的内存分配和垃圾收集机制</h2><p>经过前面的铺垫，现在终于可以一窥 JVM 的内存分配和垃圾收集机制的真面目了。</p><h3 id="41-jvm-堆内存的划分">4.1 JVM 堆内存的划分</h3><p><img src="https://img-blog.csdnimg.cn/20200201141457740.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="JVM 堆内存划分" /></p><blockquote><p>JVM 堆内存划分</p></blockquote><p>Java 堆是 JVM 所管理的内存中最大的一块，也是垃圾收集器的管理区域。大多数垃圾收集器都会将堆内存划分为上图所示的几个区域，整体分为新生代和老年代，比例为 1 : 2，新生代又进一步分为 Eden、From Survivor 和 To Survivor，默认比例为 8 : 1 : 1，请注意，可通过 SurvivorRatio 参数进行设置。请注意，从 JDK 8 开始，JVM 中已经不再有永久代的概念了。Java 堆上的无论哪个区域，存储的都只能是对象的实例，将Java 堆细分的目的只是为了更好地回收内存，或者更快地分配内存。</p><h3 id="42-分代收集原理">4.2 分代收集原理</h3><h4 id="421-新生代中对象的分配与回收">4.2.1 新生代中对象的分配与回收</h4><p>大多数情况下，对象优先在新生代 Eden 区中分配，当 Eden 区没有足够空间进行分配时，虚拟机将发起一次 Minor GC。Eden、From Survivor 和 To Survivor 的比例为 8 : 1 : 1，之所以按这个比例是因为绝大多数对象都是朝生夕灭的，垃圾收集时 Eden 存活的对象数量不会太多，Survivor 空间小一点也足以容纳，每次新生代中可用内存空间为整个新生代容量的90% (Eden 的 80% 加上 To Survivor 的 10%)，只有From Survivor 空间，即 10% 的新生代是会被“浪费”的。不会像原始的标记-复制算法那样浪费一半的内存空间。From Survivor 和 To Survivor 的空间并不是固定的，而是在 S0 和 S1 之间动态转换的，第一次 Minor GC 时会选择 S1 作为 To Survivor，并将 Eden 中存活的对象复制到其中，并将对象的年龄加1，注意新生代使用的垃圾收集算法是标记-复制算法的改良版。下面是示意图，请注意其中第一步的变色是为了醒目，虚拟机只做了标记存活对象的操作。</p><p><img src="https://img-blog.csdnimg.cn/20200201141539705.gif" alt="第一次 Minor GC 示意图" /></p><blockquote><p>第一次 Minor GC 示意图</p></blockquote><p>在后续的 Minor GC 中，S0 和 S1会交替转化为 From Survivor 和 To Survivor，Eden 和 From Survivor 中的存活对象会复制到 To Survivor 中，并将年龄加 1。如下图所示：</p><p><img src="https://img-blog.csdnimg.cn/20200201141609892.gif" alt="后续 Minor GC 示意图" /></p><blockquote><p>后续 Minor GC 示意图</p></blockquote><h4 id="422-对象晋升老年代">4.2.2 对象晋升老年代</h4><p>在以下这些情况下，对象会晋升到老年代。</p><ol><li>长期存活对象将进入老年代</li></ol><p>对象在 Survivor 区中每熬过一次Minor GC，年龄就增加1岁，当它的年龄增加到一定程度 (默认为15)，就会被晋升到老年代中。对象晋升老年代的年龄阈值，可以通过参数 -XX:MaxTenuringThreshold 设置。</p><p><img src="https://img-blog.csdnimg.cn/20200201141623101.gif" alt="长期存活对象晋升老年代示意图" /></p><blockquote><p>长期存活对象晋升老年代示意图</p></blockquote><ol start="2"><li>大对象可以直接进入老年代</li></ol><p>对于大对象，尤其是很长的字符串，或者元素数量很多的数组，如果分配在 Eden 中，会很容易过早占满 Eden 空间导致 Minor GC，而且大对象在 Eden 和两个 Survivor 之间的来回复制也还会有很大的内存复制开销。所以我们可以通过设置 -XX:PretenureSizeThreshold 的虚拟机参数让大对象直接进入老年代。</p><ol start="3"><li>动态对象年龄判断</li></ol><p>为了能更好地适应不同程序的内存状况，HotSpot 虚拟机并不是永远要求对象的年龄必须达到 -XX:MaxTenuringThreshold 才能晋升老年代，如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半，年龄大于或等于该年龄的对象就可以直接进入老年代，无须等到 -XX:MaxTenuringThreshold 中要求的年龄。</p><ol start="4"><li>空间分配担保 (Handle Promotion)</li></ol><p>当 Survivor 空间不足以容纳一次 Minor GC 之后存活的对象时，就需要依赖其他内存区域 (实际上大多数情况下就是老年代) 进行分配担保。在发生 Minor GC 之前，虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间，如果这个条件成立，那这一次 Minor GC 可以确保是安全的。如果不成立，则虚拟机会先查看 - XX:HandlePromotionFailure 参数的设置值是否允许担保失败 (Handle Promotion Failure)；如果允许，那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小，如果大于，将尝试进行一次 Minor GC，尽管这次 Minor GC 是有风险的；如果小于，或者-XX: HandlePromotionFailure设置不允许冒险，那这时就要改为进行一次 Full GC。</p><h2 id="总结">总结</h2><p>本文介绍了 JVM 的垃圾收集机制，并用大量图片和动图来帮助大家理解，如有错误，欢迎指正。后续文章会继续介绍 JVM 中的各种垃圾收集器，包括最前沿的 ZGC 和 Shenandoah 收集器，是 JVM 领域的最新科技成果，敬请期待。</p><p>最后是参考文章：</p><ul><li>《深入理解Java虚拟机》</li><li><a href="https://plumbr.io/blog/garbage-collection/minor-gc-vs-major-gc-vs-full-gc">Minor GC vs Major GC vs Full GC</a></li><li><a href="https://www.alibabacloud.com/blog/how-does-garbage-collection-work-in-java_595387">How Does Garbage Collection Work in Java?</a></li><li><a href="https://www.zhihu.com/question/41922036">Major GC和Full GC的区别是什么？触发条件呢？</a></li><li><a href="https://blog.csdn.net/weixin_36027342/article/details/79973294">面试题：JVM，GC垃圾回收机制</a></li></ul>]]>
                    </description>
                    <pubDate>Mon, 15 Jun 2020 19:34:39 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[转载-10 种垃圾回收器一网打尽]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/trash-gc</link>
                    <description>
                            <![CDATA[<p>From: <a href="https://blog.csdn.net/weixin_43207056/article/details/104536443">https://blog.csdn.net/weixin_43207056/article/details/104536443</a></p><h2 id="前言">前言</h2><p>Java 语言和 JVM 在不断迭代发展的同时，垃圾收集器也在不断地进化，从最初的的单线程收集器 Serial，到后来的并行收集器 Parallel 和并发收集器 CMS、G1，再到垃圾收集器最前沿成果——超低延迟的 Shenandoah 和 ZGC，还有不做垃圾收集的垃圾收集器 Epsilon (是的你没有看错)，正是有了这些垃圾收集器的存在，Java 开发者才得以从繁琐的手动管理中解放出来。下面将为大家一一介绍这些垃圾收集器，全文采用“总-分”结构，先总体认识一下所有的垃圾收集器，在逐个进行介绍。</p><h2 id="一垃圾收集器汇总">一、垃圾收集器汇总</h2><p>下图就是 HotSpot 虚拟机上的已商用的垃圾收集器的关系图 (此图并不包含 Shenandoah 和 ZGC，因为这两者目前都还处于实验阶段，且没有遵循经典的分代收集理论，另外的 Epsilon 也不是常规的垃圾收集器，因此也没出现在此图上)。</p><p><img src="https://img-blog.csdnimg.cn/20200227144823596.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" /></p><blockquote><p>HotSpot 虚拟机的垃圾收集器</p></blockquote><p>图中的连线表示两个垃圾收集器之间可以搭配使用，请注意，JDK 9 已不再支持 Serial + CMS 和 ParNew + Serial Old 的搭配组合。如果觉得数量太多不好记的话，可以把上图中的五个垃圾收集器分为以下三大类：</p><ol><li>Serial 类：新生代版本为 Serial，老年代版本为 Serial Old，这两个都是单线程垃圾收集器。另外，ParNew 相比 Serial 只是增加了多线程并行收集的功能，并无其他太大差别。</li><li>Parallel 类：包括 Parallel Scavenge 和 Parallel Old，多线程并行垃圾收集器经典组合，这个组合更注重于提高程序的吞吐量。</li><li>并发收集器：CMS 和 G1都可以并发进行垃圾收集，其中 CMS 只适用于老年代，而 G1 则横跨新生代和老年代。</li></ol><blockquote><p>并发 (concurrent)与并行 (parallel)：这里所说的并发与并行的概念和操作系统里的概念有所不同，这里的并发是指垃圾收集线程和用户线程可以同时执行，而并行是指多个垃圾收集线程同时执行，但用户线程必须暂停。</p></blockquote><p>除了上图这些经典的垃圾收集器，还有一些目前尚处于试验阶段的黑科技收集器，这部分仅做了解即可，万一面试的时候扯到了，还能顺带装一波逼。OracleJDK 11 新加入了 ZGC 收集器(目前还处于实验阶段)，OpenJDK 12 中也加入了 其独有的 Shenandoah 收集器 (也处于实验阶段)，OracleJDK 和 OpenJDK 的区别这里就不细说了。这两款垃圾收集器都以超低延迟为卖点，也就是尽量缩短垃圾收集时用户线程的暂停 (Stop The World)的时间，这两款收集器都宣称可以把垃圾收集的停顿时间控制在 10 毫秒以内，比之前最牛X的G1的延迟还要短。最后还有适用于微服务领域的 Epsilon，下面就为大家一一介绍这些琳琅满目、五花八门的垃圾收集器。</p><h2 id="二垃圾收集器详解">二、垃圾收集器详解</h2><h3 id="21-新生代收集器">2.1 新生代收集器</h3><h4 id="211-serial-收集器">2.1.1 Serial 收集器</h4><p>Serial 收集器是最基础、历史最悠久的垃圾收集器，在 JDK 1.3.1 之前是 HotSpot 虚拟机新生代收集器的唯一选择。既然如此，也不能指望它有多么强大的功能了，这是一款单线程收集器，不仅只有一个垃圾收集线程，更难受的是它在进行垃圾收集时必须暂停所有用户线程，也就是说垃圾收集时需要全程 “Stop The World”，如图：</p><p><img src="https://img-blog.csdnimg.cn/20200227144843675.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" /></p><blockquote><p>Serial / Serial Old 搭配的垃圾收集示意图</p></blockquote><p>可见 Serial 在进行垃圾收集是必须“Stop The World”，而且其单线程的收集效率并不高，可能造成用户程序的长时间停顿。上篇文章已经给大家介绍过了新生代和老年代的概念，接下来补充一下图中安全点的概念：</p><blockquote><p>安全点 (safepoint)：安全点是代码指令中特定的位置，这些位置记录着栈和寄存器里那些位置是引用，这样收集器在扫描垃圾对象时就不需要一个不漏地从方法区等 GC Roots 开始查找。安全点位置一般选在方法调用、循环跳转和异常跳转的代码指令处，因为这些位置的代码可以“长时间运行”。</p></blockquote><h4 id="212-parnew-收集器">2.1.2 ParNew 收集器</h4><p>ParNew 收集器实质上就是 Serial 收集器的多线程版本，这也是它的唯一优势，除了同时使用多线程进行垃圾收集之外，其他的行为包括 Serial 所有可用的控制参数 (比如 -XX:SurvivorRatio，-XX:PretenureSizeThreshold，-XX:HandlePromotionFailure等)，还垃圾收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一致。这两个收集器的底层代码大部分也是相通的。</p><p><img src="https://img-blog.csdnimg.cn/20200227144857346.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" /></p><blockquote><p>ParNew / Serial Old 搭配的垃圾收集示意图</p></blockquote><p>可以使用 -XX:+/-UseParNewGC选项来强制指定或禁用 ParNew 收集器，ParNew 还有一个特点，就是在使用 -XX:UseConcMarkSweepGC 参数激活 CMS 收集器后，新生代会默认使用 ParNew 收集器。</p><h4 id="213-parallel-scavenge-收集器">2.1.3 Parallel Scavenge 收集器</h4><p>Parallel Scavenge 收集器也是一款作用于新生代、基于标记-复制算法的多线程并行垃圾收集器，与 ParNew 有很多相似之处。相比 CMS、G1、Shenandoah 和ZGC 这些致力于降低停顿时间，也就是低延迟的收集器，Parallel Scavenge 是吞吐量 (throughput)优先的收集器，吞吐量是指 CPU 用于运行用户程序的时间与 CPU 总消耗时间的比值：</p><p><img src="https://img-blog.csdnimg.cn/20200227144911694.png" alt="在这里插入图片描述" /></p><blockquote><p>吞吐量计算表达式</p></blockquote><p>低延迟和高吞吐量的收集器有着不同的适用场景，前者适用于与用户交互较多或需要保证服务器响应质量的场景，低延迟可以带来良好的用户体验，而高吞吐量可以让 CPU 把更多的时间用在运行用户程序上面，可以更快完成任务，适用于交互性不强的后台运算场景。Parallel Scavenge 提供了两个参数用于精确控制吞吐量，分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis参数和直接设置吞吐量大小的 -XX:GCTimeRatio。</p><p>Parallel Scavenge 还有一个比较特色的开关参数：-XX:+UseAdaptiveSizePolicy，激活这个参数后，会开启自适应策略，也就是无需我们手动设置新生代大小 (-Xmn)、Eden 与 Survivor 的比例 (-XX:SurvivorRatio)和直接晋升老年代对象大小（-XX:PretenureSizeThreshold)，虚拟机会根据系统运行状态并收集性能监控信息，动态调整这些参数以提供最合适的停顿时间或最大的吞吐量。</p><h3 id="22-老年代收集器">2.2 老年代收集器</h3><h4 id="221-serial-old-收集器">2.2.1 Serial Old 收集器</h4><p>这就是 Serial 的老年代版本，也是单线程收集器，使用标记-整理算法，是 Serial 的黄金搭档：</p><p><img src="https://img-blog.csdnimg.cn/20200227144927865.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" /></p><blockquote><p>Serial / Serial Old 搭配的垃圾收集示意图</p></blockquote><p>这个搭档主要用在客户端模式下，除此之外，Serial Old 还有两个用途，那就是和 JDK 5 及之前的 Parallel Scavenge搭配使用，以及作为 CMS 收集器失败之后的备胎。</p><h4 id="222-parallel-old-收集器">2.2.2 Parallel Old 收集器</h4><p>这个是 Parallel Scavenge 的老年代版本，但是直到 JDK 6 才正式提供，之前的 Parallel Scavenge 只能和单线程的 Serial Old 搭配使用，完全发挥不了其优势，Parallel Old 出现后，“吞吐量优先”收集器终于也有了黄金搭档：</p><p><img src="https://img-blog.csdnimg.cn/20200227144944442.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" /></p><blockquote><p>Parallel Scavenge / Parallel Old 搭配的垃圾收集过程</p></blockquote><h4 id="223-cms-收集器">2.2.3 CMS 收集器</h4><p>CMS (Concurrent Mark Sweep) 收集器是一款致力于获取最短停顿时间的收集器，从它的名字中可以看出这款收集器有两个重要特点：一，这是一款可以并发进行垃圾收集的收集器；二，这款收集器是基于标记清除-算法的。它的运作过程相对于之前不能并发的垃圾收集器更加复杂，大体分为以下四个步骤：</p><ol><li>初始标记 (CMS initial mark)</li></ol><p>这一步仅仅是标记一下与 GC Roots 直接关联的对象，虽然不是并发执行，但是速度很快，用户程序会有短暂的暂停。</p><ol start="2"><li>并发标记 (CMS concurrent mark)</li></ol><p>这一步比较耗时，需要遍历所有与 GC Roots 有关联的对象，但是可以与用户线程并发执行，所以对用户程序影响不大。</p><ol start="3"><li>重新标记 (CMS remark)</li></ol><p>由于并发标记过程中用户程序是不暂停的，所以有可能引起原来的标记对象产生变动，而重新标记的作用就是修正那些变动的标记记录，这一阶段虽然无法并发执行，但是工作量很小，所以持续时间也很短。</p><ol start="4"><li>并发清除 (CMS concurrent sweep)</li></ol><p>这一阶段就是清除掉可回收的对象，回想上篇文章介绍的标记-清除算法，在清除掉垃圾对象后并不需要移动存活对象，所以这一阶段可以与用户线程并发执行。</p><p><img src="https://img-blog.csdnimg.cn/20200227144959876.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" /></p><blockquote><p>CMS 垃圾收集过程</p></blockquote><p>综上，CMS 收集器在运行过程中只需在初始标记阶段和重新标记阶段暂停用户程序，而且时间很短，其他阶段均可与用户程序并发执行，这就是它实现超短停顿的秘密所在。</p><p>CMS 的优势很明显，就是并发收集和低停顿，但也不是完美无缺的，它主要有以下三个明显缺点：</p><ol><li>CMS 收集器对处理器资源，也就是 CPU 核心数非常敏感，这也是所有并发设计的程序的共同特点。虽然它并发特点带来了低停顿的优势，但是由于挤占了处理器资源，导致总吞吐量降低，程序运行总时间也会相应延长。</li><li>CMS 无法处理“浮动垃圾”，因为并发标记阶段和并发清除阶段用户程序是在继续执行的，自然会继续产生垃圾对象，但是这些垃圾对象产生在标记阶段之后，所以无法被标记出来，自然也就无法被清除，而这可能会引发停顿时间较长的 Full GC。</li><li>空间碎片，既然 CMS 是基于标记-清除算法的，也就不能避免产生空间碎片了，空间碎片就是不连续的可用内存，这可能导致明明有剩余空间，但就是放不下新对象，从而提前触发 Full GC。</li></ol><h3 id="23-g1-收集器">2.3 G1 收集器</h3><p>Garbage First 收集器，简称 G1，可以说是垃圾收集器技术史上里程碑式的成果，它开创了收集器面向局部收集的设计思路和基于 Region 的内存布局结构。也是从 G1 开始，垃圾收集器，包括后来的 Shenandoah 和 ZGC 都不再局限于只回收新生代或只回收老年代，而是面向整个 Java 堆。</p><p>G1 收集器最大的特色就是可预测的停顿，用户可以通过 -XX:MaxPauseMillis 参数 (默认200毫秒)指定期望的最大停顿时间，但不能随意指定，要切合实际，然后 G1 会根据这一目标值筛选并回收那些回收价值最高的可回收对象，那么 G1 是怎样做到这一点的呢？关键就在于 G1 基于 Region 的内存布局，先来看一下 G1 和之前垃圾收集器的堆内存布局对比：</p><p><img src="https://img-blog.csdnimg.cn/20200227145031134.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" /></p><blockquote><p>G1 之前各款垃圾收集器的堆内存布局</p></blockquote><p><img src="https://img-blog.csdnimg.cn/2020022714504923.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" /></p><blockquote><p>G1 收集器堆内存布局</p></blockquote><p>由此可见，虽然 G1 仍然遵循分带收集理论，但是内存区域不再按照固定大小的新生代和老年代进行划分，而是把连续的 Java 对划分成多个大小相等的独立区域 (Region)，每一个 Region 都可以根据需要扮演新生代的 Eden 空间、Survivor 空间或老年代空间。收集器可以对扮演不同角色的 Region 采用不同的策略进行处理，这样无论是新创建的对象还是已经存活了一段时间的对象，抑或是熬过了多次收集的就对象都能获得很好的收集效果。Region 中还有一类特殊的 Humongous 区域，专门用来存放大对象，G1 认为只要大小超过一个 Region 容量一半的对象就可判定为大对象，每个 Region 的大小可以通过参数 -XX:G1HeapRedionSize 设定，取值范围为 1MB~32MB，且为 2 的 N 次幂，对于那些大小超过整个 Region 大小的超大对象，将会被存放在 N 个连续的 Humongous Region 中，G1 一般会把 Humongous Region 看做老年代。</p><p>在把内存分成 Region 管理之后，G1 就可以对这些 Region 各个击破了，其停顿时间之所以可控，是因为 G1 在垃圾收集时并不会把整个 Java 堆当做回收区域，而是只收集那些回收价值最高的 Region，保证能在指定最大停顿时间内回收完毕，回收价值是指回收所获得的空间大小及耗费时间的权衡结果。这样就保证了 G1 能在指定时间内获得尽可能高的回收效率。</p><p>G1 的回收过程大致可以分为以下四个步骤：</p><ol><li>初始标记 (initial marking)：仅仅标记直接与 GC Roots 关联的对象，停顿时间很短；</li><li>并发标记 (Concurrent Marking)：从 GC Roots 开始对堆中对象进行可达性分析，耗时较长，但能与用户程序并发执行，所以不会停顿；</li><li>最终标记 (Final Marking)：用户程序并发执行导致对象引用关系变化，修正变化的引用，此阶段会短暂地暂停用户程序；</li><li>筛选回收 (Live Data Counting and Evacuation)：更新 Region 的统计数据，对各个 Region 的回收价值和成本进行排序，并根据用户所期望的停顿时间指定回收计划，可以自由选择任意多个 Region 构成回收集，然后把需要回收的 Region 中存货对象复制到空的 Region 中，在清理掉旧 Region 的全部空间，这里涉及存活对象的移动，必须暂停用户线程，是由多条收集器线程并行完成的。</li></ol><p><img src="https://img-blog.csdnimg.cn/20200227145102979.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" /></p><blockquote><p>G1 收集器运行过程</p></blockquote><p>G1 和 CMS 都是以低停顿为目标的收集器，所以经常被拿来比较孰优孰劣，虽然 G1 相比 CMS 优势明显，但也并非全方位的碾压，G1相比 CMS 的优缺点如下：</p><ul><li><p>G1 优点：</p><ol><li><p>可以指定最大停顿时间；</p></li><li><p>分 Region 管理内存，按受益动态确定回收区域；</p></li><li><p>不会产生内存碎片：G1 的内存布局并不是固定大小以及固定数量的分代区域划分，而是把连续的Java堆划分为多个大小相等的独立区域 (Region)，G1 从整体来看是基于“标记-整理”算法实现的收集器，但从局部 (两个Region 之间)上看又是基于“标记-复制”算法实现，不会像 CMS (“标记-清除”算法) 那样产生内存碎片。</p></li></ol></li><li><p>G1 缺点：</p></li></ul><p>G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系，这种数据结构在 G1 中需要占用大量的内存，可能达到整个堆内存容量的 20% 甚至更多。而且 G1 中维护记忆集的成本较高，带来了更高的执行负载，影响效率。</p><p>按照《深入理解Java虚拟机》作者的说法，CMS 在小内存应用上的表现要优于 G1，而大内存应用上 G1 更有优势，大小内存的分水岭是6GB到8GB。</p><h3 id="24-最前沿科技成果低延迟垃圾收集器">2.4 最前沿科技成果：低延迟垃圾收集器</h3><p>之前最先进的 G1 收集器早在 JDK 7 上就已经发布了成熟版，而截至目前的2020年初，JDK 版本已经来到了 JDK 13，与此同时，垃圾收集器领域也早已有了更先进的黑科技，其中的代表者就是号称可以将停顿时间控制在10毫秒内低延迟收集器——Shenandoah 和 ZGC，它们最牛X的地方在于并发程度更高，连移动存活对象 (也就是标记-整理算法的整理阶段)都可以做到并发执行 (不过二者的实现原理有所区别)：</p><p><img src="https://img-blog.csdnimg.cn/20200227145125788.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" /></p><blockquote><p>各种垃圾收集器并发程度对比，绿色表示并发，黄色表示非并发</p></blockquote><p>由图可知，相比之前的收集器，Shenandoah 和 ZGC 在工作过程中几乎全程并发，只有在初始标记、最终标记这些阶段有短暂的暂停，而且这些停顿时间与堆容量和堆中对象数量没有正比例关系，这才可以将停顿时间控制在惊人的10毫秒以内。</p><h4 id="241-shenandoah-收集器">2.4.1 Shenandoah 收集器</h4><p>Shenandoah 是由 ReadHat 公司独立发展的新型垃圾收集器，并在2014年贡献给了 OpenJDK，并成为 OpenJDK 12 的正式特性之一，但是以 Oracle 公司的尿性，却不愿把它添加到 OracleJDK 中，这也导致了免费开源的 OpenJDK 反而比商业收费的 OracleJDK 功能更多，实属罕见。</p><p>Shenandoah 与 G1 有很多相似之处，比如都是基于 Region 的内存布局，都有用于存放大对象的 Humongous Region，默认回收策略也是优先处理回收价值最大的 Region。不过也有三个重大的区别：</p><ol><li>最最重要的区别，Shenandoah 支持并发的整理算法，G1 的整理阶段虽是多线程并行，但无法与用户程序并发执行；</li><li>默认不使用分代收集理论；</li><li>使用连接矩阵 (Connection Matrix)记录跨 Region 的引用关系，替换掉了 G1 中的记忆级 (Remembered Set)，内存和计算成本更低。</li></ol><p>Shenandoah 收集器的工作原理相比 G1 要复杂不少，其运行流程示意图如下：</p><p><img src="https://img-blog.csdnimg.cn/20200227145144595.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" /></p><blockquote><p>Shenandoah 收集器运行流程</p></blockquote><p>可见 Shenandoah 的并发程度明显比 G1 更高，只需要在初始标记、最终标记、初始引用更新和最终引用更新这几个阶段进行短暂的“Stop The World”，其他阶段皆可与用户程序并发执行，其中最重要的并发标记、并发回收和并发引用更新详情如下：</p><ul><li>并发标记( Concurrent Marking)</li></ul><p>与G1一样，遍历对象图，标记出全部可达的对象，这个阶段是与用户线程一起并发的，时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。</p><ul><li>并发回收( Concurrent Evacuation)</li></ul><p>并发回收阶段是 Shenandoah 与之前 HotSpot 中其他收集器的核心差异。在这个阶段， Shenandoah 要把待回收 Region 里面的存活对象先复制一份到其他未被使用的 Region之中。复制对象这件事情如果将用户线程冻结起来再做那是相当简单的，但如果两者必须要同时并发进行的话，就变得复杂起来了。其困难点是在移动对象的同时，用户线程仍然可能不停对被移动的对象进行读写访问，移动对象是一次性的行为，但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址，这是很难一瞬间全部改变过来的。对于并发回收阶段遇到的这些困难， Shenandoah 将会通过读屏障和被称为“ Brooks Pointers”的转发指针来解决。并发回收阶段运行的时间长短取决于回收集的大小。</p><blockquote><p>Brooks Pointers 简要介绍：这是一种转发指针 (Forwarding Pointer)，原理就是在所有的对象上新添加一个指针，初始状态下该指针指向对象本身，而在垃圾回收过程中，如果该对象是存活对象，则需要将其从回收区域移动到目标区域 (其实就是在目标区域复制一个新对象，这就是标记-整理算法的整理阶段，之前的 G1 收集器在此阶段无法与用户程序并发执行)，然后把旧对象的转发指针指向新的对象，这样用户程序在并发执行的情况下，就不会访问到旧对象了。</p></blockquote><ul><li>并发引用更新( Concurrent Update Reference)</li></ul><p>这个阶段是与用户线程一起并发的，时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同，它不再需要沿着对象图来搜索，只需要按照内存物理地址的顺序，线性地搜索出引用类型，把旧值改为新值即可。</p><p>Shenandoah 的高并发度让它实现了超低的停顿时间，但是更高的复杂度也伴随着更高的系统开销，这在一定程度上会影响吞吐量，下图是 Shenandoah 与之前各种收集器在停顿时间维度和系统开销维度上的对比：</p><p><img src="https://img-blog.csdnimg.cn/20200227145220700.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" /></p><blockquote><p>Shenandoah 与之前各种收集器在停顿时间维度和系统开销维度上的对比</p></blockquote><p>OracleJDK 并不支持 Shenandoah，如果你用的是 OpenJDK 12 或某些支持 Shenandoah 移植版的 JDK 的话，可以通过以下参数开启 Shenandoah：</p><blockquote><p>-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC</p></blockquote><h4 id="242-zgc-收集器">2.4.2 ZGC 收集器</h4><p>Z Garbage Collector，简称 ZGC，是 JDK 11 中新加入的尚在实验阶段的低延迟垃圾收集器。它和 Shenandoah 同属于超低延迟的垃圾收集器，但在吞吐量上比 Shenandoah 有更优秀的表现，甚至超过了 G1，接近了“吞吐量优先”的 Parallel 收集器组合，可以说近乎实现了“鱼与熊掌兼得”。</p><p><strong>ZGC 的内存布局</strong></p><p>与 Shenandoah 和 G1 一样，ZGC 也采用基于 Region 的堆内存布局，但与它们不同的是， ZGC 的 Region 具有动态性，也就是可以动态创建和销毁，容量大小也是动态的，有大、中、小三类容量:</p><p><img src="https://img-blog.csdnimg.cn/20200227145300657.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" /></p><blockquote><p>ZGC 内存布局</p></blockquote><ul><li><p>小型 Region (Small Region)：容量固定为 2MB，用于放置小于 256KB 的小对象。</p></li><li><p>中型 Region (M edium Region)：容量固定为 32MB，用于放置大于等于 256KB 但小于 4MB 的对<br />象。</p></li><li><p>大型 Region (Large Region)：容量不固定，可以动态变化，但必须为 2MB 的整数倍，用于放置 4MB 或以上的大对象。每个大型 Region 中只会存放一个大对象，这也预示着虽然名字叫作“大型 Region”，但它的实际容量完全有可能小于中型 Region，最小容量可低至 4MB。</p></li></ul><p>与 Shenandoah 一样，ZGC 在工作过程中也几乎是全程与用户程序并发的，重点也是实现了标记-整理算法的整理阶段可以与用户程序并发执行。但是二者的实现方式不同，Shenandoah 是在对象身上添加转发指针的方法，而 ZGC 则是直接在指针上动手脚，也就是传说中的染色指针 (Colored Pointers)，这个指针就是 Java 对象的引用，例如：</p><pre><code>Object o = new Object();</code></pre><p>其中“o” 只是一个引用，也就是指针，指向存在堆上的对象实例，引用自身也是要占内存的，普通引用在32位机器占4个字节，在64位机器上，开启压缩指针 (-XX:+UseCompressedOops) 的话占4个字节，不开启的话占8个字节。ZGC 的染色指针结构如下 (不支持32位机器和压缩指针)：</p><p><img src="https://img-blog.csdnimg.cn/20200227145316249.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzIwNzA1Ng==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" /></p><blockquote><p>染色指针结构示意图</p></blockquote><p>得益于染色指针上标志位的支持，ZGC 也可以像 Shenandoah 那样，实现了在移动存活对象的过程中可以与用户程序并发执行，且效率更高。ZGC 还用到了很多其他的黑科技，原理过于复杂，就不在这里详述了。</p><p>在 JDK 11 及以上版本，可以通过以下参数开启 ZGC：</p><blockquote><p>-XX:+UnlockExperimentalVMOptions -XX:+UseZGC</p></blockquote><h3 id="25-最奇葩的垃圾收集器epsilon">2.5 最奇葩的垃圾收集器——Epsilon</h3><p>上面介绍的各种收集器，比如 G1、Shenandoah 和 ZGC 等都是越来越复杂，越来越先进， 而 JDK 11 新加入的 Epsilon 却是反其道而行，这款收集器不会做任何垃圾收集的操作，也许叫做“内存分配器”更加合适。虽然很奇葩，但是它还是有用武之地的，比如越来越火的微服务领域，如果系统运行时间很短，在堆内存耗尽之前就可以结束，那么垃圾收集也就没有任何意义了，这正是 Epsilon 的使用场景。</p><h2 id="总结">总结</h2><p>本文为大家介绍了目前 HotSpot 虚拟机上的所有垃圾收集器，有的已经久经沙场，有的仍处于试验阶段，但有望在未来成为主流，在实际应用中，大家可以根据具体场景选择合适的垃圾收集器。</p><p>参考资料：</p><ul><li>《深入理解Java虚拟机》周志明</li><li><a href="https://www.baeldung.com/jvm-zgc-garbage-collector">An Introduction to ZGC: A Scalable and Experimental Low-Latency JVM Garbage Collector</a></li><li><a href="https://cloud.tencent.com/developer/article/1406198">聊聊ShenandoahGC的Brooks Pointers</a></li><li><a href="https://wiki.openjdk.java.net/display/shenandoah/Main">https://wiki.openjdk.java.net/display/shenandoah/Main</a></li></ul>]]>
                    </description>
                    <pubDate>Mon, 15 Jun 2020 13:17:41 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[深入理解JVM(6)——类加载器]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/understanding-the-jvm-6</link>
                    <description>
                            <![CDATA[<h2 id="深入理解jvm6类加载器">深入理解JVM(6)——类加载器</h2><p>虚拟机设计团队把类加载阶段中的 <strong>“通过一个类的全限定名来获取描述此类的二进制字节流(即字节码)”</strong> 这个动作放到Java虚拟机外部去实现，以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为 <strong>“类加载器”</strong>。</p><p>一般来说，Java 虚拟机使用 Java 类的方式如下：</p><ol><li><strong>Java 源程序（.java 文件）<strong>在经过 Java 编译器</strong>编译</strong>之后就被转换成<strong>字节码（.class 文件）</strong>。</li><li>类加载器负责读取 Java 字节代码，并转换成 <code>java.lang.Class</code>类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 <code>newInstance()</code>方法就可以创建出该类的一个对象。</li></ol><p>实际的情况可能更加复杂，比如 Java 字节代码可能是通过工具动态生成的，也可能是通过网络下载的。更详细的内容可以参考上一篇文章中讲类加载过程中的加载阶段时介绍的几个例子（JAR包、Applet、动态代理、JSP等）。</p><p>类加载器虽然只用于实现类的加载动作，但它在Java程序起到的作用却远大于类加载阶段。对于任意一个类，都需要由<strong>加载它的类加载器和这个类本身</strong>一同确立<strong>其在Java虚拟机中的唯一性</strong>，每一个类加载器，都拥有一个独立的类名称空间。通俗而言：比较两个类是否“相等”（这里所指的“相等”，包括类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果，也包括使用instanceof()关键字对做对象所属关系判定等情况），只有在这两个类时由同一个类加载器加载的前提下才有意义，否则，即使这两个类来源于同一个Class文件，被同一个虚拟机加载，只要加载它们的类加载器不同，那这两个类就必定不相等。</p><p>从jvm的角度来讲，只存在以下两种不同的类加载器：</p><ul><li><strong>启动类加载器（Bootstrap ClassLoader）</strong>，这个类加载器用C++实现，是虚拟机自身的一部分；</li><li><strong>所有其他类的加载器</strong>，这些类由Java实现，独立于虚拟机外部，并且全都继承自抽象类<code>java.lang.ClassLoader</code>。</li></ul><p>从Java开发人员的角度看，类加载器可以划分得更细致一些：</p><ul><li><strong>启动类加载器（Bootstrap ClassLoader）</strong> 此类加载器负责将存放在 <code>&lt;JAVA_HOME&gt;\lib</code> 目录中的，或者被 -Xbootclasspath 参数所指定的路径中的，并且是虚拟机识别的（仅按照文件名识别，如 rt.jar，名字不符合的类库即使放在lib 目录中也不会被加载）类库加载到虚拟机内存中。 启动类加载器无法被 Java 程序直接引用，用户在编写自定义类加载器时，如果需要把加载请求委派给引导类加载器，直接使用null代替即可。</li><li><strong>扩展类加载器（Extension ClassLoader）</strong> 这个类加载器是由<code>ExtClassLoader（sun.misc.Launcher$ExtClassLoader）</code>实现的。它负责将<code>&lt;Java_Home&gt;/lib/ext</code>或者被 <code>java.ext.dir</code>系统变量所指定路径中的所有类库加载到内存中，开发者可以直接使用扩展类加载器。</li><li><strong>应用程序类加载器（Application ClassLoader）</strong> 这个类加载器是由 <code>AppClassLoader（sun.misc.Launcher$AppClassLoader）</code>实现的。由于这个类加载器是<code>ClassLoader</code>中的<code>getSystemClassLoader()</code>方法的返回值，因此一般称为<strong>系统类加载器</strong>。它负责加载<strong>用户类路径（ClassPath）</strong> 上所指定的类库，开发者可以直接使用这个类加载器，如果应用程序中没有自定义过自己的类加载器，一般情况下这个就是程序中默认的类加载器。</li></ul><p>由开发人员开发的应用程序都是由这三种类加载器相互配合进行加载的，如果有必要，还可以加入自己定义的类加载器。这些类加载器的关系一般如下图所示：</p><p><img src="https://pic.yupoo.com/crowhawk/188f5d64/26536d6a.png" alt="" /></p><p>上图展示的类加载器之间的层次关系，称为类加载器的<strong>双亲委派模型（Parents Delegation Model）</strong>。该模型要求除了顶层的启动类加载器外，其余的类加载器都应有自己的父类加载器，这里类加载器之间的父子关系一般通过<strong>组合（Composition）</strong> 关系来实现，而不是通过继承（Inheritance）的关系实现。</p><p><strong>工作过程</strong></p><p>如果一个类加载器收到了类加载的请求，它首先不会自己去尝试加载，而是把这个请求委派给父类加载器，每一个层次的加载器都是如此，依次递归，因此所有的加载请求<strong>最终都应该传送到顶层的启动类加载器中</strong>，只有当父加载器反馈自己无法完成此加载请求（它搜索范围中没有找到所需类）时，子加载器才会尝试自己加载。</p><p><strong>优点</strong></p><p>使用双亲委派模型来组织类加载器之间的关系，使得Java类随着它的类加载器一起具备了一种<strong>带有优先级的层次关系</strong>。例如类<code>java.lang.Object</code>，它存放再<code>rt.jar</code>中，无论哪个类加载器要加载这个类，最终都是委派给处于模型最顶端的启动类加载器进行加载，因此Object类在程序的各种类加载器环境中都是同一个类。</p><p>相反，如果没有双亲委派模型，由各个类加载器自行加载的话，如果用户编写了一个称为<code>java.lang.Object</code>的类，并放在程序的ClassPath中，那系统中将会出现多个不同的Object类，程序将变得一片混乱。如果开发者尝试编写一个与<code>rt.jar</code>类库中已有类重名的Java类，将会发现可以正常编译，但是永远无法被加载运行。</p><p>双亲委派模型的实现如下：</p><pre><code class="language-java">   protected synchronized Class&lt;?&gt; loadClass(String name,boolean resolve)throws ClassNotFoundException{       //check the class has been loaded or not       Class c = findLoadedClass(name);       if(c == null){           try{               if(parent != null){                   c = parent.loadClass(name,false);               }else{                   c = findBootstrapClassOrNull(name);               }           }catch(ClassNotFoundException e){               //if throws the exception ,the father can not complete the load           }           if(c == null){               c = findClass(name);           }       }       if(resolve){           resolveClass(c);       }       return c;   }</code></pre><h3 id="线程上下文类加载器">线程上下文类加载器</h3><p>双亲委派模型并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多<strong>服务提供者接口（Service Provider Interface，SPI）</strong>，允许第三方为这些接口提供实现。常见的 SPI 有 <strong>JDBC、JCE、JNDI、JAXP 和 JBI</strong> 等。这些 <strong>SPI 的接口由 Java 核心库来提供</strong>，如 JAXP 的 SPI 接口定义包含在 <code>javax.xml.parsers</code>包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 <strong>jar 包</strong>被包含进来，可以通过类路径（ClassPath）来找到，如实现了 JAXP SPI 的 Apache Xerces所包含的 jar 包。<strong>SPI 接口中的代码经常需要加载具体的实现类。<strong>如 JAXP 中的 <code>javax.xml.parsers.DocumentBuilderFactory</code>类中的 <code>newInstance()</code> 方法用来生成一个新的 <code>DocumentBuilderFactory</code> 的实例。这里的实例的真正的类是继承自 <code>javax.xml.parsers.DocumentBuilderFactory</code>，由 SPI 的实现所提供的。如在 Apache Xerces 中，实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于，<strong>SPI 的接口</strong>是</strong>Java 核心库</strong>的一部分，是<strong>由引导类加载器加载</strong>的，而<strong>SPI 实现的 Java 类</strong>一般是<strong>由系统类加载器加载</strong>的。引导类加载器是无法找到 SPI 的实现类的，因为它只加载 Java 的核心库。它也不能委派给系统类加载器，因为它是系统类加载器的祖先类加载器。也就是说，类加载器的双亲委派模型无法解决这个问题。</p><p>为了解决这个问题，Java设计团队只好引入了一个不太优雅的设计：<strong>线程上下文类加载器（Thread Context ClassLoader）</strong>。线程上下文类加载器是从 JDK 1.2 开始引入的。类 <code>java.lang.Thread</code>中的方法 <code>getContextClassLoader()</code>和 <code>setContextClassLoader(ClassLoader cl)</code>用来获取和设置线程的上下文类加载器。如果没有通过 <code>setContextClassLoader(ClassLoader cl)</code>方法进行设置的话，线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是应用程序类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。</p><p>有了线程上下文类加载器，就可以做一些“舞弊”的事情了，JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码，也就是父类加载器请求子类加载器去完成类加载器的动作，这种行为实际上就是<strong>打通了双亲委派模型的层次结构来逆向使用类加载器</strong>，已经违背了双亲委派模型的一般性原则。</p><h3 id="追求程序动态性">追求程序动态性</h3><p>这里所说的“动态性”指的是当前一些非常热门的名词：<strong>代码热替换（HotSwap）</strong>、<strong>模块热部署(Hot Deployment)</strong> 等。即希望应用程序能像计算机的外设一样，接上鼠标、键盘，不用重启就能立即使用，鼠标出了问题或需要升级就换个鼠标，不用停机或重启。</p><p>当前业界“事实上”的Java模块化标准是OSGi，而OSGi实现代码热部署的关键则是它自定义的类机载器的实现。关于OSGi的细节将在稍后的案例分析中详细讲解。</p><p><strong>API</strong></p><p><img src="https://pic.yupoo.com/crowhawk/6c13f82d/1b25cd13.png" alt="" /></p><p>其中有如下三个比较重要的方法</p><table><thead><tr><th>方法</th><th>说明</th></tr></thead><tbody><tr><td><strong>defineClass(String name, byte[] b, int off, int len)</strong></td><td>把字节数组 b中的内容转换成 Java 类，该字节数组可以看成是二进制流字节组成的文件，返回的结果是<code>java.lang.Class</code>类的实例。这个方法被声明为 final的。</td></tr><tr><td><strong>loadClass(String name)</strong></td><td>上文中已贴出源码，实现了双亲委派模型，调用<code>findClass()</code>执行类加载动作,返回的是<code>java.lang.Class</code>类的实例。</td></tr><tr><td><strong>findClass(String name)</strong></td><td>通过传入的类全限定名name来获取对应的类，返回的是<code>java.lang.Class</code>类的实例，该类没有提供具体的实现，开发者在自定义类加载器时需重用此方法，在实现此方法时需调用<code>defineClass(String name, byte[] b, int off, int len)</code>方法。</td></tr></tbody></table><p>在了解完上述内容后，我们可以容易地意识到自定义类加载器有以下两种方式：</p><ul><li><strong>采用双亲委派模型</strong>：继承ClassLoader类，只需重写其的<code>findClass(String name)</code>方法，而不需重写<code>loadClass(String name)</code>方法。</li><li><strong>破坏双亲委派模型</strong>：继承ClassLoader类，需要整个重写实现了双亲委派模型逻辑的<code>loadClass(String name)</code>方法。</li></ul><h3 id="实例">实例</h3><p>下面我们来实现一个自定义类加载器，用来加载存储在文件系统上的 Java 字节代码。</p><pre><code class="language-java">   public class FileSystemClassLoader extends ClassLoader {           private String rootDir;           public FileSystemClassLoader(String rootDir) {           this.rootDir = rootDir;       }           @Override      protected Class&lt;?&gt; findClass(String name) throws ClassNotFoundException {           byte[] classData = getClassData(name);           if (classData == null) {               throw new ClassNotFoundException();           }           else {               return defineClass(name, classData, 0, classData.length);           }       }           private byte[] getClassData(String className) {           String path = classNameToPath(className);           try {               InputStream ins = new FileInputStream(path);               ByteArrayOutputStream baos = new ByteArrayOutputStream();               int bufferSize = 4096;               byte[] buffer = new byte[bufferSize];               int bytesNumRead = 0;               while ((bytesNumRead = ins.read(buffer)) != -1) {                   baos.write(buffer, 0, bytesNumRead);               }               return baos.toByteArray();           } catch (IOException e) {               e.printStackTrace();           }           return null;       }           private String classNameToPath(String className) {           return rootDir + File.separatorChar                   + className.replace('.', File.separatorChar) + &quot;.class&quot;;       }    }</code></pre><p>类 FileSystemClassLoader的 <code>findClass()</code>方法首先根据类的全名在硬盘上查找类的字节代码文件（.class 文件），然后读取该文件内容，最后通过 defineClass()方法来把这些字节代码转换成 <code>java.lang.Class</code>类的实例。</p><p>主流的Java Web服务器如Tomcat、Jetty、WebLogic、WebSphere等等，都实现了自己定义的类加载器（一般都不止一个）。因为一个功能健全的Web服务器，要解决以下问题：</p><ul><li><strong>部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。</strong> 两个不同的应用程序可能会依赖同一个第三方类库的不同版本，不能要求一个类库在一个服务器中只有一份，服务器应当保证两个应用程序的类库可以互相独立使用。</li><li><strong>部署在同一个服务器上的两个Web应用程序所使用的Java类库可以相互共享。</strong> 例如，用户可能有5个使用Spring组织的应用程序部署在同一台服务器上，如果把5份Spring分别放在各个应用程序的隔离目录中，库在使用时都要被加载到服务器内存中，JVM的方法区就会有过度膨胀的风险。</li><li><strong>服务器需要尽可能保证自身安全不受部署的Web应用程序影响。</strong> 很多Web服务器本身是用Java实现的，服务器使用的类库应该与应用程序的类库相互独立。</li><li><strong>支持JSP应用的服务器，大多数需要支持代码热替换（HotSwap）功能。</strong> JSP文件由于其纯文本存储的特性，运行时修改的概率远大于第三方类库或程序自身的Class文件，因此需要做到修改后无须重启。</li></ul><p>鉴于上述问题，各种Web服务器都不约而同地提供了数个ClassPath路径供用户存放第三方类库，这些路径一般以“lib”或“classes”命名。以Tomcat为例，有3组目录（“<strong>/common/* ”、“/server/* ”和“/shared/* ”</strong>）可以存放Java类库，另外还可以加上Web应用程序自身的目录 <strong><code>“/WEB-INF/* ”</code></strong>，一共4组，把Java类库放置在这些目录中的含义分别如下：</p><ul><li><strong>/common</strong>目录：类库可被Tomcat和所有的Web应用程序共同使用。</li><li><strong>/server</strong>目录：类库可被Tomcat使用，对所有的Web应用程序都不可见。</li><li><strong>/shared</strong>目录：类库可被所有的Web应用程序共同使用，但对Tomcat自己不可见。</li><li><strong>/WebApp/WEB-INF</strong>目录：类库仅仅可以被此Web应用程序使用，对Tomcat和其他Web应用程序都不可见。</li></ul><p>为了支持这套目录结构，并对目录里的类库进行加载和隔离，Tomcat采用如下经典的双亲委派模型来实现了多个类加载器：</p><p><img src="https://pic.yupoo.com/crowhawk/1f37efef/99e7eaa6.png" alt="" /></p><p><strong>CommonClassLoader</strong>、<strong>CatalinaClassLoader</strong>、<strong>SharedClassLoader</strong>和<strong>WebappClassLoader</strong>是Tomcat自己定义的类加载器，它们分别加载 <code>/common/*</code>、<code>/server/*</code>、<code>/shared/</code>** 和 <code>/WebApp/WEB-INF/*</code> 中的Java类库。其中WebApp类加载器和JSP类加载器通常会存在多个实例，每一个Web应用程序对应一个WebApp类加载器，每一个JSP文件对应一个JSP类加载器。</p><p><strong>CommonClassLoader</strong>能加载的类都可以被<strong>CatalinaClassLoader</strong>和<strong>SharedClassLoader</strong>使用，而<strong>CatalinaClassLoader</strong>和<strong>SharedClassLoader</strong>自己能加载的类则与对方相互隔离。<strong>WebAppClassLoader</strong>可以使用<strong>SharedClassLoader</strong>加载到的类，但各个<strong>WebAppClassLoader</strong>实例之间相互隔离。而<strong>JasperLoader</strong>的加载范围仅是这个JSP文件编译出来的那一个Class，它出现的目的就是被丢弃。当服务器检测到JSP文件被修改时，会替换掉目前的<strong>JasperLoader</strong>的实例，并通过再建立一个新的JSP类加载器来实现JSP文件的HotSwap功能。</p><p><strong>特殊场景</strong></p><p>前文提到过一个场景，如果有5个Web应用程序都是用Spring来进行组织和管理的话，可以把Spring放到<strong>Common</strong>或<strong>Shared</strong>目录下让这些程序共享。Spring要对用户程序的类进行管理，自然要能访问到用户程序的类，而用户程序放在 <strong>/WebApp/WEB-INF</strong> 目录中，这时就需要破坏双亲委派模型，使用线程上下文类加载器来完成这一工作了。</p><h3 id="osgi类加载器的灵活运用">OSGi：类加载器的灵活运用</h3><p><strong>OSGi（Open Service Gateway Initiative）<strong>是OSGi联盟制定的一个基于Java语言的动态模块化规范，现在成为了Java“事实上”的</strong>模块化标准</strong>。它为开发人员提供了面向服务和基于组件的运行环境，并提供标准的方式用来管理软件的生命周期。OSGi 已经被实现和部署在很多产品上，在开源社区也得到了广泛的支持，其中最为著名的应用莫过于大家都很熟悉的Eclipse IDE。</p><p>OSGi 中的每个<strong>模块（bundle）</strong> 都包含 <strong>Java Package 和Class</strong>。模块可以声明它所依赖的需要<strong>导入（import）</strong> 的其它模块的 Java 包和类（通过 <strong>Import-Package</strong>），也可以声明<strong>导出（export）</strong> 自己的包和类，供其它模块使用（通过 <strong>Export-Package</strong>）。也就是说需要能够隐藏和共享一个模块中的某些 Java 包和类。这是通过 OSGi 特有的类加载器机制来实现的。</p><p><strong>OSGi 中的每个模块都有对应的一个类加载器</strong>，它负责加载模块自己包含的 Java 包和类。当它需要加载 Java 核心库的类时（以 java开头的包和类），它会代理给父类加载器（通常是启动类加载器）来完成。<strong>当它需要加载所导入的 Java 类时，它会代理给导出此 Java 类的模块来完成加载。</strong> 模块也可以显式的声明某些 Java 包和类，必须由父类加载器来加载。只需要设置系统属性 <code>org.osgi.framework.bootdelegation</code>的值即可。</p><p>假设有两个模块 bundleA 和 bundleB，它们都有自己对应的类加载器 ClassLoaderA 和 ClassLoaderB。在 bundleA 中包含类 com.bundleA.Sample，并且该类被声明为导出的，也就是说可以被其它模块所使用的。bundleB 声明了导入 bundleA 提供的类 <code>com.bundleA.Sample</code>，并包含一个类 <code>com.bundleB.NewSample</code>继承自 <code>com.bundleA.Sample</code>。在 bundleB 启动的时候，其类加载器 classLoaderB 需要加载类 <code>com.bundleB.NewSample</code>，进而需要加载类 <code>com.bundleA.Sample</code>。由于 bundleB 声明了类 <code>com.bundleA.Sample</code>是导入的，classLoaderB 把加载类 <code>com.bundleA.Sample</code>的工作代理给导出该类的 bundleA 的类加载器 ClassLoaderA。ClassLoaderA 在其模块内部查找类 <code>com.bundleA.Sample</code>并定义它，所得到的类 <code>com.bundleA.Sample</code>实例就可以被所有声明导入了此类的模块使用。对于以 java开头的类，都是由父类加载器来加载的。如果声明了系统属性 <code>org.osgi.framework.bootdelegation=com.example.core.*</code>，那么对于包 <code>com.example.core</code>中的类，都是由父类加载器来完成的。 OSGi 模块的这种类加载器结构，使得一个类的不同版本可以共存在 Java 虚拟机中，带来了很大的灵活性。不过它的这种不同，也会给开发人员带来一些麻烦，尤其当模块需要使用第三方提供的库的时候。下面提供几条比较好的建议：</p><ul><li>如果一个类库只有一个模块使用，把该类库的 jar 包放在模块中，在 Bundle-ClassPath中指明即可。</li><li>如果一个类库被多个模块共用，可以为这个类库单独的创建一个模块，把其它模块需要用到的 Java 包声明为导出的。其它模块声明导入这些类。</li><li>如果类库提供了 SPI 接口，并且利用线程上下文类加载器来加载 SPI 实现的 Java 类，有可能会找不到 Java 类。如果出现了 NoClassDefFoundError异常，首先检查当前线程的上下文类加载器是否正确。通过 <code>Thread.currentThread().getContextClassLoader()</code>就可以得到该类加载器。该类加载器应该是该模块对应的类加载器。如果不是的话，可以首先通过 <code>class.getClassLoader()</code>来得到模块对应的类加载器，再通过 <code>Thread.currentThread().setContextClassLoader()</code>来设置当前线程的上下文类加载器。</li></ul><p>参考：</p><ul><li><a href="https://book.douban.com/subject/24722612/">《深入理解Java虚拟机——JVM高级特性与最佳实践》－周志明</a></li><li><a href="https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code6">深入探讨 Java 类加载器－成富</a></li></ul><p>From: <a href="https://crowhawk.github.io/2017/08/21/jvm_6/">https://crowhawk.github.io/2017/08/21/jvm_6/</a></p>]]>
                    </description>
                    <pubDate>Mon, 15 Jun 2020 13:12:58 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[深入理解JVM(5)——虚拟机类加载机制]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/understanding-the-jvm-5</link>
                    <description>
                            <![CDATA[<h2 id="深入理解jvm5虚拟机类加载机制">深入理解JVM(5)——虚拟机类加载机制</h2><blockquote><p>在Class文件中描述的各种信息，最终都需要加载到虚拟机中之后才能运行和使用。而虚拟机中，而虚拟机如何加载这些Class文件？Class文件中的信息进入到虚拟机中会发生什么变化？本文将逐步解答这些问题。</p></blockquote><p>类从被加载到虚拟机内存中开始，到卸载出内存为止，它的整个生命周期包括以下7个阶段：</p><ul><li><strong>加载（Loading）</strong></li><li><strong>验证（Verification）</strong></li><li><strong>准备（Preparation）</strong></li><li><strong>解析（Resolution）</strong></li><li><strong>初始化（Initialization）</strong></li><li><strong>使用（Using）</strong></li><li><strong>卸载（Unloading）</strong></li></ul><p>其中前五个阶段即为类加载的全过程。在后面会进行详细的介绍。而验证、准备、解析3个部分统称为<strong>连接（Linking）</strong>。这7个阶段的发生顺序如下图：</p><p><img src="https://pic.yupoo.com/crowhawk/2a1c6490/d926d9a2.png" alt="" /></p><p>在上图中，加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的，类的加载过程必须按照这种顺序按部就班地开始（开始而不是完成，这些阶段是互相交叉着进行的，在一个阶段执行过程中就会激活另一个阶段），而解析阶段则不一定：它在某些情况下可以在初始化阶段之后再开始，这是为了支持Java的<strong>运行时绑定（也称为动态绑定或晚期绑定）</strong>。</p><p>对于类加载过程的第一个阶段：加载，jvm规范中并没有进行强制约束其开始时机，可交由jvm的具体实现来自由把握。但是对于初始化阶段，jvm规范严格规定了有且只有下列5种情况必须对类进行**“初始化”**（很自然地，加载、验证、准备需要在此之前开始）：</p><ul><li>遇到<code>new</code>、<code>getstatic</code>、<code>putstatic</code>、<code>invokestatic</code>这四条字节码指令时，如果类没有进行过初始化，则必须先触发其初始化。最常见的生成这4条指令的场景是：<strong>使用new关键字实例化对象</strong>的时候；<strong>读取或设置一个类的静态字段（被final修饰、已在编译期把结果放入常量池的静态字段除外）<strong>的时候；以及</strong>调用一个类的静态方法</strong>的时候。</li><li>使用 <code>java.lang.reflect</code>包的方法对类进行<strong>反射调用</strong>的时候，如果类没有进行初始化，则需要先触发其初始化。</li><li>当初始化一个类的时候，如果发现其<strong>父类</strong>还没有进行过初始化，则需要先触发其父类的初始化。</li><li>当虚拟机启动时，用户需要制定一个要执行的<strong>主类（包含main方法的那个类）</strong>，虚拟机会先初始化这个主类；</li><li>当使用jdk1.7 的<strong>动态语言支持</strong>时，如果一个<code>java.lang.invoke.MethodHandle</code>实例最后的解析结果<code>REF_getStatic</code>, <code>REF_putStatic</code>, <code>REF_invokeStatic</code> 的方法句柄，并且这个方法句柄所对应的类没有进行过初始化，则需要先触发其初始化；</li></ul><p>以上5种场景中的行为称为对一个类进行<strong>主动引用</strong>。除此之外，所有引用类的方式都不会触发初始化，称为<strong>被动引用</strong>。被动引用的常见例子包括：</p><ul><li>通过子类引用<strong>父类的静态字段</strong>，不会导致子类初始化。</li><li>通过<strong>数组定义</strong>来引用类，不会触发此类的初始化，如<code>SuperClass[] sca = new SuperClass[10];</code>。</li><li><strong>常量</strong>在编译阶段会存入调用类的常量池中，本质上并没有直接引用到定义常量的类，因此不会触发定义常量的类的初始化。</li></ul><p><strong>接口的加载过程</strong>和类加载过程略有不同，它们真正的区别在于在前文提到的5种需要开始初始化场景中的第3种：当一个类在初始化时，要求其父类全部都已经初始化过了，但是一个接口在初始化时，并不要求其<strong>父接口</strong>全部都完成了初始化，只有在真正使用到父接口的时候（如引用接口中定义的常量）才会初始化。</p><h3 id="加载">加载</h3><p><strong>加载</strong>是**类加载（Class Loading）**过程的一个阶段，两者不要混淆。虚拟机规范规定了在在加载阶段，jvm需要完成以下三件事情：</p><ul><li>通过一个类的全限定名来获取定义此类的<strong>二进制字节流</strong>。</li><li>将这个字节流所代表的静态存储结构转化为<strong>方法区的运行时存储结构</strong>。</li><li>在内存中生成一个代表这个类的<code>java.lang.Class</code>对象，作为方法区这个类的各种<strong>数据的访问入口</strong>。</li></ul><p>这三点要求不算具体，在jvm实现时灵活度很大。例如上面的第一条，它没有指明二进制字节流要从一个Class文件中获取，准确地说没有指明要从哪里获取、怎样获取。这也为许多Java技术提供了基础，例如：</p><ul><li>从ZIP包读取，这很常见，最终成为日后JAR、EAR、WAR格式的基础。</li><li>从网络中获取，这种场景最典型的应用是Applet。</li><li>运行时计算生成，这种场景使用得最多得就是<strong>动态代理</strong>技术，在<code>java.lang.reflect.Proxy</code>中，就是用了<code>ProxyGenerator.generateProxyClass</code>的代理类的二进制字节流。</li><li>由其他文件生成，典型场景是<strong>JSP应用</strong>，即由JSP文件生成对应的Class类。</li><li>从数据库读取，这种场景相对少见，例如有些<strong>中间件服务器</strong>（如SAP Netweaver）可以选择把程序安装到数据库中来完成程序代码在集群间的分发。</li></ul><p><strong>非数组类的加载</strong></p><p>相对于类加载过程的其他阶段，一个非数组类的加载阶段（准确地说，是加载阶段中<strong>获取类的二进制字节流的动作</strong>）是开发人员可控性最强的，因为加载阶段既可以使用<strong>系统提供的引导类加载器</strong>完成，也可以由<strong>用户自定义的类加载器</strong>完成，通过自定义类加载器去控制字节流的获取方式，即重写一个类加载器的<code>loadClass()</code>方法。关于类加载器的内容将在系列的下一篇文章中介绍。</p><p><strong>数组类的加载</strong></p><p>**数组类本身不通过类加载器创建，它是由jvm直接创建的。**但数组类的元素类型（Element Type，指的是数组去掉所有维度的类型）最终是要靠类加载器去创建，一个数据类C的创建过程遵循以下规则：</p><ul><li>如果数组的<strong>组件类型</strong>（ComponentType，指的是数组去掉一个维度的类型）是<strong>引用类型</strong>，就递归采用本节中定义的加载过程去加载此组件类型，<strong>数组类将在加载该组件类型的类加载器的类名称空间上被标识</strong>（这很重要，在下一篇文章中会讲到，一个类必须与类加载器一起确定唯一性）。</li><li>如果数组的<strong>组件类型不是引用类型</strong>（例如int[]数组），Java虚拟机将会把数组类标记为与<strong>引导类加载器</strong>关联。</li><li>数组类的<strong>可见性</strong>与它的<strong>组件类型</strong>的可见性一致，如果组件类型不是引用类型，那数组类的可见性将默认为public。</li></ul><p><strong>加载阶段完成后</strong>，虚拟机<strong>外部的二进制字节流</strong>就按照虚拟机所需的格式<strong>存储在方法区之中</strong>，方法区的数据存储格式由虚拟机实现自行定义，虚拟机规范未规定此区域的具体数据结构。然后在内存中<strong>实例化一个<code>java.lang.Class</code>类的对象</strong>（并无明确规定是在Java 堆中，<strong>对于HotSpot虚拟机而言，Class对象比较特殊，它虽是对象，但存放在方法区里</strong>），这个对象将作为程序访问方法区中的这些类型数据的外部接口。</p><h3 id="验证">验证</h3><p>验证是连接阶段的第一步，这一阶段的目的是确保<strong>输入的Class文件的字节流能正确地解析并存储于方法区之内</strong>，格式上符合描述一个Java类型信息的要求，并且不会危害虚拟机自身的安全。验证阶段是否严谨，直接决定了Java虚拟机是否能承受恶意代码的攻击。 从整体上看，验证阶段大致上会完成下面四个阶段的检验动作：文件格式验证、元数据验证、字节码验证、符号引用验证。</p><p><strong>1. 文件格式验证</strong></p><p>第一阶段要验证字节流是否符合Class文件格式的规范，并且能被当前版本的虚拟机处理。这一阶段可能包括下面这些验证点：</p><ul><li>是否以魔数0xCAFEBABE开头。</li><li>主次版本号是否在当前虚拟机的处理范围之内</li><li>常量池的常量中是否有不被支持的常量类型（tag标志）。</li><li>指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。</li><li>Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。 ……</li></ul><p>这阶段的验证是<strong>基于二进制字节流</strong>进行的，只有通过了这个阶段的验证后，字节流才会<strong>进入方法区中进行存储</strong>，所以后面的3个验证阶段全部是基于方法区的存储结构进行的，不会再直接操作字节流。</p><p><strong>2. 元数据验证</strong></p><p>第二阶段是对<strong>字节码描述的信息（即类的元数据信息）<strong>进行</strong>语义分析</strong>，以保证其描述的信息符合Java语言规范的要求。例如下面这些验证点：</p><ul><li>该类是否有父类（除了<code>java.lang.Object</code>之外，所有的类都应有父类）</li><li>该类的父类是否继承了不允许被继承的类（final修饰的类）</li><li>若此类不是抽象类，是否实现了其父类或接口之中要求实现的所有方法 ……</li></ul><p>该阶段的主要目的是对类的元数据信息进行<strong>语义检验</strong>，保证不存在不符合Java语言规范的元数据信息。</p><p><strong>3. 字节码验证</strong></p><p>第三阶段的主要目的是<strong>进行数据流和控制流分析</strong>，确定程序<strong>语义</strong>是合法的、符合逻辑的。在<strong>第二阶段</strong>对元数据信息中的<strong>数据类型</strong>做完校验之后，这个阶段将对<strong>类的方法体</strong>进行校验分析，以保证<strong>被校验类的方法</strong>在运行时不会做出危害虚拟机安全的行为。例如：</p><ul><li>保证任意时刻操作数栈的<strong>数据类型</strong>与<strong>指令代码序列</strong>都能配合工作。</li><li>保证<strong>跳转指令</strong>不会跳转到方法体以外的字节码指令上。</li><li>保证<strong>方法体中类型转换</strong>是有效的，例如子类对象可以赋值给父类数据类型，但父类对象赋值给子类数据类型是危险和不合法的。 ……</li></ul><p><strong>4. 符号引用验证</strong></p><p>最后一个阶段的校验发生在虚拟机将<strong>符号引用</strong>转化为<strong>直接引用</strong>的时候，<strong>这个转化动作将在连接的第三阶段——解析阶段中发生</strong>。符号引用验证可以看做是<strong>对类自身以外（常量池中的各种符号引用）的信息进行匹配性校验</strong>，通常需要校验下列内容：</p><ul><li>符号引用中通过字符串描述的全限定名是否能找到对应的类。</li><li>指定的类中是否存在符合描述符与简单名称描述的方法与字段。</li><li>符号引用中的类、字段、方法的<strong>访问性</strong>（private、protected、public、default）是否可被当前类访问。 ……</li></ul><p>符号引用的目的是<strong>确保解析动作能正常执行</strong>。</p><p>对于jvm的类加载机制来说，验证阶段是一个非常重要但不是一定必要（因为对运行期没有影响）的阶段。如果所运行的全部代码都已经被反复验证过，那么在实施阶段就可以考虑使用<code>-Xverify:none</code>参数来关闭大部分的类验证措施，以缩短虚拟机类加载的时间。</p><h3 id="准备">准备</h3><p>准备阶段的主要任务是如下两点：</p><ul><li><strong>为类变量分配内存</strong></li><li><strong>设置类变量初始值</strong></li></ul><p>这些变量所使用的内存都将在<strong>方法区</strong>中分配。</p><p>首先，在准备阶段进行内存分配的仅包括<strong>类变量（被static修饰的变量）</strong>，而不包括<strong>实例变量</strong>，实例变量将会在<strong>对象实例化</strong>时随着对象一起分配在Java堆中。</p><p>其次，这里所说的初始值“通常情况”下是数据类型的零值，假设一个类变量的定义为：</p><pre><code>public static int value = 123;</code></pre><p>那变量value在准备阶段过后的初始值为0而不是123，因为这时候尚未开始执行任何Java方法，而把value赋值为123的<code>putstatic</code>指令是程序被编译后，存放于类构造器<code>&lt;clinit&gt;()</code>方法之中，所以把value赋值为123的动作在初始化阶段才会执行。 值得注意的是，如果类字段的字段属性中存在ConstantValue属性，那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值，假设上面类变量value的定义变为：</p><pre><code>public static final int value = 123;</code></pre><p><strong>编译时</strong>Javac将会为value生成ConstantValue属性，在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。</p><h3 id="解析">解析</h3><p>解析阶段是虚拟机将<strong>常量池</strong>内的<strong>符号引用</strong>替换为<strong>直接引用</strong>的过程。符号引用和直接引用的关联如下：</p><ul><li><strong>符号引用（Symbol References）</strong>： 符号引用<strong>以一组符号来描述所引用的目标</strong>，<strong>符号</strong>可以是<strong>任何形式的字面量</strong>，只要使用时能无歧义地定位到目标即可。<strong>符号引用与虚拟机实现的内存布局无关</strong>，引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同，但是它们能接受的符号引用必须一致，因为<strong>符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中</strong>。</li><li><strong>直接引用（Direct References）</strong>： 直接引用可以是<strong>直接目标的指针</strong>、<strong>相对偏移量</strong>或是一个<strong>能间接定位到目标的句柄</strong>。直接引用是和虚拟机实现的内存布局有关的，同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用，那么引用的目标必定已经在内存中存在。</li></ul><p>虚拟机规范并未规定解析动作发生的具体时间，仅要求在执行anewarray、checkcast、getfield、getstatic、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic这13个用于操作符号引用的字节码指令之前，先对它们所使用的符号引用进行解析。所以虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中的符号进行解析，还是等到一个符号引用将要被使用前才去解析它。</p><p><strong>对同一个符号引用进行多次解析请求</strong>是很常见的，除 invokedynamic 指令外（ invokedynamic指令是用于动态语言支持的，它所对应的引用称为**“动态调用点限定符”<strong>，必须等到程序实际运行到这条指令的时候，解析动作才能进行）虚拟机实现可能会对第一次解析的结果进行</strong>缓存（将直接引用保存在运行时常量池中）**，无论是否真正执行了多次解析动作，虚拟机实现必须保证在同一个实体中，如果一个符号引用之前已经被成功解析过，后续的引用解析请求就应当一直成功，反之亦然。</p><p>解析动作主要针对以下7类符号引用</p><ul><li>类或接口</li><li>字段</li><li>类方法（静态方法）</li><li>接口方法</li><li>方法类型</li><li>方法句柄</li><li>调用点限定符</li></ul><p>其中后三种与java的动态语言支持息息相关。</p><h3 id="初始化">初始化</h3><p>类初始化阶段是“类加载过程”中最后一步，在之前的阶段，除了在加载阶段用户应用程序可以通过自定义类加载器参与之外，其它动作完全由虚拟机主导和控制，直到初始化阶段，才真正开始<strong>执行类中定义的Java程序代码（或者说是字节码）</strong>。</p><p>在准备阶段，变量已经赋过一次系统要求的初始值，而在初始化阶段，根据程序员通过程序制定的主观计划去初始化类变量和其它资源，简单说，<strong>初始化阶段即虚拟机执行类构造器<code>&lt;clinit&gt;()</code>方法的过程</strong>。</p><p>下面来详细讲解<code>&lt;clinit&gt;()</code>方法是怎么生成的，首先来了解此方法执行过程中可能会影响到程序运行行为的特点和细节：</p><ul><li><code>&lt;clinit&gt;()</code>方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块（static{} 块）中的语句合并产生的，编译器收集的顺序由语句在源文件中出现的顺序决定，特别注意的是，静态语句块只能访问到定义在它之前的类变量，定义在它之后的类变量只能赋值，不能访问。例如以下代码：</li></ul><pre><code class="language-java">    public class Test {        static {            i = 0;  //  给变量复制可以正常编译通过            System.out.print(i);  // 这句编译器会提示“非法向前引用”          }        static int i = 1;    }</code></pre><ul><li><code>&lt;clinit&gt;()</code>方法与类的构造函数（或者说实例构造器<code>&lt;init&gt;()</code> 方法）不同，不需要显式的调用父类的()方法。虚拟机会自动保证在子类的<code>&lt;clinit&gt;()</code>方法运行之前，父类的<code>&lt;clinit&gt;()</code>方法已经执行结束。因此虚拟机中第一个执行<code>&lt;clinit&gt;()</code>方法的类肯定为<code>java.lang.Object</code>。</li><li>由于父类的<code>&lt;clinit&gt;()</code>方法先执行，也就意味着父类中定义的静态语句块要优于子类的变量赋值操作。例如以下代码：</li></ul><pre><code class="language-java">    static class Parent {            public static int A = 1;            static {                A = 2;            }    }    static class Sub extends Parent {            public static int B = A;    }    public static void main(String[] args) {            System.out.println(Sub.B);//输出结果是父类中的静态变量值A，也就是2    }</code></pre><ul><li><code>&lt;clinit&gt;()</code>方法对于类或接口不是必须的，如果一个类中不包含静态语句块，也没有对类变量的赋值操作，编译器可以不为该类生成<code>&lt;clinit&gt;()</code>方法。</li><li>接口中不可以使用静态语句块，但仍然有类变量初始化的赋值操作，因此接口与类一样都会生成<code>&lt;clinit&gt;()</code>方法。但接口与类不同的是，执行接口的<code>&lt;clinit&gt;()</code>方法不需要先执行父接口的<code>&lt;clinit&gt;()</code>方法。只有当父接口中定义的变量使用时，父接口才会初始化。另外，接口的实现类在初始化时也一样不会执行接口的<code>&lt;clinit&gt;()</code>方法。</li><li>虚拟机会保证一个类的<code>&lt;clinit&gt;()</code>方法在多线程环境下被正确的加锁和同步，如果多个线程同时初始化一个类，只会有一个线程执行这个类的<code>&lt;clinit&gt;()</code>方法，其它线程都会阻塞等待，直到活动线程执行<code>&lt;clinit&gt;()</code>方法完毕。如果在一个类的<code>&lt;clinit&gt;()</code>方法中有耗时的操作，就可能造成多个进程阻塞，在实际过程中此种阻塞很隐蔽。</li></ul><p>资料：</p><ul><li><a href="https://book.douban.com/subject/24722612/">《深入理解Java虚拟机——JVM高级特性与最佳实践》－周志明</a></li></ul><p>From: <a href="https://crowhawk.github.io/2017/08/21/jvm_5/">https://crowhawk.github.io/2017/08/21/jvm_5/</a></p>]]>
                    </description>
                    <pubDate>Mon, 15 Jun 2020 13:01:13 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[深入理解JVM(4)——如何优化Java GC「译」]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/understanding-the-jvm-4</link>
                    <description>
                            <![CDATA[<h2 id="深入理解jvm4如何优化java-gc译">深入理解JVM(4)——如何优化Java GC「译」</h2><blockquote><p>本文翻译自Sangmin Lee发表在<a href="http://www.cubrid.org/blog">Cubrid</a>上的”Become a Java GC Expert”系列文章的第三篇<a href="http://www.cubrid.org/blog/how-to-tune-java-garbage-collection">《How to Tune Java Garbage Collection》</a>,本文的作者是韩国人，写在JDK 1.8发布之前，虽然有些地方有些许过时，但整体内容还是非常有价值的。译者此前也看到有人翻译了本文，发现其中有许多错漏生硬和语焉不详之处，因此决定自己翻译一份，供大家分享。 本文系本人独立翻译，转载请注明出处。</p></blockquote><p>本文是“成为Java GC专家”系列文章的第三篇，在系列的第一篇文章<a href="http://www.cubrid.org/blog/understanding-java-garbage-collection">《理解Java GC》</a>中，我们了解到了不同GC算法的执行过程、GC的工作原理、新生代和老年代的概念、JDK 7中你需要了解的5种GC类型以及每一种GC对性能的影响。</p><p>在系列的第二篇文章<a href="http://www.cubrid.org/blog/how-to-monitor-java-garbage-collection">《如何监控Java GC》</a>中笔者已经解释了JVM进行实时GC的原理、监控GC的方法以及可以使这一过程更加迅速高效的工具。</p><p>在第三篇文章中，笔者将基于实际生产环境中的案例，介绍几个GC优化的最佳参数设置。在此我们假设你已经理解了本系列前两篇文章的内容，因此为了更深入的理解本文所讲内容，我建议你在阅读本篇文章之前先仔细阅读这两篇文章。</p><p>或者更准确地说，GC优化对Java基础服务来说是必要的吗？答案是否定的，事实上GC优化对Java基础服务来说在有些场合是可以省去的，但前提是这些正在运行的Java系统，必须包含以下参数或行为：</p><ul><li>内存大小已经通过 <strong>-Xms</strong> 和 <strong>-Xmx</strong> 参数指定过</li><li>运行在server模式下（使用 <strong>-server</strong> 参数）</li><li>系统中没有残留超时日志之类的错误日志</li></ul><p>换句话说，如果你在运行时没有手动设置内存大小并且打印出了过多的超时日志，那你就需要对系统进行GC优化。</p><p>不过你需要时刻谨记一句话：<strong>GC tuning is the last task to be done.</strong></p><p>现在来想一想GC优化的最根本原因，垃圾收集器的工作就是清除Java创建的对象，垃圾收集器需要清理的对象数量以及要执行的GC数量均取决于已创建的对象数量。因此，为了使你的系统在GC上表现良好，首先需要减少创建对象的数量。</p><p>俗话说“冰冻三尺非一日之寒”，我们在编码时要首先要把下面这些小细节做好，否则一些琐碎的不良代码累积起来将让GC的工作变得繁重而难于管理：</p><ul><li>使用<code>StringBuilder</code>或<code>StringBuffer</code>来代替<code>String</code></li><li>尽量少输出日志</li></ul><p>尽管如此，仍然会有我们束手无策的情况。XML和JSON解析过程往往占用了最多的内存，即使我们已经尽可能地少用String、少输出日志，仍然会有大量的临时内存（大约10-100MB）被用来解析XML或JSON文件，但我们又很难弃用XML和JSON。在此，你只需要知道这一过程会占据大量内存即可。</p><p>如果在经过几次重复的优化后应用程序的内存用量情况有所改善，那么久可以启动GC优化了。</p><p>笔者总结了GC优化的两个目的：</p><ol><li><strong>将进入老年代的对象数量降到最低</strong></li><li><strong>减少Full GC的执行时间</strong></li></ol><p>除了可以在JDK 7及更高版本中使用的G1收集器以外，其他分代GC都是由Oracle JVM提供的。关于分代GC，就是对象在Eden区被创建，随后被转移到Survivor区，在此之后剩余的对象会被转入老年代。也有一些对象由于占用内存过大，在Eden区被创建后会直接被传入老年代。老年代GC相对来说会比新生代GC更耗时，因此，减少进入老年代的对象数量可以显著降低Full GC的频率。你可能会以为减少进入老年代的对象数量意味着把它们留在新生代，事实正好相反，新生代内存的大小是可以调节的。</p><p>Full GC的执行时间比Minor GC要长很多，因此，如果在Full GC上花费过多的时间（超过1s），将可能出现超时错误。</p><ul><li>如果<strong>通过减小老年代内存来减少Full GC时间</strong>，可能会引起<code>OutOfMemoryError</code>或者导致Full GC的频率升高。</li><li>另外，如果<strong>通过增加老年代内存来降低Full GC的频率</strong>，Full GC的时间可能因此增加。</li></ul><p>因此，<strong>你需要把老年代的大小设置成一个“合适”的值</strong>。</p><p>正如我在系列的第一篇文章<a href="http://www.cubrid.org/blog/understanding-java-garbage-collection">《理解Java GC》</a>末尾提到的，不要幻想着“如果有人用他设置的GC参数获取了不错的性能，我们为什么不复制他的参数设置呢？”，因为对于不用的Web服务，它们创建的对象大小和生命周期都不相同。</p><p>举一个简单的例子，如果一个任务的执行条件是A，B，C，D和E，另一个完全相同的任务执行条件只有A和B，那么哪一个任务执行速度更快呢？作为常识来讲，答案很明显是后者。</p><p>Java GC参数的设置也是这个道理，设置好几个参数并不会提升GC执行的速度，反而会使它变得更慢。<strong>GC优化的基本原则</strong>是将不同的GC参数应用到两个及以上的服务器上然后比较它们的性能，然后将那些被证明可以提高性能或减少GC执行时间的参数应用于最终的工作服务器上。</p><p>下面这张表展示了与内存大小相关且会影响GC性能的GC参数</p><p><strong>表1：GC优化需要考虑的JVM参数</strong></p><table><thead><tr><th><strong>类型</strong></th><th><strong>参数</strong></th><th><strong>描述</strong></th></tr></thead><tbody><tr><td>堆内存大小</td><td><code>-Xms</code></td><td>启动JVM时堆内存的大小</td></tr><tr><td> </td><td><code>-Xmx</code></td><td>堆内存最大限制</td></tr><tr><td>新生代空间大小</td><td><code>-XX:NewRatio</code></td><td>新生代和老年代的内存比</td></tr><tr><td> </td><td><code>-XX:NewSize</code></td><td>新生代内存大小</td></tr><tr><td> </td><td><code>-XX:SurvivorRatio</code></td><td>Eden区和Survivor区的内存比</td></tr></tbody></table><p>笔者在进行GC优化时最常用的参数是<code>-Xms</code>,<code>-Xmx</code>和<code>-XX:NewRatio</code>。<code>-Xms</code>和<code>-Xmx</code>参数通常是必须的，所以<code>NewRatio</code>的值将对GC性能产生重要的影响。</p><p>有些人可能会问<strong>如何设置永久代内存大小</strong>，你可以用<code>-XX:PermSize</code>和<code>-XX:MaxPermSize</code>参数来进行设置，但是要记住，只有当出现<code>OutOfMemoryError</code>错误时你才需要去设置永久代内存。</p><p>还有一个会影响GC性能的因素是<a href="https://crowhawk.github.io/2017/08/15/jvm_3/">垃圾收集器的类型</a>,下表展示了关于GC类型的可选参数（基于JDK 6.0）：</p><p><strong>表2：GC类型可选参数</strong></p><table><thead><tr><th><strong>GC类型</strong></th><th><strong>参数</strong></th><th><strong>备注</strong></th></tr></thead><tbody><tr><td>Serial GC</td><td><code>-XX:+UseSerialGC</code></td><td>----</td></tr><tr><td>Parallel GC</td><td><code>-XX:+UseParallelGC&lt;/br&gt;-XX:ParallelGCThreads=value</code></td><td>----</td></tr><tr><td>Parallel Compacting GC</td><td>-XX:+UseParallelOldGC</td><td>----</td></tr><tr><td>CMS GC</td><td><code>-XX:+UseConcMarkSweepGC</code><br><code>-XX:+UseParNewGC</code><br><code>-XX:+CMSParallelRemarkEnabled</code><br><code>-XX:CMSInitiatingOccupancyFraction=value</code><br><code>-XX:+UseCMSInitiatingOccupancyOnly</code></td><td>----</td></tr><tr><td>G1</td><td><code>-XX:+UnlockExperimentalVMOptions</code><br><code>-XX:+UseG1GC</code></td><td>在JDK 6中这两个参数必须配合使用</td></tr></tbody></table><p>除了G1收集器外，可以通过设置上表中每种类型第一行的参数来切换GC类型，最常见的非侵入式GC就是Serial GC，它针对客户端系统进行了特别的优化。</p><p>会影响GC性能的参数还有很多，但是上述的参数会带来最显著的效果，请切记，设置太多的参数并不一定会提升GC的性能。</p><p>GC优化的过程和大多数常见的提升性能的过程相似，下面是笔者使用的流程：</p><h4 id="1监控gc状态">1.监控GC状态</h4><p>你需要监控GC从而检查系统中运行的GC的各种状态，具体方法请查看系列的第二篇文章<a href="http://www.cubrid.org/blog/how-to-monitor-java-garbage-collection">《如何监控Java GC》</a></p><h4 id="2分析监控结果后决定是否需要优化gc">2.分析监控结果后决定是否需要优化GC</h4><p>在检查GC状态后，你需要分析监控结构并决定是否需要进行GC优化。如果分析结果显示运行GC的时间只有0.1-0.3秒，那么就不需要把时间浪费在GC优化上，但如果运行GC的时间达到1-3秒，甚至大于10秒，那么GC优化将是很有必要的。</p><p>但是，如果你已经分配了大约10GB内存给Java，并且这些内存无法省下，那么就无法进行GC优化了。在进行GC优化之前，你需要考虑为什么你需要分配这么大的内存空间，如果你分配了1GB或2GB大小的内存并且出现了<code>OutOfMemoryError</code>，那你就应该执行<strong>堆转储（heap dump）</strong> 来消除导致异常的原因。</p><blockquote><p>注意：</p></blockquote><blockquote><p><strong>堆转储（heap dump）</strong> 是一个用来检查Java内存中的对象和数据的内存文件。该文件可以通过执行JDK中的<code>jmap</code>命令来创建。在创建文件的过程中，所有Java程序都将暂停，因此，不要再系统执行过程中创建该文件。</p></blockquote><blockquote><p>你可以在互联网上搜索heap dump的详细说明。对于韩国读者，可以直接参考我去年发布的书：<a href="http://book.naver.com/bookdb/book_detail.nhn?bid=6654751">《The story of troubleshooting for Java developers and system operators》</a> (Sangmin Lee, Hanbit Media, 2011, 416 pages)</p></blockquote><h4 id="3设置gc类型内存大小">3.设置GC类型/内存大小</h4><p>如果你决定要进行GC优化，那么你需要选择一个GC类型并且为它设置内存大小。此时如果你有多个服务器，请如上文提到的那样，在每台机器上设置不同的GC参数并分析它们的区别。</p><h4 id="4分析结果">4.分析结果</h4><p>在设置完GC参数后就可以开始收集数据，请在收集至少24小时后再进行结果分析。如果你足够幸运，你可能会找到系统的最佳GC参数。如若不然，你还需要分析输出日志并检查分配的内存，然后需要通过不断调整GC类型/内存大小来找到系统的最佳参数。</p><h4 id="5如果结果令人满意将参数应用到所有服务器上并结束gc优化">5.如果结果令人满意，将参数应用到所有服务器上并结束GC优化</h4><p>如果GC优化的结果令人满意，就可以将参数应用到所有服务器上，并停止GC优化。</p><p>在下面的章节中，你将会看到上述每一步所做的具体工作。</p><p>在运行中的Web应用服务器（Web Application Server,WAS）上查看GC状态的最佳方式就是使用<code>jstat</code>命令。笔者在<a href="http://www.cubrid.org/blog/how-to-monitor-java-garbage-collection">《如何监控Java GC》</a>中已经介绍过了<code>jstat</code>命令，所以在本篇文章中我将着重关注数据部分。</p><p>下面的例子展示了某个还没有执行GC优化的JVM的状态（虽然它并不是运行服务器）。</p><pre><code>$ jstat -gcutil 21719 1sS0    S1    E    O    P    YGC    YGCT    FGC    FGCT GCT48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.67348.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673</code></pre><p>我们先看一下YGC（从应用程序启动到采样时发生 Young GC 的次数）和YGCT（从应用程序启动到采样时 Young GC 所用的时间(秒)），计算YGCT/YGC会得出，平均每次新生代的GC耗时50ms，这是一个很小的数字，通过这个结果可以看出，我们大可不必关注新生代GC对GC性能的影响。</p><p>现在来看一下FGC（ 从应用程序启动到采样时发生 Full GC 的次数）和FGCT（从应用程序启动到采样时 Full GC 所用的时间(秒)），计算FGCT/FGC会得出，平均每次老年代的GC耗时19.68s。有可能是执行了三次Full GC，每次耗时19.68s，也有可能是有两次只花了1s,另一次花了58s。不管是哪一种情况，GC优化都是很有必要的。</p><p>使用<code>jstat</code>命令可以很容易地查看GC状态，但是分析GC的最佳方式是加上<code>-verbosegc</code>参数来生成日志。在之前的文章中笔者已经解释了如何分析这些日志。<strong>HPJMeter</strong>是笔者最喜欢的用于分析<code>-verbosegc</code>生成的日志的工具，它简单易用，使用HPJmeter可以很容易地查看GC执行时间以及GC发生频率。</p><p>此外，如果GC执行时间满足下列所有条件，就没有必要进行GC优化了：</p><ul><li>Minor GC执行非常迅速（50ms以内）</li><li>Minor GC没有频繁执行（大约10s执行一次）</li><li>Full GC执行非常迅速（1s以内）</li><li>Full GC没有频繁执行（大约10min执行一次）</li></ul><p>括号中的数字并不是绝对的，它们也随着服务的状态而变化。有些服务可能要求一次Full GC在0.9s以内，而有些则会放得更宽一些。因此，对于不同的服务，需要按照不同的标准考虑是否需要执行GC优化。</p><p>当检查GC状态时，不能只查看Minor GC和Full GC的时间，还必须要<strong>关注GC执行的次数</strong>。如果新生代空间太小，Minor GC将会非常频繁地执行（有时每秒会执行一次，甚至更多）。此外，传入老年代的对象数目会上升，从而导致Full GC的频率升高。因此，在执行<code>jstat</code>命令时，请使用<code>-gccapacity</code>参数来查看具体占用了多少空间。</p><h4 id="设置gc类型">设置GC类型</h4><p>Oracle JVM有5种垃圾收集器，但是在JDK 7以前的版本中，你只能在Parallel GC, Parallel Compacting GC 和CMS GC之中选择，至于具体选择哪个，则没有具体的原则和规则。</p><p>既然这样的话，<strong>我们如何来选择GC呢？<strong>最好的方法是把三种都用上，但是有一点必须明确——CMS GC通常比其他并行（Parallel）GC都要快（这是因为CMS GC是并发的GC），如果确实如此，那只选择CMS GC就可以了，不过CMS GC也不总是更快，当出现</strong>concurrent mode failure</strong>时，CMS GC就会比并行GC更慢了。</p><p><strong>Concurrent mode failure</strong></p><p>现在让我们来深入地了解一下<strong>concurrent mode failure</strong>。</p><p>并行GC和CMS GC的最大区别是并行GC采用“标记-整理”(Mark-Compact)算法而CMS GC采用“标记-清除”(Mark-Sweep)算法（具体内容可参照译者的文章<a href="https://crowhawk.github.io/2017/08/10/jvm_2/">《GC算法与内存分配策略》</a>）,compact步骤就是通过移动内存来消除内存碎片，从而消除分配的内存之间的空白区域。</p><p>对于并行GC来说，无论何时执行Full GC，都会进行compact工作，这消耗了太多的时间。不过在执行完Full GC后，下次内存分配将会变得更快（因为直接顺序分配相邻的内存）。</p><p>相反，CMS GC没有compact的过程，因此CMS GC运行的速度更快。但是也是由于没有整理内存，在进行磁盘清理之前，内存中会有很多零碎的空白区域，这也导致没有足够的空间分配给大对象。例如，在老年代还有300MB可用空间，但是连一个10MB的对象都没有办法被顺序存储在老年代中，在这种情况下，会报出 <strong>“concurrent mode failure”</strong> 的 warning，然后系统执行compact操作。但是CMS GC在这种情况下执行的compact操作耗时要比并行GC高很多，并且这还会导致另一个问题，关于 <strong>“concurrent mode failure”</strong> 的详细说明，可用参考Oracle工程师撰写的<a href="https://blogs.oracle.com/poonam/understanding-cms-gc-logs">《Understanding CMS GC Logs》</a>。</p><p>综上所述，你需要根据你的系统情况为其选择一个最适合的GC类型。</p><p>每个系统都有最适合它的GC类型等着你去寻找，如果你有6台服务器，我建议你每两个服务器设置相同的参数，然后加上<code>-verbosegc</code>参数再分析结果。</p><h4 id="设置内存大小">设置内存大小</h4><p>下面展示了内存大小、GC运行次数和GC运行时间之间的关系：</p><p><strong>大内存空间</strong></p><ul><li>减少了GC的次数</li><li>提高了GC的运行时间</li></ul><p><strong>小内存空间</strong></p><ul><li>增多了GC的次数</li><li>降低了GC的运行时间</li></ul><p>关于如何设置内存的大小，没有一个标准答案，如果服务器资源充足并且Full GC能在1s内完成，把内存设为10GB也是可以的，但是大部分服务器并不处在这种状态中，当内存设为10GB时，Full GC会耗时10-30s,具体的时间自然与对象的大小有关。</p><p>既然如此，<strong>我们该如何设置内存大小呢？</strong> 通常我推荐设为500MB，这不是说你要通过<code>-Xms500m</code>和<code>-Xmx500m</code>参数来设置WAS内存。根据GC优化之前的状态，如果Full GC后还剩余300MB的空间，那么把内存设为1GB是一个不错的选择（300MB（默认程序占用）+ 500MB（老年代最小空间）+200MB（空闲内存））。这意味着你需要为老年代设置至少500MB空间，因此如果你有三个运行服务器，可以把它们的内存分别设置为1GB，1.5GB，2GB，然后检查结果。</p><p>理论上来说，GC执行速度应该遵循1GB&gt; 1.5GB&gt; 2GB，1GB内存时GC执行速度最快。然而，理论上的1GB内存Full GC消耗1s、2GB内存Full GC消耗2 s在现实里是无法保证的，实际的运行时间还依赖于服务器的性能和对象大小。因此，最好的方法是创建尽可能多的测量数据并监控它们。</p><p>在设置内存空间大小时，你还需要设置一个参数：<code>NewRatio</code>。<code>NewRatio</code>的值是新生代和老年代空间大小的比例。如果<code>XX:NewRatio=1</code>，则新生代空间:老年代空间=1:1，如果堆内存为1GB，则新生代:老年代=500MB:500MB。如果<code>NewRatio</code>等于2，则新生代:老年代=1:2，因此，<code>NewRatio</code>的值设置得越大，则老年代空间越大，新生代空间越小。</p><p>你可能会认为把<code>NewRatio</code>设为1会是最好的选择，然而事实并非如此，根据笔者的经验，当<code>NewRatio</code>设为2或3时，整个GC的状态表现得更好。</p><p>**完成GC优化最快地方法是什么？**答案是比较性能测试的结果。为了给每台服务器设置不同的参数并监控它们，最好查看的是一或两天后的数据。当通过性能测试来进行GC优化时，你需要在不同的测试时保证它们有相同的负载和运行环境。然而，即使是专业的性能测试人员，想精确地控制负载也很困难，并且需要大量的时间准备。因此，更加方便容易的方式是直接设置参数来运行，然后等待运行的结果（即使这需要消耗更多的时间）。</p><p>在设置了GC参数和<code>-verbosegc</code>参数后，可以使用tail命令确保日志被正确地生成。如果参数设置得不正确或日志未生成，那你的时间就被白白浪费了。如果日志收集没有问题的话，在收集一或两天数据后再检查结果。最简单的方法是把日志从服务器移到你的本地PC上，然后用<strong>HPJMeter</strong>分析数据。</p><p>在分析结果时，请关注下列几点（这个优先级是笔者根据自己的经验拟定的，我认为选取GC参数时应考虑的最重要的因素是Full GC的运行时间。）：</p><ul><li>单次Full GC运行时间</li><li>单次Minor GC运行时间</li><li>Full GC运行间隔</li><li>Minor GC运行间隔</li><li>整个Full GC的时间</li><li>整个Minor GC的运行时间</li><li>整个GC的运行时间</li><li>Full GC的执行次数</li><li>Minor GC的执行次数</li></ul><p>找到最佳的GC参数是件非常幸运的，然而在大多数时候，我们并不会如此幸运，在进行GC优化时一定要小心谨慎，因为当你试图一次完成所有的优化工作时，可能会出现<code>OutOfMemoryError</code>错误。</p><p>到目前为止，我们一直在从理论上介绍GC优化，现在是时候将这些理论付诸实践了，我们将通过几个例子来更深入地理解GC优化。</p><h4 id="示例1">示例1</h4><p>下面这个例子是针对<strong>Service S</strong>的优化，对于最近刚开发出来的Service S，执行Full GC需要消耗过多的时间。</p><p>现在看一下执行<code>jstat -gcutil</code>的结果</p><pre><code>S0 S1 E O P YGC YGCT FGC FGCT GCT12.16 0.00 5.18 63.78 20.32 54 2.047 5 6.946 8.993</code></pre><p>左边的Perm区的值对于最初的GC优化并不重要，而YGC参数的值更加对于这次优化更为重要。</p><p>平均执行一次Minor GC和Full GC消耗的时间如下表所示：</p><p><strong>表3：Service S的Minor GC 和Full GC的平均执行时间</strong></p><table><thead><tr><th><strong>GC类型</strong></th><th><strong>GC执行次数</strong></th><th><strong>GC执行时间</strong></th><th><strong>平均值</strong></th></tr></thead><tbody><tr><td>Minor GC</td><td>54</td><td>2.047s</td><td>37ms</td></tr><tr><td>Full GC</td><td>5</td><td>6.946s</td><td>1.389s</td></tr></tbody></table><p><strong>37ms</strong>对于Minor GC来说还不赖，但1.389s对于Full GC来说意味着当GC发生在数据库Timeout设置为1s的系统中时，可能会频繁出现超时现象。</p><p>首先，你需要检查开始GC优化前内存的使用情况。使用<code>jstat -gccapacity</code>命令可以检查内存用量情况。在笔者的服务器上查看到的结果如下：</p><pre><code>NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC PGCMN PGCMX PGC PC YGC FGC212992.0 212992.0 212992.0 21248.0 21248.0 170496.0 1884160.0 1884160.0 1884160.0 1884160.0 262144.0 262144.0 262144.0 262144.0 54 5</code></pre><p>其中的关键值如下：</p><ul><li>新生代内存用量：212,992 KB</li><li>老年代内存用量：1,884,160 KB</li></ul><p>因此，除了永久代以外，被分配的内存空间加起来有2GB，并且新生代：老年代=1：9，为了得到比使用<code>jstat</code>更细致的结果，还需加上<code>-verbosegc</code>参数获取日志，并把三台服务器按照如下方式设置（除此以外没有使用任何其他参数）：</p><ul><li>NewRatio=2</li><li>NewRatio=3</li><li>NewRatio=4</li></ul><p>一天后我得到了系统的GC log，幸运的是，在设置完NewRatio后系统没有发生任何Full GC。</p><p>**这是为什么呢？**这是因为大部分对象在创建后很快就被回收了，所有这些对象没有被传入老年代，而是在新生代就被销毁回收了。</p><p>在这样的情况下，就没有必要去改变其他的参数值了，只要选择一个最合适的<code>NewRatio</code>值即可。那么，**如何确定最佳的NewRatio值呢？**为此，我们分析一下每种<code>NewRatio</code>值下Minor GC的平均响应时间。</p><p>在每种参数下Minor GC的平均响应时间如下：</p><ul><li>NewRatio=2：45ms</li><li>NewRatio=3：34ms</li><li>NewRatio=4：30ms</li></ul><p>我们可以根据GC时间的长短得出NewRatio=4是最佳的参数值（尽管NewRatio=4时新生代空间是最小的）。在设置完GC参数后，服务器没有发生Full GC。</p><p>为了说明这个问题，下面是服务执行一段时间后执行<code>jstat –gcutil</code>的结果:</p><pre><code>S0 S1 E O P YGC YGCT FGC FGCT GCT8.61 0.00 30.67 24.62 22.38 2424 30.219 0 0.000 30.219</code></pre><p>你可能会认为是服务器接收的请求少才使得GC发生的频率较低，实际上，虽然Full GC没有执行过，但Minor GC被执行了2424次。</p><h4 id="示例2">示例2</h4><p>这是一个Service A的例子。我们通过公司内部的应用性能管理系统（APM）发现JVM暂停了相当长的时间（超过8秒），因此我们进行了GC优化。我们努力寻找JVM暂停的原因，后来发现是因为Full GC执行时间过长，因此我们决定进行GC优化。</p><p>在GC优化的开始阶段，我们加上了<code>-verbosegc</code>参数，结果如下图所示：</p><p><img src="https://pic.yupoo.com/crowhawk/ebb4b181/a24f4e9b.png" alt="" /><strong>图1：进行GC优化之前STW的时间</strong></p><p>上图是由HPJMeter生成的图片之一。横坐标表示JVM执行的时间，纵坐标表示每次GC的时间。CMS为绿点，表示Full GC的结果，而Parallel Scavenge为蓝点，表示Minor GC的结果。</p><p>之前我说过CMS GC是最快的GC，但是上面的结果显示在一些时候CMS耗时达到了15s。 <strong>是什么导致了这一结果？</strong> 请记住我之前说的：CMS在执行compact（整理）操作时会显著变慢。此外，服务的内存通过<code>-Xms1g</code>和<code>=Xmx4g</code>设置了，而分配的内存只有4GB。</p><p>因此笔者将GC类型从CMS GC改为了Parallel GC，把内存大小设为2GB，并把<code>NewRatio</code>设为3。在执行<code>jstat -gcutil</code>几小时后的结果如下：</p><pre><code>S0 S1 E O P YGC YGCT FGC FGCT GCT0.00 30.48 3.31 26.54 37.01 226 11.131 4 11.758 22.890</code></pre><p>Full GC的时间缩短了，变成了每次3s，跟15s比有了显著提升。但是3s依然不够快，为此笔者创建了以下6种情况：</p><ul><li>Case 1: <code>-XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=2</code></li><li>Case 2: <code>-XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=3</code></li><li>Case 3: <code>-XX:+UseParallelGC -Xms1g -Xmx1g -XX:NewRatio=3</code></li><li>Case 4: <code>-XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=2</code></li><li>Case 5: <code>-XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=3</code></li><li>Case 6: <code>-XX:+UseParallelOldGC -Xms1g -Xmx1g -XX:NewRatio=3</code></li></ul><p>**上面哪一种情况最快？**结果显示，内存空间越小，运行结果最少。下图展示了性能最好的Case 6的结果图，它的最慢响应时间只有1.7s，并且响应时间的平均值已经被控制到了1s以内。</p><p><img src="https://pic.yupoo.com/crowhawk/026cb5ec/dd3bdbb9.png" alt="" /><strong>图2：Case 6的持续时间图</strong></p><p>基于上图的结果，按照Case 6调整了GC参数，但这却导致每晚都会发生<code>OutOfMemoryError</code>。很难解释发生异常的具体原因，简单地说，应该是批处理程序导致了内存泄漏，我们正在解决相关的问题。</p><p>如果只对GC日志做一些短时间的分析就将相关参数部署到所有服务器上来执行GC优化，这将是非常危险的。切记，只有当你同时仔细分析服务的执行情况和GC日志后，才能保证GC优化没有错误地执行。</p><p>在上文中，我们通过两个GC优化的例子来说明了GC优化是怎样执行的。正如上文中提到的，例子中设置的GC参数可以设置在相同的服务器之上，但前提是他们具有相同的CPU、操作系统、JDK版本并且运行着相同的服务。此外，不要把我使用的参数照搬到你的应用上，它们可能在你的机器上并不能起到同样良好的效果。</p><p>笔者没有执行heap dump并分析内存的详细内容，而是通过自己的经验进行GC优化。精确地分析内存可以得到更好的优化效果，不过这种分析一般只适用于内存使用量相对固定的场景。如果服务严重过载并占有了大量的内存，则建议你根据之前的经验进行GC优化。</p><p>笔者已经在一些服务上设置了G1 GC参数并进行了性能测试，但还没有应用于正式的生产环境。G1 GC的速度快于任何其他的GC类型，但是你必须要升级到JDK 7。此外，暂时还无法保证它的稳定性，没有人知道运行时是否会出现致命的错误，因此G1 GC暂时还不适合投入应用。</p><p>等未来JDK 7真正稳定了（这并不是说它现在不稳定），并且WAS针对JDK 7进行优化后，G1 GC最终能按照预期的那样来工作，等到那一天我们可能就不再需要GC优化了。</p><p>想了解关于GC优化的更多细节，请前往 <a href="https://www.slideshare.net/">Slideshare.com</a> 查看相关资料。强烈推荐 <a href="https://www.slideshare.net/aszegedi/everything-i-ever-learned-about-jvm-performance-tuning-twitter">Everything I Ever Learned About JVM Performance Tuning @Twitter</a>,作者是Attila Szegedi, 一名Twitter工程师，请花些时间好好阅读它。</p><hr /><ul><li>From: <a href="https://crowhawk.github.io/2017/08/21/jvm_4/">https://crowhawk.github.io/2017/08/21/jvm_4/</a></li></ul>]]>
                    </description>
                    <pubDate>Mon, 15 Jun 2020 12:55:00 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[深入理解JVM(3)——7 种垃圾收集器]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/understanding-the-jvm-3</link>
                    <description>
                            <![CDATA[<h2 id="深入理解jvm37种垃圾收集器">深入理解JVM(3)——7种垃圾收集器</h2><p><strong>如果说收集算法是内存回收的方法论，那么垃圾收集器就是内存回收的具体实现。</strong> Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定，因此不同的厂商、版本的虚拟机所提供的垃圾收集器都可能会有很大差别，并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。接下来讨论的收集器基于JDK1.7 Update 14 之后的HotSpot虚拟机（在此版本中正式提供了商用的G1收集器，之前G1仍处于实验状态），该虚拟机包含的所有收集器如下图所示：</p><p><img src="https://notes.suremotoo.site/upload/2020/06/jvm-10%20%E7%A7%8D%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8-30b29db88be149c1980684aeac94bb70.png" alt="" /></p><p>上图展示了7种作用于不同分代的收集器，如果两个收集器之间存在连线，就说明它们可以搭配使用。虚拟机所处的区域，则表示它是属于新生代收集器还是老年代收集器。Hotspot实现了如此多的收集器，正是因为目前并无完美的收集器出现，只是选择对具体应用最适合的收集器。</p><h4 id="并行和并发">并行和并发</h4><p><strong>并行（Parallel）</strong>：指多条垃圾收集线程并行工作，但此时用户线程仍然处于等待状态。<br /><strong>并发（Concurrent）</strong>：指用户线程与垃圾收集线程同时执行（但不一定是并行的，可能会交替执行），用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。</p><h4 id="吞吐量throughput">吞吐量（Throughput）</h4><p>吞吐量就是<strong>CPU用于运行用户代码的时间</strong>与<strong>CPU总消耗时间</strong>的比值，即</p><p><strong>吞吐量 = 运行用户代码时间 /（运行用户代码时间 + 垃圾收集时间）。</strong></p><p>假设虚拟机总共运行了100分钟，其中垃圾收集花掉1分钟，那吞吐量就是99%。</p><h4 id="minor-gc-和-full-gc">Minor GC 和 Full GC</h4><p><strong>新生代GC（Minor GC）</strong>：指发生在新生代的垃圾收集动作，因为Java对象大多都具备朝生夕灭的特性，所以Minor GC非常频繁，一般回收速度也比较快。具体原理见上一篇文章。<br /><strong>老年代GC（Major GC / Full GC）</strong>：指发生在老年代的GC，出现了Major GC，经常会伴随至少一次的Minor GC（但非绝对的，在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程）。Major GC的速度一般会比Minor GC慢10倍以上。</p><h4 id="serial收集器">Serial收集器</h4><p><strong>Serial（串行）<strong>收集器是最基本、发展历史最悠久的收集器，它是采用</strong>复制算法</strong>的<strong>新生代收集器</strong>，曾经（JDK 1.3.1之前）是虚拟机<strong>新生代</strong>收集的唯一选择。它是一个单线程收集器，只会使用一个CPU或一条收集线程去完成垃圾收集工作，更重要的是<strong>它在进行垃圾收集时，必须暂停其他所有的工作线程，直至Serial收集器收集结束为止（“Stop The World”）</strong>。这项工作是由虚拟机在后台自动发起和自动完成的，在用户不可见的情况下把用户正常工作的线程全部停掉，这对很多应用来说是难以接收的。</p><p>下图展示了Serial 收集器（老年代采用Serial Old收集器）的运行过程：</p><p><img src="https://pic.yupoo.com/crowhawk/6b90388c/6c281cf0.png" alt="" /></p><p>为了消除或减少工作线程因内存回收而导致的停顿，HotSpot虚拟机开发团队在JDK 1.3之后的Java发展历程中研发出了各种其他的优秀收集器，这些将在稍后介绍。但是这些收集器的诞生并不意味着Serial收集器已经“老而无用”，实际上到现在为止，它依然是H<strong>otSpot虚拟机运行在Client模式下的默认的新生代收集器</strong>。它也有着优于其他收集器的地方：<strong>简单而高效（与其他收集器的单线程相比），对于限定单个CPU的环境来说，Serial收集器由于没有线程交互的开销，专心做垃圾收集自然可以获得更高的单线程收集效率。</strong></p><p>在用户的桌面应用场景中，分配给虚拟机管理的内存一般不会很大，收集几十兆甚至一两百兆的新生代（仅仅是新生代使用的内存，桌面应用基本不会再大了），停顿时间完全可以控制在几十毫秒最多一百毫秒以内，只要不频繁发生，这点停顿时间可以接收。所以，Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。</p><h4 id="parnew-收集器">ParNew 收集器</h4><p><strong>ParNew</strong>收集器就是Serial收集器的多线程版本，它也是一个<strong>新生代收集器</strong>。除了使用多线程进行垃圾收集外，其余行为包括Serial收集器可用的所有控制参数、收集算法（复制算法）、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同，两者共用了相当多的代码。</p><p>ParNew收集器的工作过程如下图（老年代采用Serial Old收集器）：</p><p><img src="https://pic.yupoo.com/crowhawk/605f57b5/75122b84.png" alt="" /></p><p>ParNew收集器除了使用多线程收集外，其他与Serial收集器相比并无太多创新之处，但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器，其中有一个与性能无关的重要原因是，<strong>除了Serial收集器外，目前只有它能和CMS收集器（Concurrent Mark Sweep）配合工作</strong>，CMS收集器是JDK 1.5推出的一个具有划时代意义的收集器，具体内容将在稍后进行介绍。</p><p>ParNew 收集器在<strong>单CPU的环境</strong>中绝对不会有比Serial收集器有更好的效果，甚至由于存在线程交互的开销，该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。在<strong>多CPU环境</strong>下，随着CPU的数量增加，它对于GC时系统资源的有效利用是很有好处的。它默认开启的收集线程数与CPU的数量相同，在CPU非常多的情况下可使用 <strong>-XX:ParallerGCThreads</strong> 参数设置。</p><h4 id="parallel-scavenge-收集器">Parallel Scavenge 收集器</h4><p><strong>Parallel Scavenge</strong>收集器也是一个<strong>并行</strong>的<strong>多线程新生代</strong>收集器，它也使用<strong>复制算法</strong>。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同，CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间，而Parallel Scavenge收集器的目标是<strong>达到一个可控制的吞吐量（Throughput）</strong>。</p><p><strong>停顿时间越短就越适合需要与用户交互的程序</strong>，良好的响应速度能提升用户体验。而<strong>高吞吐量</strong>则可以高效率地利用CPU时间，尽快完成程序的运算任务，主要适合<strong>在后台运算而不需要太多交互的任务</strong>。</p><p>Parallel Scavenge收集器除了会显而易见地提供可以精确控制吞吐量的参数，还提供了一个参数**-XX:+UseAdaptiveSizePolicy**，这是一个开关参数，打开参数后，就不需要手工指定新生代的大小（-Xmn）、Eden和Survivor区的比例（-XX:SurvivorRatio）、晋升老年代对象年龄（-XX:PretenureSizeThreshold）等细节参数了，虚拟机会根据当前系统的运行情况收集性能监控信息，动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量，这种方式称为<strong>GC自适应的调节策略（GC Ergonomics）</strong>。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。</p><p>另外值得注意的一点是，Parallel Scavenge收集器无法与CMS收集器配合使用，所以在JDK 1.6推出Parallel Old之前，如果新生代选择Parallel Scavenge收集器，老年代只有Serial Old收集器能与之配合使用。</p><h4 id="serial-old收集器">Serial Old收集器</h4><p>Serial Old 是 Serial收集器的老年代版本，它同样是一个<strong>单线程收集器</strong>，使用 <strong>“标记-整理”（Mark-Compact）</strong> 算法。</p><p>此收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下，它还有两大用途：</p><ul><li>在JDK1.5 以及之前版本（Parallel Old诞生以前）中与Parallel Scavenge收集器搭配使用。</li><li>作为CMS收集器的后备预案，在并发收集发生<strong>Concurrent Mode Failure</strong>时使用。</li></ul><p>它的工作流程与Serial收集器相同，这里再次给出Serial/Serial Old配合使用的工作流程图：</p><p><img src="https://pic.yupoo.com/crowhawk/6b90388c/6c281cf0.png" alt="" /></p><h4 id="parallel-old收集器">Parallel Old收集器</h4><p>Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本，使用<strong>多线程</strong>和 <strong>“标记-整理”</strong> 算法。前面已经提到过，这个收集器是在JDK 1.6中才开始提供的，在此之前，如果新生代选择了Parallel Scavenge收集器，老年代除了Serial Old以外别无选择，所以在Parallel Old诞生以后，<strong>“吞吐量优先”收集器</strong>终于有了比较名副其实的应用组合，在<strong>注重吞吐量</strong>以及<strong>CPU资源敏感</strong>的场合，都可以优先考虑Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工作流程与Parallel Scavenge相同，这里给出Parallel Scavenge/Parallel Old收集器配合使用的流程图：</p><p><img src="https://pic.yupoo.com/crowhawk/9a6b1249/b1800d45.png" alt="" /></p><h4 id="cms收集器">CMS收集器</h4><p><strong>CMS（Concurrent Mark Sweep）<strong>收集器是一种以</strong>获取最短回收停顿时间</strong>为目标的收集器，它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用，这些应用都非常重视服务的响应速度。从名字上（“Mark Sweep”）就可以看出它是基于 <strong>“标记-清除”</strong> 算法实现的。</p><p>CMS收集器工作的整个流程分为以下4个步骤：</p><p><strong>初始标记（CMS initial mark）</strong>：仅仅只是标记一下GC Roots能直接关联到的对象，速度很快，需要“Stop The World”。<br /><strong>并发标记（CMS concurrent mark）</strong>：进行<strong>GC Roots Tracing</strong>的过程，在整个过程中耗时最长。<br /><strong>重新标记（CMS remark）</strong>：为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录，这个阶段的停顿时间一般会比初始标记阶段稍长一些，但远比并发标记的时间短。此阶段也需要“Stop The World”。<br /><strong>并发清除（CMS concurrent sweep）</strong></p><p>由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作，所以，从总体上来说，CMS收集器的内存回收过程是与用户线程一起并发执行的。通过下图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间：</p><p><img src="https://pic.yupoo.com/crowhawk/fffcf9a2/f60599b2.png" alt="" /></p><p><strong>优点</strong></p><p>CMS是一款优秀的收集器，它的主要<strong>优点</strong>在名字上已经体现出来了：<strong>并发收集</strong>、<strong>低停顿</strong>，因此CMS收集器也被称为<strong>并发低停顿收集器（Concurrent Low Pause Collector）</strong>。</p><p><strong>缺点</strong></p><p><strong>对CPU资源非常敏感</strong> 其实，面向并发设计的程序都对CPU资源比较敏感。在并发阶段，它虽然不会导致用户线程停顿，但会因为占用了一部分线程（或者说CPU资源）而导致应用程序变慢，总吞吐量会降低。<strong>CMS默认启动的回收线程数是（CPU数量+3）/4</strong>，也就是当CPU在4个以上时，并发回收时垃圾收集线程不少于25%的CPU资源，并且随着CPU数量的增加而下降。但是<strong>当CPU不足4个时（比如2个），CMS对用户程序的影响就可能变得很大</strong>，如果本来CPU负载就比较大，还要分出一半的运算能力去执行收集器线程，就可能导致用户程序的执行速度忽然降低了50%，其实也让人无法接受。</p><p><strong>无法处理浮动垃圾（Floating Garbage）</strong> 可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。<strong>由于CMS并发清理阶段用户线程还在运行着，伴随程序运行自然就还会有新的垃圾不断产生。<strong>这一部分垃圾出现在标记过程之后，CMS无法再当次收集中处理掉它们，只好留待下一次GC时再清理掉。这一部分垃圾就被称为</strong>“浮动垃圾”</strong>。也是由于在垃圾收集阶段用户线程还需要运行，那也就还需要预留有足够的内存空间给用户线程使用，因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集，需要预留一部分空间提供并发收集时的程序运作使用。</p><p><strong>标记-清除算法导致的空间碎片</strong> CMS是一款基于“标记-清除”算法实现的收集器，这意味着收集结束时会有大量空间碎片产生。空间碎片过多时，将会给大对象分配带来很大麻烦，往往出现老年代空间剩余，但无法找到足够大连续空间来分配当前对象。</p><p><strong>G1（Garbage-First）<strong>收集器是当今收集器技术发展最前沿的成果之一，它是一款</strong>面向服务端应用</strong>的垃圾收集器，HotSpot开发团队赋予它的使命是（在比较长期的）未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比，G1具备如下特点：</p><p><strong>并行与并发</strong> G1 能充分利用多CPU、多核环境下的硬件优势，使用多个CPU来缩短“Stop The World”停顿时间，部分其他收集器原本需要停顿Java线程执行的GC动作，G1收集器仍然可以通过并发的方式让Java程序继续执行。<br /><strong>分代收集</strong> 与其他收集器一样，分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆，但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。<br /><strong>空间整合</strong> G1从整体来看是基于 <strong>“标记-整理”</strong> 算法实现的收集器，从局部（两个Region之间）上来看是基于 <strong>“复制”</strong> 算法实现的。这意味着G1运行期间不会产生内存空间碎片，收集后能提供规整的可用内存。此特性有利于程序长时间运行，分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。<br /><strong>可预测的停顿</strong> 这是G1相对CMS的一大优势，降低停顿时间是G1和CMS共同的关注点，但G1除了降低停顿外，还能建立可预测的停顿时间模型，能让使用者明确指定在一个长度为M毫秒的时间片段内，消耗在GC上的时间不得超过N毫秒，这几乎已经是实时Java（RTSJ）的垃圾收集器的特征了。</p><p><strong>横跨整个堆内存</strong></p><p>在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代，而G1不再是这样。G1在使用时，Java堆的内存布局与其他收集器有很大区别，它<strong>将整个Java堆划分为多个大小相等的独立区域（Region）</strong>，虽然还保留新生代和老年代的概念，但<strong>新生代和老年代不再是物理隔离的了，而都是一部分Region（不需要连续）的集合</strong>。</p><p><strong>建立可预测的时间模型</strong></p><p>G1收集器之所以能建立可预测的停顿时间模型，是因为它可以<strong>有计划地避免在整个Java堆中进行全区域的垃圾收集</strong>。G1跟踪各个Region里面的垃圾堆积的价值大小（回收所获得的空间大小以及回收所需时间的经验值），<strong>在后台维护一个优先列表</strong>，每次根据允许的收集时间，<strong>优先回收价值最大的Region（这也就是Garbage-First名称的来由）</strong>。这种使用Region划分内存空间以及有优先级的区域回收方式，保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。</p><p><strong>避免全堆扫描——Remembered Set</strong></p><p>G1把Java堆分为多个Region，就是“化整为零”。但是Region不可能是孤立的，一个对象分配在某个Region中，可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候，需要扫描整个Java堆才能保证准确性，这显然是对GC效率的极大伤害。</p><p>为了避免全堆扫描的发生，虚拟机<strong>为G1中每个Region维护了一个与之对应的Remembered Set</strong>。虚拟机发现程序在对Reference类型的数据进行写操作时，会产生一个Write Barrier暂时中断写操作，检查Reference引用的对象是否处于不同的Region之中（在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象），如果是，便通过CardTable<strong>把相关引用信息记录到被引用对象所属的Region的Remembered Set之中</strong>。当进行内存回收时，在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。</p><hr /><p>如果不计算维护Remembered Set的操作，G1收集器的运作大致可划分为以下几个步骤：</p><p><strong>初始标记（Initial Marking）</strong> 仅仅只是标记一下GC Roots 能直接关联到的对象，并且修改<strong>TAMS（Nest Top Mark Start）<strong>的值，让下一阶段用户程序并发运行时，能在正确可以的Region中创建对象，此阶段需要</strong>停顿线程</strong>，但耗时很短。<br /><strong>并发标记（Concurrent Marking）</strong> 从GC Root 开始对堆中对象进行<strong>可达性分析</strong>，找到存活对象，此阶段耗时较长，但<strong>可与用户程序并发执行</strong>。<br /><strong>最终标记（Final Marking）</strong> 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录，虚拟机将这段时间对象变化记录在<strong>线程的Remembered Set Logs</strong>里面，最终标记阶段需要<strong>把Remembered Set Logs的数据合并到Remembered Set中</strong>，这阶段需要<strong>停顿线程</strong>，但是<strong>可并行执行</strong>。<br /><strong>筛选回收（Live Data Counting and Evacuation）</strong> 首先对各个Region中的回收价值和成本进行排序，根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行，但是因为只回收一部分Region，时间是用户可控制的，而且停顿用户线程将大幅度提高收集效率。</p><p>通过下图可以比较清楚地看到G1收集器的运作步骤中并发和需要停顿的阶段（Safepoint处）：</p><p><img src="https://pic.yupoo.com/crowhawk/53b7a589/0bce1667.png" alt="" /></p><p>收集器串行、并行or并发新生代/老年代算法目标适用场景</p><p><strong>Serial</strong><br />串行<br />新生代<br />复制算法<br />响应速度优先<br />单CPU环境下的Client模式</p><p><strong>Serial Old</strong><br />串行<br />老年代<br />标记-整理<br />响应速度优先<br />单CPU环境下的Client模式、CMS的后备预案</p><p><strong>ParNew</strong><br />并行<br />新生代<br />复制算法<br />响应速度优先<br />多CPU环境时在Server模式下与CMS配合</p><p><strong>Parallel Scavenge</strong><br />并行<br />新生代<br />复制算法<br />吞吐量优先<br />在后台运算而不需要太多交互的任务</p><p><strong>Parallel Old</strong><br />并行<br />老年代<br />标记-整理<br />吞吐量优先<br />在后台运算而不需要太多交互的任务</p><p><strong>CMS</strong><br />并发<br />老年代<br />标记-清除<br />响应速度优先<br />集中在互联网站或B/S系统服务端上的Java应用</p><p><strong>G1</strong><br />并发<br />both<br />标记-整理+复制算法<br />响应速度优先<br />面向服务端应用，将来替换CMS</p><p>本文通过详细介绍HotSpot虚拟机的7种垃圾收集器回答了上一篇文章开头提出的三个问题中的第三个——“如何回收”，在下一篇文章中，我们将回答最后一个未被解答的问题——“什么时候回收”。</p><p>参考：</p><ul><li><a href="https://pic.yupoo.com/crowhawk/53b7a589/0bce1667.png">《深入理解 Java 虚拟机——JVM 高级特性与最佳实践》－周志明</a></li></ul><p>From: <a href="https://crowhawk.github.io/2017/08/15/jvm_3/">https://crowhawk.github.io/2017/08/15/jvm_3/</a></p>]]>
                    </description>
                    <pubDate>Sun, 14 Jun 2020 23:56:40 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[深入理解 JVM(2)——GC 算法与内存分配策略]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/understanding-the-jvm-2</link>
                    <description>
                            <![CDATA[<h2 id="深入理解-jvm2gc-算法与内存分配策略">深入理解 JVM(2)——GC 算法与内存分配策略</h2><p>说起<strong>垃圾收集（Garbage Collection, GC）</strong>，想必大家都不陌生，它是 JVM 实现里非常重要的一环，JVM 成熟的内存动态分配与回收技术使 Java（当然还有其他运行在 JVM 上的语言，如 Scala 等）程序员在提升开发效率上获得了惊人的便利。理解 GC，对于理解 JVM 和 Java 语言有着非常重要的作用。并且当我们需要排查各种内存溢出、内存泄漏问题时，当垃圾收集称为系统达到更高并发量的瓶颈时，只有深入理解 GC 和内存分配，才能对这些 “自动化” 的技术实施必要的监控和调节。</p><p>在 Java 的运行时数据区中，程序计数器、虚拟机栈、本地方法栈三个区域都是线程私有的，随线程而生，随线程而灭，在方法结束或线程结束时，内存自然就跟着回收了，不需要过多考虑回收的问题。而<strong>Java 堆</strong>和<strong>方法区</strong>则不一样，一个接口中的多个实现类需要的内存可能不一样，一个方法中的多个分支需要的内存也可能不一样，我们只有在程序处于运行期间才能知道会创建哪些对象，这部分内存的分配和回收都是动态的，垃圾回收器关注的是这部分内存，后续讨论的 “内存” 分配回收也是指这一块，尤其需要注意。</p><p>GC 主要回答了以下三个问题：</p><ul><li>哪些内存需要回收？</li><li>什么时候回收？</li><li>如何回收？</li></ul><p>这三个问题的具体解决方案，也就是本文接下来要讲解的内容。</p><p>在堆里存放着 Java 世界中几乎所有的对象实例，垃圾收集器在对堆进行回收前，首要的就是确定这些对象中哪些还 “存活” 着，哪些已经“死去”（即不可能再被任何途径使用的对象）。</p><h4 id="引用计数算法">引用计数算法</h4><p>引用计数算法是在 JVM 中被摒弃的一种对象存活判定算法，不过它也有一些知名的应用场景（如 Python、FlashPlayer），因此在这里也简单介绍一下。</p><p>用引用计数器判断对象是否存活的过程是这样的：<strong>给对象中添加一个引用计数器，每当有一个地方引用它时，计数器加 1；当引用失效时，计数器减 1；任何时刻计数器为 0 的对象就是不可能再被使用的。</strong></p><p>引用计数算法的实现简单，判定效率也很高，大部分情况下是一个不错的算法。它没有被 JVM 采用的原因是<strong>它很难解决对象之间循环引用的问题</strong>。例如以下例子：</p><pre><code class="language-java">    /**     * testGC()方法执行后，objA 和 objB 会不会被 GC 呢？      */    public class ReferenceCountingGC {            public Object instance = null;            private static final int _1MB = 1024 * 1024;            /**         * 这个成员属性的唯一意义就是占点内存，以便在能在 GC 日志中看清楚是否有回收过         */        private byte[] bigSize = new byte[2 * _1MB];            public static void testGC() {ReferenceCountingGC objA = new ReferenceCountingGC();            ReferenceCountingGC objB = new ReferenceCountingGC();            objA.instance = objB;            objB.instance = objA;                objA = null;            objB = null;                // 假设在这行发生 GC，objA 和 objB 是否能被回收？            System.gc();}    }</code></pre><p>在上面这段代码中，对象 objA 和对象 objB 都有字段 instance，赋值令 <code>objA.instance = objB;</code>、<code>objB.instance = objA;</code>，除此之外，这两个对象再无引用。如果 JVM 采用引用计数算法来管理内存，<strong>这两个对象不可能再被访问，但是他们互相引用着对方，导致它们引用计数不为 0，所以引用计数器无法通知 GC 收集器回收它们</strong>。</p><p>而事实上执行这段代码，objA 和 objB 是可以被回收的，下面一节将介绍 JVM 实际使用的存活判定算法。</p><h4 id="可达性分析算法">可达性分析算法</h4><p>在主流商用程序语言的实现中，都是通过<strong>可达性分析（tracing GC）<strong>来判定对象是否存活的。此算法的基本思路是：通过一系列的称为</strong>“GC Roots”<strong>的对象作为起点，从这些节点向下搜索，搜索所走过的路径称为</strong>引用链（Reference Chain）</strong>，当一个对象到 GC Roots 没有任何引用链相连（用图论的话来说，就是 GC Roots 到这个对象不可达）时，则证明此对象时不可用的。用下图来加以说明：</p><p>![][1]</p><p>[1]: <a href="https://pic.yupoo.com/crowhawk/5d0246eb/0635cbe8.png">https://pic.yupoo.com/crowhawk/5d0246eb/0635cbe8.png</a>)</p><p>上图中，对象 object 5、object 6、object 7 虽然互有关联，但是它们到 GC Roots 是不可达的，所以它们将会被判定为是可回收的对象。</p><p>可以看到，GC Roots 在对象图之外，是特别定义的 <strong>“起点”</strong>，不可能被对象图内的对象所引用。</p><p>准确地说，<strong>GC Roots 其实不是一组对象，而通常是一组特别管理的指向引用类型对象的指针</strong>，这些指针是 tracing GC 的 trace 的起点。它们不是对象图里的对象，对象也不可能引用到这些 “外部” 的指针，这也是 tracing GC 算法不会出现循环引用问题的基本保证。因此也容易得出，<strong>只有引用类型的变量才被认为是 Roots，值类型的变量永远不被认为是 Roots</strong>。只有深刻理解引用类型和值类型的内存分配和管理的不同，才能知道为什么 root 只能是引用类型。</p><p>在 Java 中，可作为 GC Roots 的对象包括以下几种：</p><p><strong>虚拟机栈（栈帧中的局部变量表，Local Variable Table）</strong> 中引用的对象。<br /><strong>方法区中<em>类静态属性</em></strong> 引用的对象。<br /><strong>方法区中<em>常量</em></strong> 引用的对象。<br /><strong>本地方法栈中 JNI（即一般说的 Native 方法）</strong> 引用的对象。</p><p>看到这里你可能要问，选择这些对象的依据是什么呢？</p><p>可以概括得出，可作为 GC Roots 的节点主要在<strong>全局性的引用</strong>与<strong>执行上下文</strong>中。要明确的是，tracing gc 必须<strong>以当前存活的对象集为 Roots</strong>，因此必须选取确定存活的引用类型对象。GC 管理的区域是 Java 堆，<strong>虚拟机栈</strong>、<strong>方法区</strong>和<strong>本地方法栈</strong>不被 GC 所管理，因此选用这些区域内引用的对象作为 GC Roots，是<strong>不会被 GC 所回收</strong>的。其中虚拟机栈和本地方法栈都是线程私有的内存区域，只要线程没有终止，就能确保它们中引用的对象的存活。而方法区中类静态属性引用的对象是显然存活的。常量引用的对象在当前可能存活，因此，也可能是 GC roots 的一部分。</p><h4 id="两次标记与-finalize方法">两次标记与 finalize()方法</h4><p>即使在可达性分析算法中不可达的对象，也不是一定会死亡的，它们暂时都处于 <strong>“缓刑”</strong> 阶段，要真正宣告一个对象“死亡”，至少要经历两次标记过程：</p><p>如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链，那它将会被<strong>第一次标记</strong>并且进行一次筛选，筛选的条件是<strong>此对象是否有必要执行 <code>finaliza()</code> 方法</strong>。当对象没有覆盖 <code>finaliza()</code> 方法，或者 <code>finaliza()</code> 方法已经被虚拟机调用过，虚拟机将这两种情况都视为“没有必要执行”。</p><p>如果这个对象被判定为有必要执行 <code>finaliza()</code> 方法，那么此对象将会放置在一个叫做 F-Queue 的队列中，并在稍后由一个虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的 “执行” 是指虚拟机会触发此方法，但并不承诺会等待它运行结束，原因是：如果一个对象在 <code>finaliza()</code> 方法中执行缓慢，或者发生了死循环（更极端的情况），将很可能导致 F-Queue 队列中的其它对象永久处于等待，甚至导致整个内存回收系统崩溃。</p><p><code>finaliza()</code> 方法是对象逃脱死亡命运的最后一次机会，稍后 GC 将对 F-Queue 队列中的对象进行<strong>第二次小规模的标记</strong>。如果对象想在 <code>finaliza()</code> 方法中成功拯救自己，<strong>只要重新与引用链上的任何一个对象建立关联即可，例如把自己（this 关键字）赋值给某个类变量或者对象的成员变量，这样在第二次标记时它将被移出 “即将回收” 的集合</strong>；如果对象这时候还没有逃脱，基本上它就真的被回收了。</p><p>值得注意的是，如果代码中有两段一模一样的代码段，执行结果却是一次逃脱成功，一次失败。这是因为任何一个对象的 <code>finalize()</code> 方法都只会被系统调用一次，如果对象面临下一次回收，它的 <code>finalize()</code> 方法不会再被执行，因此第二次逃脱行动失败。</p><p>需要说明的是，使用 <code>finalize()</code> 方法来 “拯救” 对象是不值得提倡的，因为它不是 C/C++ 中的析构函数，而是 Java 刚诞生时为了使 C/C++ 程序员更容易接受它所做的一个妥协。<strong>它的运行代价高昂，不确定性大，无法保证各个对象的调用顺序。</strong><code>finalize()</code> 能做的工作，使用 <code>try-finally</code> 或者其它方法都更适合、及时，所以笔者建议大家可以忘掉此方法存在。</p><h4 id="回收方法区">回收方法区</h4><p>很多人认为方法区没有垃圾回收，Java 虚拟机规范中确实说过不要求，而且在方法区中进行垃圾收集的 “性价比” 较低：在堆中，尤其是新生代，常规应用进行一次垃圾收集可以回收 70%~95% 的空间，而方法区的效率远低于此。在 JDK 1.8 中，JVM 摒弃了永久代，用元空间来作为方法区的实现，下面介绍的将是元空间的垃圾回收。</p><p>元空间的内存管理由<strong>元空间虚拟机</strong>来完成。先前，对于类的元数据我们需要不同的垃圾回收器进行处理，现在只需要执行元空间虚拟机的 C++ 代码即可完成。<strong>在元空间中，类和其元数据的生命周期</strong>和<strong>其对应的类加载器</strong>是相同的。话句话说，<strong>只要类加载器存活，其加载的类的元数据也是存活的</strong>，因而不会被回收掉。</p><p>我们从行文到现在提到的元空间稍微有点不严谨。准确的来说，<strong>每一个<em>类加载器的存储区域</em> 都称作一个元空间，所有的元空间合在一起就是我们一直说的元空间。</strong> 当一个类加载器被垃圾回收器标记为不再存活，其对应的元空间会被回收。在元空间的回收过程中没有重定位和压缩等操作。但是元空间内的元数据会进行扫描来确定 Java 引用。</p><p>本节将介绍几种垃圾收集算法的思想及其发展过程，具体的实现将在稍后介绍。</p><h4 id="标记清除mark-sweep算法">标记－清除（Mark-Sweep）算法</h4><p><strong>标记－清除（Mark-Sweep）</strong> 算法是最基础的垃圾收集算法，后续的收集算法都是基于它的思路并对其不足进行改进而得到的。顾名思义，算法分成 “标记”、“清除” 两个阶段：首先标记出所有需要回收的对象，在标记完成后统一回收所有被标记的对象，标记过程在前一节讲述对象标记判定时已经讲过了。</p><p>标记－清除算法的不足主要有以下两点：</p><p><strong>空间问题</strong>，标记清除之后会产生大量不连续的<strong>内存碎片</strong>，空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时，无法找到足够的连续内存而不得不触发另一次垃圾收集动作。<br /><strong>效率问题</strong>，因为内存碎片的存在，操作会变得更加费时，因为查找下一个可用空闲块已不再是一个简单操作。</p><p>标记－清除算法的执行过程如下图所示：</p><p><img src="https://pic.yupoo.com/crowhawk/5a3494ae/efc6204a.png" alt="" /></p><h4 id="复制copying算法">复制（Copying）算法</h4><p>为了解决标记 - 清除算法的效率问题，一种称为 <strong>“复制”（Copying）<strong>的收集算法出现了，思想为：它</strong>将可用内存按容量分成大小相等的两块</strong>，每次只使用其中的一块。<strong>当这一块内存用完，就将还存活着的对象复制到另一块上面</strong>，然后再把已使用过的内存空间一次清理掉。</p><p>这样做使得<strong>每次都是对整个半区进行内存回收</strong>，内存分配时也就<strong>不用考虑内存碎片</strong>等复杂情况，只要<strong>移动堆顶指针，按顺序分配内存</strong>即可，实现简单，运行高效。只是这种算法的代价是<strong>将内存缩小为原来的一半</strong>，代价可能过高了。复制算法的执行过程如下图所示：</p><p><img src="https://pic.yupoo.com/crowhawk/62b8a3a8/f1cada8a.png" alt="" /></p><p><strong>Minor GC 与复制算法</strong></p><p><strong>现在的商业虚拟机都使用复制算法来回收新生代。</strong> 新生代的 GC 又叫 <strong>“Minor GC”</strong>，IBM 公司的专门研究表明：新生代中的对象 98% 是 <strong>“朝生夕死”</strong> 的，所以 Minor GC 非常频繁，一般回收速度也比较快，同时 <strong>“朝生夕死”</strong> 的特性也使得 Minor GC 使用复制算法时不需要按照 1:1 的比例来划分新生代内存空间。</p><p><strong>Minor GC 过程</strong></p><p>事实上，新生代将内存分为<strong>一块较大的 Eden 空间</strong>和<strong>两块较小的 Survivor 空间（From Survivor 和 To Survivor）</strong>，<strong>每次 Minor GC 都使用 Eden 和 From Survivor</strong>，当回收时，<strong>将 Eden 和 From Survivor 中还存活着的对象都一次性地复制到另外一块 To Survivor 空间上</strong>，最后清理掉 Eden 和刚使用的 Survivor 空间。<strong>一次 Minor GC 结束的时候</strong>，<strong>Eden</strong>空间和<strong>From Survivor</strong>空间都是空的，而<strong>To Survivor</strong>空间里面存储着存活的对象。<strong>在下次 MinorGC 的时候</strong>，两个 Survivor 空间交换他们的标签，现在是空的 <strong>“From” Survivor</strong> 标记成为 <strong>“To”</strong>，<strong>“To” Survivor</strong> 标记为 <strong>“From”</strong> 。因此，在 MinorGC 结束的时候，Eden 空间是空的，两个 Survivor 空间中的一个是空的，而另一个存储着存活的对象。</p><p>HotSpot 虚拟机默认的<strong>Eden : Survivor</strong>的比例是<strong>8 : 1</strong>，由于一共有两块 Survivor，所以<strong>每次新生代中可用内存空间为整个新生代容量的 90%（80%＋10%）</strong>，只有 10% 的容量会被“浪费”。</p><p><strong>分配担保</strong></p><p>上文说的 98% 的对象可回收只是一般场景下的数据，我们没有办法保证每次回收都只有不多于 10% 的对象存活，<strong>当 Survivor 空间不够用时</strong>，需要依赖<strong>老年代内存</strong>进行<strong>分配担保（Handle Promotion）</strong>。如果另外一块 Survivor 上没有足够空间存放上一次新生代收集下来的存活对象，这些对象将直接通过分配担保机制进入老年代。</p><h5 id="标记整理mark-compact算法">标记－整理（Mark-Compact）算法</h5><p>复制算法在对象存活率较高时要进行较多的复制操作，效率将会变低。更关键的是：如果不想浪费 50% 的空间，就需要有额外的空间进行分配担保，以应对被使用的内存中所有对象都 100% 存活的极端情况，所以在<strong>老年代一般不能直接选用复制算法</strong>。</p><p>根据老年代的特点，<strong>标记－整理（Mark-Compact）<strong>算法被提出来，主要思想为：此算法的标记过程与</strong>标记－清除</strong>算法一样，但后续步骤不是直接对可回收对象进行清理，而是**让所有存活的对象都向一端移动，然后直接清理掉边界以外的内存。**具体示意图如下所示：</p><p><img src="https://pic.yupoo.com/crowhawk/d046244a/d3d3277f.png" alt="" /></p><h5 id="分代收集generational-collection算法">分代收集（Generational Collection）算法</h5><p>当前商业虚拟机的垃圾收集都采用<strong>分代收集（Generational Collection）算法</strong>，此算法相较于前几种没有什么新的特征，主要思想为：根据对象存活周期的不同将内存划分为几块，一般是把 Java 堆分为新生代和老年代，这样就可以根据各个年代的特点采用最适合的收集算法：</p><p><strong>新生代</strong>在新生代中，每次垃圾收集时都发现有大批对象死去，只有少量存活，那就选用<strong>复制算法</strong>，只需要付出少量存活对象的复制成本就可以完成收集。</p><p><strong>老年代</strong>在老年代中，因为对象存活率高、没有额外空间对它进行分配担保，就必须使用 <strong>“标记 - 清除”</strong> 或 <strong>“标记 - 整理”</strong> 算法来进行回收。</p><p>前面两大节主要从理论上介绍了对象存活判定算法和垃圾收集算法，而在 HotSpot 虚拟机上实现这些算法时，必须对算法的执行效率有严格的考量，才能保证虚拟机高效运行。</p><h4 id="枚举根节点">枚举根节点</h4><p>从可达性分析中<strong>从 GC Roots 节点找引用链</strong>这个操作为例，可作为 GC Roots 的节点主要在<strong>全局性的引用</strong>（例如常量或类静态属性）与<strong>执行上下文</strong>（例如栈帧中的局部变量表）中，现在很多应用仅仅方法区就有数百兆，如果要逐个检查这里面的引用，那么必然会消耗很多时间。</p><p><strong>GC 停顿（”Stop The World”）</strong></p><p>另外，可达性分析工作必须在一个<strong>能确保一致性的快照</strong>中进行——这里 <strong>“一致性”</strong> 的意思是指<strong>在整个分析期间整个执行系统看起来就像被冻结在某个时间点上</strong>，不可以出现分析过程中对象引用关系还在不断变化的情况，这是保证分析结果准确性的基础。这点是导致 GC 进行时必须<strong>停顿所有 Java 执行线程</strong>（Sun 将这件事情称为 <strong>“Stop The World”</strong> ）的其中一个重要原因，即使是在号称（几乎）不会发生停顿的 CMS 收集器中，枚举根节点时也是必须要停顿的。</p><p><strong>准确式 GC 与 OopMap</strong></p><p>由于目前的主流 Java 虚拟机使用的都是<strong>准确式 GC（即使用准确式内存管理，虚拟机可用知道内存中某个位置的数据具体是什么类型）</strong>，所以当执行系统停顿下来后，并不需要一个不漏地检查完所有执行上下文和全局的引用位置，虚拟机应当是有办法直接得知哪些地方存放着对象引用。在 HotSpot 的实现中，是使用一组称为<strong>OopMap</strong>的数据结构来达到这个目的的，在类加载完成的时候，HotSpot 就把<strong>对象内什么偏移量上是什么类型的数据</strong>计算出来，在 JIT 编译过程中，也会在特定的位置记录下<strong>栈和寄存器中哪些位置是引用</strong>。这样，GC 在扫描时就可以直接得知这些信息了。</p><h4 id="安全点safepoint进行-gc-时程序停顿的位置">安全点（Safepoint）——进行 GC 时程序停顿的位置</h4><p>在 OopMap 的协助下，HotSpot 可以快速且准确地完成 GC Roots 枚举，但一个很现实的问题随之而来：可能导致引用关系变化，或者说 OopMap 内容变化的指令非常多，<strong>如果为每一条指令都生成对应的 OopMap，那将会需要大量的额外空间，这样 GC 的空间成本将会变得很高。</strong></p><p>为此，HotSpot 选择不为每条指令都生成 OopMap，而是只在 “特定的位置” 记录这些信息，这些位置便被称为<strong>安全点（Safepoint）</strong>。也就是说，<strong>程序执行时并非在所有地方都能停顿下来开始 GC，只有在到达安全点时才能暂停</strong>。Safepoint 的选定既不能太少以致于让 GC 等待时间太长，也不能过于频繁以致于过分增大运行时的负荷。所以，安全点的选定基本上是以程序 <strong>“是否具有让程序长时间执行的特征”</strong> 为标准进行选定的——因为每条指令执行的时间都非常短暂，程序不太可能因为指令流长度太长这个原因而过长时间运行，“长时间执行”的最明显特征就是<strong>指令序列复用</strong>，例如<strong>方法调用</strong>、<strong>循环跳转</strong>、<strong>异常跳转</strong>等，所以具有这些功能的指令才会产生 Safepoint。</p><p>对于 Sefepoint，另一个需要考虑的问题是如何<strong>在 GC 发生时让所有线程（这里不包括执行 JNI 调用的线程）都 “跑” 到最近的安全点上再停顿下来</strong>。这里有两种方案可供选择：</p><p><strong>抢先式中断（Preemptive Suspension）<strong>抢先式中断不需要线程的执行代码主动去配合，<strong>在 GC 发生时，首先把所有线程全部中断，如果发现有线程中断的地方不在安全点上，就恢复线程，让它 “跑” 到安全点上。<strong>现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应 GC 事件。<br /><strong>主动式中断（Voluntary Suspension）</strong> ： 主动式中断的思想是当 GC 需要中断线程的时候，不直接对线程操作，仅仅简单地</strong>设置一个标志</strong>，各个线程执行时主动去轮询这个标志，发现中断标志为真时就自己中断挂起。<strong>轮询标志的地方和安全点是重合的</strong>，另外</strong>再加上创建对象需要分配内存的地方</strong>。</p><h4 id="安全区域safe-region">安全区域（Safe Region）</h4><p><strong>Safepoint</strong>机制保证了<strong>程序执行时</strong>，在不太长的时间内就会遇到可进入 GC 的 Safepoint。但是，<strong>程序 “不执行” 的时候（如线程处于 Sleep 状态或 Blocked 状态）</strong>，这时线程无法响应 JVM 的中断请求，“走到”安全的地方去中断挂起，这时候就需要**安全区域（Safe Region）**来解决。</p><p>安全区域是指 <strong>在一段代码片段之中，引用关系不会发生变化。在这个区域中的任意地方开始 GC 都是安全的。</strong> 我们也可以把 Safe Region 看做是被扩展了的 Safepoint。</p><p>在线程执行到 Safe Region 中的代码时，首先<strong>标识自己已经进入了 Safe Region</strong>，那样，当在这段时间里 JVM 要发起 GC 时，就不用管标识自己为 Safe Region 状态的线程了。<strong>在线程要离开 Safe Region 时，它要检查系统是否已经完成了根节点枚举（或者是整个 GC 过程）</strong>，如果完成了，那线程就继续执行，否则它就必须等待直到收到可以安全离开 Safe Region 的信号为止。</p><p>Java 的自动内存管理最终可以归结为自动化地解决了两个问题：</p><p><strong>给对象分配内存</strong><br /><strong>回收分配给对象的内存</strong></p><p>对象的内存分配通常是在堆上分配（除此以外还有可能经过 JIT 编译后被拆散为标量类型并间接地栈上分配），对象主要分配在新生代的 Eden 区上，如果启动了本地线程分配缓冲，将按线程优先在 TLAB 上分配。少数情况下也可能会直接分配在老年代中，分配的规则并不是固定的，实际取决于垃圾收集器的具体组合以及虚拟机中与内存相关的参数的设置。至于内存回收策略，在上文已经描述得很详尽了。</p><p>下面以使用 Serial/Serial Old 收集器（将在下一篇文章中讲解）为例，介绍内存分配的策略。</p><h4 id="对象优先在-eden-区分配">对象优先在 Eden 区分配</h4><p>大多数情况下，对象在新生代的 Eden 区中分配。<strong>当 Eden 区没有足够空间进行分配时，虚拟机将发起一次 Minor GC。</strong></p><h4 id="大对象直接进入老年代">大对象直接进入老年代</h4><p>所谓的大对象是指，需要大量连续内存空间的 Java 对象，最典型的大对象就是很长的字符串以及数组。大对象对虚拟机的内存分配来说是一个坏消息（尤其是遇到朝生夕灭的“短命大对象”，写程序时应避免），<strong>经常出现大对象容易导致内存还有不少空间时就提前触发 GC 以获取足够的连续空间来安置它们</strong>。</p><p>虚拟机提供了一个 <strong>-XX:PretenureSizeThreshold</strong> 参数，令大于这个设置值的对象直接在老年代分配。这样做的目的是<strong>避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制</strong>（新生代采用复制算法回收内存）。</p><h4 id="长期存活的对象将进入老年代">长期存活的对象将进入老年代</h4><p>既然虚拟机采用了分代收集的思想来管理内存，那么内存回收时就必须能识别哪些对象应放在新生代，哪些对象应放在老年代中。为了做到这点，虚拟机给每个对象定义了一个<strong>对象年龄（Age）计数器</strong>。<strong>如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活，并且能被 Survivor 容纳的话，将被移动到 Survivor 空间中，并且对象年龄设为 1。对象在 Survivor 区中每 “熬过” 一次 Minor GC，年龄就增加 1 岁，当它的年龄增加到一定程度（默认为 15 岁），就将会被晋升到老年代中。<strong>对象晋升老年代的年龄阈值，可以通过参数</strong>-XX:MaxTenuringThreshold</strong>设置。</p><h4 id="动态对象年龄判定">动态对象年龄判定</h4><p>为了能更好地适应不同程序的内存状况，虚拟机并不是永远地要求对象的年龄必须达到了<strong>MaxTenuringThreshold</strong>才能晋升老年代，<strong>如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半，年龄大于或等于该年龄的对象就可以直接进入老年代</strong>，无须等到<strong>MaxTenuringThreshold</strong>中要求的年龄。</p><h4 id="空间分配担保">空间分配担保</h4><p><strong>在发生 Minor GC 之前</strong>，虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间，如果这个条件成立，那么 Minor GC 可以确保是安全的。如果不成立，则虚拟机会查看<strong>HandlePromotionFailure</strong>设置值是否允许担保失败。如果允许，那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小，如果大于，将尝试着进行一次 Minor GC，尽管这次 Minor GC 是有风险的；如果小于，或者<strong>HandlePromotionFailure</strong>设置不允许冒险，那这时也要改为进行一次<strong>Full GC</strong>。</p><p>前面提到过，新生代使用复制收集算法，但为了内存利用率，只使用其中一个 Survivor 空间来作为轮换备份，因此<strong>当出现大量对象在 Minor GC 后仍然存活的情况（最极端的情况就是内存回收后新生代中所有对象都存活），就需要老年代进行分配担保，把 Survivor 无法容纳的对象直接进入老年代。</strong> 与生活中的贷款担保类似，老年代要进行这样的担保，前提是老年代本身还有容纳这些对象的剩余空间，一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的，所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值，与老年代的剩余空间进行比较，决定是否进行 Full GC 来让老年代腾出更多空间。</p><p>取平均值进行比较其实仍然是一种动态概率的手段，也就是说，如果某次 Minor GC 存活后的对象突增，远远高于平均值的话，依然会导致<strong>担保失败（Handle Promotion Failure）</strong>。如果出现了<strong>HandlePromotionFailure</strong>失败，那就只好在失败后重新发起一次 Full GC。虽然担保失败时绕的圈子是最大的，但大部分情况下都还是会将<strong>HandlePromotionFailure</strong>开关打开，避免 Full GC 过于频繁。</p><p>对于 Minor GC，其触发条件非常简单，当 Eden 区空间满时，就将触发一次 Minor GC。而 Full GC 则相对复杂，因此本节我们主要介绍 Full GC 的触发条件。</p><h4 id="调用-systemgc">调用 System.gc()</h4><p>此方法的调用是建议 JVM 进行 Full GC, 虽然只是建议而非一定, 但很多情况下它会触发 Full GC, 从而增加 Full GC 的频率, 也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用，让虚拟机自己去管理它的内存，可通过 <strong>-XX:+ DisableExplicitGC</strong> 来禁止 RMI 调用 System.gc()。</p><h4 id="老年代空间不足">老年代空间不足</h4><p>老年代空间不足的常见场景为前文所讲的<strong>大对象直接进入老年代</strong>、<strong>长期存活的对象进入老年代</strong>等，当执行 Full GC 后空间仍然不足，则抛出如下错误： <code>Java.lang.OutOfMemoryError: Java heap space</code> 为避免以上两种状况引起的 Full GC，调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。</p><h4 id="空间分配担保失败">空间分配担保失败</h4><p>前文介绍过，使用复制算法的 Minor GC 需要老年代的内存空间作担保，如果出现了<strong>HandlePromotionFailure</strong>担保失败，则会触发 Full GC。</p><h4 id="jdk-17-及以前的永久代空间不足">JDK 1.7 及以前的永久代空间不足</h4><p>在 JDK 1.7 及以前，HotSpot 虚拟机中的方法区是用永久代实现的，永久代中存放的为一些 class 的信息、常量、静态变量等数据，当系统中要加载的类、反射的类和调用的方法较多时，Permanet Generation 可能会被占满，在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了，那么 JVM 会抛出如下错误信息： <code>java.lang.OutOfMemoryError: PermGen space</code> 为避免 PermGen 占满造成 Full GC 现象，可采用的方法为增大 PermGen 空间或转为使用 CMS GC。</p><p>在 JDK 1.8 中用元空间替换了永久代作为方法区的实现，元空间是本地内存，因此减少了一种 Full GC 触发的可能性。</p><h4 id="concurrent-mode-failure">Concurrent Mode Failure</h4><p>执行 CMS GC 的过程中同时有对象要放入老年代，而此时老年代空间不足（有时候 “空间不足” 是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC），便会报 <code>Concurrent Mode Failure</code> 错误，并触发 Full GC。</p><p>本文简要地介绍了 HotSpot 虚拟机如何去发起内存回收的问题，也解答了文章开头提出的三个问题中的前两个——“哪些内存需要回收”和“何时回收”，同时对于第三个问题——“如何回收”，在原理层面作出了解答。在下一篇文章中，笔者将通过介绍几种具体的垃圾收集器，来更深入地回答第三个问题。</p><p>参考：</p><ul><li><a href="https://book.douban.com/subject/24722612/">《深入理解 Java 虚拟机——JVM 高级特性与最佳实践》－周志明</a></li></ul><p>From: <a href="https://crowhawk.github.io/2017/08/10/jvm_2/">https://crowhawk.github.io/2017/08/10/jvm_2/</a></p>]]>
                    </description>
                    <pubDate>Sun, 14 Jun 2020 22:31:47 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[深入理解 JVM(1)——Java 内存区域与 Java 对象]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/understanding-the-jvm-1</link>
                    <description>
                            <![CDATA[<h2 id="深入理解-jvm1java-内存区域与-java-对象">深入理解 JVM(1)——Java 内存区域与 Java 对象</h2><p>JVM 载执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途，以及创建和销毁的时间，有的区域随着虚拟机进程的启动而存在，有些区域则是依赖用户线程的启动和结束而建立和销毁。具体如下图所示：</p><p><img src="https://pic.yupoo.com/crowhawk/3d24df02/776c8d55.png" alt="" /></p><p>Demo 图：<br /><img src="https://notes.suremotoo.site/upload/2020/06/JVM-fe13f746922c4ff8bd914b5a1fb3a12b.png" alt="JVM" /></p><h4 id="程序计数器program-counter-register">程序计数器（Program Counter Register）</h4><p><strong>程序计数器（Program Counter Register）<strong>是一块较小的内存空间，可以看作是当前线程所执行的字节码的</strong>行号指示器</strong>。在虚拟机概念模型中，<strong>字节码解释器</strong>工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令，分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。</p><p>程序计数器是一块 <strong>“线程私有”</strong> 的内存，如上文的图所示，每条线程都有一个独立的程序计数器，各条线程之间的计数器互不影响，独立存储。这样设计使得在多线程环境下，线程切换后能恢复到正确的执行位置。</p><p>如果线程正在执行的是一个<strong>Java 方法</strong>，这个计数器记录的是正在执行的<strong>虚拟机字节码指令的地址；<strong>若执行的是</strong>Native 方法</strong>，则<strong>计数器为空（Undefined）</strong>（因为对于 Native 方法而言，它的方法体并不是由 Java 字节码构成的，自然无法应用上述的 “字节码指令的地址” 的概念）。程序计数器也是唯一一个在 Java 虚拟机规范中没有规定任何<strong>OutOfMemoryError</strong>情况的内存区域。</p><h4 id="java-虚拟机栈java-virtual-machine-stacks">Java 虚拟机栈（Java Virtual Machine Stacks）</h4><p><strong>Java 虚拟机栈（Java Virtual Machine Stacks）<strong>描述的是 <strong>Java 方法执行的内存模型</strong>：每个方法在执行的同时都会创建一个</strong>栈帧（Stack Frame）</strong>，栈帧中存储着<strong>局部变量表</strong>、<strong>操作数栈</strong>、<strong>动态链接</strong>、<strong>方法出口</strong>等信息。<strong>每一个方法从调用直至执行完成的过程，会对应一个栈帧在虚拟机栈中入栈到出栈的过程。<strong>与程序计数器一样，Java 虚拟机栈也是</strong>线程私有</strong>的。</p><p>函数的调用有完美的嵌套关系——调用者的生命期总是长于被调用者的生命期，并且后者在前者的之内。这样，被调用者的局部信息所占空间的分配总是后于调用者的（后入），而其释放则总是先于调用者的（先出），所以正好可以满足栈的 LIFO 顺序，选用栈这种数据结构来实现调用栈是一种很自然的选择。</p><p><strong>局部变量表</strong>中存放了编译期可知的各种：</p><p><strong>基本数据类型</strong>（boolen、byte、char、short、int、 float、 long、double）<br /><strong>对象引用</strong>（reference 类型，它不等于对象本身，可能是一个指向对象起始地址的指针，也可能是指向一个代表对象的句柄或其他与此对象相关的位置）<br /><strong>returnAddress 类型</strong>（指向了一条字节码指令的地址）</p><p>其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量空间（Slot），其余数据类型只占用 1 个。<strong>局部变量表所需的内存空间在编译期间完成分配</strong>，当进入一个方法时，这个方法需要在帧中分配多大的局部变量空间是完全确定的，在方法运行期间不会改变局部变量表的大小。</p><p>Java 虚拟机规范中对这个区域规定了两种异常状况：</p><p><strong>StackOverflowError</strong>：线程请求的栈深度大于虚拟机所允许的深度，将会抛出此异常。<br /><strong>OutOfMemoryError</strong>：当可动态扩展的虚拟机栈在扩展时无法申请到足够的内存，就会抛出该异常。</p><h4 id="本地方法栈native-method-stack">本地方法栈（Native Method Stack）</h4><p><strong>本地方法栈（Native Method Stack）</strong> 与 Java 虚拟机栈作用很相似，它们的区别在于虚拟机栈为虚拟机执行 Java 方法（即字节码）服务，而本地方法栈则为虚拟机使用到的 Native 方法服务。</p><p>在虚拟机规范中对本地方法栈中使用的语言、方式和数据结构并无强制规定，因此具体的虚拟机可实现它。甚至<strong>有的虚拟机（Sun HotSpot 虚拟机）直接把本地方法栈和虚拟机栈合二为一</strong>。与虚拟机一样，本地方法栈会抛出 <strong>StackOverflowError</strong> 和 <strong>OutOfMemoryError</strong> 异常。</p><h4 id="java-堆heap">Java 堆（Heap）</h4><p>对于大多数应用而言，<strong>Java 堆（Heap）<strong>是 Java 虚拟机所管理的内存中最大的一块，它</strong>被所有线程共享的</strong>，在虚拟机启动时创建。此内存区域<strong>唯一的目的</strong>是<strong>存放对象实例</strong>，几乎所有的对象实例都在这里分配内存，且每次分配的空间是<strong>不定长</strong>的。在 Heap 中分配一定的内存来保存对象实例，实际上只是保存<strong>对象实例的属性值</strong>，<strong>属性的类型</strong>和<strong>对象本身的类型标记</strong>等，<strong>并不保存对象的方法（方法是指令，保存在 Stack 中）</strong>, 在 Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。对象实例在 Heap 中分配好以后，需要<strong>在 Stack 中保存一个 4 字节的 Heap 内存地址</strong>，用来定位该对象实例在 Heap 中的位置，便于找到该对象实例。</p><p>Java 虚拟机规范中描述道：所有的对象实例以及数组都要在堆上分配，但是随着 JIT 编译器的发展和逃逸分析技术逐渐成熟，栈上分配、标量替换优化技术将会导致一些微妙的变化发生，所有的对象都在堆上分配的定论也并不 <strong>“绝对”</strong> 了。</p><p>Java 堆是垃圾收集器管理的主要区域，因此也被称为 <strong>“GC 堆（Garbage Collected Heap）”</strong>。从内存回收的角度看内存空间可如下划分： <img src="https://pic.yupoo.com/crowhawk/5cf46998/fe5079d3.png" alt="" /></p><p><strong>新生代（Young）</strong>： 新生成的对象优先存放在新生代中，新生代对象朝生夕死，存活率很低。在新生代中，常规应用进行一次垃圾收集一般可以回收 70% ~ 95% 的空间，回收效率很高。新生代又可细分为 <strong>Eden 空间</strong>、<strong>From Survivor 空间</strong>、<strong>To Survivor 空间</strong>，默认比例为 8:1:1。它们的具体作用将在下一篇文章讲解 GC 时介绍。<br /><strong>老年代（Tenured/Old）</strong>：在新生代中经历了多次（具体看虚拟机配置的阀值）GC 后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长，存活率比较高，在老年代中进行 GC 的频率相对而言较低，而且回收的速度也比较慢。<br /><strong>永久代（Perm）</strong>：永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据，对这一区域而言，Java 虚拟机规范指出可以不进行垃圾收集，一般而言不会进行垃圾回收。</p><p>其中<strong>新生代和老年代组成了 Java 堆的全部内存区域</strong>，而<strong>永久代不属于堆空间，它在 JDK 1.8 以前被 Sun HotSpot 虚拟机用作方法区的实现</strong>，关于方法区的具体内容将在稍后介绍。</p><h4 id="方法区method-area">方法区（Method Area）</h4><p><strong>方法区（Method Area）</strong> 与 Java 堆一样，是各个线程共享的内存区域。 <strong>Object Class Data(类定义数据)</strong> 是存储在方法区的，此外，<strong>常量</strong>、<strong>静态变量</strong>、<strong>JIT 编译后的代码</strong>也存储在方法区。正因为方法区所存储的数据与堆有一种类比关系，所以它还被称为 <strong>Non-Heap</strong>。</p><p><strong>JDK 1.8 以前的永久代（PermGen）</strong></p><p>Java 虚拟机规范对方法区的限制非常宽松，除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外，还可以选择不实现垃圾收集，也就是说，Java 虚拟机规范只是规定了方法区的概念和它的作用，并没有规定如何去实现它。<strong>对于 JDK 1.8 之前的版本，HotSpot 虚拟机设计团队选择把 GC 分代收集扩展至方法区，即用永久代来实现方法区</strong>，这样 HotSpot 的垃圾收集器可以像管理 Java 堆一样管理这部分内存，能够省去专门为方法区编写内存管理代码的工作。对于其他的虚拟机（如<strong>Oracle JRockit</strong>、<strong>IBM J9</strong>等）来说是不存在永久代的概念的。</p><p>如果运行时有大量的类产生，可能会导致方法区被填满，直至溢出。常见的应用场景如：</p><ul><li>Spring 和 ORM 框架使用 CGLib 操纵字节码对类进行增强，增强的类越多，就需要越大的方法区来保证动态生成的 Class 可以加载入内存。</li><li>大量 JSP 或动态产生 JSP 文件的应用（JSP 第一次运行时需要编译为 Java 类）。</li><li>基于 OSGi 的应用（即使是同一个类文件，被不同的类加载器加载也会视为不同的类）。 ……</li></ul><p>这些都会导致方法区溢出，报出 <code>java.lang.OutOfMemoryError: PermGen space</code>。</p><p><strong>JDK 1.8 的元空间（Metaspace）</strong></p><p>在 JDK 1.8 中，HotSpot 虚拟机设计团队为了促进<strong>HotSpot</strong>与<strong>JRockit</strong>的融合，修改了方法区的实现，移除了永久代，选择使用<strong>本地化的内存空间</strong>（而不是 JVM 的内存空间）存放类的元数据，这个空间叫做<strong>元空间（Metaspace）</strong>。</p><p>做了这个改动以后，<code>java.lang.OutOfMemoryError: PermGen</code> 的空间问题将不复存在，并且不再需要调整和监控这个内存空间。且虚拟机需要为方法区设计额外的 GC 策略：如果类元数据的空间占用达到参数 <strong>“MaxMetaspaceSize”<strong>设置的值，将会触发对死亡对象和类加载器的垃圾回收。 为了限制垃圾回收的频率和延迟，适当的监控和调优</strong>元空间</strong>是非常有必要的。元空间过多的垃圾收集可能表示类、类加载器内存泄漏或对你的应用程序来说空间太小了。</p><p>元空间的内存管理由<strong>元空间虚拟机</strong>来完成。先前，对于类的元数据我们需要不同的垃圾回收器进行处理，现在只需要执行元空间虚拟机的 C++ 代码即可完成。<strong>在元空间中，类和其元数据的生命周期</strong>和<strong>其对应的类加载器</strong>是相同的。话句话说，<strong>只要类加载器存活，其加载的类的元数据也是存活的</strong>，因而不会被回收掉。</p><p>我们从行文到现在提到的元空间稍微有点不严谨。准确的来说，<strong>每一个 <em>类加载器的存储区域</em> 都称作一个元空间，所有的元空间合在一起就是我们一直说的元空间。</strong> 当一个类加载器被垃圾回收器标记为不再存活，其对应的元空间会被回收。在元空间的回收过程中没有重定位和压缩等操作。但是元空间内的元数据会进行扫描来确定 Java 引用。</p><p><strong>元空间虚拟机</strong>负责元空间的分配，其采用的形式为<strong>组块分配</strong>。组块的大小因类加载器的类型而异。在元空间虚拟机中存在一个<strong>全局的空闲组块列表</strong>。当一个类加载器需要组块时，它就会从这个全局的组块列表中获取并维持一个自己的组块列表。当一个类加载器不再存活，那么其持有的组块将会被释放，并返回给全局组块列表。类加载器持有的组块又会被分成多个块，每一个块存储一个单元的元信息。组块中的块<strong>是线性分配（指针碰撞分配形式）</strong>。组块分配自内存映射区域。这些全局的虚拟内存映射区域以链表形式连接，一旦某个虚拟内存映射区域清空，这部分内存就会返回给操作系统。</p><p><img src="https://pic.yupoo.com/crowhawk/cdaea117/7bdf00c4.png" alt="" /></p><p>上图展示的是虚拟内存映射区域如何进行元组块的分配。类加载器 1 和 3 表明使用了反射或者为匿名类加载器，他们使用了特定大小组块。 而类加载器 2 和 4 根据其内部条目的数量使用小型或者中型的组块。</p><p><strong>运行时常量池（Runtime Constant Pool）</strong></p><p><strong>运行时常量池（Runtime Constant Pool）<strong>是方法区的一部分。<strong>Class 文件</strong>中除了有类的版本、字段、方法、接口等描述信息外，还有一项信息是</strong>常量池（Constant Pool Table）</strong>，用于存放编译期生成的各种字面量和符号引用，<strong>这部分内容将在类加载后进入方法区的运行时常量池存放</strong>。</p><p>Java 虚拟机对 Class 文件每一部分（自然包括常量池）的格式有严格规定，每一个字节用于存储那种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行。但<strong>对于运行时常量池，Java 虚拟机规范没有做任何有关细节的要求</strong>，不同的提供商实现的虚拟机可以按照自己的需求来实现此内存区域。不过一般而言，除了保存 <strong>Class 文件中的描述符号引用</strong>外，还会把<strong>翻译出的直接引用</strong>也存储在运行时常量池中。</p><p>运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备<strong>动态性</strong>，Java 语言并不要求常量一定只有编译器才能产生，也就是<strong>并非置入 Class 文件中的常量池的内容才能进入方法区运行时常量池，运行期间也可能将新的常量放入池中</strong>，此特性被开发人员利用得比较多的便是 String 类的 <code>intern()</code> 方法。</p><h4 id="直接内存">直接内存</h4><p><strong>直接内存（Direct Memory）<strong>并不是虚拟机</strong>运行时数据区</strong>的一部分，也不是 Java 虚拟机规范中定义的内存区域。但这部分内存也被频繁运用，而却可能导致 <strong>OutOfMemoryError</strong> 异常出现，所以这里放到一起讲解。</p><p>以<strong>NIO（New Input/Output）</strong> 类为例，NIO 引入了一种基于通道（Channel）与缓冲区（Buffer）的 I/O 方式，它可以使用 Native 函数库直接分配堆外内存，然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能避免在 Java 堆和 Native 堆中来回复制数据，在一些场景里显著提高性能。</p><p>本机直接内存的分配不会受到 Java 堆大小的限制，但是既然是内存，还是会受到本机总内存（包括 RAM 以及 SWAP 区或分页文件）大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时，会根据实际内存设置 - Xmx 等参数信息，但经常忽略直接内存，使得各个内存区域总和大于物理内存限制（包括物理的和操作系统的限制），从而导致动态扩展时出现 <strong>OutOfMemoryError</strong> 异常。</p><h4 id="对象的创建">对象的创建</h4><p>Java 的对象创建大致有如下四种方式：</p><ol><li><strong>new 关键字</strong>这应该是我们最常见和最常用最简单的创建对象的方式。</li><li><strong>使用 <code>newInstance()</code> 方法</strong>这里包括 <strong>Class</strong> 类的 <code>newInstance()</code> 方法和<strong>Constructor</strong>类的 <code>newInstance()</code> 方法（前者其实也是调用的后者）。</li><li><strong>使用  <code>clone()</code> 方法</strong>要使用 <code>clone()</code> 方法我们必须实现实现 <strong>Cloneable</strong> 接口，用 <code>clone()</code> 方法创建对象并不会调用任何构造函数。即我们所说的 <strong>浅拷贝</strong>。</li><li><strong>反序列化</strong>要实现反序列化我们需要让我们的类实现 <strong>Serializable</strong> 接口。当我们序列化和反序列化一个对象，JVM 会给我们创建一个单独的对象，在反序列化时，JVM 创建对象并不会调用任何构造函数。即我们所说的<strong>深拷贝</strong>。</li></ol><p>上面的四种创建对象的方法除了第一种使用 new 指令之外，其他三种都是使用 <strong>invokespecial(构造函数的直接调用)</strong>。这里我们只说 new 创建对象的方式，关于 invokespecial 的内容将在后续文章中介绍。下面我们来看看当虚拟机遇到 new 指令的时候对象是如何创建的。</p><p><strong>1. 类加载检查</strong></p><p>虚拟机遇到一条 new 指令时，首先将去检查<strong>这个指令的参数是否能在常量池中定位到一个类的符号引用</strong>，并且检查<strong>这个符号引用代表的类是否已被加载、解析和初始化过的</strong>，如果没有，则必须先执行相应的类加载过程，关于类加载机制和类加载器的详细内容将在后续文章中介绍。</p><p><strong>2. 分配内存</strong></p><p>在类加载检查通过后，虚拟机就将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定（如何确定在下一节对象内存布局时再详细讲解），为对象分配空间的任务具体便等同于<strong>从 Java 堆中划出一块大小确定的内存空间</strong>，可以分如下两种情况讨论：</p><p><strong>Java 堆中内存绝对规整</strong>所有用过的内存都被放在一边，空闲的内存被放在另一边，<strong>中间放着一个指针作为分界点的指示器</strong>，那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离，这种分配方式称为 <strong>“指针碰撞”（Bump The Pointer）</strong>。<br /><strong>Java 堆中的内存不规整</strong>已被使用的内存和空闲的内存相互交错，那就没有办法简单的进行指针碰撞了，虚拟机就必须<strong>维护一个列表，记录哪些内存块是可用的</strong>，在分配的时候从列表中找到一块足够大的空间划分给对象实例，并更新列表上的记录，这种分配方式称为 <strong>“空闲列表”（Free List）</strong>。</p><p>选择哪种分配方式由 Java 堆是否规整决定，而 Java 堆是否规整又由所采用的<strong>垃圾收集器是否带有压缩整理功能</strong>决定。因此在使用 Serial、ParNew 等带 <strong>Compact</strong> 过程的收集器时，系统采用的分配算法是<strong>指针碰撞</strong>，而使用 CMS 这种基于 <strong>Mark-Sweep</strong> 算法的收集器时（说明一下，CMS 收集器可以通过 UseCMSCompactAtFullCollection 或 CMSFullGCsBeforeCompaction 来整理内存），就通常采用<strong>空闲列表</strong>。关于垃圾收集器的具体内容将在下一篇文章中介绍。</p><p>除如何划分可用空间之外，另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为，即使是仅仅修改一个指针所指向的位置，在并发情况下也<strong>并非线程安全</strong>的，可能出现正在给对象 A 分配内存，指针还没来得及修改，对象 B 又同时使用了原来的指针来分配内存。解决这个问题有如下两个方案：</p><p><strong>对分配内存空间的动作进行同步</strong>实际上虚拟机是采用 <strong>CAS</strong> 配上<strong>失败重试</strong>的方式保证更新操作的原子性。<br /><strong>把内存分配的动作按照线程划分在不同的空间之中进行</strong>即每个线程在 Java 堆中预先分配一小块内存，称为<strong>本地线程分配缓冲（TLAB ，Thread Local Allocation Buffer）</strong>，哪个线程要分配内存，就在哪个线程的 TLAB 上分配，只有 TLAB 用完，分配新的 TLAB 时才需要同步锁定。虚拟机是否使用 TLAB，可以通过 <strong>-XX:+/-UseTLAB</strong> 参数来设定。</p><p><strong>3. 初始化</strong></p><p>内存分配完成之后，虚拟机需要<strong>将分配到的内存空间都初始化为零值（不包括对象头）</strong>，如果使用 TLAB 的话，这一个工作也可以提前至 TLAB 分配时进行。这步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用。</p><p><strong>4. 设置对象头</strong></p><p>接下来，虚拟机要<strong>设置对象的信息</strong>（如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息）并存放在对象的<strong>对象头（Object Header）</strong> 中。根据虚拟机当前的运行状态的不同，如是否启用偏向锁等，对象头会有不同的设置方式。关于对象头的具体内容，在下一节再详细介绍。</p><p><strong>5. 执行 <code>&lt;init&gt;</code> 方法</strong></p><p>在上面工作都完成之后，在虚拟机的视角来看，一个新的对象已经产生了。但是在 Java 程序的视角看来，对象创建才刚刚开始——<code>&lt;init&gt;</code> 方法还没有执行，所有的字段都还为零值。所以一般来说（由字节码中是否跟随有 invokespecial 指令所决定），new 指令之后会接着执行 <code>&lt;init&gt;</code> 方法，把对象按照程序员的意愿进行初始化，这样一个真正可用的对象才算完全产生出来。</p><h4 id="对象的内存布局">对象的内存布局</h4><p>HotSpot 虚拟机中，对象在内存中存储的布局可以分为三块区域：<strong>对象头（Header）</strong>、<strong>实例数据（Instance Data）<strong>和</strong>对齐填充（Padding）</strong>。</p><p><strong>1. 对象头</strong></p><p>HotSpot 虚拟机的对象头包括两部分信息：</p><p><strong>对象自身的运行时数据  “Mark Word”</strong> 如哈希码（HashCode）、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等，这部分数据的长度在 32 位和 64 位的虚拟机（暂不考虑开启压缩指针的场景）中分别为 32 个和 64 个 Bits，官方称它为 <strong>“Mark Word”</strong>。对象需要存储的运行时数据很多，其实已经超出了 32、64 位 Bitmap 结构所能记录的限度，但是对象头信息是与对象自身定义的数据无关的额外存储成本，考虑到虚拟机的空间效率，Mark Word 被设计成一个<strong>非固定的数据结构</strong>以便在极小的空间内存储尽量多的信息，它会<strong>根据对象的状态复用自己的存储空间</strong>。例如在 32 位的 HotSpot 虚拟机中对象<strong>未被锁定</strong>的状态下，Mark Word 的 32 个 Bits 空间中的 25Bits 用于存储对象哈希码（HashCode），4Bits 用于存储对象分代年龄，2Bits 用于存储锁标志位，1Bit 固定为 0，在其他状态（轻量级锁定、重量级锁定、GC 标记、可偏向）下对象的存储内容如下图所示：<br /><img src="https://pic.yupoo.com/crowhawk/4f006175/8be38542.png" alt="" /></p><p><strong>类型指针</strong>类型指针即<strong>对象指向它的类元数据的指针</strong>，虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针，换句话说<strong>查找对象的元数据信息并不一定要经过对象本身</strong>，这点我们在下一节讨论。另外，如果对象是一个 Java 数组，那在对象头中还必须有一块用于<strong>记录数组长度</strong>的数据，因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小，但是从数组的元数据中无法确定数组的大小。</p><p><strong>2. 实例数据</strong></p><p>实例数据是对象真正存储的有效信息，也既是我们在程序代码里面所定义的各种类型的字段内容，无论是从父类继承下来的，还是在子类中定义的都需要记录起来。这部分的存储顺序会受到虚拟机分配策略参数（FieldsAllocationStyle）和字段在 Java 源码中定义顺序的影响。HotSpot 虚拟机默认的分配策略为 longs/doubles、ints、shorts/chars、bytes/booleans、oops（Ordinary Object Pointers），从分配策略中可以看出，相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下，在父类中定义的变量会出现在子类之前。如果 CompactFields 参数值为 true（默认为 true），那子类之中较窄的变量也可能会插入到父类变量的空隙之中。</p><p><strong>3. 对齐填充</strong></p><p>对齐填充并不是必然存在的，也没有特别的含义，它仅仅起着占位符的作用。由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍，换句话说就是对象的大小必须是 8 字节的整数倍。对象头部分正好似 8 字节的倍数（1 倍或者 2 倍），因此当对象实例数据部分没有对齐的话，就需要通过对齐填充来补全。</p><h4 id="对象的访问定位">对象的访问定位</h4><p>我们的 Java 程序需要通过<strong>栈上的对象引用（reference）数据（存储在栈上的局部变量表中）<strong>来操作堆上的具体对象。由于 reference 类型在 Java 虚拟机规范里面也只规定了是一个指向对象的引用，并没有定义这个引用的具体实现，对象访问方式也是取决于虚拟机实现而定的。主流的访问方式有使用</strong>句柄</strong>和<strong>直接指针</strong>两种。</p><p><strong>1. 使用句柄访问</strong></p><p>如果使用句柄访问的话，<strong>Java 堆中</strong>将会划分出一块内存来作为<strong>句柄池</strong>，reference 中存储的就是对象的句柄地址，而句柄中包含了<strong>对象实例数据</strong>与<strong>类型数据</strong>的各自的<strong>具体地址信息</strong>。如下图所示：</p><p><img src="https://pic.yupoo.com/crowhawk/af3c02ef/bfd967c5.png" alt="" /></p><p><strong>2. 使用直接指针访问</strong></p><p>如果使用直接指针访问的话，Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息，reference 中存储的直接就是对象地址，如下图所示：</p><p><img src="https://pic.yupoo.com/crowhawk/5c1acdb8/f5086a4d.png" alt="" /></p><hr /><p>这两种对象访问方式各有优势，下面分别来谈一谈：</p><p><strong>句柄</strong>使用句柄访问的最大好处就是 <strong>reference 中存储的是稳定的句柄地址</strong>，在对象被移动（垃圾收集时移动对象是非常普遍的行为）时<strong>只会改变句柄中的实例数据指针，而 reference 本身不需要被修改</strong>。<br /><strong>直接指针</strong>使用直接指针来访问最大的好处就是<strong>速度更快</strong>，它<strong>节省了一次指针定位的时间开销</strong>，由于对象访问的在 Java 中非常频繁，因此这类开销积小成多也是一项 非常可观的执行成本。从上一部分讲解的对象内存布局可以看出，<strong>HotSpot 是使用直接指针进行对象访问的</strong>，不过在整个软件开发的范围来 看，各种语言、框架中使用句柄来访问的情况也十分常见。</p><p>参考：</p><ul><li><a href="https://book.douban.com/subject/24722612/">《深入理解 Java 虚拟机——JVM 高级特性与最佳实践》－周志明</a></li></ul><p>From: <a href="https://crowhawk.github.io/2017/08/09/jvm_1/">https://crowhawk.github.io/2017/08/09/jvm_1/</a></p>]]>
                    </description>
                    <pubDate>Sun, 14 Jun 2020 01:20:44 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[浅析 Java 中的 TLAB]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/java-tlab</link>
                    <description>
                            <![CDATA[<p>From: <a href="https://www.jianshu.com/p/8be816cbb5ed">https://www.jianshu.com/p/8be816cbb5ed</a></p><h3 id="关于-jvm-中的-tlab">关于 JVM 中的 TLAB。</h3><h4 id="什么是-tlab">什么是 TLAB？</h4><p>它是干什么的？咱们先抛开这个问题，一切的开始得从 new 对象到指针碰撞开始讲起。</p><blockquote><h5 id="new-对象与指针碰撞">new 对象与指针碰撞</h5></blockquote><p>new 对象怎么就出问题了呢？<br />Java 中我们要创建一个对象, 用关键字 new 就可以了。但是，在我们日常中，有很多生命周期很短的对象。比如：</p><pre><code class="language-java">    public void dome(){    User user=new user();        user.sayhi();    }</code></pre><p>这种对象的作用域都不会逃逸出方法外，也就是说该对象的生命周期会随着方法的调用开始而开始，方法的调用结束而结束。<br />假设 JVM 所有的对象都放在堆内存中 (为什么用假设，因为 JVM 并不是这样) 一旦方法结束，没有了指向该对象的引用，该对象就需要被 GC 回收，如果存在很多这样的情况，对 GC 来说压力山大呀。</p><h4 id="那么什么又是指针碰撞呢">那么什么又是指针碰撞呢？</h4><p>假设 JVM 虚拟机上，堆内存都是规整的。堆内存被一个指针一分为二。指针的左边都被塞满了对象，指针的右变是未使用的区域。每一次有新的对象创建，指针就会向右移动一个对象 size 的距离。这就被称为指针碰撞。</p><p><img src="https://notes.suremotoo.site/upload/2020/06/%E6%8C%87%E9%92%88%E7%A2%B0%E6%92%9E-a4b48f0ed1db454d9cba1aff84cc9318.png" alt="指针碰撞" /></p><p>好，问题来了。如果我们多线程执行刚才那个 dome 方法，一个线程正在给 A 对象分配内存，指针还没有来的及修改，其它为 B 对象分配内存的线程，而且还是引用这之前的指针指向。这样就出现毛病了。<br />(要注意的是，上面两种情况解决方案不止一个，我今天主要是讲 TLAB，其他方案自行查询)</p><blockquote><h5 id="tlab-的出现">TLAB 的出现</h5></blockquote><p>我们现在已经搞清楚，我们出现了哪些问题。我在为大家介绍一下今天的主角。</p><p>TLAB 的全称是 <strong>Thread Local Allocation Buffer</strong>，即<strong>线程本地分配缓存区</strong>，这是一个<strong>线程专用的内存分配区域</strong>。</p><p>如果设置了虚拟机参数 <strong>-XX:UseTLAB</strong>，在线程初始化时，同时也会申请一块指定大小的内存，只给当前线程使用，这样每个线程都单独拥有一个空间，如果需要分配内存，就在自己的空间上分配，这样就不存在竞争的情况，可以大大提升分配效率。</p><p>TLAB 空间的内存非常小，缺省情况下仅占有整个 Eden 空间的 1%，也可以通过选项 <strong>- XX:TLABWasteTargetPercent</strong> 设置 TLAB 空间所占用 Eden 空间的百分比大小。</p><p>TLAB 的本质其实是三个指针管理的区域：start，top 和 end，每个线程都会从 Eden 分配一块空间，例如说 100KB，作为自己的 TLAB，其中 start 和 end 是占位用的，标识出 eden 里被这个 TLAB 所管理的区域，卡住 Eden 里的一块空间不让其它线程来这里分配。</p><p>TLAB 只是让每个线程有私有的分配指针，但底下存对象的内存空间还是给所有线程访问的，只是其它线程无法在这个区域分配而已。从这一点看，它被翻译为 线程私有分配区 更为合理一点<br />当一个 TLAB 用满（分配指针 top 撞上分配极限 end 了），就新申请一个 TLAB，而在老 TLAB 里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从 TLAB 分配出来的，而只关心自己是在 eden 里分配的。</p><blockquote><h5 id="tlab-的缺点">TLAB 的缺点</h5></blockquote><p>事务总不是完美的，TLAB 也又自己的缺点。因为 TLAB 通常很小，所以放不下大对象。</p><ol><li>TLAB 空间大小是固定的，但是这时候一个大对象，我 TLAB 剩余的空间已经容不下它了。(比如 100kb 的 TLAB，来了个 110KB 的对象)</li><li>TLAB 空间还剩一点点没有用到，有点舍不得。(比如 100kb 的 TLAB，装了 80KB，又来了个 30KB 的对象)<br />所以 JVM 开发人员做了以下处理，设置了最大浪费空间。<br />当剩余的空间小于最大浪费空间，那该 TLAB 属于的线程在重新向 Eden 区申请一个 TLAB 空间。进行对象创建，还是空间不够，那你这个对象太大了，去 Eden 区直接创建吧！<br />当剩余的空间大于最大浪费空间，那这个大对象请你直接去 Eden 区创建，因为我 TLAB 没有使用完的空间还是放不下你。</li></ol><p>当然，又会造成新的病垢。<br />3. Eden 空间够的时候，你再次申请 TLAB 没问题，我不够了，Heap 的 Eden 区要开始 GC。<br />4. TLAB 允许浪费空间，导致 Eden 区空间不连续，积少成多。以后还要人帮忙打理。</p>]]>
                    </description>
                    <pubDate>Wed, 10 Jun 2020 10:39:40 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[高并发编程学习(2)——线程通信详解]]>
                    </title>
                    <link>https://notes.suremotoo.cc/archives/concurrent-2</link>
                    <description>
                            <![CDATA[<p>From: <a href="https://www.javazhiyin.com/54980.html">https://www.javazhiyin.com/54980.html</a></p><blockquote><p>前序文章</p><ul><li>高并发编程学习(1)——并发基础 - <a href="https://www.wmyskxz.com/2019/11/26/gao-bing-fa-bian-cheng-xue-xi-1-bing-fa-ji-chu/">https://www.wmyskxz.com/2019/11/26/gao-bing-fa-bian-cheng-xue-xi-1-bing-fa-ji-chu/</a><a href="https://www.javazhiyin.com/wp-content/uploads/2019/11/java4-1574849271.png" title="高并发编程学习(2)——线程通信详解">1</a></li></ul></blockquote><hr /><p>上一篇文章我们提到一个应用可以创建多个线程去执行不同的任务，如果这些任务之间有着某种关系，那么线程之间<strong>必须能够通信</strong>来协调完成工作。</p><p><img src="https://www.javazhiyin.com/wp-content/uploads/2019/11/java4-1574849271.png" alt="高并发编程学习(2)——线程通信详解" title="高并发编程学习(2)——线程通信详解" /></p><p><strong>生产者消费者问题</strong>（英语：Producer-consumer problem）就是典型的多线程同步案例，它也被称为<strong>有限缓冲问题</strong>（英语：Bounded-buffer problem）。该问题描述了共享固定大小缓冲区<a href="https://www.javazhiyin.com/wp-content/uploads/2019/11/java6-1574849271.jpg" title="高并发编程学习(2)——线程通信详解">2</a>的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中，然后重复此过程。与此同时，消费者也在缓冲区消耗这些数据。<strong>该问题的关键就是要保证生产者不会在缓冲区满时加入数据，消费者也不会在缓冲区中空时消耗数据。</strong>(摘自维基百科：生产者消费者问题<a href="https://www.javazhiyin.com/wp-content/uploads/2019/11/java0-1574849271.png" title="高并发编程学习(2)——线程通信详解">3</a>)</p><p><strong>注意</strong>：生产者-消费者模式中的内存缓存区的主要功能是数据在多线程间的共享，此外，通过该缓冲区，可以缓解生产者和消费者的性能差；</p><h2 id="准备基础代码无通信的生产者消费者">准备基础代码：无通信的生产者消费者</h2><p>我们来自己编写一个例子：一个生产者，一个消费者，并且让他们让他们使用同一个共享资源，并且我们期望的是生产者生产一条放到共享资源中，消费者就会对应地消费一条。</p><p>我们先来模拟一个简单的共享资源对象：</p><pre><code class="language-java">public class ShareResource {    private String name;    private String gender;    /**     * 模拟生产者向共享资源对象中存储数据     *     * @param name     * @param gender     */    public void push(String name, String gender) {        this.name = name;        this.gender = gender;    }    /**     * 模拟消费者从共享资源中取出数据     */    public void popup() {        System.out.println(this.name + &quot;-&quot; + this.gender);    }}</code></pre><p>然后来编写我们的生产者，使用循环来交替地向共享资源中添加不同的数据：</p><pre><code class="language-java">public class Producer implements Runnable {    private ShareResource shareResource;    public Producer(ShareResource shareResource) {        this.shareResource = shareResource;    }    @Override    public void run() {        for (int i = 0; i &lt; 50; i++) {            if (i % 2 == 0) {                shareResource.push(&quot;凤姐&quot;, &quot;女&quot;);            } else {                shareResource.push(&quot;张三&quot;, &quot;男&quot;);            }        }    }}</code></pre><p>接着让我们的消费者不停地消费生产者产生的数据：</p><pre><code class="language-java">public class Consumer implements Runnable {    private ShareResource shareResource;    public Consumer(ShareResource shareResource) {        this.shareResource = shareResource;    }    @Override    public void run() {        for (int i = 0; i &lt; 50; i++) {            shareResource.popup();        }    }}</code></pre><p>然后我们写一段测试代码，来看看效果：</p><pre><code class="language-java">public static void main(String[] args) {    // 创建生产者和消费者的共享资源对象    ShareResource shareResource = new ShareResource();    // 启动生产者线程    new Thread(new Producer(shareResource)).start();    // 启动消费者线程    new Thread(new Consumer(shareResource)).start();}</code></pre><p>我们运行发现出现了诡异的现象，所有的生产者都似乎消费到了同一条数据：</p><pre><code>    张三-男      张三-男      ....以下全是张三-男....  </code></pre><p>为什么会出现这样的情况呢？照理说，我的生产者在交替地向共享资源中生产数据，消费者也应该交替消费才对呀..我们大胆猜测一下，会不会是因为消费者是直接循环了 30 次打印共享资源中的数据，而此时生产者还没有来得及更新共享资源中的数据，消费者就已经连续打印了 30 次了，所以我们让消费者消费的时候以及生产者生产的时候都小睡个 10 ms 来缓解消费太快 or 生产太快带来的影响，也让现象更明显一些：</p><pre><code class="language-java">/** * 模拟生产者向共享资源对象中存储数据 * * @param name * @param gender */public void push(String name, String gender) {    try {        Thread.sleep(10);    } catch (InterruptedException ignored) {    }    this.name = name;    this.gender = gender;}/** * 模拟消费者从共享资源中取出数据 */public void popup() {    try {        Thread.sleep(10);    } catch (InterruptedException ignored) {    }    System.out.println(this.name + &quot;-&quot; + this.gender);}</code></pre><p>再次运行代码，发现了出现了以下的几种情况：</p><ul><li><strong>重复消费</strong>：消费者连续地出现两次相同的消费情况（张三-男/ 张三-男）；</li><li><strong>性别紊乱</strong>：消费者消费到了脏数据（张三-女/ 凤姐-男）；</li></ul><h2 id="分析出现问题的原因">分析出现问题的原因</h2><ul><li><strong>重复消费</strong>：我们先来看看<strong>重复消费</strong>的问题，当生产者生产出一条数据的时候，消费者正确地消费了一条，但是当消费者再来共享资源中消费的时候，生产者还没有准备好新的一条数据，所以消费者就又消费到老数据了，<strong>这其中的根本原因是生产者和消费者的速率不一致</strong>。</li><li><strong>性别紊乱</strong>：再来分析第二种情况。不同于上面的情况，消费者在消费第二条数据时，生产者也正在生产新的数据，但是尴尬的是，生产者只生产了一半儿（也就是该执行完 <code>this.name = name</code>），也就是还没有来得及给 <code>gender</code> 赋值就被消费者给取走消费了.. 造成这样情况的<strong>根本原因是没有保证生产者生产数据的原子性</strong>。</li></ul><h2 id="解决出现的问题">解决出现的问题</h2><h3 id="加锁解决性别紊乱">加锁解决性别紊乱</h3><p>我们先来解决<strong>性别紊乱</strong>，也就是<strong>原子性</strong>的问题吧，上一篇文章里我们也提到了，对于这样的原子性操作，解决方法也很简单：<strong>加锁</strong>。稍微改造一下就好了：</p><pre><code class="language-java">/** * 模拟生产者向共享资源对象中存储数据 * * @param name * @param gender */synchronized public void push(String name, String gender) {    this.name = name;    try {        Thread.sleep(10);    } catch (InterruptedException ignored) {    }    this.gender = gender;}/** * 模拟消费者从共享资源中取出数据 */synchronized public void popup() {    try {        Thread.sleep(10);    } catch (InterruptedException ignored) {    }    System.out.println(this.name + &quot;-&quot; + this.gender);}</code></pre><ul><li>我们在方法前面都加上了 <code>synchronized</code> 关键字，来保证每一次读取和修改都只能是一个线程，这是因为当 <code>synchronized</code> 修饰在普通同步方法上时，它会自动锁住当前实例对象，也就是说这样改造之后读/ 写操作同时只能进行其一；</li><li>我把 <code>push</code> 方法小睡的代码改在了赋值 <code>name</code> 和 <code>gender</code> 的中间，以强化验证原子性操作是否成功，因为如果不是原子性的话，就很可能出现赋值 <code>name</code> 还没赋值给 <code>gender</code> 就被取走的情况，小睡一会儿是为了加强这种情况的出现概率（可以试着把 <code>synchronized</code> 去掉看看效果）；</li></ul><p>运行代码后发现，并没有出现性别紊乱的现象了，但是重复消费仍然存在。</p><h3 id="等待唤醒机制解决重复消费">等待唤醒机制解决重复消费</h3><p>我们期望的是 <code>张三-男</code> 和 <code>凤姐-女</code> 交替出现，而不是有重复消费的情况，所以我们的生产者和消费者之间需要一点沟通，最容易想到的解决方法是，我们新增加一个标志位，然后在消费者中使用 <code>while</code> 循环判断，不满足条件则不消费，条件满足则退出 <code>while</code> 循环，从而完成消费者的工作。</p><pre><code class="language-java">while (value != desire) {    Thread.sleep(10);}doSomething();</code></pre><p>这样做的目的就是为了防止「过快的无效尝试」，这种方法看似能够实现所需的功能，但是却存在如下的问题：</p><p><strong>1）难以确保及时性</strong>。在睡眠时，基本不消耗处理器的资源，但是如果睡得过久，就不能及时发现条件已经变化，也就是及时性难以保证；<br /><strong>2）难以降低开销</strong>。如果降低睡眠的时间，比如休眠 1 毫秒，这样消费者能够更加迅速地发现条件变化，但是却可能消耗更多的处理资源，造成了无端的浪费。</p><p>以上两个问题吗，看似矛盾难以调和，但是 Java 通过内置的等待/ 通知机制能够很好地解决这个矛盾并实现所需的功能。</p><p><strong>等待/ 通知机制</strong>，是指一个线程 A 调用了对象 O 的 <code>wait()</code> 方法进入等待状态，而另一个线程 B 调用了对象 O 的 <code>notifyAll()</code> 方法，线程 A 收到通知后从对象 O 的 <code>wait()</code> 方法返回，进而执行后续操作。上述两个线程都是通过对象 O 来完成交互的，而对象上的 <code>wait</code> 和 <code>notify/ notifyAll</code> 的关系就如同<strong>开关信号</strong>一样，用来完成等待方和通知方之间的交互工作。</p><blockquote><p>这里有一个比较奇怪的点是，为什么看起来像是线程之间操作的 <code>wait</code> 和 <code>notify/ notifyAll</code> 方法会是 <code>Object</code> 类中的方法，而不是 <code>Thread</code> 类中的方法呢？</p><ul><li><strong>简单来说</strong>：因为 <code>synchronized</code> 中的这把锁可以是任意对象，因为要满足任意对象都能够调用，所以属于 <code>Object</code> 类；</li><li><strong>专业点说</strong>：因为这些方法在操作同步线程时，都必须要标识它们操作线程的锁，只有同一个锁上的被等待线程，可以被同一个锁上的 <code>notify</code> 唤醒，不可以对不同锁中的线程进行唤醒。<strong>也就是说，等待和唤醒必须是同一个锁</strong>。而锁可以是任意对象，所以可以被任意对象调用的方法是定义在 <code>Object</code> 类中。</li></ul></blockquote><p>好，简单介绍完等待/ 通知机制，我们开始改造吧：</p><pre><code class="language-java">public class ShareResource {    private String name;    private String gender;    // 新增加一个标志位，表示共享资源是否为空，默认为 true    privateboolean isEmpty = true;    /**     * 模拟生产者向共享资源对象中存储数据     *     * @param name     * @param gender     */    synchronized public void push(String name, String gender) {        try {            while (!isEmpty) {                // 当前共享资源不为空的时，则等待消费者来消费                // 使用同步锁对象来调用，表示当前线程释放同步锁，进入等待池，只能被其他线程所唤醒                this.wait();            }            // 开始生产            this.name = name;            Thread.sleep(10);            this.gender = gender;            // 生产结束            isEmpty = false;            // 生产结束唤醒一个消费者来消费            this.notify();        } catch (Exception ignored) {        }    }    /**     * 模拟消费者从共享资源中取出数据     */    synchronized public void popup() {        try {            while (isEmpty) {                // 为空则等着生产者进行生产                // 使用同步锁对象来调用，表示当前线程释放同步锁，进入等待池，只能被其他线程所唤醒                this.wait();            }            // 消费开始            Thread.sleep(10);            System.out.println(this.name + &quot;-&quot; + this.gender);            // 消费结束            isEmpty = true;            // 消费结束唤醒一个生产者去生产            this.notify();        } catch (InterruptedException ignored) {        }    }}</code></pre><ul><li>我们期望生产者生产一条，然后就去通知消费者消费一条，那么在生产和消费之前，都需要考虑当前是否需要生产 or 消费，所以我们新增了一个标志位来判断，如果不满足则等待；</li><li>被通知后仍然要检查条件，条件满足，则执行我们相应的生产 or 消费的逻辑，然后改变条件（这里是 <code>isEmpty</code>），并且通知所有等待在对象上的线程；</li><li><strong>注意</strong>：上面的代码中通知使用的 <code>notify()</code> 方法，这是因为例子中写死了只有一个消费者和生产者，在实际情况中建议还是使用 <code>notifyAll()</code> 方法，这样多个消费和生产者逻辑也能够保证（可以自己试一下）；</li></ul><h2 id="小结">小结</h2><p>通过初始版本一步步地分析问题和解决问题，我们就差不多写出了我们经典生产者消费者的经典代码，但通常消费和生产的逻辑是写在各自的消费者和生产者代码里的，这里我为了方便阅读，把他们都抽离到了共享资源上，我们可以简单地再来回顾一下这个消费生产和等待通知的整个过程：</p><p><img src="https://www.javazhiyin.com/wp-content/uploads/2019/11/java6-1574849271.jpg" alt="高并发编程学习(2)——线程通信详解" title="高并发编程学习(2)——线程通信详解" /></p><p>以上就是关于生产者生产一条数据，消费者消费一次的过程了，涉及的一些具体细节我们下面来说。</p><hr /><h2 id="等待唤醒机制的替代lock-和-condition">等待唤醒机制的替代：Lock 和 Condition</h2><p>我们从上面的中看到了 <code>wait()</code> 和 <code>notify()</code> 方法，只能被同步监听锁对象来调用，否则就会报出 <code>IllegalMonitorZStateException</code> 的异常，那么现在问题来了，我们在上一篇提到的 <code>Lock</code> 机制根本就没有同步锁了，也就是没有自动获取锁和自动释放锁的概念，因为没有同步锁，也就意味着 <code>Lock</code> 机制不能调用 <code>wait</code> 和 <code>notify</code> 方法，我们怎么办呢？</p><p>好在 Java 5 中提供了 Lock 机制的同时也提供了用于 Lock 机制控制通信的 Condition 接口，如果大家理解了上面说到的 <code>Object.wait()</code> 和 <code>Object.notify()</code> 方法的话，那么就能很容易地理解 Condition 对象了。</p><p>它和 <code>wait()</code> 和 <code>notify()</code> 方法的作用是大致相同的，只不过后者是配合 <code>synchronized</code> 关键字使用的，而 Condition 是与重入锁相关联的。通过 Lock 接口（重入锁就实现了这一接口）的 <code>newCondition()</code> 方法可以生成一个与当前重入锁绑定的 Condition 实例。利用 Condition 对象，我们就可以让线程在合适的时间等待，或者在某一个特定的时刻得到通知，继续执行。</p><p>我们拿上面的生产者消费者来举例，修改成 Lock 和 Condition 代码如下：</p><pre><code class="language-java">public class ShareResource {    private String name;    private String gender;    // 新增加一个标志位，表示共享资源是否为空，默认为 true    privateboolean isEmpty = true;    private Lock lock = new ReentrantLock();    private Condition condition = lock.newCondition();    /**     * 模拟生产者向共享资源对象中存储数据     *     * @param name     * @param gender     */    public void push(String name, String gender) {        lock.lock();        try {            while (!isEmpty) {                // 当前共享资源不为空的时，则等待消费者来消费                condition.await();            }            // 开始生产            this.name = name;            Thread.sleep(10);            this.gender = gender;            // 生产结束            isEmpty = false;            // 生产结束唤醒消费者来消费            condition.signalAll();        } catch (Exception ignored) {        } finally {            lock.unlock();        }    }    /**     * 模拟消费者从共享资源中取出数据     */    public void popup() {        lock.lock();        try {            while (isEmpty) {                // 为空则等着生产者进行生产                condition.await();            }            // 消费开始            Thread.sleep(10);            System.out.println(this.name + &quot;-&quot; + this.gender);            // 消费结束            isEmpty = true;            // 消费结束唤醒生产者去生产            condition.signalAll();        } catch (InterruptedException ignored) {        } finally {            lock.unlock();        }    }}</code></pre><p>在 JDK 内部，重入锁和 Condition 对象被广泛地使用，以 ArrayBlockingQueue 为例，它的 <code>put()</code> 方法实现如下：</p><pre><code class="language-java">** Main lock guarding all access */final ReentrantLock lock;/** Condition for waiting takes */privatefinal Condition notEmpty;/** Condition for waiting puts */privatefinal Condition notFull;// 构造函数，初始化锁以及对应的 Condition 对象public ArrayBlockingQueue(int capacity, boolean fair) {    if (capacity &lt;= 0)        thrownew IllegalArgumentException();    this.items = new Object[capacity];    lock = new ReentrantLock(fair);    notEmpty = lock.newCondition();    notFull =  lock.newCondition();}public void put(E e) throws InterruptedException {    checkNotNull(e);    final ReentrantLock lock = this.lock;    lock.lockInterruptibly();    try {        while (count == items.length)            // 等待队列有足够的空间            notFull.await();        enqueue(e);    } finally {        lock.unlock();    }}private void enqueue(E x) {    // assert lock.getHoldCount() == 1;    // assert items[putIndex] == null;    final Object[] items = this.items;    items[putIndex] = x;    if (++putIndex == items.length)        putIndex = 0;    count++;    // 通知需要 take() 的线程，队列已有数据    notEmpty.signal();}</code></pre><p>同理，对应的 <code>take()</code> 方法实现如下：</p><pre><code class="language-java">public E take() throws InterruptedException {    final ReentrantLock lock = this.lock;    lock.lockInterruptibly();    try {        while (count == 0)            // 如果队列为空，则消费者队列要等待一个非空的信号            notEmpty.await();        return dequeue();    } finally {        lock.unlock();    }}</code></pre><h2 id="允许多个线程同时访问信号量semaphore">允许多个线程同时访问：信号量(Semaphore)</h2><blockquote><p>以下内容摘录 or 改编自 《实战 Java 高并发程序设计》 3.1.3 节的内容</p></blockquote><p>信号量为多线程协作提供了更为强大的控制方法。广义上说，信号量是对锁的扩展，无论是内部锁 synchronized 还是重入锁 ReentrantLock，一次都只允许一个线程访问一个资源，而<strong>信号量却可以指定多个线程，同时访问某一个资源</strong>。信号量主要提供了以下构造函数：</p><pre><code class="language-java">public Semaphore(int permits)public Semaphore(int permits, boolean fair)        // 第二个参数可以指定是否公平</code></pre><p>在构造信号量对象时，必须要指定信号量的准入数，即同时能申请多少个许可。当每个线程每次只申请一个许可时，这就相当于指定了同时有多少个线程可以访问某一个资源。信号量的主要逻辑如下：</p><pre><code class="language-java">public void acquire()public void acquireUninterruptibly()public boolean tryAcquire()public boolean tryAcquire(long timeout, TimeUnit unit)public void release()</code></pre><ul><li><code>acquire()</code> 方法尝试获得一个准入的许可。若无法获得，则线程会等待，直到有线程释放一个许可或者当前线程被中断。</li><li><code>acquireUninterruptibly()</code> 方法和 <code>acquire()</code> 方法类似，但是不响应中断。</li><li><code>tryAcquire()</code> 尝试获得一个许可，如果成功则返回 true，失败则返回 false，它不会进行等待，立即返回。</li><li><code>release()</code> 用于在线程访问资源结束后，释放一个许可，以使其他等待许可的线程可以进行资源访问。</li></ul><p>在 JDK 的官方 Javadoc 中，就有一个有关信号量使用的简单实例，有兴趣的读者可以自行去翻阅一下，这里给出一个更傻瓜化的例子：</p><pre><code class="language-java">public class SemapDemo implements Runnable {    final Semaphore semaphore = new Semaphore(5);    @Override    public void run() {        try {            semaphore.acquire();            // 模拟耗时操作            Thread.sleep(2000);            System.out.println(Thread.currentThread().getId() + &quot;:done!&quot;);            semaphore.release();        } catch (InterruptedException ignore) {        }    }    public static void main(String[] args) {        ExecutorService executorService = Executors.newFixedThreadPool(20);        final SemapDemo demo = new SemapDemo();        for (int i = 0; i &lt; 20; i++) {            executorService.submit(demo);        }    }}</code></pre><p>执行程序，就会发现系统以 5 个线程为单位，依次输出带有线程 ID 的提示文本。</p><p><strong>在实现上，Semaphore 借助了线程同步框架 AQS（AbstractQueuedSynchornizer）</strong>，同样借助了 AQS 来实现的是 Java 中可重入锁的实现。AQS 的强大之处在于，你仅仅需要继承它，然后使用它提供的 api 就可以实现任意复杂的线程同步方案，AQS 为我们做了大部分的同步工作，所以这里不细说，之后再来详细探究一下...</p><h2 id="我等着你threadjoin">我等着你：Thread.join()</h2><p>如果一个线程 A 执行了 <code>thread.join()</code> 方法，其含义是：<strong>当前线程 A 等待 thread 线程终止之后才从 <code>thread.join()</code> 返回</strong>。线程 Thread 除了提供 <code>join()</code> 方法之外，还提供了 <code>join(long millis)</code> 和 <code>join(long millis, int nanos)</code> 两个具备超时特性的方法。这两个超时方法表示，如果线程 Thread 在给定的超时时间里没有终止，那么将会从该超时方法中返回。</p><p>在下面的代码中，我们创建了 10 个线程，编号 0 ~ 9，每个线程调用前一个线程的 <code>join()</code> 方法，也就是线程 0 结束了，线程 1 才能从 <code>join()</code> 方法中返回，而线程 0 需要等待 main 线程结束。</p><pre><code class="language-java">public class Join {    public static void main(String[] args) throws InterruptedException {        Thread previous = Thread.currentThread();        for (int i = 0; i &lt; 10; i++) {            // 每个线程拥有前一个线程的引用，需要等待前一个线程终止，才能从等待中返回            Thread thread = new Thread(new Domino(previous), String.valueOf(i));            thread.start();            previous = thread;        }        TimeUnit.SECONDS.sleep(5);        System.out.println(Thread.currentThread().getName() + &quot; terminate. &quot;);    }    staticclass Domino implements Runnable {        private Thread thread;        public Domino(Thread thread) {            this.thread = thread;        }        @Override        public void run() {            try {                thread.join();            } catch (InterruptedException ignore) {            }            System.out.println(Thread.currentThread().getName() + &quot; terminate. &quot;);        }    }}</code></pre><p>运行程序，可以看到下列输出：</p><pre><code>main terminate.0 terminate.1 terminate.2 terminate.3 terminate.4 terminate.5 terminate.6 terminate.7 terminate.8 terminate.9 terminate.</code></pre><p>说明每个线程终止的前提都是前驱线程的终止，每个线程等待前驱线程结束后，才从 <code>join()</code> 方法中返回，这里涉及了等待/ 通知机制，在 JDK 的源码中，我们可以看到 <code>join()</code> 的方法如下：</p><pre><code class="language-java">public final synchronized void join(long millis)    throws InterruptedException {    long base = System.currentTimeMillis();    long now = 0;    if (millis &lt; 0) {        thrownew IllegalArgumentException(&quot;timeout value is negative&quot;);    }    if (millis == 0) {        // 条件不满足则继续等待        while (isAlive()) {            wait(0);        }        // 条件符合则返回    } else {        while (isAlive()) {            long delay = millis - now;            if (delay &lt;= 0) {                break;            }            wait(delay);            now = System.currentTimeMillis() - base;        }    }}</code></pre><p><strong>当线程终止时，会调用线程自身的 <code>notifyAll()</code> 方法，会通知所有等待在该线程对象上的线程</strong>。可以看到 <code>join()</code> 方法的逻辑结构跟我们上面写的生产者消费者类似，即加锁、循环和处理逻辑三个步骤。</p><hr /><h2 id="保证可见性volatile-关键字">保证可见性：volatile 关键字</h2><p>我们先从一个有趣的例子入手：</p><pre><code class="language-java">private static boolean isOver = false;public static void main(String[] args) throws InterruptedException {    Thread thread = new Thread(() -&gt; {        while (!isOver) {        }        System.out.println(&quot;线程已感知到 isOver 置为 true，线程正常返回!&quot;);    });    thread.start();    Thread.sleep(500);    isOver = true;    System.out.println(&quot;isOver 已置为 true&quot;);}</code></pre><p>我们开启了一个主线程和一个子线程，我们期望子线程能够感知到 <code>isOver</code> 变量的变化以结束掉死循环正常返回，但是运行程序却发现并不是像我们期望的那样发生，子线程一直处在了死循环的状态！</p><p><strong>为什么会这样呢？</strong></p><h3 id="java-内存模型">Java 内存模型</h3><p>关于这一点，我们有几点需要说明，首先需要搞懂 Java 的内存模型：</p><p><img src="https://www.javazhiyin.com/wp-content/uploads/2019/11/java0-1574849271.png" alt="高并发编程学习(2)——线程通信详解" title="高并发编程学习(2)——线程通信详解" /></p><p>Java 虚拟机规范中试图定义一种 Java 内存模型（Java Memory Model, JMM）来屏蔽掉各层硬件和操作系统的内存访问差异，以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。</p><p><strong>Java 内存模型规定了所有的变量都存储在主内存（Main Memory）中</strong>。每条线程还有自己的工作内存（Working Memory），线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝，线程对变量的所有操作（读取、赋值等）都必须在主内存中进行，而不能直接读写主内存中的变量。<strong>不同的线程之间也无法直接访问对方工作内存中的变量</strong>，线程间的变量值的传递均需要通过主内存来完成，线程、主内存、工作内存三者的关系如上图。</p><p><strong>那么不同的线程之间是如何通信的呢？</strong></p><p>在<strong>共享内存</strong>的并发模型里，线程之间共享程序的公共状态，线程之间通过写-读内存中的公共状态来隐式进行通信，典型的共享内存通信方式就是通过共享对象进行通信。</p><p><img src="https://www.javazhiyin.com/wp-content/uploads/2019/11/java1-1574849271.jpg" alt="高并发编程学习(2)——线程通信详解" title="高并发编程学习(2)——线程通信详解" /></p><p>例如上图线程 A 与 线程 B 之间如果要通信的话，那么就必须经历下面两个步骤：</p><ol><li>首先，线程 A 把本地内存 A 更新过的共享变量刷新到主内存中去</li><li>然后，线程 B 到主内存中去读取线程 A 之前更新过的共享变量<br /><img src="https://www.javazhiyin.com/wp-content/uploads/2019/11/java6-1574849271-1.jpg" alt="高并发编程学习(2)——线程通信详解" title="高并发编程学习(2)——线程通信详解" /></li></ol><p>在消息传递的并发模型里，线程之间没有公共状态，线程之间必须通过明确的发送消息来显式进行通信，在 Java 中典型的消息传递方式就是 <code>wait()</code> 和 <code>notify()</code>。</p><p>说回刚才出现的问题，就很容易理解了：每个线程都有独占的内存区域，如操作栈、本地变量表等。<strong>线程本地保存了引用变量在堆内存中的副本，线程对变量的所有操作都在本地内存区域中进行，执行结束后再同步到堆内存中去</strong>。也就是说，我们在主线程中修改的 <code>isOver</code> 的值并没有被子线程读取到（没有被刷入主内存），也就造成了子线程对于 <code>isOver</code> 变量不可见。</p><p>解决方法也很简单，只需要在 <code>isOver</code> 变量前加入 <code>volatile</code> 关键字就可以了，这是<strong>因为加入了 <code>volatile</code> 修饰的变量允许直接与主内存交互，进行读写操作</strong>，保证可见性。</p><h2 id="指令重排-happen-before-原则">指令重排/ happen-before 原则</h2><p>再从另一个有趣的例子中入手，这是在高并发场景下会存在的问题：</p><pre><code class="language-java">class LazyInitDemo {    private static TransationService service = null;    public static TransationService getTransationService(){        if (service == null) {            synchronized (this) {                if (service == null) {                    service = new TransationService();                }            }        }    }}</code></pre><p>这是一个典型的双重检查锁定思想，这段代码也是一个典型的双重检查锁定（Double-checked Locking）问题。<strong>在高并发的情况下，该对象引用在没有同步的情况下进行读写操作，导致用户可能会获取未构造完成的对象</strong>。</p><p>这是因为<strong>指令优化</strong>的结果。<strong>计算机不会根据代码顺序按部就班地执行相关指令</strong>，我们来举一个借书的例子：假如你要去还书并且想要借一个《高并发编程学习》系列丛书，而你的室友恰好也要还书，并且还想让你帮忙借一本《Java 从入门到放弃》。</p><p>这件事乍一看有两件事：你的事和你室友的事。先办完你的事，再开始处理你室友的事情是属于单线程的死板行为，此时你会潜意识地进行**「优化」**，例如你可以把你要还的书和你室友需要还的书一起还了，再一起把想要借的书借出来，这其实就相当于合并数据进行存取的操作过程了。</p><p>我们知道一条指令的执行是可以分成很多步骤的，简单地说，可以分为：</p><ul><li>取值 IF</li><li>译码和去寄存器操作数 ID</li><li>执行或者有效地址计算 EX</li><li>存储器访问 MEM</li><li>写回 WB</li></ul><p>由于每一个步骤可能使用不同的硬件完成，因此，聪明的工程师就发明了流水线技术来执行指令，如下图所示：</p><p><img src="https://www.javazhiyin.com/wp-content/uploads/2019/11/java5-1574849272.png" alt="高并发编程学习(2)——线程通信详解" title="高并发编程学习(2)——线程通信详解" /></p><p>可以看到，当第 2 条指令执行时，第 1 条执行其实并没有执行完，确切地说第一条指令还没有开始执行，只是刚刚完成了取值操作而已。这样的好处非常明显，假如这里每一个步骤都需要花费 1 毫秒，那么指令 2 等待指令 1 完全执行后再执行，则需要等待 5 毫秒，而使用流水线指令，指令 2 只需要等待 1 毫秒就可以执行了。如此大的性能提升，当然让人眼红。</p><p>回到最初的问题，我们分析一下：<strong>对于 Java 编译器来说，初始化 TransactionService 实例和将对象地址写到 <code>service</code> 字段并非原子操作，且这两个阶段的执行顺序是未定义的</strong>。加入某个线程执行 <code>new TransactionService()</code> 时，构造方法还未被调用，编译器仅仅为该对象分配了内存空间并设为默认值，此时若另一个线程调用 <code>getTransactionService()</code> 方法，由于 <code>service != null</code>，但是此时 <code>service</code> 对象还没有被赋予真正的有效值，从而无法取到正确的 <code>service</code> 单例对象。</p><p>对于此问题，一种较为简单的解决方案就是用 <code>volatile</code> 关键字修饰目标属性（适用于 JDK5 及以上版本），这样 <code>service</code> 就限制了编译器对它的相关读写操作，对它的读写操作进行指令重排，确定对象实例化之后才返回引用。</p><p>另外<strong>指令重排也有自己的规则</strong>，并非所有的指令都可以随意改变执行位置，下面列举一下基本的原则：</p><ul><li><strong>程序次序规则</strong>：一个线程内，按照代码顺序，书写在前面的操作先行发生于书写在后面的操作；</li><li><strong>锁定规则</strong>：一个 unLock 操作先行发生于后面对同一个锁的 lock 操作；</li><li><strong>volatile 变量规则</strong>：对一个变量的写操作先行发生于后面对这个变量的读操作；</li><li><strong>传递规则</strong>：如果操作 A 先行发生于操作 B，而操作 B 又先行发生于操作 C，则可以得出操作 A 先行发生于操作 C；</li><li><strong>线程启动规则</strong>：Thread 对象的 <code>start()</code> 方法先行发生于此线程的每个一个动作；</li><li><strong>线程中断规则</strong>：对线程 <code>interrupt()</code> 方法的调用先行发生于被中断线程的代码检测到中断事件的发生；</li><li><strong>线程终结规则</strong>：线程中所有的操作都先行发生于线程的终止检测，我们可以通过 <code>Thread.join()</code> 方法结束、<code>Thread.isAlive()</code> 的返回值手段检测到线程已经终止执行；</li><li><strong>对象终结规则</strong>：一个对象的初始化完成先行发生于他的 <code>finalize()</code> 方法的开始；</li></ul><h3 id="volatile-不保证原子性">volatile 不保证原子性</h3><p>volatile 解决的是多线程共享变量的可见性问题，类似于 synchronized，但不具备 synchronized 的互斥性。所以对 volatile 变量的操作并非都具有原子性，例如我们用下面的例子来说明：</p><pre><code class="language-java">public class VolatileNotAtomic {    private staticvolatilelong count = 0L;    private staticfinalint NUMBER = 10000;    public static void main(String[] args) {        Thread subtractThread = new SubstractThread();        subtractThread.start();        for (int i = 0; i &lt; NUMBER; i++) {            count++;        }        // 等待减法线程结束        while (subtractThread.isAlive()) {        }        System.out.println(&quot;count 最后的值为: &quot; + count);    }    private static class SubstractThread extends Thread {        @Override        public void run() {            for (int i = 0; i &lt; NUMBER; i++) {                count--;            }        }    }}</code></pre><p>多次执行后，发现结果基本都不为 0。只有在 <code>count++</code> 和 <code>count--</code> 两处都进行加锁时，才能正确的返回 0，了解 Java 的童鞋都应该知道这 <code>count++</code> 和 <code>count--</code> 都不是一个原子操作，这里就不作说明了。</p><h3 id="volatile-的使用优化">volatile 的使用优化</h3><p>在了解一点吧，注明的并发编程大师 Doug lea 在 JDK 7 的并发包里新增一个队列集合类 LinkedTransferQueue，它在使用 volatile 变量时，用一种<strong>追加字节的方式来优化对列出队和入队的性能</strong>，具体的可以看一下下列的链接，这里就不具体说明了。</p><ul><li>追加字节方式来优化队列性能？- <a href="https://my.oschina.net/u/3694754/blog/2990652">https://my.oschina.net/u/3694754/blog/2990652</a><a href="https://www.javazhiyin.com/wp-content/uploads/2019/11/java1-1574849271.jpg" title="高并发编程学习(2)——线程通信详解">4</a></li></ul><h2 id="保证原子性synchronized">保证原子性：synchronized</h2><p>Java 中任何一个对象都有一个唯一与之关联的锁，这样的锁作为该对象的一系列标志位存储在对象信息的头部。Java 对象头里的 Mark Word 里默认的存放的对象的 Hashcode/ 分代年龄和锁标记位。32 为 JVM Mark Word 默认存储结构如下：</p><p><img src="https://www.javazhiyin.com/wp-content/uploads/2019/11/java6-1574849272.png" alt="高并发编程学习(2)——线程通信详解" title="高并发编程学习(2)——线程通信详解" /></p><p>Java SE 1.6 中，锁一共有 4 种状态，级别从低到高依次是：<strong>无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态</strong>，这几个状态会随着竞争情况逐渐升级。<strong>锁可以升级但不能降级</strong>，意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略，目的是为了提高获得锁和释放锁的效率。</p><h3 id="偏向锁">偏向锁</h3><p>HotSpot 的作者经过研究发现，大多数情况下，锁不仅不存在多线程竞争，而且总是由同一线程多次获得，为了让线程获得锁的代价更低而引入了偏向锁。</p><ul><li><p><strong>偏向锁的获取</strong>：当一个线程访问同步块并获取锁时，会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID，以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁，只需简单地测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。如果测试成功，表示线程已经获得了锁。如果测试失败，则需要再测试一下 Mark Word 中偏向锁的标识是否设置成 1（表示当前是偏向锁），如果没有设置，则使用 CAS 竞争锁；如果设置了，则尝试使用 CAS 将对象头的偏向锁指向当前线程。</p></li><li><p><strong>偏向锁的撤销</strong>：偏向锁使用了一种<strong>等到竞争出现才释放锁</strong>的机制，所以当其他线程尝试竞争偏向锁时，持有偏向锁的线程才会释放锁。</p></li></ul><p>下图线程 1 展示了偏向锁获取的过程，线程 2 展示了偏向锁撤销的过程。</p><p><img src="https://www.javazhiyin.com/wp-content/uploads/2019/11/java7-1574849272.jpg" alt="高并发编程学习(2)——线程通信详解" title="高并发编程学习(2)——线程通信详解" /></p><h3 id="轻量级锁和自旋锁">轻量级锁和自旋锁</h3><p>如果偏向锁失败，虚拟机并不会立即挂起线程。它还会使用一种称为轻量级锁的优化手段。</p><p>线程在执行同步块之前，JVM 会先在当前线程的栈桢中创建用于存储锁记录的空间，并将对象头中的 Mark Word 复制到锁记录中，官方称为 <strong>Displaced Mark Word</strong>。然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。如果成功，当前线程获得锁，如果失败，表示其他线程竞争锁，当前线程便尝试使用<strong>自旋</strong>（自己执行几个空循环再进行尝试）来获取锁。</p><p>轻量级解锁时，会使用原子的 CAS 操作将 Displaced Mark Word 替换回到对象头，如果成功，则表示没有竞争发生。<strong>如果失败，表示当前锁存在竞争，锁就会膨胀成重量级锁</strong>。下图是两个线程同时争夺锁，导致锁膨胀的流程图。</p><p><img src="https://www.javazhiyin.com/wp-content/uploads/2019/11/java2-1574849272.jpg" alt="高并发编程学习(2)——线程通信详解" title="高并发编程学习(2)——线程通信详解" /></p><h3 id="几种锁的比较">几种锁的比较</h3><p>下图就简单概括了一下几种锁的比较：</p><p><img src="https://www.javazhiyin.com/wp-content/uploads/2019/11/java1-1574849272.jpg" alt="高并发编程学习(2)——线程通信详解" title="高并发编程学习(2)——线程通信详解" /></p><h2 id="每人一支笔threadlocal">每人一支笔：ThreadLocal</h2><p>除了控制资源的访问外，我们还可以通过增加资源来保证所有对象的线程安全。比如，让 100 个人填写个人信息表，如果只有一支笔，那么大家就得挨个写，对于管理人员来说，必须保证大家不会去哄抢这仅存的一支笔，否则，谁也填不完。从另外一个角度出发，我们可以干脆就准备 100 支笔，那么所有人都可以各自为营，很快就能完成表格的填写工作。</p><p>如果说锁是使用第一种思路，那么 ThreadLocal 就是使用第二种思路了。</p><p>当使用 ThreadLocal 维护变量时，其为每个使用该变量的线程提供独立的变量副本，所以每一个线程都可以独立的改变自己的副本，而不会影响其他线程对应的副本。</p><p><strong>ThreadLocal 内部实现机制</strong>：</p><p><img src="https://www.javazhiyin.com/wp-content/uploads/2019/11/java9-1574849272.jpg" alt="高并发编程学习(2)——线程通信详解" title="高并发编程学习(2)——线程通信详解" /></p><ol><li><p>每个线程内部都会维护一个类似 HashMap 的对象，称为 ThreadLocalMap，里边会包含若干了 Entry（K-V 键值对），相应的线程被称为这些 Entry 的属主线程；</p></li><li><p>Entry 的 Key 是一个 ThreadLocal 实例，Value 是一个线程特有对象。Entry 的作用即是：为其属主线程建立起一个 ThreadLocal 实例与一个线程特有对象之间的对应关系；</p></li><li><p>Entry 对 Key 的引用是弱引用；Entry 对 Value 的引用是强引用。</p></li></ol><h3 id="threadlodal-的副作用">ThreadLodal 的副作用</h3><p>为了让线程安全地共享某个变量，JDK 开出了 ThreadLocal 这副药方，但「是药三分毒」，ThreadLocal 也有一定的副作用。<strong>主要问题是「产生脏数据」和「内存泄漏」</strong>。这两个问题通常是在线程池中使用 ThreadLocal 引发的，因为线程池有 <strong>「线程复用」</strong> 和 <strong>「内存常驻」</strong> 两个特点。</p><h3 id="脏数据">脏数据</h3><p>线程复用会产生脏数据。<strong>由于线程池会重用 Thread 对象，那么与 Thread 绑定的类的静态属性 ThreadLocal 变量也会被重用</strong>。如果在实现的线程 <code>run()</code> 方法中不显式地 <code>remove()</code> 清理与线程相关的 ThreadLocal 信息，那么倘若下一个线程不调用 <code>set()</code> 设置初始值，就可能 <code>get()</code> 到重用的线程信息，包括 ThreadLocal 所关联的线程对象的 value 值。</p><p>为了方便理解，用一段简要代码来模拟，如下所示：</p><pre><code class="language-java">public class DirtyDataInThreadLocal {    public static ThreadLocal&lt;String&gt; threadLocal = new ThreadLocal&lt;&gt;();    public static void main(String[] args) {        // 使用固定大小为 1 的线程池，说明上一个的线程属性会被下一个线程属性复用        ExecutorService pool = Executors.newFixedThreadPool(1);        for (int i = 0; i &lt; 2; i++) {            Mythread mythread = new Mythread();            pool.execute(mythread);        }    }    private static class Mythread extends Thread {        private static boolean flag = true;        @Override        public void run() {            if (flag) {                // 第 1 个线程 set 后，并没有进行 remove                // 而第二个线程由于某种原因没有进行 set 操作                threadLocal.set(this.getName() + &quot;, session info.&quot;);                flag = false;            }            System.out.println(this.getName() + &quot; 线程是 &quot; + threadLocal.get());        }    }}</code></pre><p>执行结果：</p><pre><code>Thread-0 线程是 Thread-0, session info.Thread-1 线程是 Thread-0, session info.</code></pre><h3 id="内存泄漏">内存泄漏</h3><p>在源码注释中提示使用 static 关键字来修饰 ThreadLocal。在此场景下，寄希望于 ThreadLocal 对象失去引用后，触发<strong>弱引用</strong>机制来回首 Entry 的 Value 就变得不现实了。在上面的例子中，如果不进行 <code>remove()</code> 操作，那么这个线程执行完成后，通过 ThreadLocal 对象持有的 String 对象是不会被释放的。</p><p>以上两个问题的解决办法很简单，就是在每次使用完 ThreadLocal 时，必须要及时调用 <code>remove()</code> 方法清理。</p><hr /><ol><li>《Java 零基础入门教程》 - <a href="http://study.163.com/course/courseMain.htm?courseId=1003108028">http://study.163.com/course/courseMain.htm?courseId=1003108028</a><a href="https://www.javazhiyin.com/wp-content/uploads/2019/11/java6-1574849271-1.jpg" title="高并发编程学习(2)——线程通信详解">5</a></li><li>《Java 并发编程的艺术》</li><li>《码出高效 Java 开发手册》 - 杨冠宝（孤尽） 高海慧（鸣莎）著</li><li>Java 面试知识点解析(二)——高并发编程篇 - <a href="https://www.wmyskxz.com/2018/05/10/java-mian-shi-zhi-shi-dian-jie-xi-er-gao-bing-fa-bian-cheng-pian/">https://www.wmyskxz.com/2018/05/10/java-mian-shi-zhi-shi-dian-jie-xi-er-gao-bing-fa-bian-cheng-pian/</a><a href="https://www.javazhiyin.com/wp-content/uploads/2019/11/java5-1574849272.png" title="高并发编程学习(2)——线程通信详解">6</a></li><li>让你彻底理解 Synchronized - <a href="https://www.jianshu.com/p/d53bf830fa09">https://www.jianshu.com/p/d53bf830fa09</a><a href="https://www.javazhiyin.com/wp-content/uploads/2019/11/java6-1574849272.png" title="高并发编程学习(2)——线程通信详解">7</a></li><li>《Offer 来了 - Java 面试核心知识点精讲》 - 王磊 编著</li><li>《实战 Java 高并发程序设计》 - 葛一鸣 郭超 编著</li></ol><hr /><blockquote><p>原文始发于微信公众号（我没有三颗心脏）：<a href="http://mp.weixin.qq.com/s/WtSAKDiwZsuxGppuECqaLg">高并发编程学习(2)——线程通信详解</a></p></blockquote>]]>
                    </description>
                    <pubDate>Mon, 01 Jun 2020 23:38:36 CST</pubDate>
                </item>
    </channel>
</rss>