【JVM】如何定位、解决内存泄漏和溢出

简介: 【JVM】如何定位、解决内存泄漏和溢出

1.概述

常见的几种JVM内存溢出的场景如下:

Java堆溢出: 错误信息: java.lang.OutOfMemoryError: Java heap space 原因:Java对象实例在运行时持续创建,但不再使用的对象没有及时被垃圾回收器回收,导致堆内存耗尽。 解决方案:增加堆内存大小(-Xms和-Xmx参数),优化对象生命周期管理,减少不必要的大对象或者长时间存在的临时对象。


永久代/元空间溢出(取决于Java版本): 在Java 8及以前版本中,永久代存储类信息、常量池、静态变量等数据,若空间不足会抛出java.lang.OutOfMemoryError: PermGen space错误。 在Java 8之后,永久代已被元空间取代,元空间直接使用本地操作系统内存,可能出现java.lang.OutOfMemoryError: Metaspace。 解决方案:增大永久代或元空间的大小,检查代码是否有大量动态加载类或者反射操作生成过多类信息的情况。


栈空间溢出: 错误信息: java.lang.StackOverflowError 原因:递归调用过深或线程栈帧过大,导致线程栈空间耗尽。 解决方案:调整栈的大小(-Xss参数),改进算法避免深度递归,合理控制线程数量或每个线程栈的大小。


首先栈溢出定位很简单,直接异常栈就会告诉,去调整代码逻辑即可。这里着重要聊一下的是元空间溢出和堆溢出。


元空间溢出


JDK1.8及其以后版本,元空间替代了永久代,其主要用于存储类的元数据信息,包括类的结构信息(如字段、方法、接口、常量池等)、运行时常量池、方法字节码、静态变量等。也就是说类被加载了,其相关信息就会存在元空间中。


此处也许有些读者会有疑问:


类是要在被用到的时候才会加载,也就是一般我们new对象的时候对应的类才会被加载,那么存在元空间在堆之前被撑爆的情况吗?


答:


当然是存在的,只要你的元空间比你的堆小,或者频繁用Class.forName()、ClassLoader.loadClass()等反射的手法来加载类,但是不new对象,也能把元空间撑爆了。


元空间溢出其实是比较难遇见的,但是定位方法其实不难,直接代码全局搜Class.forName之类的语法基本就能定位元凶。


接下来本文要讲的重点是生产中最容易遇见的一种JVM内存溢出——堆溢出,以及比堆溢出藏得更深的隐形杀手——内存泄漏,这两者如何定位以及解决。

2.堆溢出、内存泄定位及解决办法

2.1.示例代码

直接的堆溢出从异常栈信息里是能看出哪里造成的OOM,很容易定位:

难定位的是哪种?怕的是内存泄漏,也就是堆还没有撑爆,但是就是在要爆不爆之间徘徊,造成频繁的GC,由于GC的时候是要”Stop The World“,会暂停所有JAVA线程的工作,这自然会浪费CPU资源,外界的感知就是”变慢了“。这里我们详细的来聊一聊如何定位内存泄漏的问题。


测试代码:

//定义一个类,该类中一旦调用一个方法,就会持续让List持有一个个1024KB大小的内存空间,但是又不会直接撑爆heap
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.util.ArrayList;
public class MyUser {
    private Byte[] bytes;
    ArrayList<byte[]> list = new ArrayList();
    public static final int OBJECT_SIZE = 1024 * 1024; // 每个对象占用1MB空间
    public static final double HEAP_UTILIZATION_RATIO = 0.8; // 目标堆利用率
    public MyUser(){}
    public void callTest(){
        try {
            MemoryMXBean memoryMxBean = ManagementFactory.getMemoryMXBean();
            long maxMemory = Runtime.getRuntime().maxMemory();
            while (true) {
                // 检查当前堆内存使用情况,如果已超过目标利用率,则退出循环
                long currentHeapUsage = memoryMxBean.getHeapMemoryUsage().getUsed();
                if ((double) currentHeapUsage / maxMemory >= HEAP_UTILIZATION_RATIO) {
                    System.out.println("当前堆内存使用率达到目标利用率,程序即将退出...");
                    break;
                }
                // 创建一个大对象
                byte[] largeObject = new byte[OBJECT_SIZE];
                // 将大对象添加到列表中
                list.add(largeObject);
                // 添加一个延时,模拟其他操作,便于观察
                Thread.sleep(100); // 等待100毫秒
                // 可以在此处添加额外的日志输出或监控代码,用于记录GC信息和内存状态
            }
            // 清理资源,防止后续分析时误判
            list.clear();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
//用junit开始测试
@Test
public void test02() throws Exception{
  MyUser myUser = new MyUser();
  myUser.callTest();

2.2.抓堆快照

运行上面的示例代码,打开jvisualVM,开始监控程序,可以看到:

垃圾回收是有在进行的,而且频率不低,但是堆的大小一直在扩,被占用率一直在攀升,说明并没有被回收掉,存在严重的内存泄露。

这时候就需要把堆的dump抓下来看看了。右上角有抓heap dump的选项。

2.3.分析堆快照

通过MAT看看了,看看到底是有哪些东西占着内存一直没被回收掉。

MAT是eclipse旗下的一款heap的分析工具,可以用来专门分析heap dump。下载地址:

Eclipse Memory Analyzer | projects.eclipse.org

MAT和JDK有版本适配关系!千万别下错了,作者用的JDK,下载的1.8.1版本:

还要注意下载的安装包和操作系统之间也是有严格的适配关系的,作者第一次就下成了x86而不是x86_64,确定好自己的操作系统平台下对应的:

用MAT打开抓下来的heap dump:

工具会分析显示除怀疑内存泄漏的地方:

其实从饼状图上已经可以看出端倪,饼状图显示了可能存在内存泄漏的对象和该对象持有的内存的对比,这个对象自身占的内存大小只有浅灰色的一小条,但是其持有的内存居然达到了300多MB,很明显的内存泄漏的情况。

我们初步断定了存在内存泄漏,接下来当然是要定位具体位置,然后才好解决它。

展开详细信息,可以看到怀疑是Main线程上的一个MyUser类型里面的一个类型为ArrayList名叫list的成员变量造成了内存泄漏:

接下来去看这里的代码逻辑就行了。

目录
相关文章
|
27天前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
211 1
|
26天前
|
监控 JavaScript 算法
如何使用内存监控工具来定位和解决Node.js应用中的性能问题?
总之,利用内存监控工具结合代码分析和业务理解,能够逐步定位和解决 Node.js 应用中的性能问题,提高应用的运行效率和稳定性。需要耐心和细致地进行排查和优化,不断提升应用的性能表现。
176 77
|
17天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
22天前
|
监控 算法 Java
jvm-48-java 变更导致压测应用性能下降,如何分析定位原因?
【11月更文挑战第17天】当JVM相关变更导致压测应用性能下降时,可通过检查变更内容(如JVM参数、Java版本、代码变更)、收集性能监控数据(使用JVM监控工具、应用性能监控工具、系统资源监控)、分析垃圾回收情况(GC日志分析、内存泄漏检查)、分析线程和锁(线程状态分析、锁竞争分析)及分析代码执行路径(使用代码性能分析工具、代码审查)等步骤来定位和解决问题。
|
26天前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
27天前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
21 3
|
27天前
|
存储 缓存 监控
Elasticsearch集群JVM调优堆外内存
Elasticsearch集群JVM调优堆外内存
45 1
|
1月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
1月前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
20 1
|
2月前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
55 10