初始化
针对类对象的内容进行初始化
执行代码块, 静态代码块, 加载父类…
类加载的时机
并非 Java 程序运行, 所有的类就会被加载
而是真正用到该类, 才会被加载
(懒汉模式)
- 常见的类加载时机
- 构造类的实例
- 调用这个类的静态方法 / 静态成员变量
- 加载子类之前, 需先加载其父类
(加载过一次之后, 后续使用就不必重复加载)
双亲委派模型
加载
找到 .class 文件, 打开文件, 读取文件(将文件内容读取至内存中)
双亲委派模型
描述的是找到 .class 文件的基本过程
- JVM 默认提供了三个
类加载器
- BootstrapClassLoader(负责加载标准库中的类)
- ExtensionClassLoader(负责加载 JVM 扩展库中的类)
- ApplicationClassLoader(负责加载用户提供的第三方库 / 用户项目代码中的类)
上述三个类加载器
, 存在"父子关系"
此处所说的父子关系并不是父类子类
(可以简单理解为 Parent 属性)
上述类加载器如何配合工作🍭
- 从 ApplicationClassLoader 开始加载一个类
- ApplicationClassLoader 会将加载任务, 交给其父(ExtensionClassLoader), 让其父去执行
- ExtensionClassLoader 会将加载任务, 交给其父(BootstrapClassLoader), 让其父去执行
- BootstrapClassLoader 会将加载任务, 交给其父(null), 让其父去执行
但 BootstrapClassLoader 的父亲是 null, 于是自行加载(自己动手丰衣足食)
此时 BootstrapClassLoader 就会搜索标准库目录相关的类
如果找到需要加载的类, 就会去进行加载
如果未找到需要加载的类, 就由其子类加载器
进行加载 - ExtensionClassLoader 加载 BootstrapClassLoader 未能加载的 JVM 扩展库中的类
此时 ExtensionClassLoader 就会搜索 JVM 扩展库目录相关的类
如果找到需要加载的类, 就会去进行加载
如果未找到需要加载的类, 就由其子类加载器
进行加载 - ExtensionClassLoader 加载 BootstrapClassLoader 未能加载的用户提供的第三方库 / 用户项目代码中的类
此时 ExtensionClassLoader 就会搜索用户提供的第三方库 / 用户项目代码目录中相关的类
如果找到需要加载的类, 就会去进行加载
如果未找到需要加载的类, 就会抛出ClassNotFoundException(未找到指定类异常)
- 类
加载器
自行加载的情况
- 该
类加载器
没有父 - 该
类加载器
的父加载完毕后仍未找到所需加载的类
为什么双亲委派模型的执行顺序是这样的?
上述过程是一个递归的过程(保证了 BootstrapClassLoader 最先执行), 避免因用户创建一些奇怪的类从而引起的 Bug
假设用户在代码中创建了一个系统已存在的类
根据上述的加载流程, 此时 JVM 会先加载标准库中的类, 而不是用户自己代码中的类
这样避免了因为类相同从而可能引起 JVM 标准库中的类出现混乱
破坏双亲委派模型
自己写的类加载器可以遵守上述的执行过程, 也可以不遵守上述的执行过程
看实际的需求
🔎GC(垃圾回收机制)
垃圾
不再使用的内存
垃圾回收
将不用的内存进行释放
如果内存一直占用, 不去释放, 就会导致剩余的空间越来越少, 从而导致后续申请内存失败
- 对于进程, 这种情况可能会随着进程的结束从而将内存恢复
- 对于服务器(7 * 24 运行), 这种情况就是致命的
由此, Java 中引入了 GC, 帮助我们自动进行释放"垃圾"
- GC 的优点: 省心, 能够自动将不用的内存释放
- GC 的缺点: 消耗额外的系统资源, 额外的性能开销(STW 问题)
STW(Stop The World)
假设内存中的垃圾很多, 此时触发一次 GC 操作
其开销可能非常大, 大到可能将系统资源耗光
另一方面, GC 回收垃圾时, 可能会涉及一些锁操作, 导致业务代码无法正常运行
这样的卡顿, 极端情况下可能是几十毫秒甚至上百毫秒
注意
Scanner sc = new Scanner(System.in);
sc.close();
类似于这种释放的是文件资源, 并非内存
GC的回收单位
- JVM 中存在的内存区域
- 堆
- 栈
- 虚拟机栈
- 本地方法栈
- 程序计数器
- 元数据
GC 主要是针对堆进行内存释放的
这是因为堆上的对象存活时间相对较长
而栈上的对象会随着方法的结束而结束
- 将内存空间大致划分为3类
- 正在使用的内存
- 不用的内存(但未回收)
- 未分配的内存
- GC 回收是以"对象"为基本单位, 并非字节
- GC 回收的是整个对象(整个对象不再使用时回收), 并非一部分使用, 一部分不使用
(一个对象可能有多个属性 ,其中一部分属性需要使用, 一部分属性用过之后不再进行使用, GC 进行回收是当整个对象不再使用时, 即该对象中所有属性不再使用)
GC的实际工作过程
- GC 的实际工作过程可以划分为2步
- 寻找垃圾(找到不再使用的内存)
- 回收垃圾(将不再使用的内存进行释放)
寻找垃圾🍭
- 寻找垃圾有2种方法
- 引用计数(
python
/php
) - 可达性分析(
Java
)
引用计数🍂
为每个对象分配一个计数器
创建一个指向该对象的引用时, 该对象的计数器 + 1
销毁一个指向该对象的引用时, 该对象的计数器 - 1
举个栗子🌰
Test t1 = new Test();// Test 对象引用计数 + 1 Test t2 = new Test();// Test 对象引用计数 + 1 Test t3 = new Test();// Test 对象引用计数 + 1 t1 = null;// Test 对象引用计数 - 1
- 引用计数的不足
- 内存空间浪费
- 循环引用
内存空间浪费
- 引用计数需要为每个对象分配一个计数器
- 当代码中的对象较少时, 空间浪费率较低
- 当代码中的对象较多时, 空间浪费率较高
- 当每个对象的体积(占用的内存空间)较小时, 此时分配的计数器所占空间会较为突出
(假设计数器所占内存空间为4字节, 当对象的体积为4字节时, 此时所消耗的额外空间相当于一个对象的体积)
循环引用
分析如下伪代码
public class Node { Node next = null; } Node a = new Node();// 1号对象, 引用计数为1 Node b = new Node();// 2号对象, 引用计数为1 a.next = b;// 2号对象, 引用计数为2(a.next 指向 b) b.next = a;// 1号对象, 引用计数为2(b.next 指向 a)
此时将 a 和 b 进行销毁
a = null;// 1号对象, 引用计数为1(2 - 1 = 1) b = null;// 2号对象, 引用计数为1(2 - 1 = 1)
此时1号对象和2号对象的引用计数为1, 表示无法释放内存
(引用计数为0时, 释放内存)
但此刻1号对象与2号对象却无法被访问(循环引用
)