1 JVM常用命令行参数
package com.mashibing.jvm.c5_gc;
import java.util.List;
import java.util.LinkedList;
public class HelloGC {
public static void main(String[] args) {
System.out.println("HelloGC!");
List list = new LinkedList();
for(;;) {
byte[] b = new byte[1024*1024];
list.add(b);
}
}
}
- 区分概念
- 内存泄露:memory leak,对象永远不被回收
- 内存溢出:out of memory,内存彻底不够了
- java -Xmn10M -Xms40M -Xmx60M -XX:+PrintCommandLineFlags -XX:+PrintGC HelloGC
- -Xms40M:最小堆大小
- -Xmx60M:最大堆大小,一般与最小值设为一样的,防止堆产生弹性的压缩,浪费系统计算资源
- -Xmn10M:年轻代大小
- -XX:+PrintGC:打印GC日志
//InitialHeapSize:初始堆大小,单位为byte,由-Xms40M设置,也确实是40M
//MaxHeapSize:最大堆大小
//NewSize:最小新生代大小
//MaxNewSize:最大新生代大小
-XX:InitialHeapSize=41943040 -XX:MaxHeapSize=62914560 -XX:MaxNewSize=10485760 -XX:NewSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGC -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
HelloGC!
//-XX:+PrintGC:可以打印下面的gc信息
//年轻代满了,所以产生YGC,而每次都回收不掉,所以扔进老年代
[GC (Allocation Failure) 7972K->6864K(39936K), 0.0027569 secs]
[GC (Allocation Failure) 14274K->14048K(39936K), 0.0022541 secs]
[GC (Allocation Failure) 21585K->21184K(39936K), 0.0018956 secs]
[GC (Allocation Failure) 28540K->28400K(39936K), 0.0026920 secs]
[Full GC (Ergonomics) 28400K->28264K(54784K), 0.0091581 secs]
[GC (Allocation Failure) 35775K->35529K(54784K), 0.0026164 secs]
[GC (Allocation Failure) 42860K->42705K(54784K), 0.0023221 secs]
//老年代也满了,产生FGC,仍然回收不掉
[Full GC (Ergonomics) 42705K->42582K(60416K), 0.0074762 secs]
[GC (Allocation Failure) 49935K->49878K(60416K), 0.0017456 secs]
[Full GC (Ergonomics) 49878K->49751K(60416K), 0.0076728 secs]
[Full GC (Ergonomics) 57084K->56925K(60416K), 0.0030637 secs]
[Full GC (Ergonomics) 57953K->57945K(60416K), 0.0029954 secs]
[Full GC (Allocation Failure) 57945K->57927K(60416K), 0.0105461 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.mashibing.jvm.c5_gc.HelloGC.main(HelloGC.java:11)
- 打印GC日志的其他方式
- -XX:+PrintGCDetails:打印GC详细日志
- -XX:+PrintGCTimeStamps:打印详细时间
- -XX:+PrintGCCause:打印gc产生原因
2 GC日志详解
2.1 PS+PO的GC日志详解
- 使用-XX:+PrintGCDetails参数,打印GC详细日志
//1. 开始时,老年代还没东西,所以年轻代占用内存和堆占用内存大小相同
//2. Times:linux中time COMMAND用于测量COMMAND命令执行时所需消耗的时间及系统资源等资讯
//[GC类型,GC表示YGC (GC产生的原因) [PS的年轻代: 回收前年轻代大小->回收后年轻代大小(年轻代总大小)] 回收前堆大小->回收后堆大小(堆总大小), 回收使用时间 secs] [Times: 用户态使用时间 内核态使用时间, 程序实际运行时间 secs]
[GC (Allocation Failure) [PSYoungGen: 8136K->712K(9216K)] 8136K->6864K(39936K), 0.0036803 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
- HeapDump日志:内存溢出后,会将整个堆中信息打印出来
Heap
// 内存起始到使用到的地址,总共8192*91%
// PS的年轻代 eden+survivor的大小, 使用大小 [内存起始地址, 使用到的地址, 内存结束地址)
// eden区大小, 使用 百分比
PSYoungGen total 9216K, used 7476K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 91% used [0x00000000ff600000,0x00000000ffd4d198,0x00000000ffe00000)
//from:表示其中一个survivor
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 51200K, used 50784K [0x00000000fc400000, 0x00000000ff600000, 0x00000000ff600000)
object space 51200K, 99% used [0x00000000fc400000,0x00000000ff598198,0x00000000ff600000)
Metaspace used 3255K, capacity 4496K, committed 4864K, reserved 1056768K
//class space:元数据区中,专门存放class的内存空间的情况,reserved:虚拟内存预留,commited:虚拟内存占用,capacity:class总容量,used:使用了多少
class space used 353K, capacity 388K, committed 512K, reserved 1048576K
2.2 CMS日志详解
- java -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC com.mashibing.jvm.gc.T15_FullGC_Problem01
//开始为年轻代的GC,使用ParNew进行回收
//[GC (产生本次GC的原因) [ParNew: 年轻代收集前->年轻代收集后(整个年轻代大小), 0.0051168 secs] 整个堆收集前->整个堆收集后(整个堆大小), 0.0051920 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 5504K->640K(6144K), 0.0051168 secs] 5504K->1624K(19840K), 0.0051920 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//老年代使用占整个老年代一定百分比后(7417/13696,默认68%作用),老年代的CMS开始工作,-XX:CMSInitiatingOccupancyFraction指定
//初始标记阶段,stw
//[GC (CMS Initial Mark) [1 CMS-initial-mark: 老年代使用(老年代总大小)] 整个堆使用(整个堆大小), 0.0007117 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (CMS Initial Mark) [1 CMS-initial-mark: 7417K(13696K)] 8080K(19840K), 0.0007117 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//并发标记开始
[CMS-concurrent-mark-start]
//这里时间意义不大,因为是并发执行,不影响工作线程
[CMS-concurrent-mark: 0.005/0.005 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
//清理之前的一个阶段,该阶段用于将某些card标记为dirty,也称为Card Marking阶段(不重要)
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//最终标记阶段,stw
//YG occupancy:年轻代占用及容量
//几个阶段
//Rescan (parallel):STW下的存活对象标记,因为工作线程已经停了,不会再产生新的对象
//weak refs processing: 弱引用处理
//class unloading: 卸载用不到的class,相当于元数据或永久代的清理
//scrub symbol(string) table: 由于class对象已被删除,此时应删除常量池中这些对象对应类的符号引用
//cleaning up symbol and string tables which hold class-level metadata and
//internalized string respectively
//CMS-remark: 7417K(13696K): 阶段过后的老年代占用及老年代容量,9185K(19840K): 阶段过后的堆占用及堆容量
[GC (CMS Final Remark) [YG occupancy: 1768 K (6144 K)][Rescan (parallel) , 0.0005858 secs][weak refs processing, 0.0000225 secs][class unloading, 0.0006967 secs][scrub symbol table, 0.0005223 secs][scrub string table, 0.0001394 secs][1 CMS-remark: 7417K(13696K)] 9185K(19840K), 0.0020718 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//标记已经完成,进行并发清理
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//重置内部数据结构,为下次gc做准备(不重要)
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (CMS Initial Mark) [1 CMS-initial-mark: 7307K(13696K)] 9562K(19840K), 0.0006696 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2.3 G1日志详解
- java -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UseG1GC com.mashibing.jvm.gc.T15_FullGC_Problem01
[GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0015790 secs]
//young -> 年轻代 Evacuation-> 复制存活对象
//initial-mark 表示mixed gc的初始标记阶段,与本次young gc同时执行,如果没有表示纯ygc
[Parallel Time: 1.5 ms, GC Workers: 1] //一个GC线程
[GC Worker Start (ms): 92635.7]
[Ext Root Scanning (ms): 1.1] //从根对象开始搜索
[Update RS (ms): 0.0] //更新了多少remeber set
[Processed Buffers: 1]
[Scan RS (ms): 0.0]
[Code Root Scanning (ms): 0.0]
[Object Copy (ms): 0.1]
[Termination (ms): 0.0]
[Termination Attempts: 1]
[GC Worker Other (ms): 0.0]
[GC Worker Total (ms): 1.2]
[GC Worker End (ms): 92636.9]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms] //clear cardtable
[Other: 0.1 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.0 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 18.8M(20.0M)->18.8M(20.0M)] //各个区的回收前与回收后,以及总大小
[Times: user=0.00 sys=0.00, real=0.00 secs]
//以下是混合回收其他阶段
[GC concurrent-root-region-scan-start]
[GC concurrent-root-region-scan-end, 0.0000078 secs]
[GC concurrent-mark-start]
//无法evacuation,进行FGC
//G1中压根不应该有full gc
[Full GC (Allocation Failure) 18M->18M(20M), 0.0719656 secs]
[Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 18.8M(20.0M)->18.8M(20.0M)], [Metaspace: 38
76K->3876K(1056768K)] [Times: user=0.07 sys=0.00, real=0.07 secs]
3 性能相关参数
3.1 传统意义上
- 吞吐量:单位时间内系统能处理的请求数量,体现系统处理请求的能力,这是目前最常用的性能测试指标
- tps:每秒事务数=并发数/平均响应时间
- qps:每秒查询数
- 并发数:系统可以同时处理请求或交易的数量
- 响应时间:多久结束
3.2 对于垃圾回收器来讲
- 吞吐量:用户代码时间 /(用户代码执行时间 + 垃圾回收时间)
- 吞吐量优先:使用PS+PO
- 响应时间:STW越短,响应时间越好
- 响应时间优先:G1、CSM
4 调优的含义
- 根据需求进行JVM规划和预调优
- 优化运行JVM运行环境(慢、卡顿)
- 解决JVM运行过程中出现的各种问题(OOM只是其中一部分)
5 调优规划
- 调优需要从业务场景开始,没有业务场景的调优都是耍流氓
- 淘宝历年来,最高并发54w,12306号称百万并发
- 无监控(压力测试,能看到结果),不调优,能及时看到调优后,性能是否有所提升
- 调优步骤:
- 熟悉业务场景(没有最好的垃圾回收器,只有最合适的垃圾回收器)
- 选择回收器组合
- 减少stw时间:CMS、G1、ZGC
- 减少gc总时间:PS
- 计算内存需求:按经验,内存大不一定好
- 选定CPU:越高越好,越好回收越快,执行也越快
- 设定年代大小、升级年龄
- 生产环境设定日志参数
- 观察日志情况
//%t:系统时间,UseGCLogFileRotation:循环生成日志,NumberOfGCLogFiles:最多存放日志文件个数,GCLogFileSize:每个日志大小
-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause
6 案例分析
- 垂直电商(只卖某一种产品),最高每日百万订单,处理订单系统需要什么样的服务器配置?
- 了解最高峰吞吐量:1000订单/秒
- 了解平均响应时间:100ms
- 非要计算内存:每次事务,所需内存(将所有涉及对象算出个总和512k),乘以1000,也就是最低需要500M内存
- 有一个50万PV的资料类网站(从磁盘提取文档到内存)原服务器32位,1.5G的堆,用户反馈网站比较缓慢,因此公司决定升级,新的服务器为64位,16G的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了
- 原网站慢的原因:很多用户浏览数据,很多数据load到内存,内存不足,频繁GC,STW长,响应时间变慢
- 内存扩大更卡顿原因:内存越大,FGC时间越长
- 解决方案:修改垃圾回收期,由PS+PO->PN + CMS或->G1
- 系统CPU经常100%,如何进行调优
- CPU100%那么一定有线程在占用系统资源
- 找出哪个进程cpu高:top
- 找出该进程中的哪个线程cpu高:top -Hp
- 如果为java程序,导出该线程的堆栈信息:jstack
- 查找哪个方法(栈帧)消耗时间 :jstack
- 工作线程占比高 | 垃圾回收线程占比高
- 系统内存飙高,如何查找问题
- 依然是先查具体是谁占用了内存
- 导出堆内存 (jmap)
- 分析 (jhat、jvisualvm、mat、jprofiler … )
- 如何监控JVM
- jstat、jvisualvm、jprofiler、arthas、top…
7 一次具体解决问题
- 测试代码:
package com.mashibing.jvm.c5_gc;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 从数据库中读取信用数据,套用模型,并把结果进行记录和传输
*/
public class T15_FullGC_Problem01 {
private static class CardInfo {
BigDecimal price = new BigDecimal(0.0);
String name = "张三";
int age = 5;
Date birthdate = new Date();
public void m() {
}
}
private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,
new ThreadPoolExecutor.DiscardOldestPolicy());
public static void main(String[] args) throws Exception {
executor.setMaximumPoolSize(50);
for (;;){
modelFit();
Thread.sleep(100);
}
}
private static void modelFit(){
List<CardInfo> taskList = getAllCardInfo();
taskList.forEach(info -> {
// do something
executor.scheduleWithFixedDelay(() -> {
//do sth with info
info.m();
}, 2, 3, TimeUnit.SECONDS);
});
}
private static List<CardInfo> getAllCardInfo(){
List<CardInfo> taskList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
CardInfo ci = new CardInfo();
taskList.add(ci);
}
return taskList;
}
}
- 执行程序:java -Xms200M -Xmx200M -XX:+PrintGC com.mashibing.jvm.gc.T15_FullGC_Problem01
- 运维团队收到内存、cpu报警
- top:内存不断增长 CPU占用率居高不下,记录占比最高的进程号
- top -Hp 进程号:观察进程中哪个线程号占比最高
- jstack -l 进程号,将该进程下所有线程列出来,通过top -Hp得到的十进制线程号,转为十六进制,在显示结果中进行查询,该值应该是nid的值
- 有可能发现该占比很高的线程为垃圾回收线程
- 此时说明一定有工作线程占用了大量的内存不释放,导致垃圾清理线程频繁进行fgc,从而浪费cpu
- 因此应重点关注线程名是我们自己创建的那种工作线程的名,状态为WAITING、BLOCKED的线程
- 这就是为什么阿里规范里规定,应自定义ThreadFactory,从而为线程指定有意义的名称
- 同时应查看这种线程的waiting on <0x00000000f89d4ad8>,<0x00000000f89d4ad8>就是synchronized锁住的对象,由于很多线程在等待这个锁的释放,那么说明持有该锁的人,就是导致其他工作线程等待的线程,可以直接复制该字符串去搜索,找到持有该字符串,且线程状态为RUNNABLE的线程
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000f89d4ad8> (a java.lang.ref.Reference$Lock)
//Reference Handler:处理reference的jvm自己的内部线程
//Finalizer:垃圾回收对应线程
//""中为线程名,找自己的线程名对应的线程
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000000301b800 nid=0x42d4 in Object.wait() [0x0000000014b8f000]
- jps:列出所有java进程,和ps -ef|grep java作用相似
- jinfo 进程号:打印java进程的一些启动参数,使用的jdk版本信息
- jstat -gc 进程号 毫秒数:每隔毫秒数打印gc日志
- 图形界面远程监控
- 一般linux、aix都不安装图形化界面
- java有一个标准的访问远程服务的协议jmx,用于远程管理、监控java进程,只需在服务器开启jmx服务,然后用一个支持标准jmx的客户端去远程连接即可实现远程图形界面监控
- 支持jmx服务的图形客户端包括jsvisual VM、jconsole、Jprofiler(收费)
- 但使用图形界面远程监控,会浪费大量资源,因此生产环境通常不用其进行监控
- 而是使用命令行,或阿里的arthas进行监控
- 图形界面只用于测试的时候进行监控(压测观察)
- 查看堆中对象信息
- jmap -histo 进程号| head -20:查找占用内存最大的前20名对象
- 可以对死掉但没重启的进程执行jmap
- jmap -dump:format=b,file=xxx pid:在线转储文件,就是将整个堆中信息以文件形式导出
- 这个命令对在线系统影响大,不能生产环境使用
- jmap -histo影响较小可以使用
- 设定参数-XX:+HeapDumpOnOutOfMemoryError,OOM的时候会自动产生堆转储文件
- jmap -histo 进程号| head -20:查找占用内存最大的前20名对象
- 使用MAT、jhat、jvisualvm对dump文件分析
- MAT:https://www.cnblogs.com/baihuitestsoftware/articles/6406271.html
- jhat -J-mx512M xxx.dump:分析后,会启动在服务器上启动一个7000端口,用于查询结果
- http://192.168.17.11:7000
- 拉到最后:找到对应链接
- 可以使用OQL查找特定问题对象
- jvisual vm:文件->装入
8 jconsole远程连接
- 程序启动加入参数:
//在服务器上,启动程序同时,开启一个11111端口,用于jmx服务
java -Djava.rmi.server.hostname=192.168.17.11 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=11111 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false XXX
- 如果遭遇 Local host name unknown:XXX的错误,修改/etc/hosts文件,把XXX加入进去
192.168.17.11 basic localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
- 关闭linux防火墙(实战中应该打开对应端口)
service iptables stop
chkconfig iptables off #永久关闭
- windows上打开 jconsole远程连接 192.168.17.11:11111
9 jvisualvm远程连接:简单做法
https://www.cnblogs.com/liugh/p/7620336.html
10 jprofiler (收费)
11 arthas:在线排查工具,无需远程挂图形界面
- https://github.com/alibaba/arthas
- 必须全量安装:https://img.shields.io/maven-central/v/com.taobao.arthas/arthas-packaging.svg?style=flat-square
- 进入bin目录
- java -jar arthas-boot.jar
- 输入进程在arthas下的编号
- 此时arthas将自己attach到这个进程上,可以使用arthas的命令,来查看这个进程状态,arthas@1442,就表示当前arthas位于1442进程上
- help:查看常用命令
- jvm:类似jinfo功能
- thread:查看有多少线程,占用信息,类似jstack功能
- thread 1:查看编号为1的线程中的一些堆栈的调用情况
- arthas没提供类似jmap的功能
- dashboard:类似top -Hp
- heapdump:导出堆文件,相当于jmap导出整个堆情况,heapdump /root/20200104.hprof,表示导入到指定路径,也是对主进程有很大影响
- jhat:对导出的堆文件,进行分析
- jhat -J-mx512 文件名:指定jhat命令使用的内存大小,不然dump文件较大时,会导致jhat命令内存溢出,如果512仍然不够用还是会内存溢出,导致分析失败
- 分析完成后,会在服务器的7000端口上启动一个服务,然后我们就可以远程通过一个端口来访问分析结果
- show instance counts for all classes:类似jmap导出的信息
- execute Object Query Language query:使用OQL语句查询对象信息,例如select s from java.lang.String s,可以把所有String对象都显示出来
- jad:对内存中的class进行反编译,jad 全限定类名,用于动态代理生成类的问题定位、查看第三方的类、查看自己编写的代码是否为最新版本
- redefine:热替换,目前只能改方法实现,不能改方法名、属性等
- 正常替换,必须停服务,然后重新更新,然后重启,但生产环境有时不允许停
- 也可以用于临时加一些日志,比如在一些关键的函数里打印出入参数,适用于上线流程复杂而且审核比较严的公司
- 先attach到进程上
- jad查看要替换的类,确实不是最新版本
- 直接修改原来的类并编译到/root/TT.class
- redefine /root/TT.class,使用/root/TT.class,替代内存中的TT类,完成热替换
12 纤程(协程) Fiber
- 程序–进程–线程–纤程:线程是进程的优化,纤程是线程的优化
- 一个QQ程序可以启动多个进程(可以登录多个账号)
- 一个进程中可以启动多个线程,例如UI线程、Socket线程
- 一个线程中可以启动多个纤程
- 创建一个新线程后,CPU将老线程的线程栈信息保存在内存,然后再通过内核空间切换到新线程linux操作系统分为两个执行级别,用户空间和内核空间,JVM是运行在用户空间的,当JVM需要启动新线程,或在两个线程间切换时,需要通过内核进行创建与切换,耗费资源和时间较大,是重量级的
- 纤程相当于是存在于用户空间的线程,每个纤程也有自己的内存栈,因为他们本身就在用户空间,所以他们之间的切换不通过内核空间,耗费资源和时间较少
- 做内存计算的适合纤程,以实际情况为准
- Go语言、Python中支持纤程,可以使用第三方纤程库Quasar来使用纤程,截止JDK13,Java官方还不支持纤程,所以Quasar fiber依赖java instrumentation修改你的代码,可以在运行时通过java Agent实现,也可以在编译时使用ant task实现。但该第三方jar包还没到1.0版本,暂时不建议使用
- 启一个线程大概占1M,当线程太多,会卡死,大量CPU与时间浪费在线程间切换,但纤程不一样,可以同时启几万个纤程