栈上分配
栈上分配(逃逸分析)
分析
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可以能被外部方法所引用,例如作为调用参数传递到其它地方种,称为方法逃逸。
甚至还有可能被外部线程访问到,譬如赋值给类变量或者可以在其它线程中访问的实例变量,称为线程逃逸。
逃逸分析
在计算机语言编译器优化原理中,<u>逃逸分析是指分析指针动态范围的方法</u>,它同编译器优化原理的指针分析和外形分析相关联。当变量/对象在方法中分配后,其指针有可能被返回或者被全局引用,这样会被其它过程或者线程所引用,这种现象称为指针/引用的逃逸(Escape)。
通俗的说,如果一个对象的指针被多个方法或者线程引用时,那么我们成这个对象的指针发生了逃逸。
逃逸的三种状态
- 全局逃逸:一个对象的引用掏出了方法或者线程。例如:一个对象的引用赋值给一个类变量,或者这个对象的用引用作为方法的返回这返回给了调用方法。
- 参数级逃逸:方法调用过程中,传递对象给另一个方法
- 没有逃逸:一个可以进行标量替换的对象,可以将这个对象不分配在传统的堆上
逃逸分析作用
通过逃逸分析,java Hotspot 编译器能够分析出一个新的对象的引用的使用范围,从而觉得是否需要将这个对象分配到堆上。
逃逸分析后的优化
- 栈上分配:一个方法中的对象,若该对象没有发生逃逸,则可以将这个对象分配在栈上
- 消除同步:线程同步的代价是相当高的,同步带来的后果是降低了并发性和程序性能。逃逸分析以判断某个对象是否始终只被一个线程访问,如果只被一个线程访问,那么该对象的同步操作就可以转化为没有同步的操作,这样可以大大提高并发性能
- 标量替换:java虚拟机中的原始数据类型(int,long等)都不能在进一步分解,他们就可以成为标量。相对的,如果一个数据可以继续分解,那么他成为聚合量,java中最典型的聚合量就是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个这个对象是可以分解的,那么程序真正执行的时候可能不创建这个对象,而改为直接创建它的若干个被这个方法能够使用到的成员变量来代替。拆散后的变量便可以被单独的分析与优化,可以分别分配在栈帧或者寄存器上,原来的对象就不需要整体被分配在堆中。
一般在java程序中,new的对象是分配在堆空间中的,但是实际的情况是,大部分的new对象会进入堆空间中,而并非是全部的对象,还有另外两个地方可以存储new的对象,我们称之为栈上分配以及TLAB(其实也是在堆上)
栈上分配:针对那些作用域不会逃逸出方法的对象,在分配内存时不在将对象分配在堆内存中,而是将对象属性打散后分配在栈(线程私有的,属于栈内存)上,这样,随着方法的调用结束,栈空间的回收就会随着将栈上分配的打散后的对象回收掉,不再给gc增加额外的无用负担,从而提升应用程序整体的性能。
小对象(一般几十个byte),在没有逃逸的情况下,可以直接分配在栈上
直接分配在栈上,可以自动回收,减轻GC压力
大对象或者逃逸对象无法在栈上分配
栈上分配需要有一定的前提
开启逃逸分析 (-XX:+DoEscapeAnalysis)
逃逸分析的作用就是分析对象的作用域是否会逃逸出方法之外,在server虚拟机模式下才可以开启(jdk1.6默认开启)
开启标量替换 (-XX:+EliminateAllocations)
标量替换的作用是允许将对象根据属性打散后分配在栈上,比如若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配。默认该配置为开启
对象分配在堆上,而堆是一个全局共享的区域,当多个线程同一时刻操作堆内存分配对象空间时,就需要进行同步,而同步带来的效果就是对象分配效率变差(尽管JVM采用了CAS的形式处理分配失败的情况),但是对于存在竞争激烈的分配场合仍然会导致效率变差。
TLAB:Thread Local Allocation Buffer 即线程本地分配缓存
JVM默认开启了TLAB功能,也可以使用-XX: +UseTLAB 显示开启
JVM提供了-XX:+PrintTLAB 参数打开跟踪TLAB的使用情况
-XX:TLABSize 通过该参数指定分配给每一个线程的TLAB空间的大小
需要TLAB的原因就是提高对象在堆上的分配效率而采用的一种手段,就是给每个线程分配一小块私有的堆空间,即TLAB是一块线程私有的堆空间(实际上是Eden区中划出的)
如果开启栈上分配,JVM会先进行栈上分配,如果没有开启栈上分配或则不符合条件的则会进行TLAB分配,如果TLAB分配不成功,再尝试在eden区分配,如果对象满足了直接进入老年代的条件,那就直接分配在老年代。如下图
对象内存分配的两种方法
为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
指针碰撞(Serial、ParNew等带Compact过程的收集器)
假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。
空闲列表(CMS这种基于Mark-Sweep算法的收集器)
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。
对象在内存的引用方式