Java - 你一定要知道的 JVM 逃逸分析

简介: Java - 你一定要知道的 JVM 逃逸分析

提到JVM,相信大家一定知道JVM是什么?但是,提到逃逸分析,相信大多数人都可能一脸懵逼,逃逸分析到底是什么呢?接下来给大家分享一下。

在Java的编译体系中,一个Java的源代码文件变成计算机可执行的机器指令的过程中,需要经历两段编译,第一段编译就是通过javac命令把java文件编译成JVM可以识别的class文件,第二段编译就是把class文件翻译成字节码指令,让计算机识别。

在第二段编译中,JVM通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。很显然,经过解释执行,其执行速度必然会比执行二进制字节码程序慢很多。这就是传统的JVM的解释器的功能。为了解决这种效率问题,引入了JIT(即时编译)技术。

引入JIT技术,Java程序虽然还是通过解释器进行解释执行,但是,当某个方法执行调用的次数比较多的时候,就会被JVM认为是个"热点代码"。那么,如果是你,你会想到怎么做呢?诶,没错,把这些"热点代码"缓存起来。JIT也是这么做的,JIT会把部分"热点代码"翻译成本地机器相关的机器码,并进行优化,然后再把翻译后的机器码缓存起来,以备下次使用。

JVM内存分配策略

在JVM中,JVM管理的内存包括方法区,虚拟机栈,本地方法栈,堆,程序计数器等。(这里就不一一介绍了,感兴趣的同学我之后会写JVM的文章)

一般情况下,JVM运行时数据都存储在栈和堆中。栈用来存放一些基本变量和对象的引用(当然这不是绝对的后面会介绍到),堆用来存放数组的元素和对象,也就是new出来的具体实例。

随着JIT编译器的发展与逃逸分析的技术成熟。栈上分配,标量替换优化技术就会导致对象都分配到堆上这个说法变的不是那么绝对了。

什么是逃逸分析?

逃逸分析就是,当一个对象被new出来之后,它可能被外部所调用,如果是作为参数传递到外部了,就称之为方法逃逸。

例如

非方法逃逸

publicstaticvoidreturnStr(){
Useruser=newUser();
user.setId(1);
user.setName("张三");
user.setAge(18);
}
publicstaticStringreturnStr(){
Useruser=newUser();
user.setId(1);
user.setName("张三");
user.setAge(18);
returnuser.toString();//这里User要实现get,set方法,还要实现toString方法}

方法逃逸

publicstaticUserreturnStr(){
Useruser=newUser();
user.setId(1);
user.setName("张三");
user.setAge(18);
returnuser;//这里User要实现get,set方法}

大家应该看出区别了吧,这里第一段的两个方法均没有逃逸,而第二段的方法却逃逸了,这说明,想要逃逸方法的话,需要让对象本身被外部调用。

使用逃逸分析,编译器可以对代码做以下优化:

  1. 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步
  2. 将堆分配转换为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
  3. 分离对象或标量替换,有点对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

这里开启和关闭逃逸分析用这个

  • -XX:+DoEscapeAnalysis : 表示开启逃逸分析
  • -XX:-DoEscapeAnalysis : 表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis

实战:通过打印GC来观察是否是栈上分配

publicclassTest {
publicstaticvoidmain(String[] args) {
Testtest=newTest();
test.createUser();
System.out.println("11");
    }
publicvoidcreateUser(){
inti=0;
while(true){
Useruser=newUser();
user.setId(i);
user.setName("张三");
user.setAge(18);
i++;
        }
    }
}
publicclassUser {
privateintid;
privateStringname;
privateintage;
publicintgetId() {
returnid;
    }
publicvoidsetId(intid) {
this.id=id;
    }
publicStringgetName() {
returnname;
    }
publicvoidsetName(Stringname) {
this.name=name;
    }
publicintgetAge() {
returnage;
    }
publicvoidsetAge(intage) {
this.age=age;
    }
}

这里有两个类,第一个是Test类,一个是User实体类,Test类中,死循环来进行创建User对象,在idea中配置 -XX:+PrintGC参数来负责打印GC信息,启动类

结果

image.png这里可以看到程序一直没有结束,但是GC信息一直没有打印,这时候我们把逃逸分析关闭 在idea中配置这两个-XX:+PrintGC -XX:-DoEscapeAnalysis

再次启动类

image.png

这里可以看到控制台一直在输出GC信息,这样是不是也可以得出结论,开启逃逸分析之后如果不是逃逸方法,那么对象就是在栈上分配。

同步省略

在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有发布到其他线程。

如果同步块所使用的锁对象通过这种分析被证明只能被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消这部分代码的同步,这个取消同步就叫做同步省略,也叫锁消除。

标量替换

通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数-XX:+EliminateAllocations,JDK7之后默认开启

标量与聚合量

标量即不可被进一步分解的量,而Java的基本数据类型就是标量(比如int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在Java中对象就是可以被进一步分解的聚合量

在JIT阶段,如果经过逃逸分析,发现一个对象不被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含若干个成员变量来替代。

publicstaticvoidmain(String[] args) {
alloc();
}
privatestaticvoidalloc() {
Pointpoint=newPoint(1,2;
System.out.println("point.x="+point.x+"; point.y="+point.y);
}
classPoint{
privateintx;
privateinty;
}

以上代码中,point对象并没有逃逸出alloc方法,并且point对象是可以拆解成标量的。那么,JIT就会不会直接创建Point对象,而是直接使用两个标量int x ,int y来替代Point对象。

替换后

privatestaticvoidalloc() {
intx=1;
inty=2;
System.out.println("point.x="+x+"; point.y="+y);
}

这种替换可以大大减少堆内存的占用,因为一旦不需要创建对象了,那么就不需要分配堆内存了。

目录
相关文章
|
14天前
|
存储 Java
深入理解Java虚拟机:JVM内存模型
【4月更文挑战第30天】本文将详细解析Java虚拟机(JVM)的内存模型,包括堆、栈、方法区等部分,并探讨它们在Java程序运行过程中的作用。通过对JVM内存模型的深入理解,可以帮助我们更好地编写高效的Java代码,避免内存溢出等问题。
|
5天前
|
算法 Java
深入浅出JVM(十六)之三色标记法与并发可达性分析
深入浅出JVM(十六)之三色标记法与并发可达性分析
|
5天前
|
Java 索引
深入浅出JVM(五)之Java中方法调用
深入浅出JVM(五)之Java中方法调用
|
5天前
|
Java
【Java多线程】分析线程加锁导致的死锁问题以及解决方案
【Java多线程】分析线程加锁导致的死锁问题以及解决方案
17 1
|
7天前
|
存储 Arthas 监控
JVM工作原理与实战(三十):堆内存状况的对比分析
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了堆内存状况的对比分析、产生内存溢出的原因等内容。
13 0
|
7天前
|
Java 编译器 对象存储
java一分钟之Java入门:认识JDK与JVM
【5月更文挑战第7天】本文介绍了Java编程的基础——JDK和JVM。JDK是包含编译器、运行时环境、类库等的开发工具包,而JVM是Java平台的核心,负责执行字节码并实现跨平台运行。常见问题包括版本不匹配、环境变量配置错误、内存溢出和线程死锁。解决办法包括选择合适JDK版本、正确配置环境变量、调整JVM内存参数和避免线程死锁。通过代码示例展示了JVM内存管理和基本Java程序结构,帮助初学者更好地理解JDK和JVM在Java编程中的作用。
20 0
|
7天前
|
监控 算法 安全
JVM工作原理与实战(二十三):堆的垃圾回收-引用计数法和可达性分析法
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了判断堆上的对象是否可以回收的方法(引用计数法、可达性分析法)、查看垃圾回收日志等内容。
13 0
|
7天前
|
存储 监控 安全
JVM工作原理与实战(十六):运行时数据区-Java虚拟机栈
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了运行时数据区、Java虚拟机栈等内容。
11 0
|
8天前
|
存储 缓存 安全
【 Java中String源码分析(JVM视角你不来看看?】
【 Java中String源码分析(JVM视角你不来看看?】
14 0
|
15天前
|
存储 机器学习/深度学习 Java
【Java探索之旅】数组使用 初探JVM内存布局
【Java探索之旅】数组使用 初探JVM内存布局
26 0