目录
一. GC 日志分析
- 生产为了定位GC问题,需要开启GC日志,进行相关配置
- 还有一种生成内存映像文件dump文件,分为手动方式,自动方式两种
二. 内存泄漏,溢出
- 什么是内存泄漏, 什么是内存溢出
- 内存泄漏是指GC垃圾回收的速度跟不上内存消耗的速度,造成OOM的情况
- 内存溢出是指程序员在申请内存时,没有足够的内存空间供其实用,OutOfMemoryError
-
OOM前一定会有GC吗: 不一定,正常情况下一定会有GC,会触发执行垃圾收集,例如在使用软引用时,当JVM内存不够时会去尝试收集软引用对象,但是如果我们创建一个差大对象,例如一个超过堆的最大值的超大数组,JVM在创建这个对象前判断存不下这个对象会直接报OutOfMemoryError
-
排查问题时要知道内存为什么不够用
- JVM堆内存设置不合理,通过"-Xms:最小值", "-Xmx:最大值"两个命令来设置堆内存大小
- 代码中出现大量大对象,并且长时间不能被垃圾回收器收集
- java8以前使用永久代时空间大小是固定的,虽然可以通过"-XX:MaxPermSize"指令设置永久代的大小,但是这个大小很难去确定,并且在永久代执行垃圾回收的频率比较低,实际比元空间更容易出现OOM,会抛出"OutOfMemoryError: PermGen space"
- java8后永久代修改为元空间,元空间跟随物理内存,如果加载jar包过多,直接内存不足,可能也会造成OOM,但是报出的是"java.lang.OutOfMemoryError: Metaspace"
导致内存泄漏的八种情况
出现内存泄漏的八种情况: 静态集合类, 单利模式, 内部类持有外部类, 各种连接,如数据库连接,网络连接和IO连接等, 变量不合理的作用域, 改变哈希值, 缓存泄漏, 监听器和回调
静态相关导致内存泄漏
- 静态集合类导致的内存溢出解释: 在下图方法中正常情况下oomTest()方法执行完毕后,该方法内创建的局部变量obj应该被回收掉,但是由于在后续步骤中将这个obj对象添加到了今天集合list中,由于静态生命周期与jvm程序一致,然后静态集合list一直持有obj,造成obj无法被回收,出现内存泄漏
- 单利模式导致内存溢出,跟静态集合导致内存溢出底层原因类似, 通常情况下创建单利模式的步骤为:私有化构造器,提供外部访问的静态方法,通过这个静态方法返回一个instance, 为了中当前程序中该对象只有一个instance,通常也使用静态关键字修饰, 由于静态成员生命周期跟随JVM,如果当前这个单利的instance持有外部对象引用,会造成这个持有的外部对象无法被回收,也是一种泄漏
内部类持有外部类导致内存泄漏
- 导致原因: 内部类持有外部类,通过外部类实例对象方法返回一个内部类实例对象,这个内部类对象被长期引用,即使外部类实例对象不再被使用,由于内部类持有外部类对象实例,造成这个外部类对象不会被回收,导致内存泄漏
各种连接资源导致内存泄漏
变量不合理的作用域
改变哈希值导致内存泄漏
- 导致原因: 将一个对象存储到HashSet集合中,如果后续修改这个对象的哈希值,造成与存储进HashSet时的哈希值不同,会出现使用contains方法检索HashSet中该对象,返回找不到的情况, 导致无法在HashSet删除这个对象,进而导致内存泄漏,这也是String设置为不可变类型的一个原因,通过不可变,让我们可以放心的将String存入HashSet中, 使用String作为HashMap的key值
缓存导致内存泄漏
- 应该说是读取数据时,由于测试环境数据较少,而实际生产上数据较多,由于某个功能加载该表或着缓存中的数据到内存进行缓存时导致项目启动慢,到直接夯死的情况, 对于这种问题,考虑实际生产上数据量,考虑多线程,分批次处理, 还有使用WeakHashMap弱引用作为缓存容器,该Map的特点是当除了自身有对key的引用外,如果该key没有其它应用,那么会自动丢弃,如果频繁使用的缓存使用软引用,因为若引用是只要垃圾回收执行都会被回收掉,而软引用是内存不足时才会回收掉
监听器回调导致内存泄漏
- 例如基于事件监听模式开发的功能, 当接收到任务时插入事件,后续通过监听去消费这个事件,假设消费不及时,一直插入,也会造成内存泄漏,解决回调被理解当做垃圾回收的最佳方法,也是保存为弱引用WeakHashMap或软引用键中
三. OOM案例
栈溢出
- 线程请求的栈深度大于虚拟机允许的最大深度 StackOverflowError
- 虚拟机在扩展栈深度时,无法申请到足够的内存空间 OutOfMemoryError
堆内存溢出案例
- 设置虚拟机相关参数:下图中设置堆的最小初始化内存,最大初始化内存都是200M,进行示例时可以适当改小为30M
- “-XX:+PrintGCDetails” : 设置打印垃圾回收信息
- “-XX:MetaspaceSize=64”: 设置元空间大小(设置这个值如果过小可能会报Metaspace size 太小异常JVM直接退出)
- “-XX:+HeapDumpOnOutOfMemoryError”: 设置JVM发送OOM时生成DUMP文件
- “-XX:HeapDumpPath=/heapdump.hprof”: 设置生成的Dump文件位置与文件名
- “-XX:+PrintGCDateStamps”: 设置打印GC时的时间,也可以使用"-XX:+PrintGCTimeStamps"
- “-Xms200M” 设置堆初始化最小大小, “-Xmx200M” 设置堆内存最大大小
- “-Xloggc:log/gc-oomHeap.log”: 指定生产GC 日志的位置
- 测试代码
public class OomDemo {
//测试方法,while无限循环不停的想strList集合中添加数据
public void test() {
ArrayList<String> strList = new ArrayList<>();
while (true) {
strList.add("aaaaaaa");
}
}
//模拟生产环境运行测试
public static void main(String[] args) {
OomDemo demo = new OomDemo();
demo.test();
}
}
- 运行Main方法会发现抛出"java.lang.OutOfMemoryError: Java heap space"
使用JDK自带jvisualvm.exe分析生成的dump文件
- 在JDK的bin目录下找到jvisualvm.exe 运行,点击文件–>装入需要分析的dump文件"heapdump.hprof"
- 分析后查看导致OOM异常方法
- 点击进入异常方法,可以看到实际是哪一行代码发生的异常
- 点击类可以查看那个对象过多发生的异常
使用Eclipse Memory Analyzer 分析生成的dump文件
解决
- 检查是否有大对象分配,大数组分配
- 通过jmap命令,把堆内存dump下来,使用MAT等工具分析是否存在内存泄漏
- 如果不存在泄漏加大堆内存"-Xmx"
- 检查是否有Finalizable对象,大量的不可达未回收垃圾对象
元空间溢出案例
- 首先元空间又被称为方法区,跟堆一样,被各个线程共享,用于存储已被虚拟机加载的类元信息,长岭,变异后的代码等,元空间存在溢出情况,也会被垃圾回收回收,回收的主要目标是针对常量池,与类型的卸载
- 例如加载第三方jar报过多,动态生成类过多,Class类型在元空间存放不下,会OOM
- 案例演示修改JVM参数
- 解决: 该OOM原因比较简单
- 检查永久代或元空间设置是否过小
- dump日志分析检查代码中是否存在大量的反射操作,存在大量通过反射生成代理类操作
GC overhead limit exceeded
- 官方解释:检查超过98%的时间用来做GC回收,但是只回收了不到2%的堆内存,就会抛出verhead limit exceeded异常, 有点像检测到垃圾回收,生成垃圾的速度大于垃圾回收的速度,也会出现oom
- 解决
- 检查项目中是否存在大量的死循环没有满足跳出条件,是否使用大内存的代码,进行优化
- 添加"-XX:-UseGCOverheadLimit" 禁用这个检查
- dump内存,检查是否存在内存泄漏,如果没有加大内存
线程溢出
- java.lang.OutOfMemoryError: unable to create new native Thread: 创建大量线程导致内存溢出
- 在java中当创建一个线程时JVM会创建一个Thread对象,同时创建一个操作系统线程,这个线程占用的是系统剩余内存,也就是(MaxProcessMemory进程可寻址的最大内存 - JVM内存 -ReservedOsMemory保留的操作系统内存),进而得出JVM内存越大,创建的线程越少
- 解决线程溢出