一、前言
如何定位和解决 Android App 因为内存不足(Java OOM)引发的线上问题一直是业界的难题。崩溃现场能抓取到的常规信息中并不包括内存分配详情——不了解内存被谁持有,自然也无法追查内存不足的根源。
针对这个问题,Client Infra 和头条抖音等业务方合作,通过一系列技术调研,自研了一套 基于 Hprof 内存快照的线上 Java OOM 归因方案 ,在内部广泛应用并取得了极佳的效果。曾帮助Helo在一个双月内 优化了80%的 Java OOM 问题,次日存留增长了2+% 。
在 火山引擎 MARS-APMPlus 应用性能监控 平台对外提供该解决方案后,美篇作为早期接入客户,也同样取得了双月周期 减少80% Java OOM 的好成绩,深受客户好评。
接下来本文将会从 Java 内存基础开始,详细介绍方案的底层原理与技术细节。希望大家能通过方案了解MARS-APMPlus 应用性能监控平台,加入我们的 MARS-APMPlus 应用性能监控 企业助力行动 , 帮助团队打造极致的用户体验 。
二、Java 内存基础
2.1 Java 内存优化的重要性
内存是计算机的稀缺资源,操作系统本身也通过虚拟内存等方式来充分的使用内存资源。
如果Java 堆内存占用过多,JVM 频繁GC会引起App的卡顿,影响App的易用性 。
更严重的Java 堆内存使用超过虚拟机限制会导致OOM崩溃,影响App的可用性 。
从App的易用性和可用性来说,Java 内存的优化还是十分重要的,特别是用户使用应用的崩溃问题,应该得到有效解决。
2.2 为什么会Java OOM崩溃
Java OOM,全称是 Java Out Of Memory
,字面意思是说Java 虚拟机的内存用完。Java有一个相关的异常类 java.lang.OutOfMemoryError
,官方有如下说明:
Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector.
就是说,当Java 虚拟机没有更多的内存可以为对象分配空间,垃圾回收器也没有更多的空间可以回收时,就会抛出这个Error。
这里面有几个关键点,理解这几个关键点,我们就会理解为什么会Java OOM崩溃
-
Java虚拟机都有哪些内存区域
-
垃圾回收器是如何工作回收内存的
-
每个对象占据多大的内存空间
-
Java 虚拟机当前的内存空间状态以及OOM是如何发生的
下面会以简洁的方式介绍这几个关键的知识点。
2.1.1 Java虚拟机的内存区域
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域,如下图所示:
下面是每个区域的一个概要说明:
名称 | 说明 |
---|---|
PC Register |
称为程序计数器, 看作是当前线程所执行的字节码的行号指示器 |
JVM Stack |
也称为虚拟机栈,记录每个栈帧(Frame)中的局部变量、方法返回地址等 |
Native Method Stack |
本地 (原生) 方法栈,是调用操作系统原生本地方法时,所需要的内存区域 |
Heap |
堆内存区,也是 GC 垃圾回收的主要场所,用于存放类的实例对象 |
Method Area |
方法区,主要存放类结构、类成员定义,static 静态成员等 |
Runtime Constant Pool |
运行时常量池,比如:字符串等 |
其中我们需要重点关注的是线程间共享的 Heep 堆内存区域。这部分区域是GC垃圾回收的主要场所,用于存放类的实例对象。我们最常见的Java OOM都是因为堆内存使用超出虚拟机最大可用内存阈值导致的崩溃。垃圾回收机制也是针对堆内存部分。
2.1.2 垃圾回收器是如何工作回收内存的
Java 虚拟机有自动内存管理机制,通过垃圾回收器来管理内存,一旦确定程序不再使用某块内存,它就会将该内存回收。
垃圾回收器当前主要通过可达性分析算法判断一个对象是否可以被回收:通过一系列称为GC Roots的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即对象到GC Roots不可达),则证明此对象已死、可回收。下图灰色部分即为可回收的内存对象。
GC Roots是可以从堆外部访问的对象,例如Java线程当前活跃的栈帧里指向GC堆里的对象的引用,就是当前正在被调用方法的引用类型的参数和局部变量等。
垃圾回收有不同的收集算法,和不同类型的垃圾收集器,这里只是概述背景不再详细说明。是否可回收的核心是判断一个对象是否到GC Roots不可达,不可达则对象会被回收释放内存空间。
这里我们知道了一个对象在什么情况下被回收的。如果在内存里没有被回收,那就是因为有GC Root对它持有引用。在内存充足并有足够大的连续空间时,虚拟机会创建对象正常分配内存。
2.1.3 对象占据多大的内存空间
上面我们知道了一个对象是如何被回收的,那么内存中的对象到底占据多大的内存呢。这里会先介绍