learn.lianglianglee.com/专栏/深入拆解Java虚拟机/23 逃逸分析.md.html
2022-05-11 18:57:05 +08:00

1229 lines
29 KiB
HTML
Raw Permalink 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>23 逃逸分析.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 开篇词 为什么我们要学习Java虚拟机.md.html">00 开篇词 为什么我们要学习Java虚拟机.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/01 Java代码是怎么运行的.md.html">01 Java代码是怎么运行的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/02 Java的基本类型.md.html">02 Java的基本类型.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/03 Java虚拟机是如何加载Java类的.md.html">03 Java虚拟机是如何加载Java类的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/04 JVM是如何执行方法调用的.md.html">04 JVM是如何执行方法调用的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/05 JVM是如何执行方法调用的.md.html">05 JVM是如何执行方法调用的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/06 JVM是如何处理异常的.md.html">06 JVM是如何处理异常的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/07 JVM是如何实现反射的.md.html">07 JVM是如何实现反射的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/08 JVM是怎么实现invokedynamic的.md.html">08 JVM是怎么实现invokedynamic的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/09 JVM是怎么实现invokedynamic的.md.html">09 JVM是怎么实现invokedynamic的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/10 Java对象的内存布局.md.html">10 Java对象的内存布局.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/11 垃圾回收(上).md.html">11 垃圾回收(上).md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/12 垃圾回收(下).md.html">12 垃圾回收(下).md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/13 Java内存模型.md.html">13 Java内存模型.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/14 Java虚拟机是怎么实现synchronized的.md.html">14 Java虚拟机是怎么实现synchronized的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/15 Java语法糖与Java编译器.md.html">15 Java语法糖与Java编译器.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 Java字节码基础篇.md.html">19 Java字节码基础篇.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/20 方法内联(上).md.html">20 方法内联(上).md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/21 方法内联(下).md.html">21 方法内联(下).md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/22 HotSpot虚拟机的intrinsic.md.html">22 HotSpot虚拟机的intrinsic.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/深入拆解Java虚拟机/23 逃逸分析.md.html">23 逃逸分析.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/24 字段访问相关优化.md.html">24 字段访问相关优化.md.html</a>
</li>
<li>
<a 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 基准测试框架JMH.md.html">28 基准测试框架JMH.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/29 基准测试框架JMH.md.html">29 基准测试框架JMH.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/30 Java虚拟机的监控及诊断工具命令行篇.md.html">30 Java虚拟机的监控及诊断工具命令行篇.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/31 Java虚拟机的监控及诊断工具GUI篇.md.html">31 Java虚拟机的监控及诊断工具GUI篇.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/32 JNI的运行机制.md.html">32 JNI的运行机制.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/33 Java Agent与字节码注入.md.html">33 Java Agent与字节码注入.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/34 Graal用Java编译Java.md.html">34 Graal用Java编译Java.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/35 Truffle语言实现框架.md.html">35 Truffle语言实现框架.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/36 SubstrateVMAOT编译框架.md.html">36 SubstrateVMAOT编译框架.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/尾声丨道阻且长,努力加餐.html.md.html">尾声丨道阻且长,努力加餐.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>23 逃逸分析</h1>
<p>我们知道Java 中<code>Iterable</code>对象的 foreach 循环遍历是一个语法糖Java 编译器会将该语法糖编译为调用<code>Iterable</code>对象的<code>iterator</code>方法,并用所返回的<code>Iterator</code>对象的<code>hasNext</code>以及<code>next</code>方法,来完成遍历。</p>
<pre><code>public void forEach(ArrayList&lt;Object&gt; list, Consumer&lt;Object&gt; f) {
for (Object obj : list) {
f.accept(obj);
}
}
</code></pre>
<p>举个例子,上面的 Java 代码将使用 foreach 循环来遍历一个<code>ArrayList</code>对象,其等价的代码如下所示:</p>
<pre><code>public void forEach(ArrayList&lt;Object&gt; list, Consumer&lt;Object&gt; f) {
Iterator&lt;Object&gt; iter = list.iterator();
while (iter.hasNext()) {
Object obj = iter.next();
f.accept(obj);
}
}
</code></pre>
<p>这里我也列举了所涉及的<code>ArrayList</code>代码。我们可以看到,<code>ArrayList.iterator</code>方法将创建一个<code>ArrayList$Itr</code>实例。</p>
<pre><code>public class ArrayList ... {
public Iterator&lt;E&gt; iterator() {
return new Itr();
}
private class Itr implements Iterator&lt;E&gt; {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
...
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings(&quot;unchecked&quot;)
public E next() {
checkForComodification();
int i = cursor;
if (i &gt;= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i &gt;= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
...
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
}
</code></pre>
<p>因此,有同学认为我们应当避免在热点代码中使用 foreach 循环,并且直接使用基于<code>ArrayList.size</code>以及<code>ArrayList.get</code>的循环方式(如下所示),以减少对 Java 堆的压力。</p>
<pre><code>public void forEach(ArrayList&lt;Object&gt; list, Consumer&lt;Object&gt; f) {
for (int i = 0; i &lt; list.size(); i++) {
f.accept(list.get(i));
}
}
</code></pre>
<p>实际上Java 虚拟机中的即时编译器可以将<code>ArrayList.iterator</code>方法中的实例创建操作给优化掉。不过,这需要方法内联以及逃逸分析的协作。</p>
<p>在前面几篇中我们已经深入学习了方法内联,今天我便来介绍一下逃逸分析。</p>
<h2>逃逸分析</h2>
<p>逃逸分析是“一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针”(出处参见 [1])。</p>
<p>在 Java 虚拟机的即时编译语境下,逃逸分析将判断<strong>新建</strong>的对象是否<strong>逃逸</strong>。即时编译器判断对象是否逃逸的依据,一是对象是否被存入堆中(静态字段或者堆中对象的实例字段),二是对象是否被传入未知代码中。</p>
<p>前者很好理解:一旦对象被存入堆中,其他线程便能获得该对象的引用。即时编译器也因此无法追踪所有使用该对象的代码位置。</p>
<p>关于后者,由于 Java 虚拟机的即时编译器是以方法为单位的,对于方法中未被内联的方法调用,即时编译器会将其当成未知代码,毕竟它无法确认该方法调用会不会将调用者或所传入的参数存储至堆中。因此,我们可以认为方法调用的调用者以及参数是逃逸的。</p>
<p>通常来说,即时编译器里的逃逸分析是放在方法内联之后的,以便消除这些“未知代码”入口。</p>
<p>回到文章开头的例子。理想情况下,即时编译器能够内联对<code>ArrayList$Itr</code>构造器的调用,对<code>hasNext</code>以及<code>next</code>方法的调用,以及当内联了<code>Itr.next</code>方法后,对<code>checkForComodification</code>方法的调用。</p>
<p>如果这些方法调用均能够被内联,那么结果将近似于下面这段伪代码:</p>
<pre><code>public void forEach(ArrayList&lt;Object&gt; list, Consumer&lt;Object&gt; f) {
Itr iter = new Itr; // 注意这里是 new 指令
iter.cursor = 0;
iter.lastRet = -1;
iter.expectedModCount = list.modCount;
while (iter.cursor &lt; list.size) {
if (list.modCount != iter.expectedModCount)
throw new ConcurrentModificationException();
int i = iter.cursor;
if (i &gt;= list.size)
throw new NoSuchElementException();
Object[] elementData = list.elementData;
if (i &gt;= elementData.length)
throw new ConcurrentModificationException();
iter.cursor = i + 1;
iter.lastRet = i;
Object obj = elementData[i];
f.accept(obj);
}
}
</code></pre>
<p>可以看到,这段代码所新建的<code>ArrayList$Itr</code>实例既没有被存入任何字段之中,也没有作为任何方法调用的调用者或者参数。因此,逃逸分析将断定该实例不逃逸。</p>
<h2>基于逃逸分析的优化</h2>
<p>即时编译器可以根据逃逸分析的结果进行诸如锁消除、栈上分配以及标量替换的优化。</p>
<p>我们先来看一下锁消除。如果即时编译器能够证明锁对象不逃逸,那么对该锁对象的加锁、解锁操作没有意义。这是因为其他线程并不能获得该锁对象,因此也不可能对其进行加锁。在这种情况下,即时编译器可以消除对该不逃逸锁对象的加锁、解锁操作。</p>
<p>实际上,传统编译器仅需证明锁对象不逃逸出线程,便可以进行锁消除。由于 Java 虚拟机即时编译的限制,上述条件被强化为证明锁对象不逃逸出当前编译的方法。</p>
<p>在介绍 Java 内存模型时,我曾提过<code>synchronized (new Object()) {}</code>会被完全优化掉。这正是因为基于逃逸分析的锁消除。由于其他线程不能获得该锁对象,因此也无法基于该锁对象构造两个线程之间的 happens-before 规则。</p>
<p><code>synchronized (escapedObject) {}</code>则不然。由于其他线程可能会对逃逸了的对象<code>escapedObject</code>进行加锁操作,从而构造了两个线程之间的 happens-before 关系。因此即时编译器至少需要为这段代码生成一条刷新缓存的内存屏障指令。</p>
<p>不过,基于逃逸分析的锁消除实际上并不多见。一般来说,开发人员不会直接对方法中新构造的对象进行加锁。事实上,逃逸分析的结果更多被用于将新建对象操作转换成栈上分配或者标量替换。</p>
<p>我们知道Java 虚拟机中对象都是在堆上分配的而堆上的内容对任何线程都是可见的。与此同时Java 虚拟机需要对所分配的堆内存进行管理,并且在对象不再被引用时回收其所占据的内存。</p>
<p>如果逃逸分析能够证明某些新建的对象不逃逸,那么 Java 虚拟机完全可以将其分配至栈上,并且在 new 语句所在的方法退出时,通过弹出当前方法的栈桢来自动回收所分配的内存空间。这样一来,我们便无须借助垃圾回收器来处理不再被引用的对象。</p>
<p>不过,由于实现起来需要更改大量假设了“对象只能堆分配”的代码,因此 HotSpot 虚拟机<strong>并没有</strong>采用栈上分配,而是使用了标量替换这么一项技术。</p>
<p>所谓的标量,就是仅能存储一个值的变量,比如 Java 代码中的局部变量。与之相反,聚合量则可能同时存储多个值,其中一个典型的例子便是 Java 对象。</p>
<p>标量替换这项优化技术,可以看成将原本对对象的字段的访问,替换为一个个局部变量的访问。举例来说,前面经过内联之后的 forEach 代码可以被转换为如下代码:</p>
<pre><code>public void forEach(ArrayList&lt;Object&gt; list, Consumer&lt;Object&gt; f) {
// Itr iter = new Itr; // 经过标量替换后该分配无意义,可以被优化掉
int cursor = 0; // 标量替换
int lastRet = -1; // 标量替换
int expectedModCount = list.modCount; // 标量替换
while (cursor &lt; list.size) {
if (list.modCount != expectedModCount)
throw new ConcurrentModificationException();
int i = cursor;
if (i &gt;= list.size)
throw new NoSuchElementException();
Object[] elementData = list.elementData;
if (i &gt;= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
lastRet = i;
Object obj = elementData[i];
f.accept(obj);
}
}
</code></pre>
<p>可以看到,原本需要在内存中连续分布的对象,现已被拆散为一个个单独的字段<code>cursor</code><code>lastRet</code>,以及<code>expectedModCount</code>。这些字段既可以存储在栈上,也可以直接存储在寄存器中。而该对象的对象头信息则直接消失了,不再被保存至内存之中。</p>
<p>由于该对象没有被实际分配,因此和栈上分配一样,它同样可以减轻垃圾回收的压力。与栈上分配相比,它对字段的内存连续性不做要求,而且,这些字段甚至可以直接在寄存器中维护,无须浪费任何内存空间。</p>
<h2>部分逃逸分析</h2>
<p>C2 的逃逸分析与控制流无关相对来说比较简单。Graal 则引入了一个与控制流有关的逃逸分析名为部分逃逸分析partial escape analysis[2]。它解决了所新建的实例仅在部分程序路径中逃逸的情况。</p>
<p>举个例子,在下面这段代码中,新建实例只会在进入 if-then 分支时逃逸。(对<code>hashCode</code>方法的调用是一个 HotSpot intrinsic将被替换为一个无法内联的本地方法调用。</p>
<pre><code>public static void bar(boolean cond) {
Object foo = new Object();
if (cond) {
foo.hashCode();
}
}
// 可以手工优化为:
public static void bar(boolean cond) {
if (cond) {
Object foo = new Object();
foo.hashCode();
}
}
</code></pre>
<p>假设 if 语句的条件成立的可能性只有 1%,那么在 99% 的情况下,程序没有必要新建对象。其手工优化的版本正是部分逃逸分析想要自动达到的成果。</p>
<p>部分逃逸分析将根据控制流信息,判断出新建对象仅在部分分支中逃逸,并且将对象的新建操作推延至对象逃逸的分支中。这将使得原本因对象逃逸而无法避免的新建对象操作,不再出现在只执行 if-else 分支的程序路径之中。</p>
<p>综上,与 C2 所使用的逃逸分析相比Graal 所使用的部分逃逸分析能够优化更多的情况,不过它编译时间也更长一些。</p>
<h2>总结与实践</h2>
<p>今天我介绍了 Java 虚拟机中即时编译器的逃逸分析,以及基于逃逸分析的优化。</p>
<p>在 Java 虚拟机的即时编译语境下,逃逸分析将判断新建的对象是否会逃逸。即时编译器判断对象逃逸的依据有两个:一是看对象是否被存入堆中,二是看对象是否作为方法调用的调用者或者参数。</p>
<p>即时编译器会根据逃逸分析的结果进行优化,如锁消除以及标量替换。后者指的是将原本连续分配的对象拆散为一个个单独的字段,分布在栈上或者寄存器中。</p>
<p>部分逃逸分析是一种附带了控制流信息的逃逸分析。它将判断新建对象真正逃逸的分支,并且支持将新建操作推延至逃逸分支。</p>
<hr />
<p>今天的实践环节有两项内容。</p>
<p>第一项内容,我们来验证一下<code>ArrayList.iterator</code>中的新建对象能否被逃逸分析所优化。运行下述代码并观察 GC 的情况。你可以通过虚拟机参数<code>-XX:-DoEscapeAnalysis</code>来关闭默认开启的逃逸分析。</p>
<pre><code>// Run with
// java -XX:+PrintGC -XX:+DoEscapeAnalysis EscapeTest
import java.util.ArrayList;
import java.util.function.Consumer;
public class EscapeTest {
public static void forEach(ArrayList&lt;Object&gt; list, Consumer&lt;Object&gt; f) {
for (Object obj : list) {
f.accept(obj);
}
}
public static void main(String[] args) {
ArrayList&lt;Object&gt; list = new ArrayList&lt;&gt;();
for (int i = 0; i &lt; 100; i++) {
list.add(i);
}
for (int i = 0; i &lt; 400_000_000; i++) {
forEach(list, obj -&gt; {});
}
}
}
</code></pre>
<p>第二项内容,我们来看一看部分逃逸分析的效果。你需要使用附带 Graal 编译器的 Java 版本,如 Java 10来运行下述代码并且观察 GC 的情况。你可以通过虚拟机参数<code>-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler</code>来启用 Graal。</p>
<pre><code>// Run with
// java -Xlog:gc Foo
// java -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler -Xlog:gc Foo
public class Foo {
long placeHolder0;
long placeHolder1;
long placeHolder2;
long placeHolder3;
long placeHolder4;
long placeHolder5;
long placeHolder6;
long placeHolder7;
long placeHolder8;
long placeHolder9;
long placeHoldera;
long placeHolderb;
long placeHolderc;
long placeHolderd;
long placeHoldere;
long placeHolderf;
public static void bar(boolean condition) {
Foo foo = new Foo();
if (condition) {
foo.hashCode();
}
}
public static void main(String[] args) {
for (int i = 0; i &lt; Integer.MAX_VALUE; i++) {
bar(i % 100 == 0);
}
}
}
</code></pre>
<p>[1] <a href="https://zh.wikipedia.org/wiki/逃逸分析">https://zh.wikipedia.org/wiki/ 逃逸分析</a>
[2] <a href="http://www.ssw.uni-linz.ac.at/Research/Papers/Stadler14/Stadler2014-CGO-PEA.pdf">http://www.ssw.uni-linz.ac.at/Research/Papers/Stadler14/Stadler2014-CGO-PEA.pdf</a></p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/深入拆解Java虚拟机/22 HotSpot虚拟机的intrinsic.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/深入拆解Java虚拟机/24 字段访问相关优化.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":"70997a0bab253cfa","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>