内存溢出问题排查及 jvm调参

jvm 性能问题

JVM 出现性能问题时表现上是 CPU100%,内存一直占用
1、 如果 CPU 的 100%,要从两个角度出发,一个有可能是业务线程疯狂运行,比如说想很多死循环。还有一种可能性,就是 GC 线程在疯狂的回收,因为 JVM 中垃圾回收器主流也是多线程的,所以很容易导致 CPU 的 100%
2、 在遇到内存溢出的问题的时候,一般情况下我们要查看系统中哪些对象占用得比较多,在实际的业务代码中,找到对应的对象,分析对应的类,找到为什么这些对象不能回收的原因,就是我们前面讲过的可达性分析算法,JVM 的内存区域,还有垃圾回收器的基础

oom内存溢出排查

先看错误日志,如果错误日志能够定位出哪个类对象导致内存溢出,针对问题改 bug就好。但很多时候单凭日志无法定位出内存溢出问题

1. top 命令对当前服务器内存大致了解

top 后 shift+M 按内存占用大到小排序,RES 是此进程实际占用内存,%MEM是占服务器总内存的49.8%
内存溢出问题排查及 jvm调参_第1张图片

2、利用ps命令查看应用服务 pid

[root@speedyao java]# ps -aux|grep java

3、利用 jstat 查看虚拟机 gc 情况

jstat -gcutil [ []
vmid:虚拟机进程号
interval:采样时间,默认单位是ms
count:采样条数

[root@speedyao java]# jstat -gcutil  17561  1000 10

以上命令代表1秒钟采样1次,总共采样10次。
内存溢出问题排查及 jvm调参_第2张图片
FULL GC 明显大于 YOUNG GC 次数,并且 FULL GC 次数很频繁,说明程序有大内存对象,并且一直无法释放

4、利用 jmap 直接生成 dump 文件

JVM 会将整个 heap 的信息 dump 写入到一个文件,heap 如果比较大的话,就会导致这个过程比较耗时,并且执行的过程中为了保证 dump 的信息是可靠的,会暂停应用。

[root@speedyao java]# jmap -dump:format=b,file=heap.prof 17561 

format=b:表示生成二进制类型的dump文件
file=:后面写的是输出的dump文件路径
17561:jvm 进程 id

5、分析dump 文件 (MAT 或者 jvisualvm)

1、用 MAT (MemoryAnalyzer Tool 为 eclipse 做的)分析 dump 文件,
2、用 jdk 自带的 jvisualvm.exe 来分析 dump 文件, 在 jdk 安装目录找到 jvisualvm.exe 双击打开即可

MAT 分析如下
内存溢出问题排查及 jvm调参_第3张图片

上图是概要,阴影部分就是大内存对象类,点击选择 “list Object”、“with incoming references”,就出现下图。下图就是这个对象的信息,RunMian 就是map对象所在的类,这样就能快速定位出哪个类中的哪个对象出现了内存异常。
内存溢出问题排查及 jvm调参_第4张图片

下图Histogram这个tab是堆内存占比从大到小排序。
内存溢出问题排查及 jvm调参_第5张图片
以上就是内存问题排查的大致步骤。

JVM常用工具(jconsole, jvisualvm,Arthas,MAT)

参考 https://blog.csdn.net/weixin_38399962/article/details/103710972

1.jconsole

在命令行中, 输入 jconsole 命令, 即可打开 jconsole,可以监控【本地】或【远程】 java 进程

内存溢出问题排查及 jvm调参_第6张图片
以本地的一个死锁的 test 进程来讲解,点击你所需要监控的进程,便可以进入监控页面,首页概况
内存溢出问题排查及 jvm调参_第7张图片
内存中可以看堆和非堆内存的详细信息,
而线程模块可以检查死锁线程,特别简单
内存溢出问题排查及 jvm调参_第8张图片
内存溢出问题排查及 jvm调参_第9张图片
比命令行敲 jps 和 jstack 轻松的多

2. jvisualvm

命令行,输入 jvisualvm .即可打开jvisualvm

打开后界面如下:

内存溢出问题排查及 jvm调参_第10张图片

哇哦, 竟然这么高大上, 不仅仅有jconsole的功能还可以查看dump文件, 并且在首页上还有官方推荐的故障排除指南

让我们感受下它吧,继续用我们的死锁test测试进程
内存溢出问题排查及 jvm调参_第11张图片

哟吼? 我们发现我们的进程怎么在发出黄色的闪光? 这是警告,我们去线程那一栏一探究竟

内存溢出问题排查及 jvm调参_第12张图片

原来是出现死锁了, 非常直观,点击线程 dump ,可以看到那个类第几行出现问题了.
内存溢出问题排查及 jvm调参_第13张图片

监控总览大同小异
内存溢出问题排查及 jvm调参_第14张图片
监控远程 java 进程 参考 https://rourou.blog.csdn.net/article/details/103717001

3.MAT

参考排查内存溢出步骤 https://zhuanlan.zhihu.com/p/126750819

mat 有 eclipse 的插件,也有独立的安装包
下载 mat 安装包,打开 heap.prof 文件,可以看到mat已经分析了我们的文件
内存溢出问题排查及 jvm调参_第15张图片
你看他就是个暖男,都帮我们分析出来了一个问题,我们点进去看看
内存溢出问题排查及 jvm调参_第16张图片
他发现了是ArrayList的问题了,我们再往下看看
内存溢出问题排查及 jvm调参_第17张图片

看到了嘛,具体代码的位置都帮我们定位好了,那排查也就是手到擒来的事情了。
内存溢出问题排查及 jvm调参_第18张图片

4.Arthas (阿尔萨斯)

github: https://github.com/alibaba/arthas

Arthas 是Alibaba开源的Java诊断工具,采用命令行交互模式,是排查jvm相关问题的利器
Arthas 也提供 heapdump 命令导出栈信息, 类似 jmap 命令的heap dump功能,但功能远不止这个
内存溢出问题排查及 jvm调参_第19张图片
下载安装

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
内存溢出问题排查及 jvm调参_第20张图片
内存溢出问题排查及 jvm调参_第21张图片

强引用(StrongReference)

我们程序中几乎所有的引用使用的都是强引用。

StringBuilder sb = new StringBuilder();

内存溢出问题排查及 jvm调参_第22张图片
上面通过在堆中创建实例,然后赋值给栈中局部变量 sb 的方式 就是强引用。有如下特点:

  • 强引用可以直接访问目标对象
  • 强引用(存在)指向的对象任何时候都不会被回收,JVM 宁愿抛出 OOM 异常,也不会回收。
  • 强引用可能会导致内存泄漏

注意: 为避免浪费内存,可以在变量 sb 不再使用后通过显示的将变量 sb 置为 null(sb = null),来加速对象的回收。

软引用(SoftReference)

软引用对应的类为 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)也占有一定的资源。

弱引用(WeakReference)

弱引用中的对象具有很短的声明周期,因为在系统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)

虚引用 就是 形同虚设 ,它并不能决定 对象的生命周期。任何时候这个只有虚引用的对象都有可能被回收。因此,虚引用主要用来跟踪对象的回收,清理被销毁对象的相关资源。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设置JVM虚拟内存的三种方式

1. 修改Elipse运行JRE默认JVM参数(全局所有项目)

打开Eclipse,选择Window–Preferences–Java–Installed JREs,编辑选择的jre 在其中的Default VM Arguments: 框中输入 -Xms128m -Xmx512m 等,这样设置 Java 拟虚机内存使用最小是 128M,最大是512M,再单击“OK”

2. 修改Java运行时参数(单个项目)

右击工程 Run AS(Debug As) -->选最下面Run…–> Arguments–>在VM arguments里面填(有内容则追加即可) -Xmx256m 等。这样就可以设置它运行时最大内存为256m

3.修改 apache-tomcat-7.0.76\bin\catalina.bat

第二行加参数即可,用 startup.bat 启动时tomcat时生效的,但是用 eclipse 启动 tomcat 好像不生效

@echo off
set JAVA_OPTS=-Xms128m -Xmx350m
4.修改 Eclipse 的配置文件 [eclipse.ini],

其实是影响的 eclipse 自身的运行(注意eclipse开发工具的运行和我们的业务应用运行基本没关系,内存溢出也是我们的业务应用),上面方式修改的是业务应用的运行内存大小,而对我们发生的具体业务应用的内存不产生丝毫应用,所以这一步基本不要做,而且改了也需要关闭 eclipse 再双击启动,直接菜单重启不生效!(eclipse.ini中的设置控制 【Eclipse运行】的JVM。它们对从Eclipse【运行的程序】没有影响。Eclipse在 运行程序时 启动单独的JVM。运行配置设置控制该JVM。对每个程序都有单独的运行配置,以便可以为每个程序设置不同的内容)

${ECLIPSE_HOME}/eclipse.ini
-Xms256m
-Xmx2048m

内存溢出

1、OutOfMemoryError: PermGen space
PermGen Space指的是内存的永久保存区,该块内存主要是被JVM用来存放class和mete信息的,当class被加载loader的时候就会被存储到该内存区中,与存放类的实例的heap区不同,java中的垃圾回收器GC不会在主程序运行期对PermGen space进行清理。
因此,程序启动时如果需要加载的信息太多,超出这个空间的大小,则会发生溢出。
解决方案:增加空间分配——增加java虚拟机中的XX:PermSize和XX:MaxPermSize参数的大小,其中XX:PermSize是初始永久保存区域大小,XX:MaxPermSize是最大永久保存区域大小。
2、OutOfMemoryError:Java heap space
heap是Java内存中的堆区,主要用来存放对象,当对象太多超出了空间大小,GC又来不及释放的时候,就会发生溢出错误。即内存泄露越来越严重时,可能会发生内存溢出。
解决方案:(1)、检查程序,减少大量重复创建对象的死循环,减少内存泄露。
(2)、增加Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的大小。
3、StackOverFlowError
stack是Java内存中的栈空间,主要用来存放方法中的变量,参数等临时性的数据的,发生溢出一般是因为分配空间太小,或是执行的方法递归层数太多创建了占用了太多栈帧导致溢出。
解决方案:修改配置参数-Xss参数增加线程栈大小之外,优化程序是尤其重要。

四、总结
内存泄漏是堆中的存在无用但可达的对象,GC无法回收。
内存溢出是空间不足的溢出,主要分为 PermGen space 不足、堆不足、栈不足。

你可能感兴趣的:(java,eclipse,java,tomcat)