JVM
程序计数器:记录字节码的地址。
栈:FILA(先进后出)存储当前线程(线程独享)运行方法需要的数据局部变量表(基本数据类型、对象的引用)、操作数栈、返回地址等。
方法区:存放类信息(ClassLoader加载的类)、常量、静态变量。
堆区:创建的实例对象与数组。(-Xmx堆的分配上限 -Xms堆的初始容量)
对象与垃圾回收机制
判断对象是否存活?
-
引用计数算法(Reference Counting)通过为每个对象维护一个整型计数器,实时追踪其被引用的次数。具体规则如下:
- 计数器增减:当对象被引用时,计数器 +1;引用失效时,计数器 -1。
- 回收条件:当计数器归零时,表示对象不再被使用,立即回收其内存空间
- 算法缺点: 循环引用问题,若多个对象互相引用,计数器无法归零,导致内存泄漏可使用弱引用解决。
(弱引用(Weak Reference)是一种 不增加对象引用计数 且 不阻止垃圾回收 的引用机制。当对象仅被弱引用关联时,若没有其他强引用存在,该对象会被垃圾回收器立即回收,在对象互相强引用的场景(如父类与子类双向依赖),将其中一方改为弱引用,可避免内存泄漏)
可达性分析(GC roots ):可达性分析(Reachability Analysis)是 JVM 垃圾回收的核心算法,通过 追踪对象引用链 判断对象是否存活。
- GC Roots 起点:以特定根对象(如线程栈变量、静态字段、JNI 引用等)为起点,构建对象引用链。
- 不可达判定:若对象无法通过任何引用链与 GC Roots 关联,则标记为可回收对象。
-
误判风险:
finalize()方法可能使不可达对象“复活”,但 JVM 仅允许一次复活机会
GC Roots 的类型
| 类别 | 示例 |
|---|---|
| 虚拟机栈局部变量 | 线程方法中的参数、局部变量、临时变量 |
| 方法区静态属性 | 类的静态变量(如 public static Object obj) |
| 方法区常量 | 字符串常量池(String Table)中的引用 |
| 本地方法栈 JNI 引用 | Native 方法引用的 Java 对象 |
可达性分析与引用计数法的区别
| 算法 | 优势 | 缺陷 | 适用场景 |
|---|---|---|---|
| 引用计数法 | 实现简单、实时性高 | 无法解决循环引用问题 | 非主流 JVM 实现 |
| 可达性分析 | 规避循环引用、精确判断对象存活状态 | 需暂停用户线程(STW)进行扫描 | 现代 JVM(HotSpot 等) |
Java引用方式
| 引用类型 | 回收条件 | 典型场景 | 实现类/接口 |
|---|---|---|---|
| 强引用 | 除非显式置为 null,否则永不回收 |
核心业务对象(如数据库连接) | 默认赋值(Object obj = new Object()) |
| 软引用 | 内存不足时回收 | 内存敏感型缓存(如图片缓存) | SoftReference<T> |
| 弱引用 | GC 运行时立即回收(无强引用关联) | 监听器、临时元数据存储 | WeakReference<T> |
| 虚引用 | 对象回收后触发通知 | 资源释放跟踪(如堆外内存管理) | PhantomReference<T> |
弱引用的使用
Object target = new Object();
WeakReference<Object> weakRef = new WeakReference<>(target);
//对象一旦失去强引用,GC 运行时弱引用指向的对象会被立即回
if (weakRef.get() != null) {
System.out.println("对象存活");
} else {
System.out.println("对象已被回收");
}
引用队列(ReferenceQueue)
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> trackedRef = new WeakReference<>(new Object(), queue);
// 异步处理回收事件
new Thread(() -> {
try {
Reference<?> ref = queue.remove();
System.out.println("对象已回收,执行清理操作");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
弱引用缓存(WeakHashMap)
Map<Object, String> cache = new WeakHashMap<>();
Object key = new Object();
cache.put(key, "临时数据");
key = null; // 移除强引用
System.gc();
System.out.println(cache.size()); // 输出可能为 0
堆的分区:
| 分区 | 描述 | 占比 |
|---|---|---|
| 年轻代 | 存放新创建的对象,频繁触发 Minor GC(复制算法)回收短生命周期对象 | 默认占堆总空间的 1/3(可通过 -XX:NewRatio 调整) |
| 老年代 | 存储长期存活的对象,触发 Full GC(标记-清除/整理算法) | 默认占堆总空间的 2/3 |
年轻代进一步划分为以下子区域:
-
Eden 区:
- 新创建的对象优先分配在 Eden 区
- 默认占年轻代 80% 空间(通过
-XX:SurvivorRatio=8设定)。
-
Survivor 区(From/To):
- 存放从 Eden 区或另一 Survivor 区复制过来的存活对象。
- 两块 Survivor 区交替使用,总占年轻代 20% 空间(每块 10%)。
示例流程:
- 对象首次分配至 Eden 区;
- Minor GC 后存活对象复制到 Survivor From 区;
- 再次 Minor GC 时,存活对象从 From 区复制到 To 区(年龄计数器+1);
- 对象年龄达到阈值(默认 15)后晋升至老年代。
GC的方式:Minor Gc(回收新生代)、Major Gc(回收老年代) 、Full Gc(回收所有堆以及方法区等)
GC垃圾清理算法
| 算法类型 | 代表实现 | 原理 |
|---|---|---|
| 标记-清除 | CMS(已淘汰) | 标记存活对象,清除未标记对象(产生内存碎片) |
| 标记-复制 | Serial / ParNew | 将存活对象复制到新内存区域,清除旧区域(无碎片,牺牲空间) |
| 标记-整理 | Parallel Old | 标记存活对象后整理内存,消除碎片(耗时较长) |
| 分代收集 | G1(主流) | 将堆划分为多个 Region,优先回收年轻代(预测停顿时间) |
| 指标 | 标记-复制 | 标记-整理 | 分代收集(G1) | ZGC/Shenandoah |
|---|---|---|---|---|
| 最大停顿时间 | 低(年轻代) | 高(老年代) | 可控(10-200ms) | 极低(亚毫秒级)3 |
| 内存占用 | 50% 额外空间 | 无额外空间 | 无额外空间 | 少量元数据开销4 |
| 吞吐量 | 中 | 高 | 高 | 中(并发处理开销) |
| 适用堆大小 | <100GB | <4TB | <4TB | 支持 PB 级3 |
垃圾回收器根据 算法设计 和 硬件适配性 划分为四代演进:
| 阶段 | 代表回收器 | 核心特性 | 适用场景 |
|---|---|---|---|
| 单线程时代 | Serial / Serial Old | 单线程回收,全程 STW(Stop-The-World)3 | 低资源环境(嵌入式设备) |
| 多线程时代 | Parallel Scavenge | 多线程并行回收,以吞吐量优先13 | 离线计算、大数据批处理 |
| 并发时代 | CMS / G1 | 并发标记与部分清理,减少 STW 时间23 | Web 服务、实时性要求中等 |
| 智能低延迟时代 | ZGC / Shenandoah | 亚毫秒级 STW,支持 TB 级堆内存,依赖染色指针和读屏障技术12 | 金融交易、实时数据分析 |
内存标记名称
USS --物理内存 进程独占内存
PSS --物理内存 USS+按比例包含共享库(常用)
RSS --物理内存 USS+包含共享库
VSS --虚拟内存 RSS+未分配的实际物理内存
命令:
adb shell cat/system/build.prop --查看APP内存分配信息与分配限制
adb shell cat proc/meminfo --设备的物理内存信息
adb shell cat proc/PID/ppm_adj --查看oom_adj(oom_adj[-16,15] 该值越大越容易被 OOM Killer杀进程)
adb shell dumpsys meminfo --查看各个APP内存使用排序情况
adb shell dumpsys meminfo -packageName --查看当前应用的内存使用情况 (主要看Heap Total NativeHeap这几类)
onLowMemory & onTrimMemory
| 方法 | 触发时机 | 参数与粒度 | 典型应用场景 |
|---|---|---|---|
onLowMemory |
系统内存严重不足,后台进程已被终止时触发 | 无参数,仅全局内存告警 | 释放所有非核心资源(如大图缓存)12 |
onTrimMemory |
内存紧张但未达到临界值,系统主动通知应用 |
int level 区分内存压力等级 |
按等级精细化释放资源(如UI隐藏时释放控件)23 |
** onTrimMemory 等级与响应策略**
-
等级分类(关键级别):
-
TRIM_MEMORY_UI_HIDDEN(UI不可见):释放与UI相关的资源(如Activity背景图、控件缓存)。 -
TRIM_MEMORY_MODERATE/RUNNING_CRITICAL(中度/严重内存压力):释放部分缓存数据(如网络请求缓存、临时数组)。 -
TRIM_MEMORY_COMPLETE(应用即将被终止):强制释放所有非必要资源
-
@Override
public void onTrimMemory(int level) {
if (level >= TRIM_MEMORY_UI_HIDDEN) {
// 释放UI资源(如清空ImageView的Bitmap引用)
imageView.setImageBitmap(null);
}
if (level >= TRIM_MEMORY_MODERATE) {
// 释放数据缓存(如清空LruCache)
memoryCache.evictAll();
}
}
内存的问题:
1.内存抖动:频繁地创建销毁对象会导致内存抖动,使内存不连续。当有新的对象创建而内存不够时会频繁触发GC,导致卡顿。
2.内存泄漏:创建的对象不再使用但是还是被GC Roots强引用导致无法回收,就会造成内存泄漏,使可用内存减少。
3.内存溢出 :当使用的内存超出JVM分配的最大内存后会导致OOM。(还有一种OOM是线程溢出,当你创建的线程太多也会导致OOM)
Bitmap的优化处理
1.质量压缩
采样率压缩是一种通过降低图像分辨率来减少内存占用的技术,核心逻辑为 间隔像素采样。
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true // 仅读取图片尺寸信息,不加载像素
BitmapFactory.decodeResource(resources, R.drawable.image, options)
}
val originalWidth = options.outWidth
val originalHeight = options.outHeight
//获取采样率
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
val (width, height) = options.run { outWidth to outHeight }
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
//压缩Bitmap
options.inJustDecodeBounds = false
options.inSampleSize = calculatedSampleSize
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.image, options)
矩阵缩放(Matrix):
加载后通过 Bitmap.createScaledBitmap() 二次调整尺寸,适配显示需求
2.修改图片格式
在不强制要求图片像素的情况下,优先使用 Bitmap.Config.RGB_565(每像素2字节)替代默认的 ARGB_8888(每像素4字节),可减少50%内存。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
3.Bitmap缓存 (Lru缓存)
// 初始化LRU缓存(内存缓存)
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
val cacheSize = maxMemory / 8 // 分配可用内存的1/8作为缓存池
val memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, bitmap: Bitmap): Int {
// 计算Bitmap占用的内存大小(单位:KB)
return bitmap.allocationByteCount / 1024
}
override fun entryRemoved(evicted: Boolean, key: String,
oldValue: Bitmap, newValue: Bitmap?) {
// 可选:Bitmap被移除时触发回收(适配低端设备)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
oldValue.recycle()
}
}
}
// 加载Bitmap并缓存
fun loadBitmap(resId: Int, imageView: ImageView) {
val key = resId.toString()
// 1. 尝试从缓存读取
val cachedBitmap = memoryCache.get(key)
if (cachedBitmap != null) {
imageView.setImageBitmap(cachedBitmap)
return
}
// 2. 缓存未命中,异步解码
val options = BitmapFactory.Options().apply {
inPreferredConfig = Bitmap.Config.RGB_565 // 降低内存占用
inSampleSize = 2 // 缩放宽高为原图的1/2
// Android 4.4+ 复用Bitmap内存(需尺寸一致)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
inBitmap = memoryCache.snapshot().values.firstOrNull()
}
}
CoroutineScope(Dispatchers.IO).launch {
val bitmap = BitmapFactory.decodeResource(resources, resId, options)
bitmap?.let {
// 3. 加入缓存并更新UI
memoryCache.put(key, it)
withContext(Dispatchers.Main) {
imageView.setImageBitmap(it)
}
}
}
}
LRU的原理
LRU(Least Recently Used) 是一种基于时间局部性原理的缓存淘汰策略,核心规则:
当缓存空间不足时,优先移除最久未被访问的数据。
LRU通常通过 哈希表(Hash Table) + 双向链表(Doubly Linked List) 实现高效操作:
- 哈希表:存储键值对的快速访问(O(1)时间查找)
- 双向链表:维护数据访问顺序,头部为最近访问,尾部为最久未访问
| 操作 | 步骤 | 时间复杂度 |
|---|---|---|
| 数据访问 | 1. 通过哈希表找到节点 2. 将节点移到链表头部 | O(1) |
| 数据插入 | 1. 若键已存在:更新值并移到头部 2. 若键不存在:创建节点插入头部,若超容则删除尾部节点 | O(1) |
| 数据淘汰 | 1. 删除链表尾部节点 2. 同步移除哈希表中对应键 | O(1) |
LinkedHashMap 内部通过 双向链表 维护键值对的顺序,支持两种模式:
- 插入顺序(默认):按数据添加顺序排列
- 访问顺序(accessOrder=true):每次访问(get/put)将节点移到链表尾部,尾部为最近使用,头部为最久未使用
LRU淘汰触发条件:
重写 removeEldestEntry 方法,当缓存容量超过阈值时,自动移除链表头部节点
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int maxCapacity; // 最大缓存容量
public LRUCache(int maxCapacity) {
// 设置accessOrder=true启用访问顺序模式
super(maxCapacity, 0.75f, true);
this.maxCapacity = maxCapacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当当前大小超过容量时触发淘汰
return size() > maxCapacity;
}
// 可选:线程安全封装(2025年推荐使用原子类)
public synchronized V safeGet(K key) {
return super.get(key);
}
public synchronized V safePut(K key, V value) {
return super.put(key, value);
}
}
4.硬件加速
针对 Android 8.0+ 设备,使用 Bitmap.Config.HARDWARE 将 Bitmap 直接存储在显存,减少主内存占用
// 启用硬件加速(显存占用更低)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
options.inPreferredConfig = Bitmap.Config.HARDWARE;
}
5.Bitmap的内存大小计算与Bitmap的文件位置
Bitmap 内存大小计算公式
Bitmap 内存占用由 像素总量 × 单像素内存 决定,具体公式为:
内存大小=宽度×高度×每像素字节数(Bytes per pixel)
不同配置的每像素内存
| Bitmap.Config | 每像素字节数 | 内存计算示例(1080×1920) |
|---|---|---|
ALPHA_8 |
1字节 | 1080×1920×1 = 2.07MB |
RGB_565 |
2字节 | 1080×1920×2 = 4.15MB |
ARGB_8888(默认) |
4字节 | 1080×1920×4 = 8.29MB |
RGBA_F16(HDR场景) |
8字节 | 1080×1920×8 = 16.58MB |
注意:Android 系统会根据资源目录的屏幕密度(dpi)自动缩放图片尺寸,实际加载的宽高可能与文件尺寸不同!
Bitmap 的文件存储位置直接影响 系统是否自动缩放尺寸,进而改变内存占用:
| 文件位置 | 缩放逻辑 | 内存影响示例(原图 1000×1000) |
|---|---|---|
| res/drawable-hdpi | 高密度设备(如xhdpi)会放大图片(×1.5) | 1500×1500 → 内存增加 125% |
| res/drawable-hdpi | 低密度设备(如mdpi)会缩小图片(×0.75) | 750×750 → 内存减少 43.75% |
资源目录策略
-
优先使用高密度目录:将图片放在
res/drawable-xxxhdpi,低密度设备自动缩小尺寸,减少内存占用(如原图尺寸 1000×1000,mdpi设备加载时缩小至 250×250)24。 -
避免低密度目录:若图片仅存于
res/drawable-mdpi,高密度设备会放大图片,导致内存浪费。
Glide用了哪些Biymap优化技术
Glide作为Android主流图片加载框架,在Bitmap处理上采用了多项核心技术以降低内存占用并提升性能,具体优化策略如下:
1. 智能尺寸适配
-
按控件尺寸解码:根据
ImageView的实际宽高动态调整解码尺寸,避免加载超大图导致内存浪费(如200x200原图显示在100x100控件时仅解码100x100)
2. 格式优化与解码策略
-
动态格式选择:
- 默认使用
ARGB_8888保证色彩精度,但若检测到图片无透明通道且无Alpha变换操作,可切换至RGB_565格式减少50%内存占用。 - Android 8.0+启用
Bitmap.Config.HARDWARE硬件加速位图,通过减少Java堆内存拷贝降低内存消耗(需注意硬件位图不支持像素级修改)。
- 默认使用
-
采样压缩(Downsampling):根据目标尺寸自动计算
inSampleSize,通过二次采样缩小原始图片分辨率
3. 内存缓存与复用机制
-
四级缓存架构:
- 活动缓存(Active Resources):存储当前显示中的Bitmap,避免频繁GC。
- 内存缓存(Memory Cache):LRU策略缓存最近使用的Bitmap,快速响应重复请求。
-
Bitmap池(BitmapPool):复用已释放的Bitmap内存,减少分配开销(需通过
GlideModule自定义池大小)。 - 磁盘缓存:存储原始数据及转换后的结果,减少网络请求和解码次数。
动态请求管理
-
滑动优化:列表滑动时自动暂停请求(
pauseRequests()),停止后恢复(resumeRequests()),避免无效加载造成的CPU/内存抖动。 -
生命周期绑定:通过
with()关联Activity/Fragment生命周期,及时释放无用资源,防止内存泄漏。
内存溢出监测工具
1.AndroidStudio ProfIler:检查实时内存变化,可视化的查看内存都懂、内存是否销毁、内存增长不正常等信息,还可以点击Dump Heap生成堆快照。选择可以对象->Path to GC Root 检查引用链等
2.MAT:对于比较负责的内存泄漏情况我们需要使用Eclipse下的插件MAT来查看对象具体的引用。首先需要将Java Heap文件转为hprof文件并导入MAT中。然后Dominator Tree排序选择需要查看的具体类(右键目标类 → Merge Shortest Paths to GC Roots → 勾选 exclude all phantom/weak/soft references,仅保留强引用链)。
outgoing references:被该对象引用的对象
incoming references:引用该对象的对象
3.手机抓取trace文件,使用Perfetto网站打开,可以查看CPU、Memory、Binder调用栈、执行方法时间等信息。
4.LeakCanary
LeakCanary 通过 弱引用(WeakReference) 和 引用队列(ReferenceQueue) 实现内存泄漏检测。当监控对象(如 Activity)调用 onDestroy() 后,LeakCanary 会为其创建一个带引用队列的弱引用,并将唯一标识符(Key)存入 retainedKeys 集合。
-
回收检查逻辑:
- 若对象被回收,弱引用会被自动加入引用队列,对应的 Key 从集合中移除。
- 若对象未被回收,强制触发 GC 后再次检查,若 Key 仍存在集合中,则判定为内存泄漏。
内存泄漏检测流程
-
监控对象注册
- 通过
RefWatcher.watch()监控目标对象(如 Activity、Fragment),创建KeyedWeakReference并关联引用队列。
- 通过
-
主动触发 GC
- 在后台线程中强制调用
System.gc(),确保弱引用对象有机会被回收。
- 在后台线程中强制调用
-
判定泄漏
- 若引用队列未包含该弱引用,且
retainedKeys集合仍保留 Key,则确认泄漏。
- 若引用队列未包含该弱引用,且
-
堆转储与分析
- 生成
.hprof堆转储文件,通过独立进程(如HeapAnalyzerService)解析文件,使用 Shark 库(或旧版 HAHA)构建泄漏对象到 GC Roots 的引用链。
- 生成
引用链分析与报告
- 可达性分析:基于垃圾回收器的可达性算法,若泄漏对象存在到 GC Roots 的强引用链(如静态变量、未解绑的监听器),则判定泄漏路径成立。
- 报告生成:展示泄漏对象的引用路径,并标记关键引用节点(如静态单例、匿名内部类)。
** 触发条件与优化**
-
触发时机:
- 默认监控 Activity/Fragment 的
onDestroy()生命周期。 - 支持自定义监控任意对象(如 ViewModel、Service)
- 默认监控 Activity/Fragment 的