JVM各种情况内存溢出分析

简介: JVM各种情况内存溢出分析

1 直接内存溢出

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 JVM 规范中定义的内存区域。但这部分内存也被频繁的使用,而且也可能导致 OutOfMemoryError 异常出现。


JDK1.4 中新引入了 NIO 机制,它是一种基于通道与缓冲区的新 I/O 方式,可以直接从操作系统中分配直接内存,即直接堆外分配内存,这样能在一些场景中提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。


2 内存溢出

内存溢出(OutOfMemory,简称OOM)是一个令人头疼的问题,它通常出现在某一块内存空间耗尽的时候。在 Java 程序中,导致内存溢出的原因有很多,其中最常见的有:堆溢出、直接内存溢出、方法区溢出等。


2.1 堆溢出

这种场景最为常见,报错信息:


java.lang.OutOfMemoryError: Java heap space


原因


1、代码中可能存在大对象分配

2、可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。


解决方法


1、检查是否存在大对象的分配,最有可能的是大数组分配

2、通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题

3、如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存

4、还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性


2.2.1 堆溢出案例

//-Xmx60M -Xms60M
public class SimpleHeapOOM {
  public static void main(String[] args) {
    List<String[]> list = new ArrayList<>();
    for (int i = 0; i < 2000; i++) {
      list.add(new String[1024*1024]);
    }
  }
}

执行上述代码报错:


Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  at com.msdn.java.hotspot.gc.SimpleHeapOOM.main(SimpleHeapOOM.java:16)

由于堆空间不可能无限增长,所以我们需要借助前文提到的 MAT 或 VIsualVM 工具,分析 dump 文件(使用-XX:+HeapDumpOnOutOfMemoryError命令生成 dump文件),弄清楚到底是出现了内存泄漏(Memory Leak) 还是内存溢出(Memory Overflow)。


如果是内存泄漏, 可进一步通过工具查看泄漏对象到 GC Roots 的引用链, 找到泄漏对象是通过怎样的引用路径、 与哪些 GC Roots 相关联, 才导致垃圾收集器无法回收它们, 根据泄漏对象的类型信息以及它到 GC Roots 引用链的信息, 一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。


如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查 Java 虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。


2.3 永久代或元空间溢出

方法区和运行时常量池的溢出

-XX:MetaspaceSize=N //设置Metaspace的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置Metaspace的最大大小,默认为-1,即不限制, 或者说只受限于本地内存大小

与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。


那么如何演示方法区溢出呢?首先我们明确方法区的主要职责是用于存放类型的相关信息,如类名、访问修饰符、常量池、字段描述、 方法描述等。对于这部分区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出为止。本文借助 CGLib 直接操作字节码运行时生成了大量的动态类。


报错信息:java.lang.OutOfMemoryError: PermGen spacejava.lang.OutOfMemoryError: Metaspace

原因

永久代是 HotSot 虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等。


JDK8后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:


字符串常量由永久代转移到堆中

和永久代相关的JVM参数已移除

可能原因有如下几种:


1、在Java7之前,频繁的错误使用String.intern()方法

2、运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载

3、应用长时间运行,没有重启


没有重启 JVM 进程一般发生在调试时,如下面 tomcat 官网的一个 FAQ:


Why does the memory usage increase when I redeploy a web application? That is because your web application has a memory leak. A common issue are “PermGen” memory leaks. They happen because the Classloader (and the Class objects it loaded) cannot be recycled unless some requirements are met (). They are stored in the permanent heap generation by the JVM, and when you redeploy a new class loader is created, which loads another copy of all these classes. This can cause OufOfMemoryErrors eventually. (*) The requirement is that all classes loaded by this classloader should be able to be gc’ed at the same time.


解决方法


因为该OOM原因比较简单,解决方法有如下几种:


1、检查是否永久代空间或者元空间设置的过小

2、检查代码中是否存在大量的反射操作

3、dump之后通过mat检查是否存在大量由于反射生成的代理类

4、放大招,重启JVM


GC overhead limit exceeded


这个异常比较的罕见,报错信息:


java.lang.OutOfMemoryError:GC overhead limit exceeded


原因


这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。


解决方法


1、检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。


2、添加参数 -XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。


3、dump内存,检查是否存在内存泄露,如果没有,加大内存。


2.3.1 永久代或元空间溢出案例

//-XX:MaxMetaspaceSize=10M
public class RuntimeConstantPoolOOM {
  static class OOMObject{
  }
  public static void main(String[] args)
      throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    while (true) {
      Enhancer enhancer = new Enhancer();
      enhancer.setSuperclass(OOMObject.class);
      enhancer.setUseCache(false);
      enhancer.setCallback(new MethodInterceptor() {
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
            throws Throwable {
          return methodProxy.invokeSuper(o, args);
        }
      });
      enhancer.create();
    }
  }
}

执行结果为:

Exception in thread "main" org.springframework.cglib.core.CodeGenerationException: java.lang.OutOfMemoryError-->Metaspace
  at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:557)
  at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)

那么在实际应用中都有哪些场景可能出现方法区溢出呢?


1、关于上述 CGLib 操作字节码生成动态类的代码,在当前很多主流框架,如 Spring、 Hibernate 对类进行增强时, 都会使用到 CGLib 这类字节码技术,当增强的类越多,就需要越大的方法区以保证动态生成的新类型可以载入内存。


2、除此之外,很多运行于Java虚拟机上的动态语言(例如Groovy等)通常都会持续创建新类型来支撑语言的动态性,随着这类动态语言的流行,方法区溢出的情况也会更加频繁。


3、大量 JSP 或动态产生 JSP 文件的应用(JSP第一次运行时需要编译为Java类)、基于 OSGi 的应用(即使是同一个类文件, 被不同的加载器加载也会视为不同的类) 等。


2.4 栈溢出

报错信息:


java.lang.OutOfMemoryError : unable to create new native Thread


原因


出现这种异常,基本上都是创建的了大量的线程导致的,以前碰到过一次,通过jstack出来一共8000多个线程。


解决方法


1、通过 -Xss 降低的每个线程栈大小的容量

2、线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:


/proc/sys/kernel/pid_max

/proc/sys/kernel/thread-max

maxuserprocess(ulimit -u)

/proc/sys/vm/maxmapcount

由于 HotSpot 虚拟机中并不区分虚拟机栈和本地方法栈,栈容量只能由-Xss参数来设定。 关于虚拟机栈和本地方法栈, 在《Java虚拟机规范》 中描述了两种异常:


如果线程请求的栈深度大于虚拟机所允许的最大深度, 将抛出 StackOverflowError 异常。

如果虚拟机的栈内存允许动态扩展, 当扩展栈容量无法申请到足够的内存时, 将抛出 OutOfMemoryError 异常。

《Java虚拟机规范》 明确允许Java虚拟机实现自行选择是否支持栈的动态扩展, 而 HotSpot 虚拟机的选择是不支持扩展, 所以除非在创建线程申请内存时就因无法获得足够内存而出现 OutOfMemoryError 异常, 否则在线程运行时是不会因为扩展而导致内存溢出的, 只会因为栈容量无法容纳新的栈帧而导致 StackOverflowError 异常。


栈溢出抛出 StackOverflowError 错误,出现此种情况是因为方法运行的时候栈的深度超过了虚拟机容许的最大深度所致。一般情况下是程序错误所致的,比如写了一个死递归,就有可能造成此种情况。下面我们通过一段代码来模拟一下此种情况的内存溢出。


2.4.1 栈溢出案例

public class StackOOM {
  private int len = 1;
  public void stackOverFlowMethod() {
    len++;
    stackOverFlowMethod();
  }
  public static void main(String[] args) {
    StackOOM stackOOM = new StackOOM();
    try {
      stackOOM.stackOverFlowMethod();
    } catch (Throwable e) {
      System.out.println("stack length: " + stackOOM.len);
      throw e;
    }
  }
}

执行结果为:

stack length: 17850
Exception in thread "main" java.lang.StackOverflowError

那么 HotSpot 虚拟机中是否可以抛出 OutOfMemoryError 呢?我们尝试如下两种方案:

方案一:使用-Xss 参数减少栈内存容量,JDK8 要求栈内存容量至少为 160K,针对上述代码设置如下参数:-Xss160k,执行结果变为:

stack length: 774
Exception in thread "main" java.lang.StackOverflowError

经过测试发现,栈内存容量变小,仍然会抛出 StackOverflowError 异常,异常出现时输出的堆栈深度相应缩小。

方案二:在方法中多定义一些局部变量,增大方法栈中拘捕变量表的长度。

修改代码如下:

  public void stackOverFlowMethod() {
    long l1, l2, l3, l4, l5, l6, l7, l8, l9;
    l1 = l2 = l3 = l4 = l5 = l6 = l7 = l8 = l9 = 100L;
    double d1, d2, d3, d4, d5, d6, d7, d8, d9;
    d1 = d2 = d3 = d4 = d5 = d6 = d7 = d8 = d9 = 100.0;
    len++;
    stackOverFlowMethod();
  }

执行结果为:

stack length: 4817
Exception in thread "main" java.lang.StackOverflowError

根据结果可知,同样抛出 StackOverflowError 异常, 异常出现时输出的堆栈深度相应缩小。


关于这句话“在 HotSpot 虚拟机上是不会由于虚拟机栈无法扩展而导致 OutOfMemoryError 异常——只要线程申请栈空间成功了就不

会有 OOM, 但是如果申请时就失败, 仍然是会出现 OOM 异常的”是什么意思呢?我们上面尝试的两个方案没有出现 OOM 异常,那是因为程序执行过程中线程数是固定的,所以只要线程申请空间成功,即开始执行,就不会有 OOM。那么我们尝试在代码中不断创建新的线程,看看结果如何。


每个线程的开启都要占用栈内存,如果线程数量过大,栈空间使用完毕,也有可能导致 OOM。如下案例所示:

//-Xmx1g
public class MultiThreadOOM {
  public static class SleepThread implements Runnable {
    @Override
    public void run() {
      try {
        Thread.sleep(1000000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
  public static void main(String[] args) {
    for (int i = 0; i < 5000; i++) {
      new Thread(new SleepThread(), "Thread" + i).start();
      System.out.println("Thread" + i);
    }
  }
}

执行结果报错:

Thread4067
Thread4068
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
  at java.lang.Thread.start0(Native Method)

注意,执行上述代码记得先保存当前的工作,Java 的线程是映射到操作系统的内核线程上,创建线程数过多可能对操作系统带来压力,针对上述 OOM 异常,即使使用 -Xss 参数减少栈内存容量,仍然会报相同的错误,又或者使用 -Xmx 降低堆内存大小,结果也样。说明当前系统仅支持创建 4069 个线程。


2.5 非常规溢出

下面这些OOM异常,可能大部分的同学都没有碰到过,但还是需要了解一下


分配超大数组


报错信息 :


java.lang.OutOfMemoryError: Requested array size exceeds VM limit


这种情况一般是由于不合理的数组分配请求导致的,在为数组分配内存之前,JVM 会执行一项检查。要分配的数组在该平台是否可以寻址(addressable),如果不能寻址(addressable)就会抛出这个错误。


解决方法就是检查你的代码中是否有创建超大数组的地方。


swap溢出


报错信息 :


java.lang.OutOfMemoryError: Out of swap space


这种情况一般是操作系统导致的,可能的原因有:


1、swap 分区大小分配不足;


2、其他进程消耗了所有的内存。


解决方案:


1、其它服务进程可以选择性的拆分出去 2、加大swap分区大小,或者加大机器内存大小


本地方法溢出


报错信息 :


java.lang.OutOfMemoryError: stack_trace_with_native_method


本地方法在运行时出现了内存分配失败,和之前的方法栈溢出不同,方法栈溢出发生在 JVM 代码层面,而本地方法溢出发生在JNI代码或本地方法处。


这个异常出现的概率极低,只能通过操作系统本地工具进行诊断,难度有点大,还是放弃为妙。

目录
相关文章
|
12天前
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
6天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
7天前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
4天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
8 1
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
60 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
17天前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
116 9
|
1月前
|
存储 缓存 算法
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
|
23天前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
46 10
|
21天前
|
并行计算 算法 IDE
【灵码助力Cuda算法分析】分析共享内存的矩阵乘法优化
本文介绍了如何利用通义灵码在Visual Studio 2022中对基于CUDA的共享内存矩阵乘法优化代码进行深入分析。文章从整体程序结构入手,逐步深入到线程调度、矩阵分块、循环展开等关键细节,最后通过带入具体值的方式进一步解析复杂循环逻辑,展示了通义灵码在辅助理解和优化CUDA编程中的强大功能。