jvm调优

1 常用命令

1. jps—查看java进程

2. jinfo:

(1)实时查看和调整JVM配置参数

(2)用法:jinfo -flag name PID 查看某个java进程的name属性的值

​ 例:jinfo -flag MaxHeapSize PID

​ jinfo -flag UseG1GC PID

(3)修改:参数只有被标记为manageable的flags可以被实时修改

​ 例:jinfo -flag [+|-] PID

​ jinfo -flag = PID

(4)查看曾今赋过值的一些参数

               jinfo -flags PID

3. jstat

(1)查看虚拟机性能统计信息

(2)查看类装载信息:

​ jstat -class PID 1000 10 查看某个java进程的类装载信息,每1000毫秒输出一次,共输出10次

(3)查看垃圾收集信息 :jstat -gc PID 1000 10

4. jstack

(1)查看线程堆栈信息

(2)用法:jstack PID

(3)排查死锁案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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 Runable {
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() + " -------否则获得obj2的锁")
}
}
}
} else {
while(true){
synchronized(MyLock.obj2){
System.out.printLn(Thread.currentThread().getName() + " -------if获得obj2的锁");
synchronized(MyLock.obj1){
System.out.printLn(Thread.currentThread().getName() + " -------否则获得obj1的锁")
}
}
}
}
}
}

jstack分析:

把打印信息拉到最后发现

5. jmap

(1)生成堆转储快照

(2)打印出堆内存相关信息

​ jmap -heap PID

​ jinfo -flag UsePSAdaptiveSurvivorSizePolicy 35352

​ -XX:SurvivorRatio=8

(3)dump出堆内存相关信息

​ jmap -dump:format=b,file=heap.hprof PID

(4)要是在发生堆内存溢出的时候,能自动dump出该文件就好了

​ 一般在开发中,JVM参数可以加上下面两句,这样内存溢出时,会自动dump出该文件

​ -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof

​ 设置堆内存大小: -Xms20M -Xmx20M

​ 启动,然后访问localhost:9090/heap,使得堆内存溢出

2 执行引擎

Person.java源码文件是Java这门高级开发语言,对程序员友好,方便我们开发。

​ javac编译器将Person.java源码文件编译成class文件[我们把这里的编译称为前期编译],交给JVM运行,因为JVM只能认识class字节码文件。同时在不同的操作系统上安装对应版本的JDK,里面包含了各自屏蔽操作系统底层细节的JVM,这样同一份class文件就能运行在不同的操作系统平台之上,得益于JVM。这也是Write Once,Run Anywhere的原因所在。

​ 最终JVM需要把字节码指令转换为机器码,可以理解为是0101这样的机器语言,这样才能运行在不同的机器上,那么由字节码转变为机器码是谁来做的呢?说白了就是谁来执行这些字节码指令的呢?这就是执行引擎。

1. 解释执行

​ Interpreter,解释器逐条把字节码翻译成机器码并执行,跨平台的保证。 刚开始执行引擎只采用了解释执行的,但是后来发现某些方法或者代码块被调用执行的特别频繁时,就会把这些代码认定为“热点代码”。

2. 即时编译器

​ Just-In-Time compilation(JIT),即时编译器先将字节码编译成对应平台的可执行文件,运行速度快。即时编译器会把这些热点代码编译成与本地平台关联的机器码,并且进行各层次的优化,保存到内存中。

3. JVM采用哪种方式

​ JVM采取的是混合模式,也就是解释+编译的方式,对于大部分不常用的代码,不需要浪费时间将其编译成机器码,只需要用到的时候再以解释的方式运行;对于小部分的热点代码,可以采取编译的方式,追求更高的运行效率。

4. 即时编译器类型

(1)HotSpot虚拟机里面内置了两个JIT:C1和C2

​ C1也称为Client Compiler,适用于执行时间短或者对启动性能有要求的程序

​ C2也称为Server Compiler,适用于执行时间长或者对峰值性能有要求的程序

(2)Java7开始,HotSpot会使用分层编译的方式

​ 也就是会结合C1的启动性能优势和C2的峰值性能优势,热点方法会先被C1编译,然后热点方法中的热点会被C2再次编译。

5. AOT和Graal VM

(1)AOT

​ 在Java9中,引入了AOT(Ahead-Of-Time)编译器,即时编译器是在程序运行过程中,将字节码翻译成机器码。而AOT是在程序运行之前,将字节码转换为机器码。

​ 优势:这样不需要在运行过程中消耗计算机资源来进行即时编译

​ 劣势:AOT 编译无法得知程序运行时的信息,因此也无法进行基于类层次分析的完全虚方法内联,或者基于程序 profile 的投机性优化(并非硬性限制,我们可以通过限制运行范围,或者利用上一次运行的程序 profile 来绕开这两个限制)

(2)Graal VM

​ 官网:https://www.oracle.com/tools/graalvm-enterprise-edition.html

​ 在Java10中,新的JIT编译器Graal被引入

​ 它是一个以Java为主要编程语言,面向字节码的编译器。跟C++实现的C1和C2相比,模块化更加明显,也更加容易维护。

​ Graal既可以作为动态编译器,在运行时编译热点方法;也可以作为静态编译器,实现AOT编译。

​ 除此之外,它还移除了编程语言之间的边界,并且支持通过即时编译技术,将混杂了不同的编程语言的代码编译到同一段二进制码之中,从而实现不同语言之间的无缝切换。

3 工具

3.1 jconsole

jconsole工具是JDK自带的可视化监控工具。查看java应用程序的运行概况、监控堆信息、永久区使用情况、类加载情况等。命令行中输入:jconsole。

3.2 jvisualvm

命令行中输入:jvisualvm。Visual GC插件下载地址:https://visualvm.github.io/pluginscenters.html。

  1. 监控本地进程:可以监控本地的java进程的CPU,类,线程等

  2. 监控远端java进程:

​ 1)在visualvm中选中“远程”,右击“添加”

​ 2)主机名上写服务器的ip地址,比如39.100.39.63,然后点击“确定”

​ 3)右击该主机”39.100.39.63”,添加“JMX”,就是通过JMX技术具体监控远端服务器哪个Java进程

​ 4)要想让服务器上的tomcat被连接,需要改一下Catalina.sh这个文件。

​ 注意下面的8998不要和服务器上其他端口冲突。

​ 5)在../conf文件中添加两个文件jmxremote.access和jmxremote.password

​ jmxremote.access:guest readonly

​ manager readwrite

​ jmxremote.password:guest guest

​ manager manager

​ 授予权限:chmod 600 jmxremot

    6)将连接服务器地址改为公网ip地址

​ 7)设置上述端口对应的阿里云安全策略和防火墙策略

    8)启动tomcat,来到bin目录:./startup.sh
 
  9)查看tomcat启动日志以及端口监听

​ tail -f ../logs/catalina.out lsof -i tcp:8080

​ 10)查看8998监听情况,可以发现多开了几个端口

​ lsof -i:8998 得到PID netstat -antup | grep PID

    11)在刚才的JMX中输入8998端口,并且输入用户名和密码则登录成功

3.3 arthas

https://github.com/alibaba/arthas

Arthas 是Alibaba开源的Java诊断工具,采用命令行交互模式,是排查jvm相关问题的利器。

(1)下载安装

1
2
3
4
5
curl -o https://alibaba.github.io/arthas/arthas-boot.jar
java -jar artjas-boot.jar
or
java -jar arthas-boot.jar -h
# 然后可以选择一个java进程

(2)常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
version:查看版本号
help:查看命名帮助信息
cls:清空屏幕
session:查看当前会话信息
quit:推出客户端
-----
dashboard:当前进程的实时数据面板
thread:当前JVM的线程堆栈信息
jvm:查看当前JVM的信息
sysprop:查看JVM的系统属性
-----
sc:查看JVM已经加载的类信息
dump:dump已经加载类的byte code到指定目录
jad:反编译指定已加载类的源码
-----
monitor:方法执行监控
watch:方法执行数据观测
trace:方法内部调用路径,并输出方法路径上的每个节点上耗时
stack:输出当前方法被调用的调用路径
...

3.4 内存分析

  1. MAT

​ Java堆分析器,用于查找内存泄漏。Heap Dump,称为堆转储文件,是Java进程在某个时间内的快照。它在触发快照的时候保存了很多信息:Java对象和类信息。通常在写Heap Dump文件前会触发一次Full GC。

下载地址 :https://www.eclipse.org/mat/downloads.php

1)获取dump文件

​ 手动:jmap -dump:format=b,file=heap.hprof 44808

​ 自动:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof

2)使用

Histogram:可以列出内存中的对象,对象的个数及其大小

​ Class Name:类名称,java类名

​ Objects:类的对象的数量,这个对象被创建了多少个

​ Shallow Heap:一个对象内存的消耗大小,不包含对其他对象的引用

​ Retained Heap:是shallow Heap的总和,即该对象被GC之后所能回收到内存的总和

​ 右击类名—>List Objects—>with incoming references—>列出该类的实例

​ 右击Java对象—>Merge Shortest Paths to GC Roots—>exclude all …—>找到GC Root以及原因

Leak Suspects:查找并分析内存泄漏的可能原因

Top Consumers:列出大对象

3.5 GC日志分析

要想分析日志的信息,得先拿到GC日志文件才行。比如打开windows中的catalina.bat,在第一行加上

​ XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps

​ -Xloggc:$CATALINA_HOME/logs/gc.log

1.不同收集器的日志

​ 这样使用startup.bat启动tomcat的时候就能够在当前目录下拿到gc.log文件,可以看到默认使用的是ParallelGC。

​ (1)Parallel GC 吞吐量优先

​ 【2019-06-10T23:21:53.305+0800: 1.303: [GC (Allocation Failure) [PSYoungGen:65536K[Young区回 收前]->10748K[Young区回收后]76288K[Young区总大小]] 65536K[整个堆回收前]->15039K[整个堆回 收后]251392K[整个堆总大小], 0.0113277 secs] [Times:user=0.00 sys=0.00, real=0.01 secs]】

​ 注意如果回收的差值中间有出入,说明这部分空间是Old区释放出来的。

(2)CMS 停顿时间优先

​ 参数设置:-XX:+UseConcMarkSweepGC -Xloggc:cms-gc.log

​ 重启tomcat获取gc日志,这里的日志格式和上面差不多,不作分析。

​ (3)G1 停顿时间优先

​ G1日志格式参考链接:https://blogs.oracle.com/poonam/understanding-g1-gc-logs。

​ 参数设置:-XX:+UseG1GC -Xloggc:g1-gc.log

-XX:+UseG1GC # 使用了G1垃圾收集器

​ # 什么时候发生的GC,相对的时间刻,GC发生的区域young,总共花费的时间,0.00478s,

​ # It is a stop-the-world activity and all

​ # the application threads are stopped at a safepoint during this time.

​ 2019-12-18T16:06:46.508+0800: 0.458: [GC pause (G1 Evacuation Pause)(young), 0.0047804 secs]

​ # 多少个垃圾回收线程,并行的时间

​ [Parallel Time: 3.0 ms, GC Workers: 4]

​ # GC线程开始相对于上面的0.458的时间刻

​ [GC Worker Start (ms): Min: 458.5, Avg: 458.5, Max: 458.5, Diff: 0.0]

​ # This gives us the time spent by each worker thread scanning the roots

​ # (globals, registers, thread stacks and VM data structures).

​ [Ext Root Scanning (ms): Min: 0.2, Avg: 0.4, Max: 0.7, Diff: 0.5, Sum: 1.7]

​ # Update RS gives us the time each thread spent in updating the RememberedSets.

​ [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]

​ …

​ # 主要是Eden区变大了,进行了调整

​ [Eden: 14.0M(14.0M)->0.0B(16.0M) Survivors: 0.0B->2048.0K Heap: 14.0M(256.0M)->3752.5K(256.0M)]

  1. GCViewer

​ java -jar gcviewer-1.36-SNAPSHOT.jar

  1. gceasy : http://gceasy.io

  2. gcplot https://it.gcplot.com/

4 性能优化

4.1 内存

  1. 内存分配

​ 正常情况下不需要设置,那如果是促销或者秒杀的场景呢?

​ 每台机器配置2c4G,以每秒3000笔订单为例,整个过程持续60秒

  1. 内存溢出,一般会有两个原因:

​ (1)大并发情况下

​ (2)内存泄露导致内存溢出

​ 3. 大并发【秒杀】

​ 浏览器缓存、本地缓存、验证码

​ CDN静态资源服务器

​ 集群+负载均衡

​ 动静态资源分离、限流[基于令牌桶、漏桶算法]

​ 应用级别缓存、接口防刷限流、队列、Tomcat性能优化

​ 异步消息中间件

​ Redis热点数据对象缓存

​ 分布式锁、数据库锁

​ 5分钟之内没有支付,取消订单、恢复库存等

​ 4. 内存泄漏导致内存溢出

​ ThreadLocal引起的内存泄露,最终导致内存溢出

1
2
3
4
5
6
7
8
public class TLController {
@RequestMapping("/tl")
public String tl(HttpServletRequest request){
ThreadLocal<Byte[]> tl = new ThreadLocal<>();
tl.set(new Byte[1024*1024]);
return "OK";
}
}

​ (1)上传到阿里云服务器

​ jvm-case-0.0.1-SNAPSHOT.jar

​ (2)启动

​ java -jar -Xms1000M -Xmx1000M -XX:+HeapDumpOnOutOfMemoryError -

​ XX:HeapDumpPath=jvm.hprof jvm-case-0.0.1-SNAPSHOT.jar

​ (3)使用jmeter模拟10000次并发

​ 39.100.39.63:8080/tl

​ (4)top命令查看

​ top

​ top -Hp PID

​ (5)jstack查看线程情况,发现没有死锁或者IO阻塞的情况

​ jstack PID

​ java -jar arthas.jar —> thread

​ (6)查看堆内存的使用,发现堆内存的使用率已经高达88.95%

​ jmap -heap PID

​ java -jar arthas.jar —> dashboard

​ (7)此时可以大体判断出来,发生了内存泄露从而导致的内存溢出,那怎么排查呢?

​ jmap -histo:live PID | more

​ 获取到jvm.hprof文件,上传到指定的工具分析,比如heaphero.io

4.2 GC(这里以G1垃圾收集器调优为例)

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/G1.html#use_cases

​ (1)50%以上的堆被存活对象占用

​ (2)对象分配和晋升的速度变化非常大

​ (3)垃圾回收时间比较长

G1调优

(1)使用G1GC垃圾收集器: -XX:+UseG1GC

修改配置参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间

(2)调整内存大小再获取gc日志分析

​ -XX:MetaspaceSize=100M

​ -Xms300M

​ -Xmx300M

​ 比如设置堆内存的大小,获取到gc日志,使用GCViewer分析吞吐量和响应时间

(3)调整最大停顿时间

​ -XX:MaxGCPauseMillis=200 设置最大GC停顿时间指标

​ 比如设置最大停顿时间,获取到gc日志,使用GCViewer分析吞吐量和响应时间

(4)启动并发GC时堆内存占用百分比

​ -XX:InitiatingHeapOccupancyPercent=45

​ G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。值为 0 则表示 “一直执行GC循环)’. 默认值为 45 (例如, 全部的 45% 或者使用了45%).

​ 比如设置该百分比参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间

G1调优最佳实战

​ 官网:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations

(1)不要手动设置新生代和老年代的大小,只要设置整个堆的大小

​ why:https://blogs.oracle.com/poonam/increased-heap-usage-with-g1-gc

​ G1收集器在运行过程中,会自己调整新生代和老年代的大小,其实是通过adapt代的大小来调整对象晋升的速度和年龄,从而达到为收集器设置的暂停时间目标,如果手动设置了大小就意味着放弃了G1的自动调优

​ (2)不断调优暂停时间目标

​ 一般情况下这个值设置到100ms或者200ms都是可以的(不同情况下会不一样),但如果设置成50ms就不太合理。暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。暂停时间只是一个目标,并不能总是得到满足。

​ (3)使用-XX:ConcGCThreads=n来增加标记线程的数量

​ IHOP如果阀值设置过高,可能会遇到转移失败的风险,比如对象进行转移时空间不足。如果阀值设置过低,就会使标记周期运行过于频繁,并且有可能混合收集期回收不到空间。IHOP值如果设置合理,但是在并发周期时间过长时,可以尝试增加并发线程数,调高ConcGCThreads。

​ (4)MixedGC调优

​ -XX:InitiatingHeapOccupancyPercent

​ -XX:G1MixedGCLiveThresholdPercent

​ -XX:G1MixedGCCountTarger

​ -XX:G1OldCSetRegionThresholdPercent

​ (5)适当增加堆内存大小

​ (6)不正常的Full GC

​ 有时候会发现系统刚刚启动的时候,就会发生一次Full GC,但是老年代空间比较充足,一般是由Metaspace区域引起的。可以通过MetaspaceSize适当增加其大家,比如256M。

4.3 CPU占用率高

(1)top

(2)top -Hp PID

​ 查看进程中占用CPU高的线程id,即tid

(3)jstack PID | grep tid

5 JVM性能优化指南

6 常见问题

  1. 内存泄漏与内存溢出的区别

​ 内存泄漏是指不再使用的对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。

​ 内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。

  1. young gc会有stw吗?

​ 不管什么 GC,都会发送 stop-the-world,区别是发生的时间长短。而这个时间跟垃圾收集器又有关系,Serial、PartNew、Parallel Scavenge 收集器无论是串行还是并行,都会挂起用户线程,而 CMS和 G1 在并发标记时,是不会挂起用户线程的,但其它时候一样会挂起用户线程,stop the world 的时间相对来说就小很多了。

  1. 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。

  1. 什么是直接内存

​ Java的NIO库允许Java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

  1. 垃圾判断的方式

​ 引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为0就会回收但是JVM没有用这种方式,因为无法判定相互循环引用(A引用B,B引用A)的情况。

​ 引用链法: 通过一种GC ROOT的对象(方法区中静态变量引用的对象等-static变量)来判断,如果有一条链能够到达GC ROOT就说明,不能到达GC ROOT就说明可以回收。

  1. 不可达的对象一定要被回收吗?

​ 即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

  1. 为什么要区分新生代和老年代?

​ 当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

  1. G1与CMS的区别是什么

​ CMS 主要集中在老年代的回收,而 G1 集中在分代回收,包括了年轻代的 Young GC 以及老年代的 Mix GC;G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的

产生;在初始化标记阶段,搜索可达对象使用到的 Card Table,其实现方式不一样。

  1. 方法区中的无用类回收

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?

​ 判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。

​ 类需要同时满足下面 3 个条件才能算是 “无用的类” :

​ a-该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。

​ b-加载该类的 ClassLoader 已经被回收。

​ c-该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。


jvm调优
http://www.zivjie.cn/2023/03/11/java基础/jvm/jvm调优/
作者
Francis
发布于
2023年3月11日
许可协议