This commit is contained in:
by931
2022-09-06 22:30:37 +08:00
parent 66970f3e38
commit 3d6528675a
796 changed files with 3382 additions and 3382 deletions

View File

@@ -191,14 +191,14 @@ function hide_canvas() {
<p>近些年来无论是使用规模、开发者人数还是技术生态成熟度、相关工具的丰富程度Java 都当之无愧是后端开发语言中不可撼动的王者,也是开发各类业务系统的首选语言。</p>
<p>时至今日,整个 IT 招聘市场上Java 开发工程师依然是缺口最大,需求最多的热门职位。另外,从整个市场环境看,传统企业的信息化,传统 IT 系统的互联网化,都还有非常大的发展空间,由此推断未来 Java 开发的市场前景广阔,从业人员的行业红利还可以持续很长时间。</p>
<p>从权威的 TIOBE 编程语言排行榜 2019 年 11 月数据来看Java 的流行程度也是稳居第一。</p>
<p><img src="assets/1en79.jpg" alt="1c91b731-e86d-4b59-85b6-8b7ec53e87d6.jpg" /></p>
<p><img src="assets/arybg.jpg" alt="4fead80b-dc2e-4c40-852f-c4bd22bab207.jpg" /></p>
<p><img src="assets/1en79.jpg" alt="png" /></p>
<p><img src="assets/arybg.jpg" alt="png" /></p>
<p>拉勾网 2019 年 9 月统计的招聘岗位比例,也可以看到 Java 和 JavaScript 是最高的,不过 Java 的求职难度只有 JavaScript 的 1/7。</p>
<p><img src="assets/gdniz.jpg" alt="f5b072d7-2235-4814-ac63-3e90f0633629.jpg" /></p>
<p><img src="assets/gdniz.jpg" alt="png" /></p>
<p>Java 平均一个岗位有 4 个人竞争,而 JavaScript 则是 28 个Perl 最夸张,超过 30 个。</p>
<p><img src="assets/cloph.jpg" alt="d70b22b6-177c-443e-8ef1-957531028c60.jpg" /></p>
<p><img src="assets/cloph.jpg" alt="png" /></p>
<p>而通过职友网的数据统计,北京、上海、杭州、深圳的 Java 程序员平均薪酬在 16-21K 之间,在广州、成都、苏州、南京等城市也有 11K-13K 的平均收入,远超一般行业的收入水平。</p>
<p><img src="assets/xvr6f.jpg" alt="fd19dbb9-87e6-40bd-9d67-4455f1ee2513.jpg" /></p>
<p><img src="assets/xvr6f.jpg" alt="png" /></p>
<p>所以学习 Java 目前还是一个非常有优势的职业发展选择。</p>
<p>而了解 JVM 则是深入学习 Java 必不可少的一环,也是 Java 开发人员迈向更高水平的一个阶梯。我们不仅要会用 Java 写代码做系统,更要懂得如何理解和分析 Java 程序运行起来以后内部发生了什么,然后可以怎么让它运行的更好。</p>
<p>就像我们要想多年开车的老司机,仅仅会开车肯定不能当一个好司机。车开多了,总会有一些多多少少大大小小的故障毛病。老司机需要知道什么现象说明有了什么毛病,需要怎么处理,不然就会导致经常抛锚,影响我们的行程。</p>

View File

@@ -208,10 +208,10 @@ function hide_canvas() {
<li>JDK = JRE + 开发工具</li>
<li>JRE = JVM + 类库</li>
</ul>
<p><img src="assets/alvxv.png" alt="0.18346271077222331.png" /></p>
<p><img src="assets/alvxv.png" alt="png" /></p>
<p>三者在开发运行 Java 程序时的交互关系:</p>
<p>简单的说,就是通过 JDK 开发的程序,编译以后,可以打包分发给其他装有 JRE 的机器上去运行。而运行的程序,则是通过 Java 命令启动的一个 JVM 实例,代码逻辑的执行都运行在这个 JVM 实例上。</p>
<p><img src="assets/mbl7s.png" alt="0.9484384203409852.png" /></p>
<p><img src="assets/mbl7s.png" alt="png" /></p>
<p>Java 程序的开发运行过程为:</p>
<p>我们利用 JDK (调用 Java API开发 Java 程序,编译成字节码或者打包程序。然后可以用 JRE 则启动一个 JVM 实例,加载、验证、执行 Java 字节码以及依赖库,运行 Java 程序。而 JVM 将程序和依赖库的 Java 字节码解析并变成本地代码执行,产生结果。</p>
<h3>1.2 JDK 的发展过程与版本变迁</h3>
@@ -348,8 +348,8 @@ function hide_canvas() {
<p>常规的 JDK一般指 OpenJDK 或者 Oracle JDK当然 Oracle 还有一个新的 JVM 叫 GraalVM也非常有意思。除了 Sun/Oracle 的 JDK 以外,原 BEA 公司(已被 Oracle 收购)的 JRockitIBM 公司的 J9Azul 公司的 Zing JVM阿里巴巴公司的分支版本 DragonWell 等等。</p>
</blockquote>
<h3>1.3 安装 JDK</h3>
<p>JDK 通常是从 <a href="https://www.oracle.com/">Oracle 官网</a>下载, 打开页面翻到底部,找 <code>Java for Developers</code> 或者 <code>Developers</code>, 进入 <a href="https://www.oracle.com/technetwork/java/index.html">Java 相应的页面</a> 或者 <a href="https://www.oracle.com/technetwork/java/javase/overview/index.html">Java SE 相应的页面</a>, 查找 Download, 接受许可协议,下载对应的 x64 版本即可。 <img src="assets/tzok8.jpg" alt="891e2fe6-e872-4aa9-b00d-d176e947f11f.jpg" /></p>
<p>建议安装比较新的 JDK8 版本, 如 <a href="https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html">JDK8u231</a><img src="assets/igolf.png" alt="3bbdc5e9-149c-407d-b757-69a061581aae.png" /></p>
<p>JDK 通常是从 <a href="https://www.oracle.com/">Oracle 官网</a>下载, 打开页面翻到底部,找 <code>Java for Developers</code> 或者 <code>Developers</code>, 进入 <a href="https://www.oracle.com/technetwork/java/index.html">Java 相应的页面</a> 或者 <a href="https://www.oracle.com/technetwork/java/javase/overview/index.html">Java SE 相应的页面</a>, 查找 Download, 接受许可协议,下载对应的 x64 版本即可。 <img src="assets/tzok8.jpg" alt="png" /></p>
<p>建议安装比较新的 JDK8 版本, 如 <a href="https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html">JDK8u231</a><img src="assets/igolf.png" alt="png" /></p>
<blockquote>
<p>注意:从 Oracle 官方安装 JDK 需要注册和登录 Oracle 账号。现在流行将下载链接放到页面底部,很多工具都这样。当前推荐下载 JDK8。 今后 JDK11 可能成为主流版本,因为 Java11 是 LTS 长期支持版本,但可能还需要一些时间才会普及,而且 JDK11 的文件目录结构与之前不同, 很多工具可能不兼容其 JDK 文件的目录结构。</p>
</blockquote>
@@ -361,7 +361,7 @@ function hide_canvas() {
<blockquote>
<p>brew cask install java</p>
</blockquote>
<p>如果电脑上有 360 软件管家或者腾讯软件管家,也可以直接搜索和下载安装 JDK版本不是最新的但不用注册登录 Oracle 账号): <img src="assets/5rwjt.png" alt="035a0b3e-de33-4e97-946c-c9adb8b68ae7.png" /></p>
<p>如果电脑上有 360 软件管家或者腾讯软件管家,也可以直接搜索和下载安装 JDK版本不是最新的但不用注册登录 Oracle 账号): <img src="assets/5rwjt.png" alt="png" /></p>
<p>如果网络不好,可以从我的百度网盘共享获取:</p>
<blockquote>
<p><a href="https://pan.baidu.com/s/16WmRDZSiBD7a2PMjhSiGJw">https://pan.baidu.com/s/16WmRDZSiBD7a2PMjhSiGJw</a></p>
@@ -404,7 +404,7 @@ find / -name javac
<p>找到满足 <code>$JAVA_HOME/bin/javac</code> 的路径即可。</p>
<p>Windows 系统,安装在哪就是哪,默认在<code>C:\Program Files (x86)\Java</code>下。通过任务管理器也可以查看某个程序的路径,注意 <code>JAVA_HOME</code> 不可能是 <code>C:\Windows\System32</code> 目录。</p>
<p>然后我们就可以在 JDK 安装路径下看到很多 JVM 工具,例如在 Mac 上:</p>
<p><img src="assets/d5uc5.png" alt="54940291.png" /> 在后面的章节里,我们会详细解决其中一些工具的用法,以及怎么用它们来分析 JVM 情况。</p>
<p><img src="assets/d5uc5.png" alt="png" /> 在后面的章节里,我们会详细解决其中一些工具的用法,以及怎么用它们来分析 JVM 情况。</p>
<h3>1.4 验证 JDK 安装完成</h3>
<p>安装完成后Java 环境一般来说就可以使用了。 验证的脚本命令为:</p>
<pre><code class="language-shell">$ java -version

View File

@@ -190,13 +190,13 @@ function hide_canvas() {
<blockquote>
<p>前面一节课阐述了 JDK 的发展过程,以及怎么安装一个 JDK在正式开始进行 JVM 的内容之前,我们先了解一下性能相关的一些基本概念和原则。</p>
</blockquote>
<p><img src="assets/bnt0w.png" alt="0.260488235671565.png" /></p>
<p><img src="assets/bnt0w.png" alt="png" /></p>
<p>如果要问目前最火热的 JVM 知识是什么? 很多同学的答案可能是 “<code>JVM 调优</code>” 或者 “<code>JVM 性能优化</code>”。但是具体需要从哪儿入手,怎么去做呢?</p>
<p>其实“调优”是一个诊断和处理手段,我们最终的目标是让系统的处理能力,也就是“性能”达到最优化,这个过程我们就像是一个医生,诊断和治疗“应用系统”这位病人。我们以作为医生给系统看病作为对比,“性能优化”就是实现“把身体的大小毛病治好,身体达到最佳健康状态”的目标。</p>
<p>那么去医院看病,医生会是怎么一个处理流程呢?先简单的询问和了解基本情况,发烧了没有,咳嗽几天了,最近吃了什么,有没有拉肚子一类的,然后给患者开了一系列的检查化验单子:去查个血、拍个胸透、验个尿之类的。然后就会有医生使用各项仪器工具,依次把去做这些项目的检查,检查的结果就是很多标准化的具体指标(这里就是我们对 JVM 进行信息收集,变成各项指标)。</p>
<p>然后拿过来给医生诊断用,医生根据这些指标数据判断哪些是异常的,哪些是正常的,这些异常指标说明了什么问题(对系统问题进行分析排查),比如是白细胞增多(系统延迟和抖动增加,偶尔宕机),说明可能有炎症(比如 JVM 配置不合理)。最后要“对症下药”,开出一些阿莫西林或者头孢(对 JVM 配置进行调整),叮嘱怎么频率,什么时间点服药,如果问题比较严重,是不是要住院做手术(系统重构和调整),同时告知一些注意事项(对日常运维的要求和建议),最后经过一段时间治疗,逐渐好转,最终痊愈(系统延迟降低,不在抖动,不再宕机)。通过了解 JVM 去让我们具有分析和诊断能力,是本课程的核心主题。</p>
<h3>2.1 量化性能相关指标</h3>
<p><img src="assets/9iyxk.png" alt="0.7784482211178771.png" /></p>
<p><img src="assets/9iyxk.png" alt="png" /></p>
<p>&quot;没有量化就没有改进&quot;,所以我们需要先了解和度量性能指标,就像在医院检查以后得到的检验报告单一样。因为人的主观感觉是不靠谱的,个人经验本身也是无法复制的,而定义了量化的指标,就意味着我们有了一个客观度量体系。哪怕我们最开始定义的指标不是特别精确,我们也可以在使用过程中,随着真实的场景去验证指标有效性,进而替换或者调整指标,逐渐的完善这个量化的指标体系,成为一个可以复制和复用的有效工具。就像是上图的<code>血常规检查报告单</code>,一旦成为这种标准化的指标,那么使用它得到的结果,也就是这个报告单,给任何一个医生看,都是有效的,一般也能得到一致的判断结果。</p>
<p>那么系统性能的诊断要做些什么指标呢?我们先来考虑,进行要做诊断,那么程序或 JVM 可能出现了问题,而我们排查程序运行中出现的问题,比如排查程序 BUG 的时候,要优先保证正确性,这时候就不仅仅是 JVM 本身的问题,例如死锁等等,程序跑在 JVM 里,现象出现在 JVM 上,很多时候还要深入分析业务代码和逻辑确定 Java 程序哪里有问题。</p>
<ol>
@@ -229,7 +229,7 @@ function hide_canvas() {
<li><strong>业务需求指标</strong>:如吞吐量(QPS、TPS)、响应时间(RT)、并发数、业务成功率等。</li>
<li><strong>资源约束指标</strong>:如 CPU、内存、I/O 等资源的消耗情况。</li>
</ul>
<p><img src="assets/uc0za.png" alt="0.3186824516633562.png" /></p>
<p><img src="assets/uc0za.png" alt="png" /></p>
<blockquote>
<p>详情可参考: <a href="https://www.jianshu.com/p/62cf2690e6eb">性能测试中服务器关键性能指标浅析</a></p>
</blockquote>
@@ -246,7 +246,7 @@ function hide_canvas() {
<li>调整 JVM 启动参数GC 策略等等</li>
</ul>
<h3>2.3 性能调优总结</h3>
<p><img src="assets/wgj7v.png" alt="9b861ce8-8350-4943-ac1f-d6fb4fa2f127.png" /></p>
<p><img src="assets/wgj7v.png" alt="png" /></p>
<p>性能调优的第一步是制定指标,收集数据,第二步是找瓶颈,然后分析解决瓶颈问题。通过这些手段,找当前的性能极限值。压测调优到不能再优化了的 TPS 和 QPS就是极限值。知道了极限值我们就可以按业务发展测算流量和系统压力以此做容量规划准备机器资源和预期的扩容计划。最后在系统的日常运行过程中持续观察逐步重做和调整以上步骤长期改善改进系统性能。</p>
<p>我们经常说“<code>脱离场景谈性能都是耍流氓</code>”,实际的性能分析调优过程中,我们需要根据具体的业务场景,综合考虑成本和性能,使用最合适的办法去处理。系统的性能优化到 3000TPS 如果已经可以在成本可以承受的范围内满足业务发展的需求,那么再花几个人月优化到 3100TPS 就没有什么意义,同样地如果花一倍成本去优化到 5000TPS 也没有意义。</p>
<p>Donald Knuth 曾说过“<code>过早的优化是万恶之源</code>”,我们需要考虑在恰当的时机去优化系统。在业务发展的早期,量不大,性能没那么重要。我们做一个新系统,先考虑整体设计是不是 OK功能实现是不是 OK然后基本的功能都做得差不多的时候当然整体的框架是不是满足性能基准可能需要在做项目的准备阶段就通过 POC概念证明阶段验证。最后再考虑性能的优化工作。因为如果一开始就考虑优化就可能要想太多导致过度设计了。而且主体框架和功能完成之前可能会有比较大的改动一旦提前做了优化可能这些改动导致原来的优化都失效了又要重新优化多做了很多无用功。</p>

View File

@@ -197,7 +197,7 @@ function hide_canvas() {
<p>我们都知道 Java 是一种基于虚拟机的静态类型编译语言。那么常见的语言可以怎么分类呢?</p>
<h4>1编程语言分类</h4>
<p>首先,我们可以把形形色色的编程从底向上划分为最基本的三大类:机器语言、汇编语言、高级语言。</p>
<p><img src="assets/g6xl5.png" alt="66340662.png" /></p>
<p><img src="assets/g6xl5.png" alt="png" /></p>
<p>按《计算机编程语言的发展与应用》一文里的定义:计算机编程语言能够实现人与机器之间的交流和沟通,而计算机编程语言主要包括汇编语言、机器语言以及高级语言,具体内容如下:</p>
<ul>
<li>机器语言:这种语言主要是利用二进制编码进行指令的发送,能够被计算机快速地识别,其灵活性相对较高,且执行速度较为可观,机器语言与汇编语言之间的相似性较高,但由于具有局限性,所以在使用上存在一定的约束性。</li>
@@ -238,8 +238,8 @@ function hide_canvas() {
<p>现在我们聊聊跨平台,为什么要跨平台,因为我们希望所编写的代码和程序,在源代码级别或者编译后,可以运行在多种不同的系统平台上,而不需要为了各个平台的不同点而去实现两套代码。典型地,我们编写一个 web 程序,自然希望可以把它部署到 Windows 平台上,也可以部署到 Linux 平台上,甚至是 MacOS 系统上。</p>
<p>这就是跨平台的能力,极大地节省了开发和维护成本,赢得了商业市场上的一致好评。</p>
<p>这样来看,一般来说解释型语言都是跨平台的,同一份脚本代码,可以由不同平台上的解释器解释执行。但是对于编译型语言,存在两种级别的跨平台: 源码跨平台和二进制跨平台。</p>
<p>1、典型的源码跨平台C++ <img src="assets/2hieg.png" alt="71212109.png" /></p>
<p>2、典型的二进制跨平台Java 字节码): <img src="assets/987sb.png" alt="71237637.png" /></p>
<p>1、典型的源码跨平台C++ <img src="assets/2hieg.png" alt="png" /></p>
<p>2、典型的二进制跨平台Java 字节码): <img src="assets/987sb.png" alt="png" /></p>
<p>可以看到C++ 里我们需要把一份源码,在不同平台上分别编译,生成这个平台相关的二进制可执行文件,然后才能在相应的平台上运行。 这样就需要在各个平台都有开发工具和编译器,而且在各个平台所依赖的开发库都需要是一致或兼容的。 这一点在过去的年代里非常痛苦,被戏称为 “依赖地狱”。</p>
<p>C++ 的口号是“一次编写,到处(不同平台)编译”,但实际情况上是一编译就报错,变成了 “一次编写,到处调试,到处找依赖、改配置”。 大家可以想象,你编译一份代码,发现缺了几十个依赖,到处找还找不到,或者找到了又跟本地已有的版本不兼容,这是一件怎样令人绝望的事情。</p>
<p>而 Java 语言通过虚拟机技术率先解决了这个难题。 源码只需要编译一次,然后把编译后的 class 文件或 jar 包,部署到不同平台,就可以直接通过安装在这些系统中的 JVM 上面执行。 同时可以把依赖库jar 文件)一起复制到目标机器,慢慢地又有了可以在各个平台都直接使用的 Maven 中央库(类似于 linux 里的 yum 或 apt-get 源macos 里的 homebrew现代的各种编程语言一般都有了这种包依赖管理机制python 的 pipdotnet 的 nugetNodeJS 的 npmgolang 的 deprust 的 cargo 等等)。这样就实现了让同一个应用程序在不同的平台上直接运行的能力。</p>

View File

@@ -393,7 +393,7 @@ SourceFile: &quot;HelloByteCode.java&quot;
<p>想要深入了解字节码技术,我们需要先对字节码的执行模型有所了解。</p>
<p>JVM 是一台基于栈的计算机器。每个线程都有一个独属于自己的线程栈(JVM stack),用于存储<code>栈帧</code>(Frame)。每一次方法调用JVM都会自动创建一个栈帧。<code>栈帧</code><code>操作数栈</code> <code>局部变量数组</code> 以及一个<code>class 引用</code>组成。<code>class 引用</code> 指向当前方法在运行时常量池中对应的 class)。</p>
<p>我们在前面反编译的代码中已经看到过这些内容。</p>
<p><img src="assets/y6bxd.jpg" alt="c0463778-bb4c-43ab-9660-558d2897b364.jpg" /></p>
<p><img src="assets/y6bxd.jpg" alt="png" /></p>
<p><code>局部变量数组</code> 也称为 <code>局部变量表</code>(LocalVariableTable), 其中包含了方法的参数,以及局部变量。 局部变量数组的大小在编译时就已经确定: 和局部变量+形参的个数有关,还要看每个变量/参数占用多少个字节。操作数栈是一个 LIFO 结构的栈, 用于压入和弹出值。 它的大小也在编译时确定。</p>
<p>有一些操作码/指令可以将值压入“操作数栈”; 还有一些操作码/指令则是从栈中获取操作数,并进行处理,再将结果压入栈。操作数栈还用于接收调用其他方法时返回的结果值。</p>
<h3>4.7 方法体中的字节码解读</h3>
@@ -408,11 +408,11 @@ SourceFile: &quot;HelloByteCode.java&quot;
<p>例如, <code>new</code> 就会占用三个槽位: 一个用于存放操作码指令自身,两个用于存放操作数。</p>
<p>因此,下一条指令 <code>dup</code> 的索引从 <code>3</code> 开始。</p>
<p>如果将这个方法体变成可视化数组,那么看起来应该是这样的:</p>
<p><img src="assets/2wcmu.jpg" alt="2087a5ff-61b1-49ab-889e-698a73ceb41e.jpg" /></p>
<p><img src="assets/2wcmu.jpg" alt="png" /></p>
<p>每个操作码/指令都有对应的十六进制(HEX)表示形式, 如果换成十六进制来表示则方法体可表示为HEX字符串。例如上面的方法体百世成十六进制如下所示</p>
<p><img src="assets/76qr6.jpg" alt="b75bd86b-45c4-4b05-9266-1b7151c7038f.jpg" /></p>
<p><img src="assets/76qr6.jpg" alt="png" /></p>
<p>甚至我们还可以在支持十六进制的编辑器中打开 class 文件,可以在其中找到对应的字符串:</p>
<p><img src="assets/poywn.jpg" alt="9f8bf31f-e936-47c6-a3d1-f0c0de0fc898.jpg" /> 此图由开源文本编辑软件Atom的hex-view插件生成</p>
<p><img src="assets/poywn.jpg" alt="png" /> 此图由开源文本编辑软件Atom的hex-view插件生成</p>
<p>粗暴一点,我们可以通过 HEX 编辑器直接修改字节码,尽管这样做会有风险, 但如果只修改一个数值的话应该会很有趣。</p>
<p>其实要使用编程的方式,方便和安全地实现字节码编辑和修改还有更好的办法,那就是使用 ASM 和 Javassist 之类的字节码操作工具,也可以在类加载器和 Agent 上面做文章,下一节课程会讨论 <code>类加载器</code>,其他主题则留待以后探讨。</p>
<h3>4.8 对象初始化指令new 指令, init 以及 clinit 简介</h3>
@@ -452,13 +452,13 @@ SourceFile: &quot;HelloByteCode.java&quot;
<li><code>dup_x1</code> 将复制栈顶元素的值,并在栈顶插入两次(图中示例5)</li>
<li><code>dup2_x1</code> 则复制栈顶两个元素的值,并插入第三个值(图中示例6)。</li>
</ul>
<p><img src="assets/kg99w.jpg" alt="9d1a9509-c0ca-4320-983c-141257b0ddf5.jpg" /></p>
<p><img src="assets/kg99w.jpg" alt="png" /></p>
<p><code>dup_x1</code><code>dup2_x1</code> 指令看起来稍微有点复杂。而且为什么要设置这种指令呢? 在栈中复制最顶部的值?</p>
<p>请看一个实际案例:怎样交换 2 个 double 类型的值?</p>
<p>需要注意的是,一个 double 值占两个槽位,也就是说如果栈中有两个 double 值,它们将占用 4 个槽位。</p>
<p>要执行交换,你可能想到了 <code>swap</code> 指令,但问题是 <code>swap</code> 只适用于单字(one-word, 单字一般指 32 位 4 个字节64 位则是双字),所以不能处理 double 类型,但 Java 中又没有 swap2 指令。</p>
<p>怎么办呢? 解决方法就是使用 <code>dup2_x2</code> 指令,将操作数栈顶部的 double 值,复制到栈底 double 值的下方, 然后再使用 <code>pop2</code> 指令弹出栈顶的 double 值。结果就是交换了两个 double 值。 示意图如下图所示:</p>
<p><img src="assets/yttg7.jpg" alt="17ee9537-a42f-4a49-bb87-9a03735ab83a.jpg" /></p>
<p><img src="assets/yttg7.jpg" alt="png" /></p>
<h4><code>dup</code><code>dup_x1</code><code>dup2_x1</code> 指令补充说明</h4>
<p>指令的详细说明可参考 <a href="https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html">JVM 规范</a></p>
<p><strong>dup 指令</strong></p>
@@ -656,7 +656,7 @@ public class LocalVariableTest {
<p>关于 <code>LocalVariableTable</code> 有个有意思的事情,就是最前面的槽位会被方法参数占用。</p>
<p>在这里,因为 <code>main</code> 是静态方法所以槽位0中并没有设置为 <code>this</code> 引用的地址。 但是对于非静态方法来说, <code>this</code> 会将分配到第 0 号槽位中。</p>
<blockquote>
<p>再次提醒: 有过反射编程经验的同学可能比较容易理解: <code>Method#invoke(Object obj, Object... args)</code>; 有JavaScript编程经验的同学也可以类比: <code>fn.apply(obj, args) &amp;&amp; fn.call(obj, arg1, arg2);</code> <img src="assets/te9bw.jpg" alt="1e17af1a-6b6b-4992-a75c-9eac959bc467.jpg" /></p>
<p>再次提醒: 有过反射编程经验的同学可能比较容易理解: <code>Method#invoke(Object obj, Object... args)</code>; 有JavaScript编程经验的同学也可以类比: <code>fn.apply(obj, args) &amp;&amp; fn.call(obj, arg1, arg2);</code> <img src="assets/te9bw.jpg" alt="png" /></p>
</blockquote>
<p>理解这些字节码的诀窍在于:</p>
<p>给局部变量赋值时,需要使用相应的指令来进行 <code>store</code>,如 <code>astore_1</code><code>store</code> 类的指令都会删除栈顶值。 相应的 <code>load</code> 指令则会将值从局部变量表压入操作数栈,但并不会删除局部变量中的值。</p>
@@ -748,11 +748,11 @@ javap -c -verbose demo/jvm0104/ForLoopTest
<p>Java 字节码中有许多指令可以执行算术运算。实际上,指令集中有很大一部分表示都是关于数学运算的。对于所有数值类型(<code>int</code>, <code>long</code>, <code>double</code>, <code>float</code>),都有加,减,乘,除,取反的指令。</p>
<p>那么 <code>byte</code><code>char</code>, <code>boolean</code> 呢? JVM 是当做 <code>int</code> 来处理的。另外还有部分指令用于数据类型之间的转换。</p>
<blockquote>
<p>算术操作码和类型 <img src="assets/58uua.jpg" alt="30666bbb-50a0-4114-9675-b0626fd0167b.jpg" /></p>
<p>算术操作码和类型 <img src="assets/58uua.jpg" alt="png" /></p>
</blockquote>
<p>当我们想将 <code>int</code> 类型的值赋值给 <code>long</code> 类型的变量时,就会发生类型转换。</p>
<blockquote>
<p>类型转换操作码 <img src="assets/yzjfe.jpg" alt="e8c82cb5-6e86-4d52-90cc-40cde0fabaa0.jpg" /></p>
<p>类型转换操作码 <img src="assets/yzjfe.jpg" alt="png" /></p>
</blockquote>
<p>在前面的示例中, 将 <code>int</code> 值作为参数传递给实际上接收 <code>double</code><code>submit()</code> 方法时,可以看到, 在实际调用该方法之前,使用了类型转换的操作码:</p>
<pre><code> 31: iload 5

View File

@@ -219,7 +219,7 @@ function hide_canvas() {
<p>按照 Java 语言规范和 Java 虚拟机规范的定义, 我们用 “<code>类加载</code>(Class Loading)” 来表示: 将 class/interface 名称映射为 Class 对象的一整个过程。 这个过程还可以划分为更具体的阶段: 加载,链接和初始化(loading, linking and initializing)。</p>
<p>那么加载 class 的过程中到底发生了些什么呢?我们来详细看看。</p>
<h3>5.1 类的生命周期和加载过程</h3>
<p><img src="assets/6zd6i.png" alt="3de64ff2-77de-4468-af3a-c61bbb8cd944.png" /></p>
<p><img src="assets/6zd6i.png" alt="png" /></p>
<p>一个类在 JVM 里的生命周期有 7 个阶段分别是加载Loading、验证Verification、准备Preparation、解析Resolution、初始化Initialization、使用Using、卸载Unloading</p>
<p>其中前五个部分(加载,验证,准备,解析,初始化)统称为类加载,下面我们就分别来说一下这五个过程。</p>
<p>1<strong>加载</strong> 加载阶段也可以称为“装载”阶段。 这个阶段主要的操作是: 根据明确知道的 class 完全限定名, 来获取二进制 classfile 格式的字节流,简单点说就是找到文件系统中/jar 包中/或存在于任何地方的“<code>class 文件</code>”。 如果找不到二进制表示形式,则会抛出 <code>NoClassDefFound</code> 错误。</p>
@@ -284,14 +284,14 @@ function hide_canvas() {
<li>应用类加载器AppClassLoader</li>
</ul>
<p>一般启动类加载器是由 JVM 内部实现的,在 Java 的 API 里无法拿到,但是我们可以侧面看到和影响它(后面的内容会演示)。后 2 种类加载器在 Oracle Hotspot JVM 里,都是在中<code>sun.misc.Launcher</code>定义的,扩展类加载器和应用类加载器一般都继承自<code>URLClassLoader</code>类,这个类也默认实现了从各种不同来源加载 class 字节码转换成 Class 的方法。</p>
<p><img src="assets/esz0u.png" alt="c32f4986-0e72-4268-a90a-7451e1931161.png" /></p>
<p><img src="assets/esz0u.png" alt="png" /></p>
<ol>
<li>启动类加载器bootstrap class loader: 它用来加载 Java 的核心类,是用原生 C++ 代码来实现的,并不继承自 java.lang.ClassLoader负责加载JDK中jre/lib/rt.jar里所有的class。它可以看做是 JVM 自带的,我们再代码层面无法直接获取到启动类加载器的引用,所以不允许直接操作它, 如果打印出来就是个 <code>null</code>。举例来说java.lang.String 是由启动类加载器加载的,所以 String.class.getClassLoader() 就会返回 null。但是后面可以看到可以通过命令行参数影响它加载什么。</li>
<li>扩展类加载器extensions class loader它负责加载 JRE 的扩展目录lib/ext 或者由 java.ext.dirs 系统属性指定的目录中的 JAR 包的类,代码里直接获取它的父类加载器为 null因为无法拿到启动类加载器</li>
<li>应用类加载器app class loader它负责在 JVM 启动时加载来自 Java 命令的 -classpath 或者 -cp 选项、java.class.path 系统属性指定的 jar 包和类路径。在应用程序代码里可以通过 ClassLoader 的静态方法 getSystemClassLoader() 来获取应用类加载器。如果没有特别指定,则在没有使用自定义类加载器情况下,用户自定义的类都由此加载器加载。</li>
</ol>
<p>此外还可以自定义类加载器。如果用户自定义了类加载器,则自定义类加载器都以应用类加载器作为父加载器。应用类加载器的父类加载器为扩展类加载器。这些类加载器是有层次关系的,启动加载器又叫根加载器,是扩展加载器的父加载器,但是直接从 ExClassLoader 里拿不到它的引用,同样会返回 null。</p>
<p><img src="assets/csrk7.png" alt="8a806e88-cd41-4a28-b552-76efb0a1fdba.png" /></p>
<p><img src="assets/csrk7.png" alt="png" /></p>
<p>类加载机制有三个特点:</p>
<ol>
<li>双亲委托:当一个自定义类加载器需要加载一个类,比如 java.lang.String它很懒不会一上来就直接试图加载它而是先委托自己的父加载器去加载父加载器如果发现自己还有父加载器会一直往前找这样只要上级加载器比如启动类加载器已经加载了某个类比如 java.lang.String所有的子加载器都不需要自己加载了。如果几个类加载器都没有加载到指定名称的类那么会抛出 ClassNotFountException 异常。</li>

View File

@@ -200,7 +200,7 @@ function hide_canvas() {
<h3>6.1 JVM 内存结构</h3>
<p>我们先来看看 JVM 整体的内存概念图:</p>
<p>JVM 内部使用的 Java 内存模型, 在逻辑上将内存划分为 <code>线程栈</code>thread stacks<code>堆内存</code> heap两个部分。 如下图所示:</p>
<p><img src="assets/4pajs.jpg" alt="6f0f8921-0768-4d1d-8811-f27a8a6608a8.jpg" /></p>
<p><img src="assets/4pajs.jpg" alt="png" /></p>
<p>JVM 中,每个正在运行的线程,都有自己的线程栈。 线程栈包含了当前正在执行的方法链/调用链上的所有方法的状态信息。</p>
<p>所以线程栈又被称为“<code>方法栈</code>”或“<code>调用栈</code>call stack。线程在执行代码时调用栈中的信息会一直在变化。</p>
<p>线程栈里面保存了调用链上正在执行的所有方法中的局部变量。</p>
@@ -216,7 +216,7 @@ function hide_canvas() {
<li>不管是创建一个对象并将其赋值给局部变量, 还是赋值给另一个对象的成员变量, 创建的对象都会被保存到堆内存中。</li>
</ul>
<p>下图演示了线程栈上的调用栈和局部变量,以及存储在堆内存中的对象:</p>
<p><img src="assets/ksq1n.jpg" alt="91015fe2-53dc-477d-ba6d-fd0fe5e864e0.jpg" /></p>
<p><img src="assets/ksq1n.jpg" alt="png" /></p>
<ul>
<li>如果是原生数据类型的局部变量,那么它的内容就全部保留在线程栈上。</li>
<li>如果是对象引用,则栈中的局部变量槽位中保存着对象的引用地址,而实际的对象内容保存在堆中。</li>
@@ -230,21 +230,21 @@ function hide_canvas() {
<li>如果两个线程同时调用某个对象的同一方法,则它们都可以访问到这个对象的成员变量,但每个线程的局部变量副本是独立的。</li>
</ul>
<p>示意图如下所示:</p>
<p><img src="assets/6j9fe.jpg" alt="5eb89250-e803-44bb-8553-a2ae74fd01ba.jpg" /></p>
<p><img src="assets/6j9fe.jpg" alt="png" /></p>
<p>总结一下:虽然各个线程自己使用的局部变量都在自己的栈上,但是大家可以共享堆上的对象,特别地各个不同线程访问同一个对象实例的基础类型的成员变量,会给每个线程一个变量的副本。</p>
<h3>6.2 栈内存的结构</h3>
<p>根据以上内容和对 JVM 内存划分的理解,制作了几张逻辑概念图供大家参考。</p>
<p>先看看栈内存(Stack)的大体结构:</p>
<p><img src="assets/6yhj7.jpg" alt="dd71b714-e026-4679-b589-52c8b9226b6f.jpg" /></p>
<p><img src="assets/6yhj7.jpg" alt="png" /></p>
<p>每启动一个线程JVM 就会在栈空间栈分配对应的<strong>线程栈</strong>, 比如 1MB 的空间(<code>-Xss1m</code>)。</p>
<p>线程栈也叫做 Java 方法栈。 如果使用了 JNI 方法,则会分配一个单独的本地方法栈(Native Stack)。</p>
<p>线程执行过程中,一般会有多个方法组成调用栈(Stack Trace), 比如 A 调用 BB 调用 C……每执行到一个方法就会创建对应的<strong>栈帧</strong>(Frame)。</p>
<p><img src="assets/ze6dq.jpg" alt="6f9940a3-486f-4137-9420-123c9ae0826c.jpg" /></p>
<p><img src="assets/ze6dq.jpg" alt="png" /></p>
<p>栈帧是一个逻辑上的概念,具体的大小在一个方法编写完成后基本上就能确定。</p>
<p>比如 <code>返回值</code> 需要有一个空间存放吧,每个<code>局部变量</code>都需要对应的地址空间,此外还有给指令使用的 <code>操作数栈</code>,以及 class 指针(标识这个栈帧对应的是哪个类的方法, 指向非堆里面的 Class 对象)。</p>
<h3>6.3 堆内存的结构</h3>
<p>Java 程序除了栈内存之外,最主要的内存区域就是堆内存了。</p>
<p><img src="assets/u0xac.jpg" alt="706185c0-d264-4a7c-b0c3-e23184ab20b7.jpg" /></p>
<p><img src="assets/u0xac.jpg" alt="png" /></p>
<p>堆内存是所有线程共用的内存空间,理论上大家都可以访问里面的内容。</p>
<p>但 JVM 的具体实现一般会有各种优化。比如将逻辑上的 Java 堆,划分为<code>堆(Heap)</code><code>非堆(Non-Heap)</code>两个部分.。这种划分的依据在于,我们编写的 Java 代码,基本上只能使用 Heap 这部分空间,发生内存分配和回收的主要区域也在这部分,所以有一种说法,这里的 Heap 也叫 GC 管理的堆(GC Heap)。</p>
<p>GC 理论中有一个重要的思想,叫做分代。 经过研究发现,程序中分配的对象,要么用过就扔,要么就能存活很久很久。</p>
@@ -267,7 +267,7 @@ function hide_canvas() {
<p>写过程序的人都知道,同样的计算,可以有不同的实现方式。 硬件指令设计同样如此,比如说我们的系统需要实现某种功能,那么复杂点的办法就是在 CPU 中封装一个逻辑运算单元来实现这种的运算,对外暴露一个专用指令。</p>
<p>当然也可以偷懒,不实现这个指令,而是由程序编译器想办法用原有的那些基础的,通用指令来模拟和拼凑出这个功能。那么随着时间的推移,实现专用指令的 CPU 指令集就会越来越复杂, ,被称为复杂指令集。 而偷懒的 CPU 指令集相对来说就会少很多,甚至砍掉了很多指令,所以叫精简指令集计算机。</p>
<p>不管哪一种指令集CPU 的实现都是采用流水线的方式。如果 CPU 一条指令一条指令地执行,那么很多流水线实际上是闲置的。简单理解,可以类比一个 KFC 的取餐窗口就是一条流水线。于是硬件设计人员就想出了一个好办法: “<code>指令乱序</code>”。 CPU 完全可以根据需要,通过内部调度把这些指令打乱了执行,充分利用流水线资源,只要最终结果是等价的,那么程序的正确性就没有问题。但这在如今多 CPU 内核的时代,随着复杂度的提升,并发执行的程序面临了很多问题。</p>
<p><img src="assets/l13o1.jpg" alt="af56a365-b03b-46f6-94d0-2983ec2259d8.jpg" /></p>
<p><img src="assets/l13o1.jpg" alt="png" /></p>
<p>CPU 是多个核心一起执行,同时 JVM 中还有多个线程在并发执行,这种多对多让局面变得异常复杂,稍微控制不好,程序的执行结果可能就是错误的。</p>
<h3>6.5 JMM 背景</h3>
<p>目前的 JMM 规范对应的是 “<a href="https://jcp.org/en/jsr/detail?id=133">JSR-133. Java Memory Model and Thread Specification</a>这个规范的部分内容润色之后就成为了《Java语言规范》的 <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4">$17.4. Memory Model章节</a>。可以看到JSR133 的最终版修订时间是在 2014 年,这是因为之前的 Java 内存模型有些坑,所以在 Java 1.5 版本的时候进行了重新设计,并一直沿用到今天。</p>

View File

@@ -199,7 +199,7 @@ java [options] -jar filename [args]
</ul>
<p>如果是使用 Tomcat 之类自带 startup.sh 等启动脚本的程序,我们一般把相关参数都放到一个脚本定义的 JAVA_OPTS 环境变量中,最后脚本启动 JVM 时会把 JAVA_OPTS 变量里的所有参数都加到命令的合适位置。</p>
<p>如果是在 IDEA 之类的 IDE 里运行的话则可以在“Run/Debug Configurations”里看到 VM 选项和程序参数两个可以输入参数的地方,直接输入即可。</p>
<p><img src="assets/dwoev.png" alt="73146375.png" /></p>
<p><img src="assets/dwoev.png" alt="png" /></p>
<p>上图输入了两个 VM 参数,都是环境变量,一个是指定文件编码使用 UTF-8一个是设置了环境变量 a 的值为 1。</p>
<p>Java 和 JDK 内置的工具,指定参数时都是一个 <code>-</code>不管是长参数还是短参数。有时候JVM 启动参数和 Java 程序启动参数,并没必要严格区分,大致知道都是一个概念即可。</p>
<p>JVM 的启动参数, 从形式上可以简单分为:</p>

View File

@@ -791,7 +791,7 @@ jinfo -flags 36663
</code></pre>
<p>不加参数过滤,则打印所有信息。</p>
<p>jinfo 在 Windows 上比较稳定。在 macOS 上需要 root 权限,或是需要在提示下输入当前用户的密码。</p>
<p><img src="assets/780663e0-2608-11ea-aa89-e1cacb5bf377" alt="56345767.png" /></p>
<p><img src="assets/780663e0-2608-11ea-aa89-e1cacb5bf377" alt="png" /></p>
<p>然后就可以看到如下信息:</p>
<pre><code>jinfo 36663
Attaching to process ID 36663, please wait...

View File

@@ -193,9 +193,9 @@ function hide_canvas() {
<h3>JConsole</h3>
<p>JConsole顾名思义就是“Java 控制台”,在这里,我们可以从多个维度和时间范围去监控一个 Java 进程的内外部指标。进而通过这些指标数据来分析判断 JVM 的状态,为我们的调优提供依据。</p>
<p>在 Windows 或 macOS 的运行窗口或命令行输入 jconsole然后回车可以看到如下界面</p>
<p><img src="assets/ff1ebc80-279b-11ea-a18c-6b6633a37de4" alt="63078367.png" /></p>
<p><img src="assets/ff1ebc80-279b-11ea-a18c-6b6633a37de4" alt="png" /></p>
<p>本地进程列表列出了本机的所有 Java 进程(远程进程我们在 JMX 课程进行讲解),选择一个要连接的 Java 进程,点击连接,然后可以看到如下界面:</p>
<p><img src="assets/526825c0-279c-11ea-ab24-c1ca9407cc22" alt="63206281.png" /></p>
<p><img src="assets/526825c0-279c-11ea-ab24-c1ca9407cc22" alt="png" /></p>
<p>注意,点击右上角的绿色连接图标,即可连接或断开这个 Java 进程。</p>
<p>上图中显示了总共 6 个标签页,每个标签页对应一个监控面板,分别为:</p>
<ul>
@@ -221,10 +221,10 @@ function hide_canvas() {
</blockquote>
<p>当我们想关注最近 1 小时或者 1 分钟的数据,就可以选择对应的档。旁边的 3 个标签页(内存、线程、类),也都支持选择时间范围。</p>
<h4>内存</h4>
<p><img src="assets/e7685fa0-279c-11ea-a18c-6b6633a37de4" alt="63726065.png" /></p>
<p><img src="assets/e7685fa0-279c-11ea-a18c-6b6633a37de4" alt="png" /></p>
<p>内存监控,是 JConsole 中最常用的面板。内存面板的主区域中展示了内存占用量随时间变化的图像,可以通过这个图表,非常直观地判断内存的使用量和变化趋势。</p>
<p>同时在左上方,我们可以在图表后面的下拉框中选择不同的内存区:</p>
<p><img src="assets/f4dfcab0-279c-11ea-98b9-1d8ae01a003b" alt="65575723.png" /></p>
<p><img src="assets/f4dfcab0-279c-11ea-98b9-1d8ae01a003b" alt="png" /></p>
<p>本例中,我们使用的是 JDK 8默认不配置 GC 启动参数。关于 GC 参数的详情请关注后面的 GC 内容,可以看到,这个 JVM 提供的内存图表包括:</p>
<ul>
<li>堆内存使用量主要包括老年代内存池“PS Old Gen”、新生代“PS Eden Space”、存活区“PS Survivor Space”</li>
@@ -240,19 +240,19 @@ function hide_canvas() {
<p>打开一段时间以后,我们可以看到内存使用量出现了直线下降(见下图),这表明刚经过了一次 GC也就是 JVM 执行了垃圾回收。</p>
<p>其实我们可以注意到,内存面板其实相当于是 <code>jstat -gc</code><code>jstat -gcutil</code> 命令的图形化展示它们的本质是一样的都是通过采样的方式拿到JVM各个内存池的数据进行统计并展示出来。</p>
<p>其实图形界面存在一个问题,如果 GC 特别频繁,每秒钟执行了很多次 GC实际上图表方式就很难反应出每一次的变化信息。</p>
<p><img src="assets/4a573780-279d-11ea-a18c-6b6633a37de4" alt="64607499.png" /></p>
<p><img src="assets/4a573780-279d-11ea-a18c-6b6633a37de4" alt="png" /></p>
<h4>线程</h4>
<p>线程面板展示了线程数变化信息,以及监测到的线程列表。</p>
<ul>
<li>我们可以常根据名称直接查看线程的状态(运行还是等待中)和调用栈(正在执行什么操作)。</li>
<li>特别地,我们还可以直接点击“检测死锁”按钮来检测死锁,如果没有死锁则会提示“未检测到死锁”。</li>
</ul>
<p><img src="assets/54832110-279d-11ea-98c4-fbbd65598995" alt="64167338.png" /></p>
<p><img src="assets/54832110-279d-11ea-98c4-fbbd65598995" alt="png" /></p>
<h4></h4>
<p>类监控面板,可以直接看到 JVM 加载和卸载的类数量汇总信息。</p>
<p><img src="assets/6286c690-279d-11ea-967e-59cb267b8d93" alt="64205914.png" /></p>
<p><img src="assets/6286c690-279d-11ea-967e-59cb267b8d93" alt="png" /></p>
<h4>VM 概要</h4>
<p><img src="assets/6bd01850-279d-11ea-a0eb-1b545b6b1d6b" alt="64231816.png" /></p>
<p><img src="assets/6bd01850-279d-11ea-a0eb-1b545b6b1d6b" alt="png" /></p>
<p>VM 概要的数据也很有用,可以看到总共有五个部分:</p>
<ul>
<li>第一部分是虚拟机的信息;</li>
@@ -268,47 +268,47 @@ function hide_canvas() {
<p>$ <code>jvisualvm</code></p>
</blockquote>
<p>JVisualVM 启动后的界面大致如下:</p>
<p><img src="assets/cb980db0-279d-11ea-98b9-1d8ae01a003b" alt="58401878.png" /></p>
<p><img src="assets/cb980db0-279d-11ea-98b9-1d8ae01a003b" alt="png" /></p>
<p>在其中可以看到本地的 JVM 实例。</p>
<p>通过双击本地进程或者右键打开,就可以连接到某个 JVM此时显示的基本信息如下图所示</p>
<p><img src="assets/dd642ce0-279d-11ea-967e-59cb267b8d93" alt="20be819f-99e1-4f28-bfc6-6bc564777966.png" /></p>
<p><img src="assets/dd642ce0-279d-11ea-967e-59cb267b8d93" alt="png" /></p>
<p>可以看到,在概述页签中有 PID、启动参数、系统属性等信息。</p>
<p>切换到监视页签:</p>
<p><img src="assets/f3540520-279d-11ea-a18c-6b6633a37de4" alt="fe7bf60f-1e1a-4fae-81a4-49e854c73fed.png" /></p>
<p><img src="assets/f3540520-279d-11ea-a18c-6b6633a37de4" alt="png" /></p>
<p>在监视页签中可以看到 JVM 整体的运行情况。比如 CPU、堆内存、类、线程等信息。还可以执行一些操作比如“强制执行垃圾回收”、“堆 Dump”等。</p>
<p>&quot;线程&quot;页签则展示了 JVM 中的线程列表。再一次看出在程序中对线程(池)命名的好处。</p>
<p><img src="assets/0c1e74a0-279e-11ea-ab24-c1ca9407cc22" alt="70799622-072c-45fd-b5df-7c3ac1433061.png" /></p>
<p><img src="assets/0c1e74a0-279e-11ea-ab24-c1ca9407cc22" alt="png" /></p>
<p>与 JConsole 只能看线程的调用栈和状态信息相比,这里可以直观看到所有线程的状态颜色和运行时间,从而帮助我们分析过去一段时间哪些线程使用了较多的 CPU 资源。</p>
<h4>抽样器与 Profiler</h4>
<p>JVisualVM 默认情况下,比 JConsole 多了抽样器和 Profiler 这两个工具。</p>
<p>例如抽样,可以配合我们在性能压测的时候,看压测过程中,各个线程发生了什么、或者是分配了多少内存,每个类直接占用了多少内存等等。</p>
<p><img src="assets/1ec6c300-279e-11ea-b223-011b186c3530" alt="58663465.png" /></p>
<p><img src="assets/26b4f190-279e-11ea-89f3-21019b5e3a69" alt="58766362.png" /></p>
<p><img src="assets/1ec6c300-279e-11ea-b223-011b186c3530" alt="png" /></p>
<p><img src="assets/26b4f190-279e-11ea-89f3-21019b5e3a69" alt="png" /></p>
<p>使用 Profiler 时,需要先校准分析器。</p>
<p><img src="assets/2fdc8a80-279e-11ea-89f3-21019b5e3a69" alt="58910878.png" /></p>
<p><img src="assets/2fdc8a80-279e-11ea-89f3-21019b5e3a69" alt="png" /></p>
<p>然后可以像抽样器一样使用了。</p>
<p><img src="assets/385a75f0-279e-11ea-9b04-dd12b122c493" alt="59113954.png" /></p>
<p><img src="assets/3fabef50-279e-11ea-967e-59cb267b8d93" alt="59294077.png" /></p>
<p><img src="assets/385a75f0-279e-11ea-9b04-dd12b122c493" alt="png" /></p>
<p><img src="assets/3fabef50-279e-11ea-967e-59cb267b8d93" alt="png" /></p>
<p>从这个面板直接能看到热点方法与执行时间、占用内存以及比例,还可以设置过滤条件。</p>
<p>同时我们可以直接把当前的数据和分析,作为快照保存,或者将数据导出,以后可以继续加载和分析。</p>
<h4>插件</h4>
<p>JVisualVM 最强大的地方在于插件。</p>
<p>JDK 8 需要安装较高版本(如 Java SE 8u211才能从官方服务器安装/更新 JVisualVM 的插件(否则只能凭运气找对应的历史版本)。</p>
<p><img src="assets/57350530-279e-11ea-ab24-c1ca9407cc22" alt="8c352918-6e46-44c3-9081-0f0c7e57c581.png" /></p>
<p><img src="assets/57350530-279e-11ea-ab24-c1ca9407cc22" alt="png" /></p>
<p>JVisualVM 安装 MBeans 插件的步骤:</p>
<blockquote>
<p>通过工具(T)–插件(G)–可用插件–勾选具体的插件–安装–下一步–等待安装完成。</p>
</blockquote>
<p><img src="assets/79c6a900-279e-11ea-9b04-dd12b122c493" alt="b65b122e-53ea-4241-88bb-844a5cad65af.png" /></p>
<p><img src="assets/79c6a900-279e-11ea-9b04-dd12b122c493" alt="png" /></p>
<p>最常用的插件是 VisualGC 和 MBeans。</p>
<p>如果看不到可用插件,请安装最新版本,或者下载插件到本地安装。 先排除网络问题,或者检查更新,重新启动试试。</p>
<p><img src="assets/861e4c30-279e-11ea-a2ee-d3acdacb5a0b" alt="4b391dfa-1074-4084-9d37-b7f48779695c.png" /></p>
<p><img src="assets/861e4c30-279e-11ea-a2ee-d3acdacb5a0b" alt="png" /></p>
<p>安装完成后,重新连接某个 JVM即可看到新安装的插件。</p>
<p>切换到 VisualGC 页签:</p>
<p><img src="assets/a274a190-279e-11ea-967e-59cb267b8d93" alt="260031cf-d2f0-4ca2-904e-298d4fe3f7b1.png" /></p>
<p><img src="assets/a274a190-279e-11ea-967e-59cb267b8d93" alt="png" /></p>
<p>在其中可以看到各个内存池的使用情况以及类加载时间、GC 总次数、GC 总耗时等信息。比起命令行工具要简单得多。</p>
<p>切换到 MBeans 标签:</p>
<p><img src="assets/b7df1d30-279e-11ea-a37f-59408ceda1f2" alt="27e732cf-75f8-405e-8686-c2389948fc35.png" /></p>
<p><img src="assets/b7df1d30-279e-11ea-a37f-59408ceda1f2" alt="png" /></p>
<p>一般人可能不怎么关注 MBean但 MBean 对于理解 GC的原理倒是挺有用的。</p>
<p>主要看 java.lang 包下面的 MBean。比如内存池或者垃圾收集器等。</p>
<p>从图中可以看到Metaspace 内存池的 Type 是 NON_HEAP。</p>
@@ -325,7 +325,7 @@ function hide_canvas() {
</ul>
<p>根据经验这些信息对分析GC性能来说不能得出什么结论。只有编写程序获取GC相关的 JMX 信息来进行统计和分析。</p>
<p>下面看怎么执行远程实时监控。</p>
<p><img src="assets/1d1f8630-279f-11ea-ab24-c1ca9407cc22" alt="56b59ee5-2885-425d-a174-b5ea279f9bf6.png" /></p>
<p><img src="assets/1d1f8630-279f-11ea-ab24-c1ca9407cc22" alt="png" /></p>
<p>如上图所示,从文件菜单中,我们可以选择“添加远程主机”,以及“添加 JMX 连接”。</p>
<p>比如“添加 JMX 连接”,填上 IP 和端口号之后,勾选“不要求 SSL 连接”,点击“确定”按钮即可。</p>
<p>关于目标 JVM 怎么启动 JMX 支持,请参考后面的 JMX 小节。</p>
@@ -333,29 +333,29 @@ function hide_canvas() {
<h3>JMC 图形界面客户端</h3>
<p>JMC 和 JVisualVM 功能类似,因为 JMC 的前身是 JRMCJRMC 是 BEA 公司的 JRockit JDK 自带的分析工具,被 Oracle 收购以后,整合成了 JMC 工具。Oracle 试图用 JMC 来取代 JVisualVM在商业环境使用 JFR 需要付费获取授权。</p>
<p>在命令行输入 jmc 后,启动后的界面如下:</p>
<p><img src="assets/96c672e0-27a0-11ea-a2ee-d3acdacb5a0b" alt="ad3d63a9-6050-4f7d-af0c-5e6fab8e81a1.jpg" /></p>
<p><img src="assets/96c672e0-27a0-11ea-a2ee-d3acdacb5a0b" alt="png" /></p>
<p>点击相关的按钮或者菜单即可启用对应的功能JMC 提供的功能和 JVisualVM 差不多。</p>
<h4>飞行记录器</h4>
<p>除了 JConsole 和 JVisualVM 的常见功能(包括 JMX 和插件以外JMC 最大的亮点是飞行记录器。</p>
<p>在进程上点击“飞行记录器”以后,第一次使用时需要确认一下取消锁定商业功能的选项:</p>
<p><img src="assets/ce4fc8c0-279f-11ea-98b9-1d8ae01a003b" alt="59819531.png" /></p>
<p><img src="assets/ce4fc8c0-279f-11ea-98b9-1d8ae01a003b" alt="png" /></p>
<p>然后就可以看到飞行记录向导:</p>
<p><img src="assets/d6dc0c10-279f-11ea-a0eb-1b545b6b1d6b" alt="59881001.png" /></p>
<p><img src="assets/d6dc0c10-279f-11ea-a0eb-1b545b6b1d6b" alt="png" /></p>
<p>点击下一步可以看到更多的配置:</p>
<p><img src="assets/e3384320-279f-11ea-a0eb-1b545b6b1d6b" alt="59960019.png" /></p>
<p><img src="assets/e3384320-279f-11ea-a0eb-1b545b6b1d6b" alt="png" /></p>
<p>这里我们可以把堆内存分析、类加载两个选型也勾选上。点击完成,等待一分钟,就可以看到飞行记录。</p>
<p><img src="assets/eb086260-279f-11ea-98b9-1d8ae01a003b" alt="60125860.png" /></p>
<p><img src="assets/eb086260-279f-11ea-98b9-1d8ae01a003b" alt="png" /></p>
<p>概况里可以使用仪表盘方式查看堆内存、CPU 占用率、GC 暂停时间等数据。</p>
<p>内存面板则可以看到 GC 的详细分析:</p>
<p><img src="assets/fb882b70-279f-11ea-967e-59cb267b8d93" alt="60966956.png" /></p>
<p><img src="assets/055ad170-27a0-11ea-89f3-21019b5e3a69" alt="60997473.png" /></p>
<p><img src="assets/fb882b70-279f-11ea-967e-59cb267b8d93" alt="png" /></p>
<p><img src="assets/055ad170-27a0-11ea-89f3-21019b5e3a69" alt="png" /></p>
<p>代码面板则可以看到热点方法的执行情况:</p>
<p><img src="assets/143bdea0-27a0-11ea-a37f-59408ceda1f2" alt="60878569.png" /></p>
<p><img src="assets/143bdea0-27a0-11ea-a37f-59408ceda1f2" alt="png" /></p>
<p>线程面板则可以看到线程的锁争用情况等:</p>
<p><img src="assets/1e886f90-27a0-11ea-967e-59cb267b8d93" alt="61168308.png" /></p>
<p><img src="assets/1e886f90-27a0-11ea-967e-59cb267b8d93" alt="png" /></p>
<p>跟 JConsole 和 JVisualVM 相比这里已经有了很多分析数据了内存分配速率、GC 的平均时间等等。</p>
<p>最后,我们也可以通过保存飞行记录为 jfr 文件,以后随时查看和分析,或者发给其他人员来进行分析。</p>
<p><img src="assets/2de5ffc0-27a0-11ea-98c4-fbbd65598995" alt="60801271.png" /></p>
<p><img src="assets/2de5ffc0-27a0-11ea-98c4-fbbd65598995" alt="png" /></p>
<h3>JStatD 服务端工具</h3>
<p>JStatD 是一款强大的服务端支持工具,用于配合远程监控,所以放到图形界面这一篇介绍。</p>
<p>但因为涉及暴露一些服务器信息,所以需要配置安全策略文件。</p>

View File

@@ -268,11 +268,11 @@ jdb -attach 8888
<p>可以看到使用 JDB 调试的话非常麻烦,所以我们一般还是在开发工具 IDEIDEA、Eclipse里调试代码。</p>
<h2>开发工具 IDEA 中使用远程调试</h2>
<p>下面介绍 IDEA 中怎样使用远程调试。与常规的 Debug 配置类似,进入编辑:</p>
<p><img src="assets/5adef100-2c99-11ea-8c87-e5afec28626d" alt="749ef972-a71a-475a-a395-ab8e78db5fdf.png" /></p>
<p><img src="assets/5adef100-2c99-11ea-8c87-e5afec28626d" alt="png" /></p>
<p>添加 Remote不是 Tomcat 下面的那个 Remote Server</p>
<p><img src="assets/6d83bcf0-2c99-11ea-8c87-e5afec28626d" alt="f6a45f68-6c1c-4c55-90ae-eae35c2dafc3.png" /></p>
<p><img src="assets/6d83bcf0-2c99-11ea-8c87-e5afec28626d" alt="png" /></p>
<p>然后配置端口号,比如 8888。</p>
<p><img src="assets/7e457100-2c99-11ea-b3f5-45352c445237" alt="82bb5db4-9dc4-443d-9bb9-00864167c52f.png" /></p>
<p><img src="assets/7e457100-2c99-11ea-b3f5-45352c445237" alt="png" /></p>
<p>然后点击应用Apply按钮。</p>
<p>点击 Debug 的那个按钮即可启动远程调试,连上之后就和调试本地程序一样了。当然,记得加断点或者条件断点。</p>
<p><strong>注意</strong>:远程调试时,需要保证服务端 JVM 中运行的代码和本地完全一致,否则可能会有莫名其妙的问题。</p>

View File

@@ -302,11 +302,11 @@ public class MXBeanTest {
</blockquote>
<p>以 JConsole 为例,我们看一下,连接到了远程 JVM 以后,在最后一个面板即可看到 MBean 信息。</p>
<p>例如,我们可以查看 JVM 的一些信息:</p>
<p><img src="assets/58f59680-2d68-11ea-a743-8137f2e46c91" alt="enter image description here" /></p>
<p><img src="assets/58f59680-2d68-11ea-a743-8137f2e46c91" alt="png" /></p>
<p>也可以直接调用方法,例如查看 VM 参数:</p>
<p><img src="assets/629d8300-2d68-11ea-a743-8137f2e46c91" alt="enter image description here" /></p>
<p><img src="assets/629d8300-2d68-11ea-a743-8137f2e46c91" alt="png" /></p>
<p>如果启动的进程是 Tomcat 或者是 Spring Boot 启动的嵌入式 Tomcat那么我们还可以看到很多 Tomcat 的信息:</p>
<p><img src="assets/6bd52180-2d68-11ea-b8bc-595e31068820" alt="enter image description here" /></p>
<p><img src="assets/6bd52180-2d68-11ea-b8bc-595e31068820" alt="png" /></p>
<h2>JMX 的 MBean 创建和远程访问</h2>
<p>前面讲了在同一个 JVM 里获取 MBean现在我们再来写一个更完整的例子创建一个 MBean然后远程访问它。</p>
<p>先定义一个 UserMBean 接口(必须以 MBean 作为后缀):</p>
@@ -386,11 +386,11 @@ public class UserJmxServer {
<p>打开 JConsole在远程输入</p>
<pre><code>service:jmx:rmi:///jndi/rmi://localhost:1099/user
</code></pre>
<p><img src="assets/7c66ebf0-2d68-11ea-b6a8-fbb27a899f8c" alt="enter image description here" /></p>
<p><img src="assets/7c66ebf0-2d68-11ea-b6a8-fbb27a899f8c" alt="png" /></p>
<p>查看 User 的属性:</p>
<p><img src="assets/84cfa1b0-2d68-11ea-8fee-9f496e85c50f" alt="enter image description here" /></p>
<p><img src="assets/84cfa1b0-2d68-11ea-8fee-9f496e85c50f" alt="png" /></p>
<p>直接修改 UserName 的值:</p>
<p><img src="assets/8cb2acb0-2d68-11ea-b6fe-19eaa05aac2e" alt="enter image description here" /></p>
<p><img src="assets/8cb2acb0-2d68-11ea-b6fe-19eaa05aac2e" alt="png" /></p>
<h3>使用 JMX 远程访问 MBean</h3>
<p>我们先使用 JMXUrl 来创建一个 MBeanServerConnection连接到 MBeanServer然后就可以通过 ObjectName也可以看做是 MBean 的地址,像反射一样去拿服务器端 MBean 里的属性,或者调用 MBean 的方法。示例如下:</p>
<pre><code class="language-java">package io.github.kimmking.jvmstudy.jmx;

View File

@@ -188,7 +188,7 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>13 常见的 GC 算法GC 的背景与原理)</h1>
<p>GC 是英文词汇 Garbage Collection 的缩写中文一般直译为“垃圾收集”。当然有时候为了让文字更流畅也会说“垃圾回收”。一般认为“垃圾回收”和“垃圾收集”是同样的意思。此外GC 也有“垃圾收集器”的意思,英文表述为 Garbage Collector。本节我们就来详细讲解常用的 GC 算法。</p>
<p><img src="assets/60752af0-322d-11ea-a161-cb02df1dc5e6" alt="f37a9c99-3c14-4c8c-b285-0130a598c756.jpg" /></p>
<p><img src="assets/60752af0-322d-11ea-a161-cb02df1dc5e6" alt="png" /></p>
<h3>闲话 GC</h3>
<p>假如我们做生意,需要仓库来存放物资。如果所有仓库都需要公司自建,那成本就太高了,一般人玩不转,而且效率也不高,成本控制不好就很难赚到钱。所以现代社会就有了一种共享精神和租赁意识,大幅度提高了整个社会的资源利用率。</p>
<p>比如说一条供应链A 公司转给 B 公司B 公司转给 C 公司,那么每个公司自己的加工车间和私有仓库,就类似于线程空间,工厂内部会有相应的流水线。因为每个公司/业务员的精力有限,这个私有空间不可能无限大。</p>
@@ -213,9 +213,9 @@ function hide_canvas() {
<p>GC 垃圾收集器就像这个仓库部门,负责分配内存,负责追踪这些内存的使用情况,并在适当的时候进行释放。</p>
<p>于是仓库部门就建立起来,专门管理这些仓库。怎么管理呢?</p>
<p>先是想了一个办法,叫做“引用计数法”。有人办业务需要来申请仓库,就找个计数器记下次数 1后续哪个业务用到呢都需要登记一下继续加 1每个业务办完计数器就减一。如果一个仓库对象使用的内存的计数到降了 0就说明可以人使用这个仓库了我们就可以随时在方便的时候去归还/释放这个仓库。(需要注意:一般不是一个仓库到 0 了就立即释放,出于效率考虑,系统总是会等一批仓库一起处理,这样更加高效。)</p>
<p><img src="assets/7e9e1d70-322d-11ea-9a23-3953d44b4f10" alt="8442223.png" /></p>
<p><img src="assets/7e9e1d70-322d-11ea-9a23-3953d44b4f10" alt="png" /></p>
<p>但是呢,如果业务变得更复杂。仓库之间需要协同工作,有了依赖关系之后。</p>
<p><img src="assets/8a56e890-322d-11ea-9111-a36492d50563" alt="8648060.png" /></p>
<p><img src="assets/8a56e890-322d-11ea-9111-a36492d50563" alt="png" /></p>
<p>这时候单纯的引用计数就会出问题,循环依赖的仓库/对象没办法回收,就像数据库的死锁一样让人讨厌,你没法让它自己变成 0。</p>
<p>这种情况在计算机中叫做“内存泄漏”,该释放的没释放,该回收的没回收。</p>
<p>如果依赖关系更复杂,计算机的内存资源很可能用满,或者说不够用,内存不够用则称为“内存溢出”。</p>
@@ -253,7 +253,7 @@ function hide_canvas() {
<li>在创建新对象时JVM 在连续的块中分配内存。如果碎片问题很严重直至没有空闲片段能存放下新创建的对象就会发生内存分配错误allocation error</li>
</ul>
<p>要避免这类问题JVM 必须确保碎片问题不失控。因此在垃圾收集过程中不仅仅是标记和清除还需要执行“内存碎片整理”过程。这个过程让所有可达对象reachable objects依次排列以消除或减少碎片。就像是我们把棋盘上剩余的棋子都聚集到一起留出来足够大的空余区域。示意图如下所示</p>
<p><img src="assets/9ce4aa10-322d-11ea-9a23-3953d44b4f10" alt="5160496.png" /></p>
<p><img src="assets/9ce4aa10-322d-11ea-9a23-3953d44b4f10" alt="png" /></p>
<p><strong>说明</strong></p>
<p>JVM 中的引用是一个抽象的概念,如果 GC 移动某个对象,就会修改(栈和堆中)所有指向该对象的引用。</p>
<p>移动/拷贝/提升/压缩一般来说是一个 STW 的过程,所以修改对象引用是一个安全的行为。但要更新所有的引用,可能会影响应用程序的性能。</p>
@@ -264,13 +264,13 @@ function hide_canvas() {
<li>还有一部分不会立即无用,但也不会持续太长时间。</li>
</ul>
<p>这些观测形成了 <strong>弱代假设</strong>Weak Generational Hypothesis即我们可以根据对象的不同特点把对象进行分类。基于这一假设VM 中的内存被分为<strong>年轻代</strong>Young Generation<strong>老年代</strong>Old Generation。老年代有时候也称为<strong>年老区</strong>Tenured</p>
<p><img src="assets/ab551b20-322d-11ea-924d-0fd6db928ace" alt="5808335.png" /></p>
<p><img src="assets/ab551b20-322d-11ea-924d-0fd6db928ace" alt="png" /></p>
<p>拆分为这样两个可清理的单独区域,我们就可以根据对象的不同特点,允许采用不同的算法来大幅提高 GC 的性能。</p>
<p>天下没有免费的午餐所以这种方法也不是没有任何问题。例如在不同分代中的对象可能会互相引用在收集某一个分代时就会成为“事实上的”GC root。</p>
<p>当然,要着重强调的是,分代假设并不适用于所有程序。因为分代 GC 算法专门针对“要么死得快”、“否则活得长”这类特征的对象来进行优化,此时 JVM 管理那种存活时间半长不长的对象就显得非常尴尬了。</p>
<h3>内存池划分</h3>
<p>堆内存中的内存池划分也是类似的,不太容易理解的地方在于各个内存池中的垃圾收集是如何运行的。请注意:不同的 GC 算法在实现细节上可能会有所不同,但和本章所介绍的相关概念都是一致的。</p>
<p><img src="assets/b3855bc0-322d-11ea-a161-cb02df1dc5e6" alt="5921643.png" /></p>
<p><img src="assets/b3855bc0-322d-11ea-a161-cb02df1dc5e6" alt="png" /></p>
<h4><strong>新生代Eden Space</strong></h4>
<p>Eden Space也叫伊甸区是内存中的一个区域用来分配新创建的对象。通常会有多个线程同时创建多个对象所以 Eden 区被划分为多个 <strong>线程本地分配缓冲区</strong>Thread Local Allocation Buffer简称 TLAB。通过这种缓冲区划分大部分对象直接由 JVM 在对应线程的 TLAB 中分配,避免与其他线程的同步操作。</p>
<p>如果 TLAB 中没有足够的内存空间,就会在共享 Eden 区shared Eden space之中分配。如果共享 Eden 区也没有足够的空间,就会触发一次 年轻代 GC 来释放内存空间。如果 GC 之后 Eden 区依然没有足够的空闲内存区域则对象就会被分配到老年代空间Old Generation</p>
@@ -283,7 +283,7 @@ function hide_canvas() {
<h4><strong>存活区Survivor Spaces</strong></h4>
<p>Eden 区的旁边是两个<strong>存活区</strong>Survivor Spaces称为 from 空间和 to 空间。需要着重强调的的是任意时刻总有一个存活区是空的empty</p>
<p>空的那个存活区用于在下一次年轻代 GC 时存放收集的对象。年轻代中所有的存活对象(包括 Eden 区和非空的那个“from”存活区都会被复制到 ”to“ 存活区。GC 过程完成后“to”区有对象而“from”区里没有对象。两者的角色进行正好切换from 变成 toto 变成 from。</p>
<p><img src="assets/c5e75250-322d-11ea-b6b0-159b6a0308ab" alt="6202084.png" /></p>
<p><img src="assets/c5e75250-322d-11ea-b6b0-159b6a0308ab" alt="png" /></p>
<p>存活的对象会在两个存活区之间复制多次,直到某些对象的存活 时间达到一定的阀值。分代理论假设,存活超过一定时间的对象很可能会继续存活更长时间。</p>
<p>这类“年老”的对象因此被<strong>提升</strong>promoted到老年代。提升的时候存活区的对象不再是复制到另一个存活区而是迁移到老年代并在老年代一直驻留直到变为不可达对象。</p>
<p>为了确定一个对象是否“足够老”可以被提升Promotion到老年代GC 模块跟踪记录每个存活区对象存活的次数。每次分代 GC 完成后,存活对象的年龄就会增长。当年龄超过<strong>提升阈值</strong>tenuring threshold就会被提升到老年代区域。</p>
@@ -318,7 +318,7 @@ function hide_canvas() {
<p>第一步记录census所有的存活对象在垃圾收集中有一个叫做 <strong>标记Marking</strong> 的过程专门干这件事。</p>
<h4><strong>标记可达对象Marking Reachable Objects</strong></h4>
<p>现代 JVM 中所有的 GC 算法,第一步都是找出所有存活的对象。下面的示意图对此做了最好的诠释:</p>
<p><img src="assets/e575a130-322d-11ea-9111-a36492d50563" alt="6696297.png" /></p>
<p><img src="assets/e575a130-322d-11ea-9111-a36492d50563" alt="png" /></p>
<p>首先,有一些特定的对象被指定为 <strong>Garbage Collection Roots</strong>GC 根元素)。包括:</p>
<ul>
<li>当前正在执行的方法里的局部变量和输入参数</li>
@@ -336,15 +336,15 @@ function hide_canvas() {
<h4><strong>清除Sweeping</strong></h4>
<p>**Mark and Sweep标记—清除**算法的概念非常简单:直接忽略所有的垃圾。也就是说在标记阶段完成后,所有不可达对象占用的内存空间,都被认为是空闲的,因此可以用来分配新对象。</p>
<p>这种算法需要使用<strong>空闲表free-list</strong>,来记录所有的空闲区域,以及每个区域的大小。维护空闲表增加了对象分配时的开销。此外还存在另一个弱点 —— 明明还有很多空闲内存,却可能没有一个区域的大小能够存放需要分配的对象,从而导致分配失败(在 Java 中就是 <a href="https://plumbr.eu/outofmemoryerror">OutOfMemoryError</a>)。</p>
<p><img src="assets/f84e7570-322d-11ea-b6b0-159b6a0308ab" alt="6898662.png" /></p>
<p><img src="assets/f84e7570-322d-11ea-b6b0-159b6a0308ab" alt="png" /></p>
<h4><strong>整理Compacting</strong></h4>
<p><strong>标记—清除—整理算法Mark-Sweep-Compact</strong>,将所有被标记的对象(存活对象),迁移到内存空间的起始处,消除了“标记—清除算法”的缺点。</p>
<p>相应的缺点就是 GC 暂停时间会增加,因为需要将所有对象复制到另一个地方,然后修改指向这些对象的引用。</p>
<p>此算法的优势也很明显碎片整理之后分配新对象就很简单只需要通过指针碰撞pointer bumping即可。使用这种算法内存空间剩余的容量一直是清楚的不会再导致内存碎片问题。</p>
<p><img src="assets/034a8040-322e-11ea-a161-cb02df1dc5e6" alt="7068361.png" /></p>
<p><img src="assets/034a8040-322e-11ea-a161-cb02df1dc5e6" alt="png" /></p>
<h4><strong>复制Copying</strong></h4>
<p>**标记—复制算法Mark and Copy**和“标记—整理算法”Mark and Compact十分相似两者都会移动所有存活的对象。区别在于“标记—复制算法”是将内存移动到另外一个空间存活区。“标记—复制方法”的优点在于标记和复制可以同时进行。缺点则是需要一个额外的内存区间来存放所有的存活对象。</p>
<p><img src="assets/1033dc20-322e-11ea-b373-e57ecc18caf9" alt="7149973.png" /></p>
<p><img src="assets/1033dc20-322e-11ea-b373-e57ecc18caf9" alt="png" /></p>
<p>下一小节,我们将介绍 JVM 中具体的 GC 算法和实现。</p>
</div>
</div>

View File

@@ -234,17 +234,17 @@ function hide_canvas() {
<p>为什么 CMS 不管年轻代了呢?前面不是刚刚完成 minor GC 嘛,再去收集年轻代估计也没什么效果。</p>
</blockquote>
<p>看看示意图:</p>
<p><img src="assets/caf715d0-32ed-11ea-8c11-e7b43c5f4201" alt="54201932.png" /></p>
<p><img src="assets/caf715d0-32ed-11ea-8c11-e7b43c5f4201" alt="png" /></p>
<h4><strong>阶段 2Concurrent Mark并发标记</strong></h4>
<p>在此阶段CMS GC 遍历老年代标记所有的存活对象从前一阶段“Initial Mark”找到的根对象开始算起。“并发标记”阶段就是与应用程序同时运行不用暂停的阶段。请注意并非所有老年代中存活的对象都在此阶段被标记因为在标记过程中对象的引用关系还在发生变化。</p>
<p><img src="assets/f30d6240-32ed-11ea-aa6e-a7e7fcb8af6c" alt="80365661.png" /></p>
<p><img src="assets/f30d6240-32ed-11ea-aa6e-a7e7fcb8af6c" alt="png" /></p>
<p>在上面的示意图中,“当前处理的对象”的一个引用就被应用线程给断开了,即这个部分的对象关系发生了变化(下面会讲如何处理)。</p>
<h4><strong>阶段 3Concurrent Preclean并发预清理</strong></h4>
<p>此阶段同样是与应用线程并发执行的,不需要停止应用线程。</p>
<p>因为前一阶段“并发标记”与程序并发运行可能有一些引用关系已经发生了改变。如果在并发标记过程中引用关系发生了变化JVM 会通过“Card卡片”的方式将发生了改变的区域标记为“脏”区这就是所谓的“卡片标记Card Marking”。</p>
<p><img src="assets/12b5efe0-32ee-11ea-9390-6160376f1fda" alt="82347169.png" /></p>
<p><img src="assets/12b5efe0-32ee-11ea-9390-6160376f1fda" alt="png" /></p>
<p>在预清理阶段,这些脏对象会被统计出来,它们所引用的对象也会被标记。此阶段完成后,用以标记的 card 也就会被清空。</p>
<p><img src="assets/3d254780-32ee-11ea-9e4a-871af6a0c6b3" alt="82835555.png" /></p>
<p><img src="assets/3d254780-32ee-11ea-9e4a-871af6a0c6b3" alt="png" /></p>
<p>此外,本阶段也会进行一些必要的细节处理,还会为 Final Remark 阶段做一些准备工作。</p>
<h4><strong>阶段 4Concurrent Abortable Preclean可取消的并发预清理</strong></h4>
<p>此阶段也不停止应用线程。本阶段尝试在 STW 的 Final Remark 阶段 之前尽可能地多做一些工作。本阶段的具体时间取决于多种因素,因为它循环做同样的事情,直到满足某个退出条件(如迭代次数,有用工作量,消耗的系统时间等等)。</p>
@@ -256,7 +256,7 @@ function hide_canvas() {
<p>在 5 个标记阶段完成之后,老年代中所有的存活对象都被标记了,然后 GC 将清除所有不使用的对象来回收老年代空间。</p>
<h4><strong>阶段 6Concurrent Sweep并发清除</strong></h4>
<p>此阶段与应用程序并发执行,不需要 STW 停顿。JVM 在此阶段删除不再使用的对象,并回收它们占用的内存空间。</p>
<p><img src="assets/4b92f970-32ee-11ea-8c11-e7b43c5f4201" alt="85886580.png" /></p>
<p><img src="assets/4b92f970-32ee-11ea-8c11-e7b43c5f4201" alt="png" /></p>
<h4><strong>阶段 7Concurrent Reset并发重置</strong></h4>
<p>此阶段与应用程序并发执行,重置 CMS 算法相关的内部数据,为下一次 GC 循环做准备。</p>
<p>总之CMS 垃圾收集器在减少停顿时间上做了很多复杂而有用的工作用于垃圾回收的并发线程执行的同时并不需要暂停应用线程。当然CMS 也有一些缺点,其中最大的问题就是老年代内存碎片问题(因为不压缩),在某些情况下 GC 会造成不可预测的暂停时间,特别是堆内存较大的情况下。</p>
@@ -269,9 +269,9 @@ function hide_canvas() {
<h4><strong>G1 GC 的特点</strong></h4>
<p>为了达成可预期停顿时间的指标G1 GC 有一些独特的实现。</p>
<p>首先,堆不再分成年轻代和老年代,而是划分为多个(通常是 2048 个)可以存放对象的 小块堆区域smaller heap regions。每个小块可能一会被定义成 Eden 区,一会被指定为 Survivor 区或者 Old 区。在逻辑上,所有的 Eden 区和 Survivor 区合起来就是年轻代,所有的 Old 区拼在一起那就是老年代,如下图所示:</p>
<p><img src="assets/60da5cb0-32ee-11ea-8c11-e7b43c5f4201" alt="4477357.png" /></p>
<p><img src="assets/60da5cb0-32ee-11ea-8c11-e7b43c5f4201" alt="png" /></p>
<p>这样划分之后,使得 G1 不必每次都去收集整个堆空间,而是以增量的方式来进行处理:每次只处理一部分内存块,称为此次 GC 的回收集collection set。每次 GC 暂停都会收集所有年轻代的内存块,但一般只包含部分老年代的内存块,见下图带对号的部分:</p>
<p><img src="assets/69d8c2c0-32ee-11ea-8c11-e7b43c5f4201" alt="36113613.png" /></p>
<p><img src="assets/69d8c2c0-32ee-11ea-8c11-e7b43c5f4201" alt="png" /></p>
<p>G1 的另一项创新是,在并发阶段估算每个小堆块存活对象的总数。构建回收集的原则是:<strong>垃圾最多的小块会被优先收集</strong>。这也是 G1 名称的由来。</p>
<p>通过以下选项来指定 G1 垃圾收集器:</p>
<pre><code>-XX:+UseG1GC -XX:MaxGCPauseMillis=50
@@ -334,13 +334,13 @@ function hide_canvas() {
<p>Remembered Sets历史记忆集用来支持不同的小堆块进行独立回收。</p>
<p>例如,在回收小堆块 A、B、C 时,我们必须要知道是否有从 D 区或者 E 区指向其中的引用,以确定它们的存活性. 但是遍历整个堆需要相当长的时间,这就违背了增量收集的初衷,因此必须采取某种优化手段。类似于其他 GC 算法中的“卡片”方式来支持年轻代的垃圾收集G1 中使用的则是 Remembered Sets。</p>
<p>如下图所示,每个小堆块都有一个 <strong>Remembered Set</strong>,列出了从外部指向本块的所有引用。这些引用将被视为附加的 GC 根。注意,在并发标记过程中,老年代中被确定为垃圾的对象会被忽略,即使有外部引用指向它们:因为在这种情况下引用者也是垃圾(如垃圾对象之间的引用或者循环引用)。</p>
<p><img src="assets/a8fc6560-32ee-11ea-a438-7fd5b76593d7" alt="79450295.png" /></p>
<p><img src="assets/a8fc6560-32ee-11ea-a438-7fd5b76593d7" alt="png" /></p>
<p>接下来的行为,和其他垃圾收集器一样:多个 GC 线程并行地找出哪些是存活对象,确定哪些是垃圾:</p>
<p><img src="assets/b0140a10-32ee-11ea-b32d-892c82ec4027" alt="79469787.png" /></p>
<p><img src="assets/b0140a10-32ee-11ea-b32d-892c82ec4027" alt="png" /></p>
<p>最后存活对象被转移到存活区survivor regions在必要时会创建新的小堆块。现在空的小堆块被释放可用于存放新的对象了。</p>
<p><img src="assets/b806cc80-32ee-11ea-96bc-d1519da8f09a" alt="79615062.png" /></p>
<p><img src="assets/b806cc80-32ee-11ea-96bc-d1519da8f09a" alt="png" /></p>
<h3>GC 选择的经验总结</h3>
<p><img src="assets/bf6df0c0-32ee-11ea-b0e0-6da2f5afc39e" alt="72433648.png" /></p>
<p><img src="assets/bf6df0c0-32ee-11ea-b0e0-6da2f5afc39e" alt="png" /></p>
<p>通过本节内容的学习,你应该对 G1 垃圾收集器有了一定了解。当然为了简洁我们省略了很多实现细节例如如何处理“巨无霸对象humongous objects”。</p>
<p>综合来看G1 是 JDK11 之前 HotSpot JVM 中最先进的<strong>准产品级production-ready</strong> 垃圾收集器。重要的是HotSpot 工程师的主要精力都放在不断改进 G1 上面。在更新的 JDK 版本中,将会带来更多强大的功能和优化。</p>
<p>可以看到G1 作为 CMS 的代替者出现,解决了 CMS 中的各种疑难问题,包括暂停时间的可预测性,并终结了堆内存的碎片化。对单业务延迟非常敏感的系统来说,如果 CPU 资源不受限制,那么 G1 可以说是 HotSpot 中最好的选择,特别是在最新版本的 JVM 中。当然这种降低延迟的优化也不是没有代价的由于额外的写屏障和守护线程G1 的开销会更大。如果系统属于吞吐量优先型的,又或者 CPU 持续占用 100%,而又不在乎单次 GC 的暂停时间,那么 CMS 是更好的选择。</p>

View File

@@ -257,12 +257,12 @@ Option -XX:+UseZGC not supported
<p>官方介绍说停顿时间在 10ms 以下,其实这个数据是非常保守的值。</p>
<p>根据基准测试(见参考材料里的 PDF 链接),在 128G 的大堆下,最大停顿时间只有 1.68ms,远远低于 10ms和 G1 算法比起来相比,改进非常明显。</p>
<p>请看下图:</p>
<p><img src="assets/92814ed0-4da2-11ea-a221-c1e42d9b4512" alt="9324cf7d-ab45-4620-9661-2035e3f1b3d2.png" /></p>
<p><img src="assets/92814ed0-4da2-11ea-a221-c1e42d9b4512" alt="png" /></p>
<p>左边的图是线性坐标,右边是指数坐标。</p>
<p>可以看到不管是平均值、95 线、99 线还是最大暂停时间ZGC 都优胜于 G1 和并行 GC 算法。</p>
<p>根据我们在生产环境的监控数据来看16G~64G 堆内存),每次暂停都不超过 3ms。</p>
<p>比如下图是一个低延迟网关系统的监控信息,几十 GB 的堆内存环境中ZGC 表现得毫无压力,暂停时间非常稳定。</p>
<p><img src="assets/c798ebf0-4da2-11ea-9765-e162b5bc6395" alt="68469069.png" /></p>
<p><img src="assets/c798ebf0-4da2-11ea-9765-e162b5bc6395" alt="png" /></p>
<p>像 G1 和 ZGC 之类的现代 GC 算法,只要空闲的堆内存足够多,基本上不触发 FullGC。</p>
<p>所以很多时候,只要条件允许,加内存才是最有效的解决办法。</p>
<p>既然低延迟是 ZGC 的核心看点,而 JVM 低延迟的关键是 GC 暂停时间,那么我们来看看有哪些方法可以减少 GC 暂停时间:</p>
@@ -284,7 +284,7 @@ Option -XX:+UseZGC not supported
</blockquote>
<h4><strong>ZGC 的原理</strong></h4>
<p>ZCG 的 GC 周期如图所示:</p>
<p><img src="assets/3011a720-4da4-11ea-bf6a-a70b6b051b63" alt="37037772.png" /></p>
<p><img src="assets/3011a720-4da4-11ea-bf6a-a70b6b051b63" alt="png" /></p>
<p>每个 GC 周期分为 6 个小阶段:</p>
<ol>
<li>暂停—标记开始阶段:第一次暂停,标记根对象集合指向的对象;</li>
@@ -305,11 +305,11 @@ Option -XX:+UseZGC not supported
<p>ZGC 使用着色指针来标记所处的 GC 阶段。</p>
<p>着色指针是从 64 位的指针中,挪用了几位出来标识表示 Marked0、Marked1、Remapped、Finalizable。所以不支持 32 位系统,也不支持指针压缩技术,堆内存的上限是 4TB。</p>
<p>从这些标记上就可以知道对象目前的状态,判断是不是可以执行清理压缩之类的操作。</p>
<p><img src="assets/c5a2af50-4da4-11ea-aeb5-6d255c028296" alt="0.21570593705169117.png" /></p>
<p><img src="assets/c5a2af50-4da4-11ea-aeb5-6d255c028296" alt="png" /></p>
<h4><strong>读屏障</strong></h4>
<p>对于 GC 线程与用户线程并发执行时业务线程修改对象的操作可能带来的不一致问题ZGC 使用的是读屏障,这点与其他 GC 使用写屏障不同。</p>
<p>有读屏障在,就可以留待之后的其他阶段,根据指针颜色快速的处理。并且不是所有的读操作都需要屏障,例如下面只有第一种语句(加载指针时)需要读屏障,后面三种都不需要,又或者是操作原生类型的时候也不需要。</p>
<p><img src="assets/d5ea2a50-4da4-11ea-9765-e162b5bc6395" alt="73cd6f97-8730-4aff-ae48-b8a30f3eaae0.jpg" /></p>
<p><img src="assets/d5ea2a50-4da4-11ea-9765-e162b5bc6395" alt="png" /></p>
<p>著名的 JVM 技术专家 RednaxelaFX 提到ZGC 的 Load Value Barrier与 Red Hat 的 Shenandoah 收集器使用的屏障不同,后者选择了 70 年代比较基础的 Brooks Pointer而 ZGC 则是在古老的 Baker barrier 基础上增加了 self healing 特性。</p>
<p>可以把“读屏障”理解为一段代码,或者是一个指令,后面挂着对应的处理函数。</p>
<p>比如下面的代码:</p>
@@ -321,7 +321,7 @@ Object b = obj.x;
<p>着色指针和读屏障,相当于在内存管理和应用程序代码之间加了一个中间层,通过这个中间层就可以实现更多的功能。但是也可以看到算法本身有一定的开销,也带来了很多复杂性。</p>
<h4><strong>ZGC 的参数介绍</strong></h4>
<p>除了上面提到的 <code>-XX:+UnlockExperimentalVMOptions -XX:+UseZGC</code> 参数可以用来启用 ZGC 以外ZGC 可用的参数见下表:</p>
<p><img src="assets/1f908910-4da5-11ea-bb27-bdb4967fc6e0" alt="62790527.png" /></p>
<p><img src="assets/1f908910-4da5-11ea-bb27-bdb4967fc6e0" alt="png" /></p>
<p>一些常用的参数介绍:</p>
<ul>
<li><code>-XX:ZCollectionInterval</code>:固定时间间隔进行 GC默认值为0。</li>
@@ -364,7 +364,7 @@ Object b = obj.x;
<p>Shenandoah 团队对外宣称 Shenandoah GC 的暂停时间与堆大小无关,无论是 200 MB 还是 200 GB 的堆内存,都可以保障具有很低的暂停时间(注意:并不像 ZGC 那样保证暂停时间在 10ms 以内)。</p>
<h4><strong>Shenandoah GC 原理介绍</strong></h4>
<p>Shenandoah GC 的原理,跟 ZGC 非常类似。</p>
<p><img src="assets/3fabe040-4da6-11ea-a221-c1e42d9b4512" alt="28583d44-89ad-4196-b96c-dd747dc43c42.png" /></p>
<p><img src="assets/3fabe040-4da6-11ea-a221-c1e42d9b4512" alt="png" /></p>
<p>部分日志内容如下:</p>
<pre><code>GC(3) Pause Init Mark 0.771ms
GC(3) Concurrent marking 76480M-&gt;77212M(102400M) 633.213ms
@@ -394,7 +394,7 @@ GC(3) Concurrent cleanup 76244M-&gt;56620M(102400M) 12.242ms
<p>需要提醒,并非只有 GC 停顿会导致应用程序响应时间变长。 除了GC长时间停顿会导致系统响应变慢其他诸如 消息队列延迟、网络延迟、计算逻辑过于复杂、以及外部服务的延时,操作提供的调度程序抖动等都可能导致响应变慢。</p>
</blockquote>
<p>使用 Shenandoah 时需要全面了解系统运行情况,综合分析系统响应时间。下图是官方给出的各种 GC 工作负载对比:</p>
<p><img src="assets/a5b45430-4da6-11ea-bb27-bdb4967fc6e0" alt="b319f998-e955-4a1e-8091-9371866633e1.jpg" /></p>
<p><img src="assets/a5b45430-4da6-11ea-bb27-bdb4967fc6e0" alt="png" /></p>
<p>可以看到,相对于 CMS、G1、Parallel GCShenandoah 在系统负载增加的情况下,延迟时间稳定在非常低的水平,而其他几种 GC 都会迅速上升。</p>
<h4><strong>常用参数介绍</strong></h4>
<p>推荐几个配置或调试 Shenandoah 的 JVM 参数:</p>
@@ -437,7 +437,7 @@ GC(3) Concurrent cleanup 76244M-&gt;56620M(102400M) 12.242ms
<p>同时针对于内存分配失败时的策略,可以通过调节 <code>ShenandoahPacing</code><code>ShenandoahDegeneratedGC</code> 参数,对线程进行一定的调节控制。如果还是没有足够的内存,最坏的情况下可能会产生 Full GC以使得系统有足够的内存不至于发生 OOM。</p>
<p>更多有关如何配置、调试 Shenandoah 的参数信息,请参阅 Shenandoah 官方 Wiki 页面。</p>
<h4><strong>各版本 JDK 对 Shenandoah 的集成情况</strong></h4>
<p><img src="assets/cef34cb0-4da7-11ea-9f6e-1bdc6229ab3f" alt="9b62765c-d0ac-436e-999c-53d964e1ca33.png" /></p>
<p><img src="assets/cef34cb0-4da7-11ea-9f6e-1bdc6229ab3f" alt="png" /></p>
<p>这张图展示了 Shenandoah GC 目前在各个 JDK 版本上的进展情况,可以看到 OpenJDK 12 和 13 上都可以用。</p>
<p>在 Red Hat Enterprise Linux、Fedora 系统中则可以在 JDK 8 和 JDK 11 版本上使用(肯定的,这两个 Linux 发行版都是 Red Hat 的,谁让这个 GC 也是 Red Hat 开发维护的呢)。</p>
<ul>

View File

@@ -201,7 +201,7 @@ function hide_canvas() {
</blockquote>
<h4><strong>GraalVM 有什么特点</strong></h4>
<p>GraalVM 既可以独立运行,也可以在不同的部署场景中使用,比如在 OpenJDK 虚拟机环境、Node.js 环境,或者 Oracle、MySQL 数据库等环境中运行。下图来自 GraalVM 官网,展示了目前支持的平台技术。</p>
<p><img src="assets/b255e7a0-5196-11ea-a2fb-85c45bbaa11c" alt="GraalVM system diagram" /></p>
<p><img src="assets/b255e7a0-5196-11ea-a2fb-85c45bbaa11c" alt="png" /></p>
<p>GraalVM 支持大量的语言,包括:</p>
<ul>
<li>基于 JVM 的语言(例如 Java、Scala、Groovy、Kotlin、Clojure 等);</li>
@@ -230,9 +230,9 @@ function hide_canvas() {
<li>占用内存更低</li>
</ul>
<p>启动时间对比:</p>
<p><img src="assets/0e6b5c50-5197-11ea-b2e1-7d26d62747f1" alt="microservices" /></p>
<p><img src="assets/0e6b5c50-5197-11ea-b2e1-7d26d62747f1" alt="png" /></p>
<p>占用内存对比:</p>
<p><img src="assets/16d152f0-5197-11ea-b2e1-7d26d62747f1" alt="microservices" /></p>
<p><img src="assets/16d152f0-5197-11ea-b2e1-7d26d62747f1" alt="png" /></p>
<h3>解决了哪些痛点</h3>
<p>GraalVM 提供了一个全面的生态系统,消除编程语言之间的隔离,打通了不同语言之间的鸿沟,在共享的运行时中实现了互操作性,让我们可以进行混合式多语言编程。</p>
<p>用 Graal 执行的语言可以互相调用,允许使用来自其他语言的库,提供了语言的互操作性。同时结合了对编译器技术的最新研究,在高负载场景下 GraalVM 的性能比传统 JVM 要好得多。</p>
@@ -306,7 +306,7 @@ function hide_canvas() {
</ul>
<p><a href="https://github.com/graalvm/graalvm-ce-builds/releases">GitHub 下载页面</a> 中找到下载链接。</p>
<p>如下图所示:</p>
<p><img src="assets/cf68b260-519a-11ea-bb37-55480bd50c9e" alt="70802368.png" /></p>
<p><img src="assets/cf68b260-519a-11ea-bb37-55480bd50c9e" alt="png" /></p>
<p>这里区分操作系统macOS/darwin、Linux、Windows、CPU 架构AArch64、AMD64Intel/AMD、以及 JDK 版本。 我们根据自己的系统选择对应的下载链接。</p>
<p>比如 macOS 系统的 JDK 11 版本,对应的下载文件为:</p>
<pre><code># GraalVM 主程序绿色安装包

View File

@@ -276,7 +276,7 @@ CommandLine flags:
<p>通过这么分析下来同学们应该发现我们关注的主要是两个数据GC 暂停时间,以及 GC 之后的内存使用量/使用率。</p>
</blockquote>
<p>此次 GC 事件的示意图如下所示:</p>
<p><img src="assets/3966e430-62f7-11ea-a4da-8f4f1ea47eee" alt="57974076.png" /></p>
<p><img src="assets/3966e430-62f7-11ea-a4da-8f4f1ea47eee" alt="png" /></p>
<h4><strong>Full GC 日志分析</strong></h4>
<p>分析完第一次 GC 事件之后,我们心中应该有个大体的模式了。一起来看看另一次 GC 事件的日志:</p>
<pre><code class="language-shell">2019-12-15T15:18:37.081-0800: 0.908:
@@ -306,7 +306,7 @@ CommandLine flags:
<p>FullGC我们主要关注 GC 之后内存使用量是否下降其次关注暂停时间。简单估算GC 后老年代使用量为 220MB 左右,耗时 50ms。如果内存扩大 10 倍GC 后老年代内存使用量也扩大 10 倍,那耗时可能就是 500ms 甚至更高,就会系统有很明显的影响了。这也是我们说串行 GC 性能弱的一个原因,服务端一般是不会采用串行 GC 的。</p>
</blockquote>
<p>此次 GC 事件的内存变化情况,可以表示为下面的示意图:</p>
<p><img src="assets/e0272f50-62f7-11ea-ae67-634092bbcd70" alt="839273.png" /></p>
<p><img src="assets/e0272f50-62f7-11ea-ae67-634092bbcd70" alt="png" /></p>
<p>年轻代看起来数据几乎没变化,怎么办?因为上下文其实还有其他的 GC 日志记录,我们照着这个格式去解读即可。</p>
<h3>Parallel GC 日志解读</h3>
<p>并行垃圾收集器对年轻代使用“标记—复制mark-copy”算法对老年代使用“标记—清除—整理mark-sweep-compact”算法。</p>
@@ -383,7 +383,7 @@ demo.jvm0204.GCLogAnalysis
<p>年轻代 GC我们可以关注暂停时间以及 GC 后的内存使用率是否正常,但不用特别关注 GC 前的使用量,而且只要业务在运行,年轻代的对象分配就少不了,回收量也就不会少。</p>
</blockquote>
<p>此次 GC 的内存变化示意图为:</p>
<p><img src="assets/f3e388f0-62fb-11ea-9b06-5f146ad63a8a" alt="8353526.png" /></p>
<p><img src="assets/f3e388f0-62fb-11ea-9b06-5f146ad63a8a" alt="png" /></p>
<h4><strong>Full GC 日志分析</strong></h4>
<p>前面介绍了并行 GC 清理年轻代的 GC 日志,下面来看看清理整个堆内存的 GC 日志:</p>
<pre><code class="language-shell">2019-12-18T00:37:47.486-0800: 0.713:
@@ -412,7 +412,7 @@ demo.jvm0204.GCLogAnalysis
<p>Full GC 时我们更关注老年代的使用量有没有下降,以及下降了多少。如果 FullGC 之后内存不怎么下降,使用率还很高,那就说明系统有问题了。</p>
</blockquote>
<p>此次 GC 的内存变化示意图为:</p>
<p><img src="assets/73861870-62fc-11ea-af20-4f6854ce034d" alt="85130696.png" /></p>
<p><img src="assets/73861870-62fc-11ea-af20-4f6854ce034d" alt="png" /></p>
<p>细心的同学可能会发现,此次 FullGC 事件和前一次 MinorGC 事件是紧挨着的0.690+0.02secs~0.713。因为 Minor GC 之后老年代使用量达到了 93%,所以接着就触发了 Full GC。</p>
<p>本节到此就结束了,下节我们接着分析 CMS GC 日志。</p>
</div>

View File

@@ -288,7 +288,7 @@ CommandLine flags
<p>GC 之后呢?年轻代使用量为 17311K ~= 17%,下降了 119107K。堆内存使用量为 360181K ~= 71%,只下降了 82197K。两个下降值相减就是年轻代提升到老年代的内存量119107-82197=36910K。</p>
<p>那么老年代空间有多大?老年代使用量是多少?正在阅读的同学,请开动脑筋,用这些数字算一下。</p>
<p>此次 GC 的内存变化示意图为:</p>
<p><img src="assets/13640c30-63ac-11ea-a283-8f19fc193c49" alt="4438116.png" /></p>
<p><img src="assets/13640c30-63ac-11ea-a283-8f19fc193c49" alt="png" /></p>
<p>哇塞,这个数字不得了,老年代使用量 98% 了,非常高了。后面紧跟着就是一条 Full GC 的日志,请接着往下看。</p>
<h4><strong>Full GC 日志分析</strong></h4>
<p>实际上这次截取的年轻代 GC 日志和 FullGC 日志是紧连着的,我们从间隔时间也能大致看出来,<code>1.067 + 0.02secs ~ 1.091</code></p>
@@ -484,7 +484,7 @@ CommandLine flags
<p>参照前面年轻代 GC 日志的分析方法,我们推算出来,上面的 CMS Full GC 之后老年代的使用量应该是445134K-153242K=291892K老年代的总容量 506816K-157248K=349568K所以 Full GC 之后老年代的使用量占比是 291892K/349568K=83%。</p>
<p>这个占比不低。说明什么问题呢? 一般来说就是分配的内存小了,毕竟我们才指定了 512MB 的最大堆内存。</p>
<p>按照惯例,来一张 GC 前后的内存使用情况示意图:</p>
<p><img src="assets/7fad0940-63ad-11ea-9c08-6f91e6eaabb6" alt="3110993.png" /></p>
<p><img src="assets/7fad0940-63ad-11ea-9c08-6f91e6eaabb6" alt="png" /></p>
<p>总之CMS 垃圾收集器在减少停顿时间上做了很多给力的工作,很大一部分 GC 线程是与应用线程并发运行的不需要暂停应用线程这样就可以在一般情况下每次暂停的时候较少。当然CMS 也有一些缺点,其中最大的问题就是老年代的内存碎片问题,在某些情况下 GC 会有不可预测的暂停时间,特别是堆内存较大的情况下。</p>
<blockquote>
<p>透露一个学习 CMS 的诀窍:参考上面各个阶段的示意图,请同学们自己画一遍。</p>

View File

@@ -347,7 +347,7 @@ Heap
<li><code>[Free CSet: 0.0 ms]</code>:将回收集中被释放的小堆归还所消耗的时间,以便他们能用来分配新的对象。</li>
</ol>
<p>此次 Young GC 对应的示意图如下所示:</p>
<p><img src="assets/e9b457b0-6864-11ea-9d64-851af22d0044" alt="58726143.png" /></p>
<p><img src="assets/e9b457b0-6864-11ea-9d64-851af22d0044" alt="png" /></p>
<h3>Concurrent Marking并发标记</h3>
<p>当堆内存的总体使用比例达到一定数值时,就会触发并发标记。这个默认比例是 45%,但也可以通过 JVM 参数 <strong>InitiatingHeapOccupancyPercent</strong> 来设置。和 CMS 一样G1 的并发标记也是由多个阶段组成,其中一些阶段是完全并发的,还有一些阶段则会暂停应用线程。</p>
<h4><strong>阶段 1Initial Mark初始标记</strong></h4>
@@ -410,7 +410,7 @@ Heap
</blockquote>
<p>标记周期一般只在碰到 region 中一个存活对象都没有的时候,才会顺手处理一把,大多数情况下都不释放内存。</p>
<p>示意图如下所示:</p>
<p><img src="assets/a3907380-6865-11ea-bc7d-05803d82869a" alt="52452256.png" /></p>
<p><img src="assets/a3907380-6865-11ea-bc7d-05803d82869a" alt="png" /></p>
<h3>Evacuation Pausemixed转移暂停混合模式</h3>
<p>并发标记完成之后G1 将执行一次混合收集mixed collection不只清理年轻代还将一部分老年代区域也加入到 collection set 中。</p>
<p>混合模式的转移暂停Evacuation Pause不一定紧跟并发标记阶段。</p>
@@ -462,9 +462,9 @@ Heap
</code></pre>
<p>因为我们的堆内存空间很小,存活对象的数量也不多,所以这里看到的 Full GC 暂停时间很短。</p>
<p>此次 Full GC 的示意图如下所示:</p>
<p><img src="assets/1b6e2e60-6866-11ea-a490-d3e65769b9bf" alt="59111406.png" /></p>
<p><img src="assets/1b6e2e60-6866-11ea-a490-d3e65769b9bf" alt="png" /></p>
<p>在堆内存较大的情况下8G+),如果 G1 发生了 Full GC暂停时间可能会退化达到几十秒甚至更多。如下面这张图片所示</p>
<p><img src="assets/2bce1360-6866-11ea-9d64-851af22d0044" alt="5b03ee3d-1e0a-4375-a5f6-aab17f4d1184.jpg" /></p>
<p><img src="assets/2bce1360-6866-11ea-9d64-851af22d0044" alt="png" /></p>
<p>从其中的 OldGen 部分可以看到118 次 Full GC 消耗了 31 分钟,平均每次达到 20 秒,按图像比例可粗略得知,吞吐率不足 30%。</p>
<p>这张图片所表示的场景是在压测 Flink 按时间窗口进行聚合计算时发生的,主要原因是对象太多,堆内存空间不足而导致的,修改对象类型为原生数据类型之后问题得到缓解,加大堆内存空间,满足批处理/流计算的需求之后 GC 问题不再复现。</p>
<p>发生持续时间很长的 Full GC 暂停时,就需要我们进行排查和分析,确定是否需要修改 GC 配置,或者增加内存,还是需要修改程序的业务逻辑。关于 G1 的调优,我们在后面的调优部分再进行介绍。</p>

View File

@@ -323,7 +323,7 @@ java.lang.InterruptedException
<p>在 Java 线程启动时会创建底层线程native Thread在任务执行完成后会自动回收。</p>
<p>JVM 中所有线程都交给操作系统来负责调度,以将线程分配到可用的 CPU 上执行。</p>
<p>根据对 Hotspot 线程模型的理解,我们制作了下面这下示意图:</p>
<p><img src="assets/56b17020-69f3-11ea-a850-c1530f386d4a" alt="62445939.png" /></p>
<p><img src="assets/56b17020-69f3-11ea-a850-c1530f386d4a" alt="png" /></p>
<p>从图中可以看到,调用 Thread 对象的 start() 方法后JVM 会在内部执行一系列的操作。</p>
<p>因为 Hotspot JVM 是使用 C++ 语言编写的,所以在 JVM 层面会有很多和线程相关的 C++ 对象。</p>
<ul>
@@ -635,9 +635,9 @@ Found 1 deadlock.
<p>可以看到,这些工具会自动发现死锁,并将相关线程的调用栈打印出来。</p>
<h4><strong>使用可视化工具发现死锁</strong></h4>
<p>当然我们也可以使用前面介绍过的可视化工具 jconsole示例如下</p>
<p><img src="assets/1f1fc390-69f9-11ea-b22a-75d53668be52" alt="79277126.png" /></p>
<p><img src="assets/1f1fc390-69f9-11ea-b22a-75d53668be52" alt="png" /></p>
<p>也可以使用 JVisualVM</p>
<p><img src="assets/26b68350-69f9-11ea-b31d-2b61fbcda176" alt="79394987.png" /></p>
<p><img src="assets/26b68350-69f9-11ea-b31d-2b61fbcda176" alt="png" /></p>
<p>各种工具导出的线程转储内容都差不多,参考前面的内容。</p>
<p>有没有自动分析线程的工具呢请参考后面的章节《fastthread 相关的工具介绍》。</p>
<h3>参考资料</h3>

View File

@@ -195,7 +195,7 @@ function hide_canvas() {
<p>请思考一个问题: 一个对象具有 100 个属性,与 100 个对象每个具有 1 个属性,哪个占用的内存空间更大?</p>
</blockquote>
<p>为了回答这个问题,我们来看看 JVM 怎么表示一个对象:</p>
<p><img src="assets/918dfb60-6ea2-11ea-97af-c3b20af12573" alt="742441.png" /></p>
<p><img src="assets/918dfb60-6ea2-11ea-97af-c3b20af12573" alt="png" /></p>
<p><strong>说明</strong></p>
<ul>
<li>alignment外部对齐比如 8 字节的数据类型 long在内存中的起始地址必须是 8 字节的整数倍。</li>
@@ -484,16 +484,16 @@ public class ClearRequestCacheFilter implements Filter{
</code></pre>
<p>双击打开 MemoryAnalyzer.exe打开 MAT 分析工具,选择菜单 File &gt; Open File… 选择对应的 dump 文件。</p>
<p>选择 Leak Suspects Report 并确定,分析内存泄露方面的报告。</p>
<p><img src="assets/fed3ce00-6eae-11ea-83c3-675e02f948ee" alt="bd3d81d4-d928-4081-a2f7-96c11de76178.png" /></p>
<p><img src="assets/fed3ce00-6eae-11ea-83c3-675e02f948ee" alt="png" /></p>
<p><strong>3. 内存报告</strong></p>
<p>然后等待,分析完成后,汇总信息如下:</p>
<p><img src="assets/7bc10970-6f1b-11ea-ab1a-832c54b4f266" alt="07acbdb7-0c09-40a5-b2c3-e7621a36870f.png" /></p>
<p><img src="assets/7bc10970-6f1b-11ea-ab1a-832c54b4f266" alt="png" /></p>
<p>分析报告显示,占用内存最大的问题根源 1</p>
<p><img src="assets/1c41ade0-6eaf-11ea-83c3-675e02f948ee" alt="345818b9-9323-4025-b23a-8f279a99eb84.png" /></p>
<p><img src="assets/1c41ade0-6eaf-11ea-83c3-675e02f948ee" alt="png" /></p>
<p>占用内存最大的问题根源 2</p>
<p><img src="assets/249ca800-6eaf-11ea-9d5e-29b50a74a9eb" alt="07bbe993-5139-416a-9e6d-980131b649bf.png" /></p>
<p><img src="assets/249ca800-6eaf-11ea-9d5e-29b50a74a9eb" alt="png" /></p>
<p>占用内存最大的问题根源 3</p>
<p><img src="assets/2dd61b40-6eaf-11ea-a6e5-c1244b77f602" alt="7308f1b5-35aa-43e0-bbb4-05cb2e3131be.png" /></p>
<p><img src="assets/2dd61b40-6eaf-11ea-a6e5-c1244b77f602" alt="png" /></p>
<p>可以看到,总的内存占用才 2GB 左右。问题根源 1 和根源 2每个占用 800MB问题很可能就在他们身上。</p>
<p>当然,根源 3 也有一定的参考价值,表明这时候有很多 JDBC 操作。</p>
<p>查看问题根源 1其说明信息如下</p>
@@ -532,12 +532,12 @@ http-nio-8086-exec-8
<p>当然,还可以分析这个根源下持有的各个类的对象数量。</p>
<p>点击根源 1 说明信息下面的 <code>Details »</code> 链接,进入详情页面。</p>
<p>查看其中的 “Accumulated Objects in Dominator Tree”</p>
<p><img src="assets/5ef3a760-6eaf-11ea-a6e5-c1244b77f602" alt="b5ff6319-a5d9-426f-99ef-19bd100fd80a.png" /></p>
<p><img src="assets/5ef3a760-6eaf-11ea-a6e5-c1244b77f602" alt="png" /></p>
<p>可以看到占用内存最多的是 2 个 ArrayList 对象。</p>
<p>鼠标左键点击第一个 ArrayList 对象,在弹出的菜单中选择 Show objects by class &gt; by outgoing references。</p>
<p><img src="assets/6e41a730-6eaf-11ea-9d98-f7fceb2428d3" alt="6dbbb72d-ec2b-485f-bc8e-9de044b21b7d.png" /></p>
<p><img src="assets/6e41a730-6eaf-11ea-9d98-f7fceb2428d3" alt="png" /></p>
<p>打开 class_references 标签页:</p>
<p><img src="assets/767c2100-6eaf-11ea-97af-c3b20af12573" alt="28fe37ed-36df-482a-bc58-231c9552638d.png" /></p>
<p><img src="assets/767c2100-6eaf-11ea-97af-c3b20af12573" alt="png" /></p>
<p>展开后发现 PO 类对象有 113 万个。加载的确实有点多,直接占用 170MB 内存(每个对象约 150 字节)。</p>
<p>事实上,这是将批处理任务,放到实时的请求中进行计算,导致的问题。</p>
<p>MAT 还提供了其他信息,都可以点开看看,也可以为我们诊断问题提供一些依据。</p>

View File

@@ -188,7 +188,7 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>24 内存分析与相关工具下篇(常见问题分析)</h1>
<p>Java 程序的内存可以分为几个部分Heap space、非堆Non-Heap、栈Stack等等如下图所示</p>
<p><img src="assets/b8e25a80-71db-11ea-964d-61a29639fe46" alt="2459010.png" /></p>
<p><img src="assets/b8e25a80-71db-11ea-964d-61a29639fe46" alt="png" /></p>
<p>最常见的 java.lang.OutOfMemoryError 可以归为以下类型。</p>
<h3>OutOfMemoryError: Java heap space</h3>
<p>JVM 限制了 Java 程序的最大内存使用量,由 JVM 的启动参数决定。</p>
@@ -233,7 +233,7 @@ function hide_canvas() {
<p>而“java.lang.OutOfMemoryError: GC overhead limit exceeded”这种错误发生的原因是<strong>程序基本上耗尽了所有的可用内存GC 也清理不了</strong></p>
<h4><strong>原因分析</strong></h4>
<p>JVM 抛出“java.lang.OutOfMemoryError: GC overhead limit exceeded”错误就是发出了这样的信号执行垃圾收集的时间比例太大有效的运算量太小。默认情况下如果 GC 花费的时间超过 98%,并且 GC 回收的内存少于 2%JVM 就会抛出这个错误。就是说,系统没法好好干活了,几乎所有资源都用来去做 GC但是 GC 也没啥效果。此时系统就像是到了癌症晚期,身体的营养都被癌细胞占据了,真正用于身体使用的非常少了,而且就算是调用所有营养去杀灭癌细胞也晚了,因为杀的效果很差了,还远远没有癌细胞复制的速度快。</p>
<p><img src="assets/f346d1f0-71dc-11ea-8377-13f07d2f46fb" alt="06cff4f1-b6a6-4cda-b7f5-8e8837f55c3a.png" /></p>
<p><img src="assets/f346d1f0-71dc-11ea-8377-13f07d2f46fb" alt="png" /></p>
<p>注意“java.lang.OutOfMemoryError: GC overhead limit exceeded”错误只在连续多次 <a href="http://blog.csdn.net/renfufei/article/details/54885190">GC</a> 都只回收了不到 2% 的极端情况下才会抛出。假如不抛出 GC overhead limit 错误会发生什么情况呢?那就是 GC 清理的这么点内存很快会再次填满,迫使 GC 再次执行。这样就形成恶性循环CPU 使用率一直是 100%,而 GC 却没有任何成果。系统用户就会看到系统卡死——以前只需要几毫秒的操作,现在需要好几分钟甚至几小时才能完成。</p>
<blockquote>
<p>这也是一个很好的<a href="https://en.wikipedia.org/wiki/Fail-fast">快速失败原则</a>的案例。</p>
@@ -394,7 +394,7 @@ public class MicroGenerator {
<p>Java 程序本质上是多线程的,可以同时执行多项任务。类似于在播放视频的时候,可以拖放窗口中的内容,却不需要暂停视频播放,即便是物理机上只有一个 CPU。</p>
<p>线程thread可以看作是干活的工人workers。如果只有一个工人在同一时间就只能执行一项任务。假若有很多工人那么就可以同时执行多项任务。</p>
<p>和现实世界类似JVM 中的线程也需要内存空间来执行自己的任务。如果线程数量太多,就会引入新的问题:</p>
<p><img src="assets/7d551650-71e1-11ea-97af-c3b20af12573" alt="7ff0ad95-3c2e-4246-badc-e007abf84978.png" /></p>
<p><img src="assets/7d551650-71e1-11ea-97af-c3b20af12573" alt="png" /></p>
<p>“java.lang.OutOfMemoryError: Unable to create new native thread”错误是<strong>程序创建的线程数量已达到上限值</strong>的异常信息。</p>
<h4><strong>原因分析</strong></h4>
<p>JVM 向操作系统申请创建新的 native thread原生线程就有可能会碰到“java.lang.OutOfMemoryError: Unable to create new native thread”错误。如果底层操作系统创建新的 native thread 失败JVM 就会抛出相应的 OutOfMemoryError。</p>
@@ -471,7 +471,7 @@ max user processes (-u) 1800
<p>一种解决办法是执行线程转储thread dump来分析具体情况我们会在后面的章节讲解。</p>
<h3>OutOfMemoryError: Out of swap space</h3>
<p>JVM 启动参数指定了最大内存限制,如 <code>-Xmx</code> 以及相关的其他启动参数。假若 JVM 使用的内存总量超过可用的物理内存,操作系统就会用到虚拟内存(一般基于磁盘文件)。</p>
<p><img src="assets/d416dd70-71e1-11ea-b76f-3b9120f55521" alt="40dddc4d-f192-40dc-9a1f-ab4df65d6f12.png" /></p>
<p><img src="assets/d416dd70-71e1-11ea-b76f-3b9120f55521" alt="png" /></p>
<p>错误信息“java.lang.OutOfMemoryError: Out of swap space”表明交换空间swap space/虚拟内存)不足,此时由于物理内存和交换空间都不足,所以导致内存分配失败。</p>
<h4><strong>原因分析</strong></h4>
<p>如果 native heap 内存耗尽,内存分配时 JVM 就会抛出“java.lang.OutOfmemoryError: Out of swap space”错误消息告诉用户请求分配内存的操作失败了。</p>
@@ -494,7 +494,7 @@ swapon swapfile
<p>因为垃圾收集器需要清理整个内存空间,所以虚拟内存对 Java GC 来说是难以忍受的。存在内存交换时,执行<a href="http://blog.csdn.net/renfufei/article/details/54885190">垃圾收集</a><a href="http://blog.csdn.net/renfufei/article/details/61924893">暂停时间</a>会增加上百倍,所以最好不要增加,甚至是不要使用虚拟内存(毕竟访问内存的速度和磁盘的速度,差了几个数量级)。</p>
<h3>OutOfMemoryError: Requested array size exceeds VM limit</h3>
<p>Java 平台限制了数组的最大长度。各个版本的具体限制可能稍有不同,但范围都在 1~21 亿之间。(想想看,为什么是 21 亿?)</p>
<p><img src="assets/15d241f0-71e2-11ea-9d98-f7fceb2428d3" alt="8cc643df-164f-4ee9-9142-7a27032418ec.png" /></p>
<p><img src="assets/15d241f0-71e2-11ea-9d98-f7fceb2428d3" alt="png" /></p>
<p>如果程序抛出“java.lang.OutOfMemoryError: Requested array size exceeds VM limit”错误就说明程序想要创建的数组长度超过限制。</p>
<h4><strong>原因分析</strong></h4>
<p>这个错误是在真正为数组分配内存之前JVM 会执行一项检查要分配的数据结构在该平台是否可以寻址addressable。当然这个错误比你所想的还要少见得多。</p>
@@ -546,7 +546,7 @@ java.lang.OutOfMemoryError: Requested array size exceeds VM limit
<p>我们知道操作系统operating system构建在进程process的基础上。进程由内核作业kernel jobs进行调度和维护其中有一个内核作业称为“Out of memory killerOOM 终结者)”,与本节所讲的 OutOfMemoryError 有关。</p>
<p>Out of memory killer 在可用内存极低的情况下会杀死某些进程。只要达到触发条件就会激活选中某个进程并杀掉。通常采用启发式算法对所有进程计算评分heuristics scoring得分最低的进程将被 kill 掉。</p>
<p>因此“Out of memory: Kill process or sacrifice child”和前面所讲的 <a href="http://blog.csdn.net/renfufei/article/category/5884735">OutOfMemoryError</a> 都不同,因为它既不由 JVM 触发,也不由 JVM 代理,而是系统内核内置的一种安全保护措施。</p>
<p><img src="assets/f1e22a20-71e2-11ea-833b-93fabc8068c9" alt="13abc504-c840-4264-a905-e9011159484f.png" /></p>
<p><img src="assets/f1e22a20-71e2-11ea-833b-93fabc8068c9" alt="png" /></p>
<p>如果可用内存(含 swap不足就有可能会影响系统稳定这时候 Out of memory killer 就会设法找出流氓进程并杀死他也就是引起“Out of memory: kill process or sacrifice child”错误。</p>
<h4><strong>原因分析</strong></h4>
<p>默认情况下Linux kernels内核允许进程申请的量超过系统可用内存。这是因为, 在大多数情况下,很多进程申请了很多内存,但实际使用的量并没有那么多。</p>

View File

@@ -382,40 +382,40 @@ Found 1 deadlock.
<p>两种方式步骤都差不多,选择 RAW 方式上传文本字符串,然后点击分析按钮。</p>
<h4><strong>分析结果页面</strong></h4>
<p>等待片刻,自动跳转到分析结果页面。</p>
<p><img src="assets/bb219280-7504-11ea-9628-dd9a4bfcf1d2" alt="6843295.png" /></p>
<p><img src="assets/bb219280-7504-11ea-9628-dd9a4bfcf1d2" alt="png" /></p>
<p>这里可以看到基本信息,以及右边的一些链接:</p>
<ul>
<li>分享报告,可以很方便地把报告结果发送给其他小伙伴。</li>
</ul>
<h4><strong>线程数汇总</strong></h4>
<p>把页面往下拉,可以看到线程数量汇总报告。</p>
<p><img src="assets/c6db4800-7504-11ea-b77f-634b57f46967" alt="6864312.png" /></p>
<p><img src="assets/c6db4800-7504-11ea-b77f-634b57f46967" alt="png" /></p>
<p>从这个报告中可以很直观地看到,线程总数为 26其中 19 个运行状态线程5 个等待状态的线程2 个阻塞状态线程。</p>
<p>右边还给了一个饼图,展示各种状态所占的比例。</p>
<h4><strong>线程组分析</strong></h4>
<p>接着是将线程按照名称自动分组。</p>
<p><img src="assets/ef8a2af0-7504-11ea-9dae-5d0db1c26be7" alt="6898070.png" /></p>
<p><img src="assets/ef8a2af0-7504-11ea-9dae-5d0db1c26be7" alt="png" /></p>
<p>这里就看到线程命名的好处了吧!如果我们的线程池统一命名,那么相关资源池的使用情况就很直观。</p>
<blockquote>
<p>所以在代码里使用线程池的时候,统一添加线程名称就是一个好的习惯!</p>
</blockquote>
<h4><strong>守护线程分析</strong></h4>
<p>接下来是守护线程分析:</p>
<p><img src="assets/08d3b580-7505-11ea-9628-dd9a4bfcf1d2" alt="6923926.png" /></p>
<p><img src="assets/08d3b580-7505-11ea-9628-dd9a4bfcf1d2" alt="png" /></p>
<p>这里可以看到守护线程与前台线程的统计信息。</p>
<h4><strong>死锁情况检测</strong></h4>
<p>当然,也少不了死锁分析:</p>
<p><img src="assets/f4af8010-7505-11ea-a691-fda85882301c" alt="6948610.png" /></p>
<p><img src="assets/f4af8010-7505-11ea-a691-fda85882301c" alt="png" /></p>
<p>可以看到,各个工具得出的死锁检测结果都差不多。并不难分析,其中给出了线程名称,以及方法调用栈信息,等待的是哪个锁。</p>
<h4><strong>线程调用栈情况</strong></h4>
<p>以及线程调用情况:</p>
<p><img src="assets/ec4f55d0-7505-11ea-bb80-67799d8258e1" alt="7008839.png" /></p>
<p><img src="assets/ec4f55d0-7505-11ea-bb80-67799d8258e1" alt="png" /></p>
<p>后面是这些线程的详情:</p>
<p><img src="assets/e5ca7b40-7505-11ea-a8c0-4fdc777140d0" alt="7058206.png" /></p>
<p><img src="assets/e5ca7b40-7505-11ea-a8c0-4fdc777140d0" alt="png" /></p>
<p>这块信息只是将相关的方法调用栈展示出来。</p>
<h4><strong>热点方法统计</strong></h4>
<p>热点方法是一个需要注意的重点,调用的越多,说明这一块可能是系统的性能瓶颈。</p>
<p><img src="assets/d789f3d0-7505-11ea-965e-2b5335ba3591" alt="7104053.png" /></p>
<p><img src="assets/d789f3d0-7505-11ea-965e-2b5335ba3591" alt="png" /></p>
<p>这里展示了此次快照中正在执行的方法。如果只看热点方法抽样的话,更精确的工具是 JDK 内置的 hprof。</p>
<p>但如果有很多方法阻塞或等待,则线程快照中展示的热点方法位置可以快速确定问题出现的代码行。</p>
<h4><strong>CPU 消耗信息</strong></h4>
@@ -426,18 +426,18 @@ Found 1 deadlock.
<p>这里看到 GC 线程数是 8 个,这个值跟具体的 CPU 内核数量相差不大就算是正常的。</p>
<p>GC 线程数如果太多或者太少,会造成很多问题,我们在后面的章节中通过案例进行讲解。</p>
<h4><strong>线程栈深度</strong></h4>
<p><img src="assets/b3d1aa50-7505-11ea-a8c0-4fdc777140d0" alt="7277060.png" /></p>
<p><img src="assets/b3d1aa50-7505-11ea-a8c0-4fdc777140d0" alt="png" /></p>
<p>这里都小于10说明堆栈都不深。</p>
<h4><strong>复杂死锁检测</strong></h4>
<p>接下来是复杂死锁检测和 Finalizer 线程的信息。</p>
<p><img src="assets/a9ec3fa0-7505-11ea-965e-2b5335ba3591" alt="7295147.png" /></p>
<p><img src="assets/a9ec3fa0-7505-11ea-965e-2b5335ba3591" alt="png" /></p>
<p>简单死锁是指两个线程之间互相死等资源锁。那么什么复杂死锁呢? 这个问题留给同学们自己搜索。</p>
<h4><strong>火焰图</strong></h4>
<p><img src="assets/a0e32b30-7505-11ea-8fb8-ffe43c2e987a" alt="7336167.png" /></p>
<p><img src="assets/a0e32b30-7505-11ea-8fb8-ffe43c2e987a" alt="png" /></p>
<p>火焰图挺有趣,将所有线程调用栈汇总到一张图片中。</p>
<h4><strong>调用栈树</strong></h4>
<p>如果我们把所有的调用栈合并到一起,整体来看呢?</p>
<p><img src="assets/7502de70-7505-11ea-a8c0-4fdc777140d0" alt="7358293.png" /></p>
<p><img src="assets/7502de70-7505-11ea-a8c0-4fdc777140d0" alt="png" /></p>
<p>树形结构在有些时候也很有用,比如大量线程都在执行类似的调用栈路径时。</p>
<p>以上这些信息,都有助于我们去分析和排查 JVM 问题,而图形工具相对于命令行工具的好处是直观、方便、快速,帮我们省去过滤一些不必要的干扰信息的时间。</p>
<h3>参考链接</h3>

View File

@@ -289,7 +289,7 @@ May 21 12:05:23 web1 kernel: CPU: 2 PID: 10467 Comm: jstatd Not tainted 3.10.0-5
<li><a href="https://github.com/btraceio/btrace/releases/download/v2.0.0/btrace-bin.zip">btrace-bin.zip</a></li>
</ul>
<p>下载完成后解压即可使用:</p>
<p><img src="assets/b12ba340-7808-11ea-b264-6326f7cc0e82" alt="1613271.png" /></p>
<p><img src="assets/b12ba340-7808-11ea-b264-6326f7cc0e82" alt="png" /></p>
<p>可以看到bin 目录下是可执行文件samples 目录下是脚本示例。</p>
<h4><strong>示例程序</strong></h4>
<p>我们先编写一个有入参有返回值的方法,示例如下:</p>
@@ -325,25 +325,25 @@ public class RandomSample {
<p>细心的同学可能已经发现,在安装 JVisualVM 的插件时有一款插件叫做“BTrace Workbench”。安装这款插件之后在对应的 JVM 实例上点右键,就可以进入 BTrace 的操作界面。</p>
<p><strong>1. BTrace 插件安装</strong></p>
<p>打开 VisualVM选择菜单“工具插件(G)”:</p>
<p><img src="assets/d59e3c10-7808-11ea-8c74-b966eb0a8d67" alt="82699966.png" /></p>
<p><img src="assets/d59e3c10-7808-11ea-8c74-b966eb0a8d67" alt="png" /></p>
<p>然后在插件安装界面中,找到“可用插件”:</p>
<p><img src="assets/0665b8f0-7809-11ea-8e11-89f2c26dd0be" alt="82770532.png" /></p>
<p><img src="assets/0665b8f0-7809-11ea-8e11-89f2c26dd0be" alt="png" /></p>
<p>勾选“BTrace Workbench”之后点击“安装(I)”按钮。</p>
<blockquote>
<p>如果插件不显示,请更新 JDK 到最新版。</p>
</blockquote>
<p><img src="assets/3661cf80-7809-11ea-a98c-83821ebf98cb" alt="82937996.png" /></p>
<p><img src="assets/3661cf80-7809-11ea-a98c-83821ebf98cb" alt="png" /></p>
<p>按照引导和提示,继续安装即可。</p>
<p><img src="assets/4aa27120-7809-11ea-86cb-4dea263d5534" alt="82991766.png" /></p>
<p><img src="assets/4aa27120-7809-11ea-86cb-4dea263d5534" alt="png" /></p>
<p>接受协议,并点击安装。</p>
<p><img src="assets/53216e00-7809-11ea-b61a-45f5a80e7f1b" alt="83219940.png" /></p>
<p><img src="assets/53216e00-7809-11ea-b61a-45f5a80e7f1b" alt="png" /></p>
<p>等待安装完成:</p>
<p><img src="assets/5c553bf0-7809-11ea-8e11-89f2c26dd0be" alt="83257210.png" /></p>
<p><img src="assets/5c553bf0-7809-11ea-8e11-89f2c26dd0be" alt="png" /></p>
<p>点击“完成”按钮即可。</p>
<p><strong>BTrace 插件使用</strong></p>
<p><img src="assets/689845b0-7809-11ea-8856-57a8f16560a7" alt="85267702.png" /></p>
<p><img src="assets/689845b0-7809-11ea-8856-57a8f16560a7" alt="png" /></p>
<p>打开后默认的界面如下:</p>
<p><img src="assets/70515a80-7809-11ea-8e11-89f2c26dd0be" alt="85419826.png" /></p>
<p><img src="assets/70515a80-7809-11ea-8e11-89f2c26dd0be" alt="png" /></p>
<p>可以看到这是一个 Java 文件的样子。然后我们参考官方文档,加一些脚本进去。</p>
<h4><strong>BTrace 脚本示例</strong></h4>
<p>我们下载的 BTrace 项目中samples 目录下有一些脚本示例。 参照这些示例,编写一个简单的 BTrace 脚本:</p>
@@ -380,7 +380,7 @@ public class TracingScript {
</code></pre>
<h4><strong>执行结果</strong></h4>
<p>可以看到,输出了简单的执行结果:</p>
<p><img src="assets/60607910-780b-11ea-8e11-89f2c26dd0be" alt="6182718.png" /></p>
<p><img src="assets/60607910-780b-11ea-8e11-89f2c26dd0be" alt="png" /></p>
<p>可以和示例程序的控制台输出比对一下。</p>
<h4><strong>更多示例</strong></h4>
<p>BTrace 提供了很多示例,照着改一改就能执行简单的监控。</p>
@@ -443,9 +443,9 @@ java -jar arthas-boot.jar
</code></pre>
<h4>使用示例</h4>
<p>启动之后显示的信息大致如下图所示:</p>
<p><img src="assets/94b56270-780b-11ea-a3f1-b7b0a6636d71" alt="8128798.png" /></p>
<p><img src="assets/94b56270-780b-11ea-a3f1-b7b0a6636d71" alt="png" /></p>
<p>然后我们输入需要连接Attach的 JVM 进程,例如 1然后回车。</p>
<p><img src="assets/a10febd0-780b-11ea-a3f1-b7b0a6636d71" alt="8296362.png" /></p>
<p><img src="assets/a10febd0-780b-11ea-a3f1-b7b0a6636d71" alt="png" /></p>
<p>如果需要退出,输入 exit 即可。</p>
<p>接着我们输入 help 命令查看帮助,返回的信息大致如下。</p>
<pre><code>[<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="751407011d1406354742464045">[email&#160;protected]</a>]$ help
@@ -492,11 +492,11 @@ java -jar arthas-boot.jar
<pre><code>help thread
</code></pre>
<p>如果查看 JVM 信息,输入命令 jvm 即可。</p>
<p><img src="assets/ab1588b0-780b-11ea-b760-29671afaaa1a" alt="8871310.png" /></p>
<p><img src="assets/ab1588b0-780b-11ea-b760-29671afaaa1a" alt="png" /></p>
<p>环境变量 sysenv</p>
<p><img src="assets/b2e1d760-780b-11ea-8856-57a8f16560a7" alt="9257854.png" /></p>
<p><img src="assets/b2e1d760-780b-11ea-8856-57a8f16560a7" alt="png" /></p>
<p>查看线程信息,输入命令 thread</p>
<p><img src="assets/bae2a390-780b-11ea-b61a-45f5a80e7f1b" alt="8831103.png" /></p>
<p><img src="assets/bae2a390-780b-11ea-b61a-45f5a80e7f1b" alt="png" /></p>
<p>查看某个线程的信息:</p>
<pre><code>[<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="f8998a8c90998bb8cacfcbcdc8">[email&#160;protected]</a>]$ thread 1
&quot;main&quot; Id=1 TIMED_WAITING
@@ -506,18 +506,18 @@ java -jar arthas-boot.jar
at demo.jvm0209.RandomSample.main(Unknown Source)
</code></pre>
<p>查看 JVM 选项 vmoption</p>
<p><img src="assets/c3a51210-780b-11ea-8e11-89f2c26dd0be" alt="9104344.png" /></p>
<p><img src="assets/c3a51210-780b-11ea-8e11-89f2c26dd0be" alt="png" /></p>
<p>某些选项可以设置,这里给出了示例 <code>vmoption PrintGCDetails true</code></p>
<p>查找类 sc</p>
<p><img src="assets/7c81e650-780c-11ea-a89b-47a48ccffebe" alt="9474443.png" /></p>
<p><img src="assets/7c81e650-780c-11ea-a89b-47a48ccffebe" alt="png" /></p>
<p>反编译代码 jad</p>
<p><img src="assets/8ec49510-780c-11ea-8c74-b966eb0a8d67" alt="9593919.png" /></p>
<p><img src="assets/8ec49510-780c-11ea-8c74-b966eb0a8d67" alt="png" /></p>
<p>堆内存转储 heapdump</p>
<p><img src="assets/85d59850-780c-11ea-b760-29671afaaa1a" alt="9834789.png" /></p>
<p><img src="assets/85d59850-780c-11ea-b760-29671afaaa1a" alt="png" /></p>
<p>跟踪方法执行时间 trace</p>
<p><img src="assets/976068c0-780c-11ea-a98c-83821ebf98cb" alt="9997095.png" /></p>
<p><img src="assets/976068c0-780c-11ea-a98c-83821ebf98cb" alt="png" /></p>
<p>观察方法执行 watch</p>
<p><img src="assets/9e27a380-780c-11ea-9105-57164a9a27ea" alt="10270279.png" /></p>
<p><img src="assets/9e27a380-780c-11ea-9105-57164a9a27ea" alt="png" /></p>
<p>可以看到,支持条件表达式,类似于代码调试中的条件断点。 功能非常强大,并且作为一个 JVM 分析的集成环境,使用起来也比一般工具方便。更多功能请参考 <a href="https://alibaba.github.io/arthas/">Arthas 用户文档</a></p>
<h3>抽样分析器Profilers</h3>
<p>下面介绍分析器(<a href="http://zeroturnaround.com/rebellabs/developer-productivity-report-2015-java-performance-survey-results/3/">profilers</a>Oracle 官方翻译是“抽样器”)。</p>
@@ -570,11 +570,11 @@ java -jar arthas-boot.jar
<li>让程序运行一段时间,以收集关于对象分配的足够信息。</li>
<li>单击下方的“Snapshot”快照按钮可以获取收集到的快照信息。</li>
</ol>
<p><img src="assets/f4c62b30-780c-11ea-b264-6326f7cc0e82" alt="85731ea7-1c8c-4f9a-8869-bd25f61e3300.png" /></p>
<p><img src="assets/f4c62b30-780c-11ea-b264-6326f7cc0e82" alt="png" /></p>
<p>完成上面的步骤后,可以得到类似这样的信息:</p>
<p><img src="assets/fcd5eb80-780c-11ea-9105-57164a9a27ea" alt="f6c5d90d-104f-474e-b9fe-fa4d351871da.png" /></p>
<p><img src="assets/fcd5eb80-780c-11ea-9105-57164a9a27ea" alt="png" /></p>
<p>上图按照每个类被创建的对象数量多少来排序。看第一行可以知道,创建的最多的对象是 int[] 数组。鼠标右键单击这行,就可以看到这些对象都在哪些地方创建的:</p>
<p><img src="assets/05cc1430-780d-11ea-8856-57a8f16560a7" alt="e9583002-b04a-4a2c-8e94-3c9e0e57557a.png" /></p>
<p><img src="assets/05cc1430-780d-11ea-8856-57a8f16560a7" alt="png" /></p>
<p>与 hprof 相比JVisualVM 更加容易使用 —— 比如上面的截图中,在一个地方就可以看到所有 int[] 的分配信息,所以多次在同一处代码进行分配的情况就很容易发现。</p>
<h4><strong>AProf</strong></h4>
<p>AProf 是一款重要的分析器,是由 Devexperts 开发的 <strong>AProf</strong>。内存分配分析器 AProf 也被打包为 Java agent 的形式。</p>

View File

@@ -204,7 +204,7 @@ function hide_canvas() {
<li>以猜测来驱动,凭历史经验进行排查。</li>
</ul>
<p>如果您倾向于选择后一种方式,那么可能会浪费大量的时间,效果得看运气。更糟糕的是,因为基本靠蒙,所以这个过程是完全不可预测的,如果时间很紧张,就会在团队内部造成压力,甚至升级为甩锅和互相指责。</p>
<p><img src="assets/7e9de660-79aa-11ea-9164-d34ec3ae1078" alt="66772038.png" /></p>
<p><img src="assets/7e9de660-79aa-11ea-9164-d34ec3ae1078" alt="png" /></p>
<p>系统出现性能问题或者故障,究竟是不是 JVM 的问题,得从各个层面依次进行排查。</p>
<h3>为什么问题排查这么困难?</h3>
<h4><strong>生产环境中进行故障排查的困难</strong></h4>
@@ -305,7 +305,7 @@ function hide_canvas() {
<p>做好监控,定位问题,验证结果,总结归纳。</p>
</blockquote>
<p>下面我们看看 JVM 领域有哪些问题.</p>
<p><img src="assets/485cc7f0-79ab-11ea-9057-3b1d25666613" alt="47867364.png" /></p>
<p><img src="assets/485cc7f0-79ab-11ea-9057-3b1d25666613" alt="png" /></p>
<p>从上图可以看到JVM 可以划分为这些部分:</p>
<ul>
<li>执行引擎包括GC、JIT 编译器</li>

View File

@@ -198,16 +198,16 @@ function hide_canvas() {
<h4><strong>问题现象描述</strong></h4>
<p>最近一段时间,通过监控指标发现,有一个服务节点的最大 GC 暂停时间经常会达到 400ms 以上。</p>
<p>如下图所示:</p>
<p><img src="assets/a2020390-7ba6-11ea-9687-397b36d0cdab" alt="85083641.png" /></p>
<p><img src="assets/a2020390-7ba6-11ea-9687-397b36d0cdab" alt="png" /></p>
<p>从图中可以看到GC 暂停时间的峰值达到了 546ms这里展示的时间点是 2020 年 02 月 04 日 09:20:00 左右。</p>
<p>客户表示这种情况必须解决,因为服务调用的超时时间为 1s要求最大 GC 暂停时间不超过 200ms平均暂停时间达到 100ms 以内,对客户的交易策略产生了极大的影响。</p>
<h4><strong>CPU 负载</strong></h4>
<p>CPU 的使用情况如下图所示:</p>
<p><img src="assets/333d8e90-7b4a-11ea-99ef-3bc3936801ae" alt="58517646.png" /></p>
<p><img src="assets/333d8e90-7b4a-11ea-99ef-3bc3936801ae" alt="png" /></p>
<p>从图中可以看到:系统负载为 4.92CPU使用率 7% 左右,其实这个图中隐含了一些重要的线索,但我们此时并没有发现什么问题。</p>
<h4><strong>GC 内存使用情况</strong></h4>
<p>然后我们排查了这段时间的内存使用情况:</p>
<p><img src="assets/b2ee4a10-7ba6-11ea-8a35-fda221135a5a" alt="85674124.png" /></p>
<p><img src="assets/b2ee4a10-7ba6-11ea-8a35-fda221135a5a" alt="png" /></p>
<p>从图中可以看到,大约 09:25 左右 old_gen 使用量大幅下跌,确实是发生了 FullGC。</p>
<p>但 09:20 前后,老年代空间的使用量在缓慢上升,并没有下降,也就是说引发最大暂停时间的这个点并没有发生 FullGC。</p>
<p>当然,这些是事后复盘分析得出的结论。当时对监控所反馈的信息并不是特别信任,怀疑就是触发了 FullGC 导致的长时间 GC 暂停。</p>
@@ -232,16 +232,16 @@ function hide_canvas() {
<pre><code>-Xmx4g -Xms4g -XX:+UseG1GC -XX:MaxGCPauseMillis=50
</code></pre>
<p>接着服务启动成功,等待健康检测自动切换为新的服务节点,继续查看指标。</p>
<p><img src="assets/36b1d380-7ba7-11ea-a711-0f902cb8434c" alt="55932616.png" /></p>
<p><img src="assets/36b1d380-7ba7-11ea-a711-0f902cb8434c" alt="png" /></p>
<p>看看暂停时间,每个节点的 GC 暂停时间都降下来了,基本上在 50ms 以内,比较符合我们的预期。</p>
<p>嗯!事情到此结束了?远远没有。</p>
<h4><strong>“彩蛋”惊喜</strong></h4>
<p>过了一段时间,我们发现了个下面这个惊喜(也许是惊吓),如下图所示:</p>
<p><img src="assets/3f7a8390-7ba7-11ea-b2e6-fdc2f968c34f" alt="50244521.png" /></p>
<p><img src="assets/3f7a8390-7ba7-11ea-b2e6-fdc2f968c34f" alt="png" /></p>
<p>中奖了,运行一段时间后,最大 GC 暂停时间达到了 1300ms。</p>
<p>情况似乎更恶劣了。</p>
<p>继续观察,发现不是个别现象:</p>
<p><img src="assets/46a51400-7ba7-11ea-8dae-453849991cc6" alt="84108207.png" /></p>
<p><img src="assets/46a51400-7ba7-11ea-8dae-453849991cc6" alt="png" /></p>
<p>内心是懵的,觉得可能是指标算错了,比如把 10s 内的暂停时间全部加到了一起。</p>
<h4><strong>注册 GC 事件监听</strong></h4>
<p>于是想了个办法,通过 JMX 注册 GC 事件监听,把相关的信息直接打印出来。</p>
@@ -292,7 +292,7 @@ for (GarbageCollectorMXBean mbean
-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
</code></pre>
<p>重新启动,希望这次能排查出问题的原因。</p>
<p><img src="assets/5431dbd0-7ba7-11ea-b2e6-fdc2f968c34f" alt="84324884.png" /></p>
<p><img src="assets/5431dbd0-7ba7-11ea-b2e6-fdc2f968c34f" alt="png" /></p>
<p>运行一段时间,又发现了超长的暂停时间。</p>
<h4><strong>分析 GC 日志</strong></h4>
<p>因为不涉及敏感数据,那么我们把 GC 日志下载到本地进行分析。</p>
@@ -356,7 +356,7 @@ CommandLine flags:
<p>看到这么多的 GC 工作线程我就开始警惕了,毕竟堆内存才指定了 4GB。</p>
<p>按照一般的 CPU 和内存资源配比,常见的比例差不多是 4 核 4GB、4 核 8GB 这样的。</p>
<p>看看对应的 CPU 负载监控信息:</p>
<p><img src="assets/880bc8d0-7ba7-11ea-8940-6df1558f5aa2" alt="4600411.png" /></p>
<p><img src="assets/880bc8d0-7ba7-11ea-8940-6df1558f5aa2" alt="png" /></p>
<p>通过和运维同学的沟通,得到这个节点的配置被限制为 4 核 8GB。</p>
<p>这样一来GC 暂停时间过长的原因就定位到了:</p>
<ul>
@@ -383,7 +383,7 @@ CommandLine flags:
<p>设置并发标记的 GC 线程数量。默认值大约是 ParallelGCThreads 的四分之一。</p>
<p>一般来说不用指定并发标记的 GC 线程数量,只用指定并行的即可。</p>
<p>重新启动之后,看看 GC 暂停时间指标:</p>
<p><img src="assets/95b9fb50-7ba7-11ea-9687-397b36d0cdab" alt="81569009.png" /></p>
<p><img src="assets/95b9fb50-7ba7-11ea-9687-397b36d0cdab" alt="png" /></p>
<p>红色箭头所指示的点就是重启的时间点,可以发现,暂停时间基本上都处于 50ms 范围内。</p>
<p>后续的监控发现,这个参数确实解决了问题。</p>
<p>那么还有没有其他的办法呢?请关注后续的章节《应对容器时代面临的挑战》。</p>

View File

@@ -411,7 +411,7 @@ function hide_canvas() {
<p>请注意,<strong>只能根据 Minor GC 计算提升速率</strong>。Full GC 的日志不能用于计算提升速率,因为 Major GC 会清理掉老年代中的一部分对象。</p>
<h4><strong>提升速率的意义</strong></h4>
<p>和分配速率一样,提升速率也会影响 GC 暂停的频率。但分配速率主要影响 <a href="http://blog.csdn.net/renfufei/article/details/54144385#t8">minor GC</a>,而提升速率则影响 <a href="http://blog.csdn.net/renfufei/article/details/54144385#t8">major GC</a> 的频率。有大量的对象提升,自然很快将老年代填满。老年代填充的越快,则 Major GC 事件的频率就会越高。</p>
<p><img src="assets/a7aac9d0-7e76-11ea-b43f-a740880350b3" alt="how-java-garbage-collection-works" /></p>
<p><img src="assets/a7aac9d0-7e76-11ea-b43f-a740880350b3" alt="png" /></p>
<p>前面章节提到过Full GC 通常需要更多的时间,因为需要处理更多的对象,还要执行碎片整理等额外的复杂过程。</p>
<h4><strong>示例</strong></h4>
<p>让我们看一个<a href="https://github.com/gvsmirnov/java-perv/blob/master/labs-8/src/main/java/ru/gvsmirnov/perv/labs/gc/PrematurePromotion.java">过早提升的示例</a>。这个程序创建/获取大量的对象/数据,并暂存到集合之中,达到一定数量后进行批处理:</p>