|
技术干货哪里找?点击上方蓝字关注我们!一、前言如何定位和解决 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,官方有如下说明:ThrownwhentheJavaVirtualMachinecannotallocateanobjectbecauseitisoutofmemory,andnomorememorycouldbemadeavailablebythegarbagecollector.就是说,当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 对象占据多大的内存空间上面我们知道了一个对象是如何被回收的,那么内存中的对象到底占据多大的内存呢。这里会先介绍一个概念Dominator Tree支配树,Dominator Tree有以下几个定义:对象XDominator Tree(支配)对象Y,当且仅当在对象树中所有到达Y的路径都必须经过 X对象Y的直接Dominator Tree,是指在对象引用关系中距离Y最近的DominatorDominator Tree利用对象引用关系构建出来对象引用关系和Dominator Tree的对应关系如下:如上图,因为A和B都引用到C,所以A释放时,C内存不会被释放。所以C这块内存不会被计算到A或者B的Retained Size中,因此,对象树在转换成Dominator Tree时,会A、B、C三个是平级的。将对象引用关系转换成Dominator Tree能帮助我们快速的发现占用内存最大的块,也能帮我们分析对象之间的依赖关系。根据支配关系,对象大小有两个定义Retained Size和Shallow Size: ShallowSize :对象本身占用内存的大小。也就是对象头加成员变量(不是成员变量的值)的总和,如一个引用占用32或64bit,一个integer占4bytes,Long占8bytes等。常规对象(非数组)的Shallow Size 由其成员变量的数量和类型决定,数组的 Shallow Size 由数组元素的类型(对象类型、基本类型)和数组长度决定。例如E的Shallow Size,只是自身大小和他引用的G没有关系。Retained Size: 对象被垃圾回收器回收后能被GC从内存中移除的所有对象内存大小之和。相对于Shallow Size,Retained Size可以更精确的反映一个对象实际占用的大小(若该对象释放,Retained Size都可以被释放)。例如E到C的引用链断开后,会释放E、G这2个对象。这2个对象的所占内存之和就是E的Retained Size。这里我们就知道了如果要优化内存或者解决泄露,优先关注 Retained Size 较大的对象,因为Retained Size大的对象所能释放的内存空间更大。2.1.4 Java OOM的发生学习了内存区域,垃圾回收机制,以及对象所占用的内存空间大小,那么Java OOM 到底是如何发生的呢。下面我们来看一个Java OOM异常时候的信息:java.lang.OutOfMemoryError: Failed to allocate a 65552 byte allocation with 23992 free bytes and 23KB until OOM, max allowed footprint 536870912, growth limit 536870912OutOfMemoryError抛出的地方在系统源码文件/runtime/gc/heap.cc//方法voidHeap::ThrowOutOfMemoryError(Thread*self,size_tbyte_count,AllocatorTypeallocator_type)//异常信息oss<<"Failedtoallocatea"<
|
|