Skip to content

Latest commit

 

History

History
162 lines (82 loc) · 12 KB

发生了内存泄露或溢出怎么办.md

File metadata and controls

162 lines (82 loc) · 12 KB

发生了内存泄露或溢出怎么办

​ 内存泄露指的是对象一直存在内存中,不会被当做垃圾回收掉,而内存溢出是指剩余内存不够为新的对象分配空间而报错,内存泄露可能会导致内存溢出。也就是说一些对象不再被应用程序使用但垃圾收集无法识别的情况。因此,这些未使用的对象仍然在Java堆空间中无限期地存在。不停的堆积最终会触发java . lang.OutOfMemoryError。

​ 通过参数 -XX:+HeapDumpOnOutOfMemoryError 让虚拟机在出现 OOM 异常的时候 Dump 出内存映像以便于分析。

​ 一般手段是先通过内存映像分析工具对 dump 出来的堆转存快照进行分析哪些对象被怀疑为内存泄漏,哪些对象占的空间最大及对象的调用关系,还可以分析线程状态,可以观察到线程被阻塞在哪个对象上,从而判断系统的瓶颈。

​ 如果是内存泄漏,可进一步通过工具查看泄漏对象到 GC Roots 的引用链。于是就能找到泄漏对象时通过怎样的路径与 GC Roots 相关联并导致垃圾收集器无法自动回收。 找到引用信息,可以准确的定位出内存泄漏的代码位置。

​ 如果不存在内存泄漏,就应当检查虚拟机的参数( -Xmx 与 -Xms )的设置是否适当,是否可以调大;修改代码逻辑,把某些对象生命周期过长,持有状态时间过长等情况的代码修改。

内存泄漏的场景:

(1)使用静态的集合类

​ 静态的集合类的生命周期和应用程序的生命周期一样长,所以在程序结束前容器中的对象不能被释放,会造成内存泄露。

​ 解决办法是最好不使用静态的集合类,如果使用的话,在不需要容器时要将其赋值为 null。

(2)单例模式可能会造成内存泄露(长生命周期的对象持有短生命周期对象的引用)

​ 单例模式只允许应用程序存在一个实例对象,并且这个实例对象的生命周期和应用程序的生命周期一样长,如果单例对象中拥有另一个对象的引用的话,这个被引用的对象就不能被及时回收。

​ 解决办法是单例对象中持有的其他对象使用弱引用,弱引用对象在 GC 线程工作时,其占用的内存会被回收掉。

(3)数据库、网络、输入输出流,这些资源没有显示的关闭

​ 垃圾回收只负责内存回收,如果对象正在使用资源的话,Java 虚拟机不能判断这些对象是不是正在进行操作,比如输入输出,也就不能回收这些对象占用的内存,所以在资源使用完后要调用 close() 方法关闭。

内存溢出场景及解决方案:

​ JVM 运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。

从JVM模型谈十种内存溢出的解决方法

元空间

​ 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

虚拟机栈

​ 每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。

本地方法栈(Native Method Stack)

​ 与虚拟机栈类似,区别是虚拟机栈执行 java 方法,本地方法站执行 native 方法。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法与数据结构没有强制规定,因此虚拟机可以自由实现它。

程序计数器(Program Counter Register)

​ 程序计数器可以看成是当前线程所执行的字节码的行号指示器。在任何一个确定的时刻,一个处理器(对于多内核来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,我们称这类内存区域为“线程私有”内存。

堆内存(Heap)

​ 堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在对上进行分配。jdk1.8后,字符串常量池从永久代中剥离出来,存放在其中。

直接内存(Direct Memory)

​ 直接内存并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中农定义的内存区域。在 JDK1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

​ JVM 运行时首先需要类加载器(classLoader)加载所需类的字节码文件。加载完毕交由执行引擎执行,在执行过程中需要一段空间来存储数据(类比 CPU 与主存)。这段内存空间的分配和释放过程正是我们需要关心的运行时数据区。内存溢出的情况就是从类加载器加载的时候开始出现的,内存溢出分为两大类:OutOfMemoryErrorStackOverflowError

1、堆内存溢出

当出现 java.lang.OutOfMemoryError:Java heap space 异常时,就是堆内存溢出了。

场景

1、设置的 jvm 内存太小,对象所需内存太大,创建对象时分配空间,就会抛出这个异常。

2、而当用户数量或数据量突然激增并超过预期的阈值时,那么就会峰值停止正常运行的操作并触发java . lang.OutOfMemoryError:Java堆空间错误。如果一次请求分配 5m 的内存的话,请求量很少垃圾回收正常就不会出错,但是一旦并发上来就会超出最大内存值,就会抛出内存溢出。

解决方案

​ 首先,如果代码没有什么问题的情况下,可以适当调整-Xms和-Xmx两个jvm参数,使用压力测试来调整这两个参数达到最优值。

​ 其次,尽量避免大的对象的申请,像文件上传,大批量从数据库中获取,这是需要避免的,尽量分块或者分批处理,有助于系统的正常稳定的执行。

​ 最后,尽量提高一次请求的执行速度,垃圾回收越早越好,否则,大量的并发来了的时候,再来新的请求就无法分配内存了,就容易造成系统的雪崩。

2、垃圾回收超时内存溢出

场景

​ 当应用程序耗尽所有可用内存时,GC 开销限制超过了错误,而 GC 多次未能清除它,这时便会引发java.lang.OutOfMemoryError。当 JVM 花费大量的时间执行 GC,而收效甚微,而一旦整个 GC 的过程超过限制便会触发错误(默认的 jvm 配置 GC 的时间超过 98%,回收堆内存低于 2%)。

解决方法

要减少对象生命周期,尽量能快速的进行垃圾回收。

3、Metaspace内存溢出

场景

​ 元空间的溢出,系统会抛出 java.lang.OutOfMemoryError: Metaspace。出现这个异常的问题的原因是系统的代码非常多或引用的第三方包非常多或者通过动态代码生成类加载等方法,导致元空间的内存占用很大。

以下是用循环动态生成class的方式来模拟元空间的内存溢出的:

从JVM模型谈十种内存溢出的解决方法

解决方案

​ 默认情况下,元空间的大小仅受本地内存限制。但是为了整机的性能,尽量还是要对该项进行设置,以免造成整机的服务停机。

1、优化参数配置,避免影响其他JVM进程

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集 。 -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。

2、慎重引用第三方包

对第三方包,一定要慎重选择,不需要的包就去掉。这样既有助于提高编译打包的速度,也有助于提高远程部署的速度。

3、关注动态生成类的框架

对于使用大量动态生成类的框架,要做好压力测试,验证动态生成的类是否超出内存的需求会抛出异常。

4、直接内存内存溢出

场景

​ 在使用 ByteBuffer 中的 allocateDirect() 的时候会用到,很多 javaNIO(像netty) 的框架中被封装为其他的方法,出现该问题时会抛出 java.lang.OutOfMemoryError: Direct buffer memory 异常。如果你在直接或间接使用了 ByteBuffer 中的 allocateDirect 方法的时候,而不做 clear 的时候就会出现类似的问题。

代码示例:

从JVM模型谈十种内存溢出的解决方法

解决方案

如果经常有类似的操作,可以考虑设置参数:-XX:MaxDirectMemorySize,并及时clear内存。

5、栈内存溢出

场景

​ 当一个线程执行一个 Java 方法时,JVM 将创建一个新的栈帧并且把它 push 到栈顶。此时新的栈帧就变成了当前栈帧,方法执行时,使用栈帧来存储参数、局部变量、中间指令以及其他数据。

​ 当一个方法递归调用自己时,新的方法所产生的数据(也可以理解为新的栈帧)将会被 push 到栈顶,方法每次调用自己时,会拷贝一份当前方法的数据并 push 到栈中。因此,递归的每层调用都需要创建一个新的栈帧。这样的结果是,栈中越来越多的内存将随着递归调用而被消耗,如果递归调用自己一百万次,那么将会产生一百万个栈帧。这样就会造成栈的内存溢出。

代码示例:

从JVM模型谈十种内存溢出的解决方法

解决方案

​ 如果程序中确实有递归调用,出现栈溢出时,可以调高 -Xss 大小,就可以解决栈内存溢出的问题了。递归调用防止形成死循环,否则就会出现栈内存溢出。

6、创建本地线程内存溢出

场景

​ 线程基本只占用 heap 以外的内存区域,也就是这个错误说明除了 heap 以外的区域,无法为线程分配一块内存区域了,这个要么是内存本身就不够,要么 heap 的空间设置得太大了,导致了剩余的内存已经不多了,而由于线程本身要占用内存,所以就不够用了。

解决方案

​ 首先检查操作系统是否有线程数的限制,使用 shell 也无法创建线程,如果是这个问题就需要调整系统的最大可支持的文件数。日常开发中尽量保证线程最大数的可控制的,不要随意使用线程池。不能无限制的增长下去。