查看原文
其他

终于,有大佬通过JS代码讲清楚了Java中的逃逸分析...

点击蓝色“Java面试那些事儿”关注我哟
加个“星标”,优质文章,第一时间送达

来源:http://1t.click/anZy


说到域,假如给小学生中学生或者没有学过编程语言的人理解,更好说明其本质,就是作用范围,而这也就是上下文的意思,无须去理解的那么抽象。接下来就从函数讲起

这里拿js的闭包来说,不解释那么多,贴几段代码的,变量的作用域无非就是两种:
全局变量和局部变量。

Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。

Js代码
 var n=999;  function f1(){    alert(n);  }  f1(); // 999


另一方面,在函数外部自然无法读取函数内的局部变量。

Js代码


function f1(){    var n=999;  }  alert(n); // error


这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

Js代码
function f1(){    n=999;  }  f1();  alert(n); // 999


如何从外部读取局部变量?


出于种种原因,我们有时候需要得到函数内的局部变量。但是,前面已经说过了,正常情况下,这是办不到的,只有通过变通方法才能实现。


那就是在函数的内部,再定义一个函数。


Js代码


function f1(){    n=999;    function f2(){      alert(n); // 999    }  }


在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1 就是不可见的。这就是Javascript语言特有的“链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!


Js代码
function f1(){    n=999;    function f2(){      alert(n);    }    return f2;  }
  var result=f1();  result(); // 999


上面例子中的f2函数,就是闭包,其实就是两个容器,大的套个小的,有点类似套娃,再往大自然延伸一点:

f1函数就是河,而f2函数就是河里的鱼,鱼的新陈代谢的产物就是其返回的结果,同样是不是类似于我们和空气的关系,吸进去的是氧气,返回的结果的是二氧化碳


同样可以联系下Java中的类的函数,是不是也是一个道理,这就是函数的上下文

# 由域联系到的逃逸分析

接着由这个可以联想到代码中的逃逸分析,其实本就不是什么高深的东西,和现实联想下一个本应被销毁的东西没有被处理掉,会造成多大的浪费,比如地雷,战时有很大作用,和平时期,假如在居民区范围,那就有很大的问题;又比如犯人,一个越了域(用这个字觉得更形象)的恐怖分子,会造成多大的恐慌和社会问题,所以说逃逸分析,分析的就是作用域。

先拿Java来说,逃逸常见分三种情况:
  • 给全局变量赋值,发生逃逸;

  • 方法的返回值,会发生逃逸

  • 实例的引用传递,也会发生逃逸


这里又引出了一个优化的问题,Java对象总是在堆中分配的,因此,Java对象的创建和回收对系统的开销也是很大的,而Java被诟病的一个地方认为其性能慢的一个原因就是Java不支持运行时栈分配对象,没有像c++里面的struct结构或者是c#里的值对象。jdk6中的swing呢刚才和性能消耗的瓶颈就是由于发生逃逸造成的,栈内置保存了对象的指针,当对象不再被引用后,需要依靠GC来遍历引用树并回收内存,如果对象的数量比较多,就会给GC带来比较大的压力,也就间接影响了应用的性能,所以,减少临时对象在堆内分配的数量,无疑是最有效的优化方法。
再次回到域这个话题,在Java应用里我们很容易想到,一般在方法体内,我们声明了一个局部变量,且该变量的方法执行生命周期内未发生逃逸,因为在方法体内未将引用暴露给外面,按照JVM内存分配机制,首先会在堆里new出变量类的实例,然后将返回的对象指针压入调用栈,再继续执行,这是jvm未优化之前的走方式。

通过逃逸分析,我们可以对jvm的这个过程进行优化,即对栈的重新分配,首先需要分析并找到这期间为发生逃逸的变量,将变量类的实例化直接在栈里发生,即不需要经过堆,分配完成后,继续在栈内执行,最后线程结束,栈空间被回收,同样,局部变量也会跟着栈消失,优化后和优化前的区别就是减少了临时对象在堆内的分配数量(即未逃逸的对象无须堆内创建)。

逃逸分析不能在静态编译时进行,必须在JIT里完成,因为我们在正常的代码过程中运行时会通过动态代理改变一个类的行为,这时就无法得志类已经变化。

举个例子,比如一个方法的返回值是void,方法内部有对象的创建,如:
public void use_a() { A a=new A(); //a.xx(); ... a=null   }


从上面代码可以看出,a这个局部对象,没有返回,没有赋值全局变量等操作,所以,其是没有发生逃逸的,由于整个生命周期都在一个方法体内,这样的对象就可以在运行时栈进行分配和销毁。

JIT在编译时,假如能分析出这种代码,那么非逃逸的对象的创建和回收就可以在栈上进行,从而大大提高Java的运行性能。

往往我们会碰到这个情况,就是方法内调用另一个方法,有点类似js的闭包了吧,这时就要进行内联分析,因为往往一些对象在被调用过程中创建并返回给调用,比如上面的a.xx(),假如有返回值赋值给另外的局部变量的过程(这下应该好理解这个概念了吧),在调用过程中使用完就被销毁回收了,其实都清楚程序的执行过程是自上而下的,其实函数的调用也无非是把一段代码嵌入到另一段代码里顺序执行处理而已,说白了两个方法内联成一个方法体,而这个也是我们平常方法重构的一个过程,这种原来通过返回传递的对象就变成了方法内的局部对象,也就变成了非逃逸对象了,这样,这些对象就可以在同一个栈上进行分配了。

Java7已经开始支持对象的栈分配和逃逸分析,这样除了上述的优化外还会带来 同步消除和矢量替代,关于这两个可以查阅相应资料

这里对方法逃逸再贴两段代码,方便大家加深认识:


public static StringBuffer craeteStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb;   }


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

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


上述代码如果想要StringBuffer sb不逃出方法,可以这样写:

public static String createStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString();    }


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

同样,我们分析下js中闭包在此的行为:

最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

Js代码

function f1(){ var n=999; nAdd=function(){n+=1} function f2(){   alert(n); } return f2;}var result=f1();result(); // 999nAdd();result(); // 1000
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。


为什么会这样呢?原因就在于f1是f2的父函数,==而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。


这段代码中另一个值得注意的地方,就是“nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。

其次,nAdd的值是一个匿名函数(anonymousfunction),而这个
匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

# 使用闭包的注意点


由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。


热文推荐

我去!你竟然不知道Spring MVC中有现成的WebSocket组件...

大新闻,Unix 之父的密码终于被破解了!

咱们团队的SQL水平竟如此糟糕,一条SQL执行时间高达5分钟...

同时,分享一份Java面试资料给大家,覆盖了算法题目、常见面试题、JVM、锁、高并发、反射、Spring原理、微服务、Zookeeper、数据库、数据结构等等。


获取方式:点在看,关注公众号并回复 面试 领取。

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存