1. 内存的分配
Java 文件加载过程
- java 文件编译成 class 字节码文件
- ClassLoader 将 class 加载到 JVM 内存中
- 内存中不同区域加载 class 中不同部分
JVM 内存分布
1. 程序计数器
- 用来标记当前线程代码执行位置,当CPU调度重新执行此线程时,从标记位置执行。
- 除了恢复线程操作之外,分支操作、循环操作、异常处理等也依赖程序计数器来完成。
关于程序计数器还有几点需要格外注意:
1. 在 Java 虚拟机规范中,对程序计数器这一区域没有规定任何 OutOfMemoryError 情况(或许是感觉没有必要吧)。2. 线程私有的,每条线程内部都有一个私有程序计数器。它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
3. 当一个线程正在执行一个 Java 方法的时候,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。
2. 虚拟机栈
虚拟机栈线程私有。该区域有 2 种异常:
- StackOverflowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出。
- OutOfMemoryError:当 Java 虚拟机动态扩展到无法申请足够内存时抛出。
虚拟机栈是用来描述 Java 方法
执行的内存模型。每个方法被执行的时候,JVM 都会在虚拟机栈中创建一个栈帧。
一个线程包含多个栈帧,每个栈帧内部都包含局部变量表、操作数栈、动态链接、返回地址等。
- 局部变量表:方法的传参,内部变量都会存储在此。编译后的class 文件中已经确定局部变量表最大深度。
- 操作数栈:先入后出。在方法执行的过程中,会有各种字节码指令被压入和弹出操作数栈 ( 比如:iadd指令就是将操作数栈中栈顶的两个元素弹出,执行加法运算,并将结果重新压回到操作数栈中 )。
- 动态链接:每个栈帧包含一个指向运行时常量池中该栈所属方法的符号引用。在一个 class 文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其所在内存地址中的直接引用,而符号引用存在于方法区中。
- 返回地址:为了帮助方法退出后返回到方法被调用的位置,恢复它的上层方法执行状态。方法退出包含两种:正常退出和异常退出。
3. 本地方法栈
本地方法栈和虚拟栈基本相同,只不过是针对本地(native)方法。 JNI 的开发中会用到这一部分内存空间。
4. 堆
用来存放对象实例,几乎所有对象实例都在堆中分配,是 GC 管理的主要区域。所有线程共享此区域。
分区:新生代(Eden、Survivor),老年代
5. 方法区
JVM 规范中规定的一块运行时数据区,用来存储已经被 JVM 加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据。
注意它只是规范,不同 JVM 对此有不同的实现。常见说法:永久区、元空间。
2. GC 内存回收机制
以 GCRoot 为起点,通过引用可达性分析,确定对象是否可以被回收。
GC Root对象包括:
1. JVM 虚拟机栈-局部变量表中引入的对象
2. 方法区中静态引用指向的对象
3. 存活状态中的线程对象
4. native 方法中 JNI 引用的对象
3. GC 回收算法
标记清除算法
- 找到内存中所有的 GC Root 对象,将与其有直接或间接关联的对象标记为存活对象,对无关联对象标记为垃圾对象。
- 将垃圾对象直接清除。
优点:实现简单,无需将对象进行移动。
缺点:需中断进程中其他组件的执行,并且可能产生内存碎片,提高了垃圾回收的频率。
复制算法
- 将内存分为两块,A 和 B,当前只使用 A。然后进行遍历,标记出存活对象和垃圾对象。
- 将存活对象依次复制到内存 B 中,清空内存 A,并设置 B 为当前使用中的内存。
优点:按顺序分配内存,实现简单,运行高效,无需考虑内存碎片。
缺点:可用内存缩小一半,对象存活率高时需要频繁进行复制。
标记压缩算法
- 遍历内存中对象,标记存活对象和垃圾对象
- 讲存活对象按顺序压缩到内存的某一端,清除存活对象边界外所有空间。
优点:避免产生碎片,也无需缩小空间。
缺点:需要将存活对象进行移动,效率降低。
4. JVM 分代回收策略
在内存分配存储对象的堆内存中,分为新生代和老年代。新创建的对象会会分配到新生代内存中,经过多次回收依然存活的对象,将被转移到老年代中进行存储。
新生代分为 3 个部分:Eden、Survivor0、Survivor1。分配比例为 8:1:1。
1. Eden 中满了之后,会触发 GC。此时策略是,将存活对象负责到 Survivor0,然后清除 Eden 中垃圾对象。
2. Eden 中再次满了后,触发 GC。此时策略是,将 Eden 和 Survivor0 中存活对象转移到 Survivor1 ,然后清空 Eden 和 Survivor0。
3. Survivor0 与 Survivor1 之间切换多次(默认 15 次)之后,如果还有存活对象,将其转移到老年代中。
如果对象很大,新生代中剩余空间不足,会将其直接分配到老年代中。老年代中可能存在引用新生代对象的情况,在新生代执行 GC 时,为了避免查询老年代中所有对象,老年代中维护了一个对新生代对象的引用记录表。
5. 内存泄漏优化
常见问题场景
- Context 或者 View 置为 static。
- 未解注册各种 Listener,比如广播接收器。
- 非静态的 Handler,在执行耗时任务时,持有当前 Activity 的引用。可将 Handler 定义成静态内部类,内部持有 Activity 的弱引用来避免内存泄漏。
- 三方库使用 context 对象存储为静态对象。尽量使用 Context.getApplicationContext。
6. 内存泄漏检测
工具
LeakCanary
实现分析
1. 如何检测内存泄漏
2. 分析内存泄漏对象引用链
实现原理
LeakCanary 中对内存泄漏的检测基于 WeakReference 和 ReferenceQueue。
在构建 WeakReference 对象时传入 ReferenceQueue,当 WeakReference 中传入的对象可以被回收时,会将 WeakReference 对象添加到 ReferenceQueue 中,倘若 WeakReference 中的对象无法被回收时,不会将 WeakReference 对象添加到 ReferenceQueue 中。这样便可以检测到,应该被回收的对象,却没有出现在 ReferenceQueue 中,这些对象就是造成内存泄漏的元凶。
检测时机
向主线程 MessageQueue 中插入了一个 IdleHandler,IdleHandler 只会在主线程空闲时才会被 Looper 从队列中取出并执行。因此能够有效避免内存检测工作占用 UI 渲染时间。