JVM内存溢出事故回顾
JVM内存溢出的排查方法个工具介绍
事故回顾
• 9:58收到报警,资讯延时1小时。
• 10:10排查出接口全部超时,超时时间2s。
• 去运维那边执行jstat发现元空间沾满了,疯狂fgc。
• 执行jmap -dump 并下载。
• 使用MAT分析,发现有大量的mongo类(动态生成的,名字前缀一样)
。
• 排查代码发现mongoTemplate没有使用单例导致。
• 修改代码并压测,使用VisualVM程序各个指标(发现镜像使用不正确导致
线程数不对)
Java 内存区域详解
01
Java 内存区域详解
对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像 C/C++ 程序开发
程序员这样为每一个 new 操作去写对应的delete/free 操作,不容易出现内存泄漏和
内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内
存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是
一个非常艰巨的任务。运行时数据区域Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。
线程私有的:
○ 程序计数器
○ 虚拟机栈
○ 本地方法栈
线程共享的:
○ 堆
○ 方法区
○ 直接内存 ( 非运行时数据区的一部分)
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
从上面的介绍中我们知道程序计数器主要有两个作用:
• 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行选择、循环、异常处理。
• 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
Java 虚拟机栈
与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 (实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)
局部变量表主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。
• StackOverFlowError : 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
• OutOfMemoryError : 若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也无法提供更多内存的话。就会抛出 OutOfMemoryError 错误。
Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地
方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。
堆
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,
在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实
例以及数组都在这里分配内存。
Java世界中“几乎”所有的对象都在堆中分配,但是,随着JIT编译期的发展与逃逸
分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的
对象都分配到堆上也渐渐变得不那么“绝对”了。从jdk 1.7开始已经默认开启逃逸分
析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去)
,那么对象可以直接在栈上分配内存。Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后
的表现形式还会有几种,比如:
1. OutOfMemoryError: GC Overhead Limit Exceeded : 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
-
java.lang.OutOfMemoryError: Java heap space : 假如在创建新的对象时
, 堆内存中的空间不足以存放新创建的对象, 就会引发java.lang.OutOfM
emoryError: Java heap space 错误。(和本机物理内存无关,和你配置
的内存大小有关!) -
…
方法区
• 方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚
拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一
个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
• 方法区也被称为永久代。
为什么要将永久代 (PermGen) 替换为元空
间 (MetaSpace) 呢?
- 整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会
更小。
当你元空间溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace
你可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。 - 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
- 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了
运行时常量池
• 运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、
方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字
面量和符号引用)
• 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常
量池无法再申请到内存时会抛出 OutOfMemoryError 错误。
直接内存
• 直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义
的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMe
moryError 错误出现。
• JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道
(Channel ) 与缓存区(Buffer ) 的 I/O 方式,它可以直接使用 Native
函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByte
Buffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提
高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
• 本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受
到本机总内存大小以及处理器寻址空间的限制。
元空间(Metaspace)
02
Metaspace 的组成
Metaspace 由两大部分组成: Klass Metaspace 和NoKlass Metaspace 。
Klass Metaspace: Klass Metaspace 就是用来存klass 的, klass 是我们熟知的class 文件在jvm 里的运行时数据结构,不过有点要提的是我们看到的类似A.class 其实是存在heap里的,是java.lang.Class 的一个对象实例。这块内存是紧接着Heap 的,和我们之前的perm 一样,这块内存大小可通过-XX:CompressedClassSpaceSize 参数来控制,这个参数前面提到了默认是1G ,但是这块内存也可以没有,假如没有开启压缩指针就不会有这块内存,这种情况下klass 都会存在NoKlass Metaspace 里,另外如果我们把-Xmx 设置大于32G 的话,其实也是没有这块内存的,因为会这么大内存会关闭压缩指针开关。还有就是这
块内存最多只会存在一块。
NoKlass Metaspace: NoKlass Metaspace 专门来存klass 相关的其他的内容,比如
method , constantPool 等,这块内存是由多块内存组合起来的,所以可以认为是不连续
的内存块组成的。这块内存是必须的,虽然叫做NoKlass Metaspace ,但是也其实可以存
klass 的内容,上面已经提到了对应场景。
Metaspace 内存管理
在metaspace 中,类和其元数据的生
命周期与其对应的类加载器相同,只
要类的类加载器是存活的,在
Metaspace 中的类元数据也是存活
的,不能被回收。
每个加载器有单独的存储空间。
省掉了GC 扫描及压缩的时间。
当GC 发现某个类加载器不再存活了,
会把对应的空间整个回收。
排查工具
03
# 排查工具(Jstat )
监视元空间大小的最简单方法是使用JDK中提供的jstat工具。当与选项-gc一起使
用时,它提供以下信息:
• S0C:第一个Survivor 区的大小
• S1C:第二个Survivor 区的大小
• S0U:第一个Survivor 区的使用大小
• S1U:第二个Survivor 区的使用大小
• EC:Eden区的大小
• EU:Eden区的使用大小
• OC:老年代大小
• OU:老年代使用大小
• MC:方法区大小
• MU:方法区使用大小
• CCSC:Klass Metaspace 空间大小
• CCSU:Klass Metaspace 使用大小
• YGC:新生代垃圾回收次数
• YGCT:新生代垃圾回收消耗时间
• FGC:老年代垃圾回收次数
• FGCT:老年代垃圾回收消耗时间
• GCT:垃圾回收消耗总时间
VisualVM
VisualVM,能够监控线程,内存情况,查看方法
的CPU时间和内存中的对 象,已被GC的对象,
反向查看分配的堆栈(如100个String对象分别由
哪几个对象分配出来的).
从界面上看还是比较简洁的,左边是树形结构,自
动显示当前本机所运行的Java程序,还可以添加
远程的Java VM,其中括号里面的PID指的是进
程ID。OverView界面显示VM启动参数以及该V
M对应的一些属性。Monitor界面则是监控Java
堆大小,Permgen大小,Classes和线程数量。j
dk不同版本中界面会不太一致,如有的含cpu监控
,有的则不含(jdk1.6.0_10 未包含)。
MAT
MAT 是Memory Analyzer tool 的
缩写,是一种快速,功能丰富的Java
堆分析工具,能帮助你查找内存泄漏
和减少内存消耗。很多情况下,我们
需要处理测试提供的hprof 文件,分
析内存相关问题,那么MAT 也绝对
是不二之选。 Eclipse 可以下载插件
结合使用,也可以作为一个独立分析
工具使用,下载地址:
https://www.eclipse.org/mat/do
wnloads.php
延伸点
上面我们使用工具jump 了,那怎么去服务器上jump 呢?
○ 找运维 jmap -dump:format=b,file=<dumpfile.hprof>
jump 出来的文件怎么下载呢?
○ 云平台的文件下载入口,找容器配置的第一个运维审核即可。
排查方向
04 重点排查
以下几点
检查代码中是否有死循环或递归调用。
检查是否有大循环重复产生新对象实体。
检查List 、MAP 等集合对象是否有使用完后
,未清除的问题。List 、MAP 等集合对象会
始终存有对对象的引用,使得这些对象不能被
GC 回收。
检测是否频繁动态生成类。
使用内存查看工具动态查看内存使用情况。
THANKS