繼續(xù)探究:一文理清JVM和GC(下)

本文主要介紹 JVM和GC解析
如有需要,可以參考
如有幫助,不忘 點(diǎn)贊 ?

創(chuàng)作不易,白嫖無義!

一、OOM的認(rèn)識(shí)

StackOverflowError

 public static void main(String[] args) {
     stackOverflowError();   //Exception in thread "main" java.lang.StackOverflowError
 }
private static void stackOverflowError() {
    stackOverflowError();
}
復(fù)制代碼

OutOfMemeoryError:java heap space

public static void main(String[] args) {
    String str = "cbuc";
    for (; ; ) {
        str += str + UUID.randomUUID().toString().substring(0,5);   //+= 不斷創(chuàng)建對(duì)象
    }
}
復(fù)制代碼

OutOfMemeoryError:GC overhead limit exceeded

程序在垃圾回收上花費(fèi)了98%的時(shí)間,卻收集不會(huì)2%的空間。
假如不拋出GC overhead limit,會(huì)造成:

  • GC清理的一點(diǎn)點(diǎn)內(nèi)存很快會(huì)再次填滿,迫使GC再次執(zhí)行,這樣就形成了惡性循環(huán)。
  • CPU的使用率一直是100%,而GC卻沒有任何成果
image.png

OutOfMemeoryError:Direct buffer memory

  • 寫NIO程序經(jīng)常使用 ByteBuffer 來讀取或者寫入數(shù)據(jù),這是一種基于通道(Channel)和緩沖區(qū)(Buffer)的 I/O 方式,它可以使用Native 函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在Java 堆里面的DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)?em>避免了在Java堆和Native堆中來回復(fù)制數(shù)據(jù)。

ByteBuffer.allocate(capability):這一種方式是分配JVM堆內(nèi)存,屬于GC管轄范圍,由于需要拷貝所以速度相對(duì)較慢。

ByteBuffer.allocateDirect(capability):這一種方式是分配OS本地內(nèi)存,不屬于GC管轄范圍,由于不需要內(nèi)存拷貝,所以速度相對(duì)較快。

但是如果不斷分配本地內(nèi)存,堆內(nèi)存很少使用,那么JVM就不需要執(zhí)行GC,DirectByteBuffer 對(duì)象就不會(huì)被回收,這時(shí)候堆內(nèi)存充足,但本地內(nèi)存可能就已經(jīng)使用光了,再次嘗試分配本地內(nèi)存就會(huì)出現(xiàn)OutOfMemeoryError,那程序就直接奔潰了。

public static void main(String[] args) {
    /**
     * 虛擬機(jī)配置參數(shù)
     * -Xms10m -Xmx10m -XX:+PrintGCDetails  -XX:MaxDirectMemorySize=5m
     */
    System.out.println("配置的maxDirectMemeory:"+     (sun.misc.VM.maxDirectMemory()/(double)1024/1024)+"MB");
    try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
    // -XX:MaxDerectMemorySize=5m  配置為5m, 這個(gè)時(shí)候我們使用6m
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6*1024*1024);

}
復(fù)制代碼
  • OutOfMemeoryError:unable to create new native thread

高并發(fā)請(qǐng)求服務(wù)器時(shí),經(jīng)常會(huì)出現(xiàn)該異常
導(dǎo)致原因

  1. 你的應(yīng)用創(chuàng)建了太多線程了,一個(gè)應(yīng)用進(jìn)程創(chuàng)建多個(gè)線程,超過系統(tǒng)承載權(quán)限。
  2. 你的服務(wù)器并不允許你的應(yīng)用程序創(chuàng)建這么多線程,linux系統(tǒng)默認(rèn)允許的那個(gè)進(jìn)程可以創(chuàng)建的線程數(shù)是1024個(gè),你的應(yīng)用創(chuàng)建超過這個(gè)數(shù)量就會(huì)報(bào)OutOfMemeoryError:unable to create new native thread

解決辦法

  1. 想方法減低你應(yīng)用程序創(chuàng)建線程的數(shù)量,分析應(yīng)用是否真的需要?jiǎng)?chuàng)建那么多線程,如果不是,該代碼將線程數(shù)降到最低。
  2. 對(duì)于有點(diǎn)應(yīng)用,確實(shí)需要?jiǎng)?chuàng)建很多線程,遠(yuǎn)超過linux系統(tǒng)默認(rèn)1024個(gè)線程的限制,可以通過修改linux服務(wù)器配置,擴(kuò)大linux默認(rèn)限制
public static void main(String[] args) {
        for (int i = 1;  ; i++) {
            System.out.println("輸出 i: " + i);
             new Thread(()->{
                 try {TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);} catch (InterruptedException e) {e.printStackTrace();}
             },"線程"+i).start();
        }
    }
復(fù)制代碼

OutOfMemeoryError:Metaspace

Java 8之后的版本使用Metaspace來替代永久代
Metaspace是方法區(qū)在HotSpot中的實(shí)現(xiàn),它與持久帶最大的區(qū)別在于:Metespace并不在虛擬機(jī)內(nèi)存中而是使用本地內(nèi)存
永久代(java8 后被原空間Metaspace取代了)存放了以下信息:

  • 虛擬機(jī)加載的類信息
  • 常量池
  • 靜態(tài)常量
  • 即時(shí)編譯后的代碼

二、4種垃圾收集器

GC算法(引用計(jì)數(shù)/復(fù)制/標(biāo)清/標(biāo)整)是內(nèi)存回收的方法,垃圾收集器就是算法的實(shí)現(xiàn)

目前為止還沒有完美的收集器出現(xiàn),更加沒有萬能的收集器,只是針對(duì)具體應(yīng)用最合適的收集器,進(jìn)行分代收集

串行垃圾回收器(Serial)

它為單線程環(huán)境設(shè)計(jì)并且只是用一個(gè)線程進(jìn)行垃圾回收,會(huì)暫停所有的用戶線程。所以不適合服務(wù)器環(huán)境。

并行垃圾回收器(parallel)

多個(gè)垃圾回收線程并行工作,此時(shí)用戶線程是暫停的,適用于科學(xué)計(jì)算/大數(shù)據(jù)處理等弱交互場(chǎng)景

并發(fā)垃圾回收器(CMS)

用戶線程和垃圾收集線程同時(shí)執(zhí)行(不一定是并行,可能交替執(zhí)行),不需要停頓用戶線程,適用于對(duì)響應(yīng)時(shí)間有要求的場(chǎng)景

G1垃圾回收器

G1垃圾回收器將堆內(nèi)存分割成不同的區(qū)域然后并發(fā)的對(duì)其進(jìn)行垃圾回收

三、垃圾收集器解析

查看默認(rèn)的垃圾收集器

java -XX:+PrintCommandLineFlags -version

image.png

默認(rèn)的垃圾收集器

  • UseSerialGC
  • UseParallelGC
  • UseConcMarkSweepGC
  • UseParNewGC
  • UseParallelOldGC
  • UseG1GC

新生代

  • 串行GC(Serial)/(Serial Coping)
    一個(gè)單線程的收集器,在進(jìn)行垃圾收集的時(shí)候,必須暫停其他所有的工作線程知道它收集結(jié)束
image.png

最穩(wěn)定以及效率高的收集器,只使用一個(gè)線程去回收但其在進(jìn)行垃圾手機(jī)過程中可能會(huì)產(chǎn)生較長的停頓(“Stop-The-World”狀態(tài))。雖然在收集垃圾過程中需要暫停所有其他的工作線程,但是它簡(jiǎn)單高效,對(duì)于限定單個(gè)CPU環(huán)境來說,==沒有線程交互的開銷可以獲得更高的單線程垃圾收集效率,== 因此Serial垃圾收集器依然是Java虛擬機(jī)運(yùn)行在Client 模式下默認(rèn)的新生代垃圾收集器。

JVM設(shè)置參數(shù)
-XX:+UseSerialGC開啟后會(huì)使用:Serial(Young區(qū)用)+Serial Old(Old區(qū)用的)收集器組合,
表示

新生代、老年代都會(huì)使用串行回收收集器,新生代使用復(fù)制算法,老年代使用標(biāo)記-整理算法

  • 并行GC(ParNew)

使用多線程進(jìn)行垃圾回收,在垃圾收集時(shí),會(huì)Stop-The-World暫停其他所有工作的線程知道它收集結(jié)束

image.png

ParNew收集器其實(shí)就是Serial收集器新生代的并行多線程版本,最常見的應(yīng)用場(chǎng)景是配合老年代的CMS GC工作,其余的行為和Serial收集器完全一樣,ParNew垃圾收集器在垃圾收集過程中同樣也要暫停所有的工作線程。它是很多java虛擬機(jī)運(yùn)行在Server模式下新生代的默認(rèn)垃圾收集器。

JVM設(shè)置參數(shù)

XX:+UseParNewGC啟用 ParNew收集器,只影響新生代的收集,不影響老年代。開啟上述參數(shù)后,會(huì)使用:ParNew (新生代區(qū)用)+Serial Old(老年代區(qū)用)策略,新生代使用復(fù)制算法,老年代使用標(biāo)記-整理算法。

  • 并行回收GC(Parallel)/(Parallel Scavenge)
image.png

Parallel Scavenge收集器類似ParNew 也是新生代垃圾收集器,使用復(fù)制算法,也是一個(gè)并行的多線程的垃圾收集器,俗稱吞吐量優(yōu)先收集器。串行收集器在新生代和老年代的并行化

關(guān)注點(diǎn):

  1. 可控制的吞吐量
  2. 自適應(yīng)調(diào)節(jié)策略也是ParallelScavenge收集器與ParallelNew收集器的一個(gè)重要區(qū)別 JVM設(shè)置參數(shù)
    -XX:UseParallelGC 或 -XX:UseParallelOldGC(可互相激活),開啟后:新生代使用復(fù)制算法,老年代使用標(biāo)記-整理算法。

老年代

  • 串行GC(Serial Old)/(Serial MSC)
    Serial Old 是Serial 垃圾收集器老年代版本,它同樣是個(gè)單線程的收集器,使用標(biāo)記-整理算法,這個(gè)收集器也主要是運(yùn)行在Client默認(rèn)的java虛擬機(jī)默認(rèn)的老年代垃圾收集器。
    用途
  1. 在JDK1.5之前版本中與新生代的Parallel Scavenge收集器搭配使用。(Parallel Scavenge+Serial Old)
  2. 作為老年代版中使用CMS收集器的后備垃圾收集方案。
  • 并行GC(Parallel Old)/(Parallel MSC)
    Parallel Old收集器是Parallel Scavenge的老年代版本,使用多線程的標(biāo)記-整理算法,Parallel Old在JDK 1.6之前,新生代使用 ParallelScavenge 收集器,只能保證新生代的吞吐量優(yōu)先,無法保證整體的吞吐量。在JDK1.6之前(Parallel Scavenge+Serial Old)
    Parallel Old 正是為了在年老代同樣提供吞吐量優(yōu)先的垃圾收集器,如果系統(tǒng)對(duì)吞吐量要求比較高,JDK1.8 后可以優(yōu)先考慮新生代Parallel Scavenge和年老代 Parallel Old收集器的搭配策略。

JVM設(shè)置參數(shù)
-XX:+UseParallelOldGC開啟 Parallel Old收集器,設(shè)置該參數(shù)后,使用 新生代Parallel + 老年代Parallel Old策略

  • 并發(fā)標(biāo)記清除GC(CMS)

優(yōu)點(diǎn): 并發(fā)收集低停頓

缺點(diǎn)

  • 并發(fā)執(zhí)行,對(duì)CPU資源壓力大:
    由于并發(fā)進(jìn)行,CMS在收集與應(yīng)用線程會(huì)同時(shí)會(huì)增加對(duì)堆內(nèi)存的占用,也就是說,CMS必須要在老年代堆內(nèi)存用盡之前完成垃圾回收,否則CMS回收失敗時(shí),將觸發(fā)擔(dān)保機(jī)制,串行老年代收集器將會(huì)以STW的方式進(jìn)行一次GC,從而造成較大停頓時(shí)間。
  • 采用的標(biāo)記清除算法會(huì)導(dǎo)致大量碎片:
    標(biāo)記清除算法無法整理空間碎片,老年代空間會(huì)隨著應(yīng)用時(shí)長被逐步耗盡,隨后將不得不通過擔(dān)保機(jī)制對(duì)堆內(nèi)存進(jìn)行壓縮。CMS也提供了參數(shù)-XX:CMSFulllGCsBeForeCompaction(默認(rèn)0,即每次都進(jìn)行內(nèi)存整理)來指定多少次CMS收集之后,進(jìn)行一次壓縮的Full GC。 關(guān)鍵4步
  1. Initial Mark (初始標(biāo)記):標(biāo)記GC Root可以直達(dá)的對(duì)象,耗時(shí)短。
  2. Concurrent Mark(并行標(biāo)記):從第一步標(biāo)記的對(duì)象出發(fā),并發(fā)地標(biāo)記可達(dá)對(duì)象。
  3. Remark(重新標(biāo)記): 重新進(jìn)行標(biāo)記,修正Concurrent Mark期間由于用戶程序運(yùn)行而導(dǎo)致對(duì)象間的變化及新創(chuàng)建的對(duì)象,耗時(shí)短。
  4. Concurrent Sweep(并行回收): 并行地進(jìn)行無用對(duì)象的回收。
image.png

如何選擇垃圾收集器

  • 單CPU或小內(nèi)存,單機(jī)程序
    -XX:+UseSerialGC
  • 多CPU,需要最大吞吐量,如后臺(tái)計(jì)算型應(yīng)用
    -XX:+UseParallelGC-XX:+UseParallelOldGC
  • 多CPU,追求低停頓時(shí)間,需快速響應(yīng)如互聯(lián)網(wǎng)應(yīng)用
    -XX:+UseConcMarkSweepGC-XX:+ParNewGC

四、G1垃圾收集器

以前垃圾收集器的特點(diǎn)

  1. 年輕代和老年代是各自獨(dú)立且連續(xù)的內(nèi)存塊
  2. 年輕代中Eden+S0+S1使用復(fù)制算法進(jìn)行收集
  3. 老年代收集必須掃描整個(gè)老年代區(qū)域
  4. 都是以盡可能少而快速地執(zhí)行GC為設(shè)計(jì)原則

G1 概念:

Garbage-First收集器,是一款面向服務(wù)端應(yīng)用的收集器,優(yōu)點(diǎn)如下:

  • 整理空閑空間更快
  • 需要更多的時(shí)間來預(yù)測(cè)GC停頓時(shí)間
  • 不希望犧牲大量的吞吐性能
  • 不需要更大的Java Heap

G1收集器的設(shè)計(jì)目標(biāo)是取代CMS收集器

G1 優(yōu)勢(shì):

  1. G1 是一個(gè)有整理內(nèi)存過程的垃圾收集器,不會(huì)產(chǎn)生很多內(nèi)存碎片
  2. G1 的Stop-The-World (STW)更可控,G1在停頓時(shí)間上添加了預(yù)測(cè)機(jī)制,用戶可以指定期望停頓時(shí)間

主要改變是Eden,Survivor和Tenured等內(nèi)存區(qū)域不再是連續(xù)的了,而是變成了一個(gè)個(gè)大小一樣的region,每個(gè)region從1M到32M不等。一個(gè)region有可能屬于Eden,Survivor或者Tenured內(nèi)存區(qū)域。

G1特點(diǎn):

  • G1能充分利用多CPU,多核環(huán)境硬件優(yōu)勢(shì),盡量縮短STW
  • G1整體上采用標(biāo)記-整理算法,局部是通過復(fù)制算法,不會(huì)產(chǎn)生內(nèi)存碎片
  • 宏觀上看G1之中不再區(qū)分年輕代和老年代。把內(nèi)存劃分成多個(gè)獨(dú)立的子區(qū)域(Region)
  • G1收集器里面將整個(gè)的內(nèi)存區(qū)都混合在一起了,但其本身依然在小范圍內(nèi)要進(jìn)行年輕代和老年代的區(qū)分,保留了新生代和老年代。
  • G1雖然也是分代收集器,但整個(gè)內(nèi)存分區(qū)不存在物理上的年輕代與老年代的區(qū)別,也不需要完全獨(dú)立的survivor(to space)堆做復(fù)制準(zhǔn)備。G1只有邏輯上的分代概念,或者說每個(gè)分區(qū)都可能隨G1的運(yùn)行在不同代之間前后切換。

G1底層原理

(1)Region區(qū)域化垃圾收集器·

區(qū)域化內(nèi)存劃片Region,整體變?yōu)榱艘幌盗胁贿B續(xù)的內(nèi)存區(qū)域,避免了全內(nèi)存區(qū)的GC操作。
核心思想

將整個(gè)堆內(nèi)存區(qū)域分成大小相同的子區(qū)域(Region),在JVM啟動(dòng)時(shí)會(huì)自動(dòng)配置這些子區(qū)域的大小。
在堆的使用上,G1并不要求對(duì)象的存儲(chǔ)一定是物理上連續(xù)的只要邏輯上連續(xù)即可,每個(gè)分區(qū)也不會(huì)固定地為某個(gè)代服務(wù),可以按需在年輕代和老年代之間切換。啟動(dòng)時(shí)可以通過參數(shù)-XX:G1HeapRegionSize=n 可指定分區(qū)大小(1MB~32MB,且必須是2的冪),默認(rèn)將整堆劃分為2048個(gè)分區(qū)。
大小范圍在1MB~32MB,最多能設(shè)置2048個(gè)區(qū)域,也即能夠支持的最大內(nèi)存為:32MB*2048=65536MV=64G內(nèi)存
最大好處就是化整為零,避免全內(nèi)存掃描,只需要按照區(qū)域來進(jìn)行掃描即可

image.png

(2)回收步驟

針對(duì)Eden區(qū)進(jìn)行收集,Eden區(qū)耗盡后會(huì)被觸發(fā),主要是小區(qū)域收集+形成連續(xù)的內(nèi)存塊,避免內(nèi)存碎片

  • Eden區(qū)的數(shù)據(jù)移動(dòng)到新的Survivor區(qū),部分?jǐn)?shù)據(jù)晉升到Old區(qū)。
  • Survivor區(qū)的數(shù)據(jù)移動(dòng)到新的Survivor區(qū),部分?jǐn)?shù)據(jù)晉升到Old區(qū)。
  • 最后Eden區(qū)收拾干凈了,GC結(jié)束,用戶的應(yīng)用程序繼續(xù)執(zhí)行。
image.png

(3)執(zhí)行四步

  • 初始標(biāo)記: 只標(biāo)記GC Roots能直接關(guān)聯(lián)到的對(duì)象
  • 并發(fā)標(biāo)記: 進(jìn)行GC Roots Tracing的過程
  • 最終標(biāo)記: 修正并發(fā)標(biāo)記期間,因程序運(yùn)行導(dǎo)致標(biāo)記發(fā)生變化的那一部分對(duì)象
  • 篩選回收: 根據(jù)時(shí)間來進(jìn)行價(jià)值最大化的回收
繼續(xù)探究:一文理清JVM和GC(下)

(4)常用配置參數(shù)

  • -XX:+UseG1GC
    開啟G1垃圾收集器
  • -XX:G1HeapRegionSize=n
    設(shè)置G1區(qū)域的大小。值是2的冪,范圍是1M到32M。目標(biāo)是根據(jù)最小的Java堆大小劃分出約2048個(gè)區(qū)域
  • -XX:MaxGCPauseMillis=n
    最大停頓時(shí)間,這是個(gè)軟目標(biāo),JVM將盡可能(但不保證)停頓時(shí)間小于這個(gè)時(shí)間
  • -XX:InitiatingHeapOccupancyPercent=n
    堆占用了多少的時(shí)候就觸發(fā)GC,默認(rèn)是45
  • -XX:ConcGCThreads=n
    并發(fā)GC使用的線程數(shù)
  • -XX:G1ReservePercent=n
    設(shè)置作為空閑時(shí)間的預(yù)留內(nèi)存百分比,以降低目標(biāo)空間溢出的風(fēng)險(xiǎn),默認(rèn)值是10%

(5)與CMS相比的優(yōu)勢(shì)

  • G1不會(huì)產(chǎn)生內(nèi)存碎片
  • 是可以精確控制停頓,該收集器是把整個(gè)堆(新生代、老年代)劃分成多個(gè)固定大小的區(qū)域,每次根據(jù)允許停頓的時(shí)間去收集垃圾最多的區(qū)域。

(6)總結(jié)

繼續(xù)探究:一文理清JVM和GC(下)

五、診斷生產(chǎn)環(huán)境服務(wù)器變慢

整機(jī)相關(guān)

top

image.png

前五行是統(tǒng)計(jì)信息
第一行是任務(wù)隊(duì)列信息,同uptime命令的執(zhí)行結(jié)果一樣
17:16:47:當(dāng)前時(shí)間
up 23:47:系統(tǒng)運(yùn)行時(shí)間
2 users:當(dāng)前登錄用戶數(shù)
load average:0.21,0.27,0.19:系統(tǒng)負(fù)載,即任務(wù)隊(duì)列的平均長度,三個(gè)數(shù)值分別為1分鐘、5分鐘、15分鐘前到現(xiàn)在的平均值

CPU相關(guān)

1)vmstat

image.png

vmstat -n 2 3
第一個(gè)參數(shù)是采樣的時(shí)間間隔數(shù)(單位:秒),第二個(gè)參數(shù)是采樣的次數(shù)
主要參數(shù)

  • procs
    r: 運(yùn)行和等待CPU時(shí)間片的進(jìn)程數(shù),原則上1核的CPU的運(yùn)行隊(duì)列不要超過2,整個(gè)系統(tǒng)的運(yùn)行隊(duì)列不能超過總核數(shù)的2倍,否則代表系統(tǒng)壓力過大。
    b: 等待資源的進(jìn)程數(shù),比如正在等待磁盤I/O,網(wǎng)絡(luò)I/O等。
  • cpu
    us: 用戶進(jìn)程消耗CPU時(shí)間百分比,us值高,用戶進(jìn)程消耗CPU時(shí)間多,如果長期大于50%,需要優(yōu)化程序
    sy: 內(nèi)核進(jìn)程消耗的CPU時(shí)間百分比
    us + sy 參考值為80%,如果us + sy 大于80%,說明可能存在CPU不足
    id: 處于空閑CPU百分比
    wa: 系統(tǒng)等待IO的CPU時(shí)間百分比
    sy: 來自于一個(gè)虛擬機(jī)偷取的CPU時(shí)間的百分比

2)mpstat

mpstat -P ALL 2
查看CPU核信息

image.png

3)pidstat

pidstat -u 1 -p 進(jìn)程號(hào)
每個(gè)進(jìn)程使用cpu的用量分解信息

內(nèi)存相關(guān)

free

應(yīng)用程序中可用內(nèi)存 / 系統(tǒng)物理內(nèi)存>70%:內(nèi)存充足
應(yīng)用程序可用內(nèi)存/系統(tǒng)物理內(nèi)存<20% 內(nèi)存不足:需要增加內(nèi)存
20%<應(yīng)用程序可用內(nèi)存/系統(tǒng)物理內(nèi)存<70%: 內(nèi)存基本夠用

繼續(xù)探究:一文理清JVM和GC(下)

硬盤相關(guān)

df

查看磁盤剩余空閑數(shù)


image.png

硬盤IO相關(guān)

iostat -xdk 2 3

image.png

六、分析生產(chǎn)環(huán)境CPU占用過高

步驟1

先用top命令找出CPU占比最高的

步驟2

ps -ef 或者 jps 進(jìn)一步定位,得知是一個(gè)怎樣的后臺(tái)程序

步驟3

定位到具體線程或者代碼
ps -mp 進(jìn)程 ==-o== THREAD,tid,time

-o:該參數(shù)是用戶自定義格式
-p:pid進(jìn)程使用cpu的時(shí)間
-m: 顯示所有線程

步驟4

將需要的線程ID轉(zhuǎn)換為16進(jìn)制格式(英文小寫格式)
再使用:printf "%x/\n" 有問題的線程ID

步驟5:

jstat 進(jìn)程ID | grep tid(16進(jìn)制線程ID小寫英文)

七、常用的JVM監(jiān)控和性能分析工具

  • jps 虛擬機(jī)進(jìn)程狀況工具
  • jinfo Java配置信息工具
  • jmap 內(nèi)存映像工具
  • jstat 統(tǒng)計(jì)信息監(jiān)控工具
image.png

原文鏈接:
https://juejin.cn/post/6844904119321411591

</article>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容