关注公众号【程序员学长】,免费获取计算机经典资料
在 Java 虚拟机中,对象是在 Java 堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可以被优化成栈上分配。这样就无需在堆上分配内存,也就无需进行垃圾回收了。这也是最常见的堆外存储技术。
如何将堆上的对象分配到栈,需要使用逃逸分析手段。这是一种可以有效减少 JAVA 程序中同步负载和堆内存分配压力的跨函数全局数据流分析方法。通过逃逸分析,Java Hotspot 编译器能够分析出一个新的对象的引用的使用范围从而决定是否将这个对象分配到堆上。逃逸分析的基本行为就是分析对象的动态作用域。
-
当一个对象在方法中被定义后,只在方法内部使用,则认为没有发生逃逸。
-
当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他方法中。
逃逸分析案例
没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除。
public class EscapeAnalysis{
public EscapeAnalysis escapeAnalysis;
//方法返回 EscapeAnalysis 对象,发生逃逸
public EscapeAnalysis getInstance(){
return escapeAnalysis==null? new EscapeAnalysis():escapeAnalysis;
}
// 为成员属性赋值,发生逃逸
public void setEsc(){
this.escapeAnalysis=new EscapeAnalysis();
}
// 对象的作用域在当前方法中,没有发生逃逸
public void useEsc(){
EscapeAnalysis esc=new EscapeAnalysis();
}
}
在 JDK1.7 版本之后,HotSpot 中默认就已经开启了逃逸分析。
结论
开发中能使用局部变量的,就不要在方法外定义。使用逃逸分析,编译器可以对代码做如下优化。
-
栈上分配:将堆分配转化为栈分配。如果一个对象在子程序中被创建,要使指向该对象的指针永远不要发生逃逸,则对象可能是栈上分配的候选,而不是在堆上分配。
-
同步省略:如果一个对象被发现只有一个线程被访问到,那么对于这个对象的 *** 作可以不考虑同步。
-
分离对象或标量替换:有的对象可能不需要一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在 CPU 寄存器中。
栈上分配
JIT 编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无需进行垃圾回收了。
下面我们来通过一个案例来看一下开启逃逸分析和未开启逃逸分析的情况。
public class StackAllocation {
public static void main(String[] args) {
long start=System.currentTimeMillis();
for(int i=0;i<100000000;i++){
alloc();
}
long end=System.currentTimeMillis();
System.out.println("分配对象花费的时间:"+(end-start)+"ms");
}
private static void alloc(){
//成员变量,未发生逃逸
User user=new User();
}
}
设置JVM参数 -XX:-DoEscapeAnalysis,表示未开启逃逸分析。
-Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
运行结果如下所示:
[GC (Allocation Failure) [PSYoungGen: 262144K->1487K(305664K)] 262144K->1495K(1005056K), 0.0030268 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 263631K->1047K(305664K)] 263639K->1063K(1005056K), 0.0017172 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 263191K->951K(305664K)] 263207K->967K(1005056K), 0.0006980 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 263095K->1031K(305664K)] 263111K->1047K(1005056K), 0.0013870 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 263175K->1015K(305664K)] 263191K->1031K(1005056K), 0.0007119 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 263159K->983K(347648K)] 263175K->999K(1047040K), 0.0012322 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 347095K->0K(346624K)] 347111K->967K(1046016K), 0.0017014 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 346112K->0K(347136K)] 347079K->967K(1046528K), 0.0003506 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
分配对象花费的时间:516ms
Heap
PSYoungGen total 347136K, used 110293K [0x00000007aab00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 345088K, 31% used [0x00000007aab00000,0x00000007b16b5688,0x00000007bfc00000)
from space 2048K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007c0000000)
to space 2048K, 0% used [0x00000007bfc00000,0x00000007bfc00000,0x00000007bfe00000)
ParOldGen total 699392K, used 967K [0x0000000780000000, 0x00000007aab00000, 0x00000007aab00000)
object space 699392K, 0% used [0x0000000780000000,0x00000007800f1f30,0x00000007aab00000)
Metaspace used 3415K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
运行时间是 516ms ,并且运行过程中还触发了 GC *** 作。
下面我们来开启逃逸分析,看一下效果。
设置JVM参数为:-Xmx1G -Xms1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
运行结果如下所示:
分配对象花费的时间:16ms
Heap
PSYoungGen total 305664K, used 26234K [0x00000007aab00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 262144K, 10% used [0x00000007aab00000,0x00000007ac49eb60,0x00000007bab00000)
from space 43520K, 0% used [0x00000007bd580000,0x00000007bd580000,0x00000007c0000000)
to space 43520K, 0% used [0x00000007bab00000,0x00000007bab00000,0x00000007bd580000)
ParOldGen total 699392K, used 0K [0x0000000780000000, 0x00000007aab00000, 0x00000007aab00000)
object space 699392K, 0% used [0x0000000780000000,0x0000000780000000,0x00000007aab00000)
Metaspace used 3425K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
运行时间为 16 ms。
同步省略
线程同步的代价是相当高的,同步的后果是降低并发性和性能。
在动态编译同步块的时候,JIT 编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问,如果是,那么JIT编译器在编译这个同步块的时候就会取消这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫做锁消除。
public void m1(){
User user=new User("张三","男");
synchronized (user){
System.out.println(user.toString());
}
}
上述代码对 user 这个对象进行加锁,但是 user 对象的生命周期只在 m1 方法中,并不被其它线程所访问到,所以 JIT 编译器在编译阶段就会对其进行优化处理,优化结果如下所示,将同步锁进行消除。
public void m1(){
User user=new User("张三","男");
System.out.println(user.toString());
}
分离对象和标量替换
标量是指一个无法再分解成更小数据的数据,Java中的原始数据类型就是标量。
相对的,那些还可以进行分解的数据叫做聚合量,Java中的对象就是聚合量,因为它可以分解成其它聚合量和标量。
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过 JIT 优化,就会把这个对象拆解成若干成员变量来替代。这个过程就叫做变量替换。
如下所示:
public void m1() {
Point p=new Point(10,20);
System.out.println("x="+p.x+",y="+p.y);
}
上述代码经过标量替换后,就会变成:
public void m1() {
int x=10;
int y=20;
System.out.println("x="+x+",y="+y);
}
可以发现,Point 这个聚合量经过逃逸分析后,发现它并没有逃逸,所以就被替换成了两个标量。那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不需要再分配堆内存了。标量替换为栈上分配提供了很好的基础。
JVM 参数 -XX:+EliminateAllocations 表示开启了标量替换(默认打开),允许将对象打散分配在栈上。
我们来看下面这段代码。
public class ObjectTest {
public static void main(String[] args) {
long start=System.currentTimeMillis();
for(int i=0;i<100000000;i++){
Point p = new Point(10,20);
}
long end=System.currentTimeMillis();
System.out.println("use time:"+(end-start)+"ms");
}
}
然后通过JVM参数配置,关闭标量替换。
-Xmx512m -Xms512m -XX:+PrintGCDetails -XX:-EliminateAllocations
输出结果如下所示,发现触发了GC,并且 use time:409ms
[GC (Allocation Failure) [PSYoungGen: 170496K->0K(172544K)] 171459K->963K(522240K), 0.0006857 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 170496K->0K(172544K)] 171459K->963K(522240K), 0.0003535 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 170496K->0K(172544K)] 171459K->963K(522240K), 0.0003353 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 170496K->0K(172544K)] 171459K->963K(522240K), 0.0003497 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
use time:409ms
Heap
PSYoungGen total 172544K, used 37506K [0x00000007b5580000, 0x00000007c0000000, 0x00000007c0000000)
eden space 170496K, 21% used [0x00000007b5580000,0x00000007b7a20998,0x00000007bfc00000)
from space 2048K, 0% used [0x00000007bfc00000,0x00000007bfc00000,0x00000007bfe00000)
to space 2048K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007c0000000)
ParOldGen total 349696K, used 963K [0x00000007a0000000, 0x00000007b5580000, 0x00000007b5580000)
object space 349696K, 0% used [0x00000007a0000000,0x00000007a00f0f40,0x00000007b5580000)
Metaspace used 3424K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
下面我们通过如下JVM参数,打开标量替换。
-Xmx512m -Xms512m -XX:+PrintGCDetails -XX:+EliminateAllocations
输出结果如下,use time:12ms
use time:12ms
Heap
PSYoungGen total 153088K, used 21081K [0x00000007b5580000, 0x00000007c0000000, 0x00000007c0000000)
eden space 131584K, 16% used [0x00000007b5580000,0x00000007b6a164d0,0x00000007bd600000)
from space 21504K, 0% used [0x00000007beb00000,0x00000007beb00000,0x00000007c0000000)
to space 21504K, 0% used [0x00000007bd600000,0x00000007bd600000,0x00000007beb00000)
ParOldGen total 349696K, used 0K [0x00000007a0000000, 0x00000007b5580000, 0x00000007b5580000)
object space 349696K, 0% used [0x00000007a0000000,0x00000007a0000000,0x00000007b5580000)
Metaspace used 3412K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
最后
这期文章就分享到这里,如果觉得不错,转发、在看、点赞安排起来吧。
你知道的越多,你的思维越开阔。我们下期再见。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)