说说JVM中对象分配流程

  最近又把JVM相关的知识复习了一遍,发现有不少之前记得不太清的知识点。今天就顺手总结一下JVM中对象大致的分配流程,加深一下记忆。对象分配的大致流程如下:如果JVM开启了栈上分配和标量替换,且经过JIT逃逸分析判定该对象的引用不会逃逸到线程外,则该对象为栈分配候选;如果不满足栈上分配的条件,则尝试TLAB分配;如果TLAB分配不成功,则尝试堆上分配,如果满足进入老年代的条件,则对象直接分配到老年代,否则对象分配在新生代的eden区域。
对象分配流程

  看上去流程还是挺简单的,但是涉及的知识还是相对复杂的。下面我将对上图提到的各个流程做一个简要的介绍,方便大家理解。

1.逃逸分析

  逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到栈上。实际上,这是JIT对于对象的作用域的一个分析判断:如果一个对象在方法中定义后,不会被外部的方法引用到或是外部线程访问到,就可以认为这个这个对象不会引用逃逸。经过逃逸分析后,可以得到三种对象的逃逸状态:
1.GlobalEscape(全局逃逸), 即一个对象的引用逃出了方法或者线程。例如,一个对象的引用是复制给了一个类变量,或者存储在在一个已经逃逸的对象当中,或者这个对象的引用作为方法的返回值返回给了调用方法。
2.ArgEscape(参数级逃逸),即在方法调用过程当中传递对象的应用给一个方法。
3.NoEscape(没有逃逸),一个可以进行标量替换的对象。可以不将这种对象分配在传统的堆上。


未引用逃逸
引用逃逸

在经过逃逸分析后,如果发现一个对象不会逃逸到方法或线程以外,那么JIT会进行一些优化。实际上,逃逸分析不是优化代码的手段,而是为其他优化手段提供依据的分析技术。那么,JIT会进行哪些优化呢?

1.1 栈上分配

  在刚刚接触JVM时,我认为对象就是在堆中分配的,这可以说是一个定律了。但是栈上分配打破了这一定律:如果经过逃逸分析确认一个对象不会逃逸出方法被其他线程访问,那么这个对象就可以作为栈分配候选(注意,是候选,不代表一定会分配到栈上)。如此一来,该对象会随着栈帧的出栈而一起被销毁,从而不需要垃圾回收器的介入,减轻GC带来的压力,提升系统性能。

1.2 标量替换

  标量是指一个数据已经无法分解成更小的数据来表示了,JVM中的原始数据类型,例如int、long等都不能再进一步分解,它们就可以被称为标量。相对的一个数据可以继续分解,那么就可以将其称为聚合量,Java中的对象就是典型的聚合量。

标量替换

如上图所示,Point就是一个聚合量,Point中的属性x,y就是标量。如果经过逃逸分析发现一个对象不会被外部访问到,并且这个对象可以被拆分为标量的话,那么这个对象可能不会被直接创建,而是创建它的若干成员变量。如此将对象拆分,就可以让一个变量分配在栈上了。

1.3 同步消除

  线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,而被其他线程访问到的话,那么这个变量的读写就不会有竞争,也不需要加锁同步,因此对于这个对象的同步措施就可以消除掉。

2.栈上分配

  实际上刚刚已经介绍了栈上分配的概念,这里再啰嗦几句:如果我们想开启栈上分配,我们需要同时启用逃逸分析(-XX:+DoEscapeAnalysis)和标量替换(-XX:+EliminateAllocations,默认开启)。如果关闭逃逸分析或标量替换中任意一个,栈上分配都不会生效。

3.TLAB分配

  TLAB,全称Thread Local Allocation Buffer,顾名思义,是线程本地分配缓存的意思。虽然名字里带一个Thread,但是人家是在堆中的,默认占eden区域大小的1%,分配时线程私有,使用时线程共享。那么,为什么要有TLAB这个东西呢?在并发环境中,对象的内存分配也是会存在线程安全问题:在同一时间,可能会有多个线程在堆上申请空间,如果不采取措施,就有可能出现给两个对象分配了同一块内存的尴尬情况。如果我们采取同步处理,比如通过CAS,在分配失败时进行重试,虽然可行,但是会降低分配的效率。因此,HotSpot虚拟机采用了TLAB这种线程专属区域来避免多线程冲突,提高对象分配的效率。
  TLAB分配可以通过参数-XX:+UseTLAB开启(默认开启);TLAB区域的大小可以通过-XX:TLABSize来手动指定;对于TLAB的使用情况,我们可以通过开启参数-XX:PrintTLAB(JDK8及以下版本有效)来进行观察。

4.大对象进入老年代

  如果一个对象很大,eden和surivor都无法容纳该对象,那么这对象自然无法生成在新生代,而是会直接晋升到老年代。-XX:PretenureSizeThreshold参数可以设置对象直接晋升到老年代的阈值。如果对象大小超过该阈值,则直接在老年代分配。另外,该参数只对Serial和PerNew两款垃圾回收器有效。