JAVA監控和調優工具操作指南

前言

我們在日常的開發和維護工作中,免不了需要對JAVA程序進行監控、調優以及問題排查。

給一個系統定位問題的時候,知識、經驗是關鍵基礎,數據是依據,工具是運用知識處理數據的手段。這裏說的數據包括∶運行日誌、異常堆棧、GC日誌、線程快照(thread dump/java core文件)、堆轉儲快照(heap dump/hprof文件)等。

經常使用適當的監控和分析工具可以加快我們分析數據、定位解決問題的速度,但在學習工具前,也應當意識到工具永遠都是知識技能的一層包裝,沒有什麼工具是"祕密武器",不可能學會了就能包治百病。

進程id的獲取

許多工具或者命令需要用到java進程的進程id,有必要回顧一下。

  1. 查看當前運行的所有的java進程:ps -ef|grep java
  2. 準確獲取定位到tomcat下正在運行的java進程的PID命令:ps -ef|grep java | grep catalina | awk '{print $2}'
  3. 準確定位到tomcat下正在運行的java進程相關信息:ps -ef|grep java | grep catalina.

jinfo/jmap訪問受限的解決

一般情況下,我們使用jinfo命令,可能會遇到如下的報錯:

這是因爲新版的Linux系統加入了 ptrace-scope 機制,該機制的目的是防止用戶訪問正在執行的進程的內存,但是如jinfo,jmap這些調試類工具本身就是利用ptrace來獲取執行進程的內存等信息。

解決:

  1. 臨時解決,該方法在下次重啓前有效:echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
  2. 永久解決,直接修改內核參數:sudo vi /etc/sysctl.d/10-ptrace.conf
    • 編輯這行: kernel.yama.ptrace_scope = 1
    • 修改爲: kernel.yama.ptrace_scope = 0
    • 重啓系統,使修改生效。

參數名:kernel.yama.ptrace_scope(值爲1:表示禁止用戶訪問正在執行的進程的內存;0表示可以訪問)

1 jps 顯示JVM進程信息

jps (Java Virtual Machine Process Status Tool),是java提供的一個顯示當前所有JAVA進程pid的命令,適合在linux/unix平臺上簡單察看當前java進程的一些簡單情況。

我們常常會用到unix系統裏的ps命令,這個命令主要是用來顯示當前系統的進程情況,有哪些進程以及進程id。

jps就是java程序版本的ps命令,它的作用是顯示當前系統的java進程情況及進程id。

格式:jps [-命令選項]

1.1 jps的選項

jps默認只會打印進程id和java類名,如果要更具體的信息,則要藉助更多的選項:

  1. jps -q

    • 只顯示pid,不顯示class名稱,jar文件名和傳遞給main方法的參數
  2. jps -m

    • 輸出傳遞給main方法的參數,在嵌入式jvm上可能是null
  3. jps -l

    • 輸出應用程序main class的完整package名或者應用程序的jar文件完整路徑名
  4. jps -v

    • 輸出傳遞給JVM的參數
  5. jps -V

    • 隱藏輸出傳遞給JVM的參數

2 jinfo 顯示JVM配置信息

jinfo 是 JDK 自帶的命令,可以用來查看正在運行的 java 應用程序的擴展參數,包括Java System屬性和JVM命令行參數;也可以動態的修改正在運行的JVM一些參數。當系統崩潰時,jinfo也可以從core文件裏面知道崩潰的Java應用程序的配置信息

Usage:
    jinfo [option] <pid>
        (to connect to running process)
    jinfo [option] <executable <core>
        (to connect to a core file)
    jinfo [option] [server_id@]<remote server IP or hostname>
        (to connect to remote debug server)

where <option> is one of:
    -flag <name>         to print the value of the named VM flag
    -flag [+|-]<name>    to enable or disable the named VM flag
    -flag <name>=<value> to set the named VM flag to the given value
    -flags               to print VM flags
    -sysprops            to print Java system properties
    <no option>          to print both of the above
    -h | -help           to print this help message

格式:jinfo [-命令選項] <pid>jinfo [-命令選項] <executable core>jinfo [-命令選項] [server_id@] <remote ip or hostname>

  • pid:對應jvm的進程id
  • executable core:產生core dump文件
  • remote server IP or hostname:遠程調試服務的ip或者hostname
  • server-id:唯一id,假如一臺主機上多個遠程debug服務;

Javacore,也可以稱爲“threaddump”或是“javadump”,它是 Java 提供的一種診斷特性,能夠提供一份可讀的當前運行的 JVM 中線程使用情況的快照。即在某個特定時刻,JVM 中有哪些線程在運行,每個線程執行到哪一個類,哪一個方法。 應用程序如果出現不可恢復的錯誤或是內存泄露,就會自動觸發 Javacore 的生成。

jinfo工具特別強大,有衆多的可選命令選項,比如:

2.1 輸出JVM進程的參數和屬性

jinfo <pid>

不帶任何選項的情況下,輸出當前 jvm 進程的全部參數和系統屬性

2.2 打印JVM特定參數的值

jinfo -flag <name> <pid>

用於打印虛擬機標記參數的值,name表示虛擬機標記參數的名稱。

2.3 開啓或關閉JVM特定參數

jinfo -flag [+|-]<name> <pid>

用於開啓或關閉虛擬機標記參數。+表示開啓,-表示關閉。

2.4 設置JVM特定參數的值

jinfo -flag <name>=<value> <pid>

用於設置虛擬機標記參數,但並不是每個參數都可以被動態修改的。

2.5 打印所有JVM參數

jinfo -flags <pid>

打印虛擬機參數。什麼是虛擬機參數呢?如-XX:NewSize,-XX:OldSize等就是虛擬機參數。

2.6 打印所有系統參數

jinfo -sysprops <pid>

打印所有系統參數

3 jmap 生成內存快照文件

jmap命令是一個可以輸出所有內存中對象的工具,甚至可以將VM 中的heap,以二進制輸出成文本。打印出某個java進程(使用pid)內存內的,所有‘對象’的情況(如:產生那些對象,及其數量)。

Usage:
    jmap [option] <pid>
        (to connect to running process)
    jmap [option] <executable <core>
        (to connect to a core file)
    jmap [option] [server_id@]<remote server IP or hostname>
        (to connect to remote debug server)

where <option> is one of:
    <none>               to print same info as Solaris pmap
    -heap                to print java heap summary
    -histo[:live]        to print histogram of java object heap; if the "live"
                         suboption is specified, only count live objects
    -clstats             to print class loader statistics
    -finalizerinfo       to print information on objects awaiting finalization
    -dump:<dump-options> to dump java heap in hprof binary format
                         dump-options:
                           live         dump only live objects; if not specified,
                                        all objects in the heap are dumped.
                           format=b     binary format
                           file=<file>  dump heap to <file>
                         Example: jmap -dump:live,format=b,file=heap.bin <pid>
    -F                   force. Use with -dump:<dump-options> <pid> or -histo
                         to force a heap dump or histogram when <pid> does not
                         respond. The "live" suboption is not supported
                         in this mode.
    -h | -help           to print this help message
    -J<flag>             to pass <flag> directly to the runtime system

64位機上使用需要使用如下方式:jmap -J-d64 -heap pid

格式:jmap [option] <pid>jmap [option] <executable <core>jmap [option] [server_id@]<remote server IP or hostname>

  • pid:對應jvm的進程id
  • executable core:產生core dump文件
  • remote server IP or hostname:遠程調試服務的ip或者hostname
  • server-id:唯一id,假如一臺主機上多個遠程debug服務;

jinfo工具特別強大,有衆多的可選命令選項,比如:

3.1 輸出hprof二進制格式的heap文件

jmap -dump:live,format=b,file=myjmapfile.txt <pid>jmap -dump:file=myjmapfile.hprof,format=b <pid>

使用hprof二進制形式,輸出jvm的heap內容到文件,file=可以指定文件存放的目錄。live子選項是可選的,假如指定live選項,那麼只輸出活的對象到文件。

3.2 打印正等候回收的對象的信息

jmap -finalizerinfo <pid>

打印正等候回收的對象的信息。

Number of objects pending for finalization: 0 表示等候回收的對象爲0個

3.3 打印heap的概要信息

jmap -heap <pid>

打印heap的概要信息,GC使用的算法,heap(堆)的配置及JVM堆內存的使用情況。

Attaching to process ID 2805, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.181-b13

using thread-local object allocation.
Parallel GC with 4 thread(s)   ##GC 方式

Heap Configuration:  ##堆配置情況,也就是JVM參數配置的結果[平常說的tomcat配置JVM參數,就是在配置這些]
   MinHeapFreeRatio         = 0  ##最小堆使用比例
   MaxHeapFreeRatio         = 100  ##最大堆可用比例
   MaxHeapSize              = 734003200 (700.0MB)  ##最大堆空間大小
   NewSize                  = 21495808 (20.5MB)  ##新生代分配大小
   MaxNewSize               = 244318208 (233.0MB)  ##最大可新生代分配大小
   OldSize                  = 43515904 (41.5MB)  ##老年代大小
   NewRatio                 = 2  ##新生代比例
   SurvivorRatio            = 8  ##新生代與suvivor的比例
   MetaspaceSize            = 21807104 (20.796875MB)  ## 元數據空間大小
   CompressedClassSpaceSize = 1073741824 (1024.0MB)  ## 壓縮空間大小
   MaxMetaspaceSize         = 17592186044415 MB  ## 最大元數據空間大小
   G1HeapRegionSize         = 0 (0.0MB)  ## G1的對region空間大小

Heap Usage:  ##堆使用情況【堆內存實際的使用情況】
PS Young Generation  ##新生代(伊甸區Eden區 + 倖存區survior(1+2)空間)
Eden Space:   ##伊甸區
   capacity = 32505856 (31.0MB)
   used     = 0 (0.0MB)
   free     = 32505856 (31.0MB)
   0.0% used
From Space:  ##survior1區
   capacity = 2621440 (2.5MB)
   used     = 0 (0.0MB)
   free     = 2621440 (2.5MB)
   0.0% used
To Space:  ##survior2 區
   capacity = 4194304 (4.0MB)
   used     = 0 (0.0MB)
   free     = 4194304 (4.0MB)
   0.0% used
PS Old Generation  ##老年代使用情況
   capacity = 21495808 (20.5MB)
   used     = 3738528 (3.565338134765625MB)
   free     = 17757280 (16.934661865234375MB)
   17.391893340320124% used

4524 interned Strings occupying 360168 bytes.

3.4 打印每個class的實例信息

jmap -histo:live <pid>jmap -histo: <pid>

打印每個class的實例數目,內存佔用,類全名信息,VM的內部類名字開頭會加上前綴”*”。如果live子參數加上後,只統計活的對象數量

採用jmap -histo pid>a.log日誌將其保存,在一段時間後,使用文本對比工具,可以對比出GC回收了哪些對象。

jmap -dump:format=b,file=outfile 3024可以將3024進程的內存heap輸出出來到outfile文件裏,再配合MAT(內存分析工具)。

3.5 打印類加載器的數據

jmap -clstats <pid>

-clstats是-permstat的替代方案,在JDK8之前,-permstat用來打印類加載器的數據。打印Java堆內存的永久保存區域的類加載器的智能統計信息。

對於每個類加載器而言,它的名稱、活躍度、地址、父類加載器、它所加載的類的數量和大小都會被打印。此外,包含的字符串數量和大小也會被打印。

3.6 指定傳遞給運行jmap的JVM的參數

jmap -J<flag> <pid>

指定傳遞給運行jmap的JVM的參數

jmap -J-d64 -heap pid表示在64位機上使用jmap -heap

4 jstack 輸出線程堆棧快照

jstack用於打印出給定的java進程ID或core file或遠程調試服務的Java堆棧信息(也就是線程),如果是在64位機器上,需要指定選項"-J-d64",Windows的jstack使用方式只支持以下的這種方式:jstack [-l] pid

如果java程序崩潰生成core文件,jstack工具可以用來獲得core文件的java stack和native stack的信息,從而可以輕鬆地知道java程序是如何崩潰和在程序何處發生問題。

另外,jstack工具還可以附屬到正在運行的java程序中,看到當時運行的java程序的java stack和native stack的信息,如果現在運行的java程序呈現hung的狀態,jstack是非常有用的。

Usage:
    jstack [-l] <pid>
        (to connect to running process)
    jstack -F [-m] [-l] <pid>
        (to connect to a hung process)
    jstack [-m] [-l] <executable> <core>
        (to connect to a core file)
    jstack [-m] [-l] [server_id@]<remote server IP or hostname>
        (to connect to a remote debug server)

Options:
    -F  to force a thread dump. Use when jstack <pid> does not respond (process is hung)
    -m  to print both java and native frames (mixed mode)
    -l  long listing. Prints additional information about locks
    -h or -help to print this help message

格式:jstack [option] <pid>jstack [option] <executable <core>jstack [option] [server_id@]<remote server IP or hostname>

  • pid:對應jvm的進程id
  • executable core:產生core dump文件
  • remote server IP or hostname:遠程調試服務的ip或者hostname
  • server-id:唯一id,假如一臺主機上多個遠程debug服務;

jstack工具特別強大,有衆多的可選命令選項和適用場景,比如:

4.1 程序沒有響應時強制打印線程

jstack -F <pid>

當pid對應的程序沒有響應時,強制打印線程堆棧信息。

4.2 打印完整的堆棧信息

jstack -l <pid>

長列表,打印關於鎖的附加信息,例如屬於java.util.concurrent的ownable synchronizers列表。

4.3 打印java/native框架的所有堆棧

jstack -m <pid>

打印java和native c/c++框架的所有棧信息。

4.4 jstack統計線程數

jstack -l 28367 | grep 'java.lang.Thread.State' | wc -l

4.5 jstack檢測死鎖

我們先寫個死鎖代碼

public class DeathLock {

    private static Lock lock1 = new ReentrantLock();
    private static Lock lock2 = new ReentrantLock();

    public static void deathLock() {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                try {
                    lock1.lock();
                    TimeUnit.SECONDS.sleep(1);
                    lock2.lock();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                try {
                    lock2.lock();
                    TimeUnit.SECONDS.sleep(1);
                    lock1.lock();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        t1.setName("mythread1");
        t2.setName("mythread2");
        t1.start();
        t2.start();
    }

    public static void main(String[] args) {
        deathLock();
    }
}

這個死鎖會輸出如下日誌

"mythread2" #12 prio=5 os_prio=0 tid=0x0000000058ef7800 nid=0x1ab4 waiting on condition [0x0000000059f8f000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000d602d610> (a java.util.concurrent.lock
s.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInt
errupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(A
bstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Abstrac
tQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLo
ck.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)

        at DeathLock$2.run(DeathLock.java:34)

   Locked ownable synchronizers:
        - <0x00000000d602d640> (a java.util.concurrent.locks.ReentrantLock$Nonfa
irSync)

"mythread1" #11 prio=5 os_prio=0 tid=0x0000000058ef7000 nid=0x3e68 waiting on condition [0x000000005947f000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000d602d640> (a java.util.concurrent.lock
s.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInt
errupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(A
bstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Abstrac
tQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLo
ck.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)

        at DeathLock$1.run(DeathLock.java:22)

   Locked ownable synchronizers:
        - <0x00000000d602d610> (a java.util.concurrent.locks.ReentrantLock$Nonfa
irSync)


Found one Java-level deadlock:
=============================
"mythread2":
  waiting for ownable synchronizer 0x00000000d602d610, (a java.util.concurrent.l
ocks.ReentrantLock$NonfairSync),
  which is held by "mythread1"
"mythread1":
  waiting for ownable synchronizer 0x00000000d602d640, (a java.util.concurrent.l
ocks.ReentrantLock$NonfairSync),
  which is held by "mythread2"

Java stack information for the threads listed above:
===================================================
"mythread2":
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000d602d610> (a java.util.concurrent.lock
s.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInt
errupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(A
bstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Abstrac
tQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLo
ck.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)

        at DeathLock$2.run(DeathLock.java:34)
"mythread1":
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000d602d640> (a java.util.concurrent.lock
s.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInt
errupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(A
bstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Abstrac
tQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLo
ck.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)

        at DeathLock$1.run(DeathLock.java:22)

Found 1 deadlock.

4.6 jstack檢測cpu高

步驟一:查看cpu佔用高進程

> top

Mem:  16333644k total,  9472968k used,  6860676k free,   165616k buffers
Swap:        0k total,        0k used,        0k free,  6665292k cached

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
17850 root      20   0 7588m 112m  11m S 100.7  0.7  47:53.80 java
 1552 root      20   0  121m  13m 8524 S  0.7  0.1  14:37.75 AliYunDun
 3581 root      20   0 9750m 2.0g  13m S  0.7 12.9 298:30.20 java
    1 root      20   0 19360 1612 1308 S  0.0  0.0   0:00.81 init
    2 root      20   0     0    0    0 S  0.0  0.0   0:00.00 kthreadd
    3 root      RT   0     0    0    0 S  0.0  0.0   0:00.14 migration/0

步驟二:查看cpu佔用高線程

> top -H -p 17850

top - 17:43:15 up 5 days,  7:31,  1 user,  load average: 0.99, 0.97, 0.91
Tasks:  32 total,   1 running,  31 sleeping,   0 stopped,   0 zombie
Cpu(s):  3.7%us,  8.9%sy,  0.0%ni, 87.4%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:  16333644k total,  9592504k used,  6741140k free,   165700k buffers
Swap:        0k total,        0k used,        0k free,  6781620k cached

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
17880 root      20   0 7588m 112m  11m R 99.9  0.7  50:47.43 java
17856 root      20   0 7588m 112m  11m S  0.3  0.7   0:02.08 java
17850 root      20   0 7588m 112m  11m S  0.0  0.7   0:00.00 java
17851 root      20   0 7588m 112m  11m S  0.0  0.7   0:00.23 java
17852 root      20   0 7588m 112m  11m S  0.0  0.7   0:02.09 java
17853 root      20   0 7588m 112m  11m S  0.0  0.7   0:02.12 java
17854 root      20   0 7588m 112m  11m S  0.0  0.7   0:02.07 java

步驟三:轉換線程ID

> printf "%x\n" 17880
45d8

步驟四:定位cpu佔用線程

jstack 17850|grep 45d8 -A 30
"pool-1-thread-11" #20 prio=5 os_prio=0 tid=0x00007fc860352800 nid=0x45d8 runnable [0x00007fc8417d2000]
   java.lang.Thread.State: RUNNABLE
        at java.io.FileOutputStream.writeBytes(Native Method)
        at java.io.FileOutputStream.write(FileOutputStream.java:326)
        at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
        at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
        - locked <0x00000006c6c2e708> (a java.io.BufferedOutputStream)
        at java.io.PrintStream.write(PrintStream.java:482)
        - locked <0x00000006c6c10178> (a java.io.PrintStream)
        at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
        at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
        at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
        - locked <0x00000006c6c26620> (a java.io.OutputStreamWriter)
        at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
        at java.io.PrintStream.write(PrintStream.java:527)
        - eliminated <0x00000006c6c10178> (a java.io.PrintStream)
        at java.io.PrintStream.print(PrintStream.java:597)
        at java.io.PrintStream.println(PrintStream.java:736)
        - locked <0x00000006c6c10178> (a java.io.PrintStream)
        at com.demo.guava.HardTask.call(HardTask.java:18)
        at com.demo.guava.HardTask.call(HardTask.java:9)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at java.lang.Thread.run(Thread.java:745)

"pool-1-thread-10" #19 prio=5 os_prio=0 tid=0x00007fc860345000 nid=0x45d7 waiting on condition [0x00007fc8418d3000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000006c6c14178> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)

5 jstat 收集JVM運行數據

Jstat是JDK自帶的一個輕量級小工具。全稱“Java Virtual Machine statistics monitoring tool”,它位於java的bin目錄下,主要利用JVM內建的指令對Java應用程序的資源和性能進行實時的命令行的監控,包括了堆內存各部分的使用量,以及加載類的數量,還有垃圾回收狀況的監控。

可見,Jstat是輕量級的、專門針對JVM的工具。

格式:jstat [-命令選項] <pid>

jstat工具特別強大,有衆多的可選項,詳細查看堆內各個部分的使用量,以及加載類的數量。使用時,需加上查看進程的進程id,和所選參數。參考格式如下:

5.1 類加載統計

jstat –class <pid>

顯示加載class的數量,及所佔空間等信息。

顯示列名 具體描述
Loaded 裝載的類的數量
Bytes 裝載類所佔用的字節數
Unloaded 卸載類的數量
Bytes 卸載類的字節數
Time 裝載和卸載類所花費的時間

5.2 編譯統計

jstat -compiler <pid>

顯示VM實時編譯的數量等信息。

顯示列名 具體描述
Compiled 編譯任務執行數量
Failed 編譯任務執行失敗數量
Invalid 編譯任務執行失效數量
Time 編譯任務消耗時間
FailedType 最後一個編譯失敗任務的類型
FailedMethod 最後一個編譯失敗任務所在的類及方法

5.3 垃圾回收統計

jstat -gc <pid>

顯示gc的信息,查看gc的次數,及時間。

顯示列名 具體描述
S0C 年輕代中第一個survivor(倖存區)的容量 (字節)
S1C 年輕代中第二個survivor(倖存區)的容量 (字節)
S0U 年輕代中第一個survivor(倖存區)目前已使用空間 (字節)
S1U 年輕代中第二個survivor(倖存區)目前已使用空間 (字節)
EC 年輕代中Eden(伊甸區)的容量 (字節)
EU 年輕代中Eden(伊甸區)目前已使用空間 (字節)
OC Old代的容量 (字節)
OU Old代目前已使用空間 (字節)
PC Perm(持久代)的容量 (字節)
PU Perm(持久代)目前已使用空間 (字節)
YGC 從應用程序啓動到採樣時年輕代中gc次數
YGCT 從應用程序啓動到採樣時年輕代中gc所用時間(s)
FGC 從應用程序啓動到採樣時old代(full gc)gc次數
FGCT 從應用程序啓動到採樣時old代(full gc)gc所用時間(s)
GCT 從應用程序啓動到採樣時gc用的總時間(s)

5.4 堆內存統計

jstat -gccapacity <pid>

顯示VM內存中三代(young,old,perm)對象的使用和佔用大小

顯示列名 具體描述
NGCMN 年輕代(young)中初始化(最小)的大小(字節)
NGCMX 年輕代(young)的最大容量 (字節)
NGC 年輕代(young)中當前的容量 (字節)
S0C 年輕代中第一個survivor(倖存區)的容量 (字節)
S1C 年輕代中第二個survivor(倖存區)的容量 (字節)
EC 年輕代中Eden(伊甸區)的容量 (字節)
OGCMN old代中初始化(最小)的大小 (字節)
OGCMX old代的最大容量(字節)
OGC old代當前新生成的容量 (字節)
OC old代的容量 (字節)
PGCMN perm代中初始化(最小)的大小 (字節)
PGCMX perm代的最大容量 (字節)
PGC perm代當前新生成的容量 (字節)
PC Perm(持久代)的容量 (字節)
YGC 從應用程序啓動到採樣時年輕代中gc次數
FGC 從應用程序啓動到採樣時old代(full gc)gc次數

5.5 新生代垃圾回收統計

jstat -gcnew <pid>

統計年輕代對象的信息

顯示列名 具體描述
S0C 年輕代中第一個survivor(倖存區)的容量 (字節)
S1C 年輕代中第二個survivor(倖存區)的容量 (字節)
S0U 年輕代中第一個survivor(倖存區)目前已使用空間 (字節)
S1U 年輕代中第二個survivor(倖存區)目前已使用空間 (字節)
TT 持有次數限制
MTT 最大持有次數限制
DSS 期望的倖存區大小
EC 年輕代中Eden(伊甸區)的容量 (字節)
EU 年輕代中Eden(伊甸區)目前已使用空間 (字節)
YGC 從應用程序啓動到採樣時年輕代中gc次數
YGCT 從應用程序啓動到採樣時年輕代中gc所用時間(s)

5.6 新生代內存統計

jstat -gcnewcapacity <pid>

統計年輕代對象的信息及其佔用量。

顯示列名 具體描述
NGCMN 年輕代(young)中初始化(最小)的大小(字節)
NGCMX 年輕代(young)的最大容量 (字節)
NGC 年輕代(young)中當前的容量 (字節)
S0CMX 年輕代中第一個survivor(倖存區)的最大容量 (字節)
S0C 年輕代中第一個survivor(倖存區)的容量 (字節)
S1CMX 年輕代中第二個survivor(倖存區)的最大容量 (字節)
S1C 年輕代中第二個survivor(倖存區)的容量 (字節)
ECMX 年輕代中Eden(伊甸區)的最大容量 (字節)
EC 年輕代中Eden(伊甸區)的容量 (字節)
YGC 從應用程序啓動到採樣時年輕代中gc次數
FGC 從應用程序啓動到採樣時old代(full gc)gc次數

5.7 老年代垃圾回收統計

jstat -gcold <pid>

統計老年代對象的信息

顯示列名 具體描述
MC 方法區大小
MU 方法區使用大小
CCSC 壓縮類空間大小
CCSU 壓縮類空間使用大小
OC Old代的容量 (字節)
OU Old代目前已使用空間 (字節)
YGC 從應用程序啓動到採樣時年輕代中gc次數
FGC 從應用程序啓動到採樣時old代(full gc)gc次數
YGCT 從應用程序啓動到採樣時年輕代中gc所用時間(s)
GCT 從應用程序啓動到採樣時gc用的總時間(s)

5.8 老年代內存統計

jstat -gcoldcapacity <pid>

統計老年代對象的信息及其佔用量

顯示列名 具體描述
OGCMN old代中初始化(最小)的大小 (字節)
OGCMX old代的最大容量(字節)
OGC old代當前新生成的容量 (字節)
OC Old代的容量 (字節)
YGC 從應用程序啓動到採樣時年輕代中gc次數
FGC 從應用程序啓動到採樣時old代(full gc)gc次數
YGCT 從應用程序啓動到採樣時年輕代中gc所用時間(s)
GCT 從應用程序啓動到採樣時gc用的總時間(s)

5.9 元數據空間統計

jstat -gcmetacapacity <pid>

統計元數據空間容量

顯示列名 具體描述
MCMN 最小元數據容量
MCMX 最大元數據容量
MC 方法區大小
CCSMN 最小壓縮類空間大小
CCSMX 最大壓縮類空間大小
CCSC 壓縮類空間大小
YGC 從應用程序啓動到採樣時年輕代中gc次數
FGC 從應用程序啓動到採樣時old代(full gc)gc次數
FGCT 從應用程序啓動到採樣時old代(full gc)gc所用時間(s)
GCT 從應用程序啓動到採樣時gc用的總時間(s)

5.10 總結垃圾回收統計

jstat -gcutil <pid>

統計gc容量佔比信息

顯示列名 具體描述
S0 年輕代中第一個survivor(倖存區)已使用的佔當前容量百分比
S1 年輕代中第二個survivor(倖存區)已使用的佔當前容量百分比
E 年輕代中Eden(伊甸區)已使用的佔當前容量百分比
O old代已使用的佔當前容量百分比
P perm代已使用的佔當前容量百分比
YGC 從應用程序啓動到採樣時年輕代中gc次數
YGCT 從應用程序啓動到採樣時年輕代中gc所用時間(s)
FGC 從應用程序啓動到採樣時old代(full gc)gc次數
FGCT 從應用程序啓動到採樣時old代(full gc)gc所用時間(s)
GCT 從應用程序啓動到採樣時gc用的總時間(s)

5.11 JVM編譯方法統計

jstat -printcompilation <pid>

統計 JVM編譯方法的信息

顯示列名 具體描述
Compiled 最近編譯方法的數量
Size 最近編譯方法的字節碼數量
Type 最近編譯方法的編譯類型。
Method 方法名標識

6 jhat 堆快照文件可視化工具

jhat(Java Virtual Machine Heap Analysis Tool)虛擬機堆轉儲快照分析工具,也是jdk內置的工具之一,是個用來分析java堆內存的命令,它會建立一個HTTP/HTML服務器,讓用戶可以在瀏覽器上查看分析結果,包括對象的數量,大小等等,並支持對象查詢語言(OQL)。

jhat的作用對象是堆快照文件,也就是dump文件或者hprof文件,文件生成後,我們再使用jaht進行分析。

  1. 使用jmap命令獲取java程序堆快照(生成dump文件)

  2. 使用jconsole選項通過HotSpotDiagnosticMXBean從運行時獲得堆快照(生成dump文件)

  3. 虛擬機啓動時如果指定了-XX:+HeapDumpOnOutOfMemoryError選項, 則在拋出OutOfMemoryError時, 會自動執行堆快照(生成dump文件)

  4. 使用 hprof 命令獲得hprof文件(生成hprof文件)

用法jhat [ options ] heap-dump-file,如jhat -J-Xmx512M app.dump

option具體選項及作用如下:

  1. -J< flag > 因爲 jhat 命令實際上會啓動一個JVM來執行, 通過 -J 可以在啓動JVM時傳入一些啓動參數. 例如, -J-Xmx512m 則指定運行 jhat 的Java虛擬機使用的最大堆內存爲 512 MB. 如果需要使用多個JVM啓動參數,則傳入多個 -Jxxxxxx
  2. -stack false|true 關閉跟蹤對象分配調用堆棧。如果分配位置信息在堆轉儲中不可用. 則必須將此標誌設置爲 false. 默認值爲 true.
  3. -refs false|true 關閉對象引用跟蹤。默認情況下, 返回的指針是指向其他特定對象的對象,如反向鏈接或輸入引用(referrers or incoming references), 會統計/計算堆中的所有對象。
  4. -port port-number 設置 jhat HTTP server 的端口號. 默認值 7000。
  5. -exclude exclude-file 指定對象查詢時需要排除的數據成員列表文件。 例如, 如果文件列出了 java.lang.String.value , 那麼當從某個特定對象 Object o 計算可達的對象列表時, 引用路徑涉及 java.lang.String.value 的都會被排除。
  6. -baseline exclude-file 指定一個基準堆轉儲(baseline heap dump)。 在兩個 heap dumps 中有相同 object ID 的對象會被標記爲不是新的(marked as not being new). 其他對象被標記爲新的(new). 在比較兩個不同的堆轉儲時很有用。
  7. -debug int 設置 debug 級別. 0 表示不輸出調試信息。 值越大則表示輸出更詳細的 debug 信息。
  8. -version 啓動後只顯示版本信息就退出。

有時dump出來的堆很大,在啓動時會報堆空間不足的錯誤,可加參數:jhat -J-Xmx512m <heap dump file>。這個內存大小可根據自己電腦進行設置。

不過實事求是地說,在實際工作中,除非真的沒有別的工具可用,否則一般不會去直接使用jhat命令來分析demp文件,主要原因有二:

  • 一是一般不會在部署應用程序的服務器上直接分析dump文件,即使可以這樣做,也會盡量將dump文件拷貝到其他機器上進行分析,因爲分析工作是一個耗時且消耗硬件資源的過程,既然都要在其他機器上進行,就沒必要受到命令行工具的限制了;
  • 另外一個原因是jhat的分析功能相對來說很簡陋,VisualVM以及專門分析dump文件的Eclipse Memory Analyzer、IBM HeapAnalyzer等工具,都能實現比jhat更強大更專業的分析功能。

6.1 jhat工具的開啓

  1. 使用jps獲取java應用的pid
$ jps
> 17904 -- process information unavailable
> 40836 Jps
> 43228 -- process information unavailable
  1. 使用jmap獲取dump文件
$ jmap -dump:file=test.dump,format=b 43228
> Dumping heap to D:\projects\i-lupro-app\test.dump ...
> Heap dump file created
  1. 使用jhat分析dump文件
$ jhat -J-Xmx512M test.dump
> Reading from test.dump...
> Dump file created Wed Nov 25 18:48:51 CST 2020
> Snapshot read, resolving...
> Resolving 197329 objects...
> Chasing references, expect 39 dots.......................................
> Eliminating duplicate references.......................................
> Snapshot resolved.
> Started HTTP server on port 7000
> Server is ready.
  1. 在瀏覽器打開http://localhost:7000/開啓可視化工具

6.2 jhat工具的功能

6.2.1 顯示出堆中所包含的所有的類

6.2.2 從根集能引用到的對象

6.2.3 顯示平臺包括的所有類的實例數量

6.2.4 堆實例的分佈表

6.2.5 執行對象查詢語句(OQL)

其輸入內容如:

# 查詢長度大於100的字符串
select s from java.lang.String s where s.count > 100

詳細的OQL可點擊上圖的“OQL help”

7 jconsole 可視化監控控制檯

Jconsole(Java Monitoring and Management Console),一種基於JMX的可視化監視、管理工具。

7.1 啓動JConsole

  • 點擊JDK/bin 目錄下面的jconsole.exe 即可啓動
  • 然後會自動自動搜索本機運行的所有虛擬機進程。
  • 選擇其中一個進程可開始進行監控

7.2 JConsole介紹

JConsole 基本包括以下基本功能:概述、內存、線程、類、VM概要、MBean

7.2.1 概覽

7.2.2 內存監控

內存頁籤相對於可視化的jstat 命令,用於監視受收集器管理的虛擬機內存。

jconsole可監控的內存有許多,如下圖

我們以堆內存爲例:

選項 描述
堆內存的大小 442032KB
已使用 249362KB 目前使用的內存量,包括所有對象,可達和不可達佔用的內存。
已提交 442032KB 保證由Java虛擬機使用的內存量。 提交的內存量可能會隨時間而改變。 Java虛擬機可能會釋放系統內存,並已提交的內存量可能會少於最初啓動時分配的內存量。 提交的內存量將始終大於或等於使用的內存量。
最大值 742400KB 可用於內存管理的最大內存量。 它的價值可能會發生變化,或者是不確定的。 如果Java虛擬機試圖增加使用的內存要大於提交的內存,內存分配可能失敗,即使使用量小於或等於最大值(例如,當系統上的虛擬內存不足)。
GC時間 parnew上的 3.487s(73收集) 累計時間花在垃圾收集和調用的總數。 它可能有多個行,其中每一個代表一個垃圾收集器算法在Java虛擬機的總耗時和執行次數
-- 堆內存是運行時數據區域,Java VM的所有類實例和數組分配內存。 可能是固定或可變大小的堆。
非堆內存 -- 非堆內存包括在所有線程和Java虛擬機內部處理或優化所需的共享的方法。 它存儲了類的結構,運行常量池,字段和方法數據,以及方法和構造函數的代碼,方法區在邏輯上是堆的一部分,看具體實現的方式。根據實現方式的不同,Java虛擬機可能不進行垃圾收集或壓縮。 堆內存一樣,方法區域可能是一個固定或可變大小。 方法區的內存不需要是連續的。

除了方法區,Java虛擬機可能需要進行內部處理或優化,這也屬於非堆內存的內存。 例如,實時(JIT)編譯器需要內存用於存儲從Java虛擬機的高性能的代碼翻譯的機器碼。

7.2.3 線程監控

如果上面的“內存”頁籤相當於可視化的jstat命令的話,“線程”頁籤的功能相當於可視化的jstack命令,遇到線程停頓時可以使用這個頁籤進行監控分析。

在左下角的“線程”列表列出了所有的活動線程。 如果你輸入一個“filter”字段中的字符串,線程列表將只顯示其名稱中包含你輸入字符串線程。 點擊一個線程在線程列表的名稱,顯示該線程的信息的權利,包括線程的名稱,狀態、阻塞和等待的次數、堆棧跟蹤。

如果要檢查您的應用程序已經陷入死鎖的線程,可以通過點擊“檢測死鎖”按鈕檢測。線程長時間停頓的主要原因主要有:等待外部資源(數據庫連接、網絡資源、設備資源等)、死循環、鎖等待(活鎖和死鎖)

我們寫個死鎖代碼

package com.jvm;
/**
 * 線程死鎖驗證
 */
public class JConsoleThreadLock {
    /**
     * 線程死鎖等待演示
     */
    static class SynAddRunalbe implements Runnable {
        int a, b;
        public SynAddRunalbe(int a, int b) {
            this.a = a;
            this.b = b;
        }
        @Override
        public void run() {
            synchronized (Integer.valueOf(a)) {
                synchronized (Integer.valueOf(b)) {
                    System.out.println(a + b);
                }
            }
        }
    }
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(new SynAddRunalbe(1, 2)).start();
            new Thread(new SynAddRunalbe(2, 1)).start();
        }
    }
}

這段代碼開了200個線程去分別計算1+2以及2+1的值,其實for循環是可省略的,兩個線程也可能會導致死鎖,不過那樣概率太小,需要嘗試運行很多次才能看到效果。一般的話,帶for循環的版本最多運行2~3次就會遇到線程死鎖,程序無法結束。

造成死鎖的原因是Integer.valueOf()方法基於減少對象創建次數和節省內存的考慮,[-128,127]之間的數字會被緩存,當valueOf()方法傳入參數在這個範圍之內,將直接返回緩存中的對象。也就是說,代碼中調用了200次Integer.valueOf()方法一共就只返回了兩個不同的對象。假如在某個線程的兩個synchronized塊之間發生了一次線程切換,那就會出現線程A等着被線程B持有的Integer.valueOf(1),線程B又等着被線程A持有的Integer.valueOf(2),結果出現大家都 跑不下去的情景。

如果檢測到任何死鎖的線程,這些都顯示在一個新的標籤,旁邊出現的“死鎖”標籤, 在圖所示。

結果描述:顯示了線程Thread-53在等待一個被線程Thread-66持有Integer對象,而點擊線程Thread-66則顯示它也在等待一個Integer對象,被線程Thread-53持有,這樣兩個線程就互相卡住,都不存在等到鎖釋放的希望了

7.2.4 類加載信息監控

類”標籤顯示關於類加載的信息。

  • 紅線表示加載的類的總數(包括後來卸載的)
  • 藍線是當前的類加載數

在選項卡底部的詳細信息部分顯示類的加載,因爲Java虛擬機開始的總數,當前加載和卸載的數量。** 跟蹤**類加載詳細的輸出,您可以勾選在頂部的右上角複選框。

7.2.5 VM概要監控

在此選項卡中提供的信息包括以下內容。

  • 摘要
    • 運行時間 :開始以來,Java虛擬機的時間總額。
    • 進程的CPU時間 :Java VM的開始,因爲它消耗的CPU時間總量。
    • 編譯總時間 :累計時間花費在JIT編譯。
  • 主題
    • 活動線程 :目前現場守護線程,加上非守護線程數量。
    • 峯值 :活動線程的最高數目,因爲Java虛擬機開始。
    • 守護線程 :當前的活動守護線程數量。
    • 總線程 :開始自Java虛擬機啓動的線程總數,包括非守護進程,守護進程和終止的線程。
    • 當前類裝載 :目前加載到內存中的類數目。
    • 總類加載 :從Java VM開始加載到內存中的類總和,包括那些後來被卸載的類。
    • 已卸載類總數 :從Java虛擬機開始從內存中卸載的類的數目。
  • 內存
    • 當前的堆大小 :目前所佔用的堆的千字節數。
    • 分配的內存 :堆分配的內存總量。
    • 最大堆最大值 :堆所佔用的千字節的最大數目。
    • 待最後確定的對象:待最後確定的對象的數量。
    • 花在執行GC的垃圾收集器 :包括垃圾收集,垃圾收集器的名稱,進行藏品的數量和總時間的信息。
  • 操作系統
    • 總物理內存
    • 空閒物理內存
    • 分配的虛擬內存
    • 其他信息
  • VM參數 :輸入參數的應用程序通過Java虛擬機,不包括的主要方法的參數。
    • 類路徑是由系統類加載器用於搜索類文件的類路徑。
    • 庫路徑 :加載庫時要搜索的路徑列表。
    • 引導類路徑 :引導類路徑是由引導類加載器用於搜索類文件。

8 jvisualvm

jvisualvm是Netbeans的profile子項目,從JDK6.0 update 7 版本開始自帶。jvisualvm同jconsole一樣,都是一個基於圖形化界面的、可以查看本地及遠程的JAVA GUI監控工具,jvisualvm是一個綜合性的分析工具,其整合了jstack、jmap、jinfo等衆多調試工具的功能,可以認爲jvisualvm是jconsole的升級版

8.1 啓動jvisualvm

JDK_HOME/bin下雙擊jvisualvm.exe,或者直接在命令行中輸入jvisualvm 都可

我們可以看到側邊框:

  • 本地:如果你本地有java進程啓動了,那麼在本地這個欄目就會顯示。
  • 遠程:監控的遠程主機
  • 快照:裝載dump文件或者hprof文件,進行分析

由於本地和遠程展示的監控界面都是相同的,接下來我們直接介紹遠程。

8.2 添加遠程監控

注意,一個主機如果希望支持遠程監控,需要在啓動時添加以下參數:


-Dcom.sun.management.jmxremote.port=1099
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

右擊"遠程"-->"添加遠程主機",出現界面:

在連接後面添加一個1099,這是遠程主機jmx監聽的端口號,點擊確定,側邊欄變爲:

點擊紅色框中的jmx連接,出現以下界面:

8.3 jvisualvm介紹

jvisualvm分爲四個選項卡:概述、監視、線程、抽樣器,下面我們一一介紹:

8.3.1 概述頁

默認顯示的就是概述選項卡,其中的信息相當於我們調用了jinfo命令獲得,其還包含了兩個子選項卡:

  • jvm參數欄:相當於我們調用jinfo -flags <pid>獲得
  • 系統屬性欄:相當於我們調用jinfo -sysprops <pid>獲得

8.3.2 監視頁

主要顯示了cpu、內存使用、類加載信息、線程信息等,這只是一個概要性的介紹,如下圖:

點擊右上角的"堆dump"會在遠程主機上,dump一個內存映射文件,之所以不直接dump到本地,主要是因爲這個文件通常比較大,直接dump到本地會很慢。

dump完成之後,可以手工下載這個文件,通過"文件"->"裝入"來進行分析。

8.3.3 線程頁

線程選項卡列出了所有線程的信息,並使用了不同的顏色標記,右下角的顏色表示了不同的狀態。

右上角的線程dump會直接把線程信息dump到本地,相當於調用了jstack命令,如:

8.3.4 抽樣器頁

主要有"cpu"和"內存"兩個按鈕,功能類似,只不過一個是抽樣線程佔用cpu的情況,一個是抽樣jvm對象內存的情況。

  1. 通過設置可以對CPU的採樣來源以及內存的刷新時間進行設置;
  2. 點擊CPU或者Memory即可開始監控,點擊Stop則停止採樣;

我們以分析cpu波動爲例,看下如何使用cpu採樣器:

8.3.4.1 分析CPU波動問題

進入抽樣器頁(Sampler),在CPU波動的時候點擊CPU對CPU進行抽樣。

注意線上環境千萬不要使用Sampler右邊的Profiler

抽樣進行一段時間後(建議3分鐘左右就行了,時間越長生成的snapshot越大),點擊”stop”,然後點擊”snapshot”生成快照

生成快照後按照”Total Time(CPU)”排序,找到那些線程最耗費CPU,從下圖中我們看到基本上都是DubboServerHandler,熟悉Dubbo框架的知道這都是我們的業務線程。

那麼我們對這些線程進行分析(多分析幾個線程,雙擊指定線程就可以看這個線程的調用棧以及耗時情況),看看這些線程在哪裏比較耗費CPU。

通過分析發現,在Dubbo遠程調用的時候驗證參數的時間比我們處理業務的時間都長(見下圖紅色方框框起來的方法)。結合Dubbo官方文檔得知,Dubbo的參數驗證這個特性是比較耗費性能的,而我們的接口參數使用了javax.validation註解來驗證參數。所以我們在調用的時候使用validation=”false”禁止使用參數驗證這個特性就好了CPU就回歸正常了。

除此之外,我們也可以動態的觀察線程的變化,功能有點類似JProfiler的“Mark Current Values”。我們點擊線程CPU時間這個tab。查看每個線程佔用cpu時間的增量數據。

8.3.4.2 內存採樣

和cpu採樣一樣的,樣進行一段時間後(建議3分鐘左右就行了,時間越長生成的snapshot越大),點擊”stop”,然後點擊”snapshot”生成快照

點擊增量同樣可以監控內存的變動情況:

點擊“執行GC”,則可以手動觸發GC; 點擊“堆Dump”,則可以手動觸發dump文件生成;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章