性能监控与调优
先来看看大厂的相关面试题
对于生产环境而言,可能会出现以下问题
我们之所以要调优,是为了解决OOM、预防OOM以及减少Full GC出现的频率,我们调优的考虑阶段一般有三个,分别是上线前、项目运行阶段以及线上出现OOM
我们监控的依据分别是运行日志、异常堆栈、GC日志、线程快照以及堆转储快照,调优的大方向有三,分别是合理地编写代码、充分并合理使用硬件资源以及合理进行JVM调优
性能优化的步骤分为性能监控、性能分析和性能调优
性能监控指的是以一种非强行的或者入侵方式手机或查看应用运营性能数据的活动,其中可能发现的问题包括但不限于GC频繁、cpu load过高、OOM、内存泄露、死锁和程序响应时间较长
性能的分析则是一种以侵入方式收集运行性能数据的活动,做这项的目的在于排查问题,一般排查问题的方法有
打印GC日志,通过GCviewer或者gceasy.io来分析异常信息
灵活运用命令行工具、jstack、jmap、jinfo等
dump出堆文件,使用内存分析工具分析文件
使用阿里Arthas、jconsole、JVisualVM来实时查看JVM状态
jstack查看堆栈信息
性能调优则是解决问题的方法,一般的方法有
适当增加内存,根据业务背景选择垃圾回收器
优化代码,控制内存使用
增加机器,分散节点压力
合理设置线程池线程数量
使用中间件提高程序效率,比如缓存、消息队列等
评价一个项目的性能的指标有以下几种
一般来说,我们比较关注停顿时间和吞吐量,我们主要优化这两个部分,下面是关于响应时间的说明
最后是其它们的关系描述
JVM监控及诊断工具
性能诊断是软件工程师在日常工作中需要经常面对和解决的问题,想要定位这些问题,性能诊断工具必不可少
我们这里先学习使用命令行形式的监控及诊断工具
命令行篇
首先要提的是,一般来说我们实际的项目使用这些命令行诊断工具都是在Linux系统下,我们这里图方便就用windows系统来演示了,这个其实大差不差
Jps
jps可以查看正在运行的Java进程,可以显示指定系统呃逆所有的HotSpot虚拟机进程
基本使用语法为jps,可以使用jps -help来查看对应的参数信息
其中-m能输出虚拟机进程时传递给main()方法的参数,参数包括一些字符串或者jar包
我们也可以远程监控主机上的java程序,这需要安装jstatd,但是这种技术容易收到IP地址欺诈攻击,因此最安全的操作其实是不用这个
Jstat
jstat能查看JVM中各种运行状态信息,可以显示本地或者远程虚拟机进程中的类装载、内存、GC和编译等数据
基本的使用语法如下,我们先来讲option参数
option一共有以下的各种选项,每个选项针对的查看情况都有所区别
interval参数用于指定输出统计数据的周期,单位为毫秒。即:查询间隔,指定对应的时间可以令其在规定的时间内一直打印
count参数用于指定查询的总次数,可以实现打印几次就停止打印的效果
-t参数可以在输出信息前加上一个Timestamp列,显示程序的运行时间。单位:秒
-h参数可以在周期性数据输出时,输出多少行数据后输出一个表头信息,表头信息指的是表格信息中的最起始一行,展示每行的数据属于什么的信息
那么我们写入下面的例子
下面是表头信息所表示意思的解释说明
我们可以计算得出GC时间占运行时间的比例来得知我们的程序现在存在什么问题
如果我们查看进程中发现OU列越来越多且呈现上升趋势,说明老年代中无法回收的对象在不断增加,很可能存在内存泄露
Jinfo
jinfo可以查看虚拟机的配置参数信息,也可以调整虚拟机的配置参数
其基本语法是jinfo [options] pid,进程id是必须要加上的
jinfo -sysprops 进程id命令可以查看由System.getProperties()取得的参数
jinfo -flags 进程id可以查看曾经赋过值的一些参数
jinfo -flag 参数名称 进程id可以查看某个java进程的具体参数信息
只有被标记为manageable的flag支持动态修改,这个修改能力是极其有限的
针对boolean类型的参数的修改,我们使用 jinfo -flag [+|-]参数名称 进程id
针对非boolean类型的参数的修改,我们使用 jinfo -flag 参数名称=参数值 进程id
java -XX:+PrintFlagsInitial命令可以查看所有JVM参数启动的初始值
java -XX:+PrintFlagsFinal命令可以查看所有JVM参数的最终值
java -参数名称:+PrintCommandLineFlags命令可以查看那些已经被用户或者JVM设置过的详细的XX参数的名称和值
Jmap
jmap可以打出内存的映像文件或者是查看内存的使用情况,值得注意的是其并不是只有这两个作用,只是这两个作用我们比较经常使用所以我们拿来当代表性作用而已
其可以查看本地的,也可以查看远程的使用情况
下面的这么多命令里,我们比较关注-dump、-heap、-histo这三个命令
导出内存镜像文件分为手动和自动两种方式,其保存的是Java进程在某个时间点的内存快照
手动方式有两种,第一种是记录所有对象的jmap -dump:format=b,file=<filename.hprof> 命令,第二种是只记录堆中存活对象的jmap -dump:live,format=b,file=<filename.hprof> 命令
一般来说我们推荐使用第二种命令
自动的方式则有-XX:+HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath=<filename.hprof>两种,前者表示启动自动保存,后者指定生成的dump文件的位置
jmap -heap 进程id 命令可以用于展示某个时间点上的JVM中的堆信息,包括每个区域的内存大小,最大大小,已经使用的大小和它们的比例等
jmap -histo 进程id 命令可以查看某一时刻堆中的类、存在的实例数量以及它们的大小
当然还有一些其他的使用,但是无法在PC上演示,还比较小众,因此只做了解
值得一提的是,由于jmap需要借助安全点机制,因此其生成的dump可能与实际结果存在偏差
Jhat
jhat是JDK自带的堆分析工具,其可以分析生成的dump文件,本质是调用jdk中的服务并分析得到网址,得到结果之后我们只要进入对应的网址查看即可
值得一提的是,这个工具比较鸡肋,后续的jdk中也已经移除,只做了解即可
当然我们还有可以设置的参数
Jstack
jstack可以打印JVM中的线程快照,线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈的集合,我们可以利用其来查看各个线程的执行情况,来发现线程执行中的各种问题,比如死锁
其基本的使用语法为jstack option pid
当然,也有对应的各种参数
Jcmd
jcmd是一个多功能工具,其可以实现除了jstat外所有命令的功能
jcmd -l 命令可以列出所有的JVM进程
jcmd 进程号 help 命令可以针对指定的进程,列出支持的所有具体命令
jcmd 进程号 具体命令 可以显示指定进程的指令命令的数据
Jstatd
jstatd是一个RMI服务器程序,可以用于启动远程监控
GUI篇
命令行监控工具存在先天的不足,因此出现了GUI图形化总和诊断工具,JDK自带的诊断工具有Jconsole、Visual VM、JMC,第三方的工具则有MAT、JProfiler等
JConsole
Jconsole是JDK5时退出的java监控和管理控制台
启动方式可以直接打开dos窗口输入jconsole
连接方式有三种,第一种是本地连接方式,使用JConsole连接一个正在本地系统运行的JVM,并且执行程序的和运行JConsole的需要是同一个用户。JConsole使用文件系统的授权通过RMI连接起链接到平台的MBean的服务器上。这种从本地连接的监控能力只有Sun的JDK具有。
第二种连接方式是远程连接方式,其使用下面的URL通过RMI连接器连接到一个JMX代理,service:jmx:rmi:///jndi/rmi://hostName:portNum/jmxrmi。JConsole为建立连接,需要在环境变量中设置mx.remote.credentials来指定用户名和密码,从而进行授权。
第三种连接方式是使用一个特殊的URL连接JMX代理。一般情况使用自己定制的连接器而不是RMI提供的连接器来连接JMX代理,或者是一个使用JDK1.4的实现了JMX和JMX Rmote的应用
作为监控工具,其主要的作用当然是监控JVM中的栈堆使用情况和虚拟机中的各种信息
Visual VM
先来说说jvisualvm和visual vm的区别: visual vm是单独下载的工具,然后将visual vm结合到jdk中就变成了jvisualvm,仅仅是添加了一个j而已,这个j应该是java的用处,所以说jvisualvm其实就是visual vm
其实一个功能强大的多合一故障诊断和性能监控的可视化工具,自JDK6之后作为JDK的一部分发布,推荐学习
Visual VM的一大特定就是支持插件扩展,我们建议安装上VisualGC这款插件,可以通过插件页面下载插件
也可以在IDEA中安装插件
连接方式分为本地连接和远程连接两种,后者比较麻烦,我们就要不演示了
其主要功能生成/读取堆内存快照,也就是生成dump文件
当然,还可以加载dump文件进行分析
还可以查看JVM参数和系统属性以及查看运行中的虚拟机进程
也可以生成/读取线程快照
当然也可以读取线程快照
可以实现对程序资源的实时监控,还有一些其他功能
更多的功能则可以在对应的主界面中进行查看,CPU抽样和内存抽样主要是为了查看CPU和内存的使用情况
Eclipse MAT
MAP是一款功能强大的Java堆内存分析器,可以用于查找内存泄露以及查看内存消耗情况
其是专门用于分析dump文件的工具,推荐用于进行对dump文件的分析
dump文件中一般存在以下内容
MAT并不能处理所有类型的堆存储文件,但是比较主流的格式其都支持。其最吸引人的功能还是能够生成内存泄露报表,方便定位和分析问题
生成dump文件有下面四种方式
利用MAP分析dump文件,可以得到下面的饼状分析图
Mat中的histogram选项可以展示各个类的实例数目以及这些实例的Shallow heap或者Retained heap的总和
thread overview选项可以查看系统中的Java线程和查看局部变量的信息
with outgoing references可以获得一个对象引用了谁的列表
with incoming references则可以获得该对象被谁引用的列表
shallow heap指的是浅堆,浅堆指的是一个对象中的各种属性以及包括其自己的内存,这里不包括引用的其他对象的实际内存,只包括其自己的属性内存
retained heap指的是深堆,Retained Set指的是保留集。一个对象的保留集指的是只能通过该对象直接或间接访问到的所有对象的集合,而深堆则指的是对象的保留集中的所有对象的浅堆大小之和
下面是一个增加理解的两个案例
然后我们再来看一个案例分析,下面是案例代码
/**
* 有一个学生浏览网页的记录程序,它将记录 每个学生访问过的网站地址。
* 它由三个部分组成:Student、WebPage和StudentTrace三个类
*
* -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=c:\code\student.hprof
* @author shkstart
* @create 16:11
*/
public class StudentTrace {
static List<WebPage> webpages = new ArrayList<WebPage>();
public static void createWebPages() {
for (int i = 0; i < 100; i++) {
WebPage wp = new WebPage();
wp.setUrl("http://www." + Integer.toString(i) + ".com");
wp.setContent(Integer.toString(i));
webpages.add(wp);
}
}
public static void main(String[] args) {
createWebPages();//创建了100个网页
//创建3个学生对象
Student st3 = new Student(3, "Tom");
Student st5 = new Student(5, "Jerry");
Student st7 = new Student(7, "Lily");
for (int i = 0; i < webpages.size(); i++) {
if (i % st3.getId() == 0)
st3.visit(webpages.get(i));
if (i % st5.getId() == 0)
st5.visit(webpages.get(i));
if (i % st7.getId() == 0)
st7.visit(webpages.get(i));
}
webpages.clear();
System.gc();
}
}
class Student {
private int id;
private String name;
private List<WebPage> history = new ArrayList<>();
public Student(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<WebPage> getHistory() {
return history;
}
public void setHistory(List<WebPage> history) {
this.history = history;
}
public void visit(WebPage wp) {
if (wp != null) {
history.add(wp);
}
}
}
class WebPage {
private String url;
private String content;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
下面是分析的图片
结论: elementData数组的浅堆是80个字节,而elementData数组中的所有WebPage对象的深堆之和是1208个字节,所以加在一起就是elementData数组的深堆之和,也就是1288个字节 解释: 我说“elementData数组的浅堆是80个字节”,其中15个对象一共是60个字节,对象头8个字节,数组对象本身4个字节,这些的和是72个字节,然后总和要是8的倍数,所以“elementData数组的浅堆是80个字节” 我说“WebPage对象的深堆之和是1208个字节”,一共有15个对象,其中0、21、42、63、84、35、70不仅仅是7的倍数,还是3或者5的倍数,所以这几个数值对应的i不能计算在深堆之内,这15个对象中大多数的深堆是152个字节,但是i是0和7的那两个深堆是144个字节,所以(13152+1442)-(6*152+144)=1208,所以这也印证了我上面的话,即“WebPage对象的深堆之和是1208个字节” 因此“elementData数组的浅堆80个字节”加上“WebPage对象的深堆之和1208个字节”,正好是1288个字节,说明“elementData数组的浅堆1288个字节”
在对象引用图中,所有指向B的路径都要经过A,那么A支配对象B,离B最近的支配对象认为是直接支配者,而由直接支配者与被支配者关系描述的树状图就是支配树
支配树的概念能用于计算一个对象能释放的空间,因为如果一个对象的直接支配者被释放,那么该对象必然也会被释放
最后我们来说一个Tomcat的堆溢出案例分析
首先我们查看dump文件,可以看到有一个最大的对象,最大的对象往往是嫌疑最大的引起OOM的对象,因此我们先查看该对象,查看其引用了什么对象。顺带一提Size指的是堆内存的大小
我们能够查看到其中存在sessions对象,占用了许多空间
我们继续点击,会发现其中存在许多实例,并且都有较大的值,所以问题很可能出在这里
所以我们构造一个搜索语句,搜索所有的session对象,发现这种对象有许多个,总计大小占堆大小的一半
然后我们点击session的数据值,查看其存活时间,根据session总数和时间计算出每秒的平均压力,可以推断发生堆移除的原因是因为Tomcat在短时间内接受了大量请求导致的OOM
MAT支持一种类似SQL的查询语言,我们称之为OQL,其可以在堆中进行对象的查找和筛选
下面是案例的示例代码
/**
* 有一个学生浏览网页的记录程序,它将记录 每个学生访问过的网站地址。
* 它由三个部分组成:Student、WebPage和StudentTrace三个类
*
* -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=c:\code\student.hprof
* @author shkstart
* @create 16:11
*/
public class StudentTrace {
static List<WebPage> webpages = new ArrayList<WebPage>();
public static void createWebPages() {
for (int i = 0; i < 100; i++) {
WebPage wp = new WebPage();
wp.setUrl("http://www." + Integer.toString(i) + ".com");
wp.setContent(Integer.toString(i));
webpages.add(wp);
}
}
public static void main(String[] args) {
createWebPages();//创建了100个网页
//创建3个学生对象
Student st3 = new Student(3, "Tom");
Student st5 = new Student(5, "Jerry");
Student st7 = new Student(7, "Lily");
for (int i = 0; i < webpages.size(); i++) {
if (i % st3.getId() == 0)
st3.visit(webpages.get(i));
if (i % st5.getId() == 0)
st5.visit(webpages.get(i));
if (i % st7.getId() == 0)
st7.visit(webpages.get(i));
}
webpages.clear();
System.gc();
}
}
class Student {
private int id;
private String name;
private List<WebPage> history = new ArrayList<>();
public Student(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<WebPage> getHistory() {
return history;
}
public void setHistory(List<WebPage> history) {
this.history = history;
}
public void visit(WebPage wp) {
if (wp != null) {
history.add(wp);
}
}
}
class WebPage {
private String url;
private String content;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
那么我们可以写入的查询语句有下
当然也有对应的语法规则,分别是SELECT、FROM、WHERE和内置的对象和方法
再谈内存泄露
内存泄露指的是一个对象已经不再被使用,但是仍然存在被指向,导致GC无法回收的情况。不过宽泛来说,由于代码写的烂,导致对象的生命周期变得很长甚至导致OOM,也可以称为是内存泄露
内存泄露指的是对象无用却无法手机,而内存溢出值得是空间不够导致的溢出,两者存在一定的因果关系
内存泄露有八种情况下面我们来一一讲解
1-静态集合类
将变量设置会静态会这导致该变量的生命周期与JVM相同,可能会导致内存泄露
2-单例模式
单例模式同样会因为静态属性的原因导致内存泄露
3-内部类持有外部类
如果一个外部类的实例对象返回一个内部的实例对象,而内部类被长期引用,那么即使外部类不再被使用,但由于内部类持有外部类的引用,会导致外部类无法被回收
4-各种连接,如数据库连接、网络连接和IO连接等
各种连接没有显性关闭,也会导致内存泄露
5-变量不合理的作用域
比如如果将容器类设置为成员变量,那么往其中添加对象将会导致这些对象无法被回收,解决的方法有两种,第一种是将容器类设置局部变量,第二种是每次出了方法之后将容器置空
6-改变哈希值
当初的对象被存储进HashSet中,但是如果我们修改了对象参与哈希计算的字段,就会导致哈希结果的改变,这样就无法删除原来的对象了,会让这个无法删除的对象一直不会被回收而发生内存泄露
下面是哈希值内存泄露的案例演示
* 演示内存泄漏
*
* @author shkstart
* @create 14:43
*/
public class ChangeHashCode {
public static void main(String[] args) {
HashSet set = new HashSet();
Person p1 = new Person(1001, "AA");
Person p2 = new Person(1002, "BB");
set.add(p1);
set.add(p2);
p1.name = "CC";//导致了内存的泄漏
set.remove(p1); //删除失败
System.out.println(set);
set.add(new Person(1001, "CC"));
System.out.println(set);
set.add(new Person(1001, "AA"));
System.out.println(set);
}
}
class Person {
int id;
String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
if (id != person.id) return false;
return name != null ? name.equals(person.name) : person.name == null;
}
@Override
public int hashCode() {
int result = id;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + ''' +
'}';
}
}
例2:
/**
* 演示内存泄漏
* @author shkstart
* @create 14:47
*/
public class ChangeHashCode1 {
public static void main(String[] args) {
HashSet<Point> hs = new HashSet<Point>();
Point cc = new Point();
cc.setX(10);//hashCode = 41
hs.add(cc);
cc.setX(20);//hashCode = 51 此行为导致了内存的泄漏
System.out.println("hs.remove = " + hs.remove(cc));//false
hs.add(cc);
System.out.println("hs.size = " + hs.size());//size = 2
System.out.println(hs);
}
}
class Point {
int x;
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Point other = (Point) obj;
if (x != other.x) return false;
return true;
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
'}';
}
}
7-缓存泄露
对象放入到缓存中容易被遗忘,日积月累会导致缓存的对象特别多,会导致项目启动奇慢,因为项目启动时要加载缓存数据
解决的方法是使用WeakHashMap的弱引用来进行缓存
下面是缓存的内存泄露的案例演示
* 演示内存泄漏
*
* @author shkstart
* @create 14:53
*/
public class MapTest {
static Map wMap = new WeakHashMap();
static Map map = new HashMap();
public static void main(String[] args) {
init();
testWeakHashMap();
testHashMap();
}
public static void init() {
String ref1 = new String("obejct1");
String ref2 = new String("obejct2");
String ref3 = new String("obejct3");
String ref4 = new String("obejct4");
wMap.put(ref1, "cacheObject1");
wMap.put(ref2, "cacheObject2");
map.put(ref3, "cacheObject3");
map.put(ref4, "cacheObject4");
System.out.println("String引用ref1,ref2,ref3,ref4 消失");
}
public static void testWeakHashMap() {
System.out.println("WeakHashMap GC之前");
for (Object o : wMap.entrySet()) {
System.out.println(o);
}
try {
System.gc();
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("WeakHashMap GC之后");
for (Object o : wMap.entrySet()) {
System.out.println(o);
}
}
public static void testHashMap() {
System.out.println("HashMap GC之前");
for (Object o : map.entrySet()) {
System.out.println(o);
}
try {
System.gc();
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("HashMap GC之后");
for (Object o : map.entrySet()) {
System.out.println(o);
}
}
}
结果: String引用ref1,ref2,ref3,ref4 消失 WeakHashMap GC之前 obejct2=cacheObject2 obejct1=cacheObject1 WeakHashMap GC之后 HashMap GC之前 obejct4=cacheObject4 obejct3=cacheObject3 Disconnected from the target VM, address: '127.0.0.1:51628', transport: 'socket' HashMap GC之后 obejct4=cacheObject4 obejct3=cacheObject3
8-监听器和回调
监听器和其他回调函数也容易出现内存泄露,解决的方法仍然是使用弱引用
下面我们来看一个内存泄露的案例,先来看看案例代码
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) { //入栈
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
上面的代码由于出栈时并没有取出引用,导出引用未置空,这样即使进行gc,栈中的对象也是不会释放的
解决方法当然是将代码置空,就是这么简单
JProfiler
JProfiler是能在IDEA中使用的分析工具
其具有许多优良特新
其主要功能有以下四种
下面是其安装和配置过程
首先是在Jprofiler中配置IDEA
然后是在IDEA中集成JProfiler
下面是其具体使用的一些操作说明
其数据的采集方式有两种,分别是instrumentation重构模式和Sampling抽样模式,我们推荐使用后者
遥感监测 Telemetries可以简单查看程序中的各种情况
内存视图 Live Memory可以看到内存中的各个信息,也可以选择自己内存信息的展示方式,一般用于分析内存中的类有哪些存在问题
堆遍历 heap walker
堆遍历可以在内存中分析出哪个类不能进行垃圾回收时用于进一步分析
cpu视图 cpu views
CPU视图可以查看具体的方法占用的时间和进行方法统计,由于执行该分析会下降项目的性能,因此默认不开启
线程视图 threads
在此中可以查看线程的阻塞情况,也可以创建线程的dump文件
监视器&锁 Monitors&locks
可以查看死锁或者是当前使用的监视器等
下面我们来看一个案例,下面是源码
public class MemoryLeak {
public static void main(String[] args) {
while (true) {
ArrayList beanList = new ArrayList();
for (int i = 0; i < 500; i++) {
Bean data = new Bean();
data.list.add(new byte[1024 * 10]);//10kb
beanList.add(data);
}
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Bean {
int size = 10;
String info = "hello,atguigu";
static ArrayList list = new ArrayList();
}
下面是分析过程
这款工具非常不错,不过是要收费的
Arthas
上面介绍的工具虽然好,但是其需要在服务端项目配置相关监控参数,然后远程连接监控,还需要收费,比较麻烦,因此出现了Arthas,其是一项命令行界面的项目问题排查工具
阿尔萨斯能够实现在线排查问题,无需重启项目
其基于各种开发项目总和开发而来
这是阿尔萨斯的官方使用文档的地址arthas.aliyun.com/doc/quick-s…
下面是其安装方式
工程目录的说明
其启动方式
查看进程的命令是jps,查看日志的命令是cat ~/logs/arthas/arthas.log,查看帮助的命令是java -jar arthas-boot.jar -h
其还支持在网页端访问,不过也是控制台的形式
有两种退出方式
下面是其基础命令,标蓝的命令比较重要
JVM相关的命令
class/classloader相关
monitor/watch/trace相关
其他命令
具体的命令里还有说明,但是呢,这些不是很重要,需要的时候自己去看阿尔萨斯的说明吧
Java Misssion Control
Java Misssion Control简称JMC,也是Oracle提供的工具之一,先来看看其历史
其启动方式可以直接在JDK中点击
下面是简介
其可以实时监控JVM运行时的状态
JFR是JMC中的一个组件,其可以用极低的性能手机Java虚拟机的性能数据
JFR的事件类型一共分为四种,分别是瞬时事件、持续事件、计时事件和取样事件
JFR的启动方式有三种,第一种是方式1-XX:StartFlightRecording=参数
第二种是使用jcmd的JFR.*子命令
第三种方式是直接在界面中启动JFR
注意取样之前必须要在IDEA上添加对应的参数
取样成功之后我们可以得到下面的展示内容
下面是我们用于飞行记录仪上的项目代码
/**
* -Xms600m -Xmx600m -XX:SurvivorRatio=8
* @author shkstart shkstart@126.com
* @create 2020 21:12
*/
public class OOMTest {
public static void main(String[] args) {
ArrayList<Picture> list = new ArrayList<>();
while(true){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(100 * 50)));
}
}
}
class Picture{
private byte[] pixels;
public Picture(int length) {
this.pixels = new byte[length];
}
public byte[] getPixels() {
return pixels;
}
public void setPixels(byte[] pixels) {
this.pixels = pixels;
}
}
下面是结果
其他工具
Flame Graphs(火焰图)可以非常直观地让我们了解cpu在整个生命周期过程中时间是如何分配的
Tprofiler可以让我们快速定位性能代码,不过现在已经不再更新了
Btrace是一个Java平台的安全的动态追踪工具
Spring Insight在我们的项目是用Spring开发的时候推荐使用
JVM参数选项
参数选项可以分为三种,分别是标准参数选项、-X参数选项和-XX参数选项
标准参数选项比较稳定,后续版本基本不会变化
下面是各种标准参数选项
-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>
使用指定的图像显示启动屏幕
还可以选择HotSpot虚拟机的模式
第二种是-X的参数选项,其仍然是比较稳定,但是可能会在后续版本中变更
下面是-X选项参数的各种选项
-Xmixed 混合模式执行 (默认)
-Xint 仅解释模式执行
-Xcomp 仅采用即时编译器模式
-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 选项是非标准选项,如有更改,恕不另行通知
-XX参数选项是使用最多的参数类型,但是其改动比较频繁,主要用于开发和调试JVM
Boolean类型格式的命令举例
数值类型格式的命令举例
非数值类型的格式命令举例
如何添加JVM参数
首先是IDEA中的方式
其次我们可以在控制台上直接运行对应的jar包,只要运行前加上对应的参数选项即可比如命令java -Xms50m -Xmx50m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -jar demo.jar
我们可以在Tomcat或者运行过程中进行参数的设置
常用的JVM参数选项
首先是打印设置的XX选项及值
对于-XX:+PrintFlagsFinal命令的补充说明
堆、栈、方法区等内存大小都有对应的命令
先来看看栈的相关命令
然后是堆内存的相关命令
关于-XX:SurvivorRatio=8命令的说明
只有显示使用Eden区和Survivor区的比例,才会让比例生效,否则比例都会自动设置,至于其中的原因,请看下面的-XX:+UseAdaptiveSizePolicy中的解释,最后推荐使用默认打开的-XX:+UseAdaptiveSizePolicy设置,并且不显示设置-XX:SurvivorRatio
关于-XX:+UseAdaptiveSizePolicy说明
1、分析
默认开启,将会导致Eden区和Survivor区的比例自动分配,因此也会引起我们默认值-XX:SurvivorRatio=8失效,所以真实比例可能不是8,比如可能是6等
2、如何设置Eden区和Survivor区的比例
-XX:SurvivorRatio=8
显示使用Eden区和Survivor区的比例,那就使用我自己的
没有显示使用Eden区和Survivor区的比例,无论打开或者关闭-XX:+UseAdaptiveSizePolicy,都会自动设置Eden区和Survivor区的比例
结论:
只有显示使用Eden区和Survivor区的比例,才会让比例生效,否则比例都会自动设置,最后推荐使用默认打开的-XX:+UseAdaptiveSizePolicy设置,并且不显示设置-XX:SurvivorRatio
-XX:NewRatio=2命令最好根据实际情况进行设置,主要根据对象生命周期来进行分配,如果对象生命周期很长,那么让老年代大一点,否则让新生代大一点
-XX:PretenureSizeThreadshold=1024命令的问题在于不好控制
-XX:MaxTenuringThreshold=15命令使用较少,一般使用默认值
接着是方法区的相关命令
最后是直接内存的相关命令
各类的垃圾回收也有对应的参数
查看默认的垃圾回收器的命令如下
Serial回收器的命令
Parnew回收器的命令
Parallel回收器的命令
CMS回收器的命令
G1回收器的命令
Mixed GC的调优参数
怎么选择垃圾收集器
OutOfMemory相关的选项如下
-XX:+HeapDumpOnOutMemoryError(在出现OOM的时候 生成dump文件)和-XX:+HeapDumpBeforeFullGC(在出现Full GC的时候生成dump文件)只能设置1个 ,如果不设置-XX:HeapDumpPath=,那么将会在当前目录下生成dump文件,如果设置的话,将会在指定位置生成dump文件
当然还有GC日志的相关参数命令
最后是一些其他参数
通过Java代码获取JVM参数
我们可以通过Java代码来获取JVM的相关参数
可以利用这些特性来做一些阈值报警处理,下面是这么做的例子
/**
*
* 监控我们的应用服务器的堆内存使用情况,设置一些阈值进行报警等处理
*
* @author shkstart
* @create 15:23
*/
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");// 最大可用总堆内存大小
}
}
上篇我们就做过通过Runtime获取对应的JVM参数的例子
GC日志参数
GC日志也有对应的参数
GC日志格式
先来复习下GC的分类以及GC会发生的情况
不同GC分类的GC细节不同,下面是演示的代码
/**
* -XX:+PrintCommandLineFlags
*
* -XX:+UseSerialGC:表明新生代使用Serial GC ,同时老年代使用Serial Old GC
*
* -XX:+UseParNewGC:标明新生代使用ParNew GC
*
* -XX:+UseParallelGC:表明新生代使用Parallel GC
* -XX:+UseParallelOldGC : 表明老年代使用 Parallel Old GC
* 说明:二者可以相互激活
*
* -XX:+UseConcMarkSweepGC:表明老年代使用CMS GC。同时,年轻代会触发对ParNew 的使用
* @author shkstart
* @create 17:19
*/
public class GCUseTest {
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();
while(true){
byte[] arr = new byte[1024 * 10];//10kb
list.add(arr);
// try {
// Thread.sleep(5);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
}
老年代使用CMS GC的结果如下
新生代使用Serial GC的结果如下
GC日志分为MinorGC和FullGC两种
前者的示意图比后者的短,不过实际上将他们的格式都是由同一个规律的字段排序组成的
然后来做GC日志的结构剖析,首先是垃圾收集器的日志分析
然后是GC前后情况
最后是GC时间
下面是YGC,也就是Minor GC的日志的内容解析
这是日志原文
这是Full GC的
这是其日志原文
GC日志分析工具
不过我们肯定不会自己慢慢看日志的,我们有日志分析工具
GCEasy是一款在线分析GC日志的工具,部分功能需要需要收费
GCViewer可以实现离线分析GC日志,就是界面真的挺丑
下载和安装的方法
最后是一些其他工具