learn.lianglianglee.com/专栏/深入浅出 Java 虚拟机-完/07 深入剖析:垃圾回收你真的了解吗?(下).md.html
2022-05-11 18:52:13 +08:00

835 lines
23 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>07 深入剖析:垃圾回收你真的了解吗?(下).md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/00 开篇词JVM一块难啃的骨头.md">00 开篇词JVM一块难啃的骨头.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/01 一探究竟:为什么需要 JVM它处在什么位置.md">01 一探究竟:为什么需要 JVM它处在什么位置.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/02 大厂面试题:你不得不掌握的 JVM 内存管理.md">02 大厂面试题:你不得不掌握的 JVM 内存管理.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/03 大厂面试题:从覆盖 JDK 的类开始掌握类的加载机制.md">03 大厂面试题:从覆盖 JDK 的类开始掌握类的加载机制.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/04 动手实践:从栈帧看字节码是如何在 JVM 中进行流转的.md">04 动手实践:从栈帧看字节码是如何在 JVM 中进行流转的.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/05 大厂面试题:得心应手应对 OOM 的疑难杂症.md">05 大厂面试题:得心应手应对 OOM 的疑难杂症.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/06 深入剖析:垃圾回收你真的了解吗?(上).md">06 深入剖析:垃圾回收你真的了解吗?(上).md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/深入浅出 Java 虚拟机-完/07 深入剖析:垃圾回收你真的了解吗?(下).md">07 深入剖析:垃圾回收你真的了解吗?(下).md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/08 大厂面试题:有了 G1 还需要其他垃圾回收器吗?.md">08 大厂面试题:有了 G1 还需要其他垃圾回收器吗?.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/09 案例实战:亿级流量高并发下如何进行估算和调优.md">09 案例实战:亿级流量高并发下如何进行估算和调优.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/10 第09讲案例实战面对突如其来的 GC 问题如何下手解决.md">10 第09讲案例实战面对突如其来的 GC 问题如何下手解决.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/11 第10讲动手实践自己模拟 JVM 内存溢出场景.md">11 第10讲动手实践自己模拟 JVM 内存溢出场景.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/12 第11讲动手实践遇到问题不要慌轻松搞定内存泄漏.md">12 第11讲动手实践遇到问题不要慌轻松搞定内存泄漏.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/13 工具进阶:如何利用 MAT 找到问题发生的根本原因.md">13 工具进阶:如何利用 MAT 找到问题发生的根本原因.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/14 动手实践:让面试官刮目相看的堆外内存排查.md">14 动手实践:让面试官刮目相看的堆外内存排查.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/15 预警与解决:深入浅出 GC 监控与调优.md">15 预警与解决:深入浅出 GC 监控与调优.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/16 案例分析:一个高死亡率的报表系统的优化之路.md">16 案例分析:一个高死亡率的报表系统的优化之路.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/17 案例分析:分库分表后,我的应用崩溃了.md">17 案例分析:分库分表后,我的应用崩溃了.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/18 动手实践:从字节码看方法调用的底层实现.md">18 动手实践:从字节码看方法调用的底层实现.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/19 大厂面试题:不要搞混 JMM 与 JVM.md">19 大厂面试题:不要搞混 JMM 与 JVM.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/20 动手实践:从字节码看并发编程的底层实现.md">20 动手实践:从字节码看并发编程的底层实现.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/21 动手实践:不为人熟知的字节码指令.md">21 动手实践:不为人熟知的字节码指令.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/22 深入剖析:如何使用 Java Agent 技术对字节码进行修改.md">22 深入剖析:如何使用 Java Agent 技术对字节码进行修改.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/23 动手实践JIT 参数配置如何影响程序运行?.md">23 动手实践JIT 参数配置如何影响程序运行?.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/24 案例分析:大型项目如何进行性能瓶颈调优?.md">24 案例分析:大型项目如何进行性能瓶颈调优?.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/25 未来JVM 的历史与展望.md">25 未来JVM 的历史与展望.md.html</a>
</li>
<li>
<a href="/专栏/深入浅出 Java 虚拟机-完/26 福利:常见 JVM 面试题补充.md">26 福利:常见 JVM 面试题补充.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>07 深入剖析:垃圾回收你真的了解吗?(下)</h1>
<p>由于上一课时篇幅比较多,我们在这一课时重点讲解上一课时中提到的 CMS 垃圾回收器,让你可以更好的理解垃圾回收的过程。</p>
<p>在这里首先给你介绍几个概念:</p>
<ul>
<li>Minor GC发生在年轻代的 GC。</li>
<li>Major GC发生在老年代的 GC。</li>
<li>Full GC全堆垃圾回收。比如 Metaspace 区引起年轻代和老年代的回收。</li>
</ul>
<p>理解了这三个概念,我们再往下看。</p>
<p>CMS 的全称是 Mostly Concurrent Mark and Sweep Garbage Collector主要并发­标记­清除­垃圾收集器它在年轻代使用<strong>复制</strong>算法,而对老年代使用<strong>标记-清除</strong>算法。你可以看到,在老年代阶段,比起 Mark-Sweep它多了一个并发字样。</p>
<p>CMS 的设计目标,是避免在老年代 GC 时出现长时间的卡顿(但它并不是一个老年代回收器)。如果你不希望有长时间的停顿,同时你的 CPU 资源也比较丰富,使用 CMS 是比较合适的。</p>
<p>CMS 使用的是 Sweep 而不是 Compact所以它的主要问题是碎片化。随着 JVM 的长时间运行,碎片化会越来越严重,只有通过 Full GC 才能完成整理。</p>
<p>为什么 CMS 能够获得更小的停顿时间呢?主要是因为它把最耗时的一些操作,做成了和应用线程并行。接下来我们简要看一下这个过程。</p>
<h2><strong>CMS 回收过程</strong></h2>
<h3><strong>初始标记Initial Mark</strong></h3>
<p>初始标记阶段,只标记直接关联 GC root 的对象,不用向下追溯。因为最耗时的就在 tracing 阶段,这样就极大地缩短了初始标记时间。</p>
<p>这个过程是 STW 的,但由于只是标记第一层,所以速度是很快的。</p>
<p><img src="assets/Cgq2xl4lRrKAQIPzAABOGxOincY196.jpg" alt="img" /></p>
<p>注意,这里除了要标记相关的 GC Roots 之外,还要标记年轻代中对象的引用,这也是 CMS 老年代回收,依然要扫描新生代的原因。</p>
<h3><strong>并发标记Concurrent Mark</strong></h3>
<p>在初始标记的基础上,进行并发标记。这一步骤主要是 tracinng 的过程,用于标记所有可达的对象。</p>
<p>这个过程会持续比较长的时间,但却可以和用户线程并行。在这个阶段的执行过程中,可能会产生很多变化:</p>
<ul>
<li>有些对象,从新生代晋升到了老年代;</li>
<li>有些对象,直接分配到了老年代;</li>
<li>老年代或者新生代的对象引用发生了变化。</li>
</ul>
<p><img src="assets/CgpOIF4lRrKAF0PnAAB8h8sikiU148.jpg" alt="img" /></p>
<p>还记得我们在上一课时提到的卡片标记么?在这个阶段受到影响的老年代对象所对应的卡页,会被标记为 dirty用于后续重新标记阶段的扫描。</p>
<h3><strong>并发预清理Concurrent Preclean</strong></h3>
<p>并发预清理也是不需要 STW 的,目的是为了让重新标记阶段的 STW 尽可能短。这个时候,老年代中被标记为 dirty 的卡页中的对象,就会被重新标记,然后清除掉 dirty 的状态。</p>
<p>由于这个阶段也是可以并发的,在执行过程中引用关系依然会发生一些变化。我们可以假定这个清理动作是第一次清理。</p>
<p>所以重新标记阶段,有可能还会有处于 dirty 状态的卡页。</p>
<h3><strong>并发可取消的预清理Concurrent Abortable Preclean</strong></h3>
<p>因为重新标记是需要 STW 的,所以会有很多次预清理动作。并发可取消的预清理,顾名思义,在满足某些条件的时候,可以终止,比如迭代次数、有用工作量、消耗的系统时间等。</p>
<p>这个阶段是可选的。换句话说,这个阶段是“并发预清理”阶段的一种优化。</p>
<p>这个阶段的第一个意图,是避免回扫年轻代的大量对象;另外一个意图,就是当满足最终标记的条件时,自动退出。</p>
<p>我们在前面说过,标记动作是需要扫描年轻代的。如果年轻代的对象太多,肯定会严重影响标记的时间。如果在此之前能够进行一次 Minor GC情况会不会变得好了许多</p>
<p>CMS 提供了参数 CMSScavengeBeforeRemark可以在进入重新标记之前强制进行一次 Minor GC。</p>
<p>但请你记住一件事情GC 的停顿是不分什么年轻代老年代的。设置了上面的参数,可能会在一个比较长的 Minor GC 之后,紧跟着一个 CMS 的 Remark它们都是 STW 的。</p>
<p>这部分有非常多的配置参数。但是一般都不会去改动。</p>
<h3><strong>最终标记Final Remark</strong></h3>
<p>通常 CMS 会尝试在年轻代尽可能空的情况下运行 Final Remark 阶段,以免接连多次发生 STW 事件。</p>
<p>这是 CMS 垃圾回收阶段的第二次 STW 阶段,目标是完成老年代中所有存活对象的标记。我们前面多轮的 preclean 阶段,一直在和应用线程玩追赶游戏,有可能跟不上引用的变化速度。本轮的标记动作就需要 STW 来处理这些情况。</p>
<p>如果预处理阶段做的不够好,会显著增加本阶段的 STW 时间。你可以看到CMS 垃圾回收器把回收过程分了多个部分,而影响最大的不是 STW 阶段本身,而是它之前的预处理动作。</p>
<h3><strong>并发清除Concurrent Sweep</strong></h3>
<p>此阶段用户线程被重新激活,目标是删掉不可达的对象,并回收它们的空间。</p>
<p>由于 CMS 并发清理阶段用户线程还在运行中伴随程序运行自然就还会有新的垃圾不断产生这一部分垃圾出现在标记过程之后CMS 无法在当次 GC 中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。</p>
<p><img src="assets/Cgq2xl4lRrKAep0SAABz3WUkbVs940.jpg" alt="img" /></p>
<h3><strong>并发重置Concurrent Reset</strong></h3>
<p>此阶段与应用程序并发执行,重置 CMS 算法相关的内部数据,为下一次 GC 循环做准备。</p>
<h2><strong>内存碎片</strong></h2>
<p>由于 CMS 在执行过程中用户线程还需要运行那就需要保证有充足的内存空间供用户使用。如果等到老年代空间快满了再开启这个回收过程用户线程可能会产生“Concurrent Mode Failure”的错误这时会临时启用 Serial Old 收集器来重新进行老年代的垃圾收集这样停顿时间就很长了STW</p>
<p>这部分空间预留,一般在 30% 左右即可,那么能用的大概只有 70%。参数 -XX:CMSInitiatingOccupancyFraction 用来配置这个比例记得要首先开启参数UseCMSInitiatingOccupancyOnly。也就是说当老年代的使用率达到 70%,就会触发 GC 了。如果你的系统老年代增长不是太快,可以调高这个参数,降低内存回收的次数。</p>
<p>其实,这个比率非常不好设置。一般在堆大小小于 2GB 的时候,都不会考虑 CMS 垃圾回收器。</p>
<p>另外CMS 对老年代回收的时候,并没有内存的整理阶段。这就造成程序在长时间运行之后,碎片太多。如果你申请一个稍大的对象,就会引起分配失败。</p>
<p>CMS 提供了两个参数来解决这个问题:</p>
<p>1 UseCMSCompactAtFullCollection默认开启表示在要进行 Full GC 的时候,进行内存碎片整理。内存整理的过程是无法并发的,所以停顿时间会变长。</p>
<p>2CMSFullGCsBeforeCompaction每隔多少次不压缩的 Full GC 后,执行一次带压缩的 Full GC。默认值为 0表示每次进入 Full GC 时都进行碎片整理。</p>
<p>所以,预留空间加上内存的碎片,使用 CMS 垃圾回收器的老年代,留给我们的空间就不是太多,这也是 CMS 的一个弱点。</p>
<p><img src="assets/CgpOIF4lRrKAV66pAAA1L5MnJkA015.jpg" alt="img" /></p>
<h2><strong>小结</strong></h2>
<p>一般的,我们将 CMS 垃圾回收器分为四个阶段:</p>
<ol>
<li>初始标记</li>
<li>并发标记</li>
<li>重新标记</li>
<li>并发清理</li>
</ol>
<p>我们总结一下 CMS 中都会有哪些停顿STW</p>
<ol>
<li>初始标记,这部分的停顿时间较短;</li>
<li>Minor GC可选在预处理阶段对年轻代的回收停顿由年轻代决定</li>
<li>重新标记,由于 preclaen 阶段的介入,这部分停顿也较短;</li>
<li>Serial-Old 收集老年代的停顿,主要发生在预留空间不足的情况下,时间会持续很长;</li>
<li>Full GC永久代空间耗尽时的操作由于会有整理阶段持续时间较长。</li>
</ol>
<p>在发生 GC 问题时你一定要明确发生在哪个阶段然后对症下药。gclog 通常能够非常详细的表现这个过程。</p>
<p>我们再来看一下 CMS 的 trade-off。</p>
<p><strong>优势:</strong></p>
<p>低延迟,尤其对于大堆来说。大部分垃圾回收过程并发执行。</p>
<p><strong>劣势:</strong></p>
<ol>
<li>内存碎片问题。Full GC 的整理阶段,会造成较长时间的停顿。</li>
<li>需要预留空间,用来分配收集阶段产生的“浮动垃圾”。</li>
<li>使用更多的 CPU 资源,在应用运行的同时进行堆扫描。</li>
</ol>
<p>CMS 是一种高度可配置的复杂算法,因此给 JDK 中的 GC 代码库带来了很多复杂性。由于 G1 和 ZGC 的产生CMS 已经在被废弃的路上。但是,目前仍然有大部分应用是运行在 Java8 及以下的版本之上,针对它的优化,还是要持续很长一段时间。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/深入浅出 Java 虚拟机-完/06 深入剖析:垃圾回收你真的了解吗?(上).md">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/深入浅出 Java 虚拟机-完/08 大厂面试题:有了 G1 还需要其他垃圾回收器吗?.md">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997a538dd43cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>