1. 常用命令
在日常工作中,我们可以使用一些命令来帮助我们快速定位JVM中的问题。
1.1 JPS
查看java进程
The jps command lists the instrumented Java HotSpot VMs on the target system.
The command is limited to reporting information on JVMs for which it has the access permissions.
1.2 jinfo
-
实时查看和调整JVM配置参数
The jinfo command prints Java configuration information for a specified Java process or core file or a remote debug server. The configuration information includes Java system properties and Java Virtual Machine (JVM) command-line flags.
-
查看用法
jinfo -flag name PID 查看某个java进程的name属性的值
jinfo -flag MaxHeapSize PID
jinfo -flag UseG1GC PID
-
修改
参数只有被标记为manageable的flags可以被实时修改
jinfo -flag [+|-] PID
jinfo -flag <name>=<value> PID
- 查看曾经赋过值的一些参数
jinfo -flags PID
1.3 jstat
-
查看虚拟机性能统计信息
The jstat command displays performance statistics for an instrumented Java HotSpot VM. The target JVM is identified by its virtual machine identifier, or vmid option.
-
查看类装载信息
jstat -class PID 1000 10 查看某个java进程的类装载信息,每1000毫秒输出一次,共输出10次
- 查看垃圾收集信息
jstat -gc PID 1000 10
1.4 jstack
-
查看线程堆栈信息
The jstack command prints Java stack traces of Java threads for a specified Java process, core file, or remote debug server.
-
用法
jstack PID
- 排查死锁案例
-
代码案例
public class DeadLockDemo { public static void main(String[] args) { DeadLock d1 = new DeadLock(true); DeadLock d2 = new DeadLock(false); Thread t1 = new Thread(d1); Thread t2 = new Thread(d2); t1.start(); t2.start(); } } //定义锁对象 class MyLock { public static Object obj1 = new Object(); public static Object obj2 = new Object(); } //死锁代码 class DeadLock implements Runnable { private boolean flag; DeadLock(boolean flag) { this.flag = flag; } public void run() { if (flag) { while (true) { synchronized (MyLock.obj1) { System.out.println(Thread.currentThread().getName() + "----if 获得obj1锁"); synchronized (MyLock.obj2) { System.out.println(Thread.currentThread().getName() + "--- -if获得obj2锁"); } } } } else { while (true) { synchronized (MyLock.obj2) { System.out.println(Thread.currentThread().getName() + "----否则 获得obj2锁"); synchronized (MyLock.obj1) { System.out.println(Thread.currentThread().getName() + "--- -否则获得obj1锁"); } } } } } }
-
运行结果
-
jps查看进程ID
-
jstack分析
我们把打印信息拉到最后可以发现找到了死锁。
1.5 jmap
-
生成堆转储快照
The jmap command prints shared object memory maps or heap memory details of a specified process, core file, or remote debug server.
-
打印出堆内存相关信息
jmap -heap PID
- dump出堆内存相关信息
jmap -dump:format=b,file=heap.hprof PID
-
要是在发生堆内存溢出的时候,能自动dump出该文件就好了
一般在开发中,JVM参数可以加上下面两句,这样内存溢出时,会自动dump出该文件:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
-
关于dump下来的文件
一般dump下来的文件可以结合工具来分析,这块后面再说。
2. 执行引擎
我们创建一个Person.java 文件 ,javac编译器将Person.java源码文件编译成class文件[我们把这里的编译称为前期编译],交给JVM运行,因为JVM只能认识class字节码文件。同时在不同的操作系统上安装对应版本的JDK,里面包含了各自屏蔽操作系统底层细节的JVM,这样同一份class文件就能运行在不同的操作系统平台之上,得益于JVM。这也 是Write Once,Run Anywhere的原因所在。
最终JVM需要把字节码指令转换为机器码,可以理解为是0101这样的机器语言,这样才能运行在不同的机器上,那么由字节码转变为机器码是谁来做的呢?说白了就是谁来执行这些字节码指令的呢?这就是执行引擎。
2.1 解释执行
Interpreter,解释器逐条把字节码翻译成机器码并执行,跨平台的保证。
刚开始执行引擎只采用了解释执行的,但是后来发现某些方法或者代码块被调用执行的特别频繁时,就会把这些代码认定为“热点代码”。
2.2 即时编译器
Just-In-Time compilation(JIT),即时编译器先将字节码编译成对应平台的可执行文件,运行速度快。即时编译器会把这些热点代码编译成与本地平台关联的机器码,并且进行各层次的优化,保存到内存中。
2.3 JVM采用哪种方式
JVM采取的是混合模式,也就是解释+编译的方式,对于大部分不常用的代码,不需要浪费时间将其编译成机器码,只需要用到的时候再以解释的方式运行;对于小部分的热点代码,可以采取编译的方式,追求更高的运行效率。
2.4 即时编译器类型
-
HotSpot虚拟机里面内置了两个JIT:C1和C2;
C1也称为Client Compiler,适用于执行时间短或者对启动性能有要求的程序;
C2也称为Server Compiler,适用于执行时间长或者对峰值性能有要求的程序;
-
Java7开始,HotSpot会使用分层编译的方式;
也就是会结合C1的启动性能优势和C2的峰值性能优势,热点方法会先被C1编译,然后热点方法中的热点会被 C2再次编译
2.5 AOT和Graal VM
2.5.1 AOT
在Java9中,引入了AOT(Ahead-Of-Time)编译器
即时编译器是在程序运行过程中,将字节码翻译成机器码。而AOT是在程序运行之前,将字节码转换为机器码。
优势 :这样不需要在运行过程中消耗计算机资源来进行即时编译
劣势 :AOT 编译无法得知程序运行时的信息,因此也无法进行基于类层次分析的完全虚方法内联,或者基于程序 profile 的投机性优化(并非硬性限制,我们可以通过限制运行范围,或者利用上一次运行的程序 profile 来绕开这两个限制)
2.5.2 Graal VM
在Java10中,新的JIT编译器Graal被引入。
它是一个以Java为主要编程语言,面向字节码的编译器。跟C++实现的C1和C2相比,模块化更加明显,也更加容易维护。
Graal既可以作为动态编译器,在运行时编译热点方法;也可以作为静态编译器,实现AOT编译。
除此之外,它还移除了编程语言之间的边界,并且支持通过即时编译技术,将混杂了不同的编程语言的代码编译到同一段二进制码之中,从而实现不同语言之间的无缝切换。
3. 工具
了解常用命令及执行引擎后,我们来看下常用的JVM工具。
3.1 jconsole
JConsole工具是JDK自带的可视化监控工具。查看java应用程序的运行概况、监控堆信息、永久区使用情况、类加载情况等。
我们只需要在命令行中输入:jconsole
3.2 jvisualvm
在命令行中输入:jvisualvm
Visual GC插件下载地址 :https://visualvm.github.io/pluginscenters.html
3.2.1 监控本地Java进程
可以监控本地的java进程的CPU,类,线程等;
3.2.2 监控远端Java进程
-
在visualvm中选中“远程”,右击“添加”
-
主机名上写服务器的ip地址,然后点击“确定”
-
右击该主机,添加“JMX”,也就是通过JMX技术具体监控远端服务器哪个Java进程
-
要想让服务器上的tomcat被连接,需要改一下Catalina.sh这个文件
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote - Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.port=8998 -Dcom.sun.management.jmxremote.ssl=false - Dcom.sun.management.jmxremote.authenticate=true - Dcom.sun.management.jmxremote.access.file=../conf/jmxremote.access - Dcom.sun.management.jmxremote.password.file=../conf/jmxremote.password"
-
在…/conf文件中添加两个文件jmxremote.access和jmxremote.password
jmxremote.access
guest readonly manager readwrite
jmxremote.password
guest guest manager manager
授予权限:chmod 600 jmxremot
-
将连接服务器地址改为公网ip地址
hostname -i 查看输出情况 172.26.225.240 172.17.0.1 vim /etc/hosts 172.26.255.240 39.100.39.63
-
设置上述端口对应的阿里云安全策略和防火墙策略
-
启动tomcat,来到bin目录
./startup.sh
-
查看tomcat启动日志以及端口监听
tail -f ../logs/catalina.out lsof -i tcp:8080
-
查看8998监听情况,可以发现多开了几个端口
lsof -i:8998 得到PID netstat -antup | grep PID
-
在刚才的JMX中输入8998端口,并且输入用户名和密码则登录成功
端口:8998 用户名:manager 密码:manager
3.2.3 arthas
下载安装
curl -O https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar
or
java -jar arthas-boot.jar -h
# 然后可以选择一个Java进程
常用命令
具体每个命令怎么使用,大家可以自己查阅资料
version:查看arthas版本号
help:查看命名帮助信息
cls:清空屏幕
session:查看当前会话信息
quit:退出arthas客户端
---
dashboard:当前进程的实时数据面板
thread:当前JVM的线程堆栈信息
jvm:查看当前JVM的信息
sysprop:查看JVM的系统属性
---
sc:查看JVM已经加载的类信息
dump:dump已经加载类的byte code到特定目录
jad:反编译指定已加载类的源码
---
monitor:方法执行监控
watch:方法执行数据观测
trace:方法内部调用路径,并输出方法路径上的每个节点上耗时
stack:输出当前方法被调用的调用路径
.........
3.3 内存分析工具
3.3.1 MAT
-
获取dump文件
-
手动
jmap -dump:format=b,file=heap.hprof PID
-
自动 (启动的时候添加参数)
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
-
-
Dump的信息
-
使用
-
Histogram:可以列出内存中的对象,对象的个数及其大小
右击类名—>List Objects—>with incoming references—>列出该类的实例 ;
右击Java对象名—>Merge Shortest Paths to GC Roots—>exclude all …—>找到GC Root以及原因;
JVM中GC Roots的大致分类:
-
Leak Suspects:查找并分析内存泄漏的可能原因
Reports—>Leak Suspects—>Details
-
Top Consumers:列出大对象
-
3.3.2 heaphero
https://heaphero.io/
3.3.3 perfma
https://console.perfma.com/
3.4 GC日志分析工具
要想分析日志的信息,得先拿到GC日志文件才行,所以得先配置一下,根据前面参数的学习,下面的配置很容易看懂。比如打开windows中的catalina.bat,在第一行加上:
XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:$CATALINA_HOME/logs/gc.log
这样使用startup.bat启动tomcat的时候就能够在当前目录下拿到gc.log文件,可以看到默认使用的是ParallelGC。
3.4.1 gceasy
GCeasy是一款在线的GC日志分析器,可以通过GC日志分析进行内存泄露检测、GC暂停原因分析、JVM配置建议优化等功能,而且是可以免费使用的(有一些服务是收费的)。
http://gceasy.io
3.4.2 gcplot
https://it.gcplot.com/
3.4.3 GCViewer
上面介绍了一款在线的GC日志分析器,下面介绍一个离线版的GCViewer,其最新版本为1.36,我用的就是这个版本,需要jdk1.8才可以使用,Github地址为https://github.com/chewiebug/GCViewer,下载下来之后执行 mvn clean install -Dmaven.test.skip=true 命令进行编译,编译完成后在target目录下会看到jar包,双击打开即可。
4. 性能优化
JVM的性能优化可以分为代码层面和非代码层面。
在代码层面,大家可以结合字节码指令进行优化,比如一个循环语句,可以将循环不相关的代码提取到循环体之外,这样在字节码层面就不需要重复执行这些代码了。
在非代码层面,一般情况可以从内存、gc以及cpu占用率等方面进行优化。
注意,JVM调优是一个漫长和复杂的过程,而在很多情况下,JVM是不需要优化的,因为JVM本身已经做了很多的内部优化操作。
那今天我们就从内存、gc以及cpu这3个方面和大家一起探讨一下JVM的优化,但是大家要注意的是不要为了调优而调优,因为我们自己调优可能真不如JVM本身的调优。
4.1 内存
4.1.1 内存分配
正常情况下不需要设置,那如果是促销或者秒杀的场景呢?
每台机器配置2c4G,以每秒3000笔订单为例,整个过程持续60秒。
具体流程以及解决方法大家可以观看下图。
4.1.2 内存溢出(OOM)
出现内存溢出一般会有两个原因:
-
大并发情况下
-
内存泄露导致内存溢出
大并发[秒杀]
- 出现大并发的话,我们可以从如下方面进行优化解决:
- 浏览器缓存、本地缓存、验证码;
- CDN静态资源服务器;
- 集群+负载均衡;
- 动静态资源分离、限流[基于令牌桶、漏桶算法];
- 应用级别缓存、接口防刷限流、队列、Tomcat性能优化;
- 异步消息中间件;
- Redis热点数据对象缓存;
- 分布式锁、数据库锁;
- 5分钟之内如果没有支付,取消订单、恢复库存等;
内存泄露导致内存溢出
针对这种情况我们可以自行创建一个测试案例,使用ThreadLocal引起内存泄露,最终导致内存溢出。
public class TLController {
@RequestMapping(value = "/tl")
public String tl(HttpServletRequest request) {
ThreadLocal<Byte[]> tl = new ThreadLocal<Byte[]>(); // 1MB
tl.set(new Byte[1024*1024]); return "ok";
}
}
-
将JAR包上传到阿里云服务器
-
启动 需要自动输出dump文件
java -jar -Xms1000M -Xmx1000M -XX:+HeapDumpOnOutOfMemoryError - XX:HeapDumpPath=jvm.hprof jvm-case-0.0.1-SNAPSHOT.jar
-
使用jmeter模拟10000次并发
-
top命令查看
-
jstack查看线程情况,发现没有死锁或者IO阻塞的情况
jstack PID java -jar arthas.jar ---> thread
-
查看堆内存的使用,发现堆内存的使用率已经高达88.95%
jmap -heap PID java -jar arthas.jar ---> dashboard
-
此时可以大体判断出来,发生了内存泄露从而导致的内存溢出,那怎么排查呢?
jmap -histo:live PID | more 获取到jvm.hprof文件,上传到指定的工具分析,比如heaphero.io
4.2 GC
这里以G1垃圾收集器调优为例;
4.2.1 是否选用G1
官网 :https://docs.oracle.com/javase/8/docs/technotes/guides/vm/G1.html#use_cases
-
50%以上的堆被存活对象占用
-
对象分配和晋升的速度变化非常大
-
垃圾回收时间比较长
4.2.2 G1调优
-
使用G1GC垃圾收集器: -XX:+UseG1GC
修改配置参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间
-
调整内存大小再获取gc日志分析
-XX:MetaspaceSize=100M -Xms300M -Xmx300M
-
调整最大停顿时间
-XX:MaxGCPauseMillis=200 设置最大GC停顿时间指标
-
启动并发GC时堆内存占用百分比
-XX:InitiatingHeapOccupancyPercent=45
G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。值为 0 则表示“一直执行GC循环)’. 默认值为 45 (例如, 全部的 45% 或者使用了45%).
4.2.3 G1调优最佳实战
官网 :https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations)
-
不要手动设置新生代和老年代的大小,只要设置整个堆的大小
G1收集器在运行过程中,会自己调整新生代和老年代的大小;
其实是通过adapt代的大小来调整对象晋升的速度和年龄,从而达到为收集器设置的暂停时间目标 ;
如果手动设置了大小就意味着放弃了G1的自动调优 ;
-
不断调优暂停时间目标
一般情况下这个值设置到100ms或者200ms都是可以的(不同情况下会不一样),但如果设置成50ms就不太合理。暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。暂停时间只是一个目标,并不能总是得到满足。
-
使用-XX:ConcGCThreads=n来增加标记线程的数量
IHOP如果阀值设置过高,可能会遇到转移失败的风险,比如对象进行转移时空间不足。如果阀值设置过低,就会使标记周期运行过于频繁,并且有可能混合收集期回收不到空间。IHOP值如果设置合理,但是在并发周期时间过长时,可以尝试增加并发线程数,调高ConcGCThreads。
-
MixedGC调优
-XX:InitiatingHeapOccupancyPercent -XX:G1MixedGCLiveThresholdPercent -XX:G1MixedGCCountTarger -XX:G1OldCSetRegionThresholdPercent
-
适当增加堆内存大小
-
不正常的Full GC
有时候会发现系统刚刚启动的时候,就会发生一次Full GC,但是老年代空间比较充足,一般是由 Metaspace区域引起的。可以通过MetaspaceSize适当增加其空间,比如256M。
4.3 CPU占用率高
-
top
-
top -Hp PID
-
jstack PID | grep tid
5. JVM性能优化指南
6. 常见问题
-
内存泄漏与内存溢出的区别
内存泄漏是指不再使用的对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。
内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。
-
young gc会有stw吗?
不管什么 GC,都会发送 stop-the-world,区别是发生的时间长短。而这个时间跟垃圾收集器又有关系,Serial、PartNew、Parallel Scavenge 收集器无论是串行还是并行,都会挂起用户线程,而 CMS和 G1 在并发标记时,是不会挂起用户线程的,但其它时候一样会挂起用户线程,stop the world 的时间相对来说就小很多了。
-
major gc和full gc的区别
Major GC在很多参考资料中是等价于 Full GC 的,我们也可以发现很多性能监测工具中只有 Minor GC 和 Full GC。一般情况下,一次 Full GC 将会对年轻代、老年代、元空间以及堆外内存进行垃圾回收。触发 Full GC 的原因有很多:当年轻代晋升到老年代的对象大小,并比目前老年代剩余的空间大小还要大时,会触发 Full GC;当老年代的空间使用率超过某阈值时,会触发 Full GC;当元空间不足时(JDK1.7永久代不足),也会触发 Full GC;当调用 System.gc() 也会安排一次 Full GC。
-
什么是直接内存
Java的NIO库允许Java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
-
垃圾判断的方式
引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为0就会回收但是JVM没有用这种方式,因为无法判定相互循环引用(A引用B,B引用A)的情况。
引用链法: 通过一种GC ROOT的对象(方法区中静态变量引用的对象等-static变量)来判断,如果有一条链能够到达GC ROOT就说明,不能到达GC ROOT就说明可以回收。
-
不可达的对象一定要被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
-
为什么要区分新生代和老年代?
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
-
G1与CMS的区别是什么?
CMS 主要集中在老年代的回收,而 G1 集中在分代回收,包括了年轻代的 Young GC 以及老年代的 Mix GC;
CMS采用标记清楚算法,会产生大量空间碎片;
G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生;在初始化标记阶段,搜索可达对象使用到的 Card Table,其实现方式不一样。
-
方法区中的无用类回收
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。
类需要同时满足下面 3 个条件才能算是 “无用的类” :
a. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
b. 加载该类的 ClassLoader 已经被回收。
c. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。