目录

1. 概述篇

1.1. 背景说明

1.3. 调优概述

1.4. 性能优化的步骤

2. JVM 监控及诊断工具-命令行篇

2.1. 概述

2.2. jps:查看正在运行的 Java 进程

2.3. jstat:查看 JVM 统计信息

2.3.1 使用方法

2.3.2 实战 如何预估JVM运行情况

2.3.3系统频繁Full GC导致系统卡顿怎么处理

2.4. jinfo:实时查看和修改 JVM 配置参数

2.5. jmap:导出内存映像文件&内存使用情况

2.6. jhat:JDK 自带堆分析工具

2.7. jstack:打印 JVM 中线程快照

2.7.1 实战1:找到死锁的线程

2.7.2 实战2:CPU突然飙高,该如何定位代码

2.8. jcmd:多功能命令行

3. JVM 监控及诊断工具-GUI 篇

3.1. 工具概述

3.2. JConsole

3.3. Visual VM

3.4. Eclipse MAT

3.5. JProfiler

3.6. Arthas

3.7. Java Misssion Control

3.8. 其他工具

4. JVM 运行时参数

4.1. JVM 参数选项

4.1.1. 类型一:标准参数选项

4.1.2. 类型二:-X 参数选项

4.1.3. 类型三:-XX 参数选项

4.2. 添加 JVM 参数选项

4.3. 常用的 JVM 参数选项

4.3.1. 打印设置的 XX 选项及值

4.3.2. 堆、栈、方法区等内存大小设置

4.3.3. OutOfMemory 相关的选项

4.3.4. 垃圾收集器相关选项

4.3.5. GC 日志相关选项

4.3.6. 其他参数

4.4. 通过 Java 代码获取 JVM 参数

5. 分析 GC 日志

5.1. GC 分类

5.2. GC 日志分类


1. 背景说明

我们学习JVM的最终目的都是为了进行JVM调优,这既是工作的需要,也是很多面试官资环考察的问题,例如生产环境中的问题有:

  • 生产环境发生了内存溢出该如何处理?

  • 生产环境应该给服务器分配多少内存合适?

  • 如何对垃圾回收器的性能进行调优?

  • 生产环境 CPU 负载飙高该如何处理?

  • 生产环境应该给应用分配多少线程合适?

  • 不加 log,如何确定请求是否执行了某一行代码?

  • 不加 log,如何实时查看某个方法的入参与返回值?

2. JVM 监控及诊断工具-命令行篇

2.1. 概述

性能诊断是软件工程师在日常工作中需要经常面对和解决的问题,在用户体验至上的今天,解决好应用的性能问题能带来非常大的收益。

Java 作为最流行的编程语言之一,其应用性能诊断一直受到业界广泛关注。可能造成 Java 应用出现性能问题的因素非常多,例如线程控制、磁盘读写、数据库访问、网络 I/O、垃圾收集等。想要定位这些问题,一款优秀的性能诊断工具必不可少。

体会 1:使用数据说明问题,使用知识分析问题,使用工具处理问题。

体会 2:无监控、不调优!

简单命令行工具

在我们刚接触 java 学习的时候,大家肯定最先了解的两个命令就是 javac,java,那么除此之外,还有没有其他的命令可以供我们使用呢?

我们进入到安装 jdk 的 bin 目录,发现还有一系列辅助工具。这些辅助工具用来获取目标 JVM 不同方面、不同层次的信息,帮助开发人员很好地解决 Java 应用程序的一些疑难杂症。

官方源码地址:jdk/jdk11: 1ddf9a99e4ad /src/jdk.jcmd/share/classes/sun/tools/

2.2. jps:查看正在运行的 Java 进程

jps(Java Process Status):显示指定系统内所有的 HotSpot 虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程。

说明:对于本地虚拟机进程来说,进程的本地虚拟机 ID 与操作系统的进程 ID 是一致的,是唯一的。

基本使用语法为:jps [options] [hostid]

我们还可以通过追加参数,来打印额外的信息。

options 参数

  • -q:仅仅显示 LVMID(local virtual machine id),即本地虚拟机唯一 id。不显示主类的名称等

  • -l:输出应用程序主类的全类名 或 如果进程执行的是 jar 包,则输出 jar 完整路径

  • -m:输出虚拟机进程启动时传递给主类 main()的参数

  • -v:列出虚拟机进程启动时的 JVM 参数。比如:-Xms20m -Xmx50m 是启动程序指定的 jvm 参数。

说明:以上参数可以综合使用。

补充:如果某 Java 进程关闭了默认开启的 UsePerfData 参数(即使用参数-XX:-UsePerfData),那么 jps 命令(以及下面介绍的 jstat)将无法探知该 Java 进程。

hostid 参数

RMI 注册表中注册的主机名。如果想要远程监控主机上的 java 程序,需要安装 jstatd。

对于具有更严格的安全实践的网络场所而言,可能使用一个自定义的策略文件来显示对特定的可信主机或网络的访问,尽管这种技术容易受到 IP 地址欺诈攻击。

如果安全问题无法使用一个定制的策略文件来处理,那么最安全的操作是不运行 jstatd 服务器,而是在本地使用 jstat 和 jps 工具。

2.3. jstat:查看 JVM 统计信息

jstat(JVM Statistics Monitoring Tool):用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据。在没有 GUI 图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。常用于检测垃圾回收问题以及内存泄漏问题。

2.3.1 使用方法

官方文档:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html

基本使用语法为:jstat -option [-t] [-hlines] vmid [interval [count]]

查看命令相关参数:jstat-h 或 jstat-help

其中 vmid 是进程 id 号,也就是 jps 之后看到的前面的号码,如下:

例如,在本人公司某个测试机下的进程如下所示:

option 参数

选项 option 可以由以下值构成。

类装载相关的:

  • -class:显示 ClassLoader 的相关信息:类的装载、卸载数量、总空间、类装载所消耗的时间等

垃圾回收相关的:

  • -gc:显示与 GC 相关的堆信息。包括 Eden 区、两个 Survivor 区、老年代、永久代等的容量、已用空间、GC 时间合计等信息。

  • -gccapacity:显示内容与-gc 基本相同,但输出主要关注 Java 堆各个区域使用到的最大、最小空间。

  • -gcutil:显示内容与-gc 基本相同,但输出主要关注已使用空间占总空间的百分比。

  • -gccause:与-gcutil 功能一样,但是会额外输出导致最后一次或当前正在发生的 GC 产生的原因。

  • -gcnew:显示新生代 GC 状况

  • -gcnewcapacity:显示内容与-gcnew 基本相同,输出主要关注使用到的最大、最小空间

  • -geold:显示老年代 GC 状况

  • -gcoldcapacity:显示内容与-gcold 基本相同,输出主要关注使用到的最大、最小空间

  • -gcpermcapacity:显示永久代使用到的最大、最小空间。

JIT 相关的:

  • -compiler:显示 JIT 编译器编译过的方法、耗时等信息

  • -printcompilation:输出已经被 JIT 编译的方法

每个字段的具体含义如下:

表头 含义(字节)
EC Eden 区的大小
EU Eden 区已使用的大小
S0C 幸存者 0 区的大小
S1C 幸存者 1 区的大小
S0U 幸存者 0 区已使用的大小
S1U 幸存者 1 区已使用的大小
MC 元空间的大小
MU 元空间已使用的大小
OC 老年代的大小
OU 老年代已使用的大小
CCSC 压缩类空间的大小
CCSU 压缩类空间已使用的大小
YGC 从应用程序启动到采样时 young gc 的次数
YGCT 从应用程序启动到采样时 young gc 消耗时间(秒)
FGC 从应用程序启动到采样时 full gc 的次数
FGCT 从应用程序启动到采样时的 full gc 的消耗时间(秒)
GCT 从应用程序启动到采样时 gc 的总时间

除此之外,我们还可以更细致的查看各个区域的状态。

堆内存统计

jstat -gccapacity 50062

具体含义是:

  • N GC MN:新生代最小容量

  • N GC MX:新生代最大容量

  • NGC:当前新生代容量

  • S0C:第一个幸存区大小

  • S1C:第二个幸存区的大小

  • EC:伊甸园区的大小

  • O GC MN:老年代最小容量

  • OGC MX:老年代最大容量

  • OGC:当前老年代大小

  • OC:当前老年代大小

  • MCMN:最小元数据容量

  • MCMX:最大元数据容量

  • MC:当前元数据空间大小

  • CCSMN:最小压缩类空间大小

  • CCSMX:最大压缩类空间大小

  • CCSC:当前压缩类空间大小

  • YGC:年轻代gc次数

  • FGC:老年代GC次数

新生代垃圾回收统计

  • S0C:第一个幸存区的大小

  • S1C:第二个幸存区的大小

  • S0U:第一个幸存区的使用大小

  • S1U:第二个幸存区的使用大小

  • TT:对象在新生代存活的次数

  • MTT:对象在新生代存活的最大次数

  • DSS:期望的幸存区大小

  • EC:伊甸园区的大小

  • EU:伊甸园区的使用大小

  • YGC:年轻代垃圾回收次数

  • YGCT:年轻代垃圾回收消耗时间

除此之外还有新生代内存统计-gcnewcapacity、老年代垃圾回收统计-gcold、老年代内存统计-gcoldcapacity、元数据空间统计-gcmetacapacity等等,可以看到对应区域更细致的信息。

2.3.2 实战 如何预估JVM运行情况

用 jstat gc -pid 命令可以计算出如下一些关键数据,有了这些数据就可以采用之前介绍过的优化思路,先给自己的系统设置一些初始性的 JVM参数,比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等。

年轻代对象增长的速率 可以执行命令 jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次,mac环境下需要去掉命令里的pid),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。

Young GC的触发频率和每次耗时 知道年轻代对象增长速率我们就能推根据eden区的大小推算出Young GC大概多久触发一次,Young GC的平均耗时可以通过 YGCT/YGC 公式算出,根据结果我们大概就能知道系统大概多久会因为Young GC的执行而卡顿多久。

每次Young GC后有多少对象存活和进入老年代 这个因为之前已经大概知道Young GC的频率,假设是每5分钟一次,那么可以执行命令 jstat -gc pid 300000 10 ,观察每次结果eden, survivor和老年代使用的变化情况,在每次gc后eden区使用一般会大幅减少,survivor和老年代都有可能增长,这些增长的对象就是每次 Young GC后存活的对象,同时还可以看出每次Young GC后进去老年代大概多少对象,从而可以推算出老年代对象增长速率。

Full GC的触发频率和每次耗时 知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。

优化思路其实简单来说就是尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年 代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。

2.3.3系统频繁Full GC导致系统卡顿怎么处理

我们可以推测下full gc比minor gc还多的原因有哪些? 1、元空间不够导致的多余full gc。 2、显示调用System.gc()造成多余的full gc,这种一般线上尽量通过­XX:+DisableExplicitGC参数禁用,如果加上了这个JVM启动参数,那么代码中调用System.gc()没有任何效果。 3、过多的对象进入老年代。

一般来说每次Full GC会在500毫秒左右,而Young GC每次都在50ms以内。图中数据是我们做的一个假定:

此时我们想到两种策略:加大老年代,还是加大新生代?

结合这个对象挪动到老年代的规则推理下,我们可以判断,如果加大了老年代空间,此时老年代执行full GC的周期肯定会变长,但是每次执行的full gc耗费的时间更长了。

而如果我们加大新生代,此时会有更多的对象熬不过S0和S1,因此会有更少的对象进入老年代,因此Full GC的频率必然低了,能解决问题。

2.4. jinfo:实时查看和修改 JVM 配置参数

jinfo(Configuration Info for Java):查看虚拟机配置参数信息,也可用于调整虚拟机的配置参数。在很多情况卡,Java 应用程序不会指定所有的 Java 虚拟机参数。而此时,开发人员可能不知道某一个具体的 Java 虚拟机参数的默认值。在这种情况下,可能需要通过查找文档获取某个参数的默认值。这个查找过程可能是非常艰难的。但有了 jinfo 工具,开发人员可以很方便地找到 Java 虚拟机参数的当前值。

基本使用语法为:jinfo [options] pid

说明:java 进程 ID 必须要加上

选项 选项说明
no option 输出全部的参数和系统属性
-flag name 输出对应名称的参数
-flag [+-]name 开启或者关闭对应名称的参数 只有被标记为 manageable 的参数才可以被动态修改
-flag name=value 设定对应名称的参数
-flags 输出全部的参数
-sysprops 输出系统属性

jinfo -sysprops

jinfo -sysprops 进程ID

例如,在本人公司某个测试机下的进程如下所示:

然后执行如下命令:

jinfo -sysprops 12913

可以看到很多基础信息:

这里包含了服务的大量基础信息

我们还可以使用jinfo -flags命令查看服务启动的详细配置信息

jinfo -flags 12913

这些信息太多了,我们可以根据需要查找特定的几个,例如:

使用jinfo -flag查看某个特定项的设置,例如

输入: jinfo -flag UseParallelGC 12913
此时输出:
-XX:+UseParallelGC
输入:
jinfo -flag UseG1GC 12913
此时会输出:
-XX:-UseG1GC

2.5. jmap:导出内存映像文件&内存使用情况

jmap(JVM Memory Map):作用一方面是获取 dump 文件(堆转储快照文件,二进制文件),它还可以获取目标 Java 进程的内存相关信息,包括 Java 堆各区域的使用情况、堆中对象的统计信息、类加载信息等。开发人员可以在控制台中输入命令“jmap -help”查阅 jmap 工具的具体使用方式和一些标准选项配置。

官方帮助文档:jmap

基本使用语法为:

  • jmap [option] pid

  • jmap [option] executable core

  • jmap [option] [server_id@] remote server IP or hostname

选项 作用
-dump 生成 dump 文件(Java 堆转储快照),-dump:live 只保存堆中的存活对象
-heap 输出整个堆空间的详细信息,包括 GC 的使用、堆配置信息,以及内存的使用信息等
-histo 输出堆空间中对象的统计信息,包括类、实例数量和合计容量,-histo:live 只统计堆中的存活对象
-J flag 传递参数给 jmap 启动的 jvm
-finalizerinfo 显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象,仅 linux/solaris 平台有效
-permstat 以 ClassLoader 为统计口径输出永久代的内存状态信息,仅 linux/solaris 平台有效
-F 当虚拟机进程对-dump 选项没有任何响应时,强制执行生成 dump 文件,仅 linux/solaris 平台有效

说明:这些参数和 linux 下输入显示的命令多少会有不同,包括也受 jdk 版本的影响。

案例1:查看加载的类信息

查看某个进行加载的类信息:
jmap -histo  49938

输出的内容如下:

其中:

num:序号
instances:实例数量
bytes:占用空间大小
class name:类名称,[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int[][]

案例2:查看内存信息

格式:

jmap heap 进程号

上面这种方式在mac下是被禁止的无法使用。另一种格式是:

jmap -dump:live,format=b,file=eureka.hprof 50062

这样我们就将对信息复制到eureka.hprof里去了。

也可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)

1. -XX:+HeapDumpOnOutOfMemoryError
2. -XX:HeapDumpPath=./ (路径)

例如,我司某个服务的参数配置如下:

#JVM参数
JVM_ARGS="-XX:ParallelGCThreads=4 \-XX:+UseCMSInitiatingOccupancyOnly -XX:+UseFastAccessorMethods \-XX:+PrintGCDetails -XX:+PrintGCApplicationStoppedTime -XX:+PrintHeapAtGC\-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=50M \-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/shared/log/heapdump.hprof \-XX:SoftRefLRUPolicyMSPerMB=0 -verbose:gc -Xloggc:/home/shared/log/gc-truman.log"

由于 jmap 将访问堆中的所有对象,为了保证在此过程中不被应用线程干扰,jmap 需要借助安全点机制,让所有线程停留在不改变堆中数据的状态。也就是说,由 jmap 导出的堆快照必定是安全点位置的。这可能导致基于该堆快照的分析结果存在偏差。

举个例子,假设在编译生成的机器码中,某些对象的生命周期在两个安全点之间,那么:live 选项将无法探知到这些对象。

另外,如果某个线程长时间无法跑到安全点,jmap 将一直等下去。与前面讲的 jstat 则不同,垃圾回收器会主动将 jstat 所需要的摘要数据保存至固定位置之中,而 jstat 只需直接读取即可。

2.6. jhat:JDK 自带堆分析工具

jhat(JVM Heap Analysis Tool):Sun JDK 提供的 jhat 命令与 jmap 命令搭配使用,用于分析 jmap 生成的 heap dump 文件(堆转储快照)。jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 文件的分析结果后,用户可以在浏览器中查看分析结果(分析虚拟机转储快照信息)。

使用了 jhat 命令,就启动了一个 http 服务,端口是 7000,即 http://localhost:7000/,就可以在浏览器里分析。

说明:jhat 命令在 JDK9、JDK10 中已经被删除,官方建议用 VisualVM 代替。

基本适用语法:jhat option dumpfile

option 参数 作用
-stack false | true 关闭|打开对象分配调用栈跟踪
-refs false | true 关闭|打开对象引用跟踪
-port port-number 设置 jhat HTTP Server 的端口号,默认 7000
-exclude exclude-file 执行对象查询时需要排除的数据成员
-baseline exclude-file 指定一个基准堆转储
-debug int 设置 debug 级别
-version 启动后显示版本信息就退出
-J flag 传入启动参数,比如-J-Xmx512m

2.7. jstack:打印 JVM 中线程快照

jstack(JVM Stack Trace):用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)。线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈的集合。

生成线程快照的作用:可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用 jstack 显示各个线程调用的堆栈情况。

官方帮助文档:jstack

在 thread dump 中,要留意下面几种状态

  • 死锁,Deadlock(重点关注)

  • 等待资源,Waiting on condition(重点关注)

  • 等待获取监视器,Waiting on monitor entry(重点关注)

  • 阻塞,Blocked(重点关注)

  • 执行中,Runnable

  • 暂停,Suspended

  • 对象等待中,Object.wait() 或 TIMED_WAITING

  • 停止,Parked

option 参数 作用
-F 当正常输出的请求不被响应时,强制输出线程堆栈
-l 除堆栈外,显示关于锁的附加信息
-m 如果调用本地方法的话,可以显示 C/C++的堆栈

2.7.1 实战1:找到死锁的线程

如何发生了死锁,我们该如何定位到哪里出现问题了呢?例如下面这个代码执行时就会死锁:

public class DeadLockTest {private static Object lock1 = new Object();private static Object lock2 = new Object();
​public static void main(String[] args) {new Thread(() -> {synchronized (lock1) {try {System.out.println("thread1 begin");Thread.sleep(5000);} catch (InterruptedException e) {}synchronized (lock2) {System.out.println("thread1 end");}}}).start();new Thread(() -> {synchronized (lock2) {try {System.out.println("thread2 begin");Thread.sleep(5000);} catch (InterruptedException e) {}synchronized (lock1) {System.out.println("thread2 end");}}}).start();
​System.out.println("main thread end");}
}

此时先使用jps找到我们的进程:

然后使用jstack查看信息:jstack 53385

这时候我们会看到如下信息;

很明显,这里thread-1 锁住了cc58,然后在等待锁住cc48,而thread-2锁住了cc48,然后等待锁住cc58,因此两个互不相让,造成死锁。

我们也可以通过jvisualvm来查看:

可以看到此时直接输出了一个“检测到死锁”,如果单击右侧的“线程Dump”就可以看到与jstack类似的信息。

2.7.2 实战2:CPU突然飙高,该如何定位代码

如果程序执行时突然CPU占比飙升很高,该如何定位到是哪个代码的问题呢?我们以jstack为主要工具就可以解决,现在来演示一下,我们首先写这样一段代码:

public class Math {public int compute() {int c = (2 + 3) * 10;return c;}
​public static void main(String[] args) {Math math = new Math();while (true) {math.compute();}}
}

很明显这段代码执行会一直执行计算,占用大量CPU资源,我们的定位方法是:

  1. 输入top命令检查占用CPU高的进程

例如上面的代码执行之后,在终端输入top命令会看到如下信息:

很明显这里的51221进程占用了95%以上的CPU资源。

2.通过top -pid 进程号,(有些环境是top -p 进程号)检查占用资源高的线程

例如:

top -pid 51221

然后输入H,可以看到进程里活动线程的情况。

但是这个功能在mac下没有,我在公司的环境下做了如下的测试,截图:

我们可以看到这里的19664是占用CPU最高的线程。然后我们可以使用jstack来搜索,为此我们先看到Math程序在jstack中的信息是什么样子的。

我们这里可以看到有个tid和nid,其中tid是JVM里的线程标识,而nid是本地的nid,我们要使用后者同时,在上图中的19664是10进制,而在jstack中是16进制,因此我们要先转换一下再执行,因此此时我们可以通过下面的命令搜索到Math方法:

 jstack 19663|grep -A 10 4cd0

这样就得到线程堆栈信息中 4cd0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调 用方法。

2.8. jcmd:多功能命令行

在 JDK 1.7 以后,新增了一个命令行工具 jcmd。它是一个多功能的工具,可以用来实现前面除了 jstat 之外所有命令的功能。比如:用它来导出堆、内存使用、查看 Java 进程、导出线程信息、执行 GC、JVM 运行时间等。

官方帮助文档:jcmd

jcmd 拥有 jmap 的大部分功能,并且在 Oracle 的官方网站上也推荐使用 jcmd 命令代 jmap 命令

jcmd -l:列出所有的 JVM 进程

jcmd 进程号 help:针对指定的进程,列出支持的所有具体命令

jcmd 进程号 具体命令:显示指定进程的指令命令的数据

  • Thread.print 可以替换 jstack 指令

  • GC.class_histogram 可以替换 jmap 中的-histo 操作

  • GC.heap_dump 可以替换 jmap 中的-dump 操作

  • GC.run 可以查看 GC 的执行情况

  • VM.uptime 可以查看程序的总执行时间,可以替换 jstat 指令中的-t 操作

  • VM.system_properties 可以替换 jinfo -sysprops 进程 id

  • VM.flags 可以获取 JVM 的配置参数信息

3. JVM 监控及诊断工具-GUI 篇

3.1. 工具概述

使用上一章命令行工具或组合能帮您获取目标 Java 应用性能相关的基础信息,但它们存在下列局限:

  • 1.无法获取方法级别的分析数据,如方法间的调用关系、各方法的调用次数和调用时间等(这对定位应用性能瓶颈至关重要)。

  • 2.要求用户登录到目标 Java 应用所在的宿主机上,使用起来不是很方便。

  • 3.分析数据通过终端输出,结果展示不够直观。

为此,JDK 提供了一些内存泄漏的分析工具,如 jconsole,jvisualvm 等,用于辅助开发人员定位问题,但是这些工具很多时候并不足以满足快速定位的需求。所以这里我们介绍的工具相对多一些、丰富一些。

JDK 自带的工具

  • jconsole:JDK 自带的可视化监控工具。查看 Java 应用程序的运行概况、监控堆信息、永久区(或元空间)使用情况、类加载情况等

  • Visual VM:Visual VM 是一个工具,它提供了一个可视界面,用于查看 Java 虚拟机上运行的基于 Java 技术的应用程序的详细信息。

  • JMC:Java Mission Control,内置 Java Flight Recorder。能够以极低的性能开销收集 Java 虚拟机的性能数据。

第三方工具

  • MAT:MAT(Memory Analyzer Tool)是基于 Eclipse 的内存分析工具,是一个快速、功能丰富的 Java heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗

  • JProfiler:商业软件,需要付费。功能强大。

3.2. JConsole

jconsole:从 Java5 开始,在 JDK 中自带的 java 监控和管理控制台。用于对 JVM 中内存、线程和类等的监控,是一个基于 JMX(java management extensions)的 GUI 性能监控工具。

官方地址:Using JConsole - Java SE Monitoring and ManagementGuide

3.3. Visual VM

Visual VM 是一个功能强大的多合一故障诊断和性能监控的可视化工具。它集成了多个 JDK 命令行工具,使用 Visual VM 可用于显示虚拟机进程及进程的配置和环境信息(jps,jinfo),监视应用程序的 CPU、GC、堆、方法区及线程的信息(jstat、jstack)等,甚至代替 JConsole。在 JDK 6 Update 7 以后,Visual VM 便作为 JDK 的一部分发布(VisualVM 在 JDK/bin 目录下)即:它完全免费。

主要功能:

  • 1.生成/读取堆内存/线程快照

  • 2.查看 JVM 参数和系统属性

  • 3.查看运行中的虚拟机进程

  • 4.程序资源的实时监控

  • 5.JMX 代理连接、远程环境监控、CPU 分析和内存分析

官方地址:VisualVM: Home

3.4. Eclipse MAT

MAT(Memory Analyzer Tool)工具是一款功能强大的 Java 堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。MAT 是基于 Eclipse 开发的,不仅可以单独使用,还可以作为插件的形式嵌入在 Eclipse 中使用。是一款免费的性能分析工具,使用起来非常方便。

MAT 可以分析 heap dump 文件。在进行内存分析时,只要获得了反映当前设备内存映像的 hprof 文件,通过 MAT 打开就可以直观地看到当前的内存信息。一般说来,这些内存信息包含:

  • 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。

  • 所有的类信息,包括 classloader、类名称、父类、静态变量等

  • GCRoot 到所有的这些对象的引用路径

  • 线程信息,包括线程的调用栈及此线程的线程局部变量(TLS)

MAT 不是一个万能工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如 Sun,HP,SAP 所采用的 HPROF 二进制堆存储文件,以及 IBM 的 PHD 堆存储文件等都能被很好的解析。

最吸引人的还是能够快速为开发人员生成内存泄漏报表,方便定位问题和分析问题。虽然 MAT 有如此强大的功能,但是内存分析也没有简单到一键完成的程度,很多内存问题还是需要我们从 MAT 展现给我们的信息当中通过经验和直觉来判断才能发现。

官方地址: Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation

3.5. JProfiler

在运行 Java 的时候有时候想测试运行时占用内存情况,这时候就需要使用测试工具查看了。在 eclipse 里面有 Eclipse Memory Analyzer tool(MAT)插件可以测试,而在 IDEA 中也有这么一个插件,就是 JProfiler。JProfiler 是由 ej-technologies 公司开发的一款 Java 应用性能诊断工具。功能强大,但是收费。

特点:

  • 使用方便、界面操作友好(简单且强大)

  • 对被分析的应用影响小(提供模板)

  • CPU,Thread,Memory 分析功能尤其强大

  • 支持对 jdbc,noSql,jsp,servlet,socket 等进行分析

  • 支持多种模式(离线,在线)的分析

  • 支持监控本地、远程的 JVM

  • 跨平台,拥有多种操作系统的安装版本

主要功能:

  • 1-方法调用:对方法调用的分析可以帮助您了解应用程序正在做什么,并找到提高其性能的方法

  • 2-内存分配:通过分析堆上对象、引用链和垃圾收集能帮您修复内存泄露问题,优化内存使用

  • 3-线程和锁:JProfiler 提供多种针对线程和锁的分析视图助您发现多线程问题

  • 4-高级子系统:许多性能问题都发生在更高的语义级别上。例如,对于 JDBC 调用,您可能希望找出执行最慢的 SQL 语句。JProfiler 支持对这些子系统进行集成分析

官网地址:Java Profiler - JProfiler

数据采集方式:

JProfier 数据采集方式分为两种:Sampling(样本采集)和 Instrumentation(重构模式)

Instrumentation:这是 JProfiler 全功能模式。在 class 加载之前,JProfier 把相关功能代码写入到需要分析的 class 的 bytecode 中,对正在运行的 jvm 有一定影响。

  • 优点:功能强大。在此设置中,调用堆栈信息是准确的。

  • 缺点:若要分析的 class 较多,则对应用的性能影响较大,CPU 开销可能很高(取决于 Filter 的控制)。因此使用此模式一般配合 Filter 使用,只对特定的类或包进行分析

Sampling:类似于样本统计,每隔一定时间(5ms)将每个线程栈中方法栈中的信息统计出来。

  • 优点:对 CPU 的开销非常低,对应用影响小(即使你不配置任何 Filter)

  • 缺点:一些数据/特性不能提供(例如:方法的调用次数、执行时间)

注:JProfiler 本身没有指出数据的采集类型,这里的采集类型是针对方法调用的采集类型。因为 JProfiler 的绝大多数核心功能都依赖方法调用采集的数据,所以可以直接认为是 JProfiler 的数据采集类型。

遥感监测 Telemetries

内存视图 Live Memory

Live memory 内存剖析:class/class instance 的相关信息。例如对象的个数,大小,对象创建的方法执行栈,对象创建的热点。

  • 所有对象 All Objects:显示所有加载的类的列表和在堆上分配的实例数。只有 Java 1.5(JVMTI)才会显示此视图。

  • 记录对象 Record Objects:查看特定时间段对象的分配,并记录分配的调用堆栈。

  • 分配访问树 Allocation Call Tree:显示一棵请求树或者方法、类、包或对已选择类有带注释的分配信息的 J2EE 组件。

  • 分配热点 Allocation Hot Spots:显示一个列表,包括方法、类、包或分配已选类的 J2EE 组件。你可以标注当前值并且显示差异值。对于每个热点都可以显示它的跟踪记录树。

  • 类追踪器 Class Tracker:类跟踪视图可以包含任意数量的图表,显示选定的类和包的实例与时间。

堆遍历 heap walker

cpu 视图 cpu views

JProfiler 提供不同的方法来记录访问树以优化性能和细节。线程或者线程组以及线程状况可以被所有的视图选择。所有的视图都可以聚集到方法、类、包或 J2EE 组件等不同层上。

  • 访问树 Call Tree:显示一个积累的自顶向下的树,树中包含所有在 JVM 中已记录的访问队列。JDBC,JMS 和 JNDI 服务请求都被注释在请求树中。请求树可以根据 Servlet 和 JSP 对 URL 的不同需要进行拆分。

  • 热点 Hot Spots:显示消耗时间最多的方法的列表。对每个热点都能够显示回溯树。该热点可以按照方法请求,JDBC,JMS 和 JNDI 服务请求以及按照 URL 请求来进行计算。

  • 访问图 Call Graph:显示一个从已选方法、类、包或 J2EE 组件开始的访问队列的图。

  • 方法统计 Method Statistis:显示一段时间内记录的方法的调用时间细节。

线程视图 threads

JProfiler 通过对线程历史的监控判断其运行状态,并监控是否有线程阻塞产生,还能将一个线程所管理的方法以树状形式呈现。对线程剖析。

  • 线程历史 Thread History:显示一个与线程活动和线程状态在一起的活动时间表。

  • 线程监控 Thread Monitor:显示一个列表,包括所有的活动线程以及它们目前的活动状况。

  • 线程转储 Thread Dumps:显示所有线程的堆栈跟踪。

线程分析主要关心三个方面:

  • 1.web 容器的线程最大数。比如:Tomcat 的线程容量应该略大于最大并发数。

  • 2.线程阻塞

  • 3.线程死锁

监控和锁 Monitors &Locks

所有线程持有锁的情况以及锁的信息。观察 JVM 的内部线程并查看状态:

  • 死锁探测图表 Current Locking Graph:显示 JVM 中的当前死锁图表。

  • 目前使用的监测器 Current Monitors:显示目前使用的监测器并且包括它们的关联线程。

  • 锁定历史图表 Locking History Graph:显示记录在 JVM 中的锁定历史。

  • 历史检测记录 Monitor History:显示重大的等待事件和阻塞事件的历史记录。

  • 监控器使用统计 Monitor Usage Statistics:显示分组监测,线程和监测类的统计监测数据

3.6. Arthas

上述工具都必须在服务端项目进程中配置相关的监控参数,然后工具通过远程连接到项目进程,获取相关的数据。这样就会带来一些不便,比如线上环境的网络是隔离的,本地的监控工具根本连不上线上环境。并且类似于 Jprofiler 这样的商业工具,是需要付费的。

那么有没有一款工具不需要远程连接,也不需要配置监控参数,同时也提供了丰富的性能监控数据呢?

阿里巴巴开源的性能分析神器 Arthas 应运而生。

Arthas 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪 Java 代码;实时监控 JVM 状态。Arthas 支持 JDK 6 +,支持 Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。当你遇到以下类似问题而束手无策时,Arthas 可以帮助你解决:

  • 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?

  • 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?

  • 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?

  • 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!

  • 是否有一个全局视角来查看系统的运行状况?

  • 有什么办法可以监控到 JVM 的实时运行状态?

  • 怎么快速定位应用的热点,生成火焰图?

官方地址:快速入门 — Arthas 3.6.2 文档

安装方式:如果速度较慢,可以尝试国内的码云 Gitee 下载。

wget https://io/arthas/arthas-boot.jar
wget https://arthas/gitee/io/arthas-boot.jar

Arthas 只是一个 java 程序,所以可以直接用 java -jar 运行。

除了在命令行查看外,Arthas 目前还支持 Web Console。在成功启动连接进程之后就已经自动启动,可以直接访问 http://127.0.0.1:8563/ 访问,页面上的操作模式和控制台完全一样。

基础指令

quit/exit 退出当前 Arthas客户端,其他 Arthas喜户端不受影响
stop/shutdown 关闭 Arthas服务端,所有 Arthas客户端全部退出
help 查看命令帮助信息
cat 打印文件内容,和linux里的cat命令类似
echo 打印参数,和linux里的echo命令类似
grep 匹配查找,和linux里的gep命令类似
tee 复制标隹输入到标准输出和指定的文件,和linux里的tee命令类似
pwd 返回当前的工作目录,和linux命令类似
cls 清空当前屏幕区域
session 查看当前会话的信息
reset 重置增强类,将被 Arthas增强过的类全部还原, Arthas服务端关闭时会重置所有增强过的类
version 输出当前目标Java进程所加载的 Arthas版本号
history 打印命令历史
keymap Arthas快捷键列表及自定义快捷键

jvm 相关



dashboard 当前系统的实时数据面板
thread 查看当前JVM的线程堆栈信息,加上 -b参数可以直接看死锁线程 
jvm 查看当前JVM的信息
sysprop 查看和修改JVM的系统属性
sysem 查看JVM的环境变量
vmoption 查看和修改JVM里诊断相关的option
perfcounter 查看当前JVM的 Perf Counter信息
logger 查看和修改logger
getstatic 查看类的静态属性
ognl 执行ognl表达式
mbean 查看 Mbean的信息
heapdump dump java heap,类似jmap命令的 heap dump功能

class/classloader 相关



sc 查看JVM已加载的类信息
  -d 输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的Classloader等详细信息。如果一个类被多个Classloader所加载,则会出现多次
  -E 开启正则表达式匹配,默认为通配符匹配
  -f 输出当前类的成员变量信息(需要配合参数-d一起使用)
  -X 指定输出静态变量时属性的遍历深度,默认为0,即直接使用toString输出
sm 查看已加载类的方法信息
  -d 展示每个方法的详细信息
  -E 开启正则表达式匹配,默认为通配符匹配
jad 反编译指定已加载类的源码
mc 内存编译器,内存编译.java文件为.class文件
retransform 加载外部的.class文件, retransform到JVM里
redefine 加载外部的.class文件,redefine到JVM里
dump dump已加载类的byte code到特定目录
classloader 查看classloader的继承树,urts,类加载信息,使用classloader去getResource
  -t 查看classloader的继承树
  -l 按类加载实例查看统计信息
  -c 用classloader对应的hashcode来查看对应的 Jar urls

monitor/watch/trace 相关

monitor 方法执行监控,调用次数、执行时间、失败率-c 统计周期,默认值为120秒
watch 方法执行观测,能观察到的范围为:返回值、抛出异常、入参,通过编写groovy表达式进行对应变量的查看-b 在方法调用之前观察(默认关闭)-e 在方法异常之后观察(默认关闭)-s 在方法返回之后观察(默认关闭)-f 在方法结束之后(正常返回和异常返回)观察(默认开启)-x 指定输岀结果的属性遍历深度,默认为0
trace 方法内部调用路径,并输出方法路径上的每个节点上耗时-n 执行次数限制
stack 输出当前方法被调用的调用路径
tt 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测

其他



jobs 列出所有job
kill 强制终止任务
fg 将暂停的任务拉到前台执行
bg 将暂停的任务放到后台执行
grep 搜索满足条件的结果
plaintext 将命令的结果去除ANSI颜色
wc 按行统计输出结果
options 查看或设置Arthas全局开关
profiler 使用async-profiler对应用采样,生成火焰图

反编译等具体操作见视频讲解。

3.7. Java Misssion Control

在 Oracle 收购 Sun 之前,Oracle 的 JRockit 虚拟机提供了一款叫做 JRockit Mission Control 的虚拟机诊断工具。

在 Oracle 收购 sun 之后,Oracle 公司同时拥有了 Hotspot 和 JRockit 两款虚拟机。根据 Oracle 对于 Java 的战略,在今后的发展中,会将 JRokit 的优秀特性移植到 Hotspot 上。其中一个重要的改进就是在 Sun 的 JDK 中加入了 JRockit 的支持。

在 Oracle JDK 7u40 之后,Mission Control 这款工具己经绑定在 Oracle JDK 中发布。

自 Java11 开始,本节介绍的 JFR 己经开源。但在之前的 Java 版本,JFR 属于 Commercial Feature 通过 Java 虚拟机参数-XX:+UnlockCommercialFeatures 开启。

Java Mission Control(简称 JMC) , Java 官方提供的性能强劲的工具,是一个用于对 Java 应用程序进行管理、监视、概要分析和故障排除的工具套件。它包含一个 GUI 客户端以及众多用来收集 Java 虚拟机性能数据的插件如 JMX Console(能够访问用来存放虚拟机齐个于系统运行数据的 MXBeans)以及虚拟机内置的高效 profiling 工具 Java Flight Recorder(JFR)。

JMC 的另一个优点就是:采用取样,而不是传统的代码植入技术,对应用性能的影响非常非常小,完全可以开着 JMC 来做压测(唯一影响可能是 full gc 多了)。

官方地址:GitHub - JDKMissionControl/jmc: This mirror is deprecated - please start using https://github.com/openjdk/jmc

Java Flight Recorder

Java Flight Recorder 是 JMC 的其中一个组件,能够以极低的性能开销收集 Java 虚拟机的性能数据。与其他工具相比,JFR 的性能开销很小,在默认配置下平均低于 1%。JFR 能够直接访问虚拟机内的敌据并且不会影响虚拟机的优化。因此它非常适用于生产环境下满负荷运行的 Java 程序。

Java Flight Recorder 和 JDK Mission Control 共同创建了一个完整的工具链。JDK Mission Control 可对 Java Flight Recorder 连续收集低水平和详细的运行时信息进行高效、详细的分析。

当启用时 JFR 将记录运行过程中发生的一系列事件。其中包括 Java 层面的事件如线程事件、锁事件,以及 Java 虚拟机内部的事件,如新建对象,垃圾回收和即时编译事件。按照发生时机以及持续时间来划分,JFR 的事件共有四种类型,它们分别为以下四种:

  • 瞬时事件(Instant Event) ,用户关心的是它们发生与否,例如异常、线程启动事件。

  • 持续事件(Duration Event) ,用户关心的是它们的持续时间,例如垃圾回收事件。

  • 计时事件(Timed Event) ,是时长超出指定阈值的持续事件。

  • 取样事件(Sample Event),是周期性取样的事件。

取样事件的其中一个常见例子便是方法抽样(Method Sampling),即每隔一段时问统计各个线程的栈轨迹。如果在这些抽样取得的栈轨迹中存在一个反复出现的方法,那么我们可以推测该方法是热点方法

3.8. 其他工具

Flame Graphs(火焰图)

在追求极致性能的场景下,了解你的程序运行过程中 cpu 在干什么很重要,火焰图就是一种非常直观的展示 CPU 在程序整个生命周期过程中时间分配的工具。火焰图对于现代的程序员不应该陌生,这个工具可以非常直观的显示出调用找中的 CPU 消耗瓶颈。

网上的关于 Java 火焰图的讲解大部分来自于 Brenden Gregg 的博客 http://new.brendangregg.com/flamegraphs.html

火焰图,简单通过 x 轴横条宽度来度量时间指标,y 轴代表线程栈的层次。

Tprofiler

案例: 使用 JDK 自身提供的工具进行 JVM 调优可以将下 TPS 由 2.5 提升到 20(提升了 7 倍),并准确 定位系统瓶颈。

系统瓶颈有:应用里释态对象不是太多、有大量的业务线程在频繁创建一些生命周期很长的临时对象,代码里有问题。

那么,如何在海量业务代码里边准确定位这些性能代码?这里使用阿里开源工具 Tprofiler 来定位 这些性能代码,成功解决掉了 GC 过于频繁的性能瓶预,并最终在上次优化的基础上将 TPS 再提升了 4 倍,即提升到 100。

  • Tprofiler 配置部署、远程操作、 日志阅谈都不太复杂,操作还是很简单的。但是其却是能够 起到一针见血、立竿见影的效果,帮我们解决了 GC 过于频繁的性能瓶预。

  • Tprofiler 最重要的特性就是能够统汁出你指定时间段内 JVM 的 top method 这些 top method 极有可能就是造成你 JVM 性能瓶颈的元凶。这是其他大多数 JVM 调优工具所不具备的,包括 JRockit Mission Control。JRokit 首席开发者 Marcus Hirt 在其私人博客《 Lom Overhead Method Profiling cith Java Mission Control》下的评论中曾明确指出 JRMC 井不支持 TOP 方法的统计。

官方地址:GitHub - alibaba/TProfiler: TProfiler是一个可以在生产环境长期使用的性能分析工具

Btrace

常见的动态追踪工具有 BTrace、HouseHD(该项目己经停止开发)、Greys-Anatomy(国人开发 个人开发者)、Byteman(JBoss 出品),注意 Java 运行时追踪工具井不限干这几种,但是这几个是相对比较常用的。

BTrace 是 SUN Kenai 云计算开发平台下的一个开源项目,旨在为 java 提供安全可靠的动态跟踪分析工具。先看一卜日 Trace 的官方定义:

大概意思是一个 Java 平台的安全的动态追踪工具,可以用来动态地追踪一个运行的 Java 程序。BTrace 动态调整目标应用程序的类以注入跟踪代码(“字节码跟踪“)。

4. JVM 运行时参数

4.1. JVM 参数选项

官网地址:java

4.1.1. 类型一:标准参数选项

> java -help
用法: java [-options] class [args...](执行类)或  java [-options] -jar jarfile [args...](执行 jar 文件)
其中选项包括:-d32          使用 32 位数据模型 (如果可用)-d64          使用 64 位数据模型 (如果可用)-server       选择 "server" VM默认 VM 是 server.
​-cp <目录和 zip/jar 文件的类搜索路径>-classpath <目录和 zip/jar 文件的类搜索路径>用 ; 分隔的目录, JAR 档案和 ZIP 档案列表, 用于搜索类文件。-D<名称>=<值>设置系统属性-verbose:[class|gc|jni]启用详细输出-version      输出产品版本并退出-version:<值>警告: 此功能已过时, 将在未来发行版中删除。需要指定的版本才能运行-showversion  输出产品版本并继续-jre-restrict-search | -no-jre-restrict-search警告: 此功能已过时, 将在未来发行版中删除。在版本搜索中包括/排除用户专用 JRE-? -help      输出此帮助消息-X            输出非标准选项的帮助-ea[:<packagename>...|:<classname>]-enableassertions[:<packagename>...|:<classname>]按指定的粒度启用断言-da[:<packagename>...|:<classname>]-disableassertions[:<packagename>...|:<classname>]禁用具有指定粒度的断言-esa | -enablesystemassertions启用系统断言-dsa | -disablesystemassertions禁用系统断言-agentlib:<libname>[=<选项>]加载本机代理库 <libname>, 例如 -agentlib:hprof另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help-agentpath:<pathname>[=<选项>]按完整路径名加载本机代理库-javaagent:<jarpath>[=<选项>]加载 Java 编程语言代理, 请参阅 java.lang.instrument-splash:<imagepath>使用指定的图像显示启动屏幕
有关详细信息, 请参阅 http://www.oracle.com/technetwork/java/javase/documentation/index.html。

Server 模式和 Client 模式

Hotspot JVM 有两种模式,分别是 server 和 client,分别通过-server 和-client 模式设置

  • 32 位系统上,默认使用 Client 类型的 JVM。要想使用 Server 模式,机器配置至少有 2 个以上的 CPU 和 2G 以上的物理内存。client 模式适用于对内存要求较小的桌面应用程序,默认使用 Serial 串行垃圾收集器

  • 64 位系统上,只支持 server 模式的 JVM,适用于需要大内存的应用程序,默认使用并行垃圾收集器

官网地址:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/server-class.html

如何知道系统默认使用的是那种模式呢?

通过 java -version 命令:可以看到 Server VM 字样,代表当前系统使用是 Server 模式

> java -version
java version "1.8.0_201"
Java(TM) SE Runtime Environment (build 1.8.0_201-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)

4.1.2. 类型二:-X 参数选项

java -X-Xmixed           混合模式执行 (默认)-Xint             仅解释模式执行-Xbootclasspath:<用 ; 分隔的目录和 zip/jar 文件>设置搜索路径以引导类和资源-Xbootclasspath/a:<用 ; 分隔的目录和 zip/jar 文件>附加在引导类路径末尾-Xbootclasspath/p:<用 ; 分隔的目录和 zip/jar 文件>置于引导类路径之前-Xdiag            显示附加诊断消息-Xnoclassgc       禁用类垃圾收集-Xincgc           启用增量垃圾收集-Xloggc:<file>    将 GC 状态记录在文件中 (带时间戳)-Xbatch           禁用后台编译-Xms<size>        设置初始 Java 堆大小-Xmx<size>        设置最大 Java 堆大小-Xss<size>        设置 Java 线程堆栈大小-Xprof            输出 cpu 配置文件数据-Xfuture          启用最严格的检查, 预期将来的默认值-Xrs              减少 Java/VM 对操作系统信号的使用 (请参阅文档)-Xcheck:jni       对 JNI 函数执行其他检查-Xshare:off       不尝试使用共享类数据-Xshare:auto      在可能的情况下使用共享类数据 (默认)-Xshare:on        要求使用共享类数据, 否则将失败。-XshowSettings    显示所有设置并继续-XshowSettings:all显示所有设置并继续-XshowSettings:vm 显示所有与 vm 相关的设置并继续-XshowSettings:properties显示所有属性设置并继续-XshowSettings:locale显示所有与区域设置相关的设置并继续
​
-X 选项是非标准选项, 如有更改, 恕不另行通知。

如何知道 JVM 默认使用的是混合模式呢?

同样地,通过 java -version 命令:可以看到 mixed mode 字样,代表当前系统使用的是混合模式

4.1.3. 类型三:-XX 参数选项

Boolean 类型格式
-XX:+<option>  启用option属性
-XX:-<option>  禁用option属性
非 Boolean 类型格式-XX:<option>=<number>  设置option数值,可以带单位如k/K/m/M/g/G
-XX:<option>=<string>  设置option字符值

4.2. 添加 JVM 参数选项

eclipse 和 idea 中配置不必多说,在 Run Configurations 中 VM Options 中配置即可,大同小异

程序运行中

运行 jar 包
java -Xms100m -Xmx100m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -jar demo.jar
Tomcat 运行 war 包# linux下catalina.sh添加
JAVA_OPTS="-Xms512M -Xmx1024M"
# windows下catalina.bat添加
set "JAVA_OPTS=-Xms512M -Xmx1024M"# 设置Boolean类型参数
jinfo -flag [+|-]<name> <pid>
# 设置非Boolean类型参数
jinfo -flag <name>=<value> <pid>

4.3. 常用的 JVM 参数选项

4.3.1. 打印设置的 XX 选项及值

-XX:+PrintCommandLineFlags 程序运行时JVM默认设置或用户手动设置的XX选项
-XX:+PrintFlagsInitial 打印所有XX选项的默认值
-XX:+PrintFlagsFinal 打印所有XX选项的实际值
-XX:+PrintVMOptions 打印JVM的参数

4.3.2. 堆、栈、方法区等内存大小设置

# 栈
-Xss128k <==> -XX:ThreadStackSize=128k 设置线程栈的大小为128K
​
# 堆
-Xms2048m <==> -XX:InitialHeapSize=2048m 设置JVM初始堆内存为2048M
-Xmx2048m <==> -XX:MaxHeapSize=2048m 设置JVM最大堆内存为2048M
-Xmn2g <==> -XX:NewSize=2g -XX:MaxNewSize=2g 设置年轻代大小为2G
-XX:SurvivorRatio=8 设置Eden区与Survivor区的比值,默认为8
-XX:NewRatio=2 设置老年代与年轻代的比例,默认为2
-XX:+UseAdaptiveSizePolicy 设置大小比例自适应,默认开启
-XX:PretenureSizeThreadshold=1024 设置让大于此阈值的对象直接分配在老年代,只对Serial、ParNew收集器有效
-XX:MaxTenuringThreshold=15 设置新生代晋升老年代的年龄限制,默认为15
-XX:TargetSurvivorRatio 设置MinorGC结束后Survivor区占用空间的期望比例
​
# 方法区
-XX:MetaspaceSize / -XX:PermSize=256m 设置元空间/永久代初始值为256M
-XX:MaxMetaspaceSize / -XX:MaxPermSize=256m 设置元空间/永久代最大值为256M
-XX:+UseCompressedOops 使用压缩对象
-XX:+UseCompressedClassPointers 使用压缩类指针
-XX:CompressedClassSpaceSize 设置Klass Metaspace的大小,默认1G
​
# 直接内存
-XX:MaxDirectMemorySize 指定DirectMemory容量,默认等于Java堆最大值

4.3.3. OutOfMemory 相关的选项

-XX:+HeapDumpOnOutMemoryError 内存出现OOM时生成Heap转储文件,两者互斥
-XX:+HeapDumpBeforeFullGC 出现FullGC时生成Heap转储文件,两者互斥
-XX:HeapDumpPath=<path> 指定heap转储文件的存储路径,默认当前目录
-XX:OnOutOfMemoryError=<path> 指定可行性程序或脚本的路径,当发生OOM时执行脚本

4.3.4. 垃圾收集器相关选项

首先需了解垃圾收集器之间的搭配使用关系

  • 红色虚线表示在 jdk8 时被 Deprecate,jdk9 时被删除

  • 绿色虚线表示在 jdk14 时被 Deprecate

  • 绿色虚框表示在 jdk9 时被 Deprecate,jdk14 时被删除

# Serial回收器
-XX:+UseSerialGC  年轻代使用Serial GC, 老年代使用Serial Old GC
# ParNew回收器
-XX:+UseParNewGC  年轻代使用ParNew GC
-XX:ParallelGCThreads  设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
_ParallelGCThreads={CPU_Count(CPU_Count<=8)3+(5∗CPU_Count/8)(CPU_Count>8)# Parallel回收器
-XX:+UseParallelGC  年轻代使用 Parallel Scavenge GC,互相激活
-XX:+UseParallelOldGC  老年代使用 Parallel Old GC,互相激活
-XX:ParallelGCThreads
-XX:MaxGCPauseMillis  设置垃圾收集器最大停顿时间(即STW的时间),单位是毫秒。为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整Java堆大小或者其他一些参数。对于用户来讲,停顿时间越短体验越好;但是服务器端注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。该参数使用需谨慎。
-XX:GCTimeRatio  垃圾收集时间占总时间的比例(1 / (N+1)),用于衡量吞吐量的大小取值范围(0,100),默认值99,也就是垃圾回收时间不超过1%。与前一个-XX:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。
-XX:+UseAdaptiveSizePolicy  设置Parallel Scavenge收集器具有自适应调节策略。在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机自己完成调优工作。# CMS回收器
-XX:+UseConcMarkSweepGC  年轻代使用CMS GC。# Serial回收器
-XX:+UseSerialGC  年轻代使用Serial GC, 老年代使用Serial Old GC
# ParNew回收器
-XX:+UseParNewGC  年轻代使用ParNew GC
-XX:ParallelGCThreads  设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
_ParallelGCThreads={CPU_Count(CPU_Count<=8)3+(5∗CPU_Count/8)(CPU_Count>8)# Parallel回收器
-XX:+UseParallelGC  年轻代使用 Parallel Scavenge GC,互相激活
-XX:+UseParallelOldGC  老年代使用 Parallel Old GC,互相激活
-XX:ParallelGCThreads
-XX:MaxGCPauseMillis  设置垃圾收集器最大停顿时间(即STW的时间),单位是毫秒。为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整Java堆大小或者其他一些参数。对于用户来讲,停顿时间越短体验越好;但是服务器端注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。该参数使用需谨慎。
-XX:GCTimeRatio  垃圾收集时间占总时间的比例(1 / (N+1)),用于衡量吞吐量的大小取值范围(0,100),默认值99,也就是垃圾回收时间不超过1%。与前一个-XX:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。
-XX:+UseAdaptiveSizePolicy  设置Parallel Scavenge收集器具有自适应调节策略。在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机自己完成调优工作。# CMS回收器
-XX:+UseConcMarkSweepGC  年轻代使用CMS GC。
  开启该参数后会自动将-XX:+UseParNewGC打开。即:ParNew(Young区)+ CMS(Old区)+ Serial Old的组合
-XX:CMSInitiatingOccupanyFraction  设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。JDK5及以前版本的默认值为68,DK6及以上版本默认值为92%。
  如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。
  反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。
  因此通过该选项便可以有效降低Fu1l GC的执行次数。
-XX:+UseCMSInitiatingOccupancyOnly  是否动态可调,使CMS一直按CMSInitiatingOccupancyFraction设定的值启动
-XX:+UseCMSCompactAtFullCollection  用于指定在执行完Full GC后对内存空间进行压缩整理
  以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。
-XX:CMSFullGCsBeforeCompaction  设置在执行多少次Full GC后对内存空间进行压缩整理。
-XX:ParallelCMSThreads  设置CMS的线程数量。
  CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数。
  当CPU 资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。
-XX:ConcGCThreads  设置并发垃圾收集的线程数,默认该值是基于ParallelGCThreads计算出来的
-XX:+CMSScavengeBeforeRemark  强制hotspot在cms remark阶段之前做一次minor gc,用于提高remark阶段的速度
-XX:+CMSClassUnloadingEnable  如果有的话,启用回收Perm 区(JDK8之前)
-XX:+CMSParallelInitialEnabled  用于开启CMS initial-mark阶段采用多线程的方式进行标记
  用于提高标记速度,在Java8开始已经默认开启
-XX:+CMSParallelRemarkEnabled  用户开启CMS remark阶段采用多线程的方式进行重新标记,默认开启
-XX:+ExplicitGCInvokesConcurrent
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
  这两个参数用户指定hotspot虚拟在执行System.gc()时使用CMS周期
-XX:+CMSPrecleaningEnabled  指定CMS是否需要进行Pre cleaning阶段# G1回收器
-XX:+UseG1GC 手动指定使用G1收集器执行内存回收任务。
-XX:G1HeapRegionSize 设置每个Region的大小。
  值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。
-XX:MaxGCPauseMillis  设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms
-XX:ParallelGCThread  设置STW时GC线程数的值。最多设置为8
-XX:ConcGCThreads  设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
-XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
-XX:G1NewSizePercent  新生代占用整个堆内存的最小百分比(默认5%)
-XX:G1MaxNewSizePercent  新生代占用整个堆内存的最大百分比(默认60%)
-XX:G1ReservePercent=10  保留内存区域,防止 to space(Survivor中的to区)溢出

怎么选择垃圾回收器?

  • 优先让 JVM 自适应,调整堆的大小

  • 串行收集器:内存小于 100M;单核、单机程序,并且没有停顿时间的要求

  • 并行收集器:多 CPU、高吞吐量、允许停顿时间超过 1 秒

  • 并发收集器:多 CPU、追求低停顿时间、快速响应(比如延迟不能超过 1 秒,如互联网应用)

  • 官方推荐 G1,性能高。现在互联网的项目,基本都是使用 G1

特别说明:

  • 没有最好的收集器,更没有万能的收集器

  • 调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器

4.3.5. GC 日志相关选项

-XX:+PrintGC <==> -verbose:gc  打印简要日志信息
-XX:+PrintGCDetails            打印详细日志信息
-XX:+PrintGCTimeStamps  打印程序启动到GC发生的时间,搭配-XX:+PrintGCDetails使用
-XX:+PrintGCDateStamps  打印GC发生时的时间戳,搭配-XX:+PrintGCDetails使用
-XX:+PrintHeapAtGC  打印GC前后的堆信息,如下图
-Xloggc:<file> 输出GC导指定路径下的文件中

-XX:+TraceClassLoading  监控类的加载
-XX:+PrintGCApplicationStoppedTime  打印GC时线程的停顿时间
-XX:+PrintGCApplicationConcurrentTime  打印垃圾收集之前应用未中断的执行时间
-XX:+PrintReferenceGC 打印回收了多少种不同引用类型的引用
-XX:+PrintTenuringDistribution  打印JVM在每次MinorGC后当前使用的Survivor中对象的年龄分布
-XX:+UseGCLogFileRotation 启用GC日志文件的自动转储
-XX:NumberOfGCLogFiles=1  设置GC日志文件的循环数目
-XX:GCLogFileSize=1M  设置GC日志文件的大小

4.3.6. 其他参数

-XX:+DisableExplicitGC  禁用hotspot执行System.gc(),默认禁用
-XX:ReservedCodeCacheSize=<n>[g|m|k]、-XX:InitialCodeCacheSize=<n>[g|m|k]  指定代码缓存的大小
-XX:+UseCodeCacheFlushing  放弃一些被编译的代码,避免代码缓存被占满时JVM切换到interpreted-only的情况
-XX:+DoEscapeAnalysis  开启逃逸分析
-XX:+UseBiasedLocking  开启偏向锁
-XX:+UseLargePages  开启使用大页面
-XX:+PrintTLAB  打印TLAB的使用情况
-XX:TLABSize  设置TLAB大小

4.4. 通过 Java 代码获取 JVM 参数

Java 提供了 java.lang.management 包用于监视和管理 Java 虚拟机和 Java 运行时中的其他组件,它允许本地或远程监控和管理运行的 Java 虚拟机。其中 ManagementFactory 类较为常用,另外 Runtime 类可获取内存、CPU 核数等相关的数据。通过使用这些 api,可以监控应用服务器的堆内存使用情况,设置一些阈值进行报警等处理。

public class MemoryMonitor {public static void main(String[] args) {MemoryMXBean memorymbean = ManagementFactory.getMemoryMXBean();MemoryUsage usage = memorymbean.getHeapMemoryUsage();System.out.println("INIT HEAP: " + usage.getInit() / 1024 / 1024 + "m");System.out.println("MAX HEAP: " + usage.getMax() / 1024 / 1024 + "m");System.out.println("USE HEAP: " + usage.getUsed() / 1024 / 1024 + "m");System.out.println("\nFull Information:");System.out.println("Heap Memory Usage: " + memorymbean.getHeapMemoryUsage());System.out.println("Non-Heap Memory Usage: " + memorymbean.getNonHeapMemoryUsage());
​System.out.println("=======================通过java来获取相关系统状态============================ ");System.out.println("当前堆内存大小totalMemory " + (int) Runtime.getRuntime().totalMemory() / 1024 / 1024 + "m");// 当前堆内存大小System.out.println("空闲堆内存大小freeMemory " + (int) Runtime.getRuntime().freeMemory() / 1024 / 1024 + "m");// 空闲堆内存大小System.out.println("最大可用总堆内存maxMemory " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "m");// 最大可用总堆内存大小
​}
}

5. 分析 GC 日志

5.1. GC 分类

针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)

  • 部分收集(Partial GC):不是完整收集整个 Java 堆的垃圾收集。其中又分为:

    • 新生代收集(Minor GC / Young GC):只是新生代(Eden / S0, S1)的垃圾收集

    • 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。目前,只有 CMS GC 会有单独收集老年代的行为。 注意,很多时候 Major GC 会和 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收。

  • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前,只有 G1 GC 会有这种行为

  • 整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集。

5.2. GC 日志分类

对于java应用我们可以通过一些配置把程序运行过程中的gc日志全部打印出来,然后分析gc日志得到关键性指标,分析 GC原因,调优JVM参数。 打印GC日志方法,在JVM参数里增加参数,%t 代表时间.

java -jar -Xloggc:./gc-%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M microservice-eureka-server.jar
我们截取一部分JVM刚启动的一部分GC日志:

我们可以看到图中第一行红框,是项目的配置参数。这里不仅配置了打印GC日志,还有相关的VM内存参数。 第二行红框中的是在这个GC时间点发生GC之后相关GC情况。 1.对于2.909: 这是从jvm启动开始计算到这次GC经过的时间,前面还有具体的发生时间日期。

  1. Full GC(Metadata GC Threshold)指这是一次full gc,括号里是gc的原因, PSYoungGen是年轻代的GC, ParOldGen是老年代的GC,Metaspace是元空间的GC。

  2. 6160K->0K(141824K),这三个数字分别对应GC之前占用年轻代的大小,GC之后年轻代占用,以及整个年轻代的大 小。

  3. 112K->6056K(95744K),这三个数字分别对应GC之前占用老年代的大小,GC之后老年代占用,以及整个老年代的 大小。

  4. 6272K->6056K(237568K),这三个数字分别对应GC之前占用堆内存的大小,GC之后堆内存占用,以及整个堆内存 的大小。

  5. 20516K->20516K(1069056K),这三个数字分别对应GC之前占用元空间内存的大小,GC之后元空间内存占用,以及整个元空间内存的大小。

  6. 0.0209707是该时间点GC总耗费时间。

从日志可以发现几次fullgc都是由于元空间不够导致的,所以我们可以将元空间调大点,也就是在-jar后面再加一下参数:

‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M

然后重新执行,调整完我们再看下gc日志发现已经没有因为元空间不够导致的fullgc了。

对于CMS和G1收集器设置的参数会不一样,输出的日志会有一点不一样,也可以试着打印下对应的gc日志分析下,可以发现gc日志里面的gc步骤跟 我们之前讲过的步骤是类似的。CMS的是:

‐Xloggc:d:/gc‐cms‐%t.log ‐Xms50M ‐Xmx50M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:+PrintGCDetails‐XX:+P rintGCDateStamps ‐XX:+PrintGCTimeStamps ‐XX:+PrintGCCause ‐XX:+UseGCLogFileRotation ‐XX:NumberOfGCLogFiles=10 ‐XX:GCLogFileSize=100M ‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC

G1的是:

‐Xloggc:d:/gc‐g1‐%t.log ‐Xms50M ‐Xmx50M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:+PrintGCDetails ‐XX:+Pr intGCDateStamps‐XX:+PrintGCTimeStamps ‐XX:+PrintGCCause ‐XX:+UseGCLogFileRotation ‐XX:NumberOfGCLogFiles=10 ‐XX:GCLogFileSize=100M ‐XX:+UseG1GC

上面的这些参数,能够帮我们查看分析GC的垃圾收集情况。但是如果GC日志很多很多,成千上万行。就算你一目十行, 看完了,脑子也是一片空白。所以我们可以借助一些功能来帮助我们分析,这里推荐一个gceasy(Universal JVM GC analyzer - Java Garbage collection log analysis made easy),可以 上传gc文件,然后他会利用可视化的界面来展现GC情况。具体下图所示

上图我们可以看到年轻代,老年代,以及永久代的内存分配,和最大使用情况。

上图我们可以看到堆内存在GC之前和之后的变化,以及其他信息。 这个工具还提供基于机器学习的JVM智能优化建议,当然现在这个功能需要付费。

一文搞定JVM常见工具和优化策略相关推荐

  1. 一文搞定JVM的内存结构

    目录 1.简介 2.程序计数器(PC寄存器) 2.1 功能演示 2.2 关于PC的面试题 3.虚拟机栈 3.1 初识虚拟机栈 3.2 栈帧的内部结构 3.2.1 局部变量表 3.2.2 操作数栈 3. ...

  2. 干货|一文搞定 uiautomator2 自动化测试工具使用

    一.背景简介 Google 官方提供了一个 Android 自动化测试工具(Java 库),基于 Accessibility 服务,功能很强,可以对第三方 App 进行测试,获取屏幕上任意一个 App ...

  3. 一文搞定 JVM 面试,教你吊打面试官~

    1.什么是类加载?类加载的过程? 类的加载指的是将类的class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个此类的对象,通过这个对象可以访问到方法区对应的类信息. ...

  4. 最强绘图AI:一文搞定Midjourney(附送咒语)

    最强绘图AI:一文搞定Midjourney(附送咒语) Midjourney官网:https://www.midjourney.com 简介 Midjourney是目前效果最棒的AI绘图工具.访问Mi ...

  5. php带参数单元测试_一文搞定单元测试核心概念

    基础概念 单元测试(unittesting),是指对软件中的最小可测试单元进行检查和验证,这里的最小可测试单元通常是指函数或者类.单元测试是即所谓的白盒测试,一般由开发人员负责测试,因为开发人员知道被 ...

  6. 一文搞定Qt读写excel以及qt读写xml数据

    一文搞定Qt读写excel以及qt读写xml数据 最终的实现效果图 RC_ICONS = logo.ico .pro文件同级目录下加入 logo.ico 图标文件,运行文件,文件的图标就被写入软件 u ...

  7. 一文搞定B站弹幕生成云图

    一文搞定B站弹幕生成云图 最近学了词云图, 感觉非常有趣,我们做一些图试试, 最近我们遭受了新冠疫情 , 关于这个点,去看看b站弹幕都在说什么? B站的弹幕接口 直接从B站某视频源中找半天没找到弹幕的 ...

  8. 【语义分割】综述——一文搞定语义分割

    本文记录了博主阅读的关于语义分割(Semantic Segmentation)的综述类文章的笔记,更新于2019.02.19. [语义分割]综述--一文搞定语义分割 参考文献网址 An overvie ...

  9. 一文搞定Python中的时间转化

    一文搞定Python中的时间转化 在生活和工作中,我们每个人每天都在和时间打交道: 早上什么时候起床? 地铁几分钟来一趟? 中午什么时候开始午休? 明天是星期几? 距离上次买衣服已经2个月呢? 领导让 ...

最新文章

  1. 命令模式的优点?_一篇目录复习完设计模式
  2. 深度学习领域最常用的10个激活函数,一文详解数学原理及优缺点
  3. GET请求中URL的最大长度限制总结
  4. 第二讲,我们来谈谈:“什么是二进制”
  5. HitFilm Pro 12中文版
  6. 简单XML文件C#操作方法
  7. python实战系列之爬取CSDN博客之星2020年度排名情况(附源码)
  8. 以太坊钱包_最大的以太坊钱包币数量还在增加
  9. FireDAC 中文字段过滤问题
  10. 深度学习常用性能评价指标
  11. Win7蓝屏代码 0x0000007B
  12. 【内核调度、负载均衡】【find_busiest_queue】
  13. Android手机上,利用bat脚本模拟用户操作
  14. 黑色主题的个人引导页,导航页html php源码
  15. 学习笔记,C,n+nn+nnn+nnnn+nnnnn
  16. 自动驾驶系统进阶与项目实战(三)基于全卷积神经网络的点云三维目标检测和ROS实战
  17. 为什么要用Linux系统?
  18. pytest合集(4)— 使用pytest-html插件生成HTML测试报告
  19. 曾经的京东001号员工,跟了刘强东18年,现在他的境况到底如何?
  20. Navicat远程连接MySQL服务器

热门文章

  1. 左边是地狱右边也是地狱_像我这样的设计师的特别地狱
  2. 多重背包问题和“二进制拆分”
  3. LeetCode 39 组合总和
  4. Discuz3.数据库数据表字典详解(完整版)
  5. 项目设计-基于SpringBoot和Vue开发的宿舍管理系统
  6. 基于matlab的手写体数字识别系统
  7. java时间戳 时间格式转换与时差
  8. Ubuntu18.04 64 位系统 安装32位支持库
  9. 正则系列2: re.search用法
  10. 基于C++的数据结构-1