内存调优
什么是内存泄漏、内存泄漏?
- 内存泄漏:在Java中如果不再使用一个对象,但是该对象依然在GC ROOT的引用链上,这个对象就不会被垃圾回收器回收。
- 内存溢出:内存的使用量超过了Java虚拟机可以分配的上限,最终产生了内存溢出OutOfMemory的错误。
内存泄漏的原因?
- 持续的内存泄漏:内存泄漏持续发生,不可被回收同时不再使用的内存越来越多,就像滚雪球一样雪球越滚越大,最终内存被消耗完无法分配更多的内存去使用,导致内存溢出。
- 并发请求问题:用户通过发送请求向Java应用获取数据,正常情况下Java应用将数据返回之后,这部分数据就可以在内存中被释放掉。但是由于用户的并发请求量有可能很大,同时处理数据的时间很长,导致大量的数据存在于内存中,最终超过了内存的上限,导致内存溢出。
内存泄漏原因
代码中
- equals()和hashCode() 导致的内存泄漏。没有重写正确的这两个方法。在使用HashMap的场景下,如果使用这个类对象作为key,HashMap在判断Key是否已经存在时会使用这些方法,如果重写方式不正确,会导致相同的数据被保存多份。
- 内部类引用外部类。非静态的内部类默认会持有外部类,垃圾回收时无法回收外部类。使用静态内部类,或静态方法。
- ThreadLocal的使用。手动创建的线程会自动回收,线程池创建的线程不会自动回收。需要调用ThreadLocal中的remove方法清理对象。参考问题 ThreadLocal中为什么要使用弱引用。
- String的intern方法。把字符串加入字符串常量池。
- 通过静态字段保存对象。大量数据在静态变量中被长期引用,数据就不会释放。减少保存在静态变量中,若不再使用则必须删除或设置为null。单例模式中,尽量使用懒加载,而不是立即加载。@Lazy。Bean中不要长期存放大对象,如果是缓存,设置过期时间。
- 资源没有正常关闭。不一定出现内存泄漏,会导致close方法不被执行。Java7开始,申请资源放在try()里面可以用于自动关闭资源。
并发中
通过发送请求向Java应用获取数据,正常情况下数据返回后,即可释放数据。当并发量很大,同时处理数据的时间长,导致大量的数据存在于内存中,导致内存溢出。
内存泄漏的解决方案
- 发现问题,通过监控工具尽可能尽早地发现内存慢慢变大的现象。
- 诊断原因,通过分析内存快照或者在线分析方法调用过程,诊断问题产生的根源,定位到出现问题的源代码。MAT打开hprof文件。
- 修复源代码中的问题,如代码bug、技术方案不合理、业务设计不合理等等。
- 在测试环境验证问题是否已经解决,最后发布上线。
-XX:+HeapDumpOnOutOfMemoryError #OOM时打印内存快照
-XX:HeapDumpPath=D:\jvm\dump\test1.hprof
-XX:+HeapDumpBeforeFullGC #可以在FullGC之前就生成内存快照
jmap -dump:live #命令导出内存快照
heapdump #arthas中
MAT内存泄漏检测的原理
支配树。
监控Java内存的常用工具
JDK自带的命令行工具:
jps 查看java进程,打印main方法所在类名和进程id
jmap 生成堆内存快照;打印类的直方图
第三方工具:
- VisualVM
- Arthas
- MAT 堆内存分析工具
- Prometheus + Grafana
在线定位
- Jmeter插件,gc插件。
- arthas stack命令在线定位步骤,可以定位类。
- btrace脚本,可以指定类,监控的方法。灵活性高。
内存溢出案例
- 分页查询文章接口:单个文章对象占用内存量较大。限制单词访问条数;不需要获取文章内容;高峰期对微服务限流保护。
- Mybatis:判断ids是否存在id的接口。foreach进行sql拼接时,会在内存中创建对象,占用空间。限制参数中最大的id个数;将id缓存到redis或内存缓存中,通过缓存校验。
- 导出大文件:excel文件导出如果使用POI的XSSFWorkbook,在大数据量下占用大量内存。使用poi的SXSSFWorkbook;hutool中BigExcelWriter;easy excel。
- ThreadLoacl:在拦截器中,ThreadLocal清理的代码被错误的放在postHandle中,如果接口发生了异常,这段代码不会调用到,这样就产生了内存泄漏,将其移动到afterCompletion就可以了。
- 文章内容审核接口:SpringBoot中@Async注解异步审核;生产者消费者模式队列持久化到数据库;mq消息队列,保存文章数据