Java内存问题排查和解决
内存都有啥
编译后地址是逻辑地址,需要经过编译映射到物理内存
MMU负责地址的转换
可用内存 = 物理内存 + 虚拟内存(swap)
RES 实际内存占用
可用内存 = free + buffers + cached
linux使用时,迅速占满内存
堆:JVM堆中的数据是共享的,是占用内存最大的一块区域
虚拟机栈:Java虚拟机栈,是基于线程的,用来服务字节码指令的运行
程序计数器:当前线程所执行的字节码的行号指示器
元空间:方法区的位置,非堆
本地内存:其他的内存占用空间
Java内存管理基本概念:
内存:
- Java内存
- 操作系统
Java内存:
- Java堆内存
- 元空间(堆外)
Java堆内存
- JVM分配的Java内存对象
- 通常使用 -Xmx -Xms 控制大小
元空间
- Metaspace默认无上限
- 原方法区在这里
内存划分:
- JVM进程内存 = 堆内内存 + 堆外内存
- 堆外内存 = 元空间 + CodeCache + 本地内存
- 堆外内存和操作系统剩余内存是此消彼长的关系
- 可分配的内存大小 = 物理内存 - SWAP
32位内存限制4GB,目前ZGC支持16TB内存
控制参数:
- 堆 -Xms -XMx
- 元空间 -XX:MaxMetaspaceSize -XX:MetaspaceSize
- 栈: -Xss
- 直接内存: -XX:MaxDirectMemorySize
- 其他堆外内存无法控制
垃圾回收:
- 自动垃圾回收:JVM自动检测和释放不再使用的内存
- Java运行时JVM会有线程执行GC,不需要程序员显示释放对象
- GC发生的时机由复杂的策略判断,自动触发,不受外部控制
- 不同的垃圾回收算法,甚至不同的JVM版本,回收策略都不一样
内存问题两种形式:
- 内存溢出(OutOfMemoryError)OOM:
- 堆是最常见的情况
- 堆外内存排查困难
- 内存泄漏(MemoryLeak)ML:
- 分配的内存没有得到释放
- 内存一直在增长,有OOM风险
- GC回收时回收不掉
- 能够回收但很快又占满
内存问题影响:
- 发生OOM Error,应用停止
- 频繁GC,GC时间长,GC线程时间片占用高
- 服务卡顿,请求响应时间边长
排查困难:
- 问题时间跨度大
- 问题解决耗费精力
- 现场保护意识不足
简单问题场景:
- 物理内存不足
- 主机物理内存非常少
- 主机上应用进程非常多
- 给应用JVM分配的内存小
- 错误的引用方法,发生了内存泄漏,没有及时切断与GC roots的关系
- 并发量大,计算需要内存大
- 没有控制取数范围
- 加载了非常多的Jar包
- 对堆外内存无限制的使用
垃圾回收器:
- CMS:将在Java14正式移除
- G1:主流应用的垃圾回收器
- ZGC 大容量(16TB),低延迟(10ms)的垃圾回收器
可达性分析法:
- Reference Chain
- 可达性分析法
- GC过程:找到活跃的对象,然后清理其他的
引用级别:
- 强引用:属于最普通最强硬的一种存在,只有在和GC Roots断绝关系时,才会被消灭掉
- 软引用:只有在内存不足时,系统则会回收软引用独享
- 弱引用:当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象
- 虚引用:虚引用主要用来跟踪对象被垃圾回收的活动
对象何时提升(Promotion)
- 常规提升 对象够老
- 分配担保 Survivor空间不够,老年代担保
- 大对象直接在老年代分配
- 动态对象年龄判定
瞬时态和历史态
- 瞬时态:
- 指当时发生的快照类型的元素
- 体积大
- 历史态
- 指按照频率抓取的
- 有固定监控项的资源变动
预防措施:
- 减少创建大对象的频率:比如byte数组的传递
- 不要缓存太多的堆内存数据:使用guava的weak引用模式
- 查询的范围一定要可控:如分库分表中间件;ES等有同样问题
- 用完的资源一定要close掉:可以使用新的try-with-resources语法
- 少用intern:字符串太长,且无法复用,就会造成内存泄漏
- 合理的Session超时时间
- 少用第三方本地代码,使用Java方案替代
- 合理的池大小
- XML(SAX/DOM)、JSON解析时要注意对象大小
总结:
- 问题发现:
- 确保加入了日志和自动转储参数
- 确定物理内存足够:free
- 确定Java进程内存足够:jmap
- 确定主机环境,剩余内存大小
- 查看Glog和其他日志
- 使用jstack对线程进行摸底
- 对堆外内存进行排查
- 保留现场
- 采取措施
- 重复观察
- 问题解决