JVM性能分析

JIT

在談到 Java 的編譯機制的時候,其實應(yīng)該按時期凌那,分為兩個階段。一個是 javac 指令將 Java 源碼變?yōu)?Java 字節(jié)碼的靜態(tài)編譯過程。另一個是 Java 字節(jié)碼編譯為本地機器碼的過程辈灼,并且因為這個過程是在程序運行時期完成的所以稱之為即時編譯(JIT),下面我們討論的編譯也都是指“即時編譯”過程役衡。

解釋器

java作為一種跨平臺的語言實現(xiàn)了一次編譯到處運行的特性茵休,這也就決定了它編譯出來的不是機器碼而是特定的字節(jié)碼。解釋器(各平臺不同)就是將字節(jié)碼解釋為機器指令,調(diào)用操作系統(tǒng)來完成程序的執(zhí)行榕莺。

編譯器

解釋器雖然實現(xiàn)了跨平臺的特性俐芯,但是解釋執(zhí)行的效率是很低的,是以犧牲性能為代價來換取的跨平臺特性钉鸯。所以 JVM 發(fā)現(xiàn)某個方法或者代碼塊的運行特別頻繁時吧史,就會把這些代碼認定為“熱點代碼”(Hot Spot Code,不知道Sun的虛擬機命名是否跟這個有聯(lián)系)唠雕。為了提高熱點代碼的執(zhí)行效率贸营,在運行時,虛擬機就會將這些代碼翻譯成與本地平臺相關(guān)的機器碼岩睁,并進行各種層次的優(yōu)化钞脂,完成這個任務(wù)的就是編譯器,被稱為即時編譯器(Just In Time Compiler捕儒,簡稱為JIT)冰啃。

HotSpot 虛擬機內(nèi)置兩個即時編譯器,稱為 Client Compiler 和 Server Compiler刘莹,分別簡稱為 C1阎毅,C2。

  • C1 編譯器是一個簡單快速的編譯器点弯,主要的關(guān)注點在于局部性的優(yōu)化扇调,適用于執(zhí)行時間較短或?qū)有阅苡幸蟮某绦?/li>
  • C2 編譯器是為長期運行的應(yīng)用程序做性能調(diào)優(yōu)的編譯器,適用于執(zhí)行時間較長或?qū)Ψ逯敌阅苡幸蟮某绦蚯栏亍狼钮?赡軙Υa進行激進的優(yōu)化來獲取更好的性能,這些優(yōu)化往往伴隨著耗時較長的代碼分析雌团,同時會設(shè)定“逃生門”在激進優(yōu)化不成立的時候回退到 C1 編譯器或者解釋器繼續(xù)執(zhí)行

分層編譯

由于即時編譯器編譯本地代碼需要占用程序運行時間燃领,而要編譯出優(yōu)化程度較高的代碼,所花費的時間可能更多锦援。為了在程序啟動速度與運行效率之間達到平衡猛蔽,HotSpot 虛擬機啟用了分層編譯(Tiered Compilation)策略。

在分層編譯中灵寺,會同時使用兩個編譯器曼库。當 C2 編譯器在等待并分析一些代碼片段來收集信息的時候,C1 編譯器首先開始編譯略板。這使得 C1 編譯器能夠快速的提高性能毁枯;而 C2 編譯器將能夠更好地提高性能,因為它擁有有熱點方法更好的信息叮称。分層編譯在 JDK1.6 時期出現(xiàn)种玛,在 JDK1.7 的 Server 模式中作為默認編譯策略開啟藐鹤。

根據(jù)編譯器編譯、優(yōu)化的規(guī)模耗時赂韵,劃分出不同的編譯級別:

Level Compiler
0 僅解釋執(zhí)行
1 執(zhí)行不帶 profiling 的 C1 代碼
2 執(zhí)行僅帶方法調(diào)用次數(shù)以及循環(huán)回邊執(zhí)行次數(shù) profiling 的 C1 代碼
3 執(zhí)行帶所有 profiling 的 C1 代碼
4 執(zhí)行 C2 代碼

profiling 就是收集能夠反映程序執(zhí)行狀態(tài)的數(shù)據(jù)娱节。其中最基本的統(tǒng)計數(shù)據(jù)就是方法的調(diào)用次數(shù),以及循環(huán)回邊的執(zhí)行次數(shù)祭示。

通常情況下肄满,C2 代碼的執(zhí)行效率要比 C1 代碼的高出 30% 以上。對于 C1 代碼的三種狀態(tài)质涛,按執(zhí)行效率從高至低則是 1 層 > 2 層 > 3 層稠歉。其中 1 層的性能比 2 層的稍微高一些,而 2 層的性能又比 3 層高出 30%汇陆。這是因為 profiling 越多怒炸,其額外的性能開銷越大。

這 5 個層次的執(zhí)行狀態(tài)中毡代,1 層和 4 層為終止狀態(tài)横媚。當一個方法被終止狀態(tài)編譯過后,如果編譯后的代碼并沒有失效月趟,那么 Java 虛擬機將不再次發(fā)出該方法的編譯請求的。

編譯路徑

上圖列舉了一些編譯的路徑恢口。

通常情況下孝宗,熱點方法會經(jīng)過 3 層的 C1 編譯,然后再被 4 層的 C2 編譯耕肩。

如果方法的字節(jié)碼數(shù)目比較少(如 getter/setter)因妇,而且 3 層的 profiling 沒有可收集的數(shù)據(jù)。那么 JVM 斷定該方法對于 C1 代碼和 C2 代碼的執(zhí)行效率相同猿诸。在這種情況下婚被,Java 虛擬機會在 3 層編譯之后,直接選擇用 1 層的 C1 編譯梳虽。由于這是一個終止狀態(tài)址芯,因此 Java 虛擬機不會繼續(xù)用 4 層的 C2 編譯。

默認啟用的是混合模式(解釋器與編譯器配合工作)
可以使用 -Xint 參數(shù)強制虛擬機運行于只有解釋器模式下
可以使用 -Xcomp 強制虛擬機運行于只有 JIT 的編譯模式下
Java8 中默認開啟分層編譯 -client窜觉,-server 參數(shù)已經(jīng)無效谷炸,如果只想開啟 C2,可以關(guān)閉分層編譯(-XX:-TieredCompilation)
如果只想開啟 C1禀挫,可以在打開分層編譯的同時旬陡,使用參數(shù):-XX:TieredStopAtLevel=1。

熱點探測

JIT 編譯器基于一個非秤镉ぃ基本的原則:編譯和優(yōu)化執(zhí)行頻率更高的代碼段描孟。如果代碼很少執(zhí)行驶睦,即使優(yōu)化之后提升 80% 的速度也是沒有必要的∧湫眩可以說熱點代碼是 JIT 編譯的前提场航,而熱點代碼的判定就是基于熱點探測技術(shù)。

基于采樣的熱點探測

主要是虛擬機會周期性的檢查各個線程的棧頂青抛,若某個或某些方法經(jīng)常出現(xiàn)在棧頂旗闽,那這個方法就是“熱點方法”。

優(yōu)點是實現(xiàn)簡單蜜另。

缺點是很難精確一個方法的熱度适室,容易受到線程阻塞或外界因素的影響。

基于計數(shù)器的熱點探測

主要就是虛擬機給每一個方法甚至代碼塊建立了一個計數(shù)器举瑰,統(tǒng)計方法的執(zhí)行次數(shù)捣辆,超過一定的閥值則標記為此方法為熱點方法。

HotSpot 虛擬機使用的基于計數(shù)器的熱點探測方法此迅。然后使用了兩類計數(shù)器:方法調(diào)用計數(shù)器和回邊計數(shù)器汽畴。當方法計數(shù)器和回邊計數(shù)器之和超過方法計數(shù)器閾值時,就會觸發(fā)JIT編譯器耸序。

  • 方法調(diào)用計數(shù)器:方法調(diào)用計數(shù)器用于統(tǒng)計方法被調(diào)用的次數(shù)忍些,默認閾值在 C1 模式下是 1500 次,在 C2 模式在是 10000 次坎怪,可通過 -XX: CompileThreshold 來設(shè)定罢坝;而在分層編譯的情況下 -XX: CompileThreshold 指定的閾值將失效,此時將會根據(jù)當前待編譯的方法數(shù)以及編譯線程數(shù)來動態(tài)調(diào)整搅窿。
  • 回邊計數(shù)器:回邊計數(shù)器用于統(tǒng)計一個方法中循環(huán)體代碼執(zhí)行的次數(shù)嘁酿,在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令稱為“回邊”(Back Edge),該計數(shù)器用于計算是否觸發(fā) C1 編譯的閾值男应。HotSpot 虛擬機提供 -XX:BackEdgeThreshold 供用戶設(shè)置闹司,但是當前的 HotSpot 虛擬機實際上并未使用此參數(shù)。而需要通過 -XX: OnStackReplacePercentage 來間接調(diào)整回邊計數(shù)器的閾值沐飘,在 C1游桩,C2 模式下計算公式也有不同,需要區(qū)別配置薪铜。而在分層編譯的情況下众弓,-XX: OnStackReplacePercentage 指定的閾值同樣會失效,此時將根據(jù)當前待編譯的方法數(shù)以及編譯線程數(shù)來動態(tài)調(diào)整隔箍。

常見編譯優(yōu)化

方法內(nèi)聯(lián)

在編譯時谓娃,將方法調(diào)用優(yōu)化為直接使用方法體中的代碼進行替換,這就是方法內(nèi)聯(lián)蜒滩,這樣做減少了方法調(diào)用過程中壓棧與出棧的開銷滨达,同時也為之后的一些優(yōu)化手段提供條件奶稠。

    @Benchmark
    public int inline() {
        CounterObj counterObj = new CounterObj();
        counterObj.add(1);
        counterObj.add(2);
        return counterObj.getCounter();
    }
    @Benchmark
    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    public int dontInline() {
        CounterObj counterObj = new CounterObj();
        counterObj.add(1);
        counterObj.add(2);
        return counterObj.getCounter();
    }
     public static class CounterObj {
        @Getter
        private int counter;
        public void add(int num) {
            this.counter = sum(this.counter, num);
        }
        public int sum(int a, int b) {
            return a + b;
        }
    }
------------------------------------------------------------------------
Benchmark                Mode  Cnt  Score   Error  Units
MethodInline.dontInline  avgt    5  3.936 ± 0.127  ns/op
MethodInline.inline      avgt    5  2.620 ± 0.042  ns/op

逃逸分析

如果一個變量的使用,在運行期檢測它的作用范圍不會超過一個方法或者一個線程的作用域捡遍。那么這個變量就不會被多個線程所共享锌订,也就是說可以不將其分配在堆空間中,而是將其線程私有化画株。

如何來檢測一個變量的作用域僅在一個方法或者線程中呢? JVM 中使用全局數(shù)據(jù)流分析機制實現(xiàn)的一種機制辆飘,稱之為逃逸分析,作為其他一些激進優(yōu)化的前提條件谓传。

可以通過 -XX:+DoEscapeAnalysis 開啟逃逸分析(jdk8中默認開啟)蜈项,-XX:-DoEscapeAnalysis 來關(guān)閉逃逸分析。下面是基于逃逸分析基礎(chǔ)上做的一些優(yōu)化续挟。

標量替換

  • 標量:即不可被進一步分解的量紧卒,Java 的基本數(shù)據(jù)類型就是標量(如:int,long 等基本數(shù)據(jù)類型以及 reference 類型等)诗祸。
  • 聚合量:標量的對立就是可以被進一步分解的量跑芳,被稱之為聚合量,Java 中對象就是聚合量直颅。

當對象不會被外部訪問博个,并且對象可以被進一步分解時,JVM 不會創(chuàng)建該對象功偿,而會將該對象成員變量分解若干個被這個方法使用的成員變量所代替坡倔,這個過程就是標量替換。對象將跟隨棧的創(chuàng)建而創(chuàng)建脖含,銷毀而銷毀,減輕了 GC 的負擔以及工作內(nèi)存跟主存的同步消耗投蝉。

很多人會把標量替換跟棧上分配拆開來解釋养葵,但我認為標量替換跟棧上分配說的是一件事情。因為在棧上是不能創(chuàng)建對象的(棧上只能存放一些基本類型以及對象的引用)瘩缆,只有進行了標量替換关拒,將聚合量拆分為標量之后才達成棧上分配的目的。

可以通過 -XX:+EliminateAllocations 開啟標量替換(jdk8 中默認開啟)庸娱,-XX:-EliminateAllocations 來關(guān)閉標量替換着绊。

    @Benchmark
    @Fork(jvmArgsAppend = "-XX:+EliminateAllocations")
    public void escaped() {
        methodA();
    }
    @Benchmark
    @Fork(jvmArgsAppend = "-XX:-EliminateAllocations")
    public void noEscape() {
        methodA();
    }
    public void methodA() {
        new Tmp();
    }
    @Data
    public static class Tmp {
        private int data;
    }
------------------------------------------------------------------------
Benchmark               Mode  Cnt  Score   Error  Units
ScalarReplace.escaped   avgt    5  0.354 ± 0.055  ns/op
ScalarReplace.noEscape  avgt    5  2.661 ± 0.264  ns/op

同步消除

當加鎖的變量不會發(fā)生逃逸,是線程私有的時候熟尉,那么完全沒有必要加鎖归露。 在 JIT 時期就可以將同步鎖去掉,以減少加鎖與解鎖造成的資源開銷斤儿。

    @Benchmark
    @Fork(jvmArgsAppend = "-XX:+EliminateLocks")
    public void escaped() {
        methodA();
    }
    @Benchmark
    @Fork(jvmArgsAppend = "-XX:-EliminateLocks")
    public void noEscape() {
        methodA();
    }
    public void methodA() {
        synchronized (new Object()) {
            // do nothing
        }
    }
------------------------------------------------------------------------
Benchmark            Mode  Cnt   Score   Error  Units
LockRemove.escaped   avgt    5   0.357 ± 0.053  ns/op
LockRemove.noEscape  avgt    5  21.847 ± 0.236  ns/op

除了上面舉例的幾種經(jīng)典優(yōu)化方式篇恒,JVM 還為我們執(zhí)行很多其他優(yōu)化逆害,如:無用代碼消除(Dead Code Elimination)次乓、循環(huán)展開(Loop Unrolling)、循環(huán)表達式外提(Loop Expression Hoisting)一铅、消除公共子表達式(Common Subexpression Elimination)、常量傳播(Constant Propagation)堕油、基本塊沖排序(Basic Block Reordering)等潘飘。

代碼緩存

經(jīng)過辛苦的編譯優(yōu)化之后的本地代碼是比較珍貴的,這些代碼會被緩存起來掉缺,當下一次運行的時候就可以直接使用了卜录,也就是所謂的代碼緩存(Code Cache)。在 32 位機器client模式默認 32MB攀圈,64 位機器默認 240MB暴凑。可以使用- XX:InitialCodeCacheSize赘来,-XX:ReservedCodeCacheSize 來修改代碼緩存的大小现喳。

代碼緩存很少引起性能問題,但是一旦發(fā)生其影響可能是毀滅性的犬辰。如果代碼緩存被占滿嗦篱,JVM 會打印出一條警告消息,并切換到 interpreted-only 模式:JIT 編譯器被停用幌缝,字節(jié)碼將不再會被編譯成機器碼灸促。應(yīng)用程序?qū)⒗^續(xù)運行,但運行速度會降低一個數(shù)量級涵卵,直到有人注意到這個問題浴栽。

通過設(shè)置 -XX:+UseCodeCacheFlushing 這個參數(shù),當代碼緩存滿了的時候轿偎,會讓 JVM 換出一部分緩存以容納新編譯的代碼典鸡,避免直接進入解釋模式使性能急劇下降。在默認情況下坏晦,這個選項是關(guān)閉的萝玷。

其他

編譯相關(guān)參數(shù)

  • -XX:+TieredCompilation:開啟分層編譯,jdk8 之后默認開啟
  • -XX:+CICompilerCount=N:編譯線程數(shù)昆婿,設(shè)置數(shù)量后球碉,JVM 會自動分配線程數(shù),C1:C2=1:2
  • -XX:TierXBackEdgeThreshold:OSR 編譯的閾值
  • -XX:TierXMinInvocationThreshold:開啟分層編譯后各層調(diào)用的閾值
  • -XX:TierXCompileThreshold:開啟分層編譯后的編譯閾值
  • -XX:ReservedCodeCacheSize:codeCache 最大大小
  • -XX:InitialCodeCacheSize:codeCache 初始大小
  • -XX:+PrintCompilation:輸出編譯過程
  • -XX:+PrintInlining:輸出方法內(nèi)聯(lián)信息仓蛆,需要跟 -XX:+UnlockDiagnosticVMOptions 一起使用

由于編譯情況復(fù)雜睁冬,JVM 也會動態(tài)調(diào)整相關(guān)的閾值來保證 JVM 的性能,所以不建議手動調(diào)整編譯相關(guān)的參數(shù)看疙。除非一些特定的 Case痴突,比如 CodeCache 滿了停止編譯搂蜓,可以適當增加 CodeCache 大小×勺埃或者一些非常常用的方法帮碰,未被內(nèi)聯(lián)到而拖累了性能,可以調(diào)整內(nèi)斂層數(shù)或者內(nèi)聯(lián)方法的大小來解決拾积。

編譯輸出信息簡介

編譯信息

上圖是一段編譯的信息輸出殉挽,從左到右依次是:

  • timestamp:從開始啟動到現(xiàn)在的時間
  • compile_id:為每個編譯過的方法賦值的一個自增 ID
  • attributes:表示正在編譯的代碼的狀態(tài)
  • tier_level:編譯的級別,可參照上文對編譯級別的介紹
  • method:編譯的方法名
  • size:Java 字節(jié)碼的大小
  • deopt:去優(yōu)化拓巧,也就是廢棄優(yōu)化

attributes信息

有五種不同類型的屬性來表示編譯的狀態(tài)斯碌。

% - The compilation is OSR (on-stack replacement).
s - The method is synchronized.
! - The method has an exception handler.
b - Compilation occurred in blocking mode.
n - Compilation occurred for a wrapper to a native method.

deopt 信息

該字段通常具有以下兩個值之一:“made not entrant”或“made zombie”。

  • made not entrant:有兩種情況會發(fā)生這種情況肛度。①分層編譯模式下傻唾,更好的的優(yōu)化代碼出現(xiàn)時,將舊的編譯代碼無效承耿,例如完成 4 層編譯時候?qū)?3 層編譯無效②編譯器收集了更多的信息冠骄,將優(yōu)化進行回滾以便能夠再次編譯它,并基于新的信息重新優(yōu)化代碼加袋。
  • made zombie:對于僵尸代碼凛辣,這基本上是一種清理機制。在一段代碼被標記為非進入者之后职烧,它最終將被標記為 zombie扁誓,并將由 GC 收集以從代碼緩存中釋放該空間。

我們可以看到為了讓我們的代碼跑的更快蚀之,JVM 默默為我們做了很多的事情蝗敢,但是凡事都是有利有弊。比如一個 QPS 較高的應(yīng)用足删,重啟之后如果沒有比較好的預(yù)熱策略前普,可能就會因為分層編譯導(dǎo)致接口響應(yīng)變慢,CPU 飆升等問題壹堰。

深入理解Java虛擬機--周志明

JVM實用參數(shù)(二)JVM類型、工作模式及代碼緩存

代碼緩存

JIT與C1及C2

JIT——即時編譯的原理

函數(shù)在實現(xiàn)過程內(nèi)存中的壓棧和出棧

jvm之方法內(nèi)聯(lián)優(yōu)化

熱點代碼骡湖、分層編譯贱纠、JIT優(yōu)化(方法內(nèi)聯(lián)、鎖消除响蕴、標量替換)

HotSpot中執(zhí)行引擎技術(shù)詳解(三)——代碼緩存機制

基本功 | Java即時編譯器原理解析及實踐

Java JIT compiler explained – Part 1

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谆焊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子浦夷,更是在濱河造成了極大的恐慌辖试,老刑警劉巖辜王,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異罐孝,居然都是意外死亡呐馆,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門莲兢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來汹来,“玉大人,你說我怎么就攤上這事改艇∈瞻啵” “怎么了?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵谒兄,是天一觀的道長摔桦。 經(jīng)常有香客問我,道長承疲,這世上最難降的妖魔是什么邻耕? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮纪隙,結(jié)果婚禮上赊豌,老公的妹妹穿的比我還像新娘。我一直安慰自己绵咱,他們只是感情好碘饼,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著悲伶,像睡著了一般艾恼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上麸锉,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天钠绍,我揣著相機與錄音,去河邊找鬼花沉。 笑死柳爽,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的碱屁。 我是一名探鬼主播磷脯,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼娩脾!你這毒婦竟也來了赵誓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎俩功,沒想到半個月后幻枉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡诡蜓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年熬甫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片万牺。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡罗珍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出脚粟,到底是詐尸還是另有隱情覆旱,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布核无,位于F島的核電站扣唱,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏团南。R本人自食惡果不足惜噪沙,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吐根。 院中可真熱鬧正歼,春花似錦、人聲如沸拷橘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽冗疮。三九已至萄唇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間术幔,已是汗流浹背另萤。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留诅挑,地道東北人四敞。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像拔妥,于是被迫代替她去往敵國和親忿危。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容