jvm里面的堆会分配指定的内存空间用来存储对象信息,但是当对象信息过多的时候,GC进行垃圾回收时,过多的对象需要进行回收,会导致效率的底下。
因此了解常见的jvm优化技巧也就很有必要了。
1.如何理解逃逸分析?
所谓的逃逸分析是指方法创建对象之后,除了在方法体内被引用到之外,还在别处也被引用到了。由于GC进行对象回收的时候需要判断该对象是否有被引用,因此当相应方法执行完毕后,由于方法类对象还被外部程序引用,就会导致相应对象无法被GC回收处理,也就造成了内存逃逸现象。
2.什么是栈上分配?
在以往的java程序运行时候,对象的内存空间都是通过堆来进行分配的。方法体内的局部变量都是通过栈来进行内存空间分配的,因此如果我们能够控制一个对象的活动范围只在一个局部方法里面的话,并且控制该对象的空间分配就存在于栈里面。这样随着栈的出栈操作,对象就会被销毁,减少了对于GC的效率影响。
3.同步消除是什么?
当通过逃逸分析之后,发下某个对象在没有被外部线程所引用的时候,他的读写竞争也就不存在了,这个时候就可以消除他的同步操作。
4.标量替换
标量是指java虚拟机里面的原始数据(那些不可再去细化分隔的基本类型,例如int,float这些)。当某个数据可以被继续进行细化拆分的时候,我们称之为聚合量(例如我们常说的对象)。如果jvm的逃逸分析分析出来了某个对象不会被外部所引用。那么当jvm执行相应函数的时候也不会直接创建相应的对象,而是通过将对象里面的各个属性拆开单独创建,单独维护。这样做的好处在于可以将原本需要连续空间存储的对象给拆分开来,减少连续内存空间的占用。
由于java的对象内存分配大多数情况下都是通过堆来进行分配,因此它的垃圾回收效率成为了java性能较慢的一个因素,这也是java语言被人吐槽的一个缺点。
5.逃逸分析总结
逃逸分析的原理理解起来很简单,但JVM在应用过程中,还是有诸多考虑。
比如,逃逸分析不能在静态编译时进行,必须在JIT里完成。原因是,与java的动态性有冲突。因为你可以在运行时,通过动态代理改变一个类的行为,此时,逃逸分析是无法得知类已经变化了。逃逸分析另一个重要的优化 - 同步消除。如果你定义的类的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。
接下来,我将会通过一段实操记录来演示逃逸分析过程中的堆栈分配情况。
jdk环境:1.8
操作系统 :win10
这段程序代码在编译器里面跑完之后,再去输入相应的jmap指令查看堆栈内存分配的时候可能报出异常,小编我谷歌了很多内容都没法解决,于是这次实验采用最原始的cmd命令来进行:
首先是java程序代码:
public class Test { static class Admin{ public String name; } public static void testEscape(){ Admin admin=new Admin(); } public static void main(String[] args) { System.out.println("start"); long begin=System.currentTimeMillis(); for(long i=0;i<1000000;i++){ testEscape(); } long end=System.currentTimeMillis(); System.out.println("time:"+(end-begin)); try { Thread.sleep(10000000); } catch (InterruptedException e1) { e1.printStackTrace(); } } } 复制代码
然后通过javac命令来将程序编译成class字节码,再通过java命令来执行程序。
这个时候程序中的线程处于睡眠状态,进入cmd窗口中,通过jps和jmap来查看相应的内存分配情况。
由于小编使用的是jdk8,因此当前环境中的逃逸分析已经开启了。所以这个时候会发现,堆里面的Admin对象一共创建了90613个。那么如果我们将逃逸分析关闭之后,实际的一个内存分配又将会是如何的了?
通过查阅相应的jvm指令之后,我们可以看到相应的关闭逃逸分析指令为:
-XX:-DoEscapeAnalysis
那么好,我们接着来继续之前的问题深究。
这一次,小编将逃逸分析关闭之后,再去通过jps,jmap命令查看内存分配信息。
这一次你会发现,堆里面创建的Admin对象为1000000个,和之前开启逃逸分析时创建的90613个相差甚远。
同样,通过是否开启逃逸分析会发现,前者的消耗时间比后者要短,也就是说开启了逃逸分析之后,会有部分对象在栈上边直接分配空间,这样GC回收的压力也就减少了,所以速率也会有所提升。
模仿着上面的操作,也可以试试标量替换指令
通过实验验证,相应的性能都会受到影响。而且这些指令生效的前提是开启了逃逸分析。
同样通过开启和关闭相应的锁指令也会影响相应的性能。
加锁之后的逃逸分析代码案例:
public class Test {
public static void lock(){ int[] numbers=new int[65]; synchronized (numbers) { numbers[0]=1; } } public static void main(String[] args) { long begin=System.currentTimeMillis(); for(int i=0;i<100000000;i++){ lock(); } long end=System.currentTimeMillis(); System.out.println(end-begin); } 复制代码
}
同样,通过相应的锁指令执行程序
java -XX:+DoEscapeAnalysis -XX:+EliminateLocks -XX:+PrintGC Test
java -XX:+DoEscapeAnalysis -XX:-EliminateLocks -XX:+PrintGC Test
常见的jvm逃逸分析的指令:
-XX:+EliminateLocks开启锁消除(jdk1.8默认开启)
-XX:-EliminateLocks 关闭锁消除
-XX:+EliminateAllocations开启标量替换(jdk1.8默认开启)
-XX:-EliminateAllocations 关闭标量替换
-XX:+DoEscapeAnalysis开启逃逸分析(jdk1.8默认开启)
-XX:-DoEscapeAnalysis 关闭逃逸分析
通过上述的这些实验,我们可以发现,开启了逃逸分析之后,java语言中的对象在创建的时候并非全都被分配到了堆里面。其实在编译期间,JIT会对程序代码做出很多的优化,主要的核心目的就在于减少对于内存堆分配压力,其中逃逸分析就是一种技术。JIT编译器通过逃逸分析的结果来判断是否要将该对象的内存分配到栈上。因此程序的运行效率也会有所提升。
但是逃逸分析这项技术早在上世纪就已经有论文提出了,却是到了jdk1.6版本之后才渐渐冒出水面,当然逃逸分析也是有它的弊端的。由于每次进行逃逸分析的时候都是需要进行消耗一定的性能,如果相应程序里面所有的对象都是出于逃逸状态,那么逃逸分析也就变得失去了意义,反而还降低了性能。