Java内存模型和垃圾回收是Java运行时环境的核心组成部分,前者定义了多线程场景下内存的访问规则,后者负责自动管理堆内存中对象的生命周期,两者共同保障了Java程序的稳定运行。

Java内存模型核心概念
主内存与工作内存
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的主内存副本。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据,不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
内存间交互操作
Java内存模型定义了8种操作来完成主内存和工作内存之间的交互,这些操作都是原子的:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占的状态
- unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中
- use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作
- assign(赋值):作用于工作内存的变量,把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
- store(存储):作用于工作内存的变量,把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
- write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中
三大特性
Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的:
- 原子性:除了long和double的非原子协定之外,基本数据类型的访问读写都是具备原子性的,更大范围的原子性可以通过
synchronized关键字来保证 - 可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,volatile、synchronized和final关键字都能实现可见性
- 有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的,volatile和synchronized可以保证线程之间操作的有序性
垃圾回收机制基础
垃圾回收的目标区域
Java内存区域中,程序计数器、虚拟机栈、本地方法栈这三个区域随线程而生,随线程而灭,不需要过多考虑回收的问题。垃圾回收主要作用于堆和方法区,其中堆是垃圾回收的主要区域,几乎所有的对象实例都在这里分配内存。
判断对象是否可回收
主流的Java虚拟机都是通过可达性分析算法来判断对象是否存活,这个算法的基本思路就是通过一系列称为GC Roots的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为引用链,如果某个对象到GC Roots间没有任何引用链相连,则此对象是不可能再被使用的。
可以作为GC Roots的对象包括以下几种:
- 在虚拟机栈中引用的对象
- 在方法区中类静态属性引用的对象
- 在方法区中常量引用的对象
- 在本地方法栈中JNI引用的对象
- Java虚拟机内部的引用
常见垃圾回收算法
| 算法名称 | 核心思路 | 优缺点 |
|---|---|---|
| 标记-清除算法 | 先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象 | 优点是实现简单,缺点是会产生大量不连续的内存碎片,可能导致后续分配大对象时无法找到足够的连续内存而提前触发垃圾回收 |
| 标记-复制算法 | 将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块用完了,就将还存活的对象复制到另一块上,然后再把已使用过的内存空间一次清理掉 | 优点是不会有内存碎片,实现简单,缺点是内存利用率只有一半 |
| 标记-整理算法 | 标记过程与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存 | 优点是不会产生内存碎片,内存利用率高,缺点是移动对象需要更新引用地址,开销较大 |
| 分代收集算法 | 根据对象存活周期的不同将内存划分为几块,一般是把堆分为新生代和老年代,新生代使用标记-复制算法,老年代使用标记-清除或者标记-整理算法 | 是目前主流虚拟机采用的算法,能够根据不同年代的特点选择合适的回收算法,提升回收效率 |
Java内存模型与垃圾回收的关联
Java内存模型定义了多线程下内存的访问规则,而垃圾回收是在这个规则下运行的自动内存管理过程。比如volatile关键字既保证了变量的可见性(属于内存模型范畴),也能避免指令重排序对对象初始化过程的影响,减少垃圾回收时可能出现的对象状态异常问题。
另外,垃圾回收过程中会触发Stop The World,也就是暂停所有用户线程,这个过程中工作内存和主内存的交互会被暂时中断,直到垃圾回收完成。理解内存模型的工作机制,能够帮助我们更好地评估垃圾回收对程序性能的影响,合理配置垃圾回收器参数,减少停顿时间对业务的影响。
代码示例:观察垃圾回收过程
下面的代码通过创建大量临时对象触发垃圾回收,我们可以通过设置JVM参数-XX:+PrintGCDetails来查看垃圾回收的详细日志:
public class GcDemo {
public static void main(String[] args) {
// 循环创建大量临时对象,触发垃圾回收
for (int i = 0; i < 10; i++) {
byte[] temp = new byte[1024 * 1024]; // 每次创建1MB的字节数组
System.out.println("第" + (i + 1) + "次创建对象");
// 主动建议垃圾回收,注意这只是建议,虚拟机不一定会立即执行
System.gc();
try {
Thread.sleep(1000); // 休眠1秒,方便观察日志
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行这段代码时,结合JVM输出的GC日志,可以看到新生代、老年代的内存变化,以及垃圾回收的执行过程,进一步理解垃圾回收的实际运作方式。同时结合Java内存模型的知识,也能明白为什么多线程场景下垃圾回收的停顿会影响所有线程的内存访问操作。