JVM 出现性能问题时表现上是 CPU100%,内存一直占用
1、 如果 CPU 的 100%,要从两个角度出发,一个有可能是业务线程疯狂运行,比如说想很多死循环。还有一种可能性,就是 GC 线程在疯狂的回收,因为 JVM 中垃圾回收器主流也是多线程的,所以很容易导致 CPU 的 100%
2、 在遇到内存溢出的问题的时候,一般情况下我们要查看系统中哪些对象占用得比较多,在实际的业务代码中,找到对应的对象,分析对应的类,找到为什么这些对象不能回收的原因,就是我们前面讲过的可达性分析算法,JVM 的内存区域,还有垃圾回收器的基础
先看错误日志,如果错误日志能够定位出哪个类对象导致内存溢出,针对问题改 bug就好。但很多时候单凭日志无法定位出内存溢出问题
top 后 shift+M 按内存占用大到小排序,RES 是此进程实际占用内存,%MEM是占服务器总内存的49.8%
[root@speedyao java]# ps -aux|grep java
jstat -gcutil [ []
vmid:虚拟机进程号
interval:采样时间,默认单位是ms
count:采样条数
[root@speedyao java]# jstat -gcutil 17561 1000 10
以上命令代表1秒钟采样1次,总共采样10次。
FULL GC 明显大于 YOUNG GC 次数,并且 FULL GC 次数很频繁,说明程序有大内存对象,并且一直无法释放
JVM 会将整个 heap 的信息 dump 写入到一个文件,heap 如果比较大的话,就会导致这个过程比较耗时,并且执行的过程中为了保证 dump 的信息是可靠的,会暂停应用。
[root@speedyao java]# jmap -dump:format=b,file=heap.prof 17561
format=b:表示生成二进制类型的dump文件
file=:后面写的是输出的dump文件路径
17561:jvm 进程 id
1、用 MAT (MemoryAnalyzer Tool 为 eclipse 做的)分析 dump 文件,
2、用 jdk 自带的 jvisualvm.exe 来分析 dump 文件, 在 jdk 安装目录找到 jvisualvm.exe 双击打开即可
上图是概要,阴影部分就是大内存对象类,点击选择 “list Object”、“with incoming references”,就出现下图。下图就是这个对象的信息,RunMian 就是map对象所在的类,这样就能快速定位出哪个类中的哪个对象出现了内存异常。
下图Histogram这个tab是堆内存占比从大到小排序。
以上就是内存问题排查的大致步骤。
参考 https://blog.csdn.net/weixin_38399962/article/details/103710972
在命令行中, 输入 jconsole 命令, 即可打开 jconsole,可以监控【本地】或【远程】 java 进程
以本地的一个死锁的 test 进程来讲解,点击你所需要监控的进程,便可以进入监控页面,首页概况
内存中可以看堆和非堆内存的详细信息,
而线程模块可以检查死锁线程,特别简单
比命令行敲 jps 和 jstack 轻松的多
命令行,输入 jvisualvm .即可打开jvisualvm
打开后界面如下:
哇哦, 竟然这么高大上, 不仅仅有jconsole的功能还可以查看dump文件, 并且在首页上还有官方推荐的故障排除指南
哟吼? 我们发现我们的进程怎么在发出黄色的闪光? 这是警告,我们去线程那一栏一探究竟
原来是出现死锁了, 非常直观,点击线程 dump ,可以看到那个类第几行出现问题了.
监控总览大同小异
监控远程 java 进程 参考 https://rourou.blog.csdn.net/article/details/103717001
参考排查内存溢出步骤 https://zhuanlan.zhihu.com/p/126750819
mat 有 eclipse 的插件,也有独立的安装包
下载 mat 安装包,打开 heap.prof 文件,可以看到mat已经分析了我们的文件
你看他就是个暖男,都帮我们分析出来了一个问题,我们点进去看看
他发现了是ArrayList的问题了,我们再往下看看
看到了嘛,具体代码的位置都帮我们定位好了,那排查也就是手到擒来的事情了。
github: https://github.com/alibaba/arthas
Arthas 是Alibaba开源的Java诊断工具,采用命令行交互模式,是排查jvm相关问题的利器
Arthas 也提供 heapdump 命令导出栈信息, 类似 jmap 命令的heap dump功能,但功能远不止这个
下载安装
curl -O https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar
常用命令
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:输出当前方法被调用的调用路径
...... 具体使用参考github帮助文档
参考 https://mp.weixin.qq.com/s/DhqdfanI8VR67J5YFikPsA
我们程序中几乎所有的引用使用的都是强引用。
StringBuilder sb = new StringBuilder();
上面通过在堆中创建实例,然后赋值给栈中局部变量 sb 的方式 就是强引用。有如下特点:
注意: 为避免浪费内存,可以在变量 sb 不再使用后通过显示的将变量 sb 置为 null(sb = null),来加速对象的回收。
软引用对应的类为 java.lang.ref.SoftReference, 一个软引用中的对象,不会很快被JVM回收,JVM会根据当前堆的使用情况来判断何时回收,当堆的使用率超过阈值时,才回去回收软引用中的对象。
软引用的案例:
Object obj = new Object();
SoftReference softRef = new SoftReference<Object>(obj);
//删除强引用
obj = null;
//调用gc
System.gc();
System.out.println("gc之后的值:" + softRef.get()); // 对象依然存在
软引用也可以和一个引用队列联合使用,如果软引用中的对象(obj)被回收,那么软引用会被 JVM 加入关联的引用队列中。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object obj = new Object();
SoftReference softRef = new SoftReference<Object>(obj,queue);
//删除强引用
obj = null;
//调用gc
System.gc();
System.out.println("gc之后的值: " + softRef.get()); // 对象依然存在
//申请较大内存使内存空间使用率达到阈值,强迫gc
byte[] bytes = new byte[100 * 1024 * 1024];
//如果obj被回收,则软引用会进入引用队列
Reference<?> reference = queue.remove();
if (reference != null){
System.out.println("对象已被回收: "+ reference.get()); // 对象为null
}
引用队列(ReferenceQueue)作用
Queue的意义在于我们在外部可以对queue中的引用进行监控,当引用中的对象被回收后,我们可以对引用对象本身继续做一些清理操作,因为我们引用对象(softRef)也占有一定的资源。
弱引用中的对象具有很短的声明周期,因为在系统GC时,只要发现弱引用,不管堆空间是否足够,都会将对象进行回收。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用的简单使用:
Object obj = new Object();
WeakReference weakRef = new WeakReference<Object>(obj);
//删除强引用
obj = null;
System.out.println("gc之后的值:" + weakRef.get()); // 对象依然存在
//调用gc
System.gc();
System.out.println("gc之后的值:" + weakRef.get()); // 对象为null
弱引用也可以和一个引用队列联合使用,如果弱引用中的对象(obj)被回收,那么软引用会被 JVM 加入关联的引用队列中。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object obj = new Object();
WeakReference weakRef = new WeakReference<Object>(obj,queue);
//删除强引用
obj = null;
System.out.println("gc之后的值: " + weakRef.get()); // 对象依然存在
//调用gc
System.gc();
//如果obj被回收,则软引用会进入引用队列
Reference<?> reference = queue.remove();
if (reference != null){
System.out.println("对象已被回收: "+ reference.get()); // 对象为null
}
软引用和弱引用都非常适合保存那些可有可无的缓存数据,当内存不足时,缓存数据被回收(再通过备选方案查询),当内存充足时,也可以存在较长时间,起到加速的作用。
应用
WeakHashMap
当key只有弱引用时,GC发现后会自动清理键和值,作为简单的缓存表解决方案。
ThreadLocal
ThreadLocal.ThreadLocalMap.Entry 继承了弱引用,key为当前线程实例,和WeakHashMap基本相同。
虚引用 就是 形同虚设 ,它并不能决定 对象的生命周期。任何时候这个只有虚引用的对象都有可能被回收。因此,虚引用主要用来跟踪对象的回收,清理被销毁对象的相关资源。PhantomReference的 get() 方法永远返回 null ,而且只提供了与引用队列同用的构造函数。所以虚引用必须和引用队列一同使用。
Map<Object, String> map = new HashMap<>();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object obj = new Object();
PhantomReference phantomRef = new PhantomReference<Object>(obj,queue);
map.put(obj,"obj val");
new CheckRefQueue(queue,map).start();
//删除强引用
obj = null;
Thread.sleep(1000);
int i = 1;
while (true){
System.out.println("第"+i+"次gc");
System.gc();
Thread.sleep(1000);
}
public class CheckRefQueue extends Thread {
private ReferenceQueue queue;
private Map<Object, String> map;
public CheckRefQueue(ReferenceQueue queue, Map<Object, String> map) {
this.queue = queue;
this.map = map;
}
@Override
public void run() {
// 等待,直到对象呗回收
Reference reference = queue.remove();
// 释放引用对象的引用
map.remove(reference.get());
}
}
打开Eclipse,选择Window–Preferences–Java–Installed JREs,编辑选择的jre 在其中的Default VM Arguments: 框中输入 -Xms128m -Xmx512m 等,这样设置 Java 拟虚机内存使用最小是 128M,最大是512M,再单击“OK”
右击工程 Run AS(Debug As) -->选最下面Run…–> Arguments–>在VM arguments里面填(有内容则追加即可) -Xmx256m 等。这样就可以设置它运行时最大内存为256m
第二行加参数即可,用 startup.bat 启动时tomcat时生效的,但是用 eclipse 启动 tomcat 好像不生效
@echo off
set JAVA_OPTS=-Xms128m -Xmx350m
其实是影响的 eclipse 自身的运行(注意eclipse开发工具的运行和我们的业务应用运行基本没关系,内存溢出也是我们的业务应用),上面方式修改的是业务应用的运行内存大小,而对我们发生的具体业务应用的内存不产生丝毫应用,所以这一步基本不要做,而且改了也需要关闭 eclipse 再双击启动,直接菜单重启不生效!(eclipse.ini中的设置控制 【Eclipse运行】的JVM。它们对从Eclipse【运行的程序】没有影响。Eclipse在 运行程序时 启动单独的JVM。运行配置设置控制该JVM。对每个程序都有单独的运行配置,以便可以为每个程序设置不同的内容)
${ECLIPSE_HOME}/eclipse.ini
-Xms256m
-Xmx2048m
PermGen Space指的是内存的永久保存区,该块内存主要是被JVM用来存放class和mete信息的,当class被加载loader的时候就会被存储到该内存区中,与存放类的实例的heap区不同,java中的垃圾回收器GC不会在主程序运行期对PermGen space进行清理。
因此,程序启动时如果需要加载的信息太多,超出这个空间的大小,则会发生溢出。
解决方案:增加空间分配——增加java虚拟机中的XX:PermSize和XX:MaxPermSize参数的大小,其中XX:PermSize是初始永久保存区域大小,XX:MaxPermSize是最大永久保存区域大小。
heap是Java内存中的堆区,主要用来存放对象,当对象太多超出了空间大小,GC又来不及释放的时候,就会发生溢出错误。即内存泄露越来越严重时,可能会发生内存溢出。
解决方案:(1)、检查程序,减少大量重复创建对象的死循环,减少内存泄露。
(2)、增加Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的大小。
stack是Java内存中的栈空间,主要用来存放方法中的变量,参数等临时性的数据的,发生溢出一般是因为分配空间太小,或是执行的方法递归层数太多创建了占用了太多栈帧导致溢出。
解决方案:修改配置参数-Xss参数增加线程栈大小之外,优化程序是尤其重要。
四、总结
内存泄漏是堆中的存在无用但可达的对象,GC无法回收。
内存溢出是空间不足的溢出,主要分为 PermGen space 不足、堆不足、栈不足。