文章目录
- 什么是存逃JIT?
- JIT内存逃逸分析
- 1. 内存逃逸分类
- 2. 逃逸分析的作用
- 3. 同步锁消除
- 4. 标量替换
- 5. 栈上分配
- 1. Java对象的分配流程
- 2. 栈上分配思路
- 3. 开启栈上分配
- 4. 分析内存
- 6. 查看JVM所有配置的参数值
什么是JIT?
JIT Compiler(Just-in-timeCompiler) 即时编译。
最早的逸分Java建置方案是由一套转译程式(interpreter),将每个Java指令都转译成对等的存逃微处理器指令,并根据转译后的逸分指令先后次序依序执行,由于一个Java指令可能被转译成十几或数十几个对等的存逃微处理器指令,这种模式执行的逸分速度相当缓慢。
详细解析见百度百科 JIT编译器
JIT内存逃逸分析
Escape Analysis 存逃内存逃逸分析是一种代码分析手段,通过动态分析创建对象的逸分使用范围。
1. 内存逃逸分类
如果一个方法创建的存逃对象,除了方法内使用到之外,逸分还被方法外使用到,存逃那么在方法执行结束后,逸分由于该对象依然被引用到,存逃那么GC就可能无法立即回收,逸分此时该对象就出现了逃逸。存逃
内存逃逸分为方法逃逸和线程逃逸
- 方法逃逸
当一个对象被定义后,以参数的形式传递给其它方法 - 线程逃逸
类变量或者示例变量或者方法返回的对象,可能被其它线程引用或者访问到
public class EscapeAnalysis { public static Object obj1; public Object obj2; public void globalVariableEscape() { obj1 = new Object(); // 静态变量,外部线程可见,会发生逃逸 } public void instanceObjectEscape() { obj2 = new Object(); // 赋值给堆中实例字段,外部线程可见,会发生逃逸 } public Object returnObjectEscape() { return new Object(); // 返回实例,外部线程可见,会发生逃逸 } public void noEscape() { Object noEscape = new Object(); // 仅创建线程可见,对象无逃逸 }}
2. 逃逸分析的作用
筛选出没有发生逃逸的对象,为其优化手段,例如栈上分配,标量替换,同步消除等提供依据。
只有server模式下才能启用逃逸分析
JVM相关参数
- 开启逃逸分析:-XX:+DoEscapeAnalysis
- 关闭逃逸分析:-XX:-DoEscapeAnalysis
- 显示逃逸分析:-XX:+PrintEscapeAnalysis
3. 同步锁消除
同步锁时非常消耗性能的,所以编译器确定一个对象没有发生逃逸时,它会移除该对象的同步锁。
JDK1.8 默认开启了同步锁,但是建立在开启逃逸分析的基础上。
-XX:+EliminateLocks #开启同步锁消除(JVM默认状态)-XX:-EliminateLocks #关闭同步锁消除
我们用一个例子来演示
@Test public void testLock(){ long t1 = System.currentTimeMillis(); for (int i = 0; i < 100_000_000; i++) { locketMethod(); } long t2 = System.currentTimeMillis(); System.out.println("耗时:"+(t2-t1)); } public static void locketMethod(){ EscapeAnalysis escapeAnalysis = new EscapeAnalysis(); synchronized(escapeAnalysis) { escapeAnalysis.obj2="abcdefg"; } }
上面这个EscapeAnalysis没有发生逃逸,JVM默认开启了同步锁消除。我们做几组试验对比
(1) 手动注释掉synchronized锁,直接运行,未设置JVM参数
(2) 保留synchronized锁,直接运行,未设置JVM参数
(3) 设置JVM参数,关闭锁消除,再次运行
java -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:-EliminateLocks
(4) 设置JVM参数,开启锁消除,再次运行
java -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+EliminateLocks
通过上述4组参照试验对比,可以得出,JDK1.8默认开启了锁消除,如果关闭锁消除,那么锁将非常消耗性能。
4. 标量替换
标量:不能被进一步分解的变量,如基础数据类型和对象的引用。
聚合量:能够被分解的变量,比如对象。
如果一个对象没有发生逃逸,可以将成员变量拆分成标量,就可以不用创建它,在栈或者寄存器中直接创建成员标量,这就叫标量替换。节省内存,提升了应用程序的性能。
JDK1.8默认开启了标量替换,也同样建立在逃逸分析的基础上
JVM相关参数
- 开启标量替换:-XX:+EliminateAllocations #JVM默认状态
- 关闭标量替换:-XX:-EliminateAllocations
- 显示逃逸分析:-XX:+PrintEliminateAllocations
@Test public void testScalarReplace(){ long t1 = System.currentTimeMillis(); for (int i = 0; i < 100_000_000; i++) { scalarReplace(); } long t2 = System.currentTimeMillis(); System.out.println("耗时:"+(t2-t1)); } public static void scalarReplace(){ User user=new User(); user.setId(1); user.setName("hello"); }
上面这个User没有发生逃逸,JVM默认开启了标量替换。我们做几组试验对比。
(1) 直接运行,未设置JVM参数
(2) 设置JVM参数,关闭标量替换
java -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:-EliminateAllocations
(3) 设置JVM参数,开启标量替换
java -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+EliminateAllocations
通过上述3组参照试验对比,可以得出,JDK1.8默认开启了标量替换,如果关闭标量替换,那么直接进行对象分配将非常影响性能。
5. 栈上分配
将原本应当分配在堆上的对象分配到栈内存上,这样可以减少堆内存的使用,从而减少GC的频率。
1. Java对象的分配流程
首先,我们需要先了解决Java对象是如何分配内存的。
- 如果开启栈上分配,JVM会先进行栈上分配
- 如果没有开启栈上分配或不符合条件,则会进入TLAB分配
- 如果TLAB分配不成功或者不符合,则判断是否可以进入年老代分配
- 如果不能进入年老代,则进入eden分配
并不是所有对象都分配在堆上,除了堆意外,还可以将对象分配到栈和TLAB中。(大多数的对象都分配在堆中)
2. 栈上分配思路
栈上分配是JVM提供的一项优化技术。
其思路是:
- 对于线程私有的对象(不能被其它线程访问的对象),可以将它们分配到栈内存上,而不是堆内存中,也就是将聚变量进行标量替换的方案。
- 分配到栈上的优势是可以在方法结束后自动销毁,不需要GC介入,提供系统性能
- 对于大量零散的对象,栈上分配提供了一种很好的对象分配策略,栈上分配速度块,可以有效避免GC回收带来的负面影响。
- 问题:由于栈内存比较小,因而大对象不能也不适合进行栈上分配。
3. 开启栈上分配
栈上分配是基于逃逸分析和标量替换的,所以必须开启逃逸分析和标量替换,当然JDK1.8是默认都是开启的。
逃逸分析和标量替换前面已经介绍了。
4. 分析内存
栈上分配时基于逃逸分析+标量替换,因此我们只要关闭逃逸分析或者标量替换任一一项,即可关闭栈上分配。
我们继续使用前面标量替换的demo。
- 关闭栈上分配,运行测试用例
java -Xmx15m -Xms15m -XX:+PrintFlagsFinal -XX:+PrintGCDetails -XX:-UseTLAB -XX:-DoEscapeAnalysis #关闭栈上分配
- 开启栈上分配,运行测试用例
java -Xmx15m -Xms15m -XX:+PrintFlagsFinal -XX:+PrintGCDetails -XX:-UseTLAB -XX:+DoEscapeAnalysis #开启栈上分配
通过对比,我们可以看到
- 开启栈上分配,将未逃逸的对象分配在栈内存中,明显运行效率更高。
- 关闭栈上分配后,GC频繁进行垃圾回收。
6. 查看JVM所有配置的参数值
java -XX:+PrintFlagsFinal #输出打印所有参数jvm参数