我们在学习使用Java的过程中,一般认为new出来的对象都是被分配在堆上,但是这个结论不是那么的绝对,通过对Java对象分配的过程分析,可以知道有两个地方会导致Java中new出来的对象并一定分别在所认为的堆上。这两个点分别是Java中的逃逸分析和TLAB(Thread Local Allocation Buffer)。本文首先对这两者进行介绍,而后对Java对象分配过程进行介绍。

1. 逃逸分析

1.1 逃逸分析的定义

逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

在计算机语言编译器优化原理中,逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。

Java在Java SE 6u23以及以后的版本中支持并默认开启了逃逸分析的选项。Java的 HotSpot JIT编译器,能够在方法重载或者动态加载代码的时候对代码进行逃逸分析,同时Java对象在堆上分配和内置线程的特点使得逃逸分析成Java的重要功能。

1.2 逃逸分析的方法

Java Hotspot编译器使用的是

[plain]  view plain copy
  1. Choi J D, Gupta M, Serrano M, et al. Escape analysis for Java[J]. Acm Sigplan Notices, 1999, 34(10): 1-19.

Jong-Deok Choi, Manish Gupta, Mauricio Seffano,Vugranam C. Sreedhar, Sam Midkiff等在论文《Escape Analysis for Java》中描述的算法进行逃逸分析的。该算法引入了连通图,用连通图来构建对象和对象引用之间的可达性关系,并在次基础上,提出一种组合数据流分析法。由于算法是上下文相关和流敏感的,并且模拟了对象任意层次的嵌套关系,所以分析精度较高,只是运行时间和内存消耗相对较大。

绝大多数逃逸分析的实现都基于一个所谓“封闭世界(closed world)”的前提:所有可能被执行的,方法在做逃逸分析前都已经得知,并且,程序的实际运行不会改变它们之间的调用关系 。但当真实的 Java 程序运行时,这样的假设并不成立。Java 程序拥有的许多特性,例如动态类加载、调用本地函数以及反射程序调用等等,都将打破所谓“封闭世界”的约定。

不管是在“封闭世界”还是在“开放世界”,逃逸分析,作为一种算法而非编程语言的存在,吸引了国内外大量的学者对其进行研究。在这里本文就不进行学术上了论述了,有需要的可以参见谷歌学术搜索:http://www.gfsoso.com/scholar?q=Escape%20Analysis。

1.3 逃逸分析后的处理

经过逃逸分析之后,可以得到三种对象的逃逸状态。
  1. GlobalEscape(全局逃逸), 即一个对象的引用逃出了方法或者线程。例如,一个对象的引用是复制给了一个类变量,或者存储在在一个已经逃逸的对象当中,或者这个对象的引用作为方法的返回值返回给了调用方法。
  2. ArgEscape(参数级逃逸),即在方法调用过程当中传递对象的应用给一个方法。这种状态可以通过分析被调方法的二进制代码确定。
  3. NoEscape(没有逃逸),一个可以进行标量替换的对象。可以不将这种对象分配在传统的堆上。
编译器可以使用逃逸分析的结果,对程序进行一下优化。
  1. 堆分配对象变成栈分配对象。一个方法当中的对象,对象的引用没有发生逃逸,那么这个方法可能会被分配在栈内存上而非常见的堆内存上。
  2. 消除同步。线程同步的代价是相当高的,同步的后果是降低并发性和性能。逃逸分析可以判断出某个对象是否始终只被一个线程访问,如果只被一个线程访问,那么对该对象的同步操作就可以转化成没有同步保护的操作,这样就能大大提高并发程度和性能。
  3. 矢量替代。逃逸分析方法如果发现对象的内存存储结构不需要连续进行的话,就可以将对象的部分甚至全部都保存在CPU寄存器内,这样能大大提高访问速度。
下面,我们看一下逃逸分析的例子。
[java]  view plain copy
  1. class Main {
  2. public static void main(String[] args) {
  3. example();
  4. }
  5. public static void example() {
  6. Foo foo = new Foo(); //alloc
  7. Bar bar = new Bar(); //alloc
  8. bar.setFoo(foo);
  9. }
  10. }
  11. class Foo {}
  12. class Bar {
  13. private Foo foo;
  14. public void setFoo(Foo foo) {
  15. this.foo = foo;
  16. }
  17. }

在这个例子当中,我们创建了两个对象,Foo对象和Bar对象,同时我们把Foo对象的应用赋值给了Bar对象的方法。此时,如果Bar对在堆上就会引起Foo对象的逃逸,但是,在本例当中,编译器通过逃逸分析,可以知道Bar对象没有逃出example()方法,因此这也意味着Foo也没有逃出example方法。因此,编译器可以将这两个对象分配到栈上。

1.4 编译器经过逃逸分析的效果

测试代码:
[java]  view plain copy
  1. package com.yang.test2;
  2. /**
  3. * Created by yangzl2008 on 2015/1/29.
  4. */
  5. class EscapeAnalysis {
  6. private static class Foo {
  7. private int x;
  8. private static int counter;
  9. public Foo() {
  10. x = (++counter);
  11. }
  12. }
  13. public static void main(String[] args) {
  14. long start = System.nanoTime();
  15. for (int i = 0; i < 1000 * 1000 * 10; ++i) {
  16. Foo foo = new Foo();
  17. }
  18. long end = System.nanoTime();
  19. System.out.println("Time cost is " + (end - start));
  20. }
  21. }

设置Idea JVM运行参数:

未开启逃逸分析设置为:
[plain]  view plain copy
  1. -server -verbose:gc

开启逃逸分析设置为:

[plain]  view plain copy
  1. -server -verbose:gc -XX:+DoEscapeAnalysis

在未开启逃逸分析的状况下运行情况如下:

[plain]  view plain copy
  1. [GC 5376K->427K(63872K), 0.0006051 secs]
  2. [GC 5803K->427K(63872K), 0.0003928 secs]
  3. [GC 5803K->427K(63872K), 0.0003639 secs]
  4. [GC 5803K->427K(69248K), 0.0003770 secs]
  5. [GC 11179K->427K(69248K), 0.0003987 secs]
  6. [GC 11179K->427K(79552K), 0.0003817 secs]
  7. [GC 21931K->399K(79552K), 0.0004342 secs]
  8. [GC 21903K->399K(101120K), 0.0002175 secs]
  9. [GC 43343K->399K(101184K), 0.0001421 secs]
  10. Time cost is 58514571

开启逃逸分析的状况下,运行情况如下:

[plain]  view plain copy
  1. Time cost is 10031306

未开启逃逸分析时,运行上诉代码,JVM执行了GC操作,而在开启逃逸分析情况下,JVM并没有执行GC操作。同时,操作时间上,开启逃逸分析的程序运行时间是未开启逃逸分析时间的1/5。

2. TLAB

JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。
也就是说,Java中每个线程都会有自己的缓冲区称作TLAB(Thread-local allocation buffer),每个TLAB都只有一个线程可以操作,TLAB结合bump-the-pointer技术可以实现快速的对象分配,而不需要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。
关于对象分配的JDK源码可以参见 JVM 之 Java对象创建[初始化]中对OpenJDK源码的分析。

3. Java对象分配的过程

  1. 编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配。如果是在堆上分配,则进入选项2.
  2. 如果tlab_top + size <= tlab_end,则在在TLAB上直接分配对象并增加tlab_top 的值,如果现有的TLAB不足以存放当前对象则3.
  3. 重新申请一个TLAB,并再次尝试存放当前对象。如果放不下,则4.
  4. 在Eden区加锁(这个区是多线程共享的),如果eden_top + size <= eden_end则将对象存放在Eden区,增加eden_top 的值,如果Eden区不足以存放,则5.
  5. 执行一次Young GC(minor collection)。
  6. 经过Young GC之后,如果Eden区任然不足以存放当前对象,则直接分配到老年代。
对象不在堆上分配主要的原因还是堆是共享的,在堆上分配有锁的开销。无论是TLAB还是栈都是线程私有的,私有即避免了竞争(当然也可能产生额外的问题例如可见性问题),这是典型的用空间换效率的做法。

TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。 
由于对象一般会分配在堆上,而堆是全局共享的。因此在同一时间,可能会有多个线程在堆上申请空间。因此,每次对象分配都必须要进行同步(虚拟机采用CAS配上失败重试的方式保证更新操作的原子性),而在竞争激烈的场合分配的效率又会进一步下降。JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。 
TLAB本身占用eEden区空间,在开启TLAB的情况下,虚拟机会为每个Java线程分配一块TLAB空间。参数-XX:+UseTLAB开启TLAB,默认是开启的。TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,当然可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。 
由于TLAB空间一般不会很大,因此大对象无法在TLAB上进行分配,总是会直接分配在堆上。TLAB空间由于比较小,因此很容易装满。比如,一个100K的空间,已经使用了80KB,当需要再分配一个30KB的对象时,肯定就无能为力了。这时虚拟机会有两种选择,第一,废弃当前TLAB,这样就会浪费20KB空间;第二,将这30KB的对象直接分配在堆上,保留当前的TLAB,这样可以希望将来有小于20KB的对象分配请求可以直接使用这块空间。实际上虚拟机内部会维护一个叫作refill_waste的值,当请求对象大于refill_waste时,会选择在堆中分配,若小于该值,则会废弃当前TLAB,新建TLAB来分配对象。这个阈值可以使用TLABRefillWasteFraction来调整,它表示TLAB中允许产生这种浪费的比例。默认值为64,即表示使用约为1/64的TLAB空间作为refill_waste。默认情况下,TLAB和refill_waste都会在运行时不断调整的,使系统的运行状态达到最优。如果想要禁用自动调整TLAB的大小,可以使用-XX:-ResizeTLAB禁用ResizeTLAB,并使用-XX:TLABSize手工指定一个TLAB的大小。 
-XX:+PrintTLAB可以跟踪TLAB的使用情况。一般不建议手工修改TLAB相关参数,推荐使用虚拟机默认行为。

对象内存分配的两种方法

为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

指针碰撞(Serial、ParNew等带Compact过程的收集器) 
假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。 
空闲列表(CMS这种基于Mark-Sweep算法的收集器) 
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。 
  

总结

总体流程 

对象分配流程 
 
如果开启栈上分配,JVM会先进行栈上分配,如果没有开启栈上分配或则不符合条件的则会进行TLAB分配,如果TLAB分配不成功,再尝试在eden区分配,如果对象满足了直接进入老年代的条件,那就直接分配在老年代。

对象在内存的引用方式 

对象在内存中的结构 

4. 参考

  1. 对象都是在堆上分配的吗?
  2. JVM 之 Java对象创建[初始化]
  3. In what cases is Java slower than C++ by a big margin?
  4. Size of Huge Objects directly allocated to Old Generation
  5. Escape analysis in Java
  6. Escape Analysis

JVM 栈分配与TLAB相关推荐

  1. JVM 栈分配、逃逸分析与TLAB

    最近在学习JVM,在看到这块地方,做一个简化的日志记录,部分内容抄录. 一般认为new出来的对象都是被分配在堆上,但是这个结论不是那么的绝对,通过对Java对象分配的过程分析,可以知道有两个地方会导致 ...

  2. JVM 的栈上分配、TLAB、PLAB 有啥区别?

    我们在学习 G1 回收器的时候,一般我们都会接触到 TLAB 和 PLAB 这两个术语.它们都是为了提高内存分配效率而存在的,但它们和栈上分配有什么区别呢?今天,就让树哥带着大家盘一盘. 栈上分配 稍 ...

  3. java tlab_java虚拟机中容易和JVM栈上分配混淆的TLAB上分配

    今天介绍一个容易跟JVM栈上分配混淆的 TLAB 上分配. 1. TLAB 上分配 Java 程序会极其频繁的创建对象并为对象分配内存空间,一般情况下对象是分配在堆上的,堆又是全局共享的,所以会存在这 ...

  4. java tlab_「原创」JVM系列05|TLAB上分配

    本文转载自[微信公众号:java进阶架构师,ID:java_jiagoushi]经微信公众号授权转载,如需转载与原文作者联系 本文是何适 JVM 修仙系列第 5 篇,文末有本系列文章汇总. 上一篇介绍 ...

  5. 关于栈上分配和TLAB的理解

    引言 我们知道,一般在java程序中,new的对象是分配在堆空间中的,但是实际的情况是,大部分的new对象会进入堆空间中,而并非是全部的对象,还有另外两个地方可以存储new的对象,我们称之为栈上分配以 ...

  6. Android性能调优篇之探索JVM内存分配

    开篇废话 今天我们一起来学习JVM的内存分配,主要目的是为我们Android内存优化打下基础. 一直在想以什么样的方式来呈现这个知识点才能让我们易于理解,最终决定使用方法为:图解+源代码分析. 欢迎访 ...

  7. jvm内存分区和TLAB

    JVM回顾 JVM = 类加载器(classloader) + 执行引擎(execution engine) + 运行时数据区域(runtime data area) 运行时数据区域 Java虚拟机在 ...

  8. JVM内存分配与垃圾回收

        其实已经有很多大牛在这方面做了很好的介绍,我在这篇文章里讲下我自己的一些理解,受限于我的认知水平,可能不一定正确,请自我甄别. JVM的GC自动垃圾回收器是JAVA的一大特色,垃圾回收器要解决 ...

  9. 一个可以参考的JVM内存分配

    下面是java命令有关JVM内存分配的参数 JAVA_MEM_OPTS="" BITS=`java -version 2>&1 | grep -i 64-bit` i ...

最新文章

  1. python读写压缩文件使用gzip和bz2
  2. Xcode 中设置部分文件ARC支持
  3. hdu_4391,线段树
  4. 内温的整体优先效应实验_陕西师范大学《普通心理学》第四章-知觉
  5. Cloud Programming Simplified: A Berkerley View on Serverless Computing笔记
  6. [shell]C语言调用shell脚本接口
  7. python分支结构说课_Python_3.8平台上的分支结构(模块.类.函数)_11
  8. vue-cli3 DllPlugin 提取公用库
  9. 一个文件版的名片管理系统(Python3)
  10. 2021 泰迪杯 A 题思路
  11. 进价移动加权核算体系
  12. luajit lua文件加密工具
  13. dijkastra算法实践poj2387
  14. 华为交换机5855设置ssh
  15. 关于域名备案后的注意事项,血淋淋的教训
  16. java运算符优先级
  17. 大数据基础之Hive(四)—— 常用函数和压缩存储
  18. HR最讨厌的几种求职者·
  19. 【STM32H7教程】第90章 STM32H7的CAN FD总线之关键知识点整理
  20. 软件测试app crash是什么意思,APP常见崩溃原因和测试方法整理

热门文章

  1. 6.PCIe协议分析3-PCIe TLP包详解2
  2. 汽车估损师跟二手车评估师的区别及鉴定方法
  3. AngularJS博友的笔记教程
  4. 使用JsonParser流式解析json,并使用DataFrame进行矩阵转置。
  5. uboot配置,编译,移植
  6. 【C语言】scanf语句里关于%c的问题
  7. php反转图片颜色,PHP 图片处理类(水印、透明度、缩放、相框、锐化、旋转、翻转、剪切、反色)...
  8. 对 VoIP 提供商的大规模 DDoS 攻击和模拟 DDoS 测试
  9. OpenCV 2.图像入门:读取、显示、保存
  10. 电脑怎么重装系统?超简单小白一键重装教程