mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-26 05:06:42 +08:00
632 lines
44 KiB
HTML
632 lines
44 KiB
HTML
<!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>34 加餐4:分析定位Java问题,一定要用好这些工具(一).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 业务开发常见错误 100 例/00 开篇词 业务代码真的会有这么多坑?.md.html">00 开篇词 业务代码真的会有这么多坑?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/01 使用了并发工具类库,线程安全就高枕无忧了吗?.md.html">01 使用了并发工具类库,线程安全就高枕无忧了吗?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/02 代码加锁:不要让“锁”事成为烦心事.md.html">02 代码加锁:不要让“锁”事成为烦心事</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/03 线程池:业务代码最常用也最容易犯错的组件.md.html">03 线程池:业务代码最常用也最容易犯错的组件</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/04 连接池:别让连接池帮了倒忙.md.html">04 连接池:别让连接池帮了倒忙</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/05 HTTP调用:你考虑到超时、重试、并发了吗?.md.html">05 HTTP调用:你考虑到超时、重试、并发了吗?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/06 2成的业务代码的Spring声明式事务,可能都没处理正确.md.html">06 2成的业务代码的Spring声明式事务,可能都没处理正确</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/07 数据库索引:索引并不是万能药.md.html">07 数据库索引:索引并不是万能药</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/08 判等问题:程序里如何确定你就是你?.md.html">08 判等问题:程序里如何确定你就是你?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/09 数值计算:注意精度、舍入和溢出问题.md.html">09 数值计算:注意精度、舍入和溢出问题</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/10 集合类:坑满地的List列表操作.md.html">10 集合类:坑满地的List列表操作</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/11 空值处理:分不清楚的null和恼人的空指针.md.html">11 空值处理:分不清楚的null和恼人的空指针</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/12 异常处理:别让自己在出问题的时候变为瞎子.md.html">12 异常处理:别让自己在出问题的时候变为瞎子</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/13 日志:日志记录真没你想象的那么简单.md.html">13 日志:日志记录真没你想象的那么简单</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/14 文件IO:实现高效正确的文件读写并非易事.md.html">14 文件IO:实现高效正确的文件读写并非易事</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/15 序列化:一来一回你还是原来的你吗?.md.html">15 序列化:一来一回你还是原来的你吗?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/16 用好Java 8的日期时间类,少踩一些“老三样”的坑.md.html">16 用好Java 8的日期时间类,少踩一些“老三样”的坑</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/17 别以为“自动挡”就不可能出现OOM.md.html">17 别以为“自动挡”就不可能出现OOM</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/18 当反射、注解和泛型遇到OOP时,会有哪些坑?.md.html">18 当反射、注解和泛型遇到OOP时,会有哪些坑?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/19 Spring框架:IoC和AOP是扩展的核心.md.html">19 Spring框架:IoC和AOP是扩展的核心</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/20 Spring框架:框架帮我们做了很多工作也带来了复杂度.md.html">20 Spring框架:框架帮我们做了很多工作也带来了复杂度</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/21 代码重复:搞定代码重复的三个绝招.md.html">21 代码重复:搞定代码重复的三个绝招</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/22 接口设计:系统间对话的语言,一定要统一.md.html">22 接口设计:系统间对话的语言,一定要统一</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/23 缓存设计:缓存可以锦上添花也可以落井下石.md.html">23 缓存设计:缓存可以锦上添花也可以落井下石</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/24 业务代码写完,就意味着生产就绪了?.md.html">24 业务代码写完,就意味着生产就绪了?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/25 异步处理好用,但非常容易用错.md.html">25 异步处理好用,但非常容易用错</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/26 数据存储:NoSQL与RDBMS如何取长补短、相辅相成?.md.html">26 数据存储:NoSQL与RDBMS如何取长补短、相辅相成?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/27 数据源头:任何客户端的东西都不可信任.md.html">27 数据源头:任何客户端的东西都不可信任</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/28 安全兜底:涉及钱时,必须考虑防刷、限量和防重.md.html">28 安全兜底:涉及钱时,必须考虑防刷、限量和防重</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/29 数据和代码:数据就是数据,代码就是代码.md.html">29 数据和代码:数据就是数据,代码就是代码</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/30 如何正确保存和传输敏感数据?.md.html">30 如何正确保存和传输敏感数据?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/31 加餐1:带你吃透课程中Java 8的那些重要知识点(一).md.html">31 加餐1:带你吃透课程中Java 8的那些重要知识点(一)</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/32 加餐2:带你吃透课程中Java 8的那些重要知识点(二).md.html">32 加餐2:带你吃透课程中Java 8的那些重要知识点(二)</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/33 加餐3:定位应用问题,排错套路很重要.md.html">33 加餐3:定位应用问题,排错套路很重要</a>
|
||
</li>
|
||
<li>
|
||
<a class="current-tab" href="/专栏/Java 业务开发常见错误 100 例/34 加餐4:分析定位Java问题,一定要用好这些工具(一).md.html">34 加餐4:分析定位Java问题,一定要用好这些工具(一)</a>
|
||
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/35 加餐5:分析定位Java问题,一定要用好这些工具(二).md.html">35 加餐5:分析定位Java问题,一定要用好这些工具(二)</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/36 加餐6:这15年来,我是如何在工作中学习技术和英语的?.md.html">36 加餐6:这15年来,我是如何在工作中学习技术和英语的?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/37 加餐7:程序员成长28计.md.html">37 加餐7:程序员成长28计</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/38 加餐8:Java程序从虚拟机迁移到Kubernetes的一些坑.md.html">38 加餐8:Java程序从虚拟机迁移到Kubernetes的一些坑</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:代码篇思考题集锦(一).md.html">答疑篇:代码篇思考题集锦(一)</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:代码篇思考题集锦(三).md.html">答疑篇:代码篇思考题集锦(三)</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:代码篇思考题集锦(二).md.html">答疑篇:代码篇思考题集锦(二)</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:加餐篇思考题答案合集.md.html">答疑篇:加餐篇思考题答案合集</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:安全篇思考题答案合集.md.html">答疑篇:安全篇思考题答案合集</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:设计篇思考题答案合集.md.html">答疑篇:设计篇思考题答案合集</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/结束语 写代码时,如何才能尽量避免踩坑?.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>34 加餐4:分析定位Java问题,一定要用好这些工具(一)</h1>
|
||
<p>你好,我是朱晔。今天,我要和你分享的内容是分析定位 Java 问题常用的一些工具。</p>
|
||
<p>到这里,我们的课程更新 17 讲了,已经更新过半了。在学习过程中,你会发现我在介绍各种坑的时候,并不是直接给出问题的结论,而是通过工具来亲眼看到问题。</p>
|
||
<p>为什么这么做呢?因为我始终认为,遇到问题尽量不要去猜,一定要眼见为实。只有通过日志、监控或工具真正看到问题,然后再回到代码中进行比对确认,我们才能认为是找到了根本原因。</p>
|
||
<p>你可能一开始会比较畏惧使用复杂的工具去排查问题,又或者是打开了工具感觉无从下手,但是随着实践越来越多,对 Java 程序和各种框架的运作越来越熟悉,你会发现使用这些工具越来越顺手。</p>
|
||
<p>其实呢,工具只是我们定位问题的手段,要用好工具主要还是得对程序本身的运作有大概的认识,这需要长期的积累。</p>
|
||
<p>因此,我会通过两篇加餐,和你分享 4 个案例,分别展示使用 JDK 自带的工具来排查 JVM 参数配置问题、使用 Wireshark 来分析网络问题、通过 MAT 来分析内存问题,以及使用 Arthas 来分析 CPU 使用高的问题。这些案例也只是冰山一角,你可以自己再通过些例子进一步学习和探索。</p>
|
||
<p>在今天这篇加餐中,我们就先学习下如何使用 JDK 自带工具、Wireshark 来分析和定位 Java 程序的问题吧。</p>
|
||
<h2>使用 JDK 自带工具查看 JVM 情况</h2>
|
||
<p>JDK 自带了很多命令行甚至是图形界面工具,帮助我们查看 JVM 的一些信息。比如,在我的机器上运行 ls 命令,可以看到 JDK 8 提供了非常多的工具或程序:</p>
|
||
<p><img src="assets/22456d9186a4f36f83209168b782dbbd.png" alt="img" /></p>
|
||
<p>接下来,我会与你介绍些常用的监控工具。你也可以先通过下面这张图了解下各种工具的基本作用:</p>
|
||
<p><img src="assets/b4e8ab0a76a8665879e0fc13964ebc0d.jpg" alt="img" /></p>
|
||
<p>为了测试这些工具,我们先来写一段代码:启动 10 个死循环的线程,每个线程分配一个 10MB 左右的字符串,然后休眠 10 秒。可以想象到,这个程序会对 GC 造成压力。</p>
|
||
<pre><code>//启动10个线程
|
||
IntStream.rangeClosed(1, 10).mapToObj(i -> new Thread(() -> {
|
||
while (true) {
|
||
//每一个线程都是一个死循环,休眠10秒,打印10M数据
|
||
String payload = IntStream.rangeClosed(1, 10000000)
|
||
.mapToObj(__ -> "a")
|
||
.collect(Collectors.joining("")) + UUID.randomUUID().toString();
|
||
try {
|
||
TimeUnit.SECONDS.sleep(10);
|
||
} catch (InterruptedException e) {
|
||
e.printStackTrace();
|
||
}
|
||
System.out.println(payload.length());
|
||
}
|
||
})).forEach(Thread::start);
|
||
|
||
TimeUnit.HOURS.sleep(1);
|
||
</code></pre>
|
||
<p>修改 pom.xml,配置 spring-boot-maven-plugin 插件打包的 Java 程序的 main 方法类:</p>
|
||
<pre><code><plugin>
|
||
<groupId>org.springframework.boot</groupId>
|
||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||
<configuration>
|
||
<mainClass>org.geekbang.time.commonmistakes.troubleshootingtools.jdktool.CommonMistakesApplication
|
||
</mainClass>
|
||
</configuration>
|
||
</plugin>
|
||
</code></pre>
|
||
<p>然后使用 java -jar 启动进程,设置 JVM 参数,让堆最小最大都是 1GB:</p>
|
||
<pre><code>java -jar common-mistakes-0.0.1-SNAPSHOT.jar -Xms1g -Xmx1g
|
||
</code></pre>
|
||
<p>完成这些准备工作后,我们就可以使用 JDK 提供的工具,来观察分析这个测试程序了。</p>
|
||
<h3>jps</h3>
|
||
<p>首先,使用 jps 得到 Java 进程列表,这会比使用 ps 来的方便:</p>
|
||
<pre><code>➜ ~ jps
|
||
12707
|
||
22261 Launcher
|
||
23864 common-mistakes-0.0.1-SNAPSHOT.jar
|
||
15608 RemoteMavenServer36
|
||
23243 Main
|
||
23868 Jps
|
||
22893 KotlinCompileDaemon
|
||
</code></pre>
|
||
<h3>jinfo</h3>
|
||
<p>然后,可以使用 jinfo 打印 JVM 的各种参数:</p>
|
||
<pre><code>➜ ~ jinfo 23864
|
||
Java System Properties:
|
||
#Wed Jan 29 12:49:47 CST 2020
|
||
...
|
||
user.name=zhuye
|
||
path.separator=\:
|
||
os.version=10.15.2
|
||
java.runtime.name=Java(TM) SE Runtime Environment
|
||
file.encoding=UTF-8
|
||
java.vm.name=Java HotSpot(TM) 64-Bit Server VM
|
||
...
|
||
|
||
VM Flags:
|
||
-XX:CICompilerCount=4 -XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=268435456 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=2576351232 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=5835340 -XX:NonProfiledCodeHeapSize=122911450 -XX:ProfiledCodeHeapSize=122911450 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
|
||
|
||
VM Arguments:
|
||
java_command: common-mistakes-0.0.1-SNAPSHOT.jar -Xms1g -Xmx1g
|
||
java_class_path (initial): common-mistakes-0.0.1-SNAPSHOT.jar
|
||
Launcher Type: SUN_STANDARD
|
||
</code></pre>
|
||
<p>查看第 15 行和 19 行可以发现,我们设置 JVM 参数的方式不对,-Xms1g 和 -Xmx1g 这两个参数被当成了 Java 程序的启动参数,整个 JVM 目前最大内存是 4GB 左右,而不是 1GB。</p>
|
||
<p>因此,当我们怀疑 JVM 的配置很不正常的时候,要第一时间使用工具来确认参数。除了使用工具确认 JVM 参数外,你也可以打印 VM 参数和程序参数:</p>
|
||
<pre><code>System.out.println("VM options");
|
||
System.out.println(ManagementFactory.getRuntimeMXBean().getInputArguments().stream().collect(Collectors.joining(System.lineSeparator())));
|
||
System.out.println("Program arguments");
|
||
System.out.println(Arrays.stream(args).collect(Collectors.joining(System.lineSeparator())));
|
||
</code></pre>
|
||
<p>把 JVM 参数放到 -jar 之前,重新启动程序,可以看到如下输出,从输出也可以确认这次 JVM 参数的配置正确了:</p>
|
||
<pre><code>➜ target git:(master) ✗ java -Xms1g -Xmx1g -jar common-mistakes-0.0.1-SNAPSHOT.jar test
|
||
VM options
|
||
-Xms1g
|
||
-Xmx1g
|
||
Program arguments
|
||
test
|
||
</code></pre>
|
||
<h3>jvisualvm</h3>
|
||
<p>然后,启动另一个重量级工具 jvisualvm 观察一下程序,可以在概述面板再次确认 JVM 参数设置成功了:</p>
|
||
<p><img src="assets/4d8a600072b0b1aea3943dee584c72e4.png" alt="img" /></p>
|
||
<p>继续观察监视面板可以看到,JVM 的 GC 活动基本是 10 秒发生一次,堆内存在 250MB 到 900MB 之间波动,活动线程数是 22。我们可以在监视面板看到 JVM 的基本情况,也可以直接在这里进行手动 GC 和堆 Dump 操作:</p>
|
||
<p><img src="assets/5be531e51f6e49d5511d419c90b29302.png" alt="img" /></p>
|
||
<h3>jconsole</h3>
|
||
<p>如果希望看到各个内存区的 GC 曲线图,可以使用 jconsole 观察。jconsole 也是一个综合性图形界面监控工具,比 jvisualvm 更方便的一点是,可以用曲线的形式监控各种数据,包括 MBean 中的属性值:</p>
|
||
<p><img src="assets/6b4c08d384eea532842d386638dddb12.png" alt="img" /></p>
|
||
<h3>jstat</h3>
|
||
<p>同样,如果没有条件使用图形界面(毕竟在 Linux 服务器上,我们主要使用命令行工具),又希望看到 GC 趋势的话,我们可以使用 jstat 工具。</p>
|
||
<p>jstat 工具允许以固定的监控频次输出 JVM 的各种监控指标,比如使用 -gcutil 输出 GC 和内存占用汇总信息,每隔 5 秒输出一次,输出 100 次,可以看到 Young GC 比较频繁,而 Full GC 基本 10 秒一次:</p>
|
||
<pre><code>➜ ~ jstat -gcutil 23940 5000 100
|
||
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
|
||
0.00 100.00 0.36 87.63 94.30 81.06 539 14.021 33 3.972 837 0.976 18.968
|
||
0.00 100.00 0.60 69.51 94.30 81.06 540 14.029 33 3.972 839 0.978 18.979
|
||
0.00 0.00 0.50 99.81 94.27 81.03 548 14.143 34 4.002 840 0.981 19.126
|
||
0.00 100.00 0.59 70.47 94.27 81.03 549 14.177 34 4.002 844 0.985 19.164
|
||
0.00 100.00 0.57 99.85 94.32 81.09 550 14.204 34 4.002 845 0.990 19.196
|
||
0.00 100.00 0.65 77.69 94.32 81.09 559 14.469 36 4.198 847 0.993 19.659
|
||
0.00 100.00 0.65 77.69 94.32 81.09 559 14.469 36 4.198 847 0.993 19.659
|
||
0.00 100.00 0.70 35.54 94.32 81.09 567 14.763 37 4.378 853 1.001 20.142
|
||
0.00 100.00 0.70 41.22 94.32 81.09 567 14.763 37 4.378 853 1.001 20.142
|
||
0.00 100.00 1.89 96.76 94.32 81.09 574 14.943 38 4.487 859 1.007 20.438
|
||
0.00 100.00 1.39 39.20 94.32 81.09 575 14.946 38 4.487 861 1.010 20.442
|
||
</code></pre>
|
||
<p>其中,S0 表示 Survivor0 区占用百分比,S1 表示 Survivor1 区占用百分比,E 表示 Eden 区占用百分比,O 表示老年代占用百分比,M 表示元数据区占用百分比,YGC 表示年轻代回收次数,YGCT 表示年轻代回收耗时,FGC 表示老年代回收次数,FGCT 表示老年代回收耗时。</p>
|
||
<p>jstat 命令的参数众多,包含 -class、-compiler、-gc 等。Java 8、Linux/Unix 平台 jstat 工具的完整介绍,你可以查看这里。jstat 定时输出的特性,可以方便我们持续观察程序的各项指标。</p>
|
||
<p>继续来到线程面板可以看到,大量以 Thread 开头的线程基本都是有节奏的 10 秒运行一下,其他时间都在休眠,和我们的代码逻辑匹配:</p>
|
||
<p><img src="assets/7a1616295b4ec51c56437d2a92652185.png" alt="img" /></p>
|
||
<p>点击面板的线程 Dump 按钮,可以查看线程瞬时的线程栈:</p>
|
||
<p><img src="assets/0ddcd3348d1c8b0bba16736f9221a900.png" alt="img" /></p>
|
||
<h3>jstack</h3>
|
||
<p>通过命令行工具 jstack,也可以实现抓取线程栈的操作:</p>
|
||
<pre><code>➜ ~ jstack 23940
|
||
2020-01-29 13:08:15
|
||
Full thread dump Java HotSpot(TM) 64-Bit Server VM (11.0.3+12-LTS mixed mode):
|
||
...
|
||
"main" #1 prio=5 os_prio=31 cpu=440.66ms elapsed=574.86s tid=0x00007ffdd9800000 nid=0x2803 waiting on condition [0x0000700003849000]
|
||
java.lang.Thread.State: TIMED_WAITING (sleeping)
|
||
at java.lang.Thread.sleep(<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="066c677067286467756346373728362835">[email protected]</a>/Native Method)
|
||
at java.lang.Thread.sleep(<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="7913180f18571b180a1c3948485749574a">[email protected]</a>/Thread.java:339)
|
||
at java.util.concurrent.TimeUnit.sleep(<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="93f9f2e5f2bdf1f2e0f6d3a2a2bda3bda0">[email protected]</a>/TimeUnit.java:446)
|
||
at org.geekbang.time.commonmistakes.troubleshootingtools.jdktool.CommonMistakesApplication.main(CommonMistakesApplication.java:41)
|
||
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="b6dcd7c0d798d4d7c5d3f6878798869885">[email protected]</a>/Native Method)
|
||
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="f399928592dd91928096b3c2c2ddc3ddc0">[email protected]</a>/NativeMethodAccessorImpl.java:62)
|
||
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="ea808b9c8bc4888b998faadbdbc4dac4d9">[email protected]</a>/DelegatingMethodAccessorImpl.java:43)
|
||
at java.lang.reflect.Method.invoke(<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="3d575c4b5c135f5c4e587d0c0c130d130e">[email protected]</a>/Method.java:566)
|
||
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48)
|
||
at org.springframework.boot.loader.Launcher.launch(Launcher.java:87)
|
||
at org.springframework.boot.loader.Launcher.launch(Launcher.java:51)
|
||
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:52)
|
||
"Thread-1" #13 prio=5 os_prio=31 cpu=17851.77ms elapsed=574.41s tid=0x00007ffdda029000 nid=0x9803 waiting on condition [0x000070000539d000]
|
||
java.lang.Thread.State: TIMED_WAITING (sleeping)
|
||
at java.lang.Thread.sleep(<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="c3a9a2b5a2eda1a2b0a683f2f2edf3edf0">[email protected]</a>/Native Method)
|
||
at java.lang.Thread.sleep(<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="4f252e392e612d2e3c2a0f7e7e617f617c">[email protected]</a>/Thread.java:339)
|
||
at java.util.concurrent.TimeUnit.sleep(<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="1973786f78377b786a7c5928283729372a">[email protected]</a>/TimeUnit.java:446)
|
||
at org.geekbang.time.commonmistakes.troubleshootingtools.jdktool.CommonMistakesApplication.lambda$null$1(CommonMistakesApplication.java:33)
|
||
at org.geekbang.time.commonmistakes.troubleshootingtools.jdktool.CommonMistakesApplication$$Lambda$41/0x00000008000a8c40.run(Unknown Source)
|
||
at java.lang.Thread.run(<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="375d564156195556445277060619071904">[email protected]</a>/Thread.java:834)
|
||
|
||
...
|
||
</code></pre>
|
||
<p>抓取后可以使用类似fastthread这样的在线分析工具来分析线程栈。</p>
|
||
<h3>jcmd</h3>
|
||
<p>最后,我们来看一下 Java HotSpot 虚拟机的 NMT 功能。</p>
|
||
<p>通过 NMT,我们可以观察细粒度内存使用情况,设置 -XX:NativeMemoryTracking=summary/detail 可以开启 NMT 功能,开启后可以使用 jcmd 工具查看 NMT 数据。</p>
|
||
<p>我们重新启动一次程序,这次加上 JVM 参数以 detail 方式开启 NMT:</p>
|
||
<pre><code>-Xms1g -Xmx1g -XX:ThreadStackSize=256k -XX:NativeMemoryTracking=detail
|
||
</code></pre>
|
||
<p>在这里,我们还增加了 -XX:ThreadStackSize 参数,并将其值设置为 256k,也就是期望把线程栈设置为 256KB。我们通过 NMT 观察一下设置是否成功。</p>
|
||
<p>启动程序后执行如下 jcmd 命令,以概要形式输出 NMT 结果。可以看到,当前有 32 个线程,线程栈总共保留了差不多 4GB 左右的内存。我们明明配置线程栈最大 256KB 啊,为什么会出现 4GB 这么夸张的数字呢,到底哪里出了问题呢?</p>
|
||
<pre><code>➜ ~ jcmd 24404 VM.native_memory summary
|
||
24404:
|
||
|
||
Native Memory Tracking:
|
||
|
||
Total: reserved=6635310KB, committed=5337110KB
|
||
\- Java Heap (reserved=1048576KB, committed=1048576KB)
|
||
(mmap: reserved=1048576KB, committed=1048576KB)
|
||
|
||
\- Class (reserved=1066233KB, committed=15097KB)
|
||
(classes #902)
|
||
(malloc=9465KB #908)
|
||
(mmap: reserved=1056768KB, committed=5632KB)
|
||
|
||
\- Thread (reserved=4209797KB, committed=4209797KB)
|
||
(thread #32)
|
||
(stack: reserved=4209664KB, committed=4209664KB)
|
||
(malloc=96KB #165)
|
||
(arena=37KB #59)
|
||
|
||
\- Code (reserved=249823KB, committed=2759KB)
|
||
(malloc=223KB #730)
|
||
(mmap: reserved=249600KB, committed=2536KB)
|
||
|
||
\- GC (reserved=48700KB, committed=48700KB)
|
||
(malloc=10384KB #135)
|
||
(mmap: reserved=38316KB, committed=38316KB)
|
||
|
||
\- Compiler (reserved=186KB, committed=186KB)
|
||
(malloc=56KB #105)
|
||
(arena=131KB #7)
|
||
|
||
\- Internal (reserved=9693KB, committed=9693KB)
|
||
(malloc=9661KB #2585)
|
||
(mmap: reserved=32KB, committed=32KB)
|
||
|
||
\- Symbol (reserved=2021KB, committed=2021KB)
|
||
(malloc=1182KB #334)
|
||
(arena=839KB #1)
|
||
|
||
\- Native Memory Tracking (reserved=85KB, committed=85KB)
|
||
(malloc=5KB #53)
|
||
(tracking overhead=80KB)
|
||
|
||
\- Arena Chunk (reserved=196KB, committed=196KB)
|
||
(malloc=196KB)
|
||
</code></pre>
|
||
<p>重新以 VM.native_memory detail 参数运行 jcmd:</p>
|
||
<pre><code>jcmd 24404 VM.native_memory detail
|
||
</code></pre>
|
||
<p>可以看到,有 16 个可疑线程,每一个线程保留了 262144KB 内存,也就是 256MB(通过下图红框可以看到,使用关键字 262144KB for Thread Stack from 搜索到了 16 个结果):</p>
|
||
<p><img src="assets/f24869cbd1190c508e085c9f3400d06b.png" alt="img" /></p>
|
||
<p>其实,ThreadStackSize 参数的单位是 KB,所以我们如果要设置线程栈 256KB,那么应该设置 256 而不是 256k。重新设置正确的参数后,使用 jcmd 再次验证下:</p>
|
||
<p><img src="assets/d7228ec216003d31064698e7e16c81c9.png" alt="img" /></p>
|
||
<p>除了用于查看 NMT 外,jcmd 还有许多功能。我们可以通过 help,看到它的所有功能:</p>
|
||
<pre><code>jcmd 24781 help
|
||
</code></pre>
|
||
<p>对于其中每一种功能,我们都可以进一步使用 help 来查看介绍。比如,使用 GC.heap_info 命令可以打印 Java 堆的一些信息:</p>
|
||
<pre><code>jcmd 24781 help GC.heap_info
|
||
</code></pre>
|
||
<p>除了 jps、jinfo、jcmd、jstack、jstat、jconsole、jvisualvm 外,JDK 中还有一些工具,你可以通过官方文档查看完整介绍。</p>
|
||
<h2>使用 Wireshark 分析 SQL 批量插入慢的问题</h2>
|
||
<p>我之前遇到过这样一个案例:有一个数据导入程序需要导入大量的数据,开发同学就想到了使用 Spring JdbcTemplate 的批量操作功能进行数据批量导入,但是发现性能非常差,和普通的单条 SQL 执行性能差不多。</p>
|
||
<p>我们重现下这个案例。启动程序后,首先创建一个 testuser 表,其中只有一列 name,然后使用 JdbcTemplate 的 batchUpdate 方法,批量插入 10000 条记录到 testuser 表:</p>
|
||
<pre><code>@SpringBootApplication
|
||
@Slf4j
|
||
public class BatchInsertAppliation implements CommandLineRunner {
|
||
@Autowired
|
||
private JdbcTemplate jdbcTemplate;
|
||
public static void main(String[] args) {
|
||
SpringApplication.run(BatchInsertApplication.class, args);
|
||
}
|
||
@PostConstruct
|
||
public void init() {
|
||
//初始化表
|
||
jdbcTemplate.execute("drop table IF EXISTS `testuser`;");
|
||
jdbcTemplate.execute("create TABLE `testuser` (\n" +
|
||
" `id` bigint(20) NOT NULL AUTO_INCREMENT,\n" +
|
||
" `name` varchar(255) NOT NULL,\n" +
|
||
" PRIMARY KEY (`id`)\n" +
|
||
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||
}
|
||
@Override
|
||
public void run(String... args) {
|
||
long begin = System.currentTimeMillis();
|
||
String sql = "INSERT INTO `testuser` (`name`) VALUES (?)";
|
||
//使用JDBC批量更新
|
||
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
|
||
@Override
|
||
public void setValues(PreparedStatement preparedStatement, int i) throws SQLException {
|
||
//第一个参数(索引从1开始),也就是name列赋值
|
||
preparedStatement.setString(1, "usera" + i);
|
||
}
|
||
@Override
|
||
public int getBatchSize() {
|
||
//批次大小为10000
|
||
return 10000;
|
||
}
|
||
});
|
||
log.info("took : {} ms", System.currentTimeMillis() - begin);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>执行程序后可以看到,1 万条数据插入耗时 26 秒:</p>
|
||
<pre><code>[14:44:19.094] [main] [INFO ] [o.g.t.c.t.network.BatchInsertApplication:52 ] - took : 26144 ms
|
||
</code></pre>
|
||
<p>其实,对于批量操作,我们希望程序可以把多条 insert SQL 语句合并成一条,或至少是一次性提交多条语句到数据库,以减少和 MySQL 交互次数、提高性能。那么,我们的程序是这样运作的吗?</p>
|
||
<p>我在加餐 3中提到一条原则,“分析问题一定是需要依据的,靠猜是猜不出来的”。现在,我们就使用网络分析工具 Wireshark 来分析一下这个案例,眼见为实。</p>
|
||
<p>首先,我们可以在这里下载 Wireshark,启动后选择某个需要捕获的网卡。由于我们连接的是本地的 MySQL,因此选择 loopback 回环网卡:</p>
|
||
<p><img src="assets/d7c3cc2d997990d0c4b94f72f1679c9b.png" alt="img" /></p>
|
||
<p>然后,Wireshark 捕捉这个网卡的所有网络流量。我们可以在上方的显示过滤栏输入 tcp.port == 6657,来过滤出所有 6657 端口的 TCP 请求(因为我们是通过 6657 端口连接 MySQL 的)。</p>
|
||
<p>可以看到,程序运行期间和 MySQL 有大量交互。因为 Wireshark 直接把 TCP 数据包解析为了 MySQL 协议,所以下方窗口可以直接显示 MySQL 请求的 SQL 查询语句。我们看到,testuser 表的每次 insert 操作,插入的都是一行记录:</p>
|
||
<p><img src="assets/bcb987cab3cccf4d8729cfe44f01a2a2.png" alt="img" /></p>
|
||
<p>如果列表中的 Protocol 没有显示 MySQL 的话,你可以手动点击 Analyze 菜单的 Decode As 菜单,然后加一条规则,把 6657 端口设置为 MySQL 协议:</p>
|
||
<p><img src="assets/6ae982e2013cf1c60300332068b58cf2.png" alt="img" /></p>
|
||
<p>这就说明,我们的程序并不是在做批量插入操作,和普通的单条循环插入没有区别。调试程序进入 ClientPreparedStatement 类,可以看到执行批量操作的是 executeBatchInternal 方法。executeBatchInternal 方法的源码如下:</p>
|
||
<pre><code>@Override
|
||
protected long[] executeBatchInternal() throws SQLException {
|
||
synchronized (checkClosed().getConnectionMutex()) {
|
||
if (this.connection.isReadOnly()) {
|
||
throw new SQLException(Messages.getString("PreparedStatement.25") + Messages.getString("PreparedStatement.26"),
|
||
MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT);
|
||
}
|
||
if (this.query.getBatchedArgs() == null || this.query.getBatchedArgs().size() == 0) {
|
||
return new long[0];
|
||
}
|
||
// we timeout the entire batch, not individual statements
|
||
int batchTimeout = getTimeoutInMillis();
|
||
setTimeoutInMillis(0);
|
||
resetCancelledState();
|
||
try {
|
||
statementBegins();
|
||
clearWarnings();
|
||
if (!this.batchHasPlainStatements && this.rewriteBatchedStatements.getValue()) {
|
||
if (((PreparedQuery<?>) this.query).getParseInfo().canRewriteAsMultiValueInsertAtSqlLevel()) {
|
||
return executeBatchedInserts(batchTimeout);
|
||
}
|
||
if (!this.batchHasPlainStatements && this.query.getBatchedArgs() != null
|
||
&& this.query.getBatchedArgs().size() > 3 /* cost of option setting rt-wise */) {
|
||
return executePreparedBatchAsMultiStatement(batchTimeout);
|
||
}
|
||
}
|
||
return executeBatchSerially(batchTimeout);
|
||
} finally {
|
||
this.query.getStatementExecuting().set(false);
|
||
clearBatch();
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>注意第 18 行,判断了 rewriteBatchedStatements 参数是否为 true,是才会开启批量的优化。优化方式有 2 种:</p>
|
||
<p>如果有条件的话,优先把 insert 语句优化为一条语句,也就是 executeBatchedInserts 方法;</p>
|
||
<p>如果不行的话,再尝试把 insert 语句优化为多条语句一起提交,也就是 executePreparedBatchAsMultiStatement 方法。</p>
|
||
<p>到这里就明朗了,实现批量提交优化的关键,在于 rewriteBatchedStatements 参数。我们修改连接字符串,并将其值设置为 true:</p>
|
||
<pre><code>spring.datasource.url=jdbc:mysql://localhost:6657/common_mistakes?characterEncoding=UTF-8&useSSL=false&rewriteBatchedStatements=true
|
||
</code></pre>
|
||
<p>重新按照之前的步骤打开 Wireshark 验证,可以看到:</p>
|
||
<p>这次 insert SQL 语句被拼接成了一条语句(如第二个红框所示);</p>
|
||
<p>这个 TCP 包因为太大被分割成了 11 个片段传输,#699 请求是最后一个片段,其实际内容是 insert 语句的最后一部分内容(如第一和第三个红框显示)。</p>
|
||
<p><img src="assets/3b7406c96a90e454a00e3c8ba82ecfbc.png" alt="img" /></p>
|
||
<p>为了查看整个 TCP 连接的所有数据包,你可以在请求上点击右键,选择 Follow->TCP Stream:</p>
|
||
<p><img src="assets/5b18a8c6c227df50ad493f5aa546f9c2.png" alt="img" /></p>
|
||
<p>打开后可以看到,从 MySQL 认证开始到 insert 语句的所有数据包的内容:</p>
|
||
<p><img src="assets/e154da637a2b44a65f9257beb842575a.png" alt="img" /></p>
|
||
<p>查看最开始的握手数据包可以发现,TCP 的最大分段大小(MSS)是 16344 字节,而我们的 MySQL 超长 insert 的数据一共 138933 字节,因此被分成了 11 段传输,其中最大的一段是 16332 字节,低于 MSS 要求的 16344 字节。</p>
|
||
<p><img src="assets/3e66a004fd4b7dba14047751a57e089e.png" alt="img" /></p>
|
||
<p>最后可以看到插入 1 万条数据仅耗时 253 毫秒,性能提升了 100 倍:</p>
|
||
<pre><code>[20:19:30.185] [main] [INFO ] [o.g.t.c.t.network.BatchInsertApplication:52 ] - took : 253 ms
|
||
</code></pre>
|
||
<p>虽然我们一直在使用 MySQL,但我们很少会考虑 MySQL Connector Java 是怎么和 MySQL 交互的,实际发送给 MySQL 的 SQL 语句又是怎样的。有没有感觉到,MySQL 协议其实并不遥远,我们完全可以使用 Wireshark 来观察、分析应用程序与 MySQL 交互的整个流程。</p>
|
||
<h2>重点回顾</h2>
|
||
<p>今天,我就使用 JDK 自带工具查看 JVM 情况、使用 Wireshark 分析 SQL 批量插入慢的问题,和你展示了一些工具及其用法。</p>
|
||
<p>首先,JDK 自带的一些监控和故障诊断工具中,有命令行工具也有图形工具。其中,命令行工具更适合在服务器上使用,图形界面工具用于本地观察数据更直观。为了帮助你用好这些工具,我们带你使用这些工具,分析了程序错误设置 JVM 参数的两个问题,并且观察了 GC 工作的情况。</p>
|
||
<p>然后,我们使用 Wireshark 分析了 MySQL 批量 insert 操作慢的问题。我们看到,通过 Wireshark 分析网络包可以让一切变得如此透明。因此,学好 Wireshark,对我们排查 C/S 网络程序的 Bug 或性能问题,会有非常大的帮助。</p>
|
||
<p>比如,遇到诸如 Connection reset、Broken pipe 等网络问题的时候,你可以利用 Wireshark 来定位问题,观察客户端和服务端之间到底出了什么问题。</p>
|
||
<p>此外,如果你需要开发网络程序的话,Wireshark 更是分析协议、确认程序是否正确实现的必备工具。</p>
|
||
<p>今天用到的代码,我都放在了 GitHub 上,你可以点击这个链接查看。</p>
|
||
<h2>思考与讨论</h2>
|
||
<p>JDK 中还有一个 jmap 工具,我们会使用 jmap -dump 命令来进行堆转储。那么,这条命令和 jmap -dump:live 有什么区别呢?你能否设计一个实验,来证明下它们的区别呢?</p>
|
||
<p>你有没有想过,客户端是如何和 MySQL 进行认证的呢?你能否对照MySQL 的文档,使用 Wireshark 观察分析这一过程呢?</p>
|
||
<p>在平时工作中,你还会使用什么工具来分析排查 Java 应用程序的问题呢?我是朱晔,欢迎在评论区与我留言分享你的想法,也欢迎你把今天的内容分享给你的朋友或同事,一起交流。</p>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="float: left">
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/33 加餐3:定位应用问题,排错套路很重要.md.html">上一页</a>
|
||
</div>
|
||
<div style="float: right">
|
||
<a href="/专栏/Java 业务开发常见错误 100 例/35 加餐5:分析定位Java问题,一定要用好这些工具(二).md.html">下一页</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
</div>
|
||
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997061ea643d60","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>
|