目录
内存泄漏
定义
当某些对象不再被应用程序所使用,但是由于仍然被引用,而导致垃圾收集器不能移除他们,释放相关资源。一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出
分类
• 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
• 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
• 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
• 隐式内存泄漏。 程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
泄露场景
- 静态集合类 / 长生命周期的对象持有短生命周期对象的引用 / 单例模式 /类加载器
- 各种连接未调用close方法及时关闭,如数据库连接、网络连接和IO连接等
- 变量不合理的作用域
- 内部类持有外部类
- 对象存入集合后改变哈希值,无法找到对象
- 持有过期引用(栈先增长,在收缩,那么从栈中弹出的对象将不会被当作垃圾回收,即使程序不再使用栈中的这些队象,他们也不会回收,因为栈中仍然保存这对象的引用,俗称过期引用)
- 缓存泄漏(将对象放入缓存中被遗忘)
- 监听器和回调(客户端在实现的API中注册回调,却没有显式的取消,那么就会积聚)
解决方式
- 尽量减少使用静态变量,类的静态变量的生命周期和类同步。
- 声明对象引用之前,明确内存对象的有效作用域,尽量减小对象的作用域,将类的成员变量改写为方法内的局部变量;
- 减少长生命周期的对象持有短生命周期的引用;
- 使用StringBuilder和StringBuffer进行字符串连接,Sting和StringBuilder以及StringBuffer等都可以代表字符串,其中String字符串代表的是不可变的字符串,后两者表示可变的字符串。如果使用多个String对象进行字符串连接运算,在运行时可能产生大量临时字符串,这些字符串会保存在内存中从而导致程序性能下降。
- 对于不需要使用的对象手动设置null值,不管GC何时会开始清理,我们都应及时的将无用的对象标记为可被清理的对象;
- 各种连接(数据库连接,网络连接,IO连接)操作,务必显示调用close关闭。
排查方案
- 堆不断增长导致FULL GC
- 查看哪些类被加载和卸载(-XX:+TraceClassLoading和-XX:+TraceClassUnloading)
- 使用pmap -x 持续观察内存地址空间的变化
内存溢出
定义
程序申请内存时,没有足够的内存供申请者使用,此时就会报错out of memory(OOM)
常见原因
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 代码中存在死循环或循环产生过多重复的对象实体;
- 使用的第三方软件中的BUG;
- 启动参数内存值设定的过小
溢出场景
- java堆内存溢出(适当调大初始内存分配大小)
- java堆内存泄漏
- 垃圾回收超时内存溢出(减少对象生命周期,尽量能快速的进行垃圾回收)
- Metaspace内存溢出(适当调整初始空间大小以及最小/最大Metaspace剩余空间容量百分比,减少为分配空间所导致的垃圾收集)
- 直接内存内存溢出(ByteBuffer中的allocateDirect()时未及时clear)
- 栈内存溢出(调整栈高,防止死循环)
- 创建本地线程内存溢出(内存本身不够或heap的空间设置得太大)
- 超出交换区内存溢出(增加系统交换区的大小)
- 数组超限内存溢出(数组长度要在平台允许的长度范围之内)
- 系统杀死进程内存溢出(升级系统内存)
排查方案
- 看当前所有java进程是否正常
- 通过JConsole、jvisualvm分析dump日志
- 通过arthas排查
- 看堆栈信息有无异常
堆外内存
堆内内存
缺点
- GC是有成本的,堆中的对象数量越多,GC的开销也会越大
- 使用堆内内存进行文件、网络的IO时,JVM会使用堆外内存做一次额外的中转,也就是会多一次内存拷贝
堆外内存
定义
内存对象分配在Java虚拟机堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响,属于用户空间
优点
- 直接使用堆外内存可以减少一次内存拷贝: 当进行网络 I/O 操作、文件读写时,堆内内存都需要转换为堆外内存,然后再与底层设备进行交互。
- 降低 JVM GC 对应用程序影响:因为堆外内存不受 JVM 管理。
- 堆外内存可以实现进程之间、JVM 多实例之间的数据共享。
实现方式
- 通过ByteBuffer.java#allocateDirect得到以一个DirectByteBuffer对象,堆外内存 DirectBuffer 创建和销毁的代价相对较高,一般都会采用复用方式,通过 Java Reference 机制来释放该内存块
- 直接调用Unsafe.java#allocateMemory分配内存,但Unsafe只能在JDK的代码中调用,一般不会直接使用该方法分配内存
使用场景
- 适合长期存在或能复用的场景
- 适合注重稳定的场景
- 适合简单对象的存储
- 适合注重IO效率的场景
回收方式
- Full GC 时以及调用 System.gc(): 通过 JVM 参数 -XX:MaxDirectMemorySize 指定堆外内存的上限大小,当堆外内存的大小超过该阈值时,就会触发一次 Full GC 进行清理回收,如果在 Full GC 之后还是无法满足堆外内存的分配,那么程序将会抛出 OOM 异常。
- 使用unsafe.freeMemory(address); 来回收:DirectByteBuffer 在初始化时会创建一个 Cleaner 对象,Cleaner 内同时会创建 Deallocator,调用 Deallocator#run() 来回收。
堆外内存溢出问题
当创建很多DirectByteBuffer对象,占了大量堆外内存,然后这些DirectByteBuffer对象还无GC线程来回收,那就不会释放,导致堆外内存OOM
常见解决方式
- 内存设置不合理,导致DirectByteBuffer对象一直慢慢进入老年代,堆外内存一直无法释放
- 设置了-XX:+DisableExplicitGC,导致Java NIO无法主动提醒去回收掉一些垃圾DirectByteBuffer对象,也导致了无法释放堆外内存
排查方法
- 看监控:收到监控告警,去监控平台 CAT 查看整个集群的各项指标。
- 猜一猜:怀疑可能出现问题的地方,并去 Review 代码。
- 硬头皮:查看日志文件,查看对应堆栈信息
- 上手段:代码中打点日志来进一步监控(注意:这里直接改生产代码,看生产日志)
- 模拟下:线下模拟,复现场景,线下验证
- 上生产:线上验证