learn.lianglianglee.com/专栏/Java并发编程实战/25 答疑课堂:模块四热点问题解答.md.html
2022-05-11 18:57:05 +08:00

1069 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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>25 答疑课堂:模块四热点问题解答.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 开篇词你为什么需要学习并发编程?.md.html">00 开篇词你为什么需要学习并发编程?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/01 如何制定性能调优标准?.md.html">01 如何制定性能调优标准?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/02 如何制定性能调优策略?.md.html">02 如何制定性能调优策略?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/03 字符串性能优化不容小觑百M内存轻松存储几十G数据.md.html">03 字符串性能优化不容小觑百M内存轻松存储几十G数据.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/04 慎重使用正则表达式.md.html">04 慎重使用正则表达式.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/05 ArrayList还是LinkedList使用不当性能差千倍.md.html">05 ArrayList还是LinkedList使用不当性能差千倍.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/06 Stream如何提高遍历集合效率.md.html">06 Stream如何提高遍历集合效率.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/07 深入浅出HashMap的设计与优化.md.html">07 深入浅出HashMap的设计与优化.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/08 网络通信优化之IO模型如何解决高并发下IO瓶颈.md.html">08 网络通信优化之IO模型如何解决高并发下IO瓶颈.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/09 网络通信优化之序列化避免使用Java序列化.md.html">09 网络通信优化之序列化避免使用Java序列化.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/10 网络通信优化之通信协议如何优化RPC网络通信.md.html">10 网络通信优化之通信协议如何优化RPC网络通信.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/11 答疑课堂深入了解NIO的优化实现原理.md.html">11 答疑课堂深入了解NIO的优化实现原理.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/12 多线程之锁优化深入了解Synchronized同步锁的优化方法.md.html">12 多线程之锁优化深入了解Synchronized同步锁的优化方法.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/13 多线程之锁优化深入了解Lock同步锁的优化方法.md.html">13 多线程之锁优化深入了解Lock同步锁的优化方法.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/14 多线程之锁优化(下):使用乐观锁优化并行操作.md.html">14 多线程之锁优化(下):使用乐观锁优化并行操作.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/15 多线程调优(上):哪些操作导致了上下文切换?.md.html">15 多线程调优(上):哪些操作导致了上下文切换?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/16 多线程调优(下):如何优化多线程上下文切换?.md.html">16 多线程调优(下):如何优化多线程上下文切换?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/17 并发容器的使用:识别不同场景下最优容器.md.html">17 并发容器的使用:识别不同场景下最优容器.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/18 如何设置线程池大小?.md.html">18 如何设置线程池大小?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/19 如何用协程来优化多线程业务?.md.html">19 如何用协程来优化多线程业务?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/20 磨刀不误砍柴工欲知JVM调优先了解JVM内存模型.md.html">20 磨刀不误砍柴工欲知JVM调优先了解JVM内存模型.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/21 深入JVM即时编译器JIT优化Java编译.md.html">21 深入JVM即时编译器JIT优化Java编译.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/22 如何优化垃圾回收机制?.md.html">22 如何优化垃圾回收机制?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/23 如何优化JVM内存分配.md.html">23 如何优化JVM内存分配.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/24 内存持续上升,我该如何排查问题?.md.html">24 内存持续上升,我该如何排查问题?.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/Java并发编程实战/25 答疑课堂:模块四热点问题解答.md.html">25 答疑课堂:模块四热点问题解答.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/26 单例模式:如何创建单一对象优化系统性能?.md.html">26 单例模式:如何创建单一对象优化系统性能?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/27 原型模式与享元模式:提升系统性能的利器.md.html">27 原型模式与享元模式:提升系统性能的利器.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/28 如何使用设计模式优化并发编程?.md.html">28 如何使用设计模式优化并发编程?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/29 生产者消费者模式:电商库存设计优化.md.html">29 生产者消费者模式:电商库存设计优化.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/30 装饰器模式:如何优化电商系统中复杂的商品价格策略?.md.html">30 装饰器模式:如何优化电商系统中复杂的商品价格策略?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/31 答疑课堂:模块五思考题集锦.md.html">31 答疑课堂:模块五思考题集锦.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/32 MySQL调优之SQL语句如何写出高性能SQL语句.md.html">32 MySQL调优之SQL语句如何写出高性能SQL语句.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/33 MySQL调优之事务高并发场景下的数据库事务调优.md.html">33 MySQL调优之事务高并发场景下的数据库事务调优.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/34 MySQL调优之索引索引的失效与优化.md.html">34 MySQL调优之索引索引的失效与优化.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/35 记一次线上SQL死锁事故如何避免死锁.md.html">35 记一次线上SQL死锁事故如何避免死锁.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/36 什么时候需要分表分库?.md.html">36 什么时候需要分表分库?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/37 电商系统表设计优化案例分析.md.html">37 电商系统表设计优化案例分析.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/38 数据库参数设置优化,失之毫厘差之千里.md.html">38 数据库参数设置优化,失之毫厘差之千里.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/39 答疑课堂MySQL中InnoDB的知识点串讲.md.html">39 答疑课堂MySQL中InnoDB的知识点串讲.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/41 如何设计更优的分布式锁?.md.html">41 如何设计更优的分布式锁?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/42 电商系统的分布式事务调优.md.html">42 电商系统的分布式事务调优.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/43 如何使用缓存优化系统性能?.md.html">43 如何使用缓存优化系统性能?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/44 记一次双十一抢购性能瓶颈调优.md.html">44 记一次双十一抢购性能瓶颈调优.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/加餐 什么是数据的强、弱一致性?.md.html">加餐 什么是数据的强、弱一致性?.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/加餐 推荐几款常用的性能测试工具.md.html">加餐 推荐几款常用的性能测试工具.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/答疑课堂:模块三热点问题解答.md.html">答疑课堂:模块三热点问题解答.md.html</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/结束语 栉风沐雨,砥砺前行!.md.html">结束语 栉风沐雨,砥砺前行!.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>25 答疑课堂:模块四热点问题解答</h1>
<p>你好,我是刘超。</p>
<p>本周我们结束了“JVM 性能监测及调优”的学习,这一期答疑课堂我精选了模块四中 11 位同学的留言,进行集中解答,希望也能对你有所帮助。另外,我想为坚持跟到现在的同学点个赞,期待我们能有更多的技术交流,共同成长。</p>
<h2>[第 20 讲]</h2>
<p><img src="assets/31a205290c3b2391f115ee77f511a43a.jpeg" alt="img" /></p>
<p>很多同学都问到了类似“黑夜里的猫&quot;问到的问题所以我来集中回复一下。JVM 的内存模型只是一个规范,方法区也是一个规范,一个逻辑分区,并不是一个物理空间,我们这里说的字符串常量放在堆内存空间中,是指实际的物理空间。</p>
<p><img src="assets/2ac5ee0c9a6fe67ce8f896be75d05f01.jpeg" alt="img" /></p>
<p>文灏的问题和上一个类似,一同回复一下。元空间是属于方法区的,方法区只是一个逻辑分区,而元空间是具体实现。所以类的元数据是存放在元空间,逻辑上属于方法区。</p>
<h2>[第 21 讲]</h2>
<p><img src="assets/f2fa07e388f5a3dbe84bb12bfea5ee76.jpeg" alt="img" /></p>
<p>Liam 同学,目前 Hotspot 虚拟机暂时不支持栈上分配对象。W.LI 同学的留言值得参考,所以这里一同贴出来了。</p>
<p><img src="assets/20e59cb2df51bd171d41c81e074821f2.jpeg" alt="img" /></p>
<h2>[第 22 讲]</h2>
<p><img src="assets/09ada15236e8ceeef2558d6ab7505425.jpeg" alt="img" /></p>
<p>非常赞Region 这块Jxin 同学讲解得很到位。这里我再总结下 CMS 和 G1 的一些知识点。</p>
<p>CMS 垃圾收集器是基于标记清除算法实现的目前主要用于老年代垃圾回收。CMS 收集器的 GC 周期主要由 7 个阶段组成,其中有两个阶段会发生 stop-the-world其它阶段都是并发执行的。</p>
<p><img src="assets/500c2f0e112ced378fd49a09c61c5caa.jpg" alt="img" /></p>
<p>G1 垃圾收集器是基于标记整理算法实现的,是一个分代垃圾收集器,既负责年轻代,也负责老年代的垃圾回收。</p>
<p>跟之前各个分代使用连续的虚拟内存地址不一样G1 使用了一种 Region 方式对堆内存进行了划分,同样也分年轻代、老年代,但每一代使用的是 N 个不连续的 Region 内存块,每个 Region 占用一块连续的虚拟内存地址。</p>
<p>在 G1 中,还有一种叫 Humongous 区域用于存储特别大的对象。G1 内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代的 YoungGC 中被回收掉。</p>
<p><img src="assets/f832278afd5cdb94decd1f6826056dbe.jpg" alt="img" /></p>
<p>G1 分为 Young GC、Mix GC 以及 Full GC。</p>
<p>G1 Young GC 主要是在 Eden 区进行,当 Eden 区空间不足时,则会触发一次 Young GC。将 Eden 区数据移到 Survivor 空间时,如果 Survivor 空间不足,则会直接晋升到老年代。此时 Survivor 的数据也会晋升到老年代。Young GC 的执行是并行的,期间会发生 STW。</p>
<p>当堆空间的占用率达到一定阈值后会触发 G1 Mix GC阈值由命令参数 -XX:InitiatingHeapOccupancyPercent 设定,默认值 45Mix GC 主要包括了四个阶段,其中只有并发标记阶段不会发生 STW其它阶段均会发生 STW。</p>
<p><img src="assets/b8090ff2c7ddf54fb5f6e3c19a36d32f.jpg" alt="img" /></p>
<p>G1 和 CMS 主要的区别在于:</p>
<ul>
<li>CMS 主要集中在老年代的回收,而 G1 集中在分代回收,包括了年轻代的 Young GC 以及老年代的 Mix GC</li>
<li>G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生;</li>
<li>在初始化标记阶段,搜索可达对象使用到的 Card Table其实现方式不一样。</li>
</ul>
<p>这里我简单解释下 Card Table在垃圾回收的时候都是从 Root 开始搜索,这会先经过年轻代再到老年代,也有可能老年代引用到年轻代对象,如果发生 Young GC除了从年轻代扫描根对象之外还需要再从老年代扫描根对象确认引用年轻代对象的情况。</p>
<p>**这种属于跨代处理,非常消耗性能。**为了避免在回收年轻代时跨代扫描整个老年代CMS 和 G1 都用到了 Card Table 来记录这些引用关系。只是 G1 在 Card Table 的基础上引入了 RSet每个 Region 初始化时,都会初始化一个 RSetRSet 记录了其它 Region 中的对象引用本 Region 对象的关系。</p>
<p>除此之外CMS 和 G1 在解决并发标记时漏标的方式也不一样CMS 使用的是 Incremental Update 算法,而 G1 使用的是 SATB 算法。</p>
<p>首先我们要了解在并发标记中G1 和 CMS 都是基于三色标记算法来实现的:</p>
<ul>
<li>黑色:根对象,或者对象和对象中的子对象都被扫描;</li>
<li>灰色:对象本身被扫描,但还没扫描对象中的子对象;</li>
<li>白色:不可达对象。</li>
</ul>
<p>基于这种标记有一个漏标的问题,也就是说,当一个白色标记对象,在垃圾回收被清理掉时,正好有一个对象引用了该白色标记对象,此时由于被回收掉了,就会出现对象丢失的问题。</p>
<p>为了避免上述问题CMS 采用了 Incremental Update 算法只要在写屏障write barrier里发现一个白对象的引用被赋值到一个黑对象的字段里那就把这个白对象变成灰色的。而在 G1 中,采用的是 SATB 算法,该算法认为开始时所有能遍历到的对象都是需要标记的,即认为都是活的。</p>
<p>G1 具备 Pause Prediction Model ,即停顿预测模型。用户可以设定整个 GC 过程中期望的停顿时间,用参数 -XX:MaxGCPauseMillis 可以指定一个 G1 收集过程的目标停顿时间,默认值 200ms。</p>
<p>G1 会根据这个模型统计出来的历史数据,来预测一次垃圾回收所需要的 Region 数量,通过控制 Region 数来控制目标停顿时间的实现。</p>
<p><img src="assets/915e9793981a278112087f0c880b96b4.jpeg" alt="img" /></p>
<p>Liam 提出的这两个问题都非常好。</p>
<p>不管什么 GC都会发送 stop-the-world区别是发生的时间长短。而这个时间跟垃圾收集器又有关系Serial、PartNew、Parallel Scavenge 收集器无论是串行还是并行,都会挂起用户线程,而 CMS 和 G1 在并发标记时是不会挂起用户线程的但其它时候一样会挂起用户线程stop the world 的时间相对来说就小很多了。</p>
<p>Major Gc 在很多参考资料中是等价于 Full GC 的,我们也可以发现很多性能监测工具中只有 Minor GC 和 Full GC。一般情况下一次 Full GC 将会对年轻代、老年代、元空间以及堆外内存进行垃圾回收。触发 Full GC 的原因有很多:</p>
<ul>
<li>当年轻代晋升到老年代的对象大小,并比目前老年代剩余的空间大小还要大时,会触发 Full GC</li>
<li>当老年代的空间使用率超过某阈值时,会触发 Full GC</li>
<li>当元空间不足时JDK1.7 永久代不足),也会触发 Full GC</li>
<li>当调用 System.gc() 也会安排一次 Full GC。</li>
</ul>
<p><img src="assets/a8a506a512922609669b4073d0dbc224.jpeg" alt="img" /></p>
<p>接下来解答 ninghtmare 的提问。我们可以通过 jstat -gc pid interval 查看每次 GC 之后,具体每一个分区的内存使用率变化情况。我们可以通过 JVM 的设置参数,来查看垃圾收集器的具体设置参数,使用的方式有很多,例如 jcmd pid VM.flags 就可以查看到相关的设置参数。</p>
<p><img src="assets/26d688a3af534fb00fe3b89d261e5c2e.jpg" alt="img" /></p>
<p>这里附上第 22 讲中,我总结的各个设置参数对应的垃圾收集器图表。</p>
<p><img src="assets/e29c9ac3e53ffbc8a5648644a87d6256.jpeg" alt="img" /></p>
<h2>[第 23 讲]</h2>
<p><img src="assets/bb92ec845c715f9d36a6ce48a0c7d276.jpeg" alt="img" /></p>
<p>我又不乱来同学的留言真是没有乱来,细节掌握得很好!</p>
<p>前提是老年代有足够接受这些对象的空间,才会进行分配担保。如果老年代剩余空间小于每次 Minor GC 晋升到老年代的平均值,则会发起一次 Full GC。</p>
<p><img src="assets/2838514b87e62d69bf51d7a7f12a0c20.jpeg" alt="img" /></p>
<p>看到这里,我发现爱提问的同学始终爱提问,非常鼓励啊,技术是需要交流的,也欢迎你有任何疑问,随时留言给我,我会知无不尽。</p>
<p>现在回答 W.LI 同学的问题。这个会根据我们创建对象占用的内存使用率,合理分配内存,并不仅仅考虑对象晋升的问题,还会综合考虑回收停顿时间等因素。针对某些特殊场景,我们可以手动来调优配置。</p>
<h2>[第 24 讲]</h2>
<p><img src="assets/1080a8574a1a1ded35b736ccbec40524.jpeg" alt="img" /></p>
<p>下面解答 Geek_75b4cd 同学的问题。</p>
<p>我们知道ThreadLocal 是基于 ThreadLocalMap 实现的,这个 Map 的 Entry 继承了 WeakReference而 Entry 对象中的 key 使用了 WeakReference 封装,也就是说 Entry 中的 key 是一个弱引用类型,而弱引用类型只能存活在下次 GC 之前。</p>
<p>如果一个线程调用 ThreadLocal 的 set 设置变量,当前 ThreadLocalMap 则会新增一条记录,但由于发生了一次垃圾回收,此时的 key 值就会被回收,而 value 值依然存在内存中,由于当前线程一直存在,所以 value 值将一直被引用。.</p>
<p>这些被垃圾回收掉的 key 就会一直存在一条引用链的关系Thread --&gt; ThreadLocalMap&gt;Entry&gt;Value。这条引用链会导致 Entry 不会被回收Value 也不会被回收,但 Entry 中的 key 却已经被回收的情况发生,从而造成内存泄漏。</p>
<p>我们只需要在使用完该 key 值之后,将 value 值通过 remove 方法 remove 掉,就可以防止内存泄漏了。</p>
<p><img src="assets/8da35d95d5b31e3f0a582dbd4d47fd9a.jpeg" alt="img" /></p>
<p>最后一个问题来自于 WL 同学。</p>
<p>内存泄漏是指不再使用的对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。例如,我在[第 03 讲]中说到的Java6 中 substring 方法就可能会导致内存泄漏。</p>
<p>当调用 substring 方法时会调用 new string 构造函数,此时会复用原来字符串的 char 数组,而如果我们仅仅是用 substring 获取一小段字符,而在原本 string 字符串非常大的情况下substring 的对象如果一直被引用,由于 substring 里的 char 数组仍然指向原字符串,此时 string 字符串也无法回收,从而导致内存泄露。</p>
<p>内存溢出则是发生了 OutOfMemoryException内存溢出的情况有很多例如堆内存空间不足栈空间不足还有方法区空间不足等都会导致内存溢出。</p>
<p>内存泄漏与内存溢出的关系:内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Java并发编程实战/24 内存持续上升,我该如何排查问题?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Java并发编程实战/26 单例模式:如何创建单一对象优化系统性能?.md.html">下一页</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":"709971933d4a3d60","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>