1. 类初始化和类加载
1.1 创建对象的过程
在 JVM 中,创建一个新对象并非简单的内存分配,它包含几个关键步骤。当执行到 new 关键字时,JVM 会先检查目标类是否已加载。若未加载,则触发类加载流程;若已加载,则继续后续步骤。
接下来是在堆内存中分配空间,随后将对象实例变量初始化为零值(对象头除外)。之后需要对对象头进行必要的设置,比如记录哈希码、分代年龄等信息。最后,才会执行对应的构造方法(init 方法)。
- 类加载:JVM 在执行到 new 关键字的时候,会先进行对应类的类是否加载,如果没有加载,那么就会先执行类加载,然后再执行 new 关键字。
- 分配内存空间:在堆中分配一段空间给对应对象。
- 初始化零值操作:给对象的空间内的对应成员初始化为零值(对象头除外)。
- 进行必要的设置:比如说:给对象头设置一些信息,类的元数据,对象的年龄,对象的哈希码。
- 执行 init 方法:执行对应的构造方法。
1.2 对象的生命周期
对象的生命周期可以概括为创建、使用、销毁三个阶段。
- 创建:即上述的类加载与对象实例化过程。
- 使用:调用该对象的成员方法或访问属性。
- 销毁:当对象没有被引用了,就会被看作垃圾进行回收。
1.3 类加载器
JVM 提供了三种主要的类加载器,它们各司其职:
- 启动类加载器:负责加载 Java 核心库里面的类,通常由 C++ 实现。
- 扩展类加载器:加载一些扩展的类,比如说框架的类。
- 应用程序类加载器:加载用户自己手动创建的类,也就是我们日常开发中最常接触的。
1.4 双亲委派模型
这是类加载器的核心原则,遵循'向上委派,向下加载'的策略。
如果一个类加载器收到了加载类的任务,不会自己先执行,而是往上委派给自己的父类,依次类推,直到委派给最终的父类也就是启动类加载器。然后从启动类加载器开始,判断该类是否被自己加载,如果能直接返回即可,如果不能继续往下加载(抛给子类),依次类推。
这种机制主要有以下作用:
- 保证类的唯一性:向上委派、向下加载的方式,保证了一个类不会被重复加载,并且保证一个类只会被一个类加载器加载。
- 保证安全性:如果用户自己编写了一些类,类的全限定名与 Java 核心库中的类一致,可能会出现优先加载用户的类,从而间接导致核心类被用户修改了。通过向上委派、向下加载的方式从上开始加载,保证了类的安全性。
- 形成了隔离和分层的效果:不同的类加载器负责不同的类,既不会重复加载,也保证了高效性。
- 简化了类加载流程:这种方式保证了一个类只能加载一次并且不同的类加载器负责不同的类,从而简化了流程。
1.5 类加载的过程
从大体上类加载分为五步:类加载、连接、初始化、使用、卸载。细分又有七步:类加载、验证、准备、解析、初始化、使用、卸载。
- 类加载:通过类的全限定名找到对应的二进制字节码文件,将该文件转换到内存形成创建对象实例的模板。
- 验证:验证该二进制字节码是否符合 JVM 规范,比如说校验魔术、主版本号等等。具体包括文件格式校验、元数据验证、字节码验证、符号引用验证。
- 准备:给该类的静态变量赋一些初始化值。
- 解析:将原来的符号引用转为直接引用。
- 初始化:执行构造器方法,执行父类的静态成员、子类的静态成员、父类的构造方法、子类的构造方法。
- 使用:用户使用。
- 卸载:判断该类没有被引用即可卸载,主要看三个方面:该类里面的所有实例对象都已经被回收了、该类的 ClassLoader 对象已经被回收了、该类的实例没有被引用。
2. 垃圾回收
2.1 垃圾回收介绍
以前传统的开发人员需要进行一个对内存的手动释放,而垃圾回收就是可以自动的对内存进行回收。它既提高了开发效率,也避免了一些问题,比如内存溢出、内存泄漏。
2.2 触发垃圾回收的方式
GC 的触发方式主要有以下几种:
- 内存不足:当内存不足的时候,会触发直接垃圾回收机制。
- 手动执行:开发人员自己手动指定执行,例如调用
System.gc()。 - JVM 调参:通过调参可以来指定垃圾回收的行为。
- 内部策略:垃圾回收内部可能指定了创建对象的数量和内存使用情况,如果超过一定的阈值就会触发垃圾回收。
2.3 判断垃圾的方法
判断对象是否为垃圾主要有两种方法:
- 引用计数法:给每次对象分配一个计数器,如果该对象被一个对象引用,那么就会将计数器进行加一。如果该对象没有被引用,那么此时计数器为 0,则被判断为可以被回收。不过这种方法无法解决循环引用的问题。
- 可达性分析法:从 GC Root 出发找对应对象进行标记,如果有一个对象没有被标记(代表没有被直接引用或间接引用),那么该对象可以被回收。GC Root 对象包括:Java 的核心库类、调用本地方法的类、活跃线程的类等。
2.4 垃圾回收算法
主流的垃圾回收算法包括标记清除、标记整理和复制算法。
- 标记清除算法:对引用的对象进行标记,然后进行清除。注意这里不是真正的清除,而是记录其对象的起始地址和结束地址到一个地址表中,下次要添加新对象时会先从表中找,找到一个适合大小的就会进行覆盖。好处是速度快,缺点是内存碎片化严重(内存不连续了,本来可以存入的对象存入不了)。
- 标记整理算法:同理进行标记,然后再对可存活对象进行整理,最后清除。好处是避免了内存碎片化问题,缺点是速度慢。
- 复制算法:将内存空间分为两份,一份存对象 from,一份为空 to。当要回收时,复制可存活对象移入为空的内存空间 to 中(移入既整理),然后对存对象的空间 from 整体清除,然后名称 from 和 to 换过来。好处是避免了内存碎片化问题,缺点是内存使用率不高。
2.5 垃圾回收器
根据作用范围不同,收集器也有所区别:
-
新生代:Serial、ParNew、Parallel Scavenge,都是使用的复制算法。
-
老年代:Serial Old(标记整理)、CMS(标记清除)、Parallel Old(标记整理)。
-
整个堆:G1(整体标记整理,细分区域使用复制)。
-
Serial GC:串行的收集器。
-
ParNew/CMS:并行的收集器,重响应时间快的特点。
-
Parallel GC:并行的收集器,重吞吐量高的特点。
-
G1:并行的收集器,比较平衡。
2.6 minorGC、majorGC、fullGC
-
minorGC:
- 范围:作用于新生代。
- 作用时机:当新生代的 Eden 区空间无法装入新创建的对象时,就会触发一次 minorGC,并且每触发一次 minorGC,其存活对象的年龄加一。
- 细节:作用时间短,次数多。
-
majorGC:
- 范围:作用于老年代。
- 作用时机:当新生代的对象需要移入老年代时,发现其空间不足,那么就会触发 majorGC。
- 细节:作用次数不多,而不常使用。
-
fullGC:
- 范围:作用于整个堆。
- 作用时机:当新生代对象需要移入老年代时,发现其空间不足,会触发 minorGC,触发之后,如果还是老年代空间不足,那么就会触发 fullGC,对整体进行整理。
- 细节:作用次数不多,作用时间长。
2.7 CMS 与 G1 的区别
| 方面 | CMS | G1 |
|---|---|---|
| 作用范围 | 作用于老年代 | 作用于整个堆 |
| STW 的时间 | CMS 是重响应时间快的,STW 的时间会更短 | G1 可以对 STW 时间进行一个可预测的操作 |
| 内存碎片化 | CMS 使用的是标记清除算法,会造成内存碎片化问题 | G1 使用的是标记整理算法,不会造成内存碎片化问题 |
| 垃圾回收过程 | 初始标记、并发标记、重新标记、并发清理 | 初始标记、并发标记、最终标记、筛选回收(如果并发标记中出现了对象引用改变,会将其对象引用存入一个队列中,直到最终标记时从队列进行进一步的筛选) |
| 浮动垃圾 | 它使用的是并发清理,那么如果垃圾回收的速度低于产生垃圾的速度,那么就会出现问题,从而导致 CMS 转变为 Serial,并且也由于在回收的过程中会产生一些浮动垃圾,因此需要一部分空间来存储对应的浮动垃圾 | 它虽然也会产生垃圾,但是不会出现该问题,并且它是使用的多个垃圾回收器进行垃圾回收,效率更高 |


