JVM
JVM基础了解
代码编译运行工作流程

在硬盘里面将java代码编译成机器能够识别的字节码,JVM将这个字节码搂到内存里面,在内存里面分配一块空间,用堆来存储该对象,在栈里面进行计算。
JVM内存主要组成部分
虚拟机栈:
局部变量表(存储成员变量和方法传参里面的参数)
操作栈(存储要进行操作的变量,进行算数操作)
动态链接(获取类或者接口并将其组合到java虚拟机的运行时状态以便可以执行的过程)
返回地址(记录调用方法的地方,等方法执行结束以后要回到这个地址继续执行)
本地方法栈:JNI调用的方法
方法区:存储类模板、常量、静态变量
堆:存储实例对象
程序计数器:记录程序运行到了那一步,方便线程切换回来时继续上一次操作
代码在内存里的呈现

内存分布主要是线程共享区和线程独占区。
线程共享区:方法区和堆区。而事实上堆和方法区都是在内存(物理内存)中
线程独占区:栈、程序计数器。栈则是在高速缓冲区里,高速缓冲区是手机CPU里的“内存”,可以理解为它相当于CPU的“口袋”,所以这也让它的计算速度比在内存里要快(离CPU近)
对象在内存中占用的内存

GcRoot
静态变量、常量、局部变量表(也就是方法里的变量)、本地变量表(C/C++方法里的变量)
垃圾回收
- 垃圾回收机制:便是根据虚拟机对每个数据的状态的记录去决定要不要回收的一套机制。
- 回收对象:当对象被虚拟机认为没有GcRoot引用他们的时候。
- 垃圾回收算法:
1、标记-清除法:
标记所有可以回收的对象,然后统一进行回收。
缺点:会产生很多不连续的内存碎片。
2、标记-复制法:将内存区域划分为两块大小一致的区域(比如A和B),每次只使用其中一块(比如A),当A块的内存用完了之后就将还存活的对象复制到B区域上去,然后将当前块A的内存空间一次清理掉。
缺点:内存浪费太大、消耗性能。
3、标记-整理法:先标记所有可以回收的对象,然后将存活的对象向内存空间的一端进行移动,将所有存活的对象都连续的放在一起,然后直接清理掉边界以外的内存。
缺点:性能开销大。

堆区
老年代:
占据堆空间的2/3,主要存放应用中生命周期长的对象,老年代不会频繁进行垃圾回收。可使用标记清除法、标记整理算法。
新生代:
占据堆空间的1/3,又细分为三个区:Eden、SurvivorFrom、ServivorTo,三个区的默认比例为:8:1:1。新生代保存着大量的刚刚创建的对象,而新生代中会频繁进行垃圾回收。可使用标记-复制法。
内存溢出
堆和虚拟机栈是发生内存泄露和内存溢出的高发区。
内存溢出的情况:
1、生产者与消费者模型,注册回调,忘记注销,然后将某对象添加到队列时忘记控制队列大小等。
2、常见的把服务器返回的json数据解析成java bean时,因为循环引用而内存溢出,比如fastjson对象转json string时因为内层对象的某个属性正好是外层对象,那这样就会循环引用,导致解析时死循环而内存溢出报错。
3、递归使用不恰当会造成栈溢出,调用方法的过程是在栈区进行,上文已经讲过,因此递归使用不恰当就会造成栈区爆掉,也就是栈溢出。
内存泄漏
创建一个对象,也就是在堆区里申请多一块空间给它,如果某个对象一直持有这块空间,该空间无法得到释放。所以如果内存泄露的次数多,最终很容易引起内存溢出。
内存泄漏的情况:
1、单例模式。
2、静态变量持有的引用导致的内存泄露。
3、非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象生命周期长时,就会内存泄露,比如常见的Handler,Thread,AsyncTask,创建一个线程去作网络请求。
4、未取消注册或回调导致的内存泄漏。
5、集合中的对象未清理造成内存泄漏。
6、资源未关闭或未释放导致内存泄漏,比如流对象、WebView等使用完后要及时关闭。
内存抖动
短时间内有大量的对象被创建或者被回收的现象。本质其实就是频繁地创建新对象(比如循环内不断new对象),如:在onDraw()方法里创建Paint、Path这些对象。
内存分析
内存分析目的
内存分析一直是一个很重要但却缺乏关注的点,内存作为程序运行最重要的资源之一,需要运行过程中做到合理的资源分配与回收,不合理的内存占用轻则使得用户应用程序运行卡顿、ANR、黑屏,重则导致用户应用程序发生 OOM(out of memory)崩溃。跟踪下来可能会发现内存出现问题的地方仅仅只是一个表现的地方,并非深层次的原因,因为内存问题相对比较复杂,它是一个逐渐挤压的过程,正好在出现问题的代码那里爆了,所以针对应用的内存问题开发者必须多加关注。
内存问题表现形式
- 内存抖动:锯齿状、GC频繁导致卡顿
- 内存泄露:可用内存逐渐减少、频繁GC
- 内存溢出:OOM、程序异常
内存抖动
- 内存抖动是由于短时间内有大量对象进出新生区导致的,内存忽高忽低,有短时间内快速上升和下滑的趋势,分析图呈锯齿状。
- 它伴随着频繁的GC,GC会大量占用UI线程和CPU资源,会导致APP整体卡顿,甚至OOM的可能。
内存泄漏
达到垃圾回收的条件:引用计数法、可达性分析。
引用计数法:没有对象在用该对象了。
可达性分析:GC Roots(静态变量、线程栈变量、常量池、JNI指针),可以通过GC Roots找到的对象就是根可达,找不到的就是根不可达,就可以被回收。
java和c/c++内存泄漏对比
每一个java进程就是一个jvm实例。
android内存泄漏
android每个应用程序会分配固定内存大小,若内存泄漏使用的内存超过该应用分配的内存大小,则应用会被杀死。
- 程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。
- 对象在引用链上,但是已经不可用了。根可达,但是内存已经不能再用了。
- java应用程序退出了,内存泄漏就没有了,应用程序之间不互相影响。
- c/c++内存泄漏需要重启操作系统。
内存性能分析工具
Android Profiler
可以帮助我们识别可能会导致应用卡顿甚至崩溃的内存泄漏和内存抖动。它显示一个应用内存使用量的实时图表,让我们可以:
- 捕获堆转储
- 强制执行垃圾回收
- 跟踪内存分配
捕获堆转储

保存文件

强制执行垃圾回收

跟踪内存分配

暂停观察

MAT(Memory Analyzer Tool)
内存快照对比,为了更有效的找出内存泄漏的对象,一般会获取两个堆转储(先dump一个,隔段时间再dump一个),通过对比后的结果可以很方便定位。
使用步骤
捕获两个堆转储

转化

在cmd使用hprof-conv工具将堆转储转化为MAT能够打开的格式


MAT打开两个文件

点开直方图

看Activity

排除掉软引用、弱引用、虚引用

可以看到谁引用了这个Activity

2个堆转储对比实例
从Activity_A跳转到Activity_B,如果Acitivity_B发生了内存泄漏,通过MAT检测出来呢?
操作:跳转之前捕获Activity_A的堆转储,得到Activity_A的dump_01文件,再跳转到Acitivty_B,跳转回Acitivty_A,再次捕获Activity_A的堆转储,得到Activity_A的dump_02文件。

原理:正常情况下,Acitivity_B没有发生内存泄漏,跳转到Acitivity_A时,执行onDestory后Acivity_B应该被释放掉了。如果Acitivity_B发生内存泄漏,跳转到Acitivity_A后,dump文件里面还有Acitivity_B。跳转后的dump文件里面多了哪些对象,就是哪些对象发生了内存泄漏。
根可达,看谁还持有该对象。

用1来对比2

多出来的就是发生内存泄漏的对象(不过,此处是垃圾回收的对象,因为实际捕获的是垃圾回收前的堆转储和垃圾回收后的堆转储。)

再次打开直方图,排除弱引用、虚引用、软引用

查看引用链,可以看到是哪一个引用导致无法被回收。

内存抖动案例
频繁创建字符串,制造一些内存抖动。
解决方法:使用StringBuilder。
内存优化综合案例
IOSStyleLoadingView(内存抖动、内存泄漏)
- 内存抖动分析过程:使用profiler record找到创建实例最多的几个对象
Path

Paint

String[]
- 内存泄漏分析过程:跳转到界面-->返回-->Capture heap dump。

也可使用MAT。
泄漏原因:动画监听,Activity返回之后,监听还在,持有了Activity的引用,导致Activity泄漏。
解决方法:在onDetachedFromWindow的时候cancel动画,移除监听。
内存泄漏案例
安卓7.0一下版本系统应用输入法导致内存泄漏。
解决方法:通过反射的方式拿到方法,把引用链斩断,根不可达。
内存泄漏检测核心原理
LeakCanary核心原理
jvm判断对象应该被回收的方式:
- 引用计数法。
当对象没有其他对象在引用该对象时,应该被回收。
- 可达性分析
GC roots(静态变量、线程栈变量、常量池、JNI指针)
在GC roots的引用链上可以找到对象的时候,就认为这个对象根可达,GC来回收时发现该对象根可达,该对象就不应该被回收。在引用链找不到该对象,该对象不可达,就应该被回收。
内存泄漏:根可达,但是已经无法被使用,又无法释放它所占用的内存。
内存泄漏后果:多次内存泄漏会造成内存吃紧,应用卡顿、崩溃、重启。
LeakCanary是Square公司的一个开源库。通过它可以在App运行过程中检测内存泄漏,当内存泄漏时会发生泄漏对象的引用链,并通知程序开发人员。
使用方法
添加依赖--》运行--》点击上方通知--》分析原因
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
profile、mat、leakcanary
共同点:捕获堆转储(动态化的数据通过静态化的文件来分析)。
不同点:leakcanary与程序一起运行,适用于线上;profile与mat是在开发过程中进行内存检测。
执行流程
- 检测保留的对象。
- 生成堆转储文件(heap dump)。
- 分析堆转储文件。
- 对泄漏进行分类。
核心原理
LeakCanary通过hook android的生命周期来自动检测Activity和Fragment何时被销毁,何时应该被垃圾回收,这些被destroy的对象传递给ObjectWatcher,ObjectWatcher持有对它们的弱引用。
- activity在执行onDestroy后,activity指向的内存应该被释放,因此需要知道activity什么时候执行onDestroy(使用hook监听activity生命周期)。
- 然后需要知道activity是否被回收了(通过弱引用)。
检测对象类型
- 已销毁的Activity实例。
- 销毁的Fragment实例。
- 销毁的Fragement View实例。
- 已清除的ViewModel实例。
常见内存泄漏情况及解决方法
- 单例造成的内存泄漏
- 非静态内部类(比如内部类、匿名内部类)创建静态实例造成的内存泄漏
- Handler造成的内存泄漏
- 线程造成的内存泄漏
- 资源未关闭造成的内存泄漏
- WebView、或百度、高德地图的MapView引起的内存泄漏
ADB查看内存相关命令
命令
dumpsys meminfo [包名]或[PID]
功能:根据进程ID或包名查看指定程序内存使用情况
输出结果
| 划分类型 | 排序 | 解释 |
| process | PSS | 以进程的PSS从大到小依次排序显示,每行显示一个进程; |
| OOM adj | PSS | Native/System/Persistent/Foreground/Visible/Perceptible/A Services/Home/B Services/Cached,分别显示每类的进程情况 |
| category | PSS | 以Dalvik/Native/.art mmap/.dex map等划分的各类进程的总PSS情况 |
| total | - | 总内存、剩余内存、可用内存、其他内存 |
procrank
功能: 获取所有进程的内存使用情况的排行榜,排行是以Pss的大小而排序。procrank命令比dumpsys meminfo命令,能输出更详细的VSS/RSS/PSS/USS内存指标。
cat /proc/meminfo
功能:能否查看更加详细的内存信息
free
功能:查看可用内存,缺省单位KB。该命令比较简单、轻量,专注于查看剩余内存情况。数据来源于/proc/meminfo。
showmap
功能:用于查看虚拟地址区域的内存情况
vmstat
功能:不仅可以查看内存情况,还可以查看进程运行队列、系统切换、CPU时间占比等情况,另外该指令还是周期性地动态输出。
- procs(进程)
- r: Running队列中进程数量
- b: IO wait的进程数量
- memory(内存)
- free: 可用内存大小
- mapped:mmap映射的内存大小
- anon: 匿名内存大小
- slab: slab的内存大小
- system(系统)
- in: 每秒的中断次数(包括时钟中断)
- cs: 每秒上下文切换的次数
- cpu(处理器)
- us: user time
- ni: nice time
- sy: system time
- id: idle time
- wa: iowait time
- ir: interrupt time
adb shell top
功能:查看设备cpu和内存占用情况
adb shell top -m 6
功能:查看内存占用前八的程序
adb shell top -n 1
功能:刷新一次内存信息,返回
adb shell top -m 6 -n 1
功能:查看某一时刻,内存占用前八的程序
top -m 10 -s cpu
功能:查看CPU使用情况(-m显示最大数量,-s 按指定行排序)
参数含义:
- PID : progress identification,应用程序ID
- S : 进程的状态,其中S表示休眠,R表示正在运行,Z表示僵死状态,N表示该进程优先值是负数
- #THR : 程序当前所用的线程数
- VSS : Virtual Set Size虚拟耗用内存(包含共享库占用的内存)
- RSS : Resident Set Size实际使用物理内存(包含共享库占用的内存)
- PCY : 前台(fg)和后台(bg)进程
- UID : User Identification,用户身份ID
- Name : 应用程序名称
getprop|grep heapgrowthlimit
- 查看系统设置单个进程的内存上限
am_pss
I am_pss : [16438,10036,com.android.systemui,52458496,47415296,146432]
16438 Pid 进程id
10036 Uid 用户id,Android作为单用户系统,Uid用于数据共享,可在AndroidManifest.xml中配置
com.android.systemui 进程名称
后面几个数字分别是
Pss Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
Uss Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)
SwapPss swap交换分区,表示交换分区内存占用?
am_pss 关键字
(Pid|1|5),(UID|1|5),(Process Name|3),(Pss|2|2),(Uss|2|2),(SwapPss|2|2)
1|5 |3 2|2
前一个参数值类型
# The data type is a number from the following values:
# 1: int
# 2: long
# 3: string
# 4: list
# 5: float
后一个参数类型
# The data unit is a number taken from the following list:
# 1: Number of objects
# 2: Number of bytes
# 3: Number of milliseconds
# 4: Number of allocations
# 5: Id
# 6: Percent
# Default value for data of type int/long is 2 (bytes).
适用场景
- dumpsys meminfo适用场景: 查看进程的oom adj,或者dalvik/native等区域内存情况,或者某个进程或apk的内存情况,功能非常强大;
- procrank适用场景: 查看进程的VSS/RSS/PSS/USS各个内存指标;
- cat /proc/meminfo适用场景: 查看系统的详尽内存信息,包含内核情况;
- free适用场景: 只查看系统的可用内存;
- showmap适用场景: 查看进程的虚拟地址空间的内存分配情况;
- vmstat适用场景: 周期性地打印出进程运行队列、系统切换、CPU时间占比等。
1552

被折叠的 条评论
为什么被折叠?



