几种内存问题的区别?
- 内存抖动:在短时间内反复地发生内存增长和回收,导致程序卡顿甚至OOM内存溢出
- 内存泄漏:一些内存对象无法按预期的释放
- 内存溢出:申请分配内存时,超过系统所能提供的
OOM的几种类型
- java.lang.StackOverflowError 栈溢出:递归、死循环等,因为每次方法调用都会有一个方法栈压入导致OOM
- java.lang.OutOfMemoryError:Java heap space:创建了非常多的对象实例/存储了过大的数据导致无法在新建对象
- java.lang.OutOfMemoryError:GC overhead limit exceeded:频繁GC,并且回收的内存空间很少,导致死循环(超过98%的时间用来做GC并且回收了不到2%的堆内存)
- java.lang.OutOfMemoryError:unable to create new native thread:创建的线程太多了,一般默认一个进程可以创建的线程数为1024个
- java.lang.OutOfMemoryError:Direct buffer memory:本地内存满了,一般是NIO中ByteBuffer.allocateDirect()分配的内存,这个内存不属于GC管辖可能不会被及时回收
java堆和native堆的区别
- java堆主要是Java代码分配的对象
- native堆主要是C代码(malloc)分配的内存
- 一些Java代码也会造成native堆内存分配,比如bitmap
虚拟内存
- 作为内存地址的抽象,提供一个映射到真实的内存地址,使每个进程认为自己拥有连续的内存地址,提高内存使用的效率
- 内存包含和隔离:每个进程只能访问到自己的内存地址(避免直接操作物理内存)
- 分页与置换:物理内存与虚拟内存都是以页为单位进行管理(一般是4K或8K),以页为单位的管理使内存管理更加高效
内存优化常见思路
- 对象复用/享元模式
- 减少不必要的内存分配
- 监测是否内存泄漏、是否因为生产者消费者模型未及时处理导致内容堆积
- 使用合理的数据结构,例如SparseArray、ArrayMap 来替代 HashMap
内存引用的几种类型
- 强引用:在内存不足时不会被回收。平常用的最多的对象,如新创建的对象。
- 软引用:在内存不足时会被回收(无强引用)。用于实现内存敏感的高速缓存。
- 弱引用:只要GC回收器发现了它,就会将之回收(无强引用)。用于Map数据结构中,引用占用内存空间较大的对象。
- 虚引用:在任何时候都可能被垃圾回收器回收(无强引用)。
堆、程序计数器、方法区、本地方法栈、虚拟机栈,其中方法区和堆是线程共享的
方法区:线程共享,存储类信息、静态变量、常量、即时编译出来的代码数据,可造成OOM
堆:线程共享,存放几乎所有的对象实例,GC的主要区域
程序计数器
- 一块较小的内存空间,线程私有,存储当前线程执行的字节码行号指示器
- 字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令:分支、循环、跳转等
- 每个线程都有一个独立的程序计数器
- 唯一一个在java虚拟机中不会OOM的区域
本地方法栈
- 为虚拟机中Native方法服务,对本地方法栈中使用的语言、数据结构、使用方式没有强制规定,虚 拟机可自有实现
- 占用的内存区大小是不固定的,可根据需要动态扩展
虚拟机栈
- 线程私有区域,每个java方法在执行的时候会创建一个栈帧用于存储局部变量表、操作数栈、动态 链接、方法出口等信息。方法从执行开始到结束过程就是栈帧在虚拟机栈中入栈出栈过程
- 局部变量表存放编译期可知的基本数据类型、对象引用、returnAddress类型。所需的内存空间会 在编译期间完成分配,进入一个方法时在帧中局部变量表的空间是完全确定的,不需要运行时改变
- 若线程申请的栈深度大于虚拟机允许的最大深度,会抛出SatckOverFlowError错误
- 虚拟机动态扩展时,若无法申请到足够内存,会抛出OutOfMemoryError错误
- 给对象添加引用计数器,每当一个地方引用时,计数器加1,引用失效时计数器减1;当引用计数 器为0时即为对象不可用
- 实现简单,效率高,但是无法解决相互引用问题,主流虚拟机一般不使用此方法判断对象是否存活
- 以GC Roots作为起点,向下搜索,搜索走过的路径称为引用链,当一个对象到 GC Roots没有任何引用链时即为对象不可用,可被回收的
- 可被称为GC Roots的对象:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中 常量引用的对象、本地方法栈中引用的对象
- Class-由系统ClassLoader加载的对象
- Thread-活着的线程
- Stack Local-Java方法的local变量或参数
- JNI Local - JNI方法的local变量或参数
- JNI Global - 全局JNI引用
- Monitor Used - 用于同步的监控对象
- 首先标记出需要回收的对象,在标记完成后统一回收所有标记的对象
- 缺点:
- 标记和清除的过程效率低
- 会产生很多不连续的内存碎片,申请大内存时容易失败而触发GC
- 标记整理算法标记过程和标记清除算法一样,但清除过程并不是对可回收对象直接清理,而是将所有存 活对象像一端移动,然后集中清理到端边界以外的内存。
- 将可用内存按空间分为大小相同的两小块,每次只使用其中的一块,等这块内存使用完了将还存活的对 象复制到另一块内存上,然后将这块内存区域对象整体清除掉。每次对整个半区进行内存回收,不会导 致碎片问题,实现简单高效。
- 缺点:内存可用空间减半
- 根据对象存活周期的不同将内存划分为 新生代 和 老年代,根据其特点采用最合适的算法
- 新生代存活对象较少,每次垃圾回收都有大量对象死去,一般采用复制算法,只需要付出复制少量 存活对象的成本就可以实现垃圾回收
- 老年代存活对象较多,没有额外空间进行分配担保,就必须采用标记清除算法和标记整理算法进行 回收
合理的线程使用可提高应用程序的运行效率,过度使用反而会增加CPU及内存的负担。为避免这一情况的发生,可结合进程状态及当前的线程列表进行分析:
当前状态:读取进程状态 /proc/pid/status,并解释Threads字段
具体分析:调用Thread.getAllStackTraces() 获取当前所有线程的信息,包括线程名、调用栈及状态等
adb shell dumpsys meminfo <package_name>
字段 | 含义 | 备注 |
---|---|---|
Shallow Size | Shallow Size是指实例自身占用的内存, 可以理解为保存该’数据结构’需要多少内存, 注意不包括它引用的其他实例 | |
Retained Size | 实例A的Retained Size是指, 当实例A被回收时, 可以同时被回收的实例的Shallow Size之和 | |
缩写 | ||
VSS | Virtual Set Size | 虚拟耗用内存(包含共享库占用的内存 |
RSS | Resident Set Size | 实际使用物理内存(包含共享库占用的内存 |
PSS | Proportional set size | 实际使用的物理内存(比例分配共享库占用的内存 |
USS | Unique Set Size | 进程独自占用的物理内存(不包含共享库占用的内存) |
其他 | VSS>=RSS>=PSS>=USS |
ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
Log.i(TAG,"可用内存:" + memoryInfo.availMem);
Log.i(TAG,"总内存:" + memoryInfo.totalMem);
Log.i(TAG,"是否内存低:" + memoryInfo.lowMemory);
private void dumpJNIRef(){
try {
Class.forName("dalvik.system.VMDebug")
.getMethod("dumpReferenceTables").invoke(null);
} catch (Exception e) {
}
}
查看日志有JNI引用表
global reference table dump:
Last 10 entries (of 611):
610: 0x12c9dca0 java.lang.ref.WeakReference (referent is a android.view.inputmethod.InputMethodManager$ImeInputEventSender)
609: 0x12c9dc80 android.hardware.input.InputManager$InputDevicesChangedListener
608: 0x12c9dc60 android.view.ViewRootImpl$AccessibilityInteractionConnection
607: 0x12c9dc48 android.os.Binder
606: 0x12c9dc30 java.lang.ref.WeakReference (referent is a android.view.ViewRootImpl$WindowInputEventReceiver)
605: 0x12c9dc10 android.view.ViewRootImpl$W
604: 0x12c9dbf0 android.view.accessibility.AccessibilityManager$1
603: 0x12c9dbd0 android.view.ViewRootImpl$AccessibilityInteractionConnection
602: 0x12c9dbb8 android.os.Binder
601: 0x12c9dba0 java.lang.ref.WeakReference (referent is a android.view.ViewRootImpl$WindowInputEventReceiver)
Summary:
319 of java.lang.Class (243 unique instances)
253 of java.nio.DirectByteBuffer (253 unique instances)
4 of java.lang.ref.WeakReference (4 unique instances)
3 of android.opengl.EGLContext (2 unique instances)
3 of android.opengl.EGLDisplay (2 unique instances)
3 of android.opengl.EGLSurface (2 unique instances)
2 of dalvik.system.PathClassLoader (1 unique instances)
2 of java.lang.String (2 unique instances)
2 of java.lang.ThreadGroup (2 unique instances)
2 of android.os.Binder (2 unique instances)
2 of android.app.LoadedApk$ReceiverDispatcher$InnerReceiver (2 unique instances)
2 of android.view.ViewRootImpl$AccessibilityInteractionConnection (2 unique instances)
2 of android.view.ViewRootImpl$W (2 unique instances)
2 of android.view.accessibility.AccessibilityManager$1 (2 unique instances)
1 of java.nio.ByteOrder
1 of dalvik.system.VMRuntime
1 of android.app.ActivityThread
获取状态: 通过读取 /proc/pid/status
中的 VmSize
字段
具体分析: 读取 /proc/pid/smaps
分析mapping及各个内存大小相关的字段
大小限制: 4GB(32位)或512GB(64位)
获取更大对空间:AndroidManifest 配置
application.largeHeap
版本差异:8.0之前,图片缓存容易消耗大量堆空间,8.0及之后像素数据被修改到了native层
Linux把一切设备都视作文件,File Descriptor(文件描述符)为设备相关的编程提供了统一的方法。当我们执行IO、Socket及线程等相关操作时,都存在与之相对应的FD。进程的FD信息可通过读取/proc下的虚拟文件来获取:- - 大小限制:读取进程状态
/proc/pid/limits
,并解释Max open files字段
/proc/pid/fd
,计算文件数量/proc/pid/fd
,并通过Os.readlink解释文件链接adb shell "getprop | grep dalvik"
[dalvik.vm.appimageformat]: lz4
[dalvik.vm.dex2oat-Xms]: 64m
[dalvik.vm.dex2oat-Xmx]: 512m
[dalvik.vm.dex2oat-max-image-block-size]: 524288
[dalvik.vm.dex2oat-minidebuginfo]: true
[dalvik.vm.dex2oat-resolve-startup-strings]: true
[dalvik.vm.dex2oat-updatable-bcp-packages-file]: /system/etc/updatable-bcp-packages.txt
[dalvik.vm.dexopt.secondary]: true
[dalvik.vm.heapmaxfree]: 8m
[dalvik.vm.heapminfree]: 512k
[dalvik.vm.heapsize]: [512m] # 单个进程可用的最大内存
[dalvik.vm.heapstartsize]: [8m] # 堆分配的起始大小
[dalvik.vm.heaptargetutilization]: 0.75
[dalvik.vm.image-dex2oat-Xms]: 64m
[dalvik.vm.image-dex2oat-Xmx]: 64m
[dalvik.vm.isa.arm.features]: default
[dalvik.vm.isa.arm.variant]: cortex-a9
[dalvik.vm.isa.arm64.features]: default
[dalvik.vm.isa.arm64.variant]: generic
[dalvik.vm.lockprof.threshold]: 500
[dalvik.vm.minidebuginfo]: true
[dalvik.vm.usejit]: true
[dalvik.vm.usejitprofiles]: true
[persist.debug.dalvik.vm.core_platform_api_policy]: just-warn
[persist.sys.dalvik.vm.lib.2]: libart.so
[ro.dalvik.vm.native.bridge]: 0
通常说的Native内存是相对于Java堆而言的,Java堆区的内存有虚拟机代为申请和释放,Java层的业务代码无需关心。Native内存主要说的是由业务动态申请的内存,一般是业务so库,业务代码是c/c++实现的,常用的方式就是调用 malloc函数申请内存,调用free释放内存。这些内存的申请都需要合理的释放,否则会导致内存不足。可结合Debug.getMemoryInfo()以及/proc/pid/smap文件来分析。
- 可以使用KOOM和matrix进行监控
- 二者的原理都是通过hook C的内存分配和释放(malloc与free函数)
- KOOM使用FP Unwind(frame pointer unwind)进行回溯(并不是所有的库都支持),matrix集成了unwind库进行堆栈回溯
- 根据堆栈,再通过add2line工具进行函数定位
- KOOM使用和原理
- 字节流动-LeakTracer集成检测
// 配置检测 Android native 内存泄漏的工具
MemoryHook.INSTANCE
//单独配置so的方式 ".*libasr\\.so$",".*libgram\\.so$",".*libvad\\.so$"
// .addHookSo(".*com\\.hjl\\.test.*\\.so$") //全匹配的方式 ".*com\\.byd\\.dm.*\\.so$"
.addHookSo(".*")
.enableStacktrace(true)
.stacktraceLogThreshold(0)
.enableMmapHook(true);
add2line 使用
- 路径:Android\Sdk\ndk\21.0.6113669\toolchains\aarch64-linux-android-4.9\prebuilt\windows-x86_64\bin\aarch64-linux-android-addr2line.exe
- 命令参考:aarch64-linux-android-addr2line.exe -e xxx.so 2ba0 -f
首先把profiler导出的内存文件转换成hprof文件
Android/sdk/platorm-tools路径下,执行
hprof-conv 刚刚生成的hprof文件 memory-mat.hprof
使用MAT分析,上面链接有教程 Android内存优化参考
修改leakcanary,在可能泄露的地方监控
KOOM 在内存达到一定预警值的时候,新开一个进程dump内存信息
Matrix 接入
根据上述指标,自行进行监控与上报
不一定。
第一次标记:如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记;
第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。在 finalize() 方法中没有重新与引用链建立关联关系的,将被进行第二次标记。第二次标记成功的对象将真的会被回收,如果对象在 finalize() 方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。
由于项目使用消息队列的方式进行处理,因为某些消息处理比较耗时,导致消息堆积(消息携带byte数组),最终导致内存暴增