<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; 性能剖析</title>
	<atom:link href="https://blog.gmem.cc/tag/%e6%80%a7%e8%83%bd%e5%89%96%e6%9e%90/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Fri, 17 Apr 2026 09:20:32 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>Go应用性能剖析</title>
		<link>https://blog.gmem.cc/go-program-profiling</link>
		<comments>https://blog.gmem.cc/go-program-profiling#comments</comments>
		<pubDate>Tue, 24 Sep 2019 08:56:35 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>
		<category><![CDATA[性能剖析]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=29033</guid>
		<description><![CDATA[<p>简介 Go SDK自带了Profiling库，可以用来识别程序的缺陷、性能瓶颈。内置以下剖析能力： CPU剖析（profile）：报告CPU的使用情况，定位到热点（消耗CPU周期最多的）代码。默认情况下Go以100HZ的频率进行CPU采样 内存剖析（heap）：报告堆内存当前分配（存活对象）情况。默认情况下每分配512KB进行内存采样。你可以使用URL参数gc，提示报告前进行GC 内存剖析（allocs）：报告所有内存分配历史 线程创建（threadcreate）：报告导致新OS线程创建的代码片段，分析阻塞式系统调用 协程剖析（goroutine）：报告所有Goroutine的调用栈 阻塞剖析（block）：报告Goroutine在哪些同步原语（包括定时器通道）上阻塞，显示调用栈。必须显式调用[crayon-69e29c56602a8512025772-i/]来启用此特性。默认每发生一次阻塞均采样 互斥量剖析（mutex）：报告锁竞争情况，显示持有互斥量的代码的调用栈。当你认为CPU英文锁竞争而没有被完全使用时，显式调用[crayon-69e29c56602ae309236445-i/] 来启用此特性 执行追踪（trace）：追踪当前应用程序的执行栈 此外，Go还允许定制自己的剖析，在代码中手工报告性能分析数据。 数据采集 要采集一个Go应用的剖析数据，有两种方式： 利用runtime/pprof包，进行剖析数据采集，并且在应用退出时将剖析数据写入到文件 利用net/http/pprof包，进行剖析数据采集，支持连接到HTTP服务实时分析 不管使用那种方式，都需要增加一些代码。 采集CPU 进行CPU剖析，添加如下代码： [crayon-69e29c56602b1364730288/] 采集内存 进行内存剖析，添加如下代码： <a class="read-more" href="https://blog.gmem.cc/go-program-profiling">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/go-program-profiling">Go应用性能剖析</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Go SDK自带了Profiling库，可以用来识别程序的缺陷、性能瓶颈。内置以下剖析能力：</p>
<ol>
<li>CPU剖析（profile）：报告CPU的使用情况，定位到热点（消耗CPU周期最多的）代码。默认情况下Go以100HZ的频率进行CPU采样</li>
<li>内存剖析（heap）：报告堆内存当前分配（存活对象）情况。默认情况下每分配512KB进行内存采样。你可以使用URL参数gc，提示报告前进行GC</li>
<li>内存剖析（allocs）：报告所有内存分配历史</li>
<li>线程创建（threadcreate）：报告导致新OS线程创建的代码片段，分析阻塞式系统调用</li>
<li>协程剖析（goroutine）：报告所有Goroutine的调用栈</li>
<li>阻塞剖析（block）：报告Goroutine在哪些同步原语（包括定时器通道）上阻塞，显示调用栈。必须显式调用<pre class="crayon-plain-tag">runtime.SetBlockProfileRate</pre>来启用此特性。默认每发生一次阻塞均采样</li>
<li>互斥量剖析（mutex）：报告锁竞争情况，显示持有互斥量的代码的调用栈。当你认为CPU英文锁竞争而没有被完全使用时，显式调用<pre class="crayon-plain-tag">runtime.SetMutexProfileFraction</pre> 来启用此特性</li>
<li>执行追踪（trace）：追踪当前应用程序的执行栈</li>
</ol>
<p>此外，Go还允许定制自己的剖析，在代码中手工报告性能分析数据。</p>
<div class="blog_h1"><span class="graybg">数据采集</span></div>
<p>要采集一个Go应用的剖析数据，有两种方式：</p>
<ol>
<li>利用runtime/pprof包，进行剖析数据采集，并且在应用退出时将剖析数据写入到文件</li>
<li>利用net/http/pprof包，进行剖析数据采集，支持连接到HTTP服务实时分析</li>
</ol>
<p>不管使用那种方式，都需要增加一些代码。</p>
<div class="blog_h2"><span class="graybg">采集CPU</span></div>
<p>进行CPU剖析，添加如下代码：</p>
<pre class="crayon-plain-tag">f, err := os.Create(*cpuprofile)
if err != nil {
    log.Fatal("could not create CPU profile: ", err)
}
defer f.Close()
if err := pprof.StartCPUProfile(f); err != nil {
    log.Fatal("could not start CPU profile: ", err)
}
// 停止采样，并将剖析概要信息记录到文件
// 此方法实际上会将采样率设置为0
defer pprof.StopCPUProfile()</pre>
<div class="blog_h2"><span class="graybg">采集内存</span></div>
<p>进行内存剖析，添加如下代码：</p>
<pre class="crayon-plain-tag">// 设置采样率，默认每分配512*1024字节采样一次。如果设置为0则禁止采样，只能设置一次
runtime.MemProfileRate = *memProfileRate


f, err := os.Create(*memprofile)
if err != nil {
    log.Fatal("could not create memory profile: ", err)
}
defer f.Close()
runtime.GC() // 执行GC，避免垃圾对象干扰
// 将剖析概要信息记录到文件
if err := pprof.WriteHeapProfile(f); err != nil {
    log.Fatal("could not write memory profile: ", err)
}</pre>
<div class="blog_h2"><span class="graybg">采集阻塞</span></div>
<p>调用下面的方法启用此功能：</p>
<pre class="crayon-plain-tag">runtime.SetBlockProfileRate(5)</pre>
<p>参数5表示，每发生5次Goroutine阻塞事件则采样一次。默认值1。 </p>
<p>下面的代码演示了如何将阻塞剖析概要信息记录到文件：</p>
<pre class="crayon-plain-tag">func stopBlockProfile() {
    if *blockProfile != "" &amp;&amp; *blockProfileRate &gt;= 0 {
        f, err := os.Create(*blockProfile)
        if err = pprof.Lookup("block").WriteTo(f, 0); err != nil {
            fmt.Fprintf(os.Stderr, "Can not write %s: %s", *blockProfile, err)
        }
        f.Close()
    }
} </pre>
<div class="blog_h2"><span class="graybg">采集互斥锁</span></div>
<p>从Go 1.8开始，支持采集处于竞态条件的互斥锁，调用下面的方法启用此功能：</p>
<pre class="crayon-plain-tag">runtime.SetMutexProfileFraction(5)</pre>
<p>此调用允许你捕获<span style="background-color: #c0c0c0;">处于竞态条件的Goroutine的调用栈的一部分</span>。</p>
<p>在进行测试时，不需要上述显式的调用，使用命令行参数即可：</p>
<pre class="crayon-plain-tag">go test -mutexprofile=mutex.out </pre>
<div class="blog_h2"><span class="graybg">通过HTTP暴露</span></div>
<p>包net/http/pprof能够将实时剖析数据通过HTTP暴露为pprof可视化工具能识别的格式。</p>
<p>要使用此包，需要导入：</p>
<pre class="crayon-plain-tag">import _ "net/http/pprof"</pre>
<p>你需要为pprof提供一个HTTP服务器：</p>
<pre class="crayon-plain-tag">go func() {
	log.Println(http.ListenAndServe("localhost:6060", nil))
}()</pre>
<p>如果你不使用<pre class="crayon-plain-tag">http.DefaultServeMux</pre>（如上代码），则需要手工注册路由规则：</p>
<pre class="crayon-plain-tag">r.HandleFunc("/debug/pprof/", pprof.Index)
r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
r.HandleFunc("/debug/pprof/profile", pprof.Profile)
r.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
r.HandleFunc("/debug/pprof/trace", pprof.Trace) </pre>
<div class="blog_h1"><span class="graybg">pprof</span></div>
<div class="blog_h2"><span class="graybg">读取剖析数据</span></div>
<div class="blog_h3"><span class="graybg">通过HTTP</span></div>
<p>要连接到HTTP服务进行实时分析，使用如下命令：</p>
<pre class="crayon-plain-tag"># 设置剖析摘要信息存放目录
export PPROF_TMPDIR=/tmp/pprof

# 获取堆快照
go tool pprof http://localhost:6060/debug/pprof/heap
# 获取从启动依赖的内存分配历史
go tool pprof http://localhost:6060/debug/pprof/allocs

# 30秒CPU分析，需要等待30秒才能看到命令提示符
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# Goroutine阻塞分析
go tool pprof http://localhost:6060/debug/pprof/block

# 收集5秒的执行调用栈
wget http://localhost:6060/debug/pprof/trace?seconds=5

# 互斥锁分析
go tool pprof http://localhost:6060/debug/pprof/mutex</pre>
<p>要查看所有可用的剖析， 访问 http://localhost:6060/debug/pprof/ 。</p>
<div class="blog_h3"><span class="graybg">通过文件</span></div>
<pre class="crayon-plain-tag">#             可执行文件路径
#                      保存的剖析摘要文件
go tool pprof bin/Temp mutex.mprof</pre>
<div class="blog_h2"><span class="graybg">交互式命令</span></div>
<p>通过工具go tool pprof打开URL或文件后，会显示一个<pre class="crayon-plain-tag">(pprof)</pre>提示符，你可以使用以下命令：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 80px; text-align: center;">命令</td>
<td style="width: 20%; text-align: center;">参数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">gv</td>
<td>[focus]</td>
<td>
<p>将当前概要文件以图形化和层次化的形式显示出来。当没有任何参数时，在概要文件中的所有采样都会被显示</p>
<p>如果指定了focus参数，则只显示<span style="background-color: #c0c0c0;">调用栈中有名称与此参数相匹配的函数或方法的采样</span>。<pre class="crayon-plain-tag">focus</pre>参数应该是一个正则表达式</p>
<p>需要dot、gv命令，执行下面的命令安装：</p>
<p><pre class="crayon-plain-tag">sudo apt-get install graphviz
sudo apt-get install gv</pre>
</td>
</tr>
<tr>
<td class="blog_h3"><strong>web</strong></td>
<td>[focus]</td>
<td>与gv命令类似，web命令也会用图形化的方式来显示概要文件。但不同的是，web命令是在一个Web浏览器中显示它</td>
</tr>
<tr>
<td class="blog_h3"><strong>list</strong></td>
<td>[routine_regexp]</td>
<td>列出名称与参数<pre class="crayon-plain-tag">routine_regexp</pre>代表的正则表达式相匹配的函数或方法的相关源代码</td>
</tr>
<tr>
<td class="blog_h3">weblist</td>
<td>[routine_regexp]</td>
<td> 在Web浏览器中显示与list命令的输出相同的内容。它与list命令相比的优势是，在我们点击某行源码时还可以显示相应的汇编代码 </td>
</tr>
<tr>
<td class="blog_h3"><strong>top[N]</strong></td>
<td>[--cum]</td>
<td>
<p>top命令可以以本地采样计数为顺序列出函数或方法及相关信息</p>
<p>如果存在标记<pre class="crayon-plain-tag">--cum</pre>则以累积采样计数为顺序</p>
<p>默认情况下top命令会列出前10项内容。但是如果在top命令后面紧跟一个数字，那么其列出的项数就会与这个数字相同</p>
</td>
</tr>
<tr>
<td>traces</td>
<td> </td>
<td>打印所有采集的样本</td>
</tr>
<tr>
<td class="blog_h3">disasm</td>
<td>[routine_regexp]</td>
<td>显示名称与参数<pre class="crayon-plain-tag">routine_regexp</pre>相匹配的函数或方法的反汇编代码。并且，在显示的内容中还会标注有相应的采样计数</td>
</tr>
<tr>
<td class="blog_h3">callgrind</td>
<td>[filename]</td>
<td>利用callgrind工具生成统计文件。在这个文件中，说明了程序中函数的调用情况。如果未指定<pre class="crayon-plain-tag">filename</pre>参数，则直接调用kcachegrind工具。kcachegrind可以以可视化的方式查看callgrind工具生成的统计文件</td>
</tr>
<tr>
<td class="blog_h3">help</td>
<td> </td>
<td>显示帮助</td>
</tr>
<tr>
<td class="blog_h3">quit</td>
<td> </td>
<td>退出 </td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Web UI</span></div>
<p>调用pprof时，可以选择启动一个Web UI，指定-http选项即可：</p>
<pre class="crayon-plain-tag">go tool pprof -http=:8080 http://localhost:6060/debug/pprof/heap</pre>
<p> 你可以访问Web UI来查看烈焰图等高级图表。</p>
<div class="blog_h1"><span class="graybg">数据分析</span></div>
<div class="blog_h2"><span class="graybg">分析CPU</span></div>
<div class="blog_h3"><span class="graybg">测试代码</span></div>
<p>这里使用一段CPU密集型代码来学习CPU剖析：</p>
<pre class="crayon-plain-tag">package main

import (
	"net/http"
	_ "net/http/pprof"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	go func() {
		http.ListenAndServe("localhost:6060", nil)
	}()

	wg.Add(1)
	go calculte(wg)
	wg.Wait()
}

func calculte(wg sync.WaitGroup) {
	for i := 0; i &lt; 100000000; i++ {
		time.Sleep(time.Millisecond)
		cpuBound(i)
	}
	wg.Done()
}

func cpuBound(i int) {
	factorial(i)
	sieveOfEratosthenes(i)
}
func sieveOfEratosthenes(N int) (primes []int) {
	b := make([]bool, N)
	for i := 2; i &lt; N; i++ {
		if b[i] == true {
			continue
		}
		primes = append(primes, i)
		for k := i * i; k &lt; N; k += i {
			b[k] = true
		}
	}
	return
}

func factorial(x int) int {
	if x == 0 {
		return 1
	}

	return x * factorial(x-1)
}</pre>
<div class="blog_h3"><span class="graybg">top</span></div>
<pre class="crayon-plain-tag">(pprof) top10
Showing nodes accounting for 24630ms, 83.75% of 29410ms total
Dropped 107 nodes (cum &lt;= 147.05ms)
Showing top 10 nodes out of 31
      flat  flat%   sum%        cum   cum%
    4720ms 16.05% 16.05%     4990ms 16.97%  main.sieveOfEratosthenes
    4510ms 15.33% 31.38%    19440ms 66.10%  runtime.gentraceback
    3550ms 12.07% 43.45%    23490ms 79.87%  main.factorial
    2810ms  9.55% 53.01%     8460ms 28.77%  runtime.getStackMap
    2130ms  7.24% 60.25%     3220ms 10.95%  runtime.funcdata
    2070ms  7.04% 67.29%     2630ms  8.94%  runtime.findfunc
    1540ms  5.24% 72.53%     1740ms  5.92%  runtime.pcvalue
    1400ms  4.76% 77.29%     1400ms  4.76%  runtime.add
    1000ms  3.40% 80.69%     9610ms 32.68%  runtime.adjustframe
     900ms  3.06% 83.75%     1070ms  3.64%  runtime.pcdatastart
...
         0     0% 97.18%      5.01s 17.04%  main.calculte
         0     0% 97.18%      4.99s 16.97%  main.cpuBound</pre>
<p>使用top命令可以直接看到消耗CPU最多的方法，各列含义如下：</p>
<ol>
<li>flat：在采样期间，<span style="background-color: #c0c0c0;">此函数正在执行的次数 * 10ms</span>。 可以用来粗略估计函数的运行耗时，不包含当前函数调用其它函数并等待返回的时间</li>
<li>flat%：flat / 总采样时间。估算函数运行耗CPU占比</li>
<li>sum%：当前行加上前面所有行的flat%总和</li>
<li>cum：在采样期间，此函数出现在调用栈的次数*10ms。和flat相比，该指标包含子函数耗时</li>
<li>cum%：cum/总采样时间</li>
</ol>
<p>要以cum降序输出，执行<pre class="crayon-plain-tag">top10 -cum</pre>。</p>
<p>上面的例子中，factorial是自递归调用，其cum值不知道为何比父例程cpuBound还要大得多，不符合只觉。</p>
<div class="blog_h3"><span class="graybg">list</span></div>
<p>通过top定位到耗时函数后，可以进一步使用该命令，分析函数每一行代码消耗多少时间。</p>
<p>函数cpuBound调用两个子例程factorial、sieveOfEratosthenes，它们都是非常耗时的：</p>
<pre class="crayon-plain-tag">(pprof) list cpuBound
Total: 29.41s
ROUTINE ======================== main.cpuBound in /home/alex/Go/workspaces/default/src/git.gmem.cc/alex/go-study/golang/profile.go
         0      4.99s (flat, cum) 16.97% of Total
         .          .     27:	wg.Done()
         .          .     28:}
         .          .     29:
         .          .     30:func cpuBound(i int) {
         .          .     31:	factorial(i)
         .      4.99s     32:	sieveOfEratosthenes(i)
         .          .     33:}
         .          .     34:func sieveOfEratosthenes(N int) (primes []int) {
         .          .     35:	b := make([]bool, N)
         .          .     36:	for i := 2; i &lt; N; i++ {
         .          .     37:		if b[i] == true {</pre>
<p>从上面的输出可以看到， cpuBound的全部时间均消耗在对sieveOfEratosthenes的调用上，而factorial这个自递归调用的耗时无法体现。</p>
<div class="blog_h3"><span class="graybg">web </span></div>
<p>使用此命令可以生成一个SVG图片，清晰的显示调用关系图。图中越红的节点消耗CPU越多： </p>
<p><img class="aligncenter" style="width: 611px;" src="https://cdn.gmem.cc/wp-content/uploads/2019/09/pprof001.svg" alt="" height="879" /></p>
<p>从图中可以看到factorial函数引发的调用链最耗时，calculte其次。由于factorial是自递归调用，calculte到factorial的调用关系没有识别出来。</p>
<div class="blog_h2"><span class="graybg">分析内存</span></div>
<div class="blog_h3"><span class="graybg">测试代码</span></div>
<p>这里使用一段内存消耗型代码来学习CPU剖析：</p>
<pre class="crayon-plain-tag">package main

import (
	"net/http"
	_ "net/http/pprof"
	"time"
)

type pkg struct {
	blob
}
type blob struct {
	data [1024]int
}

func main() {
	go func() {
		http.ListenAndServe("localhost:6060", nil)
	}()
	consume()
}

func consume() {
	for i := 0; i &lt; 100000000; i++ {
		time.Sleep(time.Millisecond * 1)
		b100 := createBlob(100)
		println(b100)
		p100 := createPkg(100)
		println(p100)
	}
}

func createPkg(i int) interface{} {
	ps := make([]pkg, 0)
	for x := 0; x &lt; i; x++ {
		ps = append(ps, pkg{})
	}
	return ps
}

func createBlob(i int) []blob {
	bs := make([]blob, 0)
	for x := 0; x &lt; i; x++ {
		bs = append(bs, blob{})
	}
	return bs
}</pre>
<div class="blog_h3"><span class="graybg">heap</span></div>
<p>执行下面的命令，可以获取一个堆快照，快照中包含所有存活对象：</p>
<pre class="crayon-plain-tag">go tool pprof http://localhost:6060/debug/pprof/heap</pre>
<p>使用top命令可以看到哪些方法分配了最多的内存：</p>
<pre class="crayon-plain-tag">(pprof) top
Showing nodes accounting for 1.95MB, 100% of 1.95MB total
      flat  flat%   sum%        cum   cum%
    1.95MB   100%   100%     1.95MB   100%  main.createPkg
         0     0%   100%     1.95MB   100%  main.consume
         0     0%   100%     1.95MB   100%  main.main
         0     0%   100%     1.95MB   100%  runtime.main</pre>
<p>可以看到，在本次快照中，createPkg分配的内存最多。</p>
<p>使用list命令，可以进一步定位到createPkg的哪一行代码分配了这些内存：</p>
<pre class="crayon-plain-tag">(pprof) list createPkg
Total: 1.95MB
ROUTINE ======================== main.createPkg in /home/alex/Go/workspaces/default/src/git.gmem.cc/alex/go-study/golang/heap.go
    1.95MB     1.95MB (flat, cum)   100% of Total
         .          .     31:}
         .          .     32:
         .          .     33:func createPkg(i int) interface{} {
         .          .     34:   ps := make([]pkg, 0)
         .          .     35:   for x := 0; x &lt; i; x++ {
    1.95MB     1.95MB     36:           ps = append(ps, pkg{})
         .          .     37:   }
         .          .     38:   return ps
         .          .     39:}
         .          .     40:
         .          .     41:func createBlob(i int) []blob {</pre>
<p>可以看到，全部内存均由于代码<pre class="crayon-plain-tag">ps = append(ps, pkg{})</pre>分配。 </p>
<p>类似的，使用web命令可以展示出内存分配的调用栈。</p>
<div class="blog_h3"><span class="graybg">allocs</span></div>
<p>执行下面的命令，可以获取程序运行依赖，所有内存分配的历史：</p>
<pre class="crayon-plain-tag">go tool pprof http://localhost:6060/debug/pprof/allocs</pre>
<p>用top命令看，分配内存的量明显比heap剖析大的多：</p>
<pre class="crayon-plain-tag">(pprof) top
Showing nodes accounting for 5.69TB, 100% of 5.69TB total
Dropped 33 nodes (cum &lt;= 0.03TB)
      flat  flat%   sum%        cum   cum%
    2.85TB 50.00% 50.00%     2.85TB 50.00%  main.createPkg
    2.85TB 50.00%   100%     2.85TB 50.00%  main.createBlob
         0     0%   100%     5.69TB   100%  main.consume
         0     0%   100%     5.69TB   100%  main.main
         0     0%   100%     5.69TB   100%  runtime.main</pre>
<p>使用web命令，可以展示出内存分配的调用栈：</p>
<p><img class="aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2019/09/pprof002.svg" alt="" width="694" height="266" /></p>
<div class="blog_h3"><span class="graybg">内存泄漏</span></div>
<p>所谓内存泄漏，意味着占用的内存一直无法释放。在pprof中，泄漏的内存一直存在于heap剖析的输出中，并且随着运行时间的增加，迟早会出现在top命令的输出中。</p>
<div class="blog_h2"><span class="graybg">分析阻塞</span></div>
<div class="blog_h3"><span class="graybg">测试代码</span></div>
<p>这里使用一段内存消耗型代码来学习阻塞剖析：</p>
<pre class="crayon-plain-tag">package main

import (
	"net/http"
	_ "net/http/pprof"
	"runtime"
	"sync"
	"time"
)

var mutex sync.Mutex

func main() {
	// rate = 1：统计所有的 block event,
	// rate &lt;=0：关闭block profiling
	// rate &gt; 1：阻塞时间t&gt;rate那秒的event 一定会被统计，小于rate则有t/rate 的几率被统计
	runtime.SetBlockProfileRate(1 * 1000 * 1000)
	go func() {
		http.ListenAndServe("localhost:6060", nil)
	}()
	var wg sync.WaitGroup
	for ; ; {
		wg.Add(1)
		mutex.Lock()
		go worker(&amp;wg)
		time.Sleep(2 * time.Millisecond)
		mutex.Unlock()
		wg.Wait()
	}
}
func worker(wg *sync.WaitGroup) {
	defer wg.Done()
	mutex.Lock()
	time.Sleep(1 * time.Millisecond)
	mutex.Unlock()
}</pre>
<div class="blog_h3"><span class="graybg">top</span></div>
<p>执行下面的命令，可以获取所有录制的阻塞事件：</p>
<pre class="crayon-plain-tag">go tool pprof http://localhost:6060/debug/pprof/block</pre>
<p>top显示的是阻塞时间最长的方法：</p>
<pre class="crayon-plain-tag">(pprof) top
Showing nodes accounting for 2.99mins, 100% of 2.99mins total
Dropped 9 nodes (cum &lt;= 0.01mins)
      flat  flat%   sum%        cum   cum%
  1.95mins 65.37% 65.37%   1.95mins 65.37%  sync.(*Mutex).Lock
  1.03mins 34.63%   100%   1.03mins 34.63%  sync.(*WaitGroup).Wait
         0     0%   100%   1.03mins 34.63%  main.main
         0     0%   100%   1.95mins 65.37%  main.worker
         0     0%   100%   1.03mins 34.63%  runtime.main
(pprof)</pre>
<p>这里可以看到阻塞时间都消耗在互斥量的Lock和等待组的Wait方法上。 </p>
<div class="blog_h3"><span class="graybg">web </span></div>
<p>使用top无法感知什么代码导致了阻塞，你可以使用web，展示导致阻塞的调用栈。 </p>
<div class="blog_h2"><span class="graybg">分析互斥锁</span></div>
<div class="blog_h3"><span class="graybg">测试代码</span></div>
<pre class="crayon-plain-tag">package main

import (
	"math/rand"
	"net/http"
	_ "net/http/pprof"
	"runtime"
	"sync"
	"time"
)

var mutex sync.Mutex

func main() {
	// rate = 0：关闭 mutex prof
	// rate = 1：记录所有的 mutex event
	// rate &gt; 1：随机记录 1/rate 的 mutex event
	runtime.SetMutexProfileFraction(1)
	go func() {
		http.ListenAndServe("localhost:6060", nil)
	}()

	go worker()
	go worker()
	var wg sync.WaitGroup
	wg.Add(1)
	wg.Wait()
}
func worker() {
	for ; ; {
		mutex.Lock()
		time.Sleep(time.Duration(rand.New(rand.NewSource(time.Now().Unix())).Intn(10)) * time.Second)
		mutex.Unlock()
	}
}</pre>
<div class="blog_h3"><span class="graybg">top</span></div>
<p>执行下面的命令，可以获取所有录制的互斥锁事件：</p>
<pre class="crayon-plain-tag">go tool pprof http://localhost:6060/debug/pprof/mutex</pre>
<p>使用top命令，可以看到锁竞争的位置。</p>
<div class="blog_h2"><span class="graybg">分析Goroutine</span></div>
<p>可以执行：</p>
<pre class="crayon-plain-tag">curl -s  http://localhost:6060/debug/pprof/goroutine?debug=2</pre>
<p>来获取所有Goroutine的状态、调用栈。</p>
<p>如果Goroutine因为读写通道而阻塞，可以看到类似下面的输出：</p>
<pre class="crayon-plain-tag">goroutine 1 [chan receive, 7 minutes]:
main.main()
	/home/alex/Go/workspaces/default/src/git.gmem.cc/alex/go-study/golang/goroutine.go:21 +0x71</pre>
<p>在这个例子中，主协程已经等待达7分钟。这种在通道上的等待是无法通过阻塞分析看到的。</p>
<p>如果Goroutine正在等待（包括网络）IO完成，可以看到类似下面的输出：</p>
<pre class="crayon-plain-tag">goroutine 436 [IO wait]:
internal/poll.runtime_pollWait(0x7ff9056e2dd8, 0x72, 0xb)</pre>
<div class="blog_h1"><span class="graybg">定制剖析</span></div>
<p>Go允许开发人员扩展自己的Profile，来跟踪任何资源的创建/释放。</p>
<p>假设你负责编写某个Blob服务器的客户端库，用户的需求是随时了解某个客户端实例打开了多少Blob。 你可以使用定制剖析满足此需求：</p>
<pre class="crayon-plain-tag">package blobstore

import "runtime/pprof"

// 定制剖析
var openBlobProfile = pprof.NewProfile("blobstore.Open")

// 打开一个Blob，所有Blob不再使用之后需要关闭
func Open(name string) (*Blob, error) {
	blob := &amp;Blob{name: name}

	// ... 在这里加载并初始化Blob


	// 此方法将当前调用栈加入到剖析中，并且将此栈关联到对象blob
	// 信息存放在内部的一个map中，以blob为key，这意味着：
	//   1.blob必须适合用作key，而且它
	//   2.在显示调用Remove之前不会被GC
	// 如果剖析已经包含blob的调用栈，则panic

	// 2 表示跳过的栈帧数量，对于调用栈
	//   Add
	//   called from rpc.NewClient
	//   called from mypkg.Run
	//   called from main.main
	//  skip=0 从rpc.NewClient中的Add调用处开始记录
	//  skip=1 从mypkg.Run的NewClient调用处开始记录 
	openBlobProfile.Add(blob, 2)
	return blob, nil
}

// 关闭Blob并释放底层资源
func (b *Blob) Close() error {
	// 从Profile中移除对象b关联的调用栈
	openBlobProfile.Remove(b)
	return nil
}</pre>
<p>如果此客户端库的使用者，想知道自己的程序当前打开了多少Blob，在什么地方（代码位置）打开的，可以这样编写：</p>
<pre class="crayon-plain-tag">package main

import (
	"fmt"
	"math/rand"
	"net/http"
	_ "net/http/pprof"
	"time"

	"myproject.org/blobstore"
)

func main() {
	for i := 0; i &lt; 1000; i++ {
		name := fmt.Sprintf("task-blob-%d", i)
		go func() {
			// 打开Blob，会导致剖析记录数据
			b, err := blobstore.Open(name)
			if err != nil {
			}
			defer b.Close()
		}()
	}
	http.ListenAndServe("localhost:6060", nil)
}</pre>
<p>程序运行期间，使用如下命令即可看到打开了哪些Blob：</p>
<pre class="crayon-plain-tag">go tool pprof http://localhost:6060/debug/pprof/blobstore.Open

(pprof) top
Showing nodes accounting for 800, 100% of 800 total
      flat  flat%   sum%        cum   cum%
       800   100%   100%        800   100%  main.main.func1 /Users/jbd/src/hello/main.go</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/go-program-profiling">Go应用性能剖析</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/go-program-profiling/feed</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>使用Sysdig进行系统性能分析</title>
		<link>https://blog.gmem.cc/sysdig</link>
		<comments>https://blog.gmem.cc/sysdig#comments</comments>
		<pubDate>Sat, 19 Jan 2019 09:47:58 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[性能剖析]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=36135</guid>
		<description><![CDATA[<p>sysdig 简介 Sysdig是一个开源的系统性能分析工具，可以实现strace、tcpdump、lsof、top、iftop等工具所具有的功能。 如果需要源代码级别的、通用剖析工具，可以参考：利用perf剖析Linux应用程序。 安装 [crayon-69e29c5660f14841772692/] 选项 -c 运行指定的chisel，如果chisel需要参数，则必须用--chisel=chiselname chiselargs形式-cl 列出可用的chisel，从./chisels, ~/.chisels,/usr/share/sysdig/chisels搜索-M 在指定秒数之后停止收集-n 在获取指定数量的事件之后停止收集-S 在捕获结束，例如列出top事件之后，打印汇总摘要-s 捕获IO缓冲的前N字节，默认80 基本用法 不加任何参数调用，每行显示一个捕获的事件，不断刷新。输出格式： [crayon-69e29c5660f18925670975/] 可以将结果输出到文件而非控制台：[crayon-69e29c5660f1b170335643-i/]，随后你可以读取先前保存的文件：[crayon-69e29c5660f1d126850104-i/] 过滤器 可以使用过滤器对sysdig输出进行过滤，过滤器支持 <a class="read-more" href="https://blog.gmem.cc/sysdig">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/sysdig">使用Sysdig进行系统性能分析</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">sysdig</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>Sysdig是一个开源的系统性能分析工具，可以实现strace、tcpdump、lsof、top、iftop等工具所具有的功能。</p>
<p>如果需要源代码级别的、通用剖析工具，可以参考：<a href="/perf">利用perf剖析Linux应用程序</a>。</p>
<div class="blog_h2"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag"># CentOS 7
rpm --import https://s3.amazonaws.com/download.draios.com/DRAIOS-GPG-KEY.public
curl -s -o /etc/yum.repos.d/draios.repo http://download.draios.com/stable/rpm/draios.repo
# yum update
yum -y install epel-release

yum install kernel-devel-$(uname -r)
yum install sysdig
/usr/lib/dkms/dkms_autoinstaller start
sysdig-probe-loader</pre>
<div class="blog_h2"><span class="graybg">选项</span></div>
<p style="padding-left: 30px;">-c 运行指定的chisel，如果chisel需要参数，则必须用--chisel=chiselname chiselargs形式<br />-cl 列出可用的chisel，从./chisels, ~/.chisels,/usr/share/sysdig/chisels搜索<br />-M 在指定秒数之后停止收集<br />-n 在获取指定数量的事件之后停止收集<br />-S 在捕获结束，例如列出top事件之后，打印汇总摘要<br />-s 捕获IO缓冲的前N字节，默认80</p>
<div class="blog_h2"><span class="graybg">基本用法</span></div>
<p>不加任何参数调用，每行显示一个捕获的事件，不断刷新。输出格式：</p>
<pre class="crayon-plain-tag"># 事件号 发生事件          CPU号   进程名(PID） 事件方向
#                                             事件类型 事件参数
1462218 16:40:38.068514659 3 &lt;NA&gt; (2941270) &gt; switch next=2941697(sysdig) pgft_maj=0 pgft_min=0 vm_size=0 vm_rss=0 vm_swap=0                                                             
1462220 16:40:38.068515560 6 java (2556857) &gt; futex addr=7F3B3888C598 op=129(FUTEX_PRIVATE_FLAG|FUTEX_WAKE) val=1                                                                        
1462222 16:40:38.068515997 5 java (2557004) &gt; futex addr=7F3AEC01C894 op=137(FUTEX_PRIVATE_FLAG|FUTEX_WAIT_BITSET) val=1                                                                 
1462224 16:40:38.068516460 6 java (2556857) &lt; futex res=0</pre>
<p>可以将结果输出到文件而非控制台：<pre class="crayon-plain-tag">sysdig -w result.dump</pre>，随后你可以读取先前保存的文件：<pre class="crayon-plain-tag">sysdig -r result.dump</pre></p>
<div class="blog_h2"><span class="graybg">过滤器</span></div>
<p>可以使用过滤器对sysdig输出进行过滤，过滤器支持 =、!=、contains等操作符</p>
<p>下面的命令列出可用的过滤器：</p>
<pre class="crayon-plain-tag">sysdig -l

----------------------
Field Class: fd

fd.num          # 文件描述符号
fd.type         # 文件描述符类型，支持的值：'file', 'directory', 'ipv4', 'ipv6', 'unix',
                # 'pipe', 'event', 'signalfd', 'eventpoll', 'inotify', 'signal fd'
fd.typechar     # 文件描述符类型简写，支持的值：f                 4       6       u
                #  p      e         s           l            i
                # o表示未知
fd.name         # 文件描述符全名，对于文件，即完整路径；对于套接字，即连接元组
fd.directory    # 文件描述符是文件，且它必须位于此目录下
fd.filename     # 文件描述符是文件，且它的basename是此值
fd.ip           # 匹配客户端或服务器IP地址
fd.cip          # 匹配客户端IP地址
fd.sip          # 匹配服务器IP地址
fd.lip          # 匹配本地地址
fd.rip          # 匹配远程地址
fd.port         # 匹配客户端或服务器端口
fd.cport        # 对于TCP/UDP，客户端端口
fd.sport        # 对于TCP/UDP，客户端端口
fd.lport        # 对于TCP/UDP，本地端口
fd.rport        # 对于TCP/UDP，远程端口
fd.l4proto      # 套接字协议，可选 'tcp', 'udp', 'icmp' or 'raw'
fd.sockfamily   # 套接字家族，可选 'ip' or 'unix'
fd.is_server    # 指定为true则用于套接字的进程必须是服务器端
fd.uid          # FD的唯一标识
fd.containername
                # container ID + FD name
fd.containerdirectory
                # container ID + directory name
fd.proto        # FD的协议
fd.cproto       # 对于TCP/UDP，客户端协议
fd.sproto       # 对于TCP/UDP，服务器协议
fd.lproto       # 对于TCP/UDP，本地协议
fd.rproto       # 对于TCP/UDP，远程协议
fd.net          # IP网络
fd.cnet         
fd.snet         
fd.lnet         
fd.rnet         
fd.connected    # 对于TCP/UDP，设置为true则仅仅匹配已连接的套接字

----------------------
Field Class: process

proc.pid        # 生成事件的进程的ID
proc.exe        # 第一个命令行参数，通常是可执行文件名
proc.name       # 生成事件的可执行文件的名称
proc.args       # 生成事件的进程的命令行参数
proc.env        # 环境变量 
proc.cmdline    # 完整命令行，也就是 proc.name + proc.args
proc.exeline    # 完整命令行，也就是 proc.exe + proc.args
proc.cwd        # 当前工作目录
proc.nthreads   # 进程包括的线程数量
proc.nchilds    # 进程包括的子线程数量（除去主线程）
proc.ppid       # 父进程ID
proc.pname      # 父进程名称
proc.pcmdline   # 父进程的cmdline
proc.apid       # 某个祖先进程的ID
proc.aname      # 某个祖先进程的名称
proc.loginshellid
                # 登陆Shell ID
proc.duration   # 进程已经启动的纳秒
proc.fdopencount
                # 进程打开的文件描述符数量
proc.fdlimit    # 进程能够打开的文件描述符数量
proc.fdusage    # 打开/最大文件描述符比率
proc.vmsize     # 进程虚拟内存大小，单位KB
proc.vmrss      # 驻留内存大小
proc.vmswap     # 交换到磁盘的内存大小
thread.pfmajor  # 进程启动以来主页面错误次数
thread.pfminor  # 进程启动以来次页面错误次数
thread.tid      # 线程ID
thread.ismain   # 如果指定为true，生成事件的线程必须是主线程
thread.exectime # 最后被调度的线程消耗的CPU纳秒数
thread.totexectime
                # 从捕获开始，当前线程使用的CPU纳秒总数
thread.cgroups  # 线程所属的Cgroup，聚合为单个字符串
thread.cgroup   # 线程所属的某个Cgroup子系统
thread.vtid     # 从产生事件的线程自己的PID命名空间看到的，自己的PID
proc.vpid       # 从产生事件的进程自己的PID命名空间看到的，自己的PID
thread.cpu      # 最近一秒消耗的CPU时间
thread.cpu.user # 最近一秒消耗的CPU时间（用户态）
thread.cpu.system
                # 最近一秒消耗的CPU时间（内核态）
thread.vmsize   # 对于主线程，即虚拟内存大小，对于非主线程，为0
proc.sid        # 会话ID
proc.sname      # 会话名称
proc.tty        # 进程的控制终端，0为无控制终端的进程
proc.exepath    # 可执行文件的完整路径
proc.vpgid      # 从产生事件的进程自己的PID命名空间看到的，自己的进程组ID

----------------------
Field Class: evt

evt.num         # 事件编号
evt.time        # 事件时间戳，包含纳秒部分
evt.time.s      # 事件时间戳，不包含纳秒
evt.datetime    # 事件时间戳，包含日期部分
evt.rawtime     # 原始时间戳，19700101到现在的纳秒数
evt.rawtime.s   # 原始时间戳，19700101到现在的秒数
evt.rawtime.ns  # 原始时间戳，小数部分（纳秒部分）
evt.reltime     # 从捕获到发生事件时流逝的时间
evt.reltime.s  
evt.reltime.ns  
evt.latency     # 配对的enter/exit事件的延迟，纳秒
evt.latency.s   
evt.latency.ns  
evt.latency.human
                # 可读格式，例如10.3ms
evt.deltatime   # 当前事件和上一个事件之间的延迟
evt.deltatime.s 
evt.deltatime.ns
evt.dir         # 事件方向，&gt;为enter事件，&lt;为exit事件
evt.type        # 事件（类型的）名称，例如open
evt.type.is     # evt.type.is.open匹配所有open事件
syscall.type    # 对于系统调用事件，指定系统调用名称，例如open
evt.category    # 事件分类，例如file表示文件操作包括open/close，net表示网络操作
                # memory表示mmap/brk等操作
evt.cpu         # 事件所在的CPU序号
evt.args        # 聚合为字符串的事件参数
evt.arg         # 单个事件参数，使用参数名或序号，例如evt.arg.fd或'evt.arg[0]
evt.res         # 事件结果字符串，如果成功SUCCESS，否则是错误码字符串，例如ENOENT 
evt.failed      # 如果true匹配失败事件
evt.is_io       # 匹配读写FD的事件，例如 read(), send, recvfrom()
evt.is_io_read  
evt.is_io_write 
evt.io_dir      # IO方向，r或w
evt.is_wait     # 如果true则匹配那些导致线程等待的事件，例如sleep(), select(), poll()
evt.wait_latency
                # 等待事件返回（exit）消耗的时间阈值
evt.is_syslog   # 事件是否是写入syslog
evt.is_open_read
                # 如果为true，匹配打开以进行读的open/openat事件
evt.is_open_write
                # 如果为true，匹配打开以进行写的open/openat事件

----------------------
Field Class: user

user.uid        # 用户ID
user.name       # 用户名称
user.homedir    # 家目录
user.shell      # 用户的Shell
user.loginuid   # 审计用户ID
user.loginname  # 审计用户名

----------------------
Field Class: group

group.gid       # 组ID
group.name      # 组名称

----------------------
Field Class: container

container.id    # 容器ID
container.name  # 容器名称
container.image # 容器镜像名称
container.image.id
                # 容器镜像ID
container.type  # 容器类型，例如docker, rkt
container.privileged
                # true则匹配运行在特权模式的容器
container.image.repository
container.image.tag
container.image.digest

----------------------
Field Class: k8s

k8s.pod.name    # Pod名称
k8s.pod.id      # Pod标识符
k8s.pod.label   # Pod的标签，例如k8s.pod.label.foo
k8s.pod.labels  # 逗号分隔的多个Pod标签
k8s.rc.name     # 复制控制器
k8s.rc.id       
k8s.rc.label    
k8s.rc.labels   
k8s.svc.name    # 服务
k8s.svc.id      
k8s.svc.label   
k8s.svc.labels  
k8s.ns.name     # 命名空间
k8s.ns.id       
k8s.ns.label    
k8s.ns.labels
k8s.rs.name     # 复制集
k8s.rs.id      
k8s.rs.label   
k8s.rs.labels   
                
k8s.deployment.name
                # Deployment
k8s.deployment.id
k8s.deployment.label
k8s.deployment.labels</pre>
<p>过滤特定进程产生的事件： </p>
<pre class="crayon-plain-tag"># 仅仅捕获sshd产生的事件
sysdig proc.name=sshd

# 读取Dump，仅仅显示mysqld产生的事件
sysdig -r result.dump proc.name=mysqld</pre>
<div class="blog_h2"><span class="graybg">Chisels</span></div>
<p>Sysdig中的chisels是一小段Lua脚本，用于分析sysdig事件流。Chisels支持：</p>
<ol>
<li>实时分析：每秒刷新一次</li>
<li>离线分析：会打印总体的汇总信息</li>
</ol>
<p>执行下面的命令列出可用的Chisels：<br /> </p>
<pre class="crayon-plain-tag">sysdig -cl

Category: Application
---------------------
httplog         # HTTP请求日志
httptop         # HTTP请求TOP
memcachelog     # memcache请求日志

Category: CPU Usage
-------------------
spectrogram     # OS延迟的可视化直方图
subsecoffset    # 
topcontainers_cpu
                # CPU占用最高的容器
topprocs_cpu    # CPU占用最高的进程

Category: Errors
----------------
topcontainers_error
                # 错误最多的容器
topfiles_errors # 错误最多的文件
topprocs_errors # 错误最多的进程

Category: I/O
-------------
echo_fds        # 打印进程读写的文件
fdbytes_by      # IO字节数，根据一个任意的过滤器字段进行聚合
fdcount_by      # FD计数，根据一个任意的过滤器字段进行聚合
fdtime_by       # FD时间
iobytes         # 任何类型的FD的IO字节数总和
iobytes_file    # 文件IO字节数总和
spy_file        # 回响任何进程对所有文件进行的任何读写，可选的，你可以提供一个文件名，这样只会拦截
                # 单个文件上发生的读写
stderr          # 打印进程的标准错误
stdin           # 打印进程的标准输入
stdout          # 打印进程的标准输出
topcontainers_file
                # 读写磁盘字节数最多的容器
topfiles_bytes  # 被读写字节数最多的文件
topfiles_time   # 被访问时间最长的文件
topprocs_file   # 读写磁盘字节数最多的进程

Category: Logs
--------------
spy_logs        # 回响任何进程对任何日志文件执行的写入
spy_syslog      # 打印任何写入到syslog的消息

Category: Misc
--------------
around          # 在指定过滤器匹配时，输出指定事件范围附近的事件

Category: Net
-------------
iobytes_net     # 显示总和网络IO字节数
spy_ip          # 显示和指定IP地址的数据交换情况
spy_port        # 显示和指定端口的数据交换情况
topconns        # IO字节数最多的网络链接
topcontainers_net
                # 导致网络IO最多的容器
topports_server # 读写字节数最多的TCP/UDP端口
topprocs_net    # 导致网络IO最多的进程

Category: Performance
---------------------
bottlenecks     # 最缓慢的系统调用
fileslower      # 跟踪缓慢的文件IO
netlower        # 跟踪缓慢的网络IO
proc_exec_time  # 显示进程执行时间
scallslower     # 跟踪缓慢的系统调用
topscalls       # 调用次数最多的系统调用
topscalls_time  # 消耗时间最多的系统调用

Category: Security
------------------
list_login_shells
                # 列出登陆Shell的ID
shellshock_detect
                # 打印 shellshock攻击
spy_users       # 显示交互式的用户活动

Category: System State
----------------------
lscontainers    # 列出运行中的荣iq
lsof            # 列出（可过滤）打开的文件描述符
netstat         # 列出（可过滤）打开的网络链接
ps              # 列出（可过滤）运行中的进程</pre>
<div class="blog_h2"><span class="graybg">示例</span></div>
<div class="blog_h3"><span class="graybg">网络分析</span></div>
<p>显示使用网络带宽最多的进程： </p>
<pre class="crayon-plain-tag">sysdig -c topprocs_net</pre>
<p>查看网络IO最高的进程： </p>
<pre class="crayon-plain-tag">sysdig -c topprocs_net                                                                                                                                         
# Bytes               Process             PID                                                                                                                                              
# --------------------------------------------------------------------------------                                                                                                         
# 38.48KB             msgr-worker-1       198488                                                                                                                                           
# 24.36KB             msgr-worker-0       198488                                                                                                                                           
# 8.12KB              msgr-worker-2       198488                                                                                                                                           
# 76B                 bird                3951084                                                                                                                                          
# 52B                 zabbix_agentd       5930                                                                                                                                             
 #31B                 zabbix_agentd       5931</pre>
<p>打印和目标主机之间的数据交换： </p>
<pre class="crayon-plain-tag"># 以二进制形式打印
sysdig -s2000 -X -c echo_fds fd.cip=10.5.39.41
# 以ASCII形式打印
sysdig -s2000 -A -c echo_fds fd.cip=192.168.0.1</pre>
<p>最繁忙服务器端口，按当前已建立连接数：</p>
<pre class="crayon-plain-tag"># 按服务器端口分组
sysdig -c fdcount_by fd.sport "evt.type=accept"</pre>
<p>最繁忙客户端地址，按收发字节数：</p>
<pre class="crayon-plain-tag">sysdig -c fdbytes_by fd.cip</pre>
<p>列出不是发往Apache的入站请求： </p>
<pre class="crayon-plain-tag">sysdig -p "%proc.name %fd.name" "evt.type=accept and proc.name!=httpd"</pre>
<div class="blog_h3"><span class="graybg">打开文件</span></div>
<p>捕获打开特定文件的进程： </p>
<pre class="crayon-plain-tag"># 针对/var/log文件的事件
sysdig fd.name=/var/log
# 事件参数fd中包含/var/log的事件
sysdig fd.name contains /var/log
# 7227884 16:52:37.116806685 4 svlogd (5880) &gt; write fd=6(&lt;f&gt;/var/log/calico/libnetwork/current) size=50 
# 7227885 16:52:37.116816800 4 svlogd (5880) &lt; write res=50 data=./run: exec: line 3: libnetwork-plugin: not found.</pre>
<div class="blog_h3"><span class="graybg">IO分析</span></div>
<p>下面的例子，显示被读写字节数最多的文件（瞬时）：</p>
<pre class="crayon-plain-tag">sysdig -c topfiles_bytes

# Bytes               Filename                                                                                                                                                             
# --------------------------------------------------------------------------------                                                                                                         
# 2.53KB              /etc/hosts                                                                                                                                                           
# 585B                /sys/fs/cgroup/memory/kubepods/besteffort/pod56e20dda-38bf-11e9-99bd-44a8421fdcc8/memory.stat                                                                        
# 563B                /sys/fs/cgroup/memory/kubepods/besteffort/memory.stat                                                                                                                
# 116B                /sys/fs/cgroup/cpu,cpuacct/kubepods/besteffort/cpuacct.usage_percpu</pre>
<p>联用过滤器，仅仅显示/root下读写字节数最多的文件（瞬时）：</p>
<pre class="crayon-plain-tag">sysdig -c topfiles_bytes "fd.name contains /root"</pre>
<p>联用过滤器，仅仅显示admin用户读写字节数最多的文件（瞬时）： </p>
<pre class="crayon-plain-tag">sysdig -c topfiles_bytes "user.name=admin" </pre>
<p>显示延迟超过1ms的IO调用： </p>
<pre class="crayon-plain-tag">sysdig -c fileslower 1
# evt.datetime            proc.name    evt.type LATENCY(ms)  fd.name
# ----------------------- ------------ -------- ------------ -----------------------------------------
# 2019-03-25 11:15:54.119 node_exporte read               75 /host/sys/fs/xfs/sda2/stats/stats</pre>
<p>根据FD类型汇总IO数据量：</p>
<pre class="crayon-plain-tag">sysdig -r t.scap -c fdbytes_by fd.type

# Bytes     fd.type
# ------------------------------
# 485.00M   file
# 4.63M     unix
# 509.92KB  pipe
# 99.83KB   ipv4
# 53.79KB   event
# 3.08KB    inotify</pre>
<p>根据目录汇总IO数据量，仅仅考虑文件IO： </p>
<pre class="crayon-plain-tag">sysdig -r t.scap -c fdbytes_by fd.directory "fd.type=file"

# Bytes     fd.directory
# ------------------------------
# 101.89M   /tmp/
# 90.26M    /root/.ccache/tmp/
# 85.75M    /usr/include/c++/4.7.2/bits/
# 24.74M    /usr/include/c++/4.7.2/</pre>
<p>在特定目录下，汇总每个文件的IO数据量：  </p>
<pre class="crayon-plain-tag">sysdig -r t.scap -c fdbytes_by fd.filename "fd.directory=/tmp/"

# Bytes     fd.filename
# ------------------------------
# 13.64M    ccJvb4Mi.s
# 9.33M     cc4pbJkV.s
# 6.72M     ccx9zir6.s</pre>
<p>发现导致特定目录下文件IO的进程： </p>
<pre class="crayon-plain-tag"># 根据进程名聚合
                                         # 逻辑与：IO的目标是/tmp/*.s文件
sysdig -r t.scap -c fdbytes_by proc.name "fd.directory=/tmp/ and fd.filename contains .s"

# Bytes     proc.name
# ------------------------------
# 50.57M    as
# 49.03M    cc1plus
# 1.54M     cc1
# 1B        httpd</pre>
<p>发现针对特定文件IO的调用栈： </p>
<pre class="crayon-plain-tag">sysdig -A -r t.scap -c echo_fds "fd.filename=ccJvb4Mi.s"

# ------ Write 4.00KB to /tmp/ccJvb4Mi.s
# .file"chisel.cpp"
# .text
# .Ltext0:
# .section.text._ZNSt9exceptionC2Ev,"axG",@p </pre>
<div class="blog_h3"><span class="graybg">容器</span></div>
<pre class="crayon-plain-tag"># CPU用量最大的进程
sysdig -pc -c topprocs_cpu
# 网络带宽用量最大的进程
sysdig -pc -c topprocs_net 
# 文件IO字节数最大
sysdig -pc -c topfiles_bytes
# 网络连接数最高
sysdig -pc -c topconns </pre>
<div class="blog_h1"><span class="graybg">csysdig</span></div>
<p>sysdig的图形化用户接口，类似于top或htop，但是功能要强大的多：</p>
<ol>
<li>支持实时分析，也支持分析sysdig跟踪文件。跟踪文件可以来自其它机器</li>
<li>支持CPU、内存、磁盘IO、网络IO等多方面指标的详细分析</li>
<li>支持跟踪进程、文件、网络连接的IO活动</li>
<li>可以钻取到进程、文件、网络连接</li>
<li>支持sysdig的过滤语法</li>
<li>支持容器和K8S</li>
</ol>
<p>csysdig基于view，每个view是一小段Lua脚本，负责收集指标并展示在屏幕上</p>
<div class="blog_h2"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag">apt-get install sysdig</pre>
<div class="blog_h2"><span class="graybg">选项</span></div>
<p style="padding-left: 30px;"> -d 刷新间隔，默认2000<br />-pc  容器支持，显示额外的容器相关的字段<br />-k 启用K8S支持，指定API Server的URL，可用环境变量SYSDIG_K8S_API代替<br />-K 使用指定的凭证来访问K8S，可用环境变量SYSDIG_K8S_API_CERT代替<br />-N 不把端口号转换为名称<br />-r 读取跟踪文件</p>
<div class="blog_h2"><span class="graybg">用法</span></div>
<div class="blog_h3"><span class="graybg">基本用法</span></div>
<ol>
<li>不使用参数调用该命令，执行实时分析；使用-r来分析跟踪文件</li>
<li>按F2切换view</li>
<li>Enter可以钻取，Backspace可以退回</li>
<li>按F5可以显示选中条目的IO活动</li>
<li>按F6可以看到选中条目的sysdig事件</li>
</ol>
<div class="blog_h3"><span class="graybg">钻取</span></div>
<p>你可以钻取、上卷，快捷键分别为Enter、Backspace。例如你可以从进程钻取到线程，在不同的钻取级别，你都可以切换、查看其它view，非常强大</p>
<div class="blog_h3"><span class="graybg">动作</span></div>
<ol>
<li>进程视图：k杀死进程</li>
<li>容器视图：b打开Shell</li>
</ol>
<div class="blog_h3"><span class="graybg">光谱图</span></div>
<p>以系统调用为例，颜色越靠近红色，说明那个耗时区间的系统调用越多： </p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2008/04/csysdig-spectrograms.png"><img class="aligncenter size-full wp-image-22601" src="https://blog.gmem.cc/wp-content/uploads/2008/04/csysdig-spectrograms.png" alt="csysdig-spectrograms" width="100%" /></a></p>
<div class="blog_h3"><span class="graybg">输出字段</span></div>
<p>F2选择视图时，右侧会给出每个字段的含义</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/sysdig">使用Sysdig进行系统性能分析</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/sysdig/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>利用perf剖析Linux应用程序</title>
		<link>https://blog.gmem.cc/perf</link>
		<comments>https://blog.gmem.cc/perf#comments</comments>
		<pubDate>Fri, 18 Jan 2019 02:07:43 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[性能剖析]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=24443</guid>
		<description><![CDATA[<p>简介 剖析（Profiling）是一种有效的、细粒度的软件性能检查手段，大部分编程语言的生态圈都有各种性能剖析工具。本文的讨论内容与具体编程语言无关，而关注在Linux系统上对应用程序的性能进行剖析。 Linux内核实现了非常有价值的性能分析基础设施（perf infrastructure），可以用于剖析各种CPU或软件事件。这一功能由perf_events接口暴露，并提供[crayon-69e29c566181e578292963-i/]这一用户空间工具，通过此工具，你可以发现以下问题的答案： 为什么内核使用太多的CPU，哪些代码使用了这些CPU时间 什么代码导致CPU二级缓存不命中 CPU是否因内存IO而卡顿 什么代码在分配内存，分配了多少 什么触发了TCP重传 某个内核函数是否被频繁调用 线程离开CPU的原因  perf_events是内核2.6+的一部分，用户空间工具perf在包linux-tools-common中，需要安装才能使用。要想从剖析中获得更多内核相关信息，你需要符号（Symbol）和栈追踪，这可能需要安装额外的包，甚至使用特定选项重新编译你的内核。剖析用户空间代码时，也要求目标应用程序的调试信息（符号表）被保留。 工作模式 perf_events有三种工作模式： counting，在内核上下文对各种指标进行计数 sampling，对事件进行采样，并将性能数据存放到内核缓冲，然后异步的写入到perf.data bpf，4.4版本内核引入的新特性，允许在内核中执行一段用户自定义的代码，以执行高效的过滤、汇总 基于事件的采样 perf_events的采样是基于事件进行的。采样的周期以事件的数量来表示，而非基于时间。当目标事件计数溢出指定的数值，则产生一个采样。 样本中包含的信息取决于用户和工具指定的度量类型，但是最重要的信息是指令指针（instruction pointer），也就是程序被中断时所处的位置。 这种基于中断的采样，在现代处理器上存在刹车效应。也就是说，样本中记录的指针，和程序被中断以处理PMU事件时的指令指针，可能相隔数十个指令。 <a class="read-more" href="https://blog.gmem.cc/perf">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/perf">利用perf剖析Linux应用程序</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>剖析（Profiling）是一种有效的、细粒度的软件性能检查手段，大部分编程语言的生态圈都有各种性能剖析工具。本文的讨论内容与具体编程语言无关，而关注在Linux系统上对应用程序的性能进行剖析。</p>
<p>Linux内核实现了非常有价值的性能分析基础设施（perf infrastructure），可以用于剖析各种CPU或软件事件。这一功能由perf_events接口暴露，并提供<pre class="crayon-plain-tag">perf</pre>这一用户空间工具，通过此工具，你可以发现以下问题的答案：</p>
<ol>
<li>为什么内核使用太多的CPU，哪些代码使用了这些CPU时间</li>
<li>什么代码导致CPU二级缓存不命中</li>
<li>CPU是否因内存IO而卡顿</li>
<li>什么代码在分配内存，分配了多少</li>
<li>什么触发了TCP重传</li>
<li>某个内核函数是否被频繁调用</li>
<li>线程离开CPU的原因 </li>
</ol>
<p>perf_events是内核2.6+的一部分，用户空间工具perf在包linux-tools-common中，需要安装才能使用。要想从剖析中获得更多内核相关信息，你需要符号（Symbol）和栈追踪，这可能需要安装额外的包，甚至使用特定选项重新编译你的内核。剖析用户空间代码时，也要求目标应用程序的调试信息（符号表）被保留。</p>
<div class="blog_h2"><span class="graybg">工作模式</span></div>
<p>perf_events有三种工作模式：</p>
<ol>
<li>counting，在内核上下文对各种指标进行计数</li>
<li>sampling，对事件进行采样，并将性能数据存放到内核缓冲，然后异步的写入到perf.data</li>
<li>bpf，4.4版本内核引入的新特性，允许在内核中执行一段用户自定义的代码，以执行高效的过滤、汇总</li>
</ol>
<div class="blog_h2"><span class="graybg">基于事件的采样</span></div>
<p>perf_events的采样是基于事件进行的。<span style="background-color: #c0c0c0;">采样的周期以事件的数量来表示</span>，而非基于时间。当目标事件计数<span style="background-color: #c0c0c0;">溢出指定的数值，则产生一个采样</span>。</p>
<p>样本中包含的信息取决于用户和工具指定的度量类型，但是最重要的信息是指令指针（instruction pointer），也就是程序被中断时所处的位置。</p>
<p>这种基于中断的采样，在现代处理器上存在<span style="background-color: #c0c0c0;">刹车效应</span>。也就是说，样本中记录的指针，和程序被中断以处理PMU事件时的指令指针，可能相隔数十个指令。</p>
<p>record子命令默认使用cycle事件，类似于定期采样。</p>
<div class="blog_h2"><span class="graybg">事件类型</span></div>
<p>perf_events可以捕获的事件类型包括：</p>
<ol>
<li>硬件事件，来自CPU自己或CPU的PMU（Performance Monitoring Unit，性能监控单元），包含一系列微架构事件例如时钟周期、L1缓存丢失等。具体支持的事件类型取决于CPU型号</li>
<li>软件事件，基于内核计数器的低级事件，例如CPU迁移、上下文切换、Minor Faults、Major Faults（页面错误）</li>
<li>追踪点事件，由内核中的ftrace实现，包括：
<ol>
<li>内核追踪点事件，静态的、内核级的追踪点，硬编码到内核</li>
<li>用户静态定义追踪（USDT），用户态应用程序硬编码的追踪点</li>
<li>动态追踪，软件可以被动态instrumented，在任何位置创建事件。对于内核软件，使用kprobes框架，对于用户软件，使用uprobes</li>
<li>定时追踪，以任意频率抓取快照，主要用于CPU剖析，工作机制是定期引发中断</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">动态追踪</span></div>
<p>要启用内核动态追踪，需要使用内核编译参数CONFIG_KPROBES=y、CONFIG_KPROBE_EVENTS=y。要追踪基于帧指针的内核栈，需要内核编译参数CONFIG_FRAME_POINTER=y。</p>
<p>要启用用户动态追踪，需要使用内核编译参数CONFIG_UPROBES=y、CONFIG_UPROBE_EVENTS=y</p>
<div class="blog_h3"><span class="graybg">事件限定符</span></div>
<p>事件有多种表示方式，最简单的是它的字符串表示。引用事件时，可以指定限定符：</p>
<pre class="crayon-plain-tag"># 剖析用户级、内核级的cycle事件
-e cycles
-e cycles:uk
# 仅用户级
-e cycles:u</pre>
<p>限定符包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 80px; text-align: center;">限定符</td>
<td style="text-align: center;">说明</td>
<td style="text-align: center;">示例</td>
</tr>
</thead>
<tbody>
<tr>
<td><span style="color: #000000;">u</span></td>
<td>监控特权级别3, 2, 1 ，也就是用户态</td>
<td><span style="color: #000000;">event:u</span></td>
</tr>
<tr>
<td><span style="color: #000000;">k</span></td>
<td>监控特权级别0，也就是内核态</td>
<td>event:k</td>
</tr>
<tr>
<td>h</td>
<td>在虚拟化环境下监控Hypervisor事件</td>
<td>event:h</td>
</tr>
<tr>
<td>H</td>
<td>在虚拟化环境下监控宿主机事件</td>
<td>event:H</td>
</tr>
<tr>
<td>G</td>
<td>在虚拟化环境下监控客户机事件</td>
<td>event:G</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">监控范围</span></div>
<p>perf可以在不同的范围收集性能数据：</p>
<ol>
<li>单线程（per-thread）：仅仅监控选择的线程。如果线程被调度失去CPU，对它的性能计数暂停，如果线程被调度到其它CPU，计数会存储并恢复，继续在新CPU上进行计数</li>
<li>单进程（ per-process）：进程的所有线程都被监控，计数被汇总</li>
<li>单CPU（ per-cpu）：仅仅监控单颗、或者选定的几颗CPU上的事件</li>
<li>全局（system-wide）：监控所有CPU上的事件</li>
</ol>
<div class="blog_h2"><span class="graybg">调试信息</span></div>
<div class="blog_h3"><span class="graybg">符号</span></div>
<p>和其它调试工具一样，perf需要符号信息，符号用于将内存地址转换为可读的函数、变量名称。没有符号信息，你会看到0x7ff3c1cddf29这样的HEX代码，无法阅读。</p>
<p>如果需要被剖析的软件通过包管理器安装，你需要找到并安装对应的调试包（可能以-dbgsym为后缀）。如果找不到调试包，可以考虑手工编译。</p>
<div class="blog_h3"><span class="graybg">JIT符号</span></div>
<p>Java、Node.js等基于虚拟机的语言，在自己的虚拟CPU上执行代码，因而具有自己管理栈和执行函数的方法。</p>
<p>为了通过perf来剖析这类语言的程序，你需要：</p>
<ol>
<li>对于Java，使用<a href="https://github.com/jvm-profiling-tools/perf-map-agent">perf-map-agent</a></li>
<li>对于Node.js，使用命令行选项<pre class="crayon-plain-tag">--perf_basic_prof</pre></li>
</ol>
<p>你可能看不到完整的Java栈，这是因为X86上的Hotspot忽略了帧指针（Frame pointer）。在8u60+版本以后，可以使用-XX:+PreserveFramePointer修复这一问题。</p>
<div class="blog_h3"><span class="graybg">栈追踪</span></div>
<p>为了方便剖析，编译时尽量保留帧指针，编译器的优化行为可能导致难以调试，但是很多情况下优化掉帧指针是默认行为。</p>
<p>缺少帧指针会导致无法看到完整的调用栈，解决此问题的方法有几种：</p>
<ol>
<li>使用DWARF（ELF文件通常使用的调试信息格式）信息来Unwind栈</li>
<li>使用LBR（Last Branch Record），这是处理器特性，因此可能不可用</li>
<li>保留帧指针</li>
</ol>
<p>下面是默认情况下编译（<span style="background-color: #c0c0c0;">-O2，忽略帧指针</span>）得到的OpenSSH的剖析结果：</p>
<pre class="crayon-plain-tag">57.14%     sshd  libc-2.15.so        [.] connect           
               |
               --- connect
                  |          
                  |--25.00%-- 0x7ff3c1cddf29
                  |          
                  |--25.00%-- 0x7ff3bfe82761
                  |          0x7ff3bfe82b7c
                  |          
                  |--25.00%-- 0x7ff3bfe82dfc
                   --25.00%-- [...]</pre>
<p>以<pre class="crayon-plain-tag">-fno-omit-frame-pointer</pre>重新编译后：</p>
<pre class="crayon-plain-tag">100.00%     sshd  libc-2.15.so   [.] __GI___connect_internal
               |
               --- __GI___connect_internal
                  |          
                  |--30.00%-- add_one_listen_addr.isra.0
                  |          add_listen_addr
                  |          fill_default_server_options
                  |          main
                  |          __libc_start_main
                  |          
                  |--20.00%-- __nscd_get_mapping
                  |          __nscd_get_map_ref
                  |          
                  |--20.00%-- __nscd_open_socket
                   --30.00%-- [...]</pre>
<p>可以看到符号都显示出来了，另外add_one_listen_addr的祖先帧也显示出来了。 </p>
<div class="blog_h3"><span class="graybg">关于内核</span></div>
<p>你需要使用<pre class="crayon-plain-tag">CONFIG_FRAME_POINTER=y</pre>来编译内核，以保留栈指针信息。</p>
<div class="blog_h3"><span class="graybg">DWARF</span></div>
<p>从内核3.9版本开始，缺失栈指针的问题有个变通的解决方案（对于用户空间的栈）——使用libunwind。libunwind基于dwarf，调用perf时传入--call-graph dwarf即可。</p>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">Ubuntu</span></div>
<p>在Ubuntu上可以这样安装：</p>
<pre class="crayon-plain-tag">apt install linux-tools-common</pre>
<p>系统可能会提示你安装其它的包，安装即可。</p>
<div class="blog_h1"><span class="graybg">子命令列表</span></div>
<p>perf支持一系列的子命令：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">子命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>annotate</td>
<td>读取perf.data并显示被注解的代码</td>
</tr>
<tr>
<td>bench</td>
<td>基准测试的框架</td>
</tr>
<tr>
<td>config</td>
<td>在配置文件中读写配置项</td>
</tr>
<tr>
<td>diff</td>
<td>读取perf.data并显示剖析差异</td>
</tr>
<tr>
<td>evlist</td>
<td>列出perf.data中的事件名称</td>
</tr>
<tr>
<td>inject</td>
<td>用于增强事件流的过滤器</td>
</tr>
<tr>
<td>kmem</td>
<td>跟踪/度量内核内存属性</td>
</tr>
<tr>
<td>kvm</td>
<td>跟踪/度量KVM客户机系统</td>
</tr>
<tr>
<td>list</td>
<td>显示符号化的事件列表</td>
</tr>
<tr>
<td>lock</td>
<td>分析锁事件</td>
</tr>
<tr>
<td>mem</td>
<td>分析内存访问</td>
</tr>
<tr>
<td>record</td>
<td>执行剖析</td>
</tr>
<tr>
<td>report</td>
<td>显示剖析结果</td>
</tr>
<tr>
<td>sched</td>
<td>分析调度器</td>
</tr>
<tr>
<td>stat</td>
<td>获取性能计数</td>
</tr>
<tr>
<td>top</td>
<td>显示成本最高的操作并动态刷新</td>
</tr>
<tr>
<td>trace</td>
<td>类似于strace的工具</td>
</tr>
<tr>
<td>probe</td>
<td>定义新的动态追踪点</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">stat</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>运行一个命令，并收集性能计数器。</p>
<p>对于任何支持计数的事件，perf都能够在进程执行期间对发生的事件进行计数，计数结果会直接打印在控制台上。</p>
<p>stat子命令可以在线程、进程、CPU、全局级别进行计数。默认情况下进程的所有线程、子进程的计数都被统计在内，可以通过-i选项修改此行为。</p>
<div class="blog_h2"><span class="graybg">选项</span></div>
<p>参考record子命令。</p>
<div class="blog_h2"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag"># 如果不指定-e，则默认收集所有事件的计数器
perf stat [-e &lt;EVENT&gt; | --event=EVENT] [-a] &lt;command&gt;
perf stat [-e &lt;EVENT&gt; | --event=EVENT] [-a] -- &lt;command&gt; [&lt;options&gt;]</pre>
<div class="blog_h2"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag"># CPU性能计数器
perf stat command

# 详细的CPU性能计数器
perf stat -d command

# 收集指定进程的性能计数，直到Ctrl-C
perf stat -p PID

# 收集整个系统的性能计数，持续5秒
perf stat -a sleep 5
# Performance counter stats for 'system wide':
#      20010.623960      cpu-clock (msec)          #    4.000 CPUs utilized          
#            61,947      context-switches          #    0.003 M/sec                  
#             4,153      cpu-migrations            #    0.208 K/sec                  
#                99      page-faults               #    0.005 K/sec                  
#     5,003,068,069      cycles                    #    0.250 GHz                      (49.96%)
#     2,000,087,622      instructions              #    0.40  insn per cycle           (75.00%)
#       393,183,513      branches                  #   19.649 M/sec                    (75.05%)
#        24,391,051      branch-misses             #    6.20% of all branches          (74.99%)

#       5.003219775 seconds time elapsed


# 指定需要收集的几种计数器，持续10秒
perf stat -e cycles,instructions,cache-references,cache-misses,bus-cycles -a sleep 10

# CPU一级缓存计数器
perf stat -e L1-dcache-loads,L1-dcache-load-misses,L1-dcache-stores command

# TLB计数器
perf stat -e dTLB-loads,dTLB-load-misses,dTLB-prefetch-misses command

# CPU最后一级缓存计数器
perf stat -e LLC-loads,LLC-load-misses,LLC-stores,LLC-prefetches command

# 统计指定进程的系统调用
perf stat -e 'syscalls:sys_enter_*' -p PID

# 统计全局系统调用
perf stat -e 'syscalls:sys_enter_*' -a sleep 5

# 统计调度器相关事件
perf stat -e 'sched:*' -p PID
perf stat -e 'sched:*' -p PID sleep 10

# 统计Ext4相关事件
perf stat -e 'ext4:*' -a sleep 10

# 统计块IO事件
perf stat -e 'block:*' -a sleep 10

# 收集多种事件
perf stat -e cycles,instructions,cache-misses</pre>
<div class="blog_h1"><span class="graybg">top</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>动态显示执行成本最高的项。</p>
<p>默认情况下以cycles事件采样，样本最高的输出在最顶部。</p>
<div class="blog_h2"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag"># 动态显示成本最高的地址和符号，不保存到文件
perf top -F 49
# 动态显示成本最高的命令
perf top -F 49 -ns comm,dso</pre>
<div class="blog_h1"><span class="graybg">record</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>此命令可以启动一个命令，并对其进行剖析，然后把剖析数据记录到文件（默认perf.data）。按Ctrl - C可以随时结束剖析。</p>
<p>此命令可以在线程、进程、CPU、全局级别进行剖析。</p>
<p>record, report, annotate是一组相关的命名，通常的使用流程是：</p>
<ol>
<li>在被剖析机器上调用record录制数据</li>
<li>拷贝录制的perf.data，在任意机器上调用report、annotate进行分析</li>
</ol>
<p>该命令不是记录所有事件，还是进行采样。默认情况下采样基于cycle事件，也就是进行定期采样。</p>
<p>perf_events接口允许通过两种方式描述采样周期：</p>
<ol>
<li>period：事件发生的次数</li>
<li>frequency：每秒样本的平均个数</li>
</ol>
<p>perf默认使用第二种，具体来说对应到-F选项。-F 1000表示1000Hz，也就是每秒平均采样1000个。<span style="background-color: #c0c0c0;">内核会动态的调整采样周期</span>，以尽量满足需求。</p>
<div class="blog_h2"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag">perf record [-e &lt;EVENT&gt; | --event=EVENT] [-l] [-a]    &lt;command&gt;
perf record [-e &lt;EVENT&gt; | --event=EVENT] [-l] [-a] -- &lt;command&gt; [&lt;options&gt;]</pre>
<div class="blog_h2"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-e</td>
<td>
<p>剖析的事件类型，也就是性能监控单元（Performance Monitoring Unit，PMU）类型。可以指定：</p>
<ol>
<li>事件名称，执行<pre class="crayon-plain-tag">perf list</pre>可以显示支持的事件列表</li>
<li>一组事件，以花括号包围<pre class="crayon-plain-tag">{event1,event2,...}</pre></li>
<li>其它形式</li>
</ol>
</td>
</tr>
<tr>
<td>--filter</td>
<td>事件过滤器，必须和指定了追踪点（tracepoint）事件的-e配合使用</td>
</tr>
<tr>
<td>--exclude-perf</td>
<td>不记录perf自己发起的事件</td>
</tr>
<tr>
<td>-a</td>
<td>
<p>使用Per-CPU模式，如果不指定-C，则相当于全局模式</p>
<p>如果指定-C，则可以选定若干CPU</p>
</td>
</tr>
<tr>
<td>-p</td>
<td>收集指定进程的事件，逗号分割的PID列表</td>
</tr>
<tr>
<td>-t</td>
<td>收集指定线程的事件，逗号分割的线程ID列表</td>
</tr>
<tr>
<td>-u</td>
<td>收集指定用户的进程的事件</td>
</tr>
<tr>
<td>-r</td>
<td>使用RT SCHED_FIFO优先级收集数据</td>
</tr>
<tr>
<td>-c</td>
<td>采样周期</td>
</tr>
<tr>
<td>-o</td>
<td>输出文件的名称</td>
</tr>
<tr>
<td>-i</td>
<td>不包括子任务的事件，不监控子进程、线程、子进程的线程</td>
</tr>
<tr>
<td>-F</td>
<td>以指定的频率剖析</td>
</tr>
<tr>
<td>-g</td>
<td>记录调用栈</td>
</tr>
<tr>
<td>--call-graph</td>
<td>
<p>收集调用栈使用的方法：</p>
<ol>
<li>fp，栈指针</li>
<li>dwarf，DWARF的调用帧信息（CFI）</li>
<li>lbr，Hardware Last Branch Record facility</li>
</ol>
<p>某些系统上，如果应用通过GCC的--fomit-frame-pointer参数构建，fp模式下可能只能看到匿名调用帧，可以用dwarf代替</p>
</td>
</tr>
<tr>
<td>-s</td>
<td>记录每个线程的事件计数器，配合<pre class="crayon-plain-tag">perf report -T</pre>使用</td>
</tr>
<tr>
<td>-d</td>
<td>记录样本地址</td>
</tr>
<tr>
<td>-T</td>
<td>记录样本时间戳</td>
</tr>
<tr>
<td>-P</td>
<td>记录样本周期</td>
</tr>
<tr>
<td>-C</td>
<td>仅仅从指定的CPU上收集样本，示例：<pre class="crayon-plain-tag">-C 0,2-3</pre></td>
</tr>
<tr>
<td>-G</td>
<td>仅仅记录指定控制组的事件</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">示例</span></div>
<div class="blog_h3"><span class="graybg">性能剖析</span></div>
<pre class="crayon-plain-tag"># 以99HZ的频率剖析指定的命令，默认情况下工作在Per-thread模式
perf record -F 99 command

# 以99HZ的频率剖析指定的PID
perf record -F 99 -p PID
# 以99HZ的频率剖析指定的PID，持续10秒
perf record -F 99 -p PID sleep 10
# 以99HZ的频率剖析整个系统
perf record -F 99 -ag -- sleep 10

# 基于事件发生次数，而非采样频率来指定采样周期
perf record -e retired_instructions:u -c 2000

# 进行栈追踪（通过Frame Pointer）
perf record -F 99 -p PID -g -- sleep 10
# 进行栈追踪（通过DWARF）
perf record -F 99 -p PID --call-graph dwarf sleep 10
# 全局性栈追踪
perf record -F 99 -ag -- sleep 10  # 4.11之前
perf record -F 99 -g -- sleep 10   # 4.11之后
# 追踪某个容器的栈
perf record -F 99 -e cpu-clock --cgroup=docker/1d567f4393190204...etc... -a -- sleep 10

# 每发生10000次L1缓存丢失，进行一次采样
perf record -e L1-dcache-load-misses -c 10000 -ag -- sleep 5
# 每发生以100次最后级别的CPU缓存丢失，进行一次采用
perf record -e LLC-load-misses -c 100 -ag -- sleep 5 

# 采样内核指令
perf record -e cycles:k -a -- sleep 5 
# 采用用户指令
perf record -e cycles:u -a -- sleep 5 
# 精确采样用户指令，基于PEBS
perf record -e cycles:up -a -- sleep 5</pre>
<div class="blog_h3"><span class="graybg">静态追踪</span></div>
<pre class="crayon-plain-tag"># 追踪新进程的创建
perf record -e sched:sched_process_exec -a

# 追踪上下文切换
perf record -e context-switches -a
perf record -e context-switches -c 1 -a
perf record -e context-switches -ag
# 追踪通过sched跟踪点的上下文切换
perf record -e sched:sched_switch -a

# 追踪CPU迁移
perf record -e migrations -a -- sleep 10

# 追踪所有connect调用（出站连接）
perf record -e syscalls:sys_enter_connect -ag
# 追踪所有accepts调用（入站连接）
perf record -e syscalls:sys_enter_accept* -ag

# 追踪所有块设备请求
perf record -e block:block_rq_insert -ag
# 追踪所有块设备发起和完成
perf record -e block:block_rq_issue -e block:block_rq_complete -a
# 追踪100KB（200扇区）以上的块操作完成
perf record -e block:block_rq_complete --filter 'nr_sector &gt; 200'
# 追踪所有同步写操作的完成
perf record -e block:block_rq_complete --filter 'rwbs == "WS"'
# 追踪所有写操作的完成
perf record -e block:block_rq_complete --filter 'rwbs ~ "*W*"'

# 采样Minor faults（RSS尺寸增加）
perf record -e minor-faults -ag
perf record -e minor-faults -c 1 -ag
# 采样页面错误
perf record -e page-faults -ag

# 追踪所有Ext4调用，并把结果写入到非Ext4分区
perf record -e 'ext4:*' -o /tmp/perf.data -a 

# 追踪kswapd唤醒事件
perf record -e vmscan:mm_vmscan_wakeup_kswapd -ag

# 添加Node.js USDT 探针，要求Linux 4.10+
perf buildid-cache --add `which node`
# 跟踪Node.js http__server__request USDT事件
perf record -e sdt_node:http__server__request -a </pre>
<div class="blog_h1"><span class="graybg">report</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>此命令用于分析perf record生成的perf.data文件。</p>
<div class="blog_h2"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag">perf report [-i &lt;file&gt; | --input=file]</pre>
<div class="blog_h2"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-i</td>
<td>指定输入文件</td>
</tr>
<tr>
<td>-n</td>
<td>显示每个符号的样本数量</td>
</tr>
<tr>
<td>-T</td>
<td>显示每个线程的事件计数器</td>
</tr>
<tr>
<td>--pid</td>
<td>仅仅显示指定PID的事件</td>
</tr>
<tr>
<td>--tid</td>
<td>仅仅显示指定TID的事件</td>
</tr>
<tr>
<td>-s</td>
<td>
<p>指定直方图的排序方式，逗号分隔多个排序方式，优先级降序</p>
<p style="padding-left: 30px;">comm 任务的命令<br />pid 任务的PID<br />dso 采样时执行的库或者模块的名称<br />symbol 采样时执行的函数<br />srcline,srcfile 代码文件和行数，需要DWARF调试信息<br />overhead  采样的执行成本<br />overhead_sys 采样在内核态的执行成本<br />overhead_us 采样在用户态的执行成本<br />sample 采样数量</p>
</td>
</tr>
<tr>
<td>-F</td>
<td>指定输出字段，逗号分隔多个字段</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">输出</span></div>
<div class="blog_h3"><span class="graybg">默认输出</span></div>
<p>默认情况下，样本根据函数（符号）分类，数量最多的样本位于最前面：</p>
<pre class="crayon-plain-tag">#  Overhead        Command  Shared Object                      Symbol
+  21.70%          swapper  [kernel.kallsyms]              [k] 0xffffffff8104f45a
+   8.89%  qemu-system-x86  [kernel.kallsyms]              [k] 0xffffffff810496f6
+   3.32%             Xorg  [kernel.kallsyms]              [k] 0xffffffff8172da33
+   2.96%       vmware-vmx  [kernel.kallsyms]              [k] 0xffffffff81121880
+   2.23%           chrome  chrome                         [.] 0x0000000004fdb6e6
+   2.17%             java  [kernel.kallsyms]              [k] 0xffffffff8109f02b
+   1.71%           chrome  [kernel.kallsyms]              [k] 0xffffffff8172d982
+   1.56%             Xorg  [nvidia]                       [k] _nv008054rm
+   1.55%             Xorg  [nvidia]                       [k] _nv014095rm</pre>
<p>各列含义如下：</p>
<ol>
<li>Overhead，在对应函数中采样到的样本计数</li>
<li> Command，样本从什么应用程序中采集到</li>
<li>Shared Object，样本来自的ELF镜像。如果：
<ol>
<li>应用程序是动态链接的，这里可能显示共享库的名称</li>
<li>如果样本来自内核空间，则<span style="background-color: #c0c0c0;">一律显示伪ELF镜像名“kernel.kallsyms”</span></li>
</ol>
</li>
<li>第4列是特权级别，点号表示用户级别，k、g、u、H分别表示内核级别、客户机内核级别、客户机用户级别、hypervisor</li>
<li>最后一列是符号名称，如果缺乏调试信息，则显示为0x开头的函数内存地址</li>
</ol>
<div class="blog_h3"><span class="graybg">指定分组规则</span></div>
<p>你可以指定多种分组规则，对样本数进行统计。</p>
<p>下面的例子根据共享对象分组：</p>
<pre class="crayon-plain-tag">perf report --sort=dso

+  48.05%  [kernel.kallsyms]                                                                                                                                                             
+   8.06%  libglib-2.0.so.0.4600.1                                                                                                                                                       
+   5.81%  libc-2.19.so                                                                                                                                                                  
+   4.93%  [nvidia]                                                                                                                                                                      
+   2.83%  libgobject-2.0.so.0.4600.1                                                                                                                                                    
+   2.39%  [kvm_intel]</pre>
<p>下面的例子根据CPU序号分组：</p>
<pre class="crayon-plain-tag">perf report --sort=cpu

+  17.10%  0                                                                                                                                                                             
+  15.21%  2                                                                                                                                                                             
+  14.76%  4                                                                                                                                                                             
+  12.81%  6                                                                                                                                                                             
+  11.98%  3                                                                                                                                                                             
+  10.00%  5                                                                                                                                                                             
+   9.85%  1                                                                                                                                                                             
+   8.28%  7</pre>
<div class="blog_h3"><span class="graybg">内核符号</span></div>
<p>perf不知道如何从压缩内核（vmlinuz）中抽取符号，如果你的内核保留了调试符号，则可以用-k来指出未压缩内核的位置：</p>
<pre class="crayon-plain-tag">perf report -k /tmp/vmlinux</pre>
<div class="blog_h3"><span class="graybg">Children/Self</span></div>
<p>如果在record时收集了调用链，则Overhead可以在Children、Self两个列中显示。Children显示子代函数的样本计数、Self显示函数自己的样本计数</p>
<div class="blog_h2"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag"># 报告perf.data
perf report

# 显示样本数量
perf report -n

# 树状结构显示，展开，可以追踪到高成本的执行路径
perf report --stdio
# 每行显示一个帧，扁平化显示，按成本降序排列
perf report --stdio -n -g folded</pre>
<div class="blog_h1"><span class="graybg">annotate</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>此命令用于源码级别的分析。</p>
<div class="blog_h2"><span class="graybg">输出</span></div>
<p>所有被采样到的函数都会被反汇编，每个指令占据采样的比例会被输出。</p>
<div class="blog_h3"><span class="graybg">源码信息</span></div>
<p>如果应用程序以-ggdb编译，annotate还能够生成源码级别信息：</p>
<pre class="crayon-plain-tag">------------------------------------------------
 Percent |   Source code &amp; Disassembly of noploop
------------------------------------------------
         :
         :
         :
         :   Disassembly of section .text:
         :
         :   08048484 &lt;main&gt;:
         :   #include &lt;string.h&gt;
         :   #include &lt;unistd.h&gt;
         :   #include &lt;sys/time.h&gt;
         :
         :   int main(int argc, char **argv)
         :   {
    0.00 :    8048484:       55                      push   %ebp
    0.00 :    8048485:       89 e5                   mov    %esp,%ebp
[...]
    0.00 :    8048530:       eb 0b                   jmp    804853d &lt;main+0xb9&gt;
         :                           count++;
   14.22 :    8048532:       8b 44 24 2c             mov    0x2c(%esp),%eax
    0.00 :    8048536:       83 c0 01                add    $0x1,%eax
   14.78 :    8048539:       89 44 24 2c             mov    %eax,0x2c(%esp)
         :           memcpy(&amp;tv_end, &amp;tv_now, sizeof(tv_now));
         :           tv_end.tv_sec += strtol(argv[1], NULL, 10);
         :           while (tv_now.tv_sec &lt; tv_end.tv_sec ||
         :                  tv_now.tv_usec &lt; tv_end.tv_usec) {
         :                   count = 0;
         :                   while (count &lt; 100000000UL)
   14.78 :    804853d:       8b 44 24 2c             mov    0x2c(%esp),%eax
   56.23 :    8048541:       3d ff e0 f5 05          cmp    $0x5f5e0ff,%eax
    0.00 :    8048546:       76 ea                   jbe    8048532 &lt;main+0xae&gt;
[...]</pre>
<p>再次强调一下刹车现象，百分比信息可能不准确。</p>
<div class="blog_h3"><span class="graybg">内核符号</span></div>
<p>对于压缩内核，你可以指定非压缩版本内核的位置，以解析符号：</p>
<pre class="crayon-plain-tag">perf annotate -k /tmp/vmlinux -d symbol </pre>
<div class="blog_h1"><span class="graybg">list</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>显示支持的事件列表。</p>
<div class="blog_h2"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag"># 显示所有事件
perf list

# 显示跟踪点sched下的事件
perf list 'sched:*'</pre>
<div class="blog_h1"><span class="graybg">probe</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>用于定义新的追踪点，实现动态追踪。</p>
<div class="blog_h2"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag">perf probe [options] --add=PROBE [...]
perf probe [options] PROBE
perf probe [options] --del=[GROUP:]EVENT [...]
perf probe --list[=[GROUP:]EVENT]
perf probe [options] --line=LINE
perf probe [options] --vars=PROBEPOINT
perf probe [options] --funcs</pre>
<div class="blog_h2"><span class="graybg">示例</span></div>
<div class="blog_h3"><span class="graybg">动态追踪</span></div>
<pre class="crayon-plain-tag"># 为tcp_sendmsg函数添加进入追踪点，--add可以省略
perf probe --add tcp_sendmsg

# 移除tcp_sendmsg进入追踪点
perf probe -d tcp_sendmsg

# 为tcp_sendmsg函数添加退出追踪点
perf probe 'tcp_sendmsg%return'

# 显示tcp_sendmsg的可用变量，需要debuginfo
perf probe -V tcp_sendmsg
# 显示tcp_sendmsg的可用变量和外部变量
perf probe -V tcp_sendmsg --externs

# 显示tcp_sendmsg可用的行探针
perf probe -L tcp_sendmsg

# 显示tcp_sendmsg的81行可用的探针
perf probe -V tcp_sendmsg:81

# 为tcp_sendmsg添加进入进入追踪点，使用3个参数寄存器（依赖于平台）
perf probe 'tcp_sendmsg %ax %dx %cx'

# 为tcp_sendmsg添加进入进入追踪点，且将cx寄存器设置别名bytes（依赖于平台）
perf probe 'tcp_sendmsg bytes=%cx'

# 追踪tcp_sendmsg，要求%cx变量大于100
perf record -e probe:tcp_sendmsg --filter 'bytes &gt; 100'

# 捕获返回值
perf probe 'tcp_sendmsg%return $retval'

# 为tcp_sendmsg和它的入口参数size添加追踪点，需要debuginfo，但是不依赖平台
perf probe 'tcp_sendmsg size'

# 为tcp_sendmsg和size、Socket状态添加追踪点
perf probe 'tcp_sendmsg size sk-&gt;__sk_common.skc_state'

# size大于0并且套接字状态不为TCP_ESTABLISHED时追踪tcp_sendmsg
perf record -e probe:tcp_sendmsg --filter 'size &gt; 0 &amp;&amp; skc_state != 1' -a

# 在tcp_sendmsg的81行添加追踪点，使用局部变量seglen
perf probe 'tcp_sendmsg:81 seglen'

# 为libc的用户态malloc函数添加探针
perf probe -x /lib64/libc.so.6 malloc

# 列出可用的动态探针
perf probe -l?</pre>
<div class="blog_h1"><span class="graybg">inject</span></div>
<div class="blog_h2"><span class="graybg">说明</span></div>
<p>该命令使用额外的信息来增强事件流。</p>
<div class="blog_h2"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-b</td>
<td>注入build-ids</td>
</tr>
<tr>
<td>-v</td>
<td>冗长输出</td>
</tr>
<tr>
<td>-i</td>
<td>指定输入文件</td>
</tr>
<tr>
<td>-o</td>
<td>指定输出文件</td>
</tr>
<tr>
<td>-s</td>
<td>合并sched_stat、sched_switch，以获得任务在何处睡眠、睡眠了多久</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">剖析示例</span></div>
<div class="blog_h2"><span class="graybg">零散示例</span></div>
<div class="blog_h3"><span class="graybg">全局剖析</span></div>
<p>进行10秒钟的剖析：</p>
<pre class="crayon-plain-tag">perf record -a -g sleep 10</pre>
<p>根据应用程序的名称进行分组，整体上根据样本的执行成本进行排序：</p>
<pre class="crayon-plain-tag">perf report --sort comm,dso</pre>
<p>可以展开执行成本高的项，追踪到CPU时间都消耗在哪里了：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2013/01/perf.png"><img class="aligncenter size-full wp-image-24461" src="https://blog.gmem.cc/wp-content/uploads/2013/01/perf.png" alt="perf" width="1004" height="348" /></a><a href="https://blog.gmem.cc/wp-content/uploads/2013/01/perf.png"><br /></a>对于上面这个例子，可以看到，kubelet消耗的资源主要用于Prometheus相关的指标收集，以及grpc的客户端。</p>
<div class="blog_h3"><span class="graybg">CPU性能计数</span></div>
<pre class="crayon-plain-tag">perf stat -- tar xzf ideaIU-2018.3.3.tar.gz 

 Performance counter stats for 'tar xzf ideaIU-2018.3.3.tar.gz':
    # 反映CPU利用率
       9725.627068 task-clock (msec)         #    0.941 CPUs utilized          
    # 上下文切换数量
            45,568 context-switches          #    0.005 M/sec                  
             1,370 cpu-migrations            #    0.141 K/sec        
    # 页面错误数量          
               623 page-faults               #    0.064 K/sec                  
    # 经过的时钟周期的数量
    36,249,427,884 cycles                    #    3.727 GHz                    
    # IPC，每时钟周期指令数（instructions per cycle），通常越高越好，表示工作处理过程是优化的
    # 但是，最好还是检查实际执行的指令，Spin loop就是高指令频率但是实际上什么事都不做的反例
    46,378,956,536 instructions              #    1.28  insns per cycle
     # 分支和分支预测失败
     8,274,686,762 branches                  #  850.813 M/sec                  
       214,332,307 branch-misses             #    2.59% of all branches        

      10.334029429 seconds time elapsed</pre>
<div class="blog_h3"><span class="graybg">定期采样</span></div>
<p>perf_events支持使用固定频率来采样指令指针或栈追踪，进而分析CPU使用。采样频率通过-F选项指定。</p>
<p>频率可以选用99，而非100HZ，以避免采样和某些周期性活动的步调完全一致而导致结果失真。如果需要更高分辨率的采样，可以使用997这样的频率，但是注意频率越高剖析本身的成本也越高。</p>
<p>示例：</p>
<pre class="crayon-plain-tag"># 以99HZ频率，全局的采样30秒
perf record -F 99 -a -g -- sleep 30</pre>
<div class="blog_h3"><span class="graybg">硬件计数器触发采样</span></div>
<p>除了定时触发（固定频率采样），采样也可以由特定的CPU硬件计数器触发。使用这些计数器可以分析缓存丢失、memory stall cycles（由于访问内存而导致的时钟周期浪费，内存访问速度相对CPU时钟周期要慢的多）等更低级别的处理器事件。</p>
<p>可用的事件列表：</p>
<pre class="crayon-plain-tag">perf list | grep Hardware
  cpu-cycles OR cycles                               [Hardware event]
  instructions                                       [Hardware event]
  cache-references                                   [Hardware event]
  cache-misses                                       [Hardware event]
  branch-instructions OR branches                    [Hardware event]
  branch-misses                                      [Hardware event]
  bus-cycles                                         [Hardware event]
  ref-cycles                                         [Hardware event]
  L1-dcache-loads                                    [Hardware cache event]
  L1-dcache-load-misses                              [Hardware cache event]
  L1-dcache-stores                                   [Hardware cache event]
  L1-dcache-store-misses                             [Hardware cache event]
  L1-dcache-prefetch-misses                          [Hardware cache event]
  L1-icache-load-misses                              [Hardware cache event]
  LLC-loads                                          [Hardware cache event]
  LLC-stores                                         [Hardware cache event]
  LLC-prefetches                                     [Hardware cache event]
  dTLB-loads                                         [Hardware cache event]
  dTLB-load-misses                                   [Hardware cache event]
  dTLB-stores                                        [Hardware cache event]
  dTLB-store-misses                                  [Hardware cache event]
  iTLB-loads                                         [Hardware cache event]
  iTLB-load-misses                                   [Hardware cache event]
  branch-loads                                       [Hardware cache event]
  branch-load-misses                                 [Hardware cache event]
  mem:&lt;addr&gt;[:access]                                [Hardware breakpoint]</pre>
<p>如果每当发生这些事件，就记录栈追踪，对系统性能有严重的影响。因此，通常使用-c选项，当计数器增加一定数量后，记录一次栈追踪：</p>
<pre class="crayon-plain-tag">perf record -e L1-dcache-load-misses -c 10000 -ag -- sleep 5</pre>
<p>上面这个例子，每当L1缓存丢失10000次，记录一次栈追踪。</p>
<div class="blog_h3"><span class="graybg">统计系统调用</span></div>
<p>下面的命令统计所有调用次数大于0的系统调用的数量：</p>
<pre class="crayon-plain-tag">perf stat -e 'syscalls:sys_enter_*' tar xzf ideaIU-2018.3.3.tar.gz 2&gt;&amp;1 | awk '$1 != 0'
 Performance counter stats for 'tar xzf ideaIU-2018.3.3.tar.gz':

                 2      syscalls:sys_enter_statfs                                   
            12,417      syscalls:sys_enter_utimensat                                   
  ...                                 
            12,417      syscalls:sys_enter_unlinkat                                   
  ...                                
            12,432      syscalls:sys_enter_newfstat                                   
                 1      syscalls:sys_enter_lseek                                    
           184,818      syscalls:sys_enter_read                                     
           192,154      syscalls:sys_enter_write                                    
                12      syscalls:sys_enter_access                                   
            12,417      syscalls:sys_enter_fchmod                                   
            12,417      syscalls:sys_enter_fchown                                   
                26      syscalls:sys_enter_open                                     
            24,834      syscalls:sys_enter_openat                                   
            12,442      syscalls:sys_enter_close                                                                 
  ...
      38.365080478 seconds time elapsed</pre>
<p>可以看到，主要的系统调用都是read/write。</p>
<p>使用strace命令可以获得类似的结果，但是strace的成本要高的多。测试结果显示，perf可能让程序变慢2.5倍，而strace则会让程序变慢高达60倍。</p>
<div class="blog_h3"><span class="graybg">统计新建进程 </span></div>
<pre class="crayon-plain-tag">perf record -e sched:sched_process_exec -a
# Ctrl-C
perf report -n --sort comm --stdio

# Overhead       Samples  Command
# ........  ............  .......
#
    32.14%             9  ionice 
    32.14%             9  nice   
    21.43%             6  du     
    10.71%             3  find   
     3.57%             1  ipset</pre>
<p>追踪点sched:sched_process_exec在某个进程调用exec()产生另外一个进程时生效，这通常是新进程的产生方式。但是这个例子的结果不一定准确，原因是：</p>
<ol>
<li>进程可能fork()出子进程，追踪点sched:sched_process_fork可以用于这种情况</li>
<li>应用程序可以reexec —— 针对自己再次调用exec() —— 这种用法可以清空地址空间，但是不会产生新进程</li>
</ol>
<div class="blog_h3"><span class="graybg">追踪出站连接</span></div>
<p>某些时候你希望了解服务器发起了哪些网络连接，是哪些进程发起的，为何发起。这些网络连接可能是延迟的根源。</p>
<p>追踪connect系统调用，即可了解出站连接：</p>
<pre class="crayon-plain-tag">perf record -e syscalls:sys_enter_connect -ag
perf report --stdio

# Children      Self  Trace output
# ........  ........  ............................................................
#
     5.88%     5.88%  fd: 0x00000005, uservaddr: 0xc4206fa390, addrlen: 0x0000001b
            |
            ---0x894812ebc0312874
               0xc42008e000
               runtime.main
               main.main
               github.com/projectcalico/node/pkg/readiness.Run
               github.com/projectcalico/node/pkg/readiness.checkBIRDReady
               github.com/projectcalico/node/pkg/readiness/bird.GRInProgress
               net.Dial
               net.(*Dialer).Dial
               net.(*Dialer).DialContext
               net.dialSerial
               net.dialSingle
               net.dialUnix
               net.unixSocket
               net.socket
               net.(*netFD).dial
               net.(*netFD).connect
               syscall.Connect
               syscall.Syscall</pre>
<p>可以看到，占比最高的出站连接是由Calico Node容器发起的。</p>
<div class="blog_h3"><span class="graybg">追踪套接字缓冲</span></div>
<p>通过剖析套接字缓冲的使用情况，也是定位到什么代码使用了最多的网络I/O的一种方法：</p>
<pre class="crayon-plain-tag">perf record -e 'skb:consume_skb' -ag</pre>
<div class="blog_h3"><span class="graybg">睡眠时间分析</span></div>
<p>使用perf可以了解应用程序为何休眠、在何处休眠、休眠多久。主要通过收集sched_stat、sched_switch事件实现。</p>
<p>剖析的目标代码如下：</p>
<pre class="crayon-plain-tag">for (i = 0; i &lt;  10; i++) {
        ts1.tv_sec = 0;
        ts1.tv_nsec = 10000000;
        // 休眠
        nanosleep(&amp;ts1, NULL);

        tv1.tv_sec = 0;
        tv1.tv_usec = 40000;
        // 休眠
        select(0, NULL, NULL, NULL,&amp;tv1);
}</pre>
<p>执行下面的命令开始剖析：</p>
<pre class="crayon-plain-tag">./perf record -e sched:sched_stat_sleep -e sched:sched_switch  -e sched:sched_process_exit -g -o perf.data.raw  sleep_test</pre>
<p>合并sched_start、sched_switch事件：</p>
<pre class="crayon-plain-tag">perf inject -v -s -i perf.data.raw -o perf.data</pre>
<p>报告：</p>
<pre class="crayon-plain-tag">perf report --stdio --show-total-period -i perf.data

  100.00%     502408738      foo  [kernel.kallsyms]  [k] __schedule
               |
               --- __schedule
                   schedule
                  |          
                  |--79.85%-- schedule_hrtimeout_range_clock
                  |          schedule_hrtimeout_range
                  |          poll_schedule_timeout
                  |          do_select
                  |          core_sys_select
                  |          sys_select
                  |          system_call_fastpath
                  |          __select
                  |          __libc_start_main
                  |          
                   --20.15%-- do_nanosleep
                             hrtimer_nanosleep
                             sys_nanosleep
                             system_call_fastpath
                             __GI___libc_nanosleep
                             __libc_start_main</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/perf">利用perf剖析Linux应用程序</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/perf/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用Eclipse Memory Analyzer分析JVM堆Dump</title>
		<link>https://blog.gmem.cc/jvm-dump-analysis-with-mat</link>
		<comments>https://blog.gmem.cc/jvm-dump-analysis-with-mat#comments</comments>
		<pubDate>Mon, 05 Mar 2018 05:15:38 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[JVM]]></category>
		<category><![CDATA[性能剖析]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=19351</guid>
		<description><![CDATA[<p>简介 Eclipse Memory Analyzer（MAT）是一个图形化的Java堆分析工具，速度快且特性丰富，可以用于取代JDK自带的堆Dump浏览器（jhat）。使用MAT，你可以快速分析包含上亿对象的生产环境Dump文件，快速计算某种对象导致的内存占用量，进而评估内存泄漏风险。 你可以将MAT安装为独立运行的RCP应用，也可以作为插件安装到现有的Eclipse中。 jhat jhat不是本文的主题，这里做一个简单的介绍。 简介 这是JVM自带的堆Dump分析工具，执行如下命令即可使用： [crayon-69e29c566225c608289704/] jhat默认在7000端口暴露HTTP服务，你可以通过浏览器进行以下查询： 查询 说明 All classes including platform 显示堆中所有类型（Java类）的列表，这些类被加载到JVM中 All Classes excluding platform 类似上面，但是不包括平台（即JDK内部）的类 <a class="read-more" href="https://blog.gmem.cc/jvm-dump-analysis-with-mat">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/jvm-dump-analysis-with-mat">使用Eclipse Memory Analyzer分析JVM堆Dump</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p><a href="https://www.eclipse.org/mat/">Eclipse Memory Analyzer</a>（MAT）是一个图形化的Java堆分析工具，速度快且特性丰富，可以用于取代JDK自带的堆Dump浏览器（jhat）。使用MAT，你可以快速分析包含上亿对象的生产环境Dump文件，快速计算某种对象导致的内存占用量，进而评估内存泄漏风险。</p>
<p>你可以将MAT安装为独立运行的RCP应用，也可以作为插件安装到现有的Eclipse中。</p>
<div class="blog_h1"><span class="graybg">jhat</span></div>
<p>jhat不是本文的主题，这里做一个简单的介绍。</p>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>这是JVM自带的堆Dump分析工具，执行如下命令即可使用：</p>
<pre class="crayon-plain-tag">jhat a.map  # a.map为Dump文件路径</pre>
<p>jhat默认在7000端口暴露HTTP服务，你可以通过浏览器进行以下查询：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">查询</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>All classes including platform</td>
<td>显示堆中所有类型（Java类）的列表，这些类被加载到JVM中</td>
</tr>
<tr>
<td>All Classes excluding platform</td>
<td>类似上面，但是不包括平台（即JDK内部）的类</td>
</tr>
<tr>
<td>Show all members of the rootset</td>
<td>
<p>显示所有GC Root的对外引用，按照以下几类GC Root，分段展示：</p>
<ol>
<li>Java类的静态成员</li>
<li>Java局部变量（显示持有这些本地变量的线程）</li>
<li>Native全局变量</li>
<li>Native局部变量</li>
</ol>
</td>
</tr>
<tr>
<td>Show instance counts for all classes (including platform)</td>
<td>显示所有类型的实例计数（包含平台类）</td>
</tr>
<tr>
<td>Show instance counts for all classes (excluding platform)</td>
<td>显示所有类型的实例计数（不包含平台类）</td>
</tr>
<tr>
<td>Show heap histogram</td>
<td>
<p>显示一个表格，统计各类型的实例数量、内存占用，按内存占用降序排列</p>
<p>内存占用显示为Shallow size，即对象自身的内存占用。除了数组、字符串等之外的对象Shallow size通常很小，通过引用（指针）关联的其它对象不计入Shallow size，仅仅计入固定尺寸的指针大小</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">示例应用</span></div>
<div class="blog_h3"><span class="graybg">代码</span></div>
<p>下面是一段急剧消耗内存的示例代码：</p>
<pre class="crayon-plain-tag">package com.dangdang.digital.test;

import org.apache.commons.lang.math.RandomUtils;

import java.util.concurrent.TimeUnit;

public class BusyApplication {

    public static void main( String[] args ) {
        new MemoryPredator( Integer.parseInt( args[0] ), Integer.parseInt( args[1] ) ).start();
    }

}

class MemoryPredator extends Thread {

    private final int consumeInterval;

    private int retainTime;

    public MemoryPredator( int consumeInterval, int retainTime ) {
        this.consumeInterval = consumeInterval;
        this.retainTime = retainTime;
    }

    @Override
    public void run() {
        for ( ; ; ) {
            consumeMemory( retainTime );
            try {
                TimeUnit.MILLISECONDS.sleep( consumeInterval );
            } catch ( InterruptedException e ) {
            }
        }
    }

    private void consumeMemory( final int retainTime ) {
        new MemoryPreyThread( retainTime ).start();
    }

}

class MemoryPreyThread extends Thread {

    private final int retainTime;

    public MemoryPreyThread( int retainTime ) {
        this.retainTime = retainTime;
    }

    @Override
    public void run() {
        int n = ( RandomUtils.nextInt( 100 ) + 1 );
        Object bigObj = createBigObject( n );
        System.out.println( String.format( "%d MB consumed", n ) );
        try {
            TimeUnit.MILLISECONDS.sleep( retainTime );
        } catch ( InterruptedException e ) {
        }
    }

    private Object createBigObject( int n ) {
        Object big = new byte[n * 1024 * 1024];
        return big;
    }
}</pre>
<p>让上述程序执行一段时间（应用程序参数使用500 7000），然后使用下面的命令获取堆Dump：</p>
<pre class="crayon-plain-tag"># 只Dump可达对象，使用二进制格式，输出为bzapp.hprof
jmap -dump:live,format=b,file=bzapp.hprof  `jps | grep BusyApplication | cut -d' ' -f1`</pre>
<div class="blog_h3"><span class="graybg">分析</span></div>
<p>通过jhat开启服务后，打开浏览器，首先看到的是堆中包含的<span style="background-color: #c0c0c0;">类的列表</span>（不包含平台类）：</p>
<pre class="crayon-plain-tag">All Classes (excluding platform)

Package com.dangdang.digital.test
class com.dangdang.digital.test.BusyApplication [0xc0005048]
class com.dangdang.digital.test.MemoryPredator [0xc0004ff0]
class com.dangdang.digital.test.MemoryPreyThread [0xc0004f98]

Package com.jarego.jayatana
class com.jarego.jayatana.Agent [0xc0005200]

Package org.apache.commons.lang.math
class org.apache.commons.lang.math.JVMRandom [0xc0004e60]
class org.apache.commons.lang.math.RandomUtils [0xc0004f00]</pre>
<p>点击界面下方的Show heap histogram连接，可以看到Shallow size最大的类型：</p>
<pre class="crayon-plain-tag">Class                                    Instance Count        Total Size

class [B                                 1063                  788662183
class [I                                 512                   6789172
class [C                                 2552                  349224
class java.lang.Class                    566                   54336
class [Ljava.lang.Object;                467                   48496
class [S                                 770                   44518
class java.lang.String                   2494                  39904
class java.net.URL                       346                   30448
class [Ljava.util.WeakHashMap$Entry;     156                   22464
class java.util.HashMap$Entry            771                   21588</pre>
<p>可以看到byte[]类型，即[B，占用绝大部分内存，接近800MB。</p>
<p>点击class [B上的链接，进入byte[]<span style="background-color: #c0c0c0;">类型的主页面</span>“Class 0xc009a858”，后面的是类对象的内存地址，和Dump分析关系不大。留意“References to this object”这一段内容：</p>
<pre class="crayon-plain-tag">[B@0xbb0b6e18 (240 bytes) : ??
[B@0xc00bad20 (30 bytes) : ??
[B@0xbae503c0 (29 bytes) : ??
[B@0xbb091708 (115 bytes) : ??
[B@0xbb057350 (29 bytes) : ??

... 

[B@0xc87c6368 (15728656 bytes) : ??       # 大对象</pre>
<p>任何类型的示例，都会持有该类型的Class对象的引用，byte[]也不例外。所有存活的byte[]对象因此列于“References to this object”中。我们需要关心的通常（取决于应用场景）是大对象，简单的滚动页面，留意那些更长的行，就可以定位到大的byte[]了。</p>
<p>可以看到对象[B@0xc87c6368占据了15M的内存空间，点击其上的链接，进入[B@0xc87c6368<span style="background-color: #c0c0c0;">对象的主页面</span>。在页面的下方，可以看到Other Queries - Reference Chains from Rootset - Exclude weak refs，点击后，可以查看从GC Root到此对象的<span style="background-color: #c0c0c0;">引用链页面</span>：</p>
<pre class="crayon-plain-tag">References to [B@0xc87c6368 (15728656 bytes)

Java Local References

Java Local Reference (from com.dangdang.digital.test.MemoryPreyThread@0xc4ec6208) : # 这是GCRoot

--&gt; [B@0xc87c6368 (15728656 bytes)</pre>
<p>至此，我们可以大概判定，引发内存问题的代码，位于类MemoryPreyThread中。</p>
<div class="blog_h1"><span class="graybg">MAT界面</span></div>
<div class="blog_h2"><span class="graybg">首页</span></div>
<p>点击菜单项 File ⇨  Open Heap Dump ...，看到如下界面：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/03/mat-ui.png"><img class="aligncenter  wp-image-19415" src="https://blog.gmem.cc/wp-content/uploads/2018/03/mat-ui.png" alt="mat-ui" width="917" height="649" /></a></p>
<div class="blog_h3"><span class="graybg">Inspector面板</span></div>
<p>上半部分显示当前正在分析的对象的基本信息，以上面的截图为例：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;"><span style="font-family: Ubuntu, Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="font-weight: normal; line-height: 19px;">值 </span></span></td>
<td style="text-align: center;"><span style="font-family: Ubuntu, Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="font-weight: normal; line-height: 19px;">意义 </span></span></td>
</tr>
</thead>
<tbody>
<tr>
<td><span style="font-family: Ubuntu, Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="line-height: 19px;">0xcb4c69f8<br /></span></span></td>
<td><span style="font-family: Ubuntu, Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="line-height: 19px;">对象的地址</span></span></td>
</tr>
<tr>
<td><span style="font-family: Ubuntu, Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="line-height: 19px;">MemoryPreyThread</span></span></td>
<td><span style="font-family: Ubuntu, Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="line-height: 19px;">对象的类型</span></span></td>
</tr>
<tr>
<td><span style="font-family: Ubuntu, Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="line-height: 19px;">com.dangdang.digital.test</span></span></td>
<td><span style="font-family: Ubuntu, Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="line-height: 19px;">对象的包名</span></span></td>
</tr>
<tr>
<td><span style="font-family: Ubuntu, Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="line-height: 19px;">class com.dangdang.digital.test.MemoryPreyThread @ 0xc0004f98<br /></span></span></td>
<td><span style="font-family: Ubuntu, Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="line-height: 19px;">对象的类型，以及类对象的地址</span></span></td>
</tr>
<tr>
<td><span style="font-family: Ubuntu, Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="line-height: 19px;">java.lang.Thread</span></span></td>
<td><span style="font-family: Ubuntu, Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="line-height: 19px;">对象的父类型</span></span></td>
</tr>
<tr>
<td>sun.misc.Launcher$AppClassLoader @ 0xc0000bd8</td>
<td>加载对象的类的ClassLoader</td>
</tr>
<tr>
<td>112(shadlow size)</td>
<td>此对象本身的大小</td>
</tr>
<tr>
<td>104,857,784(retained size)</td>
<td>此对象本身的大小 + 此对象引用（包括间接）对象的大小</td>
</tr>
<tr>
<td>GC root: Native Stack,Thread</td>
<td>提示此对象是否GC Root</td>
</tr>
</tbody>
</table>
<p>下半部分包括4个选项卡：</p>
<ol>
<li>Statics：此对象的类变量列表</li>
<li>Attributes：此对象的实例变量列表</li>
<li>Class Hierarchy：类层次图</li>
<li>Value：对象的值</li>
</ol>
<div class="blog_h3"><span class="graybg">Overview面板</span></div>
<p>此面板使用饼图显示Retained Size最大的对象，并列出其导致的内存驻留的尺寸。这些对象往往就是问题所在。</p>
<p>饼图上方的按钮区、和饼图下方的Actions/Reports/Step By Step，功能是对应的。</p>
<p>其中Actions包括：</p>
<ol>
<li>Histogram：类似jhat的Histogram类似，但是支持统计Retained Size，支持排序</li>
<li>Dominator Tree：显示那些导致内存驻留最严重的对象，按对象导致驻留的比例，降序排列。可以展开这些对象的引用链</li>
<li>Top Consumers：以饼图、列表方式显示导致内存驻留最严重的对象、类、类加载器、包</li>
<li>Duplicate Classes：显示不同类加载器重复加载的类</li>
</ol>
<p>其中Reports包括：</p>
<ol>
<li>Leak Suspects：可以的内存泄漏报告，更加细粒度的、辅助人工分析的报告</li>
</ol>
<div class="blog_h1"><span class="graybg">使用MAT分析示例应用</span></div>
<p>使用jhat，我们就可以获取堆Dump中的全部信息了。 但是其UI非常简陋，缺乏排序、统计类功能，这些功能正是MAT能提供的。下面我们使用MAT来分析上一章的示例应用。</p>
<p>打开Dump后，在Overview面板的饼图上，可以立即看到所有大内存驻留都是由com.dangdang.digital.test.MemoryPreyThread类型的对象所引发。</p>
<p>单击任何一个饼图切片，点击菜单List Objects ⇨ With Outgoing References，可以看到对象引用的哪些对象的Shallow Size较大。这些对象就是内存驻留的根源：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/03/mat-list-object.png"><img class="aligncenter size-full wp-image-19427" src="https://blog.gmem.cc/wp-content/uploads/2018/03/mat-list-object.png" alt="mat-list-object" width="857" height="364" /></a></p>
<p>很明显， MemoryPreyThread所引用的、byte[]类型的局部变量，其Shallow Size占据了全部内存份额。注意：如果<span style="background-color: #c0c0c0;">上级对象的Outgoing引用特别多，则问题所在的子级对象可能看不到，这种情况下可以点击Retained Heap列排序或者手工展开</span>。</p>
<p>对于像示例应用这样简单的代码，分析到这里就直接能定位到问题代码所在行号了。生产环境中的情况要复杂的多，单纯基于堆内存Dump进行静态分析不一定能奏效，需要结合线程栈Dump，甚至是剖析工具（JMC、JProfiler等）录制的内存分配历史。</p>
<div class="blog_h1"><span class="graybg">实例</span></div>
<div class="blog_h2"><span class="graybg">单个线程消耗6G内存</span></div>
<p>概要情况如下图：</p>
<p><img class="aligncenter size-full wp-image-20035" src="https://blog.gmem.cc/wp-content/uploads/2018/03/mat-5g-1.png" alt="mat-5g-1" width="849" height="354" /></p>
<p>某个线程引用了接近6G的数据。从线程名字上来看，它是一个普通的工作线程，持有的6G内存应该是某种临时性对象。</p>
<p>分析此线程的Outgoing引用，排序Retained Heap列，将大内存条目显示出来：</p>
<p>可以看到，内存由一个局部变量（&lt;Java Local&gt;）导致。需要注意，<span style="background-color: #c0c0c0;">局部变量的GC Root都是当前执行线程</span>。出现问题的变量引用一个ArrayList对象，进一步展开可以猜测到此对象是数据库查询的结果集：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/03/mat-5g-2.png"><img class="aligncenter size-full wp-image-20037" src="https://blog.gmem.cc/wp-content/uploads/2018/03/mat-5g-2.png" alt="mat-5g-2" width="855" height="789" /></a></p>
<p>可以看到，此ArrayList的元素多达1000万。很可能是错误的SQL语句把大量行甚至整个表都读取过来，一下子把JVM内存撑爆了。</p>
<p>展开一个ByteArrayRow，查看行中的元素，进一步定位到所操作的表。配合JMC查看Hot Methods，定位到了存在问题的方法。</p>
<div class="blog_h2"><span class="graybg">Dubbo消费者内存溢出</span></div>
<div class="blog_h3"><span class="graybg">概要情况</span></div>
<p>线上环境某服务，因为内存溢出、连续FGC被监控进程杀死，我们获得的堆Dump，内存占用总计1.7GB，Netty 3.x中的类NioClientSocketPipelineSink导致了绝大部分的内存驻留。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/03/nio-client-socket-pipeline-sink.png"><img class="aligncenter size-full wp-image-19697" src="https://blog.gmem.cc/wp-content/uploads/2018/03/nio-client-socket-pipeline-sink.png" alt="nio-client-socket-pipeline-sink" width="852" height="420" /></a></p>
<div class="blog_h3"><span class="graybg">IncomingRef分析</span></div>
<p>点击饼图中最大的切片NioClientSocketPipelineSink进行分析，打开菜单项List Objects ⇨ With Incoming References，展开：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/03/nio-client-socket-pipeline-sink-ir.png"><img class="aligncenter size-full wp-image-19703" src="https://blog.gmem.cc/wp-content/uploads/2018/03/nio-client-socket-pipeline-sink-ir.png" alt="nio-client-socket-pipeline-sink-ir" width="858" height="814" /></a></p>
<p>可以看到NioClientSocketPipelineSink是Netty处理管线（Pipeline）的一部分。在很多框架中，管线常常和SRC、SINK等术语一起出现，SRC常常表示管线的输入、SINK则表示输出。</p>
<p>一个全局性的ChannelFactory、<span style="background-color: #c0c0c0;">大量DefaultChannelPipeline</span>通过sink属性引用了NioClientSocketPipelineSink。这些DefaultChannelPipeline应该是某种一次性对象，不熟悉代码的话只能猜测到这个程度，可以考虑通过JMC来录制对象分配的调用栈，来证实这种猜测。</p>
<p>那么，谁创建了这些DefaultChannelPipeline呢？从上面的截图中可以看到Dubbo的NettyClient间接引用了NioClientSocketPipelineSink，再随机抽取几个DefaultChannelPipeline展开：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/03/nio-client-socket-pipeline-sink-ir2.png"><img class="aligncenter size-full wp-image-19707" src="https://blog.gmem.cc/wp-content/uploads/2018/03/nio-client-socket-pipeline-sink-ir2.png" alt="nio-client-socket-pipeline-sink-ir2" width="861" height="819" /></a></p>
<p>可以看到来自Dubbo的NettyClient通过channel ⇨ pipeline ⇨ sink 引用了DefaultChannelPipeline。com.alibaba.dubbo上面引用链上唯一的非Netty包，我们有理由猜测Dubbo和内存溢出有关。</p>
<p>我们可以看一下相关对象的计数。对于希望统计数量的对象，在调用链上点选它，左下面板切换到Class Hierarchy，在类名上右击，选择List Object  ⇨ With Incoming References：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/03/incoming-ref-of-class.png"><img class="aligncenter size-full wp-image-19721" src="https://blog.gmem.cc/wp-content/uploads/2018/03/incoming-ref-of-class.png" alt="incoming-ref-of-class" width="565" height="374" /></a></p>
<p>&nbsp;</p>
<p>由于类的每个对象都会引用类对象，因此展开右侧窗口的根节点，即可知晓对象的大概数量。在本例中，DefaultChannelPipeline、NioClientSocketChannel的数量大概是130万，NettyClient的数量是33。</p>
<div class="blog_h3"><span class="graybg">DominatorTree分析</span></div>
<p>MAT的支配树功能，可以帮助你追踪实际（Shallow）消耗内存的对象：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/03/pipelinesink-dominator-tree.png"><img class="aligncenter  wp-image-19713" src="https://blog.gmem.cc/wp-content/uploads/2018/03/pipelinesink-dominator-tree.png" alt="pipelinesink-dominator-tree" width="911" height="635" /></a></p>
<p>可以看到，Netty内部的某种链表结构（LinkedTransferQueue）的每个元素的RegisterTask对象占用了1KB的内存，但是链表长度非常大，因此这个链表消耗了大量的内存。</p>
<p>从名字LinkedTransferQueue可以猜测，这是Netty用来存储待处理的网络数据的。</p>
<div class="blog_h3"><span class="graybg">代码分析</span></div>
<p>从MAT分析的结果来看，NioClientSocketPipelineSink的内部类Boss的实例变量registerTaskQueue，占据了高达1.64GB的内存。我们来看一下这个类是做什么的：</p>
<pre class="crayon-plain-tag">// 代码中不相关的部分做了删减
class NioClientSocketPipelineSink extends AbstractChannelSink {

    final Executor bossExecutor;

    private final Boss[] bosses;
    private final NioWorker[] workers;

    private final AtomicInteger bossIndex = new AtomicInteger();
    private final AtomicInteger workerIndex = new AtomicInteger();

    // 这是外部能影响队列registerTaskQueue的唯一入口
    public void eventSunk( ChannelPipeline pipeline, ChannelEvent e) throws Exception {
        if (e instanceof ChannelStateEvent) {

            ChannelState state = event.getState();
            Object value = event.getValue();

            switch (state) {
            case CONNECTED:
                if (value != null) {
                    // 连接到客户端通道
                    connect(channel, future, (SocketAddress) value);
                } else {
                    channel.worker.close(channel, future);
                }
                break;
        } else if (e instanceof MessageEvent) {}
    }


    private void connect(  final NioClientSocketChannel channel, final ChannelFuture cf, SocketAddress remoteAddress) {
        try {
            if (channel.socket.connect(remoteAddress)) {
                channel.worker.register(channel, cf);
            } else {
                // 通道是非阻塞模式，因此会走到这个分支
                nextBoss().register(channel);
            }

        } catch (Throwable t) {}
    }
    Boss nextBoss() {
        return bosses[Math.abs( bossIndex.getAndIncrement() % bosses.length)];
    }


    private final class Boss implements Runnable {

        private final AtomicBoolean wakenUp = new AtomicBoolean();
        private final Object startStopLock = new Object();
        private final Queue&lt;Runnable&gt; registerTaskQueue = new LinkedTransferQueue&lt;Runnable&gt;();

        // 这个方法直接导致registerTaskQueue增大
        void register(NioClientSocketChannel channel) {
            Runnable registerTask = new RegisterTask(this, channel);

            synchronized (startStopLock) {
                // 如果尚未启动，则使用Executor启动Client Boss线程
                // 每当有新的连接请求到达，都会尝试启动Client Boss线程
                if (!started) {
                    DeadLockProofWorker.start(
                                bossExecutor,
                                new ThreadRenamingRunnable(
                                        this, "New I/O client boss #" + id + '-' + subId));
                }
                // 添加一个RegisterTask
                boolean offered = registerTaskQueue.offer(registerTask);
            }

            if (wakenUp.compareAndSet(false, true)) {
                // 唤醒选择器
                selector.wakeup();
            }
        }

        public void run() {
            Selector selector = this.selector;
            long lastConnectTimeoutCheckTimeNanos = System.nanoTime();
            for (;;) {
                wakenUp.set(false);

                try {
                    // 最多阻塞500ms
                    int selectedKeyCount = selector.select(500);
                    if (wakenUp.get()) {
                        selector.wakeup();
                    }
                    // 消费registerTaskQueue队列
                    processRegisterTaskQueue();

                    
                    if (selector.keys().isEmpty()) {
                        if (shutdown ||
                            bossExecutor instanceof ExecutorService &amp;&amp; ((ExecutorService) bossExecutor).isShutdown()) {

                            synchronized (startStopLock) {
                                if (registerTaskQueue.isEmpty() &amp;&amp; selector.keys().isEmpty()) {
                                    // 标记Client Boss线程为停止状态，下次再有连接请求到达，会再次启动
                                    started = false;
                                    try {
                                        selector.close();
                                    } catch (IOException e) {
                                        logger.warn(
                                                "Failed to close a selector.", e);
                                    } finally {
                                        this.selector = null;
                                    }
                                    // 退出条件
                                    break;
                                } else {
                                    shutdown = false;
                                }
                            }
                        } else {
                            shutdown = true;
                        }
                    } else {
                        shutdown = false;
                    }

                } catch (Throwable t) {}
            }
        }

        private void processRegisterTaskQueue() {
            for (;;) {
                final Runnable task = registerTaskQueue.poll();
                if (task == null) {
                    break;
                }

                task.run();
            }
        }

    private static final class RegisterTask implements Runnable {

        RegisterTask(Boss boss, NioClientSocketChannel channel) {
            this.boss = boss;
            this.channel = channel;
        }

        public void run() {
            try {
                // 在指定的选择器上注册通道的事件
                channel.socket.register( boss.selector, SelectionKey.OP_CONNECT, channel);
            } catch (ClosedChannelException e) {
                channel.worker.close(channel, succeededFuture(channel));
            }

            int connectTimeout = channel.getConfig().getConnectTimeoutMillis();
            if (connectTimeout &gt; 0) {
                channel.connectDeadlineNanos = System.nanoTime() + connectTimeout * 1000000L;
            }
        }
    }
}</pre>
<p>可以看到：</p>
<ol>
<li>每当有新的连接请求时，都会调用Boss.register，从而唤醒Boss.run线程</li>
<li>一旦Boss.run线程线程启动，它就会消费掉所有堆积的registerTaskQueue元素</li>
</ol>
<p>也就是说，正常情况下，registerTaskQueue队列不应该出现堆积的。由于发现问题时没有开启Dubbo或Netty的日志，准备等待下次问题重现后继续深入分析。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/jvm-dump-analysis-with-mat">使用Eclipse Memory Analyzer分析JVM堆Dump</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/jvm-dump-analysis-with-mat/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用Chrome开发者工具分析内存泄漏</title>
		<link>https://blog.gmem.cc/detect-memleaks-with-devtools</link>
		<comments>https://blog.gmem.cc/detect-memleaks-with-devtools#comments</comments>
		<pubDate>Mon, 25 Jul 2016 05:28:08 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Web]]></category>
		<category><![CDATA[性能剖析]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=12398</guid>
		<description><![CDATA[<p>基础知识 内存问题及其表现 从用户角度来看，Web应用内存问题可以表现为以下几种形式： 问题形式 症状 内存泄漏 页面的性能随着运行时间的增加越来越差，这是因为页面消耗越来越多的内存 内存消耗量和性能呈负相关的原因包括： 内存消耗量越大，GC运行越频繁、耗时。而GC运行会导致JS引擎暂停执行 内存消耗量越大，某些遍历性质的操作就越耗时 内存暴涨 页面性能一直很差，这是因为页面使用大量的内存，超过页面速度优化的限度 所谓“大量”并没有量化标准，主要取决于客户端的硬件资源 频繁GC 页面延迟或者卡顿。这时因为GC时脚本执行被暂停 这三种表现形式并不是孤立的。缓慢的内存泄漏最终会导致内存的大量使用，内存泄漏/暴涨会导致GC消耗更多的时间以及更频繁的运作。 本文主要探讨内存泄漏问题。 内存泄漏 所谓内存泄漏，是指应用程序不再需要的内存，由于某种原因没有归还给操作系统。 JavaScript是一种基于垃圾回收机制（GCed）的语言，垃圾回收器可以辅助内存的回收：它检查某一片内存是否从应用其它部分可达，以确定该内存是否可以被安全回收。GCed语言中的内存泄漏主要由多余引用（unwanted references）导致。所谓多余引用，是从活动GC Root直接/间接指向一片不再需要的内存的引用。 <a class="read-more" href="https://blog.gmem.cc/detect-memleaks-with-devtools">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/detect-memleaks-with-devtools">使用Chrome开发者工具分析内存泄漏</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">基础知识</span></div>
<div class="blog_h2"><span class="graybg">内存问题及其表现</span></div>
<p>从用户角度来看，Web应用内存问题可以表现为以下几种形式：</p>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 20%; text-align: center;">问题形式</td>
<td style="text-align: center;">症状</td>
</tr>
</thead>
<tbody>
<tr>
<td>内存泄漏</td>
<td>
<p>页面的性能随着运行时间的增加越来越差，这是因为页面消耗越来越多的内存</p>
<p>内存消耗量和性能呈负相关的原因包括：</p>
<ol>
<li>内存消耗量越大，GC运行越频繁、耗时。而GC运行会导致JS引擎暂停执行</li>
<li>内存消耗量越大，某些遍历性质的操作就越耗时</li>
</ol>
</td>
</tr>
<tr>
<td>内存暴涨</td>
<td>
<p>页面性能一直很差，这是因为页面使用大量的内存，超过页面速度优化的限度</p>
<p>所谓“大量”并没有量化标准，主要取决于客户端的硬件资源</p>
</td>
</tr>
<tr>
<td>频繁GC</td>
<td>页面延迟或者卡顿。这时因为GC时脚本执行被暂停</td>
</tr>
</tbody>
</table>
<p>这三种表现形式并不是孤立的。缓慢的内存泄漏最终会导致内存的大量使用，内存泄漏/暴涨会导致GC消耗更多的时间以及更频繁的运作。</p>
<p>本文主要探讨内存泄漏问题。</p>
<div class="blog_h2"><span class="graybg">内存泄漏</span></div>
<p>所谓内存泄漏，是指应用程序不再需要的内存，由于某种原因没有归还给操作系统。</p>
<p>JavaScript是一种基于垃圾回收机制（GCed）的语言，垃圾回收器可以辅助内存的回收：它检查<span style="background-color: #c0c0c0;">某一片内存是否从应用其它部分可达</span>，以确定该内存是否可以被安全回收。GCed语言中的内存泄漏主要由<span style="background-color: #c0c0c0;">多余引用（unwanted references）</span>导致。所谓多余引用，是<span style="background-color: #c0c0c0;">从活动GC Root直接/间接指向一片不再需要的内存的引用</span>。</p>
<p>尽管现代JS引擎能够在一些情况下自动执行垃圾回收，由于代码逻辑错误导致的内存泄漏仍然存在，特别是在单页面应用/Ajax成为主流的今天，<span style="background-color: #c0c0c0;">长时间运行的Web页面</span>往往因此渐渐变慢甚至崩溃。</p>
<div class="blog_h3"><span class="graybg">常见内存泄漏原因</span></div>
<p>“多余引用”在Web应用的场景下可以进一步的细分。下表列出常见的情形：</p>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 22%; text-align: center;">原因</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>错误的使用全局变量</td>
<td>
<p>在JavaScript中不通过<pre class="crayon-plain-tag">var</pre> 关键字声明，而直接使用的变量是全局变量，这类变量本质上是<pre class="crayon-plain-tag">window</pre> 对象的属性。</p>
<p>全局变量（指向的内存）直到页面刷新之前，不会自动回收。因此如果使用全局变量保存大量数据，务必在不再需要这些数据时，手工设置全局变量的值为<pre class="crayon-plain-tag">null</pre> ，解除引用</p>
</td>
</tr>
<tr>
<td>忘记清理定时器</td>
<td>
<p><pre class="crayon-plain-tag">setInterval()</pre> 函数经常被使用，但是忘记清理。考虑下面的代码：</p>
<pre class="crayon-plain-tag">var resource = getData();
setInterval(function() {
    var node = document.getElementById('node');
    // 某个时间点之后node可能不再存在，因此该定时器什么也不做
    if(node) node.innerHTML = JSON.stringify(someResource));
}, 1000);
// 但是，定时器函数没有被销毁，而它引用resource这个数据（可能是一大片内存），这就造成了潜在的内存泄漏

// 因此，应当记住，定时器不需要的时候，手工销毁它
var handle = setInterval(NoOp);
clearInterval(handle); //执行清理</pre>
</td>
</tr>
<tr>
<td>忘记清理回调函数</td>
<td>
<p>某些JS库则提供基于回调的模式实现，例如观察者模式
<p>你应当在不需要时，明确的移除观察者（Listener函数），实例代码如下：</p>
<pre class="crayon-plain-tag">var e = document.getElementById( 'button' );
function clickHandler( event ) {
    e.innerHtml = 'text';
}
e.addEventListener( 'click', clickHandler );

// 先移除观察者
e.removeEventListener( 'click', clickHandler );
// 然后再移除DOM节点
e.parentNode.removeChild( e );</pre>
<p>对于某些<span style="background-color: #c0c0c0;">不能管理循环引用的浏览器</span>（例如IE6，它不能检测DOM节点和JS代码之间的循环引用）来说，明确移除步骤非常重要。<span style="background-color: #c0c0c0;">现代浏览器一般都能在被观察对象不可达时自动清理对应的观察者</span>，尽管如此，明确移除观察者仍然是很好的实践</p>
<p>JS库，例如jQuery，会在移除DOM节点时自动完成上述“明确移除”</p>
</td>
</tr>
<tr>
<td>忘记清理DOM节点的引用</td>
<td>
<p>为了操作方便，有时候你会<span style="background-color: #c0c0c0;">在数据结构中引用DOM节点</span>。 这会导致DOM节点有两个in引用，一个来自DOM树，一个来自你的数据结构。当DOM节点不再需要时，你需要确保这两个in引用都不可达</p>
<p>特别需要注意的是：子DOM节点不可回收的时候，其祖先节点亦不可回收</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">Chrome任务管理器<br /></span></div>
<p>按快捷键Shift + ESC，可以打开Chrome任务管理器（Task Manager）。通过持续的观察任务管理器，你可以查看每个页面的内存用量的变化，进而获得可能的内存泄漏提示。常用输出列如下：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">列</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Task</td>
<td>显示浏览器主进程，或者扩展、页面、插件的名称</td>
</tr>
<tr>
<td>Memory</td>
<td>
<p>Native内存用量，DOM节点存放在Native内存中，如果此列持续增加，提示存在无法释放的DOM节点</p>
<p>只有DOM节点从页面<span style="background-color: #c0c0c0;">DOM树中移除</span>，同时<span style="background-color: #c0c0c0;">没有JS变量引用之</span>，DOM节点才能够被释放。已经从DOM中移除，却仍然被JS代码引用的DOM节点，称为<span style="background-color: #c0c0c0;">Detached节点</span>，这种节点是引发内存泄漏的常见原因</p>
<p>注意：Chrome开发者工具会导致<span style="background-color: #c0c0c0;">被调试页面占用的Native内存大量增加</span>。在启用堆时间线记录时，甚至可以增大数倍。关闭开发者工具增大的内存会自动释放</p>
</td>
</tr>
<tr>
<td>JavaScript Memory</td>
<td>
<p>JS堆内存用量，该列显示两个值，其中括号中的值（live）指示可达对象占用的内存。如果live值持续增加，提示存在无法释放的JS对象</p>
<p>结合Windows任务管理器观察，发现Windows任务管理器中chrome.exe进程的内存（私有工作集）总是和上面的Memory相同。因此<span style="background-color: #c0c0c0;">JavaScript Memory应该是Memory的一部分，Memory是页面占用的总内存</span></p>
</td>
</tr>
<tr>
<td>CPU</td>
<td>页面的CPU占用率</td>
</tr>
<tr>
<td>Network</td>
<td>页面的网络通信速率</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">使用DevTools定位内存泄漏</span></div>
<p>Chrome任务管理器只能给出一些粗略的提示。实际工作中我们通常使用Chrome Developer Tools来发现并定位内存泄漏。Dev Tools由9个面板组成，其中Timeline、Profiles有助于内存分析。</p>
<div class="blog_h2"><span class="graybg">Timeline面板</span></div>
<p>通过该面板可以直观的看到不正常的内存使用，你可以使用它获得对问题的初步认识。</p>
<p>切换到Timeline面板，然后勾选Memory，点击<img class="aligncenter size-full wp-image-12426 inlineBlock" src="https://blog.gmem.cc/wp-content/uploads/2016/07/Selection_003.png" alt="Selection_003" width="15" height="14" />可以启动时间线记录，点击弹窗的Finish按钮则停止记录。注意，在启动、停止前，你应该点击<img class="aligncenter size-full wp-image-12427 inlineBlock" src="https://blog.gmem.cc/wp-content/uploads/2016/07/collect-garbage.png" alt="collect-garbage" width="18" height="20" />执行强制垃圾回收。时间线记录示例如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2016/07/dev-tools-timeline.jpg"><img class="aligncenter wp-image-12410 size-large" src="https://blog.gmem.cc/wp-content/uploads/2016/07/dev-tools-timeline-1024x756.jpg" alt="dev-tools-timeline" width="100%" /></a></p>
<p>在这个示例中，随着时间的推进，周期性的内存用量跳涨而之后不能收缩到原有水平、Nodes、Listeners计数的持续增加，这些都是明显的内存泄漏信号。</p>
<div class="blog_h2"><span class="graybg">Profiles面板</span></div>
<p>该面板是定位内存泄漏的关键，使用它你可以：</p>
<ol>
<li>获取内存的快照，分析JS对象、DOM节点如何占用内存。支持比对两个快照，发现内存使用情况的变化</li>
<li>录制内存分配随时间的变化，可用于隔离出内存泄漏。</li>
</ol>
<div class="blog_h3"><span class="graybg">堆快照剖析</span></div>
<p>切换到Profiles面板，点选Take Heap Snapsot，然后点击Task Snapshot按钮，即可获得一张实时的堆快照。注意：<span style="background-color: #c0c0c0;">获取快照前，会自动执行垃圾回收</span>。快照示例如下：</p>
<p><img class="aligncenter size-large wp-image-12415" src="https://blog.gmem.cc/wp-content/uploads/2016/07/dev-tools-profile-heapsnapshot-1024x429.png" alt="dev-tools-profile-heapsnapshot" width="100%" /></p>
<p>点击左上角的下拉菜单，可以在几个视图之间切换：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">视图</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Summary</td>
<td>
<p>在上半部分的<span style="background-color: #c0c0c0;">对象列表子面板</span>中，显示<span style="background-color: #c0c0c0;">获取快照的那一时刻</span>，各类型对象总体分配情况，数据项包括：</p>
<ol>
<li>Constructor：显示对象类型（以其构造函数名称表示） ⇨ 对象实例 ⇨ 对象属性...树列出当前驻留对象。如果一个条目的<span style="background-color: #c0c0c0;">背景为黄色，表示它可以直接通过JavaScript代码访问到。</span>闭包引用的对象，可能是无法直接访问的，因此不会显示黄色背景</li>
<li>Distance：距离GC Root的距离</li>
<li>Objects Count：此类对象的数量</li>
<li>Shallow Size：此类对象的总大小，注意对象大小是指变量引用的目标内存的大小。通常情况下只有数组、字符串可能有很大的Shallow Size，字符串常常会将其主体内容存放在Renderer内存，仅仅在JS Heap中暴露一个很小的Wrapper</li>
<li>Retained Size：Shallow Size的总大小 + 因为此类对象而驻留（因为<span style="background-color: #c0c0c0;">被此类对象引用，无法释放</span>）的其它对象的总大小。对于没有out引用的对象，例如一个数字或者字符串类型的对象属性，其Shallow Size = Retained Size</li>
</ol>
<p>你可以点击一个<span style="background-color: #c0c0c0;">对象类型</span>前面的小箭头（或者按Right方向键）展开，查看此类型<span style="background-color: #c0c0c0;">对象的实例</span>，注意实例后面会跟着灰色的<span style="color: #999999;">@id</span>，id是对象的唯一标识符。进一步展开，可以看到<span style="background-color: #c0c0c0;">对象实例的属性</span></p>
<p>选中一个对象的实例/对象实例属性，可以在下半部分的<span style="background-color: #c0c0c0;">Retainers子面板</span>中查看选中条目的引用链，这个链从直接引用选中条目的那个对象开始，逐层上溯</p>
<p>点选顶部工具栏All objects右侧的小箭头，你可以选择查看所有对象，或者在任意两个快照之间分配的对象</p>
</td>
</tr>
<tr>
<td>Comparison</td>
<td>对比两个快照，查看其分配内存大小、对象个数的比值</td>
</tr>
<tr>
<td>Containment</td>
<td>已一系列根对象为起点，显示其容纳对象的树</td>
</tr>
<tr>
<td>Statistics</td>
<td>使用饼图来展现各大类对象的内存用量比例</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">定位内存泄漏的流程</span></div>
<p>定位内存泄漏需要一些技巧，但是一般都是从快照的Summary视图开始分析。一个常用的工作流程是所谓<span style="background-color: #c0c0c0;">三快照（three snapshot）</span>技术：</p>
<ol>
<li>执行必要的预热操作，确保那些需要长期驻留的对象初始化完毕</li>
<li>获取一个快照</li>
<li>执行可能导致内存泄漏的操作</li>
<li>获取一个快照</li>
<li>再次执行你觉得可能导致内存泄漏的操作</li>
<li>再次获取一个快照</li>
<li>选择最后一个快照，在列表顶部选择：<img class="aligncenter size-full wp-image-12417" src="https://blog.gmem.cc/wp-content/uploads/2016/07/Selection_002.png" alt="Selection_002" width="315" height="90" /></li>
<li>这样，你就可以看到那些在快照1-快照2这个时间段内分配的，到目前为止仍然驻留在内存中的那些对象。这些对象可能是导致内存泄漏的对象</li>
<li>展开对象类型节点，选中一个对象，这时下面的Retainers面板会显示该对象的驻留树——即该对象是如何被其它对象引用的</li>
</ol>
<p>三快照技术有效的依据是：在应用经过预热（warm-up）阶段之后，分配的新对象一般都是由（定时器/用户）操作触发的、朝生暮死的对象。这些本该朝生暮死的对象如果一直驻留，意味着存在内存泄漏。</p>
<div class="blog_h3"><span class="graybg">堆分配时间线剖析</span></div>
<p>切换到Profiles面板，点选Record Heap Allocations，然后点击Start按钮，即可录制<span style="background-color: #c0c0c0;">内存分配沿着时间线的变化</span>情况——堆分配时间线（Heap Timelines）。点击<img class="aligncenter size-full wp-image-12469 inlineBlock" src="https://blog.gmem.cc/wp-content/uploads/2016/07/stop-recording.png" alt="stop-recording" width="20" height="19" />按钮可以停止录制，并生成一个快照。</p>
<p>注意：时间线对象列表子面板，仅仅包含在<span style="background-color: #c0c0c0;">时间线结尾时刻仍然存活</span>的对象。</p>
<p>堆分配时间线的优势在于，你可以任意选择大小的时间区间，查看区间内发生的内存分配，<span style="background-color: #c0c0c0;">有利于隔离出内存泄漏</span>。时间线示例如下：</p>
<p><img class="aligncenter size-full wp-image-12472" src="https://blog.gmem.cc/wp-content/uploads/2016/07/heap-timeline.png" alt="heap-timeline" width="100%" /></p>
<p>截图中，最上面的是时间轴，每一个蓝色/灰色的柱子表示一系列相关的内存分配。柱子的高度表示分配内存的大小，<span style="background-color: #c0c0c0;">蓝色部分</span>表示<span style="background-color: #c0c0c0;">到结束录制的那个时间点为止</span>，<span style="background-color: #c0c0c0;">仍然驻留</span>的内存大小，<span style="background-color: #c0c0c0;">灰色部分</span>表示<span style="background-color: #c0c0c0;">已经被回收</span>的内存大小。</p>
<p>中间的对象列表面板，显示选中的<span style="background-color: #c0c0c0;">时间区间内的内存分配</span>，按类型分组。展开一个类型节点，可以看到仍然存活的实例。</p>
<p>下面是Retainers面板，可以显示某个存活实例的引用链。例如上图中当前选中的字符串实例被window.arr[18]引用。</p>
<div class="blog_h3"><span class="graybg">对象类型说明</span></div>
<p>在对象列表子面板的Constructor列、Retainers子面板的Object列，都有以构造器表示的对象类型，这些类型的说明如下：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">对象类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>(closure)</td>
<td>闭包，这些闭包引用了上下文对象</td>
</tr>
<tr>
<td>(compiled code)</td>
<td>被Chrome编译过的JavaScript代码，引擎内部使用，我们无法控制</td>
</tr>
<tr>
<td>(array)</td>
<td><span style="background-color: #c0c0c0;">引擎内部使用</span>的数组对象</td>
</tr>
<tr>
<td>Array</td>
<td>JavaScript数组，这些数组常常包含大量的数据</td>
</tr>
<tr>
<td>Object</td>
<td>简单（plain old）JavaScript对象</td>
</tr>
<tr>
<td>system / Context</td>
<td>调用一个函数所需要的潜在对象，例如闭包使用的实际数据</td>
</tr>
<tr>
<td>system</td>
<td>引擎内部使用的数据</td>
</tr>
<tr>
<td>(shared function info)</td>
<td>引擎内部使用的函数共享信息</td>
</tr>
<tr>
<td>HTML***Element</td>
<td>一个瘦包装器，引用位于Native内存中的DOM对象</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">示例：Detached DOM树泄漏</span></div>
<p>这里我们举一个利用堆快照剖析DOM树泄漏的例子。考虑下面的代码：</p>
<pre class="crayon-plain-tag">&lt;button id="btn"&gt;Create&lt;/button&gt;
&lt;script type="text/javascript"&gt;
    var detachedTree;
    document.getElementById('btn').addEventListener('click', function () {
        var div = document.createElement('div');
        for (var i = 0; i &lt; 10; i++) {
            var span = document.createElement('span');
            div.appendChild(span);
        }
        detachedTree = div;
    });
&lt;/script&gt;</pre>
<p>点击Create按钮后，监听器会创建一个div元素，其内部包含10个span元素。由于div元素尚未添加到页面DOM树中且被全局变量detachedTree引用，因此它属于Detached节点。</p>
<p>我们在点击Create按钮后获取一个快照，在Summary视图的Class filter中搜索Detached，可以看到如下结果：<img class="aligncenter size-full wp-image-12456" src="https://blog.gmem.cc/wp-content/uploads/2016/07/Selection_001.png" alt="Selection_001" width="100%" /></p>
<p>在对象列表面板中，<span style="background-color: #c0c0c0;">黄色背景</span>表示Detached DOM子树的根节点，这类节点<span style="background-color: #c0c0c0;">被JavaScript代码直接引用</span>；红色背景（非直接引用）则表示Detached DOM子树的分支/叶子节点，没有被JavaScript代码直接引用。</p>
<p>在上图中点选一个对象，下面的Retainers面板会显示该对象的引用链：</p>
<p><img class="aligncenter size-large wp-image-12463" src="https://blog.gmem.cc/wp-content/uploads/2016/07/detached-tree-retainers-1024x347.png" alt="detached-tree-retainers" width="100%" /></p>
<p>可以看到，引用链<span style="background-color: #c0c0c0;">从直接引用对象的变量开始，逆向追溯</span>。引用链上的每个元素使用如下语法描述：</p>
<pre class="crayon-plain-tag"># name 可以是对象属性名称、native（表示该引用是本地代码）、[数组索引]
# ObjectType 指明对象的类型
# @id 指明对象的唯一标识
name1 in ObjectType1 @id1
  name2 in ObjectType2 @id2
  # ObjectType2类型的对象id2的name2属性指向ObjectType1类型的对象id1</pre>
<p> 对于上图左右两个引用链，应当这样解读：</p>
<ol>
<li>对于HTMLDivElement，window的detachedTree属性（就是我们声明的全局变量）直接引用之</li>
<li>对于HTMLSpanElement，window的detachedTree属性引用了HTMLDivElement ⇨ HTMLDivElement的native属性引用了DOM子树数组  ⇨ DOM子树数组的第1个元素引用了HTMLSpanElement</li>
</ol>
<p>在追踪DOM树泄漏时，可以不关注红色背景节点，而仅<span style="background-color: #c0c0c0;">追踪黄色背景节点被哪些JavaScript变量使用，然后清除多余引用</span>。</p>
<div class="blog_h1"><span class="graybg">实战：机房实景内存泄漏问题</span></div>
<div class="blog_h2"><span class="graybg">背景简介</span></div>
<p>“机房实景”是某系统中的一个模块，它以图形化的方式展示数据中心的各种设备：包括服务器、网络设备、监控设备。设备及其状态基于位图、矢量图、文本等形式展示。如果数据中心规模较大，可以为其设计多张实景图，并在这些实景图之间轮巡（即定期切换显示），以了解数据中心的网络和环境状态。</p>
<p>从技术角度来说，该模块以<a href="http://www.draw2d.org/draw2d/">Draw 2D Touch 5.3.4</a>为基础，而后者的底层框架则是<a href="http://raphaeljs.com/">Raphael</a>以及<a href="https://jquery.com/">jQuery</a>。</p>
<div class="blog_h2"><span class="graybg">故障现象</span></div>
<p>在测试阶段，我们发现长时间运行机房实景模块后，浏览器占用的内存不断上涨。特别是启用实景图轮询的情况下，数小时后内存占用超过1G，最终浏览器会崩溃。</p>
<div class="blog_h2"><span class="graybg">查看任务管理器</span></div>
<p>通过Chrome任务管理器查看：</p>
<ol>
<li>刚进入机房实景模块时：Native内存占用80MB，JS内存占用20MB</li>
<li>10分钟后，强制GC后：Native内存占用110MB，JS内存占用45MB</li>
<li>20分钟后，强制GC后：Native内存占用130MB，JS内存占用64MB</li>
</ol>
<p>这组数据说明机房实景模块很可能存在内存泄漏问题。</p>
<div class="blog_h2"><span class="graybg">查看堆分配时间线</span></div>
<p>重新进入机房实景模块，等待模块预热完毕（机房实景模块在启动时需要加载所有实景图的UI元素数据，并在内存中创建相应的数据结构），启用轮巡，在Profiles面板中启动堆时间线记录，持续十分钟，执行GC后停止记录，结果如下：</p>
<p><img class="aligncenter size-full wp-image-12487" src="https://blog.gmem.cc/wp-content/uploads/2016/07/Selection_0011.png" alt="Selection_001" width="100%" /></p>
<p>可以看到，大尺寸内存分配，周期非常稳定，这恰恰是轮巡切换实景图的周期。这些内存分配的大部分（蓝色）始终无法回收，说明实景图切换会导致相当严重的内存泄漏。为何实景图切换会导致内存泄漏？这很可能是大量界面元素的复杂引用关系没有被正确的清理，后面我们会证实这一点。</p>
<p>重新进入机房实景模块，等待模块预热完毕，禁用轮巡， 再次启动堆时间线记录，持续十分钟，执行GC后停止记录，结果如下：</p>
<p><img class="aligncenter size-full wp-image-12489" src="https://blog.gmem.cc/wp-content/uploads/2016/07/Selection_0021.png" alt="Selection_002" width="100%" /></p>
<p>可以看到，在禁用轮巡的情况下，只有2分钟左右存在一次可能的JavaScript内存泄漏，其它时间分配的内存都大多数被回收了（灰色）。</p>
<div class="blog_h2"><span class="graybg">禁用轮巡时的堆时间线分析</span></div>
<p>首先看看禁用轮巡时，那唯一高耸的蓝色柱子是怎么回事，缩小区间，查看对象列表面板：</p>
<p><img class="aligncenter size-full wp-image-12491" src="https://blog.gmem.cc/wp-content/uploads/2016/07/Selection_0031.png" alt="Selection_003" width="288" height="277" /></p>
<p>这个所谓的system类型，是浏览器内部行为导致的内存分配，和应用程序没有直接关系。因此我们可以认为它不是内存泄漏，并进一步推断：禁用轮巡时机房实景模块没有明显的JavaScript内存泄漏。</p>
<div class="blog_h2"><span class="graybg">启<span style="font-weight: bold;">用轮巡时的堆时间线分析</span></span></div>
<div class="blog_h3"><span class="graybg">概览</span></div>
<p>我们着重分析启用轮巡时那一系列蓝色柱子，这些柱子高度常常以M计，再多的系统内存也经不起这样的泄漏。</p>
<p>缩小区间，选择第一个柱子，设置对象列表面板按Retained Size降序排列，Retainers面板按Distance排列，结果如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2016/07/timeline-polling-enabled.png"><img class="aligncenter size-large wp-image-12506" src="https://blog.gmem.cc/wp-content/uploads/2016/07/timeline-polling-enabled.png" alt="timeline-polling-enabled" width="100%" /></a></p>
<p>这样排序的根据是：</p>
<ol>
<li>Retained Size大的对象，可能造成的<span style="background-color: #c0c0c0;">内存泄漏量也大</span></li>
<li>离目标对象距离一样的多个引用引用，其<span style="background-color: #c0c0c0;">Distance值越小</span>（即离GC Root越近）<span style="background-color: #c0c0c0;">越可能是内存泄漏的根源</span>。上图的cache、159两个引用前者更有可能导致了内存泄漏</li>
</ol>
<p>注意第一个驻留对象Object@1483747，其导致1870.752KB的内存无法释放；第二个对象Object@1483541，其导致1870.556KB内存无法释放。然后，所有Object总计才导致1873.092KB内存无法释放，这是怎么回事呢？</p>
<p>其实展开Object@1483747你就可以看到原因何在——Object@1483747的events字段恰恰引用了Object@1483541：</p>
<p><img class="aligncenter size-full wp-image-12513" src="https://blog.gmem.cc/wp-content/uploads/2016/07/retained-size-issue-1.png" alt="retained-size-issue-1" width="477" height="109" /></p>
<p>在任何时候，你都可以按Esc调出Console。选中<span style="background-color: #c0c0c0;">对象列表面板</span>或者<span style="background-color: #c0c0c0;">Retainers面板</span>中一条数据后，在Console中输入<pre class="crayon-plain-tag">$0</pre> 可以<span style="background-color: #c0c0c0;">打印该条数据对应的对象</span>（对于Retainers是in 后面的那个的对象，而非in前面属性名所指向的对象）的详细信息。你也可以悬停鼠标在一条数据上，相应对象的详细信息也会自动显示。</p>
<div class="blog_h3"><span class="graybg">分析Object@8364337</span></div>
<p>我们选取Retained Size较大的一个Object进行分析，显示其详细信息：</p>
<p><img class="aligncenter size-full wp-image-12519" src="https://blog.gmem.cc/wp-content/uploads/2016/07/Object@8364337.png" alt="Object@8364337" width="100%" /></p>
<p>可以看到这是一个简单对象，有events、handle两个属性。你会注意到events属性的Retained Size远远大于events的子属性Retained之和，这一现象我也没弄清楚是怎么回事。</p>
<p>查看Retainers面板，可以发现jQuery.cache引用了Object@8364337，而jQuery.cache导致的内存驻留接近28M。看起来这个cache在占据大量内存，它会不会随着时间不停变大呢？继续启用轮巡运行机房实景模块15分钟，证实了这一猜测，cache的Retained Size膨胀到120MB：</p>
<p><img class="aligncenter size-full wp-image-12521" src="https://blog.gmem.cc/wp-content/uploads/2016/07/jquery.cache_.ins_.png" alt="jquery.cache.ins" width="100%" /></p>
<div class="blog_h3"><span class="graybg">jQuery.cache为何膨胀</span></div>
<p>上节我们已经确认jQuery.cache存在大量的内存泄漏，那么这个cache是做什么用的呢，为何会无限制的膨胀呢？</p>
<p>我们看看Object@8364337是如何分配，并加入到cache中的，切换Retainers到Allocation stack，查看该对象被创建时的调用栈：</p>
<p><img class="aligncenter size-full wp-image-12525" src="https://blog.gmem.cc/wp-content/uploads/2016/07/Selection_0012.png" alt="Selection_001" width="478" height="383" /></p>
<p><span style="color: #808080;"><em>（注意：默认不会录制内存分配时的栈信息，需要在开发者工具设置 <em>⇨ </em>General ⇨ Profiler中启用。）</em></span></p>
<p>有了这个调用栈，即使你不熟悉实景图模块，也能够下手进行调试了。通过单步跟踪，发现大概的程序逻辑如下：</p>
<ol>
<li>Ext.define.onTick其实一个定时器，每过一定时间它就会调用mm.takeTurnTask.run()方法，即执行轮巡，切换实景图。切换是通过调用MapManager.switchTo()实现的</li>
<li>Ext.define.switchTo：轮巡定时器触发，切换实景图<br />
<pre class="crayon-plain-tag">Ext.define( 'kssi.map.run.MapManager', {
    switchTo: function ( mapId ) {
        var me = this;
        var curMap = this.getCurMap();
        // 只在目标实景图不是当前实景图的情况下才切换
        if ( curMap &amp;&amp; mapId == curMap.getId() ) {
            return;
        }
        me.show( mapId );
    },
    show: function ( mapId ) {
        var me = this;
        // 缓存当前实景图
        me.cache( me.getCurMap() );
        // 创建新的需要切换到的实景图
        me.createMapComp( mapId );  // ⇨
        me.updateMapView();
    }
});</pre>
</li>
<li>Ext.define.createMapComp：创建新的<span style="background-color: #c0c0c0;">实景图UI包装器</span>组件，在创建前总是会尝试移除原先的组件：<br />
<pre class="crayon-plain-tag">Ext.define( 'kssi.map.run.MapManager', {
    createMapComp: function ( mapId ) {
        var me = this;
        if ( me.currMapComp ) {
            // 创建新实景图之前，移除老的实景图
            me.mapCntr.remove( me.currMapComp, true );
            me.currMapComp = null;
        }
        // 创建新的实景图并添加到容器
        var mapComp = Ext.create( 'kssi.map.design.MapComponent', mapData );
        me.afterMapCompCreated( mapComp );
        me.mapCntr.add( mapComp );
    }
});</pre>
</li>
<li>Map$d2d是实景图UI组件，基于Draw2D的实现。在其重绘（repaint）函数中，会创建Draw2D画布对象，并调用其初始化方法：<br />
<pre class="crayon-plain-tag">Ext.define( 'com.kingsmartsi.secmon.map.domain.Map$d2d', {
    extend: 'com.kingsmartsi.secmon.map.domain.Map',
    canvas: null,
    repaint: function () {
        var map = this;
        var items = map.painted ? map.marshalItems() : map.items;
        map.clear();
        map.dom.innerHTML = '';
        // 就是把当前ExtJS对应的DOM元素传递给Draw2d画布的构造函数
        map.canvas = new draw2d.Canvas( map.dom.id );  // ⇨ 
    }
}</pre>
</li>
<li>Draw2D画布类遵循Class.js的类继承模拟机制，其构造函数的职责实质上由初始化方法init()承担<br />
<pre class="crayon-plain-tag">(function () {
    // 声明一个全局变量
    this.Class = function () {
    };

    Class.extend = function ( prop ) {
        // ...
        function Class() {
            // 实际的构造了逻辑是委托给init方法完成的
           if ( !initializing &amp;&amp; this.init )
                this.init.apply( this, arguments );
        }
        // ...
        return Class;
    };
})();</pre></p>
<p> 因此，repaint()传递给new draw2d.Canvas()的map.dom.id，会直接转给init()，作为画布的容器元素</p>
<pre class="crayon-plain-tag">draw2d.Canvas = Class.extend(
    {
        init: function ( canvasId, width, height ) {

            this.canvasId = canvasId;
            // Map$d2d组件对应的DOM元素、Draw2D画布的html属性指向的DOM元素是同一个，只不过后者是一个jQuery元素封装
            this.html = $( "#" + canvasId );  // canvasId  ==  Map$d2d.dom.id 
            // 构建Raphael画布对象
            if ( typeof height !== "undefined" ) {
                this.paper = Raphael( canvasId, width, height );
            }
            else {
                this.paper = Raphael( canvasId, this.getWidth(), this.getHeight() );
            }
            // 对容器元素进行事件绑定
            this.html.bind( "mouseup touchend", function ( event ) { // ⇨
            } );
            // bind()方法只是on()的一个代理，可以认为两者一回事
            this.html.on( "dblclick", function ( event ) { // ⇨
            } );
            // 更多事件绑定...
        }
}</pre>
<p>可以看到Draw2D画布在初始化时，调用jQuery的bind/on进行事件绑定。我们开始接近jQuery.cache膨胀的源头了</p>
</li>
<li>jQuery在进行事件绑定时，实际上是调用jQuery.event对象的add()方法：<br />
<pre class="crayon-plain-tag">jQuery.fn.extend(
        {
            on: function ( types, selector, data, fn, /*INTERNAL*/ one ) {
                // 遍历jQuery元素集合（这里只有一个id为canvasId的DIV元素），分别进行添加事件
                return this.each( function () {
                    jQuery.event.add( this, types, fn, data, selector ); // ⇨
                } );
            }
        }
);</pre>
</li>
<li>jQuery.event.add在一开始就会调用_data()方法：<br />
<pre class="crayon-plain-tag">jQuery.event = {
    add: function( elem, types, handler, data, selector ) {
        // 这个_data()其实是从缓存中拿数据，下面可以看到这一点
        var elemData = jQuery._data( elem );  // ⇨ 
        if ( !elemData ) return;
    }
};

jQuery.extend({
    _data: function( elem, name, data ) {
        return internalData( elem, name, data, true );  // ⇨ 
    }
});</pre>
</li>
<li>而_data()则是internalData的简单代理，后者会<span style="background-color: #c0c0c0;">初始化元素缓存</span>：<br />
<pre class="crayon-plain-tag">function internalData( elem, name, data, pvt /* Internal Use Only */ ) {
    // 对于元素节点，使用jQuery.cache，也就是就是我们的泄漏对象
    var cache = isNode ? jQuery.cache : elem;
    // 分配缓存key
    if ( !id ) {
        // Only DOM nodes need a new unique ID for each element since their data
        // ends up in the global cache
        if ( isNode ) {
            id = elem[ internalKey ] = core_deletedIds.pop() || jQuery.guid++;
        }
        else {
            id = internalKey;
        }
    }
    // 如果尚未缓存，为元素建立缓存条目
    if ( !cache[ id ] ) {
        // Avoid exposing jQuery metadata on plain JS objects when the object
        // is serialized using JSON.stringify
        cache[ id ] = isNode ? {} : { toJSON: jQuery.noop };
    }
    thisCache = cache[ id ];
    // 缓存一条数据，以name为key，data为value
    if ( data !== undefined ) {
        thisCache[ jQuery.camelCase( name ) ] = data;
    }
    // ...
    return ret;
}</pre>
</li>
</ol>
<p>从上面的分析可以看到，通过jQuery来<span style="background-color: #c0c0c0;">注册事件监听器时，会创建目标元素的缓存，从而导致jQuery.cache膨胀</span>。</p>
<div class="blog_h3"><span class="graybg">jQuery.cache如何收缩</span></div>
<p>jQuery的设计中，必然包含了控制jQuery.cache大小的合理逻辑，否则缓存可能会不断膨胀，最终导致内存耗尽。</p>
<p>很多缓存系统使用注入LRU之类的算法，来限制缓存的大小，jQuery.cache好像没有这样设计。</p>
<p>既然jQuery.cache是关于元素的缓存，那么元素销毁后，其缓存条目理当一并销毁。如果这一设想成立，你必须让jQuery知道你销毁了元素，这样jQuery才有机会删除缓存条目（缓存属于jQuery内部实现）。</p>
<p>我们先从代码中寻找删除jQuery.cache条目的逻辑。直接在jquery-1.10.2.js中搜索“jQuery.cache”，匹配结果只有4条，很容易找到清理缓存条目的函数：</p>
<pre class="crayon-plain-tag">function internalRemoveData( elem, name, pvt ) {
	if ( !jQuery.acceptData( elem ) ) {
		return;
	}
	var thisCache, i,
		isNode = elem.nodeType, //元素1，文本3，注释8，文档9，文档类型10，文档片段11
		cache = isNode ? jQuery.cache : elem,
		id = isNode ? elem[ jQuery.expando ] : jQuery.expando;

	// 如果缓存条目不存在，直接返回
	if ( !cache[ id ] ) {
		return;
	}

	if ( name ) {
	    // 获得当前元素的缓存对象
		thisCache = pvt ? cache[ id ] : cache[ id ].data;

		if ( thisCache ) {
			// 把name转换为缓存key的数组
			i = name.length;
			while ( i-- ) {
				// 删除name中所有key对应的缓存数据
				delete thisCache[ name[i] ];
			}
			// 如果缓存条目中还有数据，不销毁缓存条目
                        // isEmptyDataObject：data属性不影响为空判断
                        // isEmptyObject：必须真正的没有任何属性
			if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) {
				return;
			}
		}
	}

	// 销毁缓存条目
	if ( isNode ) {
		jQuery.cleanData( [ elem ], true );  // ⇨ 
	} else if ( jQuery.support.deleteExpando || cache != cache.window ) {
		delete cache[ id ];
	} else {
		cache[ id ] = null;
	}
}</pre>
<p>如果入参elem是DOM节点，则调用cleanData()执行复杂的处理逻辑；否则，仅仅将缓存条目从jQuery.cache中移除</p>
<pre class="crayon-plain-tag">jQuery.extend({
        cleanData: function (elems, /* internal */ acceptData) {
            var elem, type, id, data,
                i = 0,
                internalKey = jQuery.expando,
                cache = jQuery.cache,
                deleteExpando = jQuery.support.deleteExpando,
                special = jQuery.event.special;
            //在这里，elems必然是jQuery元素集合对象，遍历
            for (; (elem = elems[i]) != null; i++) {
                // 如果支持缓存
                if (acceptData || jQuery.acceptData(elem)) {

                    id = elem[internalKey]; // 缓存条目键
                    data = id &amp;&amp; cache[id]; // 缓存条目值

                    if (data) {
                        // 移除事件处理函数
                        if (data.events) {
                            for (type in data.events) {
                                if (special[type]) {
                                    jQuery.event.remove(elem, type);
                                } else {
                                    jQuery.removeEvent(elem, type, data.handle);
                                }
                            }
                        }

                        if (cache[id]) {
                            // 从jQuery.cache中删除该缓存条目
                            delete cache[id];
                            if (deleteExpando) {
                                delete elem[internalKey];
                            } else if (typeof elem.removeAttribute !== core_strundefined) {
                                elem.removeAttribute(internalKey);
                            } else {
                                elem[internalKey] = null;
                            }
                            core_deletedIds.push(id);
                        }
                    }
                }
            }
        }
});</pre>
<p>那么，哪些函数调用此函数呢？直接调用它的函数有两个：</p>
<pre class="crayon-plain-tag">jQuery.extend({
    cache: {},
    removeData: function (elem, name) {
        return internalRemoveData(elem, name);
    },
    _removeData: function (elem, name) {
        return internalRemoveData(elem, name, true);
    }
});</pre>
<p>对于第二个函数，存在以下调用链：jQuery.off()  ⇨ jQuery.event.remove()  ⇨ jQuery._removeData()。因此，解除注册元素上的事件处理器，可能会让元素缓存条目被清理。</p>
<p>在上面的分析中，我们知道internalRemoveData()可能调用cleanData()来清理节点的缓存数据，那么还有其它函数调用cleanData()么？搜索发现，下面的方法也会调用它：</p>
<pre class="crayon-plain-tag">jQuery.fn.extend( {
    // 移除匹配的元素
    remove: function ( selector, keepData ) {
        var elem,
            elems = selector ? jQuery.filter( selector, this ) : this,
            i = 0;
        for ( ; (elem = elems[ i ]) != null; i++ ) {

            if ( !keepData &amp;&amp; elem.nodeType === 1 ) {
                jQuery.cleanData( getAll( elem ) );
            }
        }

        return this;
    },
    // 清空子节点
    empty: function () {
        var elem,
            i = 0;

        for ( ; (elem = this[ i ]) != null; i++ ) {
            // Remove element nodes and prevent memory leaks
            if ( elem.nodeType === 1 ) {
                jQuery.cleanData( getAll( elem, false ) );
            }
        }
        return this;
    },
} );</pre>
<p>小结一下：</p>
<ol>
<li>函数internalRemoveData负责：
<ol>
<li>从缓存条目中删除指定的键值对</li>
<li>当缓存条目“为空”时，清理DOM节点的事件监听器数据，并且从jQuery.cache中移除缓存条目</li>
</ol>
</li>
<li>方法cleanData负责：清理元素的缓存数据</li>
<li>当通过jQuery API移除元素时，它的位于jQuery.cache中的缓存条目被删除</li>
<li>当通过jQuery API移除元素的事件监听器时，它位于jQuery.cache中缓存条目的数据项会被删除，如果缓存条目在此数据项删除后变“空”，则删除缓存条目</li>
</ol>
<p>因此：由于你通过jQuery API操控DOM，而导致其驻留jQuery.cache后，<span style="background-color: #c0c0c0;">必须通过jQuery的API</span>——例如remove()、off()进行清理操作，才能确保缓存条目在不需要时顺利移除。</p>
<div class="blog_h3"><span class="graybg">实景图的销毁逻辑</span></div>
<p>通过上一节的分析，我们已经确信：必须通过jQuery的API进行清理操作，才能避免jQuery.cache的内存泄漏。实景图模块并没有直接使用jQuery，直接使用它的是Draw2D。那么，后者是否暴露了适当的接口供上层组件执行清理？</p>
<p>我们看一下实景图模块的清理逻辑。在前面的分析中我们已经看到，轮巡（switchTo）时，原有实景图UI包装器组件会被删除：</p>
<pre class="crayon-plain-tag">switchTo: function ( mapId ) {
    var me = this;
    me.show( mapId );
}
show: function ( mapId ) {
    var me = this;
    //首先缓存当前实景图UI包装器组件的数据内容
    me.cache( me.getCurMap() );
    //然后创新新实景图
    me.createMapComp( mapId );
    me.updateMapView();
}
createMapComp: function ( mapId ) {
    var me = this;
    if ( me.currMapComp ) {
        // 移除老的实景图
        me.mapCntr.remove( me.currMapComp, true );
        me.currMapComp = null;
    }
    var mapData = me.mapDataCache[ mapId ];
    // 创建新实景图并添加到容器中
    var mapComp = Ext.create( 'kssi.map.design.MapComponent', mapData );
}</pre>
<p>上面的调用ExtJS标准API：Container.remove()用来移除原有的实景图UI包装器组件，由于实景图组件被设置为autoDestory，因此它会被销毁。除了ExtJS组件标准的销毁逻辑以外。实景图模块还扩展了一些逻辑：</p>
<pre class="crayon-plain-tag">// 实景图UI包装器组件
Ext.define( 'kssi.map.design.MapComponent', {
    map : null ,  //Map$d2d
    // 扩展的逻辑
    onDestroy : function()
    {
        this.map.onDestroy();
    }
    // 销毁从这里开始，这个是ExtJS框架的逻辑，复制过来便于理解流传
    destory : function() {
        me.onDestroy();
        if (!me.preserveElOnDestroy) {
            me.el.remove(); // 移除DOM元素
        }
    }
} );
//实景图UI组件
Ext.define( 'com.kingsmartsi.secmon.map.domain.Map$d2d', {
    canvas: null,
    // 扩展的逻辑
    onDestroy: function () {
        this.canvas.destroy();
        this.tip.close();
    }
}</pre>
<p>到这里为止，我们可以确认：</p>
<ol>
<li>Draw2D画布的目标元素，亦即ExtJS组件MapComponent对应的DOM元素，最终被（ExtJS API）销毁了。注意上一节我们的结论——必须通过jQuery API对操纵过的元素进行清理操作，因此这里的销毁肯定不能阻止jQuery.cache的泄漏</li>
<li>在销毁上述DOM之前，实景图模块调用Draw2D画布的destroy()方法</li>
</ol>
<p>这个destroy()方法应当是删除Draw2D画布的相关资源，我们看看它的代码：</p>
<pre class="crayon-plain-tag">draw2d.Canvas = Class.extend(
        destroy: function () {
            // 清空
            this.clear();
            // 解除document上的事件绑定
            $( document ).unbind( "keydown", this.keydownCallback );
            $( document ).unbind( "keyup", this.keyupCallback );
            this.eventSubscriptions = {};
            // 删除Raphael画布对象
            this.paper.remove();
        },
        // 清理画布上的Figure、Line等元素
        clear: function () {
            this.fireEvent( "clear" );
            var _this = this;
            // 移除所有线条
            this.lines.clone().each( function ( i, e ) {
                _this.remove( e );
            } );
            // 移除所有图形
            this.figures.clone().each( function ( i, e ) {
                _this.remove( e );
            } );
            // 重置实例变量
            this.zoomFactor = 1.0;
            this.selection.clear();
            // ...
            return this;
        }
    }
);</pre>
<p>注意到没有？Draw2D的Canvas类在初始化时注册了很多事件监听器（this.html.on/bind调用），但是在上面的destroy()方法中：</p>
<ol>
<li>没有通过off()调用解除this.html的监听器绑定</li>
<li>也没有通过remove()调用删除this.html元素</li>
</ol>
<p><span style="color: #808080;"><em>（为了防止代码分析存在遗漏，你可以在jQuery的internalRemoveData、cleanData函数上添加断点，然后再运行实景图模块，如果断点没有命中或者调用针对的元素不是this.html，则证明上述结论无误）</em></span></p>
<p>因此，jQuery.cache导致的内存泄漏发生了。</p>
<div class="blog_h3"><span class="graybg">解决jQuery.cache泄漏</span></div>
<p>弄清楚来龙去脉，解决这个问题就非常简单了，只需要再Map$d2d的onDestroy()钩子中添加一行代码即可：</p>
<pre class="crayon-plain-tag">Ext.define( 'com.kingsmartsi.secmon.map.domain.Map$d2d', {
    onDestroy: function () {
        this.canvas.destroy();
        // 解决jQuery.cache内存泄漏问题
        this.canvas.html.remove();
        this.tip.close();
    }
});</pre>
<div class="blog_h3"><span class="graybg">重新分析时间线</span></div>
<p>上节我们通过修改Draw2D的代码，处理了jQuery.cache泄漏问题，现在我们需要验证一下，问题是不是真正解决了。重新录制10分钟的堆时间线： </p>
<p> <img class="aligncenter size-full wp-image-12551" src="https://blog.gmem.cc/wp-content/uploads/2016/07/timeline-after-jquery-cache-fixed.png" alt="timeline-after-jquery-cache-fixed" width="694" height="81" /></p>
<p>和一开始的录制的时间线对比一下可以看到，我们解决jQuery.cache泄漏后，实景图整体内存消耗有了很大改善。 观察此图，我们可以看到：</p>
<ol>
<li>每个高柱子（都大于1.0MB线）都是在轮巡切换实景图时发生的内存分配</li>
<li>总有4-5个蓝色高柱子，它们总是位于时间线的最右侧。这是个好现象，说明随着时间的推移，以前分配的内存大多被回收了</li>
<li>仍然有少量内存过了很久都没有被回收，注意观察那些非常低矮的蓝色短柱</li>
</ol>
<p>通过多次重新录制、查看，发现上面第3条提及的<span style="background-color: #c0c0c0;">蓝色短柱是随机出现</span>的，其Retained Size也主要由Chrome本身导致，因此<span style="background-color: #c0c0c0;">不需要刻意去分析</span>。</p>
<p>那么，大片的灰色区域中，是否存在<span style="background-color: #c0c0c0;">“肉眼不可见”（量太少以至于画不出蓝色柱子）的内存泄漏</span>呢？选定一个大范围的区间查看：</p>
<p><img class="aligncenter size-full wp-image-12561" src="https://blog.gmem.cc/wp-content/uploads/2016/07/timeline-after-jquery-cache-fixed-2.png" alt="timeline-after-jquery-cache-fixed-2" width="100%" /></p>
<p>那些带括号的一般是JS引擎内部使用，可以不去考虑。因此可能属于内存泄漏的类型包括：constructor、Object、Array、HasListeners。我们分别展开这些类型的第一个对象实例，合并到一张图片中进行对照：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2016/07/ext-cmp-1035.png"><img class="aligncenter size-full wp-image-12564" src="https://blog.gmem.cc/wp-content/uploads/2016/07/ext-cmp-1035.png" alt="ext-cmp-1035" width="100%" /></a>注意在这4张图片的Retainers子面板中出现的ext-com-1035，很明显，后3个对象分别是第1个对象的events、protoEl.classList、hasListeners属性。只要第1个对象被回收，其它3个对象自然会被回收。因此，我们只需要搞清楚第1个对象是什么，为什么一直驻留就可以了。</p>
<div class="blog_h3"><span class="graybg">分析constructor@2398693</span></div>
<p>下面看看constructor对象的列表，注意那些<span style="background-color: #c0c0c0;">Retained Size一样的对象，它们往往是基于同样的逻辑创建出来</span>了，一般你只需要分析其中一个就可以了：</p>
<p><img class="aligncenter size-full wp-image-12568" src="https://blog.gmem.cc/wp-content/uploads/2016/07/Selection_0013.png" alt="Selection_001" width="614" height="241" /></p>
<p>以constructor@2398693为例，它的引用链是：<pre class="crayon-plain-tag">window.Ext.ComponentMgr.all.map['ext-comp-1035']</pre> 。如果你熟悉ExtJS的架构，就知道所有ExtJS组件创建后都在组件管理器（ComponentMgr）中注册，销毁前则从组件管理器中解除注册：</p>
<pre class="crayon-plain-tag">Ext.define('Ext.AbstractComponent', {
    constructor : function(config) {
        // ...
        me.initComponent();
        // 在initComponent阶段之后，把自己注册到组件管理器
        Ext.ComponentManager.register(me);
    },
    destroy : function() {
        // ...
        Ext.ComponentManager.unregister(me);
    }
});</pre>
<p>constructor@2398693一直没有解除注册，可能因为实景图模块没有适当的销毁它。 </p>
<p>下面看一下该对象的Allocation stack，看看是什么代码创建了constructor@2398693：</p>
<p><img class="aligncenter size-full wp-image-12570" src="https://blog.gmem.cc/wp-content/uploads/2016/07/Selection_0022.png" alt="Selection_002" width="337" height="525" /></p>
<p>从栈顶向下跟踪，可以发现实景图UI组件创建了它：</p>
<pre class="crayon-plain-tag">Ext.define( 'com.kingsmartsi.secmon.map.domain.Map$d2d', {
    constructor: function () {
        this.callParent( arguments );
        this.tip = Ext.create( 'Ext.tip.Tip', {
            cls: 'tooltip'
        } );
        return this;
    }
});</pre>
<p>原来constructor@2398693是一个Tooltip，其实你随时可以通过鼠标悬停或<pre class="crayon-plain-tag">$0</pre> 来查看constructor@2398693的细节，进而获知它是一个Tooltip。</p>
<p>我们顺便看看Map$d2d是怎样销毁这个Tooltip的：</p>
<pre class="crayon-plain-tag">Ext.define( 'com.kingsmartsi.secmon.map.domain.Map$d2d', {
    onDestroy: function () {
        this.canvas.destroy();
        //解决jQuery.cache内存泄漏问题
        this.canvas.html.remove();
        this.tip.close();
    }
});</pre>
<p>Map$d2d调用Toltip的close()方法（这个方法从Ext.panel.Panel继承得到）。这个方法的行为是什么呢？官方文档描述如下：</p>
<p style="padding-left: 60px;"><em>关闭面板，默认行为是将其从DOM中移除，删除面板对象及其所有子组件。注意：该方法的行为受到closeAction设置的影响，如果要明确的控制，请使用destroy()或者hide()方法</em></p>
<p>注意到了吗，close()不一定销毁组件，也可能是隐藏组件，这取决于closeAction设置，而Ext.tip.ToolTip的closeAction默认是hide，这个ExtJS官方文档说明的很清楚。所以实景图模块存在BUG。</p>
<div class="blog_h3"><span class="graybg">解决Ext.tip.ToolTip泄漏</span></div>
<p>要解决上面的泄漏很简单，要么在onDestroy()时明确调用destroy()，要么修改closeAction设置：</p>
<pre class="crayon-plain-tag">Ext.define( 'com.kingsmartsi.secmon.map.domain.Map$d2d', {
    constructor: function () {
        this.tip = Ext.create( 'Ext.tip.Tip', {
            cls: 'tooltip',
            closeAction: 'destroy'
        } );
    },
    onDestroy: function () {
        this.tip.destroy();
    },
});</pre>
<p>回过头来想想，真的需要为每个实景图UI组件配备单独的Tooltip实例吗？完全没必要，ToolTip本身就倾向于全局使用。 </p>
<div class="blog_h3"><span class="graybg">重新分析时间线</span></div>
<p>处理好Tooltip泄漏后，更新服务器，再次录制10分钟的时间线：</p>
<p><img class="aligncenter size-full wp-image-12575" src="https://blog.gmem.cc/wp-content/uploads/2016/07/timeline-final.png" alt="timeline-final" width="100%" /></p>
<p>可以看到，驻留的对象都是Chrome引擎内部使用的对象。连续运行实景图轮巡一小时，通过Chrome任务管理器观察，内存也没有明显增长。我们认为不存在内存泄漏了。</p>
<div class="blog_h2"><span class="graybg">启用轮巡时的堆时间线分析：Round 2</span></div>
<p>上面的分析结束后，我们认为内存泄漏问题已经解决。</p>
<p>但是，实景图轮巡是需要支持无限时间连续运行的，所以我们还要进行长期稳定性测试。连续运行16小时后，实景图模块占用的内存从100MB增长到400MB左右。看来，还是存在内存泄漏——平均每小时泄漏18MB内存，只是由于垃圾回收机制的干扰，我们无法在短时间内直观的察觉。</p>
<div class="blog_h3"><span class="graybg">重新分析时间线</span></div>
<p>这里，我们还是首先考虑通过时间线分析。不要刷新浏览器，录制一个时间线。</p>
<p>和上一个时间线一样，我们选取一大片区间，中间并没有可供分析的对象。是不是泄漏都是在时间线之前分配的呢？毕竟实景图模块在录制前已经连续运行16小时了。这里有个技巧：<span style="background-color: #c0c0c0;">将区间起点拉到最左边，可以看到在录制时间线之前分配的那些对象</span>：</p>
<p><img class="aligncenter size-full wp-image-12584" src="https://blog.gmem.cc/wp-content/uploads/2016/07/obj-alloc-before-timeline.png" alt="obj-alloc-before-timeline" width="691" height="369" /></p>
<p>看到没有，在这16小时内：Object导致116MB内存驻留；constructor导致97MB内存驻留；Array导致80MB内存驻留。虽然这些内存可能会有重合、这些对象也不一定都是内存泄漏，但是，内存泄漏的存在是无容置疑的——最初运行的1小时内，实景图模块总计占据的内存还不到100MB。</p>
<div class="blog_h3"><span class="graybg">iframe导致的实景图管理器泄漏</span></div>
<p>我们先看看Object，大概浏览一下前面的几十条，发现它们都是window.mapManager.mapDataCache：</p>
<p><img class="aligncenter size-full wp-image-12586" src="https://blog.gmem.cc/wp-content/uploads/2016/07/two-mm.png" alt="two-mm" width="620" height="235" /></p>
<p>也就是说，内存中存在多个实景图管理器（kssi.map.run.MapManager）对象（它与mapDataCache是1:1关系）。这是严重的编程错误，在实景图模块，实景图管理器是全局唯一的对象，绝不应该存在多个实例。</p>
<p>检查代码，发现创建实景图管理器的逻辑是存放在Ext.onReady()方法中的，这意味着在页面的生命周期中，实景图只会创建一次。那么时间线中的多个mapManager，该如何解释？</p>
<p>页面的生命周期中，实景图只会创建一次，这个推论没有问题。出现多个mapManager，是因为时间线跨越了页面的生命周期，说它跨越的依据是 in Window@后面的数字。看到了吗，<span style="background-color: #c0c0c0;">左边9601而右边是17947</span>。window的标识符都变了，说明页面刷新了。我们重新录制时间线、结合console.log可以证明这一点：</p>
<p><img class="aligncenter size-full wp-image-12590" src="https://blog.gmem.cc/wp-content/uploads/2016/07/timeline-3600.png" alt="timeline-3600" width="589" height="228" /></p>
<p>看到没有，在运行1小时后，出现大量内存分配，日志也显示mapManager被创建。到这里我们可以确信实景图模块有自动刷新的逻辑，搜索reload找到：</p>
<pre class="crayon-plain-tag">Ext.TaskManager.start( {
    run: function () {
        mm.cacheTransientState( true );
        window.location.reload();
    },
    interval: 3600 * 1000
} );</pre>
<p>这段代码原本是用作临时解决内存泄漏的，而现在，解药却变成了毒药。这是为什么？</p>
<p>页面刷新后、页面内存被清空，整个时间线信息应该丢失才对。而我们的时间线完整的记录了多个window对象实例。这个现象可能的原因是，<span style="background-color: #c0c0c0;">实景图是运行在iframe中</span>的。top窗口一直没有刷新。你可以切换到Elements面板，搜索iframe发现：</p>
<pre class="crayon-plain-tag">&lt;iframe src="/pems-web-manager/maprun/group" name="frame-frame" width="100%" height="100%" frameborder="0" id="ext-gen1114"&gt;
&lt;/iframe&gt;</pre>
<p>页面刷新后，其内存仍然不能销毁，唯一可解释的原因是：<span style="background-color: #c0c0c0;">其它iframe或者top窗口的某个变量直接/间接引用该页面中的变量</span>。</p>
<p>通过查看top窗口的脚本monitoring.js，我们看到其中大量的引用了子页面的mapManager。</p>
<div class="blog_h3"><span class="graybg">解决实景图管理器泄漏</span></div>
<p>由于“每小时刷新实景图页面”是以前解决内存泄漏的临时方案，现在已经不需要了，删除定时器代码即可。</p>
<div class="blog_h2"><span class="graybg">总结</span></div>
<p>关于如何发现、定位并解决内存泄漏，请参考下面的几条建议：</p>
<ol>
<li>不要猜测！去测试、去剖析</li>
<li>再小的内存泄漏，经过足够长的时间后也会对应用稳定性和性能产生严重影响，因此不能放过</li>
<li>尽早测试，特别是那些需要长时间运行的程序。随着程序规模的增长、新框架和新组件的引入，定位内存泄漏变得越来越困难</li>
<li>通过堆时间线去定位、隔离出内存泄漏</li>
<li>识别Chrome引擎随机性的内存分配，这些分配在对象列表中，其类型往往有括号包围</li>
<li>记录Allocation stack，快速的跟踪什么代码创建了对象</li>
<li>必要时，开启Debug进行单步跟踪，了解自己不熟悉的第三方框架的行为</li>
<li>解决了一个内存泄漏后，最好重新录制堆时间线，因为其它对象的驻留可能也是由这个已解决的内存泄漏引起</li>
</ol>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/detect-memleaks-with-devtools">使用Chrome开发者工具分析内存泄漏</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/detect-memleaks-with-devtools/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用Oracle Java Mission Control监控JVM运行状态</title>
		<link>https://blog.gmem.cc/jvm-monitoring-with-oracle-jmc</link>
		<comments>https://blog.gmem.cc/jvm-monitoring-with-oracle-jmc#comments</comments>
		<pubDate>Thu, 06 Nov 2014 03:11:25 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[JMC]]></category>
		<category><![CDATA[JVM]]></category>
		<category><![CDATA[性能剖析]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=2525</guid>
		<description><![CDATA[<p>简介 Oracle  Java Mission Control（以下称JMC）是一个集成到JVM（jdk7u40+）的性能剖析和诊断工具，相比起JProfiler之类的性能剖析工具，JMC更加简单易用，界面友好。 JMC使用了JVM内部特定的基于事件的接口，不同于一般性能剖析工具使用JVMPI/JVMTI方式实现。因此JMC几乎不会给应用造成额外的压力，可以用在负载很高的生产环境中： JFR在默认设置continuous下运行几乎对性能没有影响 JFR在默认设置profiling下运行通常造成小于2%的性能下降 在JDK 7 Update 40及其以后的版本中已经集成了JMC（%JDK_HOME%\bin\jmc.exe），打开此程序，会发现它是一个典型的Eclipse RCP应用。本文简单的介绍如何使用JMC来监控、记录JVM的运行过程。 目标JVM配置 注意：你看到的基于Eclipse的JMC用户界面，是一个前端的工具，在被监控的JVM（目标JVM）上需要开启以下Java Options才能对其进行监控、进行黑匣子记录。 [crayon-69e29c566319c759524260/] 如果要在目标JVM上启动持续不断的黑匣子记录，并在JVM正常退出时Dump记录为文件，可以参考下面的脚本设置JVM参数： [crayon-69e29c56631a0336614407/] 连接到目标JVM 配置并重启目标JVM后，打开JMC，点击 File <a class="read-more" href="https://blog.gmem.cc/jvm-monitoring-with-oracle-jmc">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/jvm-monitoring-with-oracle-jmc">使用Oracle Java Mission Control监控JVM运行状态</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Oracle  Java Mission Control（以下称JMC）是一个集成到JVM（jdk7u40+）的性能剖析和诊断工具，相比起JProfiler之类的性能剖析工具，JMC更加简单易用，界面友好。</p>
<p>JMC使用了JVM内部特定的基于事件的接口，不同于一般性能剖析工具使用JVMPI/JVMTI方式实现。因此JMC几乎不会给应用造成额外的压力，可以用在负载很高的生产环境中：</p>
<ol>
<li>JFR在默认设置continuous下运行几乎对性能没有影响</li>
<li>JFR在默认设置profiling下运行通常造成小于2%的性能下降</li>
</ol>
<p>在<a href="http://www.oracle.com/technetwork/articles/javase/index-jsp-138363.html" target="_blank">JDK 7 Update 40</a>及其以后的版本中已经集成了JMC（%JDK_HOME%\bin\jmc.exe），打开此程序，会发现它是一个典型的Eclipse RCP应用。本文简单的介绍如何使用JMC来监控、记录JVM的运行过程。</p>
<div class="blog_h1"><span class="graybg">目标JVM配置</span></div>
<div>注意：你看到的基于Eclipse的JMC用户界面，是一个前端的工具，在被监控的JVM（目标JVM）上需要开启以下Java Options才能对其进行监控、进行黑匣子记录。</div>
<pre class="crayon-plain-tag">-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.port=7777
-Dcom.sun.management.jmxremote
-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder</pre>
<p>如果要在目标JVM上启动持续不断的黑匣子记录，并在JVM正常退出时Dump记录为文件，可以参考下面的脚本设置JVM参数：</p>
<pre class="crayon-plain-tag">for /f "tokens=1-3 delims=/ " %%a in ('date /t') do (set md=%%a-%%b-%%c)
for /f "tokens=1-3 delims=/:." %%a in ("%TIME%") do (set mt=%%a%%b%%c)
set CUR_TIME=%md%-%mt%

rem 启用黑匣子默认记录行为。官方文档注明dumponexitpath支持目录，但我在7u71版本下测试失败
set JAVA_OPTS=%JAVA_OPTS% -XX:FlightRecorderOptions=defaultrecording=true,disk=true,dumponexit=true,dumponexitpath=%LOG_DIR%\jfr\dump-on-exit-%CUR_TIME%.jfr,maxage=3600s,settings=%JRE_HOME%\lib\jfr\profile.jfc</pre>
<div class="blog_h1"><span class="graybg">连接到目标JVM</span></div>
<p>配置并重启目标JVM后，打开JMC，点击<span style="background-color: #c0c0c0;"> File - Connect... - Create New connection</span>，参考下图设置，完毕后点击Test connection确认可以连通，最后点击Finish结束配置。</p>
<p><img src="https://blog.gmem.cc/wp-content/uploads/2014/11/jmc-1.jpg" alt="" width="100%" /></p>
<div class="blog_h1"><span class="graybg">MBean Server</span></div>
<p>在左侧窗格JVM Browser视图中，双击刚刚创建的JMC连接，点击MBean Server，即可在右侧主窗体查看实时的JVM状态信息，供6个选项卡，下面依次描述。</p>
<div class="blog_h2"><span class="graybg">总览</span></div>
<p>包含JVM概要性的实时图表，包含3个面板：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 250px; text-align: center;">面板 </td>
<td style="text-align: center;"> 说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>仪表盘（Dashboard）</td>
<td>
<p>以仪表方式显示系统运行参数，默认显示堆内存占用、JVM处理器占用、垃圾回收前后的堆内存情况。注意仪表上有有两个指针，灰色的通常显示最大值，黑色的显示当前值。</p>
<p>面板右侧的+号按钮，点击后可以添加目标JVM支持的任何运行参数到仪表盘中</p>
</td>
</tr>
<tr>
<td>处理器（Processor）</td>
<td>显示JVM、操作系统的动态CPU占用曲线</td>
</tr>
<tr>
<td>内存（Memory）</td>
<td>默认显示Java堆内存占用情况，可以选择查看物理内存占用等项目</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">MBean浏览器</span></div>
<p>以树形结构显示目标JVM支持的所有MBean列表和属性、操作集。</p>
<div class="blog_h2"><span class="graybg">触发器</span></div>
<p>包含若干预定义的触发器，可以在<span style="background-color: #c0c0c0;">特定条件下，执行预定义的动作</span>。支持的触发器包括：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 250px; text-align: center;"> 触发器</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>CPU Usage - JVM Process (Too High)</td>
<td>当JVM的进程的CPU占用过高时</td>
</tr>
<tr>
<td>CPU Usage - JVM Process (Too Low)</td>
<td>当JVM的进程的CPU占用过低时</td>
</tr>
<tr>
<td>CPU Usage - Machine (Too High)</td>
<td>当OS的CPU占用过高时</td>
</tr>
<tr>
<td>CPU Usage - Machine (Too Low)</td>
<td>当OS的CPU占用过低时</td>
</tr>
<tr>
<td>Deadlocked Threads</td>
<td>当发生JVM线程死锁时</td>
</tr>
<tr>
<td>Live Set (Too Large)</td>
<td>当垃圾回收之后，存活对象占堆大小百分比过大时</td>
</tr>
<tr>
<td>Thread Count (Too High)</td>
<td>当JVM线程总数过大时</td>
</tr>
</tbody>
</table>
<p>此外，任何MBean属性均可以作为触发器使用，中间件（例如Tomcat、Weblogic、ActiveMQ）都有自己的扩展。 双击触发器后，右侧即显示规则详情（Rule Details），可以定义触发的阈值（到达时，或者从异常值恢复时）、执行的动作，可用的动作包括：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 250px; text-align: center;"> 动作</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>Application alert</td>
<td>在应用程序上显示一个警告对话框</td>
</tr>
<tr>
<td>Console output</td>
<td>在控制台（stdout）上输出警告信息</td>
</tr>
<tr>
<td>Dump Flight Recording</td>
<td>保存黑匣子记录内容</td>
</tr>
<tr>
<td>HPROF Dump</td>
<td>保存堆镜像</td>
</tr>
<tr>
<td>Log to file</td>
<td>在日志文件中输出警告信息</td>
</tr>
<tr>
<td>Send e-mail</td>
<td>通过电子邮件发送警告信息</td>
</tr>
<tr>
<td>Start Continuous Flight Recording</td>
<td>启动不间断的黑匣子记录</td>
</tr>
<tr>
<td>Start Time Limited Flight Recording</td>
<td>启动限制时间长度的黑匣子记录</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">LiveSet</span></div>
<p>所谓LiveSet，是指在一次<span style="background-color: #c0c0c0;">垃圾回收之后，堆内存的占用比率</span>，取值在0-1之间。</p>
<p>如果GC后，可用的内存空间不足，则LiveSet接近100%，这预示可能存在OOM风险。在生产环境下，我们可以监控LiveSet，并在其过大时，把堆内存映像Dump出来供后续分析。</p>
<p>触发器配置示例：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2014/11/liveset-trigger-1.png"><img class="aligncenter  wp-image-19045" src="https://blog.gmem.cc/wp-content/uploads/2014/11/liveset-trigger-1.png" alt="liveset-trigger-1" width="924" height="372" /></a></p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2014/11/liveset-trigger-2.png"><img class="aligncenter wp-image-19047" src="https://blog.gmem.cc/wp-content/uploads/2014/11/liveset-trigger-2.png" alt="liveset-trigger-2" width="925" height="295" /></a></p>
<p>其中Condition面板：</p>
<ol>
<li>Current Value为指标的当前值，当发生GC后，自动刷新</li>
<li>Max trigger value为触发器激活时的指标值</li>
<li>Sustained period，指标必须到达Max trigger value至少多长时间，触发器才可能激活</li>
<li>Limit period，连续两次激活触发器的最小间隔时间</li>
</ol>
<p>其中Action面板：</p>
<ol>
<li>Only live，如果勾选，仅仅GCRoot可达的对象才被Dump出来</li>
<li>hprof路径为服务器的路径</li>
</ol>
<p>其中Constraints面板：可以指定此触发器在什么日期、什么时间段启用。</p>
<p>要分析内存映像，你可以使用如下命令启动一个Web UI：</p>
<pre class="crayon-plain-tag">jhat -port 7770 -J-Xmx4G default.hprof</pre>
<div class="blog_h2"><span class="graybg">系统</span></div>
<p>显示JVM和系统的概要信息，包含3个面板：</p>
<ol>
<li>系统信息：例如OS类型、硬件架构、系统内存、JVM的PID、JVM版本、Classpath、JVM启动时间、JVM参数（Java Options）</li>
<li>JVM统计：例如已加载的类数量、已运行时间</li>
<li>系统属性：显示JVM的系统属性</li>
</ol>
<div class="blog_h2"><span class="graybg">内存</span></div>
<p>显示JVM内存占用情况，分为上下两个面板。</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 250px; text-align: center;">面板</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>GC Table</td>
<td>
<p>显示垃圾回收器（ParNew：新生代的并行回收器，ConcurrentMarkSweep：并发垃圾回收器…）的工作情况，包括：GC总计时间、GC总次数、最近GC开始结束时间、GC标识符、GC线程数</p>
</td>
</tr>
<tr>
<td>Active Memory Pools</td>
<td>
<p>显示当前的内存池的大小、已用大小、类型（堆/非堆），可能有以下内存池：</p>
<ol>
<li>Code Cache：代码缓存</li>
<li>Par Eden Space：并行（-XX:+UseParNewGC）垃圾回收的新生代Eden区</li>
<li>Par Survivor Space：并行垃圾回收的新生代Survivor区</li>
<li>CMS Perm Gen：并发垃圾回收永久代</li>
<li>CMS Old Gen：并发垃圾收集年老代</li>
<li>PS Old Gen：并行（-XX:+UseParallelGC）垃圾收集年老代</li>
</ol>
<p>关于垃圾回收器内存分代，参考：<a href="/jvm-options-and-performance-tuning" target="_blank">JVM参数与性能调优</a></p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">线程</span></div>
<p>显示JVM线程活动情况，分为三个面板。</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 250px; text-align: center;">面板</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>Live Thread Graph</td>
<td>
<p>显示当前存活线程的数量、守护线程的数量、线程数量峰值的实时曲线</p>
</td>
</tr>
<tr>
<td>Live Threads</td>
<td>
<p>显示当前存活线程的列表，可以进行CPU占用分析、死锁检测、线程分配内存量的分析</p>
</td>
</tr>
<tr>
<td>Stack traces for selected threads</td>
<td>显示瞬时的线程调用栈</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">启动黑匣子</span></div>
<p>正如民航的黑匣子一样，JMC的这个功能，可以用于事后分析系统性能低下，甚至崩溃的原因。JMC支持两种方式来启动黑匣子记录：通过触发器自动启动， 或者在左侧面板，双击Flight Recorder，手工启动。手工启动可以参考下图设置：<a href="/wp-content/uploads/2014/11/jmc-2.jpg"><img class="size-full wp-image-2552 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2014/11/jmc-2.jpg" alt="jmc-2" width="793" height="626" /></a></p>
<p>其中：Time fixed recording表示<span style="background-color: #c0c0c0;">记录定长时间</span>，时间到达后，自动停止记录；Continuous recording表示进行<span style="background-color: #c0c0c0;">持续不断的记录</span>，直到JVM退出或者接收到停止指令，这种记录方式下可以随时进行dump操作。</p>
<p>虽然黑匣子在JVM本地记录数据，但不在Java堆中记录，因此它并不会影响内存特性和垃圾收集。</p>
<p>黑匣子录制完成后，会在你选择的目录中生成一个.jfr文件，该文件包含录制期间JVM中发生的各种事件。</p>
<div class="blog_h2"><span class="graybg">工作机制</span></div>
<p>黑匣子通过JVM内部API来收集数据，并存放在一个线程本地（thread-local）的缓冲中，并最终刷入全局内存缓冲（global in-memory buffer），内存缓冲中的数据，会被刷入磁盘，磁盘文件时经过压缩的。每个记录数据块，要么在内存中，要么在磁盘中，这意味着——进程被停止或者宕机时，部分数据会丢失，JVM崩溃会也可能导致一部分数据丢失。</p>
<p>可以配置黑匣子不使用磁盘空间，这种场景下，全局内存缓冲被循环使用，<span style="background-color: #c0c0c0;">旧的数据自动丢弃</span>。</p>
<div class="blog_h2"><span class="graybg">黑匣子记录模板</span></div>
<p>在JMC界面上，点击 Windows - Flight Recording Template Manager，看到类似如下界面。可以导入、导出、编辑JFR的设置文件。</p>
<p><a href="/wp-content/uploads/2014/11/jmc-5.jpg"><img class="size-full wp-image-2658 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2014/11/jmc-5.jpg" alt="jmc-5" width="768" height="600" /></a></p>
<p>我们可以导入%JDK_HOME%\jre\lib\jfr下的两个自带模板：default.jfc（Continuous）、profile.jfc（Profiling）。这两个模板的对比如下：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;"> 模板</td>
<td style="text-align: center;">特性 </td>
</tr>
</thead>
<tbody>
<tr>
<td> Continuous</td>
<td>
<p>Garbage Collector:正常<br />Compiler:正常<br />Method Sampling: 正常<br />Thread Dump: 最少一次<br />Exceptions:仅Error<br />Synchronization Threshold: 20 ms<br />File I/O Threshold: 20 ms<br />Socket I/O Threshold: 20 ms<br />Heap Statistics: 禁用<br />Class Loading:禁用<br />Allocation Profiling:禁用</p>
<p>未启用记录的事件类型：Exception Throw、<span style="background-color: #c0c0c0;">Object Alloc In New TLAB、Object Alloc Outside TLAB</span>、Class Loading、<span style="background-color: #c0c0c0;">Compile/Failure</span>、GC/Object Count、GC/Object Count After GC、GC/Phases/Phase Level 3、Profiling/Execution Sample Info、</p>
</td>
</tr>
<tr>
<td> Profiling</td>
<td>
<p>Garbage Collector:正常<br />Compiler:详细<br /><span style="background-color: #c0c0c0;">Method Sampling: 最大化</span><br /><span style="background-color: #c0c0c0;">Thread Dump: 每分钟</span><br />Exceptions:仅Error<br /><span style="background-color: #c0c0c0;">Synchronization Threshold: 10 ms</span><br /><span style="background-color: #c0c0c0;">File I/O Threshold: 10 ms</span><br /><span style="background-color: #c0c0c0;">Socket I/O Threshold: 10 ms</span><br />Heap Statistics: 禁用<br />Class Loading:禁用<br /><span style="background-color: #c0c0c0;">Allocation Profiling:启用</span></p>
<p>未启用记录的事件类型：Exception Throw、Class Loading、GC/Object Count、GC/Object Count After GC、GC/Phases/Phase Level 3、Profiling/Execution Sample Info、</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">黑匣子诊断命令</span></div>
<p>利用诊断命令，可以通过脚本控制黑匣子的行为，命令的示例如下：</p>
<pre class="crayon-plain-tag">rem 获取进程号为5361的JVM关于JFR.start命令的帮助
jcmd 5361 help JFR.start

rem 检查进程的JFR运行状态
jcmd 7648 JFR.check
rem 输出如下。注意recording=0表示正在运行的记录的标识符，后续操作需要用到这个标识符
rem 7648:
rem Recording: recording=0 name="HotSpot default" maxage=1h (running)

rem 启动一个15分钟的黑匣子记录，延迟5分钟启动，使用JDK自带的默认设置（default.jfc），保存Dump文件到D盘
jcmd 7648 JFR.start name=SampleRcd delay=300 duration=900s filename=D:\temp\SampleRcd.jfr 
     \ settings=D:\JavaEE\jdk\x64\jre\lib\jfr\default.jfc

rem 相对路径取决于目标JVM
jcmd 1234 JFR.start delay=0s duration=${jfr_du}s name=$name filename=$PWD/$name.jfr settings=profile

rem 下面的脚本，可以用于定期进行记录，按记录时间保存为不同的文件
setlocal
set DUMP_DIR=D:\temp
set SERVICE_NAME=Tomcat7
set JAVA_HOME=D:\JavaEE\jdk\x64\1.7
pushd %JAVA_HOME%\bin
for /f "tokens=1-3 delims=/ " %%a in ('date /t') do (set md=%%a-%%b-%%c)
for /f "tokens=1-3 delims=/:." %%a in ("%TIME%") do (set mt=%%a%%b%%c)
set CUR_TIME=%md%-%mt%
for /F "tokens=1,3" %%a in ('sc queryex %SERVICE_NAME%') do (
    if "%%a" == "PID" set "JPID=%%b"
)

rem 启动飞行记录，如果目标JVM是以SYSTEM身份启动的NT Service，可以使用PSTools发起jcmd命令，否则会报错：insufficient memory or insufficient privileges to attach
rem psexec -s jcmd %JPID% JFR.start ...
jcmd %JPID% JFR.start name=rcd-%CUR_TIME% duration=900s compress=true filename=%DUMP_DIR%\rcd-%CUR_TIME%.jfr settings=%JAVA_HOME%\jre\lib\jfr\profile.jfc
popd
endlocal

rem 导出JFR的Dump文件
rem 如果目标JVM正在在运行持续不断的黑匣子记录，使用该命令，再结合定时任务机制，可以定期保存黑匣子记录的Dump文件
jcmd 7648 JFR.dump recording=0 filename=F:\temp\dump.jfr</pre>
<div class="blog_h2"><span class="graybg">命令列表</span></div>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;"> 命令</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>JFR.start</td>
<td>
<p>启动一个JFR（黑匣子记录）实例，参数：</p>
<p>name 本次记录的名称<br />settings 服务器上的设置模板<br />defaultrecording 是否启动默认录制<br />delay 录制延迟时间，默认0s<br />duration 录制持续时间，默认0s，表示无限<br />filename 录制文件的名称<br />compress 是否使用GZip压缩结果，默认否<br />maxage 数据最大生命周期，默认0s，表示无限制<br />maxsize 数据最大字节数，默认0，表示无限制</p>
</td>
</tr>
<tr>
<td>JFR.check</td>
<td>
<p>显示运行中的JFR的状态，参数：</p>
<p>name 记录名称<br />recording 记录标识符<br />verbose 打印冗余信息，默认否</p>
</td>
</tr>
<tr>
<td>JFR.stop</td>
<td>
<p>停止运行中的JFR，参数：</p>
<p>name 记录名称<br />recording 记录标识符<br />discard 是否丢弃录制数据<br />filename 拷贝录制数据到文件</p>
</td>
</tr>
<tr>
<td>JFR.dump</td>
<td>
<p>将录制中的JFR保存到文件，参数：</p>
<p>name 记录名称<br />recording 记录标识符<br />filename 拷贝录制数据到文件</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">黑匣子分析</span></div>
<p>打开黑匣子记录的.jfr文件，即可看到类似下面的界面：左侧是记录的不同视图，可以点击切换；右侧主面板，顶部是事件的时间轴，<span style="background-color: #c0c0c0;">拖动滑块，可以改变聚焦的时间范围（勾选右边的同步选择可以在切换视图时保持聚集范围）</span>，底部有若干选项卡。</p>
<p><a href="/wp-content/uploads/2014/11/jmc-3.jpg"><img class="alignnone  wp-image-2582" src="https://blog.gmem.cc/wp-content/uploads/2014/11/jmc-3.jpg" alt="jmc-3" width="917" height="653" /></a></p>
<p>下面，按照视图分别描述。</p>
<div class="blog_h2"><span class="graybg">基本信息</span></div>
<p>包含4个选项卡：</p>
<div class="blog_h3"><span class="graybg">总览</span></div>
<p style="padding-left: 30px;">可以看到聚焦时间范围内的实时仪表（堆占用、CPU占用、GC停顿时间），以及CPU、堆使用情况的曲线图。</p>
<div class="blog_h3"><span class="graybg">JVM信息</span></div>
<p style="padding-left: 30px;">可以看到JVM的一些基本信息，包括启动时间、JVM参数等。</p>
<div class="blog_h3"><span class="graybg">系统属性</span></div>
<p style="padding-left: 30px;">可以看到JVM的系统属性列表</p>
<div class="blog_h3"><span class="graybg">记录</span></div>
<p style="padding-left: 30px;">可以显示记录的事件的列表</p>
<div class="blog_h3"><span class="graybg">内存</span></div>
<p>包含6个选项卡</p>
<div class="blog_h3"><span class="graybg">总览</span></div>
<p style="padding-left: 30px;">Memory Usage：可以显示聚焦时间范围内OS总内存、OS已使用内存、提交的堆、使用的堆的大小曲线图。</p>
<p style="padding-left: 30px;">GC Configuration：显示简单的GC配置</p>
<p style="padding-left: 30px;">GC Statistics：显示简单的GC统计，包括垃圾回收次数、停顿时间</p>
<div class="blog_h3"><span class="graybg">垃圾收集</span></div>
<p style="padding-left: 30px;">上面的面板，包含4个选项卡：</p>
<p style="padding-left: 60px;">Heap：堆的使用情况曲线，红色小柱形显示了GC停顿的持续时间（刻度在右边）</p>
<p style="padding-left: 60px;">Reference Objects：包含软引用、弱引用、幻引用、一般引用的数量变化区域图</p>
<p style="padding-left: 60px;">Failed Promotions：失败的晋升列表，所谓晋升是指：年轻代对象向年老代转移的GC动作，包含对象个数和大小等信息</p>
<p style="padding-left: 60px;">Failed Evacuations：</p>
<p style="padding-left: 30px;">下面的面板，左侧显示历次GC的列表，右侧显示单次GC的详细信息，如下：</p>
<p style="padding-left: 60px;">General：显示GC类型、触发原因、ID、开始结束时间、暂停时间</p>
<p style="padding-left: 60px;">GC Phase：GC每个步骤的开始和持续时间</p>
<p style="padding-left: 60px;">Reference Objects：本次GC涉及的各类引用的数量</p>
<p style="padding-left: 60px;">Heap：堆在GC前后的大小对比</p>
<p style="padding-left: 60px;">Perm Gen：永久代在GC前后的大小对比</p>
<div class="blog_h3"><span class="graybg">垃圾回收时间</span></div>
<p style="padding-left: 30px;">显示各代、各次GC所消耗的时间</p>
<div class="blog_h3"><span class="graybg">垃圾回收配置</span></div>
<p style="padding-left: 30px;">显示GC的详细配置，包括GC基本配置、堆的配置、年轻代的配置</p>
<div class="blog_h3"><span class="graybg"> 内存分配</span></div>
<p>显示聚焦时间范围内的内存分配统计信息</p>
<p style="padding-left: 30px;">General：在线程<span style="background-color: #c0c0c0;">本地分配缓冲（TLAB）内、外（即Eden区域）</span>中分配的对象个数、内存大小。面板下部是内存分配大小按时间的柱形图</p>
<p style="padding-left: 30px;">Allocation in new TLAB：在TLAB中分配的内存，按对象类型、或者按执行分配的线程的占比饼图；Allocation Profile可以剖析分配内存热点所在的调用栈</p>
<p style="padding-left: 30px;">Allocation outside TLAB：在Eden区域的内存分配情况，包含的子选项卡同上</p>
<p>示例：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2014/11/jfr-memory-alloc.png"><img class="aligncenter wp-image-19061" src="https://blog.gmem.cc/wp-content/uploads/2014/11/jfr-memory-alloc.png" alt="jfr-memory-alloc" width="927" height="709" /></a></p>
<p>在Allocation By Class选项卡中：</p>
<ol>
<li>上面板Allocation Pressure，按对象类型，显示内存占用最大的对象</li>
<li>下面板Stack Trace，选中上面板的某个对象类型后，在此显示分配对象的调用栈。第一行为直接分配对象的代码</li>
</ol>
<div class="blog_h3"><span class="graybg">对象统计</span></div>
<p style="padding-left: 30px;">显示对象数量和内存占用的统计信息，包含上下两部分</p>
<p style="padding-left: 30px;">上面的面板：显示占有堆内存百分比大于0.5%的类型的列表</p>
<p style="padding-left: 30px;">下面的面板：显示增长数量最快的类型的列表</p>
<div class="blog_h2"><span class="graybg">代码</span></div>
<p>包含6个选项卡：</p>
<div class="blog_h3"><span class="graybg">总览</span></div>
<p style="padding-left: 30px;">显示占用CPU时间最多的代码，分为上下两部分</p>
<p style="padding-left: 30px;">上面的面板：显示CPU时间占用最多的包，包括列表和饼图</p>
<p style="padding-left: 30px;">下面的面板：显示CPU占用最多的类，按占比降序排列</p>
<div class="blog_h3"><span class="graybg">热点方法</span></div>
<p style="padding-left: 30px;">显示热点方法（CPU占用时间最多），按占比降序排列，点击左侧小箭头可以向下钻取（找到哪些代码调用了这个方法导致的热点）</p>
<div class="blog_h3"><span class="graybg">调用树</span></div>
<p style="padding-left: 30px;">类似于热点方法选项卡，但是会显示所有热点方法的完整调用栈</p>
<div class="blog_h3"><span class="graybg">异常</span></div>
<p style="padding-left: 30px;">包含3个选项卡，显示异常（包括错误）的统计信息</p>
<p style="padding-left: 30px;">Overview：显示异常和错误的总数、最频繁发生异常的调用栈</p>
<p style="padding-left: 30px;">Exceptions、Error：单独显示异常或者错误的总数、频繁调用栈，注意，异常默认是不记录的</p>
<div class="blog_h3"><span class="graybg">编译</span></div>
<p style="padding-left: 30px;">显示即时编译的次数、编译大小、失败的日志</p>
<div class="blog_h3"><span class="graybg">类加载</span></div>
<p style="padding-left: 30px;">显示按时间的类加载、卸载的数量曲线图</p>
<div class="blog_h2"><span class="graybg">线程</span></div>
<div class="blog_h3"><span class="graybg">总览</span></div>
<p style="padding-left: 30px;">分为上下两部分：</p>
<p style="padding-left: 30px;">上面的面板：显示OS、JVM的CPU占用率的按时间统计的区域图</p>
<p style="padding-left: 30px;">下面的面板：显示守护线程、活动线程数量的曲线图</p>
<div class="blog_h3"><span class="graybg">热点线程</span></div>
<p style="padding-left: 30px;">显示CPU占用最多的线程，及选中线程的热点方法，分为上下两部分</p>
<p style="padding-left: 30px;">上面的面板：显示CPU占用最多的线程，按占比降序排列</p>
<p style="padding-left: 30px;">下面的面板：显示上面板选中线程的热点方法、调用栈 </p>
<div class="blog_h3"><span class="graybg">资源争用</span></div>
<p style="padding-left: 30px;">显示因为锁争用导致的线程阻塞的统计信息，分为3个选项卡：</p>
<p style="padding-left: 60px;">Top Blocking Locks：发生阻塞最多的监视器（锁）对象的列表，包含了锁定次数、最长时间</p>
<p style="padding-left: 60px;">Top Blocked Threads：因为等待锁被阻塞时间最长的线程的列表</p>
<p style="padding-left: 60px;">Top Blocking Threads：因为占用锁导致其他线程等待时间最长的线程的列表</p>
<div class="blog_h3"><span class="graybg">延迟</span></div>
<p style="padding-left: 30px;">显示因为：Java线程休眠、监视器等待、Socket读取、监视器阻塞等原因导致的线程执行延迟的情况，包括延迟信息的列表和饼图，已经导致延迟的线程调用栈。延迟相关的事件包括：Java Thread Park（Waiting）、Java Monitor Wait（等待被唤醒）、Java Monitor  Blocked（积极进入同步区被阻塞）等。</p>
<div class="blog_h3"><span class="graybg">线程栈Dump</span></div>
<p style="padding-left: 30px;">包含定时捕获的JVM中所有线程的瞬时调用栈镜像</p>
<div class="blog_h3"><span class="graybg">监视器实例</span></div>
<p style="padding-left: 30px;">Lock Instances：监视器对象的列表，包含锁定次数、总计持续时间</p>
<p style="padding-left: 30px;">Trace Tree：选中监视器对象的锁定调用栈，每个调用栈锁定的次数</p>
<p style="padding-left: 30px;">Top Threads：占用监视器次数最多的线程列表</p>
<div class="blog_h2"><span class="graybg">输入输出</span></div>
<div class="blog_h3"><span class="graybg">总览</span></div>
<p style="padding-left: 30px;">分为两个选项卡</p>
<p style="padding-left: 30px;">File Overview：</p>
<p style="padding-left: 60px;">File Reads and Writes：显示按时间读写文件字节数的曲线</p>
<p style="padding-left: 60px;">File Read、File Write：显示针对具体文件的读写字节数、次数的列表</p>
<p style="padding-left: 30px;">Socket Overview：</p>
<p style="padding-left: 60px;">Socket Read and Writes：显示按时间读写Socket字节数的曲线</p>
<p style="padding-left: 60px;">Socket  Read、Socket Writes：显示针对具体目标主机、端口读写字节数、次数的列表</p>
<div class="blog_h3"><span class="graybg">读文件</span></div>
<p>按文件、线程、事件统计文件读操作的列表，以及每个文件读取按时间的变化、线程调用栈</p>
<div class="blog_h3"><span class="graybg">写文件</span></div>
<p>按文件、线程、事件统计文件写操作的列表，以及每个文件写入按时间的变化、线程调用栈</p>
<div class="blog_h3"><span class="graybg">读Socket</span></div>
<p>按目标主机、线程、事件统计文Socket读操作的列表，以及每个Socket读取按时间的变化、线程调用栈</p>
<div class="blog_h3"><span class="graybg">写Socket</span></div>
<p>按目标主机、线程、事件统计文Socket写操作的列表，以及每个Socket写入按时间的变化、线程调用栈</p>
<div class="blog_h2"><span class="graybg">系统</span></div>
<div>包含操作系统和硬件的基本信息</div>
<div class="blog_h2"><span class="graybg">事件</span></div>
<p>显示黑匣子记录的事件的相关信息，<span style="background-color: #c0c0c0;">左侧面板Event Types可以过滤</span>关注的事件类型。总体来说，事件可以分为三类：<span style="background-color: #c0c0c0;">持续事件</span>（duration，持续一段时间，例如GC事件）；<span style="background-color: #c0c0c0;">瞬时事件</span>（instant，立即发生和结束，例如内存溢出）；<span style="background-color: #c0c0c0;">样本事件</span>（sample，通过定期采样获取，例如栈dump）。</p>
<div class="blog_h3"><span class="graybg">总览</span></div>
<p>Producers：显示事件生产者的列表，以及制造事件的占比饼图</p>
<p style="padding-left: 30px;">Event Types：显示各类型事件持续总时间、次数，占比饼图</p>
<div class="blog_h3"><span class="graybg">日志</span></div>
<p style="padding-left: 30px;">显示每一个事件的记录，按时间排列，为每个事件显示：开始时间、结束时间、持续时间、产生事件的线程</p>
<div class="blog_h3"><span class="graybg">图表</span></div>
<p style="padding-left: 30px;">以时间轴的形式展示事件历史</p>
<div class="blog_h3"><span class="graybg">线程</span></div>
<p style="padding-left: 30px;">以列表形式展示各线程产生事件的数量、持续时间</p>
<div class="blog_h3"><span class="graybg">调用栈跟踪</span></div>
<p style="padding-left: 30px;">按产生事件持续时间长短，降序排列相关的调用栈</p>
<div class="blog_h3"><span class="graybg">事件统计</span></div>
<p style="padding-left: 30px;">可以按单个事件类型进行简单的统计分析，支持不同的分组方式，支持总数、平均数、次数等指标，选中单个统计结果，可以显示调用栈。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/jvm-monitoring-with-oracle-jmc">使用Oracle Java Mission Control监控JVM运行状态</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/jvm-monitoring-with-oracle-jmc/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>JProfiler学习笔记</title>
		<link>https://blog.gmem.cc/jprofiler-study-note</link>
		<comments>https://blog.gmem.cc/jprofiler-study-note#comments</comments>
		<pubDate>Mon, 22 Mar 2010 02:17:31 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[JVM]]></category>
		<category><![CDATA[学习笔记]]></category>
		<category><![CDATA[性能剖析]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=1908</guid>
		<description><![CDATA[<p>剖析视图 内存视图（Memory Views） 该视图专注于对象的内存分配情况，包含以下选项卡：  All Objects 显示堆中实时的对象情况：显示某种类型（聚合级别可以是类、包等）对象的数量（Instance Count）、浅尺寸（Shallow size）关于浅尺寸的计算： 普通对象大小的计算不包括引用、类变量，由变量本身地址、基本类型的变量占用的内存等组成 数组整体计算，不分为单个元素计算大小/数量 注意，有些对象虽然没有引用，但是可能没被GC回收，可以手工Run GC 右击某一类型，可以转到Class Tracker或者Heaper Walker视图 Recorded Objects 显示已经进行记录的对象。与All Objects相比，可以查看对象分配调用树（Allocation Call Tree）、分配热点（Allocation <a class="read-more" href="https://blog.gmem.cc/jprofiler-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/jprofiler-study-note">JProfiler学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">剖析视图</span></div>
<div class="blog_h3"><span class="graybg">内存视图（Memory Views）</span></div>
<p>该视图专注于对象的内存分配情况，包含以下选项卡：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<tbody>
<tr>
<td style="width: 150px;"> All Objects</td>
<td>显示堆中实时的对象情况：显示某种类型（聚合级别可以是类、包等）对象的数量（Instance Count）、浅尺寸（Shallow size）关于浅尺寸的计算：</p>
<ol>
<li>普通对象大小的计算不包括引用、类变量，由变量本身地址、基本类型的变量占用的内存等组成</li>
<li>数组整体计算，不分为单个元素计算大小/数量</li>
</ol>
<p>注意，有些对象虽然没有引用，但是可能没被GC回收，可以手工Run GC</p>
<p>右击某一类型，可以转到Class Tracker或者Heaper Walker视图</td>
</tr>
<tr>
<td>Recorded Objects</td>
<td>显示已经进行记录的对象。与All Objects相比，可以查看对象分配调用树（Allocation Call Tree）、分配热点（Allocation Hot Sports）点击工具栏：Start Memory，可以录制对象分配</td>
</tr>
<tr>
<td>Allocation Call Tree</td>
<td>显示选中的、已录制对象类型的分配调用数，分析调用栈的哪些栈帧比较消耗资源。从线程执行开始处，一直追溯到对象被创建的地方，对象占用内存的大小被显示，效果如下图：<img src="https://blog.gmem.cc/wp-content/uploads/2010/03/alloc-tree.jpg" alt="" /></td>
</tr>
<tr>
<td>Allocation Hot Sports</td>
<td>显示选中的、已录制对象类型的分配热点：以创建对象的方法为根显示调用树，效果如下图：<img src="https://blog.gmem.cc/wp-content/uploads/2010/03/alloc-hotsopt.jpg" alt="alloc-hotsopt" width="514" height="71" /></td>
</tr>
<tr>
<td>Class Tracker</td>
<td> 显示选中类型对象的实例个数的曲线图（按时间）</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">堆遍历（Heap Walker）</span></div>
<p>该视图用于静态分析堆快照，在生成快照之前，会进行一次Full GC。包含以下选项卡：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<tbody>
<tr>
<td style="width: 150px;">Classes</td>
<td>类似于Memory Views的All Objects，可以显示快照中所有对象的数量、大小。右击选择Use Selected Instances，则可以显示单种类型的数据</td>
</tr>
<tr>
<td>Allocations</td>
<td>分析对象分配情况：包括4种显示方式：Cumulated Allocation Tree：累积调用树，显示对象分配调用树，包含对象sizeAllocation Tree：调用树，显示对象分配调用树，包含对象sizeAllocation Tree Map：以Tree Map形式显示调用树Allocation Hot spots：显示分配对象最多的方法</td>
</tr>
<tr>
<td>Biggest Objects</td>
<td>显示占用内存最多的对象</td>
</tr>
<tr>
<td>References</td>
<td>显示单种类型的引用情况包含4种显示方式：Incoming references：其它对象对每一个选中对象的引用Outcoming references：每一个选中对象对其它对象的引用Cumulated Incoming references：显示其它对象、字段对选中对象类型的总体引用统计Cumulated Outcoming references：显示选中对象类型对其它对象、字段的总体引用统计</td>
</tr>
<tr>
<td>Time</td>
<td>分析对象分配的消耗时间</td>
</tr>
<tr>
<td>Inspections</td>
<td>提供多种精细的对象分配分析方式</td>
</tr>
<tr>
<td>Graph</td>
<td>图形化方式显示对象的引用图</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">CPU视图（CPU Views）</span></div>
<p>该视图用于分析方法的CPU占用时间，包括以下选项卡：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<tbody>
<tr>
<td> Call Tree</td>
<td>显示自顶而下的调用堆栈的树，树的根是线程开始处，树的分叉表示的不同的调用路径。暗红色显示当前节点及其子节点消耗的时间、高亮红色显示本级节点消耗的时间。</p>
<div>显示未过滤类对过滤类的直接调用，不显示深入的调用，在被过滤类的左上角显示<span style="color: #ff0000;"><strong>红三角</strong></span>。main方法、线程的run方法总是显示，不予过滤。</div>
<div>通过Profiling Settings可以选择CPU时间的度量方式：Elapsed time/Estimated CPU time，前者为Wall clock time（物理世界真实时间），后者为占用CPU的时间。由于操作系统的固有限制，10ms以下的时间可能不具有统计学意义。</div>
<div>界面右上角的Thread Status用于选择参与分析的线程状态，通常选择Runnable来分析性能问题，有时可以结合Net I/O、Blocked</div>
<div></div>
<div>工具栏Start Tracking，可以追踪某些任务在不同线程之间的传递（调用点/执行点Call site/Execution site）</div>
<p>效果如下图：<br />
<img src="https://blog.gmem.cc/wp-content/uploads/2010/03/call-tree.jpg" alt="call-tree" width="478" height="166" /></td>
</tr>
<tr>
<td style="width: 150px;"> Hot Spots</td>
<td>显示耗时最高的方法调用，显示消耗时间、平均消耗时间、调用次数等字段，以耗时方法为根，可以追溯到线程启动处Filtered Classes设置：选择被过滤类占用时间，可以单独列出，或者算在调用它的元素上（main、线程run不能过滤）<br />
每个方法可能被多个调用栈调用，百分比显示不同调用栈对此方法的固有时间的“贡献”<br />
不显示Total time，只显示Inherent Time</td>
</tr>
<tr>
<td>Call Graph</td>
<td>显示方法调用的序列，包括某个方法的调用与被调用情况</td>
</tr>
<tr>
<td>Method Stat</td>
<td>点击工具栏图标，可以录制方法的统计信息并进行分析</td>
</tr>
<tr>
<td>Call Tracer</td>
<td>点击工具栏图标，可以对方法调用进行跟踪</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">线程视图（Thread Views）</span></div>
<p>该视图用于分析线程的状态和历史分析、检测死锁，并可以获取线程快照，包含以下选项卡：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<tbody>
<tr>
<td style="width: 150px;">Thread History</td>
<td>线程状态时间线，可以选择显示活着的或者死去的线程</p>
<div>
<div><span style="font-family: 微软雅黑;"><b><span style="color: #328712;">绿色：Runnable</span></b>：表示线程可以接受CPU调度，但不一定正在占用CPU，和线程优先级、调度算法、系统负载有关</span></div>
<div><span style="font-family: 微软雅黑;"><b><span style="color: #ffaf00;">黄色：Waiting：</span></b>表示线程正在睡眠（java.lang.Thread.Sleep），或者在监视器上等待（java.lang.Object.wait），前者不会放弃占有的监视器</span></div>
<div><span style="font-family: 微软雅黑;"><b><span style="color: #e30000;">红色：Blocked：</span></b>表示线程被阻塞，正在积极尝试进入同步区</span></div>
<div><span style="font-family: 微软雅黑;"><b><span style="color: #1eccff;">浅蓝色：Net I/O：</span></b>线程正在等待网络操作的完成，监听Socket或者读写Socket时产生此状态</span></div>
<div>效果图如下：</div>
<div><img src="https://blog.gmem.cc/wp-content/uploads/2010/03/thread-history.jpg" alt="thread-history" width="497" height="78" /></div>
</div>
</td>
</tr>
<tr>
<td>Thread Monitor</td>
<td>显示线程的开始结束时间、父线程、状态等信息</td>
</tr>
<tr>
<td>Thread Dumps</td>
<td>获取线程快照，可以分析瞬时系统各线程的调用栈</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">监视器视图（Monitor Views）</span></div>
<p>该视图用于锁状态检测，包含以下选项卡：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<tbody>
<tr>
<td> Current Locking Graph</td>
<td>当前锁状态图，显示监视器、以及在监视器上等待的线程。</p>
<p><strong>黑色箭头</strong>表示当前拥有此监视器的线程</p>
<p><span style="color: #800000;"><strong>红色虚线</strong></span>表示被阻塞的线程（积极尝试获得锁）</p>
<p><span style="color: #ffcc00;"><strong>黄色箭头</strong></span>表示在监视器上等待的线程（java.lang.Object.wait）</p>
<p>效果图如下：</p>
<p><img src="https://blog.gmem.cc/wp-content/uploads/2010/03/current-monitor.jpg" alt="current-monitor" width="363" height="117" /></td>
</tr>
<tr>
<td>Current Monitors</td>
<td>显示当前活动的监视器，包括监视器状态、监视器类、等待线程、拥有者线程等信息</td>
</tr>
<tr>
<td style="width: 150px;">Locking History Graph</td>
<td>可以录制监视器锁定的历史</td>
</tr>
<tr>
<td>Monitor History</td>
<td>可以显示所有监视器的历史</td>
</tr>
<tr>
<td>Monitor Usage Stat</td>
<td>以监视器、线程等方式分组，显示监视器的使用统计，包括阻塞次数、阻塞持续时间、等待次数、等待持续时间等</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">虚拟机遥感视图（VM Telemetry Views）</span></div>
<p>该视图可以显示时间线上的系统宏观信息，包括以下选项卡：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<tbody>
<tr>
<td>Memory</td>
<td>显示已有、空闲内存的区域图</td>
</tr>
<tr>
<td>Recorded Objects</td>
<td>显示堆中对象（数组、非数组）的数量</td>
</tr>
<tr>
<td>Recorded Throughput</td>
<td>显示已记录对象的创建和销毁数量</td>
</tr>
<tr>
<td>GC Activity</td>
<td>显示GC活动率百分比</td>
</tr>
<tr>
<td>Classes</td>
<td>显示加载的类的数量</td>
</tr>
<tr>
<td>Threads</td>
<td>显示处于各种状态的线程的数量</td>
</tr>
<tr>
<td>CPU Load</td>
<td>显示CPU负载百分比</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">JavaEE以及探针（JEE &amp; Probes）</span></div>
<p>可以使用多种探针来检测不同的应用领域，包括：JDBC、JPA/Hibernate、JNDI、JMS、Servlet、RMI、Socket、WebServices、Files等。</p>
<div class="blog_h2"><span class="graybg">剖析配置</span></div>
<div class="blog_h3"><span class="graybg">触发器</span></div>
<p>用于在特定条件下触发剖析动作<br />
<strong>触发条件：</strong>方法被调用、堆使用阈值、CPU使用率阈值、内存溢出异常、定时器、虚拟机启动、虚拟机关闭等<br />
<strong>剖析动作：</strong>启/停录制、调用跟踪、监视器跟踪，触发堆dump、线程dump，启/停探针录制，保存快照，保存HPROF快照，运行脚本等</p>
<div class="blog_h2"><span class="graybg">离线剖析</span></div>
<p><strong>使用场景</strong>：脚本启动剖析、定期快照保存、剖析远程服务器</p>
<div><span style="color: #000000;">Session ID：需要指定id=xxx参数来激活相应的Profiling Settings，此ID显示在Session Settings - Application Settings的左上角，如果配置文件中只包含一个Session，则不需要配置。</span></div>
<div>
<div><span style="color: #000000;">配置文件位置：可以指定config=xxx.xml，来指明设置的存放位置，默认是~/.jprofiler7/config.xml</span></div>
<div>离线剖析命令行样例：</div>
<div>
<pre class="crayon-plain-tag">java 
"-agentpath:D:\JavaEE\jprofiler\7.2.2\bin\windows-x64\jprofilerti.dll=offline,id=109,config=D:\JavaEE\Config\jprofiler-offline.xml"
"-Xbootclasspath/a:D:\JavaEE\jprofiler\7.2.2\\bin\agent.jar"
-classpath myapp.jar cc.gmem.demo.MyApp</pre><br />
&nbsp;</p>
</div>
</div>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/jprofiler-study-note">JProfiler学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/jprofiler-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
