Posts Java 类加载与垃圾回收
Post
Cancel

Java 类加载与垃圾回收

Android 类加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 1、如果类已被加载,则直接返回
    // First, check if the class has already been loaded
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                // 2.1、如果父类不为空则委托父类加载
                c = parent.loadClass(name, false);
            } else {
                // 2.2、委派给启动类加载器加载,Android 这里永远返回 null
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // ClassNotFoundException thrown if class not found
            // from the non-null parent class loader
        }
        // 3、仍未加载到,则通过 findClass() 自己来加载
        if (c == null) {
            // If still not found, then invoke findClass in order
            // to find the class.
            c = findClass(name);
        }
    }
    return c;
}

调用流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
findClass() -> BaseDexClassLoader#findClass() ->
DexPathList#findClass() -> DexPathList.Element#findClass() ->
DexFile#loadBinaryClassName() -> #defineClass() -> #defineClassNative()
art/runtime/native/dalvik_system_DexFile.cc#defineClassNative()

// 继承结构
   ClassLoader
       ^
       |
BaseDexClassLoader
       ^
       |
 PathClassLoader

PathClassLoader 中实际是调用父类 BaseDexClassLoader#findClass() 来加载类的,其核心实现是在 DexPathList 类中,
从该类的 dexElements 数组中(Element) 中加载类,热修复技术的核心在于将修复的类打包为 dex 文件并插入到 dexElements 
数组的前面

Android 中通过自定义 ClassLoader 继承 PathClassLoader 可以加载 apk 之外的类,这是插件化技术的核心

Android 中无启动类加载器
Android 中默认父加载器为 BootClassLoader
Android 中 app 类加载器为 PathClassLoader

双亲委派模型

  • 1、加载类时先查看是否已加载过;
  • 2、未加载过:
    • 2.1、若存在父加载器则先交给父加载器尝试加载;(Android 中默认父加载器为 BootClassLoader)
    • 2.2、若不存在父加载器则交给启动类加载器加载;(Android 中无启动类加载器)
  • 3、若父加载器和启动类加载器都加载不到时,才会交给自己加载 findClass();

有什么好处?

  • 1、避免类重复加载;
  • 2、保护核心类不被恶意破坏;

类加载过程

  • 1、装载:查找和导入 .class 文件
  • 2、链接:把 .class 文件二进制数据合并到 JRE 中
    • 2.1、校验:检查载入 .class 二进制文件正确性
    • 2.2、准备:给静态变量分配存储空间并赋初值
    • 2.3、解析:将符号引用转换为直接(地址)引用
  • 3、初始化:按顺序对类的静态变量、静态代码块进行初始化操作

引申

类加载的方式有几种?

  • 1、Class.forName()

类卸载?

  • 1、加载类的 ClassLoader 被 GC 回收;
  • 2、该类所有的实例都被 GC,即 JVM 中不存在该类的任何实例;
  • 3、该类的 Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法;

何时触发初类的始化?

  • 1、JVM 启动时对包含 main(String[] args) 方法的类进行初始化;
  • 2、new/getstatic/putstatic/invokestatic,即:new 对象、获取/设置静态变量、调用静态方法
  • 3、Java 反射机制对类进行反射调用时可能触发对类的初始化;
  • 4、初始化子类时,可能触发对父类的初始化;

static 变量/代码块初始化的顺序?

  • 从上到下的顺序来初始化

运行时数据区

程序计数器、JVM 栈、本地方法栈、堆区、方法区、运行时常量区

程序计数器

作用:记录下一条要执行的字节码指令

字节码解释器的工作:改变 PC 计数器来选取下一条要执行的字节码指令

多线程:通过轮流切换并分配处理器执行时间的方式实现的

每个线程会有独立的 PC 计数器,因此程序计数器是线程私有的

native 方法时:PC 计数器指向 Undefined Java 方法时:PC 计数器指向下一条指令地址

不会 OOM,但是会 SOE

虚拟机栈

每一条 JVM 线程都有一个私有的虚拟机栈,生命周期与线程一致

作用:存储线程中 Java 方法调用的状态,包括局部变量、参数、返回值以及中间运算结果

本地方法栈

作用:用来支持 native 语言

Hotspot 将本地方法栈和虚拟机栈合二为一

堆区

堆是所有线程所共享的运行时内存区域

作用:存放对象实例以及数组

堆中存储的对象被 GC 管理

内存回收角度:可粗略分为新生代和老年代 内存分配角度:多个线程私有的分配缓冲区

划分的目的:更快的回收或者分配内存

堆容量:可固定、可扩展 堆使用的内存在物理上不需要连续,逻辑上连续即可

当空间不足时会出现 OOM 异常

方法区

方法去是所有线程共享的运行时内存区域

作用:存储已经被 JVM 加载的类的结构信息,包括运行时常量池、字段、方法信息、静态变量等数据

方法区是堆区的逻辑组成部分,在物理上不需要连续

运行时常量池

是方法区的一部分,可以理解为是类或接口的常量池的运行时表现形式

GC 算法

如何判断一个对象是否是垃圾对象?

1、可达性分析法

是否存在到达 GC Roots 的路径

  • GC Roots 有哪些?
    • 线程
    • 虚拟机栈
    • 本地方法栈
    • 系统类
    • 类变量

2、引用计数法

引用计数为 0 则为垃圾对象

标记-清除算法

当堆中有效内存空间被耗尽时,就会进行垃圾回收工作

  • 标记垃圾对象:遍历所有的 GC Roots,然后将所有 GC Roots 可达对象标记为存活对象
  • 清除垃圾对象:遍历堆中所有对象,将没有标记的对象全部清除掉

缺点:

  • 效率低、stop the world
  • 产生内存碎片(清理出的空闲内存是不连续的)、分配大对象时容易导致 GC

复制算法

背景:解决标记-清除算法的缺点

  • 将内存空间分为活动区间和空闲区间,活动区间用于存储动态分配的对象;
  • 遍历活动空间并将活着的对象全部复制到空闲区间且按照内存地址依次排列
  • GC 将更新存活对象的内存引用指向新的内存地址

缺点:

  • 空间利用率减少一半
  • 要求对象存活率非常低

标记-整理算法

  • 标记:遍历所有的 GC Roots,将存活的对象标记;
  • 整理:移动所有存活的对象且按照内存地址依次序排列,然后将末端地址以后的内存全部回收

弥补了内存分散的缺点和内存减半的缺点

缺点:

  • 效率不高

总结

  • 算法效率:复制 > 标记-整理 > 标记-清除
  • 内存整齐度:复制 = 标记-整理 > 标记-清除
  • 内存利用率:标记-整理 = 标记-清除 > 复制

分代收集算法

  • 新生代:朝生夕死的对象,如:方法中的局部变量
  • 老年代:单例对象、Class 对象
  • 永久代:string 常量池中的对象、享元对象
  • 元空间:Java 1.8 出现,用于替换永久代

minor GC:针对新生代

major GC/Full GC:针对老年代,偶尔会对永久带回收

多次 minor GC 后会触发 major GC

Stop The World

停止除了 GC 外的所有线程,只进行 GC 工作

内存优化:尽量减少 GC 次数

This post is licensed under CC BY 4.0 by the author.