7 逃逸分析

1 逃逸分析的直观认识

主要就是分析对象的动态作用域,分析一个对象的动态作用域是否会逃逸出方法范围、或者线程范围。

如果一个对象在一个方法内定义,如果被方法外部的引用所指向,那认为它逃逸了。否者,这个对象,没有发生逃逸。

2 逃逸分析的官方概念

一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针。在JVM的即时编译语境下,逃逸分析将判断新建的对象是否逃逸。

即时编译判断对象是否逃逸的依据:一种是对象是否被存入堆中(静态字段或者堆中对象的实例字段),另一种就是对象是否被传入未知代码。

有人觉得官方概念晦涩难懂,那是因为很多java程序员对计算机底层的东西不理解。所以想在技术领域深入的话就必须苦练基本功,我在带团队的时候都希望团队的小伙伴能抓基本功,要不然讲很多书面化的东西听不懂。题外话先不聊,我们继续讲技术

3 逃逸分析的类型

方法逃逸:当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其它方法中。

  1. 通过调用参数,将对象地址传递到其他方法中
  1. 对象通过return语句将对象指针,返回给其他方法

我们看下面的代码

//StringBuffer对象发生了方法逃逸

public static StringBuffer createStringBuffer(String s1, String s2) {

StringBuffer sb = new StringBuffer();

sb.append(s1);

sb.append(s2);

return sb;

}

上面的例子中,StringBuffer 对象通过return语句返回。

StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。

甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。

不直接返回 StringBuffer,那么StringBuffer将不会逃逸出方法

具体代码如下:

// 非方法逃逸

public static String createString(String s1, String s2) {

StringBuffer sb = new StringBuffer();

sb.append(s1);

sb.append(s2);

return sb.toString();

}

可以看出,想要逃逸方法的话,需要让对象本身被外部调用,或者说, 对象的指针,传递到了 方法之外。

线程逃逸:当一个对象可能被外部线程访问到,这种称为线程逃逸。例如赋值给类变量或可以在其它线程中访问的实例变量

4 逃逸分析后的代码优化

从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。

如果能证明一个对象不会逃逸到方法或线程之外(换句话说是别的方法或线程无法通过任何途径访问到这个对象),或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可能为这个对象实例采取不同程度的优化。

通过逃逸分析,编译器会对代码进行优化。

//StringBuffer对象发生了方法逃逸

public static StringBuffer createStringBuffer(String s1, String s2) {

StringBuffer sb = new StringBuffer();

sb.append(s1);

sb.append(s2);

return sb;

}

// 非方法逃逸

public static String createString(String s1, String s2) {

StringBuffer sb = new StringBuffer();

sb.append(s1);

sb.append(s2);

return sb.toString();

}

如果能够证明一个对象不会逃逸到方法外或者线程外,或者说逃逸程度比较低,则可以对这个对象采用

不同程度的优化:

栈上分配

完全不会逃逸的局部变量和不会逃逸出的线程对象,采用栈上分配,

对于发生逃逸的、不老实的对象,才使用 堆上分配。

栈上分配可以快速地在栈帧上创建和销毁对象,不用再将对象分配到堆空间,可以有效地减少 JVM 垃圾回收的压力。

标量替换

一个对象可能不需要作为一个连续的存储空间,也能被访问到,那么对象的部分可以不存储的在连续的内存,而是存可以打散存储,甚至部分存储或者打散在CPU寄存器中。

通过逃逸分析确定该对象不会被外部访问后,JVM判断对象是否可以被进一步分解,如果对象可以打散为 变量,则 JVM不会创建该对象,而是化整为零, 将该对象成员变量分解若干个被这个方法使用的成员变量,

JVM将一个大的对象打散成若干变量的过程,叫做标量替换,也称之为 分离对象

如下面的代码:

public static void main(String[] args) {

alloc();

}

private static void alloc() {

Point point = new Point(1,2);

System.out.println(\”point.x=\”+point.x+\”; point.y=\”+point.y);

}

class Point{

private int x;

private int y;

}

从以上代码可以看出,Point对象并没有逃逸出alloc方法,并且Point对象是可以拆解成标量的。

此时,JIT就会不会直接创建Point对象,而是直接使用两个标量int x,int y来替代Point对象

为啥要 化整为零 呢?

因为 栈空间是非常有限的,很多的场景下,一个线程的栈空间就是1M的大小。

标量替换之后的成员变量,可以选择在栈帧分配,也可以就近在寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。

开启标量替换参数-XX:+EliminateAllocations,JDK7之后默认开启。

消除同步锁

如果JVM通过逃逸分析,发现一个对象只能从一个线程被访问到,则访问这个对象时,可以不加同步锁。

具体来说:

如果同步块所使用的锁对象通过这种分析后,发现只能够被一个线程访问,根本用不着同步,那么,JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步锁。所以:如果程序中使用了synchronized内置锁锁,则JVM会将synchronized内置锁消除。

这种情况针对的是synchronized锁,而对于非内置锁,比如 Lock 显示锁、CAS乐观锁等等,则JVM并不能消除。

要开启同步消除,需要加加上两个JVM启动选项:

-XX:+EliminateLocks启动选项,表示启动同步锁消除。

-XX:+DoEscapeAnalysis 选项,表示启动逃逸分析。

因为同步锁消除依赖逃逸分析,所以同时要打开 -XX:+DoEscapeAnalysis 选项。

逃逸分析相关的参数:

-XX:+DoEscapeAnalysis 开启逃逸分析

-XX:+PrintEscapeAnalysis 开启逃逸分析后,可通过此参数查看分析结果。

-XX:+EliminateAllocations 开启标量替换

-XX:+EliminateLocks 开启同步消除

-XX:+PrintEliminateAllocations 开启标量替换后,查看标量替换情况。

5 逃逸分析的底层原理

在Java的编译体系中,一个Java的源代码文件变成计算机可执行的机器指令的过程中,需要经过两段编译:

第一段编译,指前端编译器把.java文件转换成 .class文件(字节码文件)。前端编译器产品可以是JDK的Javac、Eclipse JDT中的增量式编译器。

第二编译阶段,JVM 通过解释字节码将其翻译成对应的机器指令,逐条读入字节码,逐条解释翻译成机器码。

很显然,由于有一个解释的中间过程,其执行速度必然会比可执行的二进制字节码程序慢很多。这就是传统的JVM的解释器(Interpreter)的功能。

如何去掉中间商,提升效率?

为了解决这种效率问题,引入了JIT(即时编译器,Just In Time Compiler)技术。引入了 JIT 技术后,Java程序还是通过解释器进行解释执行,也就是说,主体还是解释执行,只是局部去掉中间环节。

怎么做局部去掉中间环节呢?

当JVM发现某个方法或代码块运行特别频繁的时候,就会认为这是“热点代码”(Hot Spot Code)。然后JIT会把部分“热点代码”翻译成本地机器相关的机器码,并进行优化,然后再把翻译后的机器码缓存起来,以备下次使用。

把翻译后的机器码缓存在哪里呢? 这个 缓存,叫做 Code Cache。 可见,JVM和WEB应用实现高并发的手段是类似的,还是使用了缓存架构。

当JVM下次遇到相同的热点代码时,跳过解释的中间环节,直接从 Code Cache加载机器码,直接执行,无需 再编译。

所以,JIT总的目标是发现热点代码, 热点代码变成了提升性能的关键,hotspot JVM的名字,也就是这么来的,把识别热点代码,写在名字上,作为毕生的追求。所以,JVM总的策略为:

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;

另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。

JIT(即时编译)的出现与 解释器的区别

(1)解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释。

(2)JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需

再编译。

(3)解释器是将字节码解释为针对所有平台都通用的机器码。

(4)JIT 会根据平台类型,生成平台特定的机器码。

JVM包含多个即时编译器,主要有C1和C2,还有个Graal (实验性的)。多个即时编译器, 都会对字节码进行优化并生成机器码C1会对字节码进行简单可靠的优化,包括方法内联、去虚拟化、冗余消除等,编译速度较快,可以通过-client强制指定C1编译C2会对字节码进行激进优化,包括分支频率预测、同步擦除等,

可以通过-server强制指定C2编译JVM 将执行状态分成了 5 个层次:

0 层,解释执行(Interpreter)

1 层,使用 C1 即时编译器编译执行(不带 profiling)

2 层,使用 C1 即时编译器编译执行(带基本的 profiling)

3 层,使用 C1 即时编译器编译执行(带完全的 profiling)

4 层,使用 C2 即时编译器编译执行

JVM不会直接启用C2,而是先通过C1编译收集程序的运行状态,再根据分析结果判断是否启用C2。

分层编译模式下, 虚拟机执行状态由简到繁、由快到慢分为5层在编译期间,JIT 除了对 热点代码做缓存提速,会对代码做很多优化。

其中有一部分优化的目的就是减少内存堆分配压力,其中JIT优化中一种重要的技术叫做逃逸分析。根据逃逸分析,即时编译器会在编译过程中对代码做如下优化:锁消除:当一个锁对象只被一个线程加锁时,即时编译器会把锁去掉

栈上分配:当一个对象没有逃逸时,会将对象直接分配在栈上,随着线程回收,由于JVM的大量代码都是堆分配,所以目前JVM不支持栈上分配,而是采用标量替换

标量替换:当一个对象没有逃逸时,会将当前对象打散成若干局部变量,并分配在虚拟机栈的局部变量表中

Java注解原理详解(4大原理步骤)

Java注解的底层实现原理主要基于Java反射机制,下面我就来详解Java注解原理@mikechen

本篇已收于mikechen原创超30万字《阿里架构师进阶专题合集》里面。

具体来说,Java注解原理实现可以分为以下4大步骤:

1.定义Java注解

Java注解的定义实际上就是一个接口,它可以包含多个成员变量和方法,其中的成员变量称为注解的元素(Element)。

这个注解包含了一个属性value,表示注解的值。

2.注解标记

使用Java注解时,需要在对应的类、方法、字段等上添加注解标记。

例如:

3.编译期间处理

Java编译器在编译源代码时,会扫描源代码中所有使用了注解的地方,并将注解处理成一个与注解元素相关的数据结构。

这个数据结构包含了:注解的所有元素以及其值,并保存在编译后的Java字节码文件中,这个过程称为注解的编译时处理。

在编译期间,Java编译器会将这些注解信息转换为Java字节码中的注解信息,并将这些信息存储在Class文件中。

4.反射调用

在运行时,可以通过Java反射机制读取和处理注解信息。

例如:

这里的MyClass是包含doSomething方法的类,通过反射机制获取doSomething方法的注解,然后获取注解的value属性值。

Java注解原理小结

Java注解原理基于Java反射机制,通过在编译期间将注解信息转换为Java字节码中的注解信息,并在运行时通过反射机制读取和处理注解信息,实现了对程序的控制。

以上

本篇已收于mikechen原创超30万字《阿里架构师进阶专题合集》里面。

什么是java?Java语言的诞生与演进,Java 特点!

Java呢是于1995年由Sun公司推出的一个极富创造力的面向对象程序设计语言。这门语言,不仅仅是一种技术,更是一场程序设计的革命。

作为一名热爱技术的程序员,一直对编程语言充满好奇。

它呢是由Java之父之称的Sun公司研究院院士詹姆士·戈士林博士亲手设计而成的。

当时呢戈士林的小组在致力于为未来的智能设备开发出一种新的编程语言。

但是,戈士林对C++的执行过程中的表现非常的不满,于是啊,他就把自己封闭在了办公室里编写了一种新的语言。

这种新的语言编写出来之后起名字又成了头疼的事。那正巧,戈士林办公室外正好有一棵橡树,而橡树的英文名字叫做Oak。好那就叫Oak吧。

这时候,这个Oak已经具备了安全性、网络通信、面向对象多线程等特性,它是一款相当优秀的程序语言。

可是后来,由于去注册这个Oak商标的时候,他发现这个名字已经被另外一家公司注册了。所以就不得不改名。但是要取什么名字呢,工程师边喝着咖啡边不停的讨论。

这时候,大家看了看手里的咖啡,在想到印度尼西亚有一个重要的盛产咖啡的岛屿叫做爪哇岛。好吧那干脆就叫Java吧。于是Java语言就诞生了。

Java的标志就是一杯飘着热气的咖啡,象征着它的活力和全球普及。

Java是一种通用解释方式的执行语言。它的语法和C++类似,但不同于C++糟糕的可移植性。

Java是一种跨平台的程序设计语言,用Java编写的程序可以运行在任何平台和设备上,比如Windows平台、个人电脑、Unix平台即服务器、iOS系统也就是苹果的电脑及其各种微处理器平台,真正地实现了一次编写,到处运行的这个特点。

Java语言编写的程序既是编译型的,又是解释型的。

Java程序经过编译之后,可以转换成一种叫做Java字节码的中间语言。而这个编译器就是我们常用的JDK。而这个生成的字节码再交给Java虚拟机,由Java虚拟机将字节码变成机器可以识别的机器码。而这个Java虚拟机就是我们常用的JRE。这个过程看起来比较复杂是吧,好,那我们就换一种方式来理解。

我们编程时使用的开发工具就相当于一支笔,我们可以拿它来写字。而我们写的代码相当于一个菜谱。

而Java虚拟机它就是一个什么菜都会做的大厨。而我们则是那一个写菜单的人。我们把写好的代码,也就是我们这个菜单交给了大厨,大厨就会按照这个菜单做出一些菜来,而这些菜就是我们最终写好的程序。

我们只是写了Java代码,而真正将代码变成可执行程序的是Java虚拟机。

好那我们再回头看一下,这个Java是如何运行的。我们这个源代码就是我们编写的菜单。我们把这个菜单交给了这个JDK,也就是交给大厨。

大厨将这个菜切好,JDK将程序源码变成字节码这个过程就相当于大厨将这个菜切好了。此时这个字节码,他只是美食的一个半成品。

这时,大厨要按照菜单继续做菜了,于是大厨开始烹饪这个美食,就是将字节码交给虚拟机了。

然后从虚拟机出来的机器码才是真正的美食,也就是我们真正运行的这个程序。而这一个过程,就是Java运行的过程。

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。