文章目录
-
- [1. 对象的三个"逃逸"等级](#1. 对象的三个“逃逸”等级)
- [2. 逃逸分析后能做什么?(三大神技)](#2. 逃逸分析后能做什么?(三大神技))
-
- [① 栈上分配 (Stack Allocation) ------ 最核心的优化](#① 栈上分配 (Stack Allocation) —— 最核心的优化)
- [② 标量替换 (Scalar Replacement)](#② 标量替换 (Scalar Replacement))
- [③ 同步消除 (Lock Elision)](#③ 同步消除 (Lock Elision))
- [3. 它是如何判断对象"逃逸"的?](#3. 它是如何判断对象“逃逸”的?)
- [4. 为什么要强调它是 JIT 的功能?](#4. 为什么要强调它是 JIT 的功能?)
- 总结
逃逸分析的核心目的就是判断有没有多线程竞争,如果没有多线程竞争,就会删除锁,提高性能
它的核心任务是:分析一个在方法内创建的对象,它的生命周期是否会超出这个方法的范围(即"逃出"当前方法的控制)。
1. 对象的三个"逃逸"等级
JVM 会根据对象的使用范围,将其标记为三个等级:
- 从不逃逸 (No Escape):对象只在当前方法内使用,方法结束,对象就没用了。
- 方法逃逸 (Arg Escape):对象作为参数传递给了其他方法,或者被赋值给了其他线程能看到的变量。
- 线程逃逸 (Global Escape):对象被赋值给了类变量(static)或者存入了堆中,其他线程可以访问它。
2. 逃逸分析后能做什么?(三大神技)
一旦分析出对象"没有逃逸",JVM 就可以大胆地进行以下"骚操作"来提升性能:
① 栈上分配 (Stack Allocation) ------ 最核心的优化
- 传统逻辑:Java 几乎所有的对象都在**堆(Heap)**上分配。堆是共享的,创建对象需要加锁找空间,回收对象需要 GC(垃圾回收),压力很大。
- 优化逻辑:如果对象不逃逸,JVM 就直接在**栈(Stack)**上分配。
- 结果 :方法一执行完,栈帧弹出,对象直接随之销毁。完全不需要 GC 介入,效率极高。
② 标量替换 (Scalar Replacement)
- 概念 :有些对象可能不需要作为一个完整的"块"存在。比如一个
Point对象只有int x和int y。 - 优化逻辑 :如果不逃逸,JVM 会把这个对象拆解掉,直接在栈上创建两个基本类型的变量
x和y。 - 结果:连对象头(Object Header)的内存开销都省了,直接变成了寄存器或栈上的基本运算。
③ 同步消除 (Lock Elision)
- 这就是你之前提到的:如果对象不逃逸,说明只有当前线程能看到它,那它身上的
synchronized锁就是摆设,JIT 会直接把锁删掉。
3. 它是如何判断对象"逃逸"的?
让我们看一段直观的代码:
java
public class EscapeTest {
public static Object globalVariable;
// 场景 1:发生逃逸(Global Escape)
public void globalEscape() {
Object obj = new Object();
globalVariable = obj; // 对象被全局变量引用,逃逸了
}
// 场景 2:发生逃逸(Arg Escape)
public Object methodEscape() {
Object obj = new Object();
return obj; // 对象被返回给了调用者,逃逸了
}
// 场景 3:没有逃逸(No Escape)
public void noEscape() {
Object obj = new Object(); // 仅在方法内使用
System.out.println(obj.hashCode());
}
}
4. 为什么要强调它是 JIT 的功能?
逃逸分析是一项耗时的静态分析。
- 如果 JVM 在启动时对每一行逻辑都做分析,Java 启动会慢得像蜗牛。
- 因此,逃逸分析通常只发生在热点代码(经常被调用的代码)上,由 C2 编译器在运行时动态进行。
总结
逃逸分析就像是一个"侦探",它观察对象的动向:
- 如果发现对象是"宅男"(不出方法区),就给它安排栈上分配 和标量替换,省去 GC 麻烦。
- 如果发现对象是"社交达人"(会逃到别处),那就老老实实去堆里待着。
这是 Java 能够在中大规模高并发场景下,依然保持极致性能的重要原因之一。
你现在对 Java 内存模型中的"堆"和"栈"的区分是否已经比较清晰了?这直接关系到为什么"栈上分配"会比"堆"快那么多。