问题
- 生产环境发生了内存溢出该如何处理?
- 生产环境应该给服务器分配多少内存?
- 如何对垃圾收集器的性能进行调优?
- 生产环境CPU负载飙升如何处理?
- 生产环境应该给应用分配多少线程?
- 如何不加log就确定是否执行了某一行代码?
- 如何不加log就能实时查看某个方法的参数值和返回值?
- JVM的字节码
- 循环体中用"+"做字符串拼接为什么效率低,“+”的底层一定是StringBuilder.append()吗?
- String常量池
- i++与++i哪种写法效率更高?
- jvm监控和调试工具的使用
- 理解JVM的GC机制,学会GC调优
- TomCat性能监控与调优
一 基于JDK命令行工具的监控
1. JVM的三种参数类型
1.1 标准参数
jvm的标准参数,在jvm各个版本中基本不变。
使用java
获取所有标准参数
- -help
- -server -client ①
- -version -showversion
- -cp -classpath
使用java -version
命令可以输出java的版本信息,从第4行可以看到:
JVM的名字(HotSpot)、类型(Server)、build ID(24.79-b02),JVM以混合模式(mixed mode)在运行,这是HotSpot默认的运行模式,意味着JVM在运行时可以动态的把字节码编译为本地代码。关于切换类型(Server Client)以及二者的区别详见参考文档1。
C:\Users\Administrator>java -version
java version "1.8.0_51"
Java(TM) SE Runtime Environment (build 1.8.0_51-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.51-b03, mixed mode)
1.2 X 参数
非标准化参数,在jvm各个版本中会有一些小变化。
使用java -X
获取所有X参数
-
-Xint:JVM以解释方式执行所有的字节码,会显著降低运行速度(interpreted)
-
-Xcomp:JVM在第一次使用时就把所有字节码编译为本地代码(compiled)
-
-Xmixed:混合模式(默认),由jvm自己决定是否编译为本地代码,会将字节码中多次被调用的部分便以为本地代码以提高执行效率;被调用很少的方法会在解释模式下执行,减少编译和优化成本
# 默认是mixed mode(混合模式),在java的版本信息中显示 C:\Users\Administrator>java -version java version "1.8.0_51" Java(TM) SE Runtime Environment (build 1.8.0_51-b16) Java HotSpot(TM) 64-Bit Server VM (build 25.51-b03, mixed mode) # 修改为interpreted mode(解释执行) C:\Users\Administrator>java -Xint -version java version "1.8.0_51" Java(TM) SE Runtime Environment (build 1.8.0_51-b16) Java HotSpot(TM) 64-Bit Server VM (build 25.51-b03, interpreted mode) # 修改为compiled mode(编译执行) C:\Users\Administrator>java -Xcomp -version java version "1.8.0_51" Java(TM) SE Runtime Environment (build 1.8.0_51-b16) Java HotSpot(TM) 64-Bit Server VM (build 25.51-b03, compiled mode
测试三种JVM工作模式可以使用死循环代码测试运行时间,具体命令如下:
javac HelloWorld.java java -Xint HelloWorld java -Xcomp HelloWorld java -Xmixed HelloWorld
1.3 XX 参数
非标准化参数,在jvm各个版本中变化较大,主要用于jvm调优和debug,也是最常用的参数
使用java -XX:+PrintFlagsFinal -version > flags1.txt
命令获取所有XX参数③,大约有700+参数
-
Boolean 类型
格式:-XX:[+-]<name> 表示启用或者禁用name属性
举例:-XX:+UseConcMarkSweepGC 启用CMS垃圾收集器
-XX:+UseG1GC 启用G1垃圾收集器 -
K-V 类型
格式:-XX:[+-]<name>=<value> 表示name属性的值是value
举例:-XX:InitialHeapSize=3116367872 初始化堆内存,常用
-Xms512m
表示,不要误认为是X参数-XX:MaxHeapSize=3116367872
最大堆内存,常用-Xmx1024m
表示-XX:ThreadStackSize=1024
线程堆栈大小,常用-Xss128k
表示-XX:MaxGCPauseMills=500
表示GC的最大停顿时间是500ms-XX:GCTimeRatio=19
1.4 常用命令
-
jps
:查看java进程。-m
显示传递给main
方法的参数。-l
显示应用程序main
类的完整包名称或应用程序的JAR文件的完整路径名。-v
显示传递给JVM的参数 -
jinfo
:查看Java进程的修改过的jvm参数jinfo -flags 2345
查看进程id为2345的jvm参数jinfo -flag MaxheapSize 2345
查看进程id为2345的jvm参数最大堆内存MaxheapSize
的值
2. jstat查看虚拟机统计信息
2.1 类加载信息
# 进程id21640 输出间隔1000ms 输出次数3
# Loaded加载类的数量 Bytes加载的kB数 Unloaded:卸载的类数 Bytes:卸载的Kbytes数Time:执行类加载和卸载操作所花费的时间
C:\Users\Administrator>jstat -class 21640 1000 3
Loaded Bytes Unloaded Bytes Time
3010 5823.5 52 77.1 3.48
3010 5823.5 52 77.1 3.48
3010 5823.5 52 77.1 3.48
2.2 垃圾回收信息
# 输出结果各项指标详见参考文档2
C:\Users\Administrator>jstat -gc 21640
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
18432.0 19456.0 0.0 0.0 77824.0 48459.9 126976.0 10715.1 19456.0 18288.3 2304.0 1968.8 13 0.204 1 0.139 0.342
这里利用springboot来创建一个Controller,模拟gc
@Controller("/bug")
public class MemController {
public static final int m = 1024 * 1024;
@RequestMapping(method = RequestMethod.GET)
@ResponseBody
public String allocationMem(Integer num) {
System.out.println("request success.....");
for (int i = 0; i < num; i++) {
byte[] mem = new byte[m];
}
return "success";
}
}
启动项目之后,我们访问http://localhost:8080/bug?num=100,就可以增加内存了,使用jstat -gcutil
命令查看内存占用,可以发现,访问前Eden区内存占比为55.43%,访问后32.46%,中间发生了一次YGC
C:\Users\User>jstat -gcutil 7264 200 3
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 0.00 55.43 18.17 95.65 93.81 5 0.050 2 0.089 0.139
0.00 0.00 55.43 18.17 95.65 93.81 5 0.050 2 0.089 0.139
0.00 0.00 55.43 18.17 95.65 93.81 5 0.050 2 0.089 0.139
C:\Users\User>jstat -gcutil 7264 200 3
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
7.57 0.00 32.46 18.17 95.24 93.35 6 0.053 2 0.089 0.141
7.57 0.00 32.46 18.17 95.24 93.35 6 0.053 2 0.089 0.141
7.57 0.00 32.46 18.17 95.24 93.35 6 0.053 2 0.089 0.141
2.3 JIT编译信息
# 查看JIT编译信息
C:\Users\Administrator>jstat -compiler 21640
Compiled Failed Invalid Time FailedType FailedMethod
3054 1 0 12.35 1 org/apache/tomcat/util/IntrospectionUtils setProperty
3. jmap + MAT分析内存溢出 [实战]
堆内存结构3.1 模拟内存溢出
/**
* Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit
* exceeded
*/
private static void heapSize2() {
Map<Object,Object> map = new HashMap();
Random r = new Random();
Integer i = 0;
while (true) {
map.put(i++, "aaa");
}
}
/**
* Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
*/
private static void heapSize() {
List<String> names = new ArrayList();
for (;;) {
names.add("bbb");
}
}
3.2 导出dump文件
-
使用命令行参数,内存溢出时自动导出(最常用)
-XX+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./
另外也可以使用
-Xrunhprof:head=site
参数生成java.hprof.txt 文件,不过这样会影响 JVM的运行效率,不建议在生产环境中使用 -
使用
jmap
命令手动导出jmap -dump:format=b,file=heap.hprof 2345
format=b
表示以二进制形式导出,file=heap.hprof
表示文件名称为heap.hprof,2345
表示要操作的java进程id
更多jmap
命令选项详见参考文档2 - 使用JConsole生成Dump文件 JConsole生成dump文件
3.3 使用MAT分析dump文件
导出dump文件之后,就是分析dump文件了,这里我们选择最常用的MAT来进行分析。首先下载MAT (Memory Analyzer),主要参考MAT教程④⑤,这里我们只介绍一些常用的功能。
mat分析Shallow Heap表示对象本身所占的内存④⑤,Retained Heap表示对象和对象中的引用总共占的内存大小。比如User对象包含name属性,会引用String对象。
排除虚引用,根据GC Roots查看对象引用 mat根据GC Roots查看对象引用 mat查看对象引用树4. jstack分析死循环与死锁 [实战]
CPU负载(load average)⑦和使用率过高,很可能的原因就是死循环。
-
首先可以使用top命令查看占用CPU最高的进程信息,获取进程pid
-
使用
jstack [pid] > myStack.txt
命令打印指定java进程的所有线程堆栈信息 -
使用Linux命令
top打印指定进程的所有线程信息top -p [pid] -H
监控指定进程中所有线程信息,然后找到CPU占用率高的线程nid(windows中可以process exlporer工具查看指定进程中所有线程信息)如图可以看到
8247(0x2037)
线程的CPU使用率达到了96.7%,此时我们就可以确认是这个线程的问题了 -
在
jstack查看线程信息myStack.txt
文件中查看线程id为nid的堆栈信息(myStack.txt
线程id是16进制,top -p
命令线程id是10进制),找到导致CPU飙升的线程[10进制 PID=8247,16进制 nid=0x2037]
堆栈信息了,然后分析问题,解决问题。
根据上图中线程8247(0x2037)
的堆栈信息,我们猜测是CpuController.loop()
方法导致的cpu负载过高,然后我们就可以分析代码,解决问题了。
- 另外
jstack
命令还能自动检测死锁,如果存在死锁,在myStack.txt末尾会有一行found 1 deadlock
,也会打印死锁线程的相关信息,根据这些信息就可以轻松定位到死锁代码,解决CPU负载过高的问题了。
另外,相比jstack
命令来检测死锁,我们还有更轻松的方式,使用JConsole工具来检测死锁。
// 未完待续
二 基于JVisualVM的可视化监控
三 基于Btrace的监控调试
4.1 简介
BTrace可以动态地向目标应用程序的字节码注入追踪代码,用到的技术JavaCompilerAPI,JVMTI,Agent,Instrumentation+ASM
1、接口性能变慢,分析每个方法的耗时情况;
2、当在Map中插入大量数据,分析其扩容情况;
3、分析哪个方法调用了System.gc(),调用栈如何;
4、执行某个方法抛出异常时,分析运行时参数;
4.2 环境准备
下载JVisualVM BTrace插件https://visualvm.github.io/pluginscenters.html
下载BTrace https://github.com/btraceio/btrace/releases/tag/v1.3.11
jar包
4.2 使用BTrace
4.3.1 简单使用
import com.sun.btrace.AnyType;
import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.BTrace;
import com.sun.btrace.annotations.Kind;
import com.sun.btrace.annotations.Location;
import com.sun.btrace.annotations.OnMethod;
import com.sun.btrace.annotations.ProbeClassName;
import com.sun.btrace.annotations.ProbeMethodName;
@BTrace
public class PrintArgSimple {
// 在Ch4Controller类的fun1方法的入口处(ENTRY)进行追踪
@OnMethod(clazz = "com.imooc.monitor_tuning.chapter4.Ch4Controller",
method = "fun1", location = @Location(Kind.ENTRY))
public static void anyRead(@ProbeClassName String className, @ProbeMethodName String methodName, AnyType[] args) {
BTraceUtils.printArray(args); //fun1方法的所有参数
BTraceUtils.println(className + ":" + methodName);
}
}
btrace [pid] MyBtrace.java
4.3.2 拦截构造函数,重载方法
4.3.3 拦截返回值,异常,行号
4.3.4 拦截复杂参数,环境变量
4.3.5 注意事项
四 Tomcat性能监控与调优
五 JVM层GC调优
当项目较大且启动较慢时(如Kettle),需要加载的类较多,占用元空间较大,我们可以打印GC日志
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log
下图是项目启动的GC日志,7s时间就出现了两次Full GC(Metadata GC threshold)
,说明元空间较小,我们可以在启动参数中使用命令
-XX:MetaspaceSize
-XX:MaxMetaspaceSize
来调节元空间大小,提高项目的启动速度
六 JVM字节码与Java代码层调优
七 总结
image参考文档:
- 关于JVM的类型和模式
- Java命令行工具帮助文档 - Oracle
- 打印所有XX参数
- 生成dump文件与MAT的使用
- MAT使用教程
- MAT中文文档
- 理解Linux系统负荷 - 阮一峰
- JVisual VM使用教程
- 如何在生产环境使用Btrace进行调试 - 占小狼
- 如何回答"线上CPU100%排查"面试问题
扩展阅读
如何使用MAT进行内存泄露分析 - 占小狼
JVM系列
使用 JITWatch 查看 JVM 的 JIT 编译代码
深入浅出 JIT 编译器
JVM杂谈之JIT
关于JVM内存的N个问题
jvisualvm安装Visual GC插件
MAT入门到精通(一) - dqVoic
JVM发生OOM的 8 种原因、及解决办法 - 占小狼
JVM 性能调优监控工具 jps、jstack、jmap、jhat、jstat、hprof 使用详解
网友评论